@opencode/ai 0.0.0-dev-20120 → 0.0.0-dev-20123
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/experimental/evaluation.js +1 -1
- package/dist/generation.d.ts +1 -7
- package/dist/generation.js +1 -11
- package/dist/image.d.ts +2 -2
- package/dist/image.js +2 -2
- package/dist/llm.js +1 -1
- package/dist/media-model.d.ts +0 -2
- package/dist/media-model.js +0 -2
- package/dist/protocols/alibaba-messages.d.ts +1 -1
- package/dist/protocols/anthropic-messages.d.ts +34 -34
- package/dist/protocols/assemblyai-transcription.d.ts +2 -1
- package/dist/protocols/assemblyai-transcription.js +10 -14
- package/dist/protocols/bfl-images.js +15 -31
- package/dist/protocols/cartesia-speech.d.ts +2 -2
- package/dist/protocols/cartesia-speech.js +10 -16
- package/dist/protocols/deepgram-speech.d.ts +3 -3
- package/dist/protocols/deepgram-speech.js +7 -11
- package/dist/protocols/deepgram-transcription.d.ts +2 -1
- package/dist/protocols/deepgram-transcription.js +9 -13
- package/dist/protocols/elevenlabs-speech.d.ts +3 -3
- package/dist/protocols/elevenlabs-speech.js +9 -13
- package/dist/protocols/fal-images.d.ts +2 -1
- package/dist/protocols/fal-images.js +16 -29
- package/dist/protocols/fal-video.d.ts +2 -2
- package/dist/protocols/fal-video.js +9 -24
- package/dist/protocols/gemini.d.ts +13 -13
- package/dist/protocols/google-images.d.ts +3 -3
- package/dist/protocols/google-images.js +11 -21
- package/dist/protocols/google-speech.js +7 -13
- package/dist/protocols/google-transcription.d.ts +2 -1
- package/dist/protocols/google-transcription.js +8 -20
- package/dist/protocols/google-video.d.ts +2 -2
- package/dist/protocols/google-video.js +14 -30
- package/dist/protocols/meta-images.d.ts +3 -4
- package/dist/protocols/meta-images.js +12 -21
- package/dist/protocols/meta-messages.d.ts +5 -5
- package/dist/protocols/meta-responses.js +5 -2
- package/dist/protocols/mistral-chat.js +6 -3
- package/dist/protocols/openai-images.d.ts +4 -5
- package/dist/protocols/openai-images.js +24 -55
- package/dist/protocols/openai-responses.js +8 -2
- package/dist/protocols/openai-speech.js +7 -11
- package/dist/protocols/openai-transcription.js +21 -29
- package/dist/protocols/replicate-images.js +11 -17
- package/dist/protocols/runway-video.d.ts +3 -3
- package/dist/protocols/runway-video.js +10 -16
- package/dist/protocols/shared.d.ts +5 -17
- package/dist/protocols/shared.js +3 -39
- package/dist/protocols/stability-images.d.ts +2 -1
- package/dist/protocols/stability-images.js +24 -36
- package/dist/protocols/utils/fal-queue.d.ts +1 -3
- package/dist/protocols/utils/fal-queue.js +4 -6
- package/dist/protocols/utils/media-input.d.ts +15 -1
- package/dist/protocols/utils/media-input.js +22 -0
- package/dist/protocols/utils/speech-stream.d.ts +3 -3
- package/dist/protocols/utils/speech-stream.js +1 -4
- package/dist/protocols/utils/tool-schema.js +19 -6
- package/dist/protocols/xai-images.d.ts +4 -4
- package/dist/protocols/xai-images.js +12 -28
- package/dist/protocols/xai-video.js +12 -18
- package/dist/protocols/zai-images.d.ts +2 -2
- package/dist/protocols/zai-images.js +8 -11
- package/dist/protocols/zai-messages.d.ts +1 -1
- package/dist/providers/alibaba.d.ts +1 -1
- package/dist/providers/anthropic-compatible.d.ts +5 -5
- package/dist/providers/anthropic.d.ts +5 -5
- package/dist/providers/assemblyai.d.ts +1 -1
- package/dist/providers/assemblyai.js +5 -10
- package/dist/providers/azure.js +2 -2
- package/dist/providers/black-forest-labs.d.ts +1 -1
- package/dist/providers/black-forest-labs.js +5 -10
- package/dist/providers/cartesia.d.ts +1 -1
- package/dist/providers/cartesia.js +5 -10
- package/dist/providers/cloudflare-ai-gateway.d.ts +10 -10
- package/dist/providers/deepgram.d.ts +1 -1
- package/dist/providers/deepgram.js +6 -12
- package/dist/providers/elevenlabs.d.ts +1 -1
- package/dist/providers/elevenlabs.js +5 -10
- package/dist/providers/fal.d.ts +1 -1
- package/dist/providers/fal.js +5 -12
- package/dist/providers/google-vertex-messages.d.ts +5 -5
- package/dist/providers/google-vertex.d.ts +3 -3
- package/dist/providers/google.d.ts +3 -3
- package/dist/providers/google.js +7 -12
- package/dist/providers/meta.d.ts +8 -8
- package/dist/providers/meta.js +4 -8
- package/dist/providers/minimax.d.ts +5 -5
- package/dist/providers/moonshot.d.ts +5 -5
- package/dist/providers/openai.d.ts +7 -8
- package/dist/providers/openai.js +10 -11
- package/dist/providers/opencode-zen.js +1 -1
- package/dist/providers/openrouter.d.ts +6 -7
- package/dist/providers/openrouter.js +1 -1
- package/dist/providers/replicate.d.ts +1 -1
- package/dist/providers/replicate.js +5 -10
- package/dist/providers/runway.d.ts +1 -1
- package/dist/providers/runway.js +5 -10
- package/dist/providers/stability.d.ts +1 -1
- package/dist/providers/stability.js +6 -11
- package/dist/providers/typesafe-ai.js +1 -1
- package/dist/providers/vercel-ai-gateway.js +1 -1
- package/dist/providers/xai.js +5 -10
- package/dist/providers/zai-coding-plan.d.ts +1 -1
- package/dist/providers/zai.js +4 -8
- package/dist/route/client.d.ts +0 -1
- package/dist/route/client.js +1 -6
- package/dist/route/endpoint.d.ts +1 -0
- package/dist/route/endpoint.js +2 -2
- package/dist/route/framing.d.ts +10 -1
- package/dist/route/framing.js +45 -5
- package/dist/route/media-protocol.d.ts +42 -35
- package/dist/route/media-protocol.js +67 -62
- package/dist/route/media.d.ts +6 -2
- package/dist/route/media.js +40 -29
- package/dist/schema/options.d.ts +6 -3
- package/dist/schema/options.js +7 -3
- package/dist/speech.d.ts +2 -2
- package/dist/speech.js +2 -2
- package/dist/transcription.js +2 -2
- package/dist/utils/json.d.ts +4 -0
- package/dist/utils/json.js +4 -0
- package/dist/video.d.ts +2 -2
- package/dist/video.js +2 -2
- package/package.json +3 -3
- package/dist/protocols/utils/meta-image.d.ts +0 -2
- package/dist/protocols/utils/meta-image.js +0 -13
- package/dist/protocols/utils/openai-image.d.ts +0 -5
- package/dist/protocols/utils/openai-image.js +0 -18
|
@@ -6,7 +6,7 @@ import { OpenResponses } from "./open-responses.js";
|
|
|
6
6
|
import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared.js";
|
|
7
7
|
import { ResponsesHostedTools } from "./utils/responses-hosted-tools.js";
|
|
8
8
|
import { ToolSchemaProjection } from "./utils/tool-schema.js";
|
|
9
|
-
import {
|
|
9
|
+
import { detectMediaType } from "../utils/media-type.js";
|
|
10
10
|
const ADAPTER = "meta-responses";
|
|
11
11
|
const NAME = "Meta Responses";
|
|
12
12
|
export const WebSearch = Schema.Struct({
|
|
@@ -101,7 +101,10 @@ const HOSTED_TOOLS = {
|
|
|
101
101
|
if (!item.result)
|
|
102
102
|
return yield* ProviderShared.eventError(ADAPTER, "Meta returned an image without data", ProviderShared.encodeJson(raw));
|
|
103
103
|
const data = yield* Effect.fromResult(Encoding.decodeBase64(item.result)).pipe(Effect.mapError((cause) => ProviderShared.eventError(ADAPTER, "Meta returned invalid image base64", ProviderShared.encodeJson(raw), cause)));
|
|
104
|
-
|
|
104
|
+
// Responses image items can omit output_format, including when PNG/JPEG was requested.
|
|
105
|
+
const mime = item.output_format === undefined
|
|
106
|
+
? (detectMediaType(data) ?? "application/octet-stream")
|
|
107
|
+
: `image/${item.output_format}`;
|
|
105
108
|
return {
|
|
106
109
|
type: "content",
|
|
107
110
|
value: [{ type: "file", uri: `data:${mime};base64,${item.result}`, mime }],
|
|
@@ -10,6 +10,7 @@ import { classifyProviderFailure } from "../provider-error.js";
|
|
|
10
10
|
import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared.js";
|
|
11
11
|
import { Lifecycle } from "./utils/lifecycle.js";
|
|
12
12
|
import { MistralToolID } from "./utils/mistral-tool-id.js";
|
|
13
|
+
import { ToolSchemaProjection } from "./utils/tool-schema.js";
|
|
13
14
|
import { ToolStream } from "./utils/tool-stream.js";
|
|
14
15
|
const ADAPTER = "mistral-chat";
|
|
15
16
|
const DONE = "[DONE]";
|
|
@@ -272,9 +273,9 @@ const lowerMessages = Effect.fn("MistralChat.lowerMessages")(function* (request)
|
|
|
272
273
|
}
|
|
273
274
|
return messages;
|
|
274
275
|
});
|
|
275
|
-
const lowerTool = (tool) => ({
|
|
276
|
+
const lowerTool = (tool, inputSchema) => ({
|
|
276
277
|
type: "function",
|
|
277
|
-
function: { name: tool.name, description: tool.description, parameters:
|
|
278
|
+
function: { name: tool.name, description: tool.description, parameters: inputSchema, strict: false },
|
|
278
279
|
});
|
|
279
280
|
export const fromRequest = Effect.fn("MistralChat.fromRequest")(function* (request) {
|
|
280
281
|
const options = yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(MistralOptions))(request.providerOptions ?? {});
|
|
@@ -295,7 +296,9 @@ export const fromRequest = Effect.fn("MistralChat.fromRequest")(function* (reque
|
|
|
295
296
|
return {
|
|
296
297
|
model: request.model.id,
|
|
297
298
|
messages: yield* lowerMessages(flattened.request),
|
|
298
|
-
tools: flattened.tools.length > 0
|
|
299
|
+
tools: flattened.tools.length > 0
|
|
300
|
+
? flattened.tools.map((tool) => lowerTool(tool, ToolSchemaProjection.modelCompatibility(tool.inputSchema, request.model)))
|
|
301
|
+
: undefined,
|
|
299
302
|
tool_choice: toolChoice,
|
|
300
303
|
stream: true,
|
|
301
304
|
max_tokens: request.generation?.maxTokens,
|
|
@@ -2,16 +2,15 @@ import { ImageModel, type ImageRequestFor } from "../image.js";
|
|
|
2
2
|
import { Media } from "../media.js";
|
|
3
3
|
import { MediaProtocol } from "../route/media-protocol.js";
|
|
4
4
|
import { MediaRoute } from "../route/media.js";
|
|
5
|
-
import { type MediaUsage } from "../schema/index.js";
|
|
5
|
+
import { type MediaUsage, type OpenString } from "../schema/index.js";
|
|
6
6
|
export declare const DEFAULT_BASE_URL = "https://api.openai.com/v1";
|
|
7
7
|
export declare const PATH = "/images/generations";
|
|
8
8
|
export declare const EDIT_PATH = "/images/edits";
|
|
9
|
-
export type OpenAIImageString<Known extends string> = Known | (string & {});
|
|
10
9
|
/** Provider-native options. Common fields (`n`, `size`, `format`, `images`, `mask`) live on the request. */
|
|
11
10
|
export type OpenAIImageOptions = {
|
|
12
|
-
readonly quality?:
|
|
13
|
-
readonly background?:
|
|
14
|
-
readonly moderation?:
|
|
11
|
+
readonly quality?: OpenString<"auto" | "low" | "medium" | "high" | "standard" | "hd">;
|
|
12
|
+
readonly background?: OpenString<"auto" | "opaque" | "transparent">;
|
|
13
|
+
readonly moderation?: OpenString<"auto" | "low">;
|
|
15
14
|
readonly outputCompression?: number;
|
|
16
15
|
/** Previews sent before the final image when streaming (default 2); ignored by `Image.generate`. */
|
|
17
16
|
readonly partialImages?: number;
|
|
@@ -4,12 +4,10 @@ import { Media } from "../media.js";
|
|
|
4
4
|
import { Framing } from "../route/framing.js";
|
|
5
5
|
import { MediaProtocol } from "../route/media-protocol.js";
|
|
6
6
|
import { MediaRoute } from "../route/media.js";
|
|
7
|
-
import {
|
|
7
|
+
import { mergeJsonRecords } from "../schema/index.js";
|
|
8
8
|
import { ProviderShared } from "./shared.js";
|
|
9
9
|
import { MediaInput } from "./utils/media-input.js";
|
|
10
|
-
const
|
|
11
|
-
const NAME = "OpenAI Images";
|
|
12
|
-
const PROVIDER = ProviderID.make("openai");
|
|
10
|
+
const route = MediaProtocol.identity({ id: "openai-images", name: "OpenAI Images", provider: "openai" });
|
|
13
11
|
export const DEFAULT_BASE_URL = "https://api.openai.com/v1";
|
|
14
12
|
export const PATH = "/images/generations";
|
|
15
13
|
export const EDIT_PATH = "/images/edits";
|
|
@@ -49,7 +47,7 @@ const StreamEvent = Schema.Union([
|
|
|
49
47
|
usage: Schema.optional(Usage),
|
|
50
48
|
}),
|
|
51
49
|
]);
|
|
52
|
-
const decodeEvent =
|
|
50
|
+
const decodeEvent = route.decodeFrame(StreamEvent);
|
|
53
51
|
const decodeDocument = Schema.decodeUnknownEffect(Schema.fromJsonString(OpenAIImageResponse));
|
|
54
52
|
// ---------------------------------------------------------------------------
|
|
55
53
|
// 5. Request body construction
|
|
@@ -66,24 +64,14 @@ const streamOptions = (request) => {
|
|
|
66
64
|
if (request.mode !== "stream")
|
|
67
65
|
return Effect.succeed(undefined);
|
|
68
66
|
if (request.model.id.startsWith("dall-e"))
|
|
69
|
-
return Effect.fail(
|
|
70
|
-
operation: "media.stream",
|
|
71
|
-
provider: PROVIDER,
|
|
72
|
-
route: ADAPTER,
|
|
73
|
-
message: `${request.model.id} does not stream; use Image.generate or a GPT image model`,
|
|
74
|
-
}));
|
|
67
|
+
return Effect.fail(route.unsupported("media.stream", `${request.model.id} does not stream; use Image.generate or a GPT image model`));
|
|
75
68
|
if (request.n !== undefined && request.n > 1)
|
|
76
|
-
return Effect.fail(
|
|
77
|
-
operation: "media.n",
|
|
78
|
-
provider: PROVIDER,
|
|
79
|
-
route: ADAPTER,
|
|
80
|
-
message: `${NAME} streams one image; use Image.generate for n=${request.n}`,
|
|
81
|
-
}));
|
|
69
|
+
return Effect.fail(route.unsupported("media.n", `${route.name} streams one image; use Image.generate for n=${request.n}`));
|
|
82
70
|
return Effect.succeed({ stream: true, partial_images: request.providerOptions?.partialImages ?? 2 });
|
|
83
71
|
};
|
|
84
72
|
const isEdit = (request) => (request.images?.length ?? 0) > 0;
|
|
85
73
|
const isInline = (asset) => asset.source.type === "bytes" || asset.source.type === "base64";
|
|
86
|
-
const reference = (asset) => ProviderShared.mediaReference(asset,
|
|
74
|
+
const reference = (asset) => ProviderShared.mediaReference(asset, route.provider, route.name).pipe(Effect.map((item) => (item.type === "ref" ? { file_id: item.value } : { image_url: item.value })));
|
|
87
75
|
const fromRequest = Effect.fn("OpenAIImages.fromRequest")(function* (request) {
|
|
88
76
|
const images = request.images ?? [];
|
|
89
77
|
const mask = request.mask;
|
|
@@ -93,17 +81,11 @@ const fromRequest = Effect.fn("OpenAIImages.fromRequest")(function* (request) {
|
|
|
93
81
|
// Owned bytes go through multipart edits; remote URLs and file IDs use the JSON edits body instead.
|
|
94
82
|
if (images.length > 0 && images.every(isInline) && (mask === undefined || isInline(mask))) {
|
|
95
83
|
const form = new FormData();
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
Object.entries(fields ?? {}).forEach(([key, value]) => {
|
|
99
|
-
if (RESERVED_FORM_FIELDS.has(key))
|
|
100
|
-
return;
|
|
101
|
-
form.append(key, typeof value === "string" ? value : ProviderShared.encodeJson(value));
|
|
102
|
-
});
|
|
103
|
-
const uploads = yield* Effect.forEach(images, (image) => MediaInput.inlineBytes(ADAPTER, image));
|
|
84
|
+
MediaInput.appendFields(form, { model: request.model.id, prompt: request.prompt }, { overlay: fields, reserved: RESERVED_FORM_FIELDS });
|
|
85
|
+
const uploads = yield* Effect.forEach(images, (image) => MediaInput.inlineBytes(route.id, image));
|
|
104
86
|
uploads.forEach((data, index) => form.append("image[]", MediaInput.blob(data, images[index].mediaType), `image-${index}`));
|
|
105
87
|
if (mask !== undefined)
|
|
106
|
-
form.append("mask", MediaInput.blob(yield* MediaInput.inlineBytes(
|
|
88
|
+
form.append("mask", MediaInput.blob(yield* MediaInput.inlineBytes(route.id, mask), mask.mediaType), "mask");
|
|
107
89
|
return MediaProtocol.multipart(form);
|
|
108
90
|
}
|
|
109
91
|
const references = yield* Effect.forEach(images, reference);
|
|
@@ -133,45 +115,40 @@ const usage = (value) => value === undefined
|
|
|
133
115
|
total: value.total_tokens,
|
|
134
116
|
details: { openai: value },
|
|
135
117
|
};
|
|
136
|
-
const eventImage = (frame, label, data, format) => MediaInput.decodedAsset((message, cause) =>
|
|
118
|
+
const eventImage = (frame, label, data, format) => MediaInput.decodedAsset((message, cause) => route.frameError(message, frame, cause), label, data, `image/${format}`, {
|
|
119
|
+
info: { format },
|
|
120
|
+
});
|
|
137
121
|
const onEvent = Effect.fn("OpenAIImages.onEvent")(function* (state, frame) {
|
|
138
122
|
const event = yield* decodeEvent(frame);
|
|
139
123
|
const format = event.output_format;
|
|
140
124
|
if ("partial_image_index" in event) {
|
|
141
|
-
const image = yield* eventImage(frame, `${
|
|
125
|
+
const image = yield* eventImage(frame, `${route.name} partial image`, event.b64_json, format);
|
|
142
126
|
return [state, [ImagePartialEvent.make({ index: event.partial_image_index, image })]];
|
|
143
127
|
}
|
|
144
|
-
const image = yield* eventImage(frame, `${
|
|
128
|
+
const image = yield* eventImage(frame, `${route.name} result ${state.completed}`, event.b64_json, format);
|
|
145
129
|
return [
|
|
146
130
|
{ ...state, completed: state.completed + 1, format, usage: usage(event.usage) },
|
|
147
131
|
[ImageOutputEvent.make({ index: state.completed, image })],
|
|
148
132
|
];
|
|
149
133
|
});
|
|
150
134
|
const onDocument = Effect.fn("OpenAIImages.onDocument")(function* (frame) {
|
|
151
|
-
const invalid = (message, cause) =>
|
|
152
|
-
const decoded = yield* decodeDocument(frame.document).pipe(Effect.mapError((cause) => invalid(`${
|
|
135
|
+
const invalid = (message, cause) => route.frameError(message, frame.document, cause);
|
|
136
|
+
const decoded = yield* decodeDocument(frame.document).pipe(Effect.mapError((cause) => invalid(`${route.name} returned an invalid response`, cause)));
|
|
153
137
|
const format = decoded.output_format ?? frame.requested ?? "png";
|
|
154
138
|
const mediaType = `image/${format}`;
|
|
155
|
-
const images = yield* Effect.forEach(decoded.data, (item, index) => {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
info: { format },
|
|
160
|
-
providerMetadata,
|
|
161
|
-
});
|
|
162
|
-
if (item.url)
|
|
163
|
-
return Effect.succeed(Media.url(item.url, { mediaType, info: { format }, providerMetadata }));
|
|
164
|
-
return Effect.fail(invalid(`${NAME} result ${index} has neither image data nor a URL`));
|
|
165
|
-
});
|
|
139
|
+
const images = yield* Effect.forEach(decoded.data, (item, index) => MediaInput.imageOutput(invalid, `${route.name} result ${index}`, item, mediaType, {
|
|
140
|
+
info: { format },
|
|
141
|
+
providerMetadata: item.revised_prompt === undefined ? undefined : { openai: { revisedPrompt: item.revised_prompt } },
|
|
142
|
+
}));
|
|
166
143
|
if (images.length === 0)
|
|
167
|
-
return yield* invalid(`${
|
|
144
|
+
return yield* invalid(`${route.name} returned no images`);
|
|
168
145
|
const state = { completed: images.length, format, usage: usage(decoded.usage) };
|
|
169
146
|
return [state, images.map((image, index) => ImageOutputEvent.make({ index, image }))];
|
|
170
147
|
});
|
|
171
148
|
const step = (state, frame) => (typeof frame === "string" ? onEvent(state, frame) : onDocument(frame));
|
|
172
149
|
const finish = (state) => {
|
|
173
150
|
if (state.completed === 0)
|
|
174
|
-
return Effect.fail(
|
|
151
|
+
return Effect.fail(route.incomplete());
|
|
175
152
|
return Effect.succeed([
|
|
176
153
|
ImageFinishEvent.make({ usage: state.usage, providerMetadata: { openai: { outputFormat: state.format } } }),
|
|
177
154
|
]);
|
|
@@ -179,9 +156,7 @@ const finish = (state) => {
|
|
|
179
156
|
// ---------------------------------------------------------------------------
|
|
180
157
|
// 7. Protocol and route
|
|
181
158
|
// ---------------------------------------------------------------------------
|
|
182
|
-
export const protocol = MediaProtocol.stream({
|
|
183
|
-
id: ADAPTER,
|
|
184
|
-
name: NAME,
|
|
159
|
+
export const protocol = MediaProtocol.stream(route, {
|
|
185
160
|
unsupported: ["aspectRatio", "seed"],
|
|
186
161
|
body: { from: fromRequest },
|
|
187
162
|
frames: (bytes, context) => context.request.mode === "stream"
|
|
@@ -193,13 +168,7 @@ export const protocol = MediaProtocol.stream({
|
|
|
193
168
|
step,
|
|
194
169
|
finish,
|
|
195
170
|
});
|
|
196
|
-
export const model = (input) => ImageModel.fromRoute({
|
|
197
|
-
id: ADAPTER,
|
|
198
|
-
provider: PROVIDER,
|
|
199
|
-
protocol,
|
|
200
|
-
baseURL: DEFAULT_BASE_URL,
|
|
201
|
-
path: ({ request }) => (isEdit(request) ? EDIT_PATH : PATH),
|
|
202
|
-
}, input);
|
|
171
|
+
export const model = (input) => ImageModel.fromRoute({ protocol, baseURL: DEFAULT_BASE_URL, path: ({ request }) => (isEdit(request) ? EDIT_PATH : PATH) }, input);
|
|
203
172
|
export const OpenAIImages = {
|
|
204
173
|
protocol,
|
|
205
174
|
model,
|
|
@@ -10,7 +10,6 @@ import { resolveEffortUpdates } from "../effort-updates.js";
|
|
|
10
10
|
import { OpenResponses } from "./open-responses.js";
|
|
11
11
|
import { OpenResponsesOptions } from "./utils/open-responses-options.js";
|
|
12
12
|
import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared.js";
|
|
13
|
-
import { OpenAIImage } from "./utils/openai-image.js";
|
|
14
13
|
import { ResponsesHostedTools } from "./utils/responses-hosted-tools.js";
|
|
15
14
|
import { ToolSchemaProjection } from "./utils/tool-schema.js";
|
|
16
15
|
import { OpenResponsesChannel } from "./open-responses-channel.js";
|
|
@@ -35,7 +34,14 @@ const OpenAIResponsesImageGenerationTool = Schema.Struct({
|
|
|
35
34
|
output_format: Schema.optional(Schema.Literals(["png", "jpeg", "webp"])),
|
|
36
35
|
partial_images: Schema.optional(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))),
|
|
37
36
|
quality: Schema.optional(Schema.Literals(["auto", "low", "medium", "high"])),
|
|
38
|
-
size: Schema.optional(
|
|
37
|
+
size: Schema.optional(Schema.String.check(Schema.makeFilter((value) => {
|
|
38
|
+
if (value === "auto")
|
|
39
|
+
return undefined;
|
|
40
|
+
const match = /^(\d+)x(\d+)$/.exec(value);
|
|
41
|
+
if (!match)
|
|
42
|
+
return "image size must be `auto` or `{width}x{height}`";
|
|
43
|
+
return Number(match[1]) > 0 && Number(match[2]) > 0 ? undefined : "image dimensions must be positive integers";
|
|
44
|
+
}))),
|
|
39
45
|
});
|
|
40
46
|
const OpenAIResponsesHostedToolItem = Schema.Union([
|
|
41
47
|
Schema.StructWithRest(Schema.Struct({
|
|
@@ -2,12 +2,10 @@ import { Effect, Schema } from "effect";
|
|
|
2
2
|
import { Framing } from "../route/framing.js";
|
|
3
3
|
import { MediaProtocol } from "../route/media-protocol.js";
|
|
4
4
|
import { MediaRoute } from "../route/media.js";
|
|
5
|
-
import {
|
|
5
|
+
import { mergeJsonRecords } from "../schema/index.js";
|
|
6
6
|
import { SpeechModel } from "../speech.js";
|
|
7
7
|
import { SpeechStream } from "./utils/speech-stream.js";
|
|
8
|
-
const
|
|
9
|
-
const NAME = "OpenAI Speech";
|
|
10
|
-
const PROVIDER = ProviderID.make("openai");
|
|
8
|
+
const route = MediaProtocol.identity({ id: "openai-speech", name: "OpenAI Speech", provider: "openai" });
|
|
11
9
|
export const DEFAULT_BASE_URL = "https://api.openai.com/v1";
|
|
12
10
|
export const PATH = "/audio/speech";
|
|
13
11
|
/** `pcm` is raw 24 kHz, 16-bit signed little-endian mono samples without a header. */
|
|
@@ -26,7 +24,7 @@ const SpeechStreamEvent = Schema.Union([
|
|
|
26
24
|
})),
|
|
27
25
|
}),
|
|
28
26
|
]);
|
|
29
|
-
const decodeEvent =
|
|
27
|
+
const decodeEvent = route.decodeFrame(SpeechStreamEvent);
|
|
30
28
|
// ---------------------------------------------------------------------------
|
|
31
29
|
// 5. Request body construction
|
|
32
30
|
// ---------------------------------------------------------------------------
|
|
@@ -71,9 +69,9 @@ const onEvent = Effect.fn("OpenAISpeech.onEvent")(function* (state, frame) {
|
|
|
71
69
|
});
|
|
72
70
|
const finish = (state, context) => {
|
|
73
71
|
if (isSse(context.body) && !state.done)
|
|
74
|
-
return Effect.fail(
|
|
72
|
+
return Effect.fail(route.incomplete());
|
|
75
73
|
const format = context.request.format ?? "mp3";
|
|
76
|
-
return SpeechStream.finish(
|
|
74
|
+
return SpeechStream.finish(route, state, {
|
|
77
75
|
...(format === "pcm" ? SpeechStream.pcm("pcm_s16le", PCM_SAMPLE_RATE) : SpeechStream.container(format)),
|
|
78
76
|
usage: state.usage,
|
|
79
77
|
});
|
|
@@ -81,9 +79,7 @@ const finish = (state, context) => {
|
|
|
81
79
|
// ---------------------------------------------------------------------------
|
|
82
80
|
// 7. Protocol and route
|
|
83
81
|
// ---------------------------------------------------------------------------
|
|
84
|
-
export const protocol = MediaProtocol.stream({
|
|
85
|
-
id: ADAPTER,
|
|
86
|
-
name: NAME,
|
|
82
|
+
export const protocol = MediaProtocol.stream(route, {
|
|
87
83
|
unsupported: ["language", "timestamps"],
|
|
88
84
|
body: { from: fromRequest },
|
|
89
85
|
frames: (bytes, context) => (isSse(context.body) ? Framing.sse.frame(bytes) : bytes),
|
|
@@ -91,7 +87,7 @@ export const protocol = MediaProtocol.stream({
|
|
|
91
87
|
step: SpeechStream.step(onEvent),
|
|
92
88
|
finish,
|
|
93
89
|
});
|
|
94
|
-
export const model = (input) => SpeechModel.fromRoute({
|
|
90
|
+
export const model = (input) => SpeechModel.fromRoute({ protocol, baseURL: DEFAULT_BASE_URL, path: PATH }, input);
|
|
95
91
|
export const OpenAISpeech = {
|
|
96
92
|
protocol,
|
|
97
93
|
model,
|
|
@@ -2,14 +2,12 @@ import { Effect, Schema, Stream } from "effect";
|
|
|
2
2
|
import { Framing } from "../route/framing.js";
|
|
3
3
|
import { MediaProtocol } from "../route/media-protocol.js";
|
|
4
4
|
import { MediaRoute } from "../route/media.js";
|
|
5
|
-
import {
|
|
5
|
+
import { mergeJsonRecords } from "../schema/index.js";
|
|
6
6
|
import { TranscriptionFinishEvent, TranscriptionModel, TranscriptionSegmentEvent, TranscriptionTextDeltaEvent, } from "../transcription.js";
|
|
7
7
|
import { mediaTypeExtension } from "../utils/media-type.js";
|
|
8
8
|
import { ProviderShared } from "./shared.js";
|
|
9
9
|
import { MediaInput } from "./utils/media-input.js";
|
|
10
|
-
const
|
|
11
|
-
const NAME = "OpenAI Transcription";
|
|
12
|
-
const PROVIDER = ProviderID.make("openai");
|
|
10
|
+
const route = MediaProtocol.identity({ id: "openai-transcription", name: "OpenAI Transcription", provider: "openai" });
|
|
13
11
|
export const DEFAULT_BASE_URL = "https://api.openai.com/v1";
|
|
14
12
|
export const PATH = "/audio/transcriptions";
|
|
15
13
|
// ---------------------------------------------------------------------------
|
|
@@ -45,8 +43,8 @@ const Event = Schema.Union([
|
|
|
45
43
|
Schema.Struct({ type: Schema.Literal("transcript.text.done"), ...transcriptFields }),
|
|
46
44
|
]);
|
|
47
45
|
const Transcript = Schema.Struct(transcriptFields);
|
|
48
|
-
const decodeEvent =
|
|
49
|
-
const decodeTranscript =
|
|
46
|
+
const decodeEvent = route.decodeFrame(Event);
|
|
47
|
+
const decodeTranscript = route.decodeFrame(Transcript);
|
|
50
48
|
const TRANSCRIBE = { stream: true, timestamps: [], diarize: false, languageField: "language" };
|
|
51
49
|
const capabilities = (model) => {
|
|
52
50
|
if (model.startsWith("whisper"))
|
|
@@ -58,20 +56,19 @@ const capabilities = (model) => {
|
|
|
58
56
|
return { ...TRANSCRIBE, languageField: "languages" };
|
|
59
57
|
return TRANSCRIBE;
|
|
60
58
|
};
|
|
61
|
-
const unsupported = (operation, message) => Effect.fail(ProviderShared.unsupportedOperation({ operation, provider: PROVIDER, route: ADAPTER, message }));
|
|
62
59
|
const validate = (request, model) => {
|
|
63
60
|
const id = request.model.id;
|
|
64
61
|
if (request.mode === "stream" && !model.stream)
|
|
65
|
-
return unsupported("media.stream", `${id} does not stream; use Transcription.generate`);
|
|
62
|
+
return Effect.fail(route.unsupported("media.stream", `${id} does not stream; use Transcription.generate`));
|
|
66
63
|
if (request.diarize === true && !model.diarize)
|
|
67
|
-
return unsupported("media.diarize", `${id} does not diarize; use gpt-4o-transcribe-diarize`);
|
|
64
|
+
return Effect.fail(route.unsupported("media.diarize", `${id} does not diarize; use gpt-4o-transcribe-diarize`));
|
|
68
65
|
if (request.prompt !== undefined && model.diarize)
|
|
69
|
-
return unsupported("media.prompt", `${id} does not accept a prompt`);
|
|
66
|
+
return Effect.fail(route.unsupported("media.prompt", `${id} does not accept a prompt`));
|
|
70
67
|
if (request.timestamps === undefined ||
|
|
71
68
|
request.timestamps === "none" ||
|
|
72
69
|
model.timestamps.includes(request.timestamps))
|
|
73
70
|
return Effect.void;
|
|
74
|
-
return unsupported("media.timestamps", `${id} does not return ${request.timestamps} timestamps`);
|
|
71
|
+
return Effect.fail(route.unsupported("media.timestamps", `${id} does not return ${request.timestamps} timestamps`));
|
|
75
72
|
};
|
|
76
73
|
const RESERVED_FORM_FIELDS = new Set([
|
|
77
74
|
"file",
|
|
@@ -82,26 +79,22 @@ const RESERVED_FORM_FIELDS = new Set([
|
|
|
82
79
|
"timestamp_granularities",
|
|
83
80
|
"stream",
|
|
84
81
|
]);
|
|
85
|
-
const appendField = (form, key, value) => {
|
|
86
|
-
if (Array.isArray(value))
|
|
87
|
-
return value.forEach((item) => form.append(`${key}[]`, String(item)));
|
|
88
|
-
form.append(key, typeof value === "object" && value !== null ? ProviderShared.encodeJson(value) : String(value));
|
|
89
|
-
};
|
|
90
82
|
const fromRequest = Effect.fn("OpenAITranscription.fromRequest")(function* (request) {
|
|
91
83
|
const model = capabilities(request.model.id);
|
|
92
84
|
yield* validate(request, model);
|
|
93
85
|
// The API detects the audio format from the upload's filename extension.
|
|
94
86
|
const extension = mediaTypeExtension(request.audio.mediaType);
|
|
95
87
|
if (extension === undefined)
|
|
96
|
-
return yield* ProviderShared.invalidRequest(`${
|
|
97
|
-
const audio = yield* MediaInput.inlineBytes(
|
|
88
|
+
return yield* ProviderShared.invalidRequest(`${route.name} cannot name a ${request.audio.mediaType} upload; send mp3, mp4, m4a, wav, webm, ogg, or flac audio`);
|
|
89
|
+
const audio = yield* MediaInput.inlineBytes(route.id, request.audio);
|
|
98
90
|
const responseFormat = model.diarize
|
|
99
91
|
? "diarized_json"
|
|
100
92
|
: request.timestamps === undefined || request.timestamps === "none"
|
|
101
93
|
? undefined
|
|
102
94
|
: "verbose_json";
|
|
103
|
-
const
|
|
104
|
-
|
|
95
|
+
const form = new FormData();
|
|
96
|
+
form.append("file", MediaInput.blob(audio, request.audio.mediaType), `audio.${extension}`);
|
|
97
|
+
MediaInput.appendFields(form, {
|
|
105
98
|
model: request.model.id,
|
|
106
99
|
language: model.languageField === "language" ? request.language : undefined,
|
|
107
100
|
languages: model.languageField === "languages" && request.language !== undefined ? [request.language] : undefined,
|
|
@@ -111,10 +104,11 @@ const fromRequest = Effect.fn("OpenAITranscription.fromRequest")(function* (requ
|
|
|
111
104
|
// Diarizing audio longer than 30 seconds requires a chunking strategy.
|
|
112
105
|
chunking_strategy: model.diarize ? "auto" : undefined,
|
|
113
106
|
stream: request.mode === "stream" ? true : undefined,
|
|
114
|
-
},
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
107
|
+
}, {
|
|
108
|
+
overlay: mergeJsonRecords(request.providerOptions, request.http?.body),
|
|
109
|
+
reserved: RESERVED_FORM_FIELDS,
|
|
110
|
+
repeatArrays: true,
|
|
111
|
+
});
|
|
118
112
|
return MediaProtocol.multipart(form);
|
|
119
113
|
});
|
|
120
114
|
// ---------------------------------------------------------------------------
|
|
@@ -155,7 +149,7 @@ const usage = (value) => {
|
|
|
155
149
|
const finish = (state) => {
|
|
156
150
|
const transcript = state.transcript;
|
|
157
151
|
if (transcript === undefined)
|
|
158
|
-
return Effect.fail(
|
|
152
|
+
return Effect.fail(route.incomplete());
|
|
159
153
|
const segments = transcript.segments?.map(segment) ?? state.segments;
|
|
160
154
|
return Effect.succeed([
|
|
161
155
|
TranscriptionFinishEvent.make({
|
|
@@ -171,9 +165,7 @@ const finish = (state) => {
|
|
|
171
165
|
// ---------------------------------------------------------------------------
|
|
172
166
|
// 7. Protocol and route
|
|
173
167
|
// ---------------------------------------------------------------------------
|
|
174
|
-
export const protocol = MediaProtocol.stream({
|
|
175
|
-
id: ADAPTER,
|
|
176
|
-
name: NAME,
|
|
168
|
+
export const protocol = MediaProtocol.stream(route, {
|
|
177
169
|
unsupported: ["speakers"],
|
|
178
170
|
body: { from: fromRequest },
|
|
179
171
|
frames: (bytes, context) => context.request.mode === "stream"
|
|
@@ -183,7 +175,7 @@ export const protocol = MediaProtocol.stream({
|
|
|
183
175
|
step,
|
|
184
176
|
finish,
|
|
185
177
|
});
|
|
186
|
-
export const model = (input) => TranscriptionModel.fromRoute({
|
|
178
|
+
export const model = (input) => TranscriptionModel.fromRoute({ protocol, baseURL: DEFAULT_BASE_URL, path: PATH }, input);
|
|
187
179
|
export const OpenAITranscription = {
|
|
188
180
|
protocol,
|
|
189
181
|
model,
|
|
@@ -3,11 +3,9 @@ import { ImageModel, ImageResponse } from "../image.js";
|
|
|
3
3
|
import { Media } from "../media.js";
|
|
4
4
|
import { MediaProtocol } from "../route/media-protocol.js";
|
|
5
5
|
import { MediaRoute } from "../route/media.js";
|
|
6
|
-
import {
|
|
6
|
+
import { mergeJsonRecords } from "../schema/index.js";
|
|
7
7
|
import { ProviderShared, optionalNull } from "./shared.js";
|
|
8
|
-
const
|
|
9
|
-
const NAME = "Replicate";
|
|
10
|
-
const PROVIDER = ProviderID.make("replicate");
|
|
8
|
+
const route = MediaProtocol.identity({ id: "replicate-images", name: "Replicate", provider: "replicate" });
|
|
11
9
|
export const DEFAULT_BASE_URL = "https://api.replicate.com";
|
|
12
10
|
const OUTPUT_RETENTION = Duration.hours(1);
|
|
13
11
|
const MAX_DATA_URL_BYTES = 256 * 1024;
|
|
@@ -50,8 +48,8 @@ const inlineSize = (source) => {
|
|
|
50
48
|
};
|
|
51
49
|
const fileInput = (asset) => {
|
|
52
50
|
if (inlineSize(asset.source) > MAX_DATA_URL_BYTES)
|
|
53
|
-
return Effect.fail(ProviderShared.invalidRequest(`${
|
|
54
|
-
return ProviderShared.mediaReference(asset, undefined,
|
|
51
|
+
return Effect.fail(ProviderShared.invalidRequest(`${route.name} data URL inputs are limited to 256 KB; pass a larger file by https URL`));
|
|
52
|
+
return ProviderShared.mediaReference(asset, undefined, route.name).pipe(Effect.map((reference) => reference.value));
|
|
55
53
|
};
|
|
56
54
|
const inputValue = (value) => {
|
|
57
55
|
if (value instanceof Media.Asset)
|
|
@@ -69,7 +67,7 @@ const fromRequest = Effect.fn("ReplicateImages.fromRequest")(function* (request)
|
|
|
69
67
|
// ---------------------------------------------------------------------------
|
|
70
68
|
// 6. Response decoding
|
|
71
69
|
// ---------------------------------------------------------------------------
|
|
72
|
-
const decodePrediction =
|
|
70
|
+
const decodePrediction = route.decodeJson(Prediction);
|
|
73
71
|
const decodeStart = Effect.fn("ReplicateImages.decodeStart")(function* (response) {
|
|
74
72
|
const output = yield* decodePrediction(response);
|
|
75
73
|
const prediction = output.value;
|
|
@@ -88,16 +86,16 @@ const decodeResult = Effect.fn("ReplicateImages.decodeResult")(function* (respon
|
|
|
88
86
|
const prediction = output.value;
|
|
89
87
|
const status = yield* MediaProtocol.status(STATUS, prediction.status, output);
|
|
90
88
|
if (status === "failed" || status === "cancelled" || status === "expired")
|
|
91
|
-
return yield* output.ended(status, `${
|
|
89
|
+
return yield* output.ended(status, `${route.name} prediction ${context.token.id} ${prediction.status}${typeof prediction.error === "string" ? `: ${prediction.error}` : ""}`);
|
|
92
90
|
if (status !== "completed")
|
|
93
|
-
return yield* output.invalid(`${
|
|
91
|
+
return yield* output.invalid(`${route.name} prediction ${context.token.id} has not finished`);
|
|
94
92
|
if (prediction.data_removed === true)
|
|
95
|
-
return yield* output.ended("expired", `${
|
|
93
|
+
return yield* output.ended("expired", `${route.name} removed the output of prediction ${context.token.id}`);
|
|
96
94
|
if (!isOutput(prediction.output))
|
|
97
|
-
return yield* output.invalid(`${
|
|
95
|
+
return yield* output.invalid(`${route.name} prediction ${context.token.id} returned output that is not image URLs`);
|
|
98
96
|
const urls = typeof prediction.output === "string" ? [prediction.output] : prediction.output;
|
|
99
97
|
if (urls.length === 0)
|
|
100
|
-
return yield* output.invalid(`${
|
|
98
|
+
return yield* output.invalid(`${route.name} prediction ${context.token.id} returned no images`);
|
|
101
99
|
const predictTime = prediction.metrics?.predict_time ?? undefined;
|
|
102
100
|
const completedAt = prediction.completed_at ?? undefined;
|
|
103
101
|
const expiresAt = completedAt === undefined ? undefined : Date.parse(completedAt) + Duration.toMillis(OUTPUT_RETENTION);
|
|
@@ -110,9 +108,7 @@ const decodeResult = Effect.fn("ReplicateImages.decodeResult")(function* (respon
|
|
|
110
108
|
// ---------------------------------------------------------------------------
|
|
111
109
|
// 7. Protocol and route
|
|
112
110
|
// ---------------------------------------------------------------------------
|
|
113
|
-
export const protocol = MediaProtocol.queued({
|
|
114
|
-
id: ADAPTER,
|
|
115
|
-
name: NAME,
|
|
111
|
+
export const protocol = MediaProtocol.queued(route, {
|
|
116
112
|
token: Token,
|
|
117
113
|
unsupported: ["images", "mask", "n", "size", "aspectRatio", "seed", "format"],
|
|
118
114
|
start: { body: { from: fromRequest }, decode: decodeStart },
|
|
@@ -121,8 +117,6 @@ export const protocol = MediaProtocol.queued({
|
|
|
121
117
|
cancel: { method: "POST", path: (token) => token.cancelURL },
|
|
122
118
|
});
|
|
123
119
|
export const model = (input) => ImageModel.fromRoute({
|
|
124
|
-
id: ADAPTER,
|
|
125
|
-
provider: PROVIDER,
|
|
126
120
|
protocol,
|
|
127
121
|
baseURL: DEFAULT_BASE_URL,
|
|
128
122
|
path: ({ request }) => isOfficial(request.model.id) ? `/v1/models/${request.model.id}/predictions` : "/v1/predictions",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Schema } from "effect";
|
|
2
2
|
import { MediaProtocol } from "../route/media-protocol.js";
|
|
3
3
|
import { MediaRoute } from "../route/media.js";
|
|
4
|
+
import { type OpenString } from "../schema/index.js";
|
|
4
5
|
import { VideoModel, VideoResponse, type VideoRequestFor } from "../video.js";
|
|
5
6
|
export declare const DEFAULT_BASE_URL = "https://api.dev.runwayml.com/v1";
|
|
6
7
|
/** Every Runway request must pin the API version. */
|
|
@@ -9,7 +10,6 @@ export declare const TEXT_TO_VIDEO_PATH = "/text_to_video";
|
|
|
9
10
|
export declare const IMAGE_TO_VIDEO_PATH = "/image_to_video";
|
|
10
11
|
export declare const VIDEO_TO_VIDEO_PATH = "/video_to_video";
|
|
11
12
|
export declare const TASKS_PATH = "/tasks";
|
|
12
|
-
export type RunwayVideoString<Known extends string> = Known | (string & {});
|
|
13
13
|
/**
|
|
14
14
|
* Provider-native options. Common fields lower to Runway's names: `aspectRatio` → `ratio` (Runway expects pixel
|
|
15
15
|
* ratios such as `1280:720` for most models), `durationSeconds` → `duration`, `audio`, `negativePrompt`,
|
|
@@ -17,9 +17,9 @@ export type RunwayVideoString<Known extends string> = Known | (string & {});
|
|
|
17
17
|
*/
|
|
18
18
|
export type RunwayVideoOptions = {
|
|
19
19
|
readonly contentModeration?: {
|
|
20
|
-
readonly publicFigureThreshold?:
|
|
20
|
+
readonly publicFigureThreshold?: OpenString<"auto" | "low">;
|
|
21
21
|
};
|
|
22
|
-
readonly outputFormat?:
|
|
22
|
+
readonly outputFormat?: OpenString<"mp4" | "prores" | "png_sequence">;
|
|
23
23
|
} & Record<string, unknown>;
|
|
24
24
|
export type Request = VideoRequestFor<RunwayVideoOptions>;
|
|
25
25
|
export declare const Token: Schema.Struct<{
|
|
@@ -2,12 +2,10 @@ import { Duration, Effect, Schema } from "effect";
|
|
|
2
2
|
import { Media } from "../media.js";
|
|
3
3
|
import { MediaProtocol } from "../route/media-protocol.js";
|
|
4
4
|
import { MediaRoute } from "../route/media.js";
|
|
5
|
-
import {
|
|
5
|
+
import { mergeJsonRecords } from "../schema/index.js";
|
|
6
6
|
import { VideoModel, VideoResponse } from "../video.js";
|
|
7
7
|
import { ProviderShared, optionalArray, optionalNull } from "./shared.js";
|
|
8
|
-
const
|
|
9
|
-
const NAME = "Runway";
|
|
10
|
-
const PROVIDER = ProviderID.make("runway");
|
|
8
|
+
const route = MediaProtocol.identity({ id: "runway-video", name: "Runway", provider: "runway" });
|
|
11
9
|
export const DEFAULT_BASE_URL = "https://api.dev.runwayml.com/v1";
|
|
12
10
|
/** Every Runway request must pin the API version. */
|
|
13
11
|
export const API_VERSION = "2024-11-06";
|
|
@@ -44,7 +42,7 @@ const STATUS = {
|
|
|
44
42
|
// 5. Request body construction
|
|
45
43
|
// ---------------------------------------------------------------------------
|
|
46
44
|
// Runway accepts HTTPS URLs, `runway://` upload URIs, and data URIs, all as one string.
|
|
47
|
-
const mediaUri = (asset) => ProviderShared.mediaReference(asset,
|
|
45
|
+
const mediaUri = (asset) => ProviderShared.mediaReference(asset, route.provider, route.name).pipe(Effect.map((reference) => reference.value));
|
|
48
46
|
const fromRequest = Effect.fn("RunwayVideo.fromRequest")(function* (request) {
|
|
49
47
|
const first = request.frames?.first === undefined ? undefined : yield* mediaUri(request.frames.first);
|
|
50
48
|
const last = request.frames?.last === undefined ? undefined : yield* mediaUri(request.frames.last);
|
|
@@ -71,11 +69,11 @@ const fromRequest = Effect.fn("RunwayVideo.fromRequest")(function* (request) {
|
|
|
71
69
|
// ---------------------------------------------------------------------------
|
|
72
70
|
// 6. Response decoding
|
|
73
71
|
// ---------------------------------------------------------------------------
|
|
74
|
-
const decodeStart =
|
|
72
|
+
const decodeStart = route.decodeStarted(StartResponse, (value) => ({
|
|
75
73
|
token: { taskID: value.id },
|
|
76
74
|
snapshot: { id: value.id, status: "queued" },
|
|
77
75
|
}));
|
|
78
|
-
const decodeTask =
|
|
76
|
+
const decodeTask = route.decodeJson(Task);
|
|
79
77
|
const decodeStatus = Effect.fn("RunwayVideo.decodeStatus")(function* (response, context) {
|
|
80
78
|
const output = yield* decodeTask(response);
|
|
81
79
|
const status = yield* MediaProtocol.status(STATUS, output.value.status, output);
|
|
@@ -87,19 +85,19 @@ const decodeResult = Effect.fn("RunwayVideo.decodeResult")(function* (response,
|
|
|
87
85
|
const status = yield* MediaProtocol.status(STATUS, task.status, output);
|
|
88
86
|
if (status === "failed") {
|
|
89
87
|
const code = task.failureCode ?? undefined;
|
|
90
|
-
const message = `${
|
|
88
|
+
const message = `${route.name} task failed${code === undefined ? "" : ` (${code})`}${task.failure ? `: ${task.failure}` : ""}`;
|
|
91
89
|
// Runway failure codes are dotted paths; every moderation outcome carries a SAFETY segment.
|
|
92
90
|
if (code !== undefined && /(^|\.)SAFETY(\.|$)/.test(code))
|
|
93
91
|
return yield* output.contentPolicy(message);
|
|
94
92
|
return yield* output.ended("failed", message);
|
|
95
93
|
}
|
|
96
94
|
if (status === "cancelled")
|
|
97
|
-
return yield* output.ended("cancelled", `${
|
|
95
|
+
return yield* output.ended("cancelled", `${route.name} task ${context.token.taskID} was cancelled`);
|
|
98
96
|
if (status !== "completed")
|
|
99
|
-
return yield* output.invalid(`${
|
|
97
|
+
return yield* output.invalid(`${route.name} task ${context.token.taskID} has not finished`);
|
|
100
98
|
const urls = task.output ?? [];
|
|
101
99
|
if (urls.length === 0)
|
|
102
|
-
return yield* output.invalid(`${
|
|
100
|
+
return yield* output.invalid(`${route.name} task succeeded without any output`);
|
|
103
101
|
return new VideoResponse({
|
|
104
102
|
videos: yield* Effect.forEach(urls, (url) => MediaProtocol.expiringUrl(url, OUTPUT_RETENTION, { mediaType: "video/mp4" })),
|
|
105
103
|
usage: task.cost === undefined ? undefined : { type: "credits", credits: task.cost.credits },
|
|
@@ -115,9 +113,7 @@ const decodeResult = Effect.fn("RunwayVideo.decodeResult")(function* (response,
|
|
|
115
113
|
// 7. Protocol and route
|
|
116
114
|
// ---------------------------------------------------------------------------
|
|
117
115
|
const taskPath = (token) => `${TASKS_PATH}/${token.taskID}`;
|
|
118
|
-
export const protocol = MediaProtocol.queued({
|
|
119
|
-
id: ADAPTER,
|
|
120
|
-
name: NAME,
|
|
116
|
+
export const protocol = MediaProtocol.queued(route, {
|
|
121
117
|
token: Token,
|
|
122
118
|
unsupported: ["n"],
|
|
123
119
|
start: { body: { from: fromRequest }, decode: decodeStart },
|
|
@@ -133,8 +129,6 @@ const startPath = (request) => {
|
|
|
133
129
|
return TEXT_TO_VIDEO_PATH;
|
|
134
130
|
};
|
|
135
131
|
export const model = (input) => VideoModel.fromRoute({
|
|
136
|
-
id: ADAPTER,
|
|
137
|
-
provider: PROVIDER,
|
|
138
132
|
protocol,
|
|
139
133
|
baseURL: DEFAULT_BASE_URL,
|
|
140
134
|
headers: { "X-Runway-Version": API_VERSION },
|