@opencode/ai 0.0.0-dev-20180 → 0.0.0-dev-20183
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/README.md +1 -2
- package/dist/protocols/assemblyai-transcription.js +1 -1
- package/dist/protocols/bfl-images.js +3 -1
- package/dist/protocols/google-images.js +1 -1
- package/dist/protocols/google-video.js +1 -1
- package/dist/protocols/openai-images.d.ts +3 -0
- package/dist/protocols/openai-images.js +49 -19
- package/dist/protocols/openai-responses.d.ts +10 -10
- package/dist/protocols/openai-transcription.d.ts +4 -0
- package/dist/protocols/openai-transcription.js +25 -5
- package/dist/protocols/replicate-images.js +1 -1
- package/dist/protocols/runway-video.js +1 -1
- package/dist/protocols/stability-images.js +1 -1
- package/dist/protocols/xai-images.js +2 -1
- package/dist/protocols/xai-video.js +1 -1
- package/dist/protocols/zai-images.js +3 -3
- package/dist/providers/azure.d.ts +4 -4
- package/dist/providers/cloudflare-ai-gateway.d.ts +4 -4
- package/dist/providers/openai.d.ts +2 -2
- package/dist/route/media-protocol.d.ts +2 -0
- package/dist/route/media-protocol.js +11 -4
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -621,8 +621,7 @@ persist the bytes promptly if they must remain available.
|
|
|
621
621
|
### Partial images
|
|
622
622
|
|
|
623
623
|
OpenAI's GPT image models stream previews. `Image.stream` sends `stream: true` with `partialImages` (0–3, default 2)
|
|
624
|
-
and emits `image-partial` events before each final `image`; `Image.generate` keeps the plain JSON request
|
|
625
|
-
`dall-e-*` models do not stream and fail typed:
|
|
624
|
+
and emits `image-partial` events before each final `image`; `Image.generate` keeps the plain JSON request:
|
|
626
625
|
|
|
627
626
|
```ts
|
|
628
627
|
import { Stream } from "effect"
|
|
@@ -94,7 +94,7 @@ const decodeResult = Effect.fn("AssemblyAITranscription.decodeResult")(function*
|
|
|
94
94
|
if (status === "failed")
|
|
95
95
|
return yield* output.ended("failed", `${route.name} transcription failed${error === undefined ? "" : `: ${error}`}`);
|
|
96
96
|
if (status !== "completed")
|
|
97
|
-
return yield* output.
|
|
97
|
+
return yield* output.pending(context.token.transcriptID);
|
|
98
98
|
const duration = transcript.audio_duration ?? undefined;
|
|
99
99
|
return new TranscriptionResponse({
|
|
100
100
|
text: transcript.text ?? "",
|
|
@@ -121,7 +121,9 @@ const decodeResult = Effect.fn("BlackForestLabsImages.decodeResult")(function* (
|
|
|
121
121
|
return yield* output.contentPolicy(`${route.name} moderated the generation`);
|
|
122
122
|
if (status === "failed" || status === "expired")
|
|
123
123
|
return yield* output.ended(status, `${route.name} generation ${context.token.id} ended with ${document.status}`);
|
|
124
|
-
if (status !== "completed"
|
|
124
|
+
if (status !== "completed")
|
|
125
|
+
return yield* output.pending(context.token.id);
|
|
126
|
+
if (document.result === undefined || document.result === null)
|
|
125
127
|
return yield* output.invalid(`${route.name} generation ${context.token.id} has no result`);
|
|
126
128
|
const { sample, seed, prompt, ...rest } = document.result;
|
|
127
129
|
// A settled `cost` on the result supersedes the submit-time cost carried on the token.
|
|
@@ -61,7 +61,7 @@ const generationConfig = (request) => {
|
|
|
61
61
|
};
|
|
62
62
|
const fromRequest = Effect.fn("GoogleImages.fromRequest")(function* (request) {
|
|
63
63
|
if (request.n !== undefined && request.n > 1)
|
|
64
|
-
return yield* route.unsupported("
|
|
64
|
+
return yield* route.unsupported("media.n", `${route.name} generates one image per request; call it once per image instead of n=${request.n}`);
|
|
65
65
|
const parts = yield* Effect.forEach(request.images ?? [], (image) => GeminiGenerateContent.mediaPart(route.name, image));
|
|
66
66
|
return MediaProtocol.json(mergeJsonRecords({
|
|
67
67
|
contents: [{ role: "user", parts: [{ text: request.prompt }, ...parts] }],
|
|
@@ -90,7 +90,7 @@ const decodeResult = Effect.fn("GoogleVideo.decodeResult")(function* (response,
|
|
|
90
90
|
const operation = output.value;
|
|
91
91
|
const status = statusOf(operation);
|
|
92
92
|
if (status === "running")
|
|
93
|
-
return yield* output.
|
|
93
|
+
return yield* output.pending(context.token.operation);
|
|
94
94
|
if (status === "failed")
|
|
95
95
|
return yield* output.ended("failed", `${route.name} operation failed${operation.error?.message === undefined ? "" : `: ${operation.error.message}`}`);
|
|
96
96
|
const generated = operation.response?.generateVideoResponse;
|
|
@@ -24,6 +24,9 @@ type Frame = string | {
|
|
|
24
24
|
interface State {
|
|
25
25
|
readonly completed: number;
|
|
26
26
|
readonly format?: string;
|
|
27
|
+
readonly size?: string;
|
|
28
|
+
readonly quality?: string;
|
|
29
|
+
readonly background?: string;
|
|
27
30
|
readonly usage?: MediaUsage;
|
|
28
31
|
}
|
|
29
32
|
export declare const protocol: MediaProtocol.Streamed<Request, {
|
|
@@ -21,13 +21,16 @@ const Usage = Schema.Struct({
|
|
|
21
21
|
input_tokens_details: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
|
22
22
|
output_tokens_details: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
|
23
23
|
});
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
b64_json: Schema.optional(Schema.String),
|
|
27
|
-
url: Schema.optional(Schema.String),
|
|
28
|
-
revised_prompt: Schema.optional(Schema.String),
|
|
29
|
-
})),
|
|
24
|
+
/** What the provider actually rendered; it can differ from the request when `auto` or a default applied. */
|
|
25
|
+
const Settings = {
|
|
30
26
|
output_format: Schema.optional(Schema.String),
|
|
27
|
+
size: Schema.optional(Schema.String),
|
|
28
|
+
quality: Schema.optional(Schema.String),
|
|
29
|
+
background: Schema.optional(Schema.String),
|
|
30
|
+
};
|
|
31
|
+
const OpenAIImageResponse = Schema.Struct({
|
|
32
|
+
data: Schema.Array(Schema.Struct({ b64_json: Schema.String })),
|
|
33
|
+
...Settings,
|
|
31
34
|
usage: Schema.optional(Usage),
|
|
32
35
|
});
|
|
33
36
|
// ---------------------------------------------------------------------------
|
|
@@ -38,11 +41,13 @@ const StreamEvent = Schema.Union([
|
|
|
38
41
|
type: Schema.Literals(["image_generation.partial_image", "image_edit.partial_image"]),
|
|
39
42
|
b64_json: Schema.String,
|
|
40
43
|
partial_image_index: Schema.Number,
|
|
44
|
+
...Settings,
|
|
41
45
|
output_format: Schema.String,
|
|
42
46
|
}),
|
|
43
47
|
Schema.Struct({
|
|
44
48
|
type: Schema.Literals(["image_generation.completed", "image_edit.completed"]),
|
|
45
49
|
b64_json: Schema.String,
|
|
50
|
+
...Settings,
|
|
46
51
|
output_format: Schema.String,
|
|
47
52
|
usage: Schema.optional(Usage),
|
|
48
53
|
}),
|
|
@@ -63,8 +68,6 @@ const nativeOptions = (options) => {
|
|
|
63
68
|
const streamOptions = (request) => {
|
|
64
69
|
if (request.mode !== "stream")
|
|
65
70
|
return Effect.succeed(undefined);
|
|
66
|
-
if (request.model.id.startsWith("dall-e"))
|
|
67
|
-
return Effect.fail(route.unsupported("media.stream", `${request.model.id} does not stream; use Image.generate or a GPT image model`));
|
|
68
71
|
if (request.n !== undefined && request.n > 1)
|
|
69
72
|
return Effect.fail(route.unsupported("media.n", `${route.name} streams one image; use Image.generate for n=${request.n}`));
|
|
70
73
|
return Effect.succeed({ stream: true, partial_images: request.providerOptions?.partialImages ?? 2 });
|
|
@@ -115,19 +118,31 @@ const usage = (value) => value === undefined
|
|
|
115
118
|
total: value.total_tokens,
|
|
116
119
|
details: { openai: value },
|
|
117
120
|
};
|
|
118
|
-
|
|
119
|
-
|
|
121
|
+
/** `size` echoes the rendered `WIDTHxHEIGHT`; `auto` or any other value leaves the dimensions unknown. */
|
|
122
|
+
const info = (format, size) => {
|
|
123
|
+
const match = size?.match(/^(\d+)x(\d+)$/);
|
|
124
|
+
return match ? { format, width: Number(match[1]), height: Number(match[2]) } : { format };
|
|
125
|
+
};
|
|
126
|
+
const eventImage = (frame, label, data, format, size) => MediaInput.decodedAsset((message, cause) => route.frameError(message, frame, cause), label, data, `image/${format}`, {
|
|
127
|
+
info: info(format, size),
|
|
120
128
|
});
|
|
121
129
|
const onEvent = Effect.fn("OpenAIImages.onEvent")(function* (state, frame) {
|
|
122
130
|
const event = yield* decodeEvent(frame);
|
|
123
131
|
const format = event.output_format;
|
|
124
132
|
if ("partial_image_index" in event) {
|
|
125
|
-
const image = yield* eventImage(frame, `${route.name} partial image`, event.b64_json, format);
|
|
133
|
+
const image = yield* eventImage(frame, `${route.name} partial image`, event.b64_json, format, event.size);
|
|
126
134
|
return [state, [ImagePartialEvent.make({ index: event.partial_image_index, image })]];
|
|
127
135
|
}
|
|
128
|
-
const image = yield* eventImage(frame, `${route.name} result ${state.completed}`, event.b64_json, format);
|
|
136
|
+
const image = yield* eventImage(frame, `${route.name} result ${state.completed}`, event.b64_json, format, event.size);
|
|
129
137
|
return [
|
|
130
|
-
{
|
|
138
|
+
{
|
|
139
|
+
completed: state.completed + 1,
|
|
140
|
+
format,
|
|
141
|
+
size: event.size,
|
|
142
|
+
quality: event.quality,
|
|
143
|
+
background: event.background,
|
|
144
|
+
usage: usage(event.usage),
|
|
145
|
+
},
|
|
131
146
|
[ImageOutputEvent.make({ index: state.completed, image })],
|
|
132
147
|
];
|
|
133
148
|
});
|
|
@@ -135,14 +150,19 @@ const onDocument = Effect.fn("OpenAIImages.onDocument")(function* (frame) {
|
|
|
135
150
|
const invalid = (message, cause) => route.frameError(message, frame.document, cause);
|
|
136
151
|
const decoded = yield* decodeDocument(frame.document).pipe(Effect.mapError((cause) => invalid(`${route.name} returned an invalid response`, cause)));
|
|
137
152
|
const format = decoded.output_format ?? frame.requested ?? "png";
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
info: { format },
|
|
141
|
-
providerMetadata: item.revised_prompt === undefined ? undefined : { openai: { revisedPrompt: item.revised_prompt } },
|
|
153
|
+
const images = yield* Effect.forEach(decoded.data, (item, index) => MediaInput.decodedAsset(invalid, `${route.name} result ${index}`, item.b64_json, `image/${format}`, {
|
|
154
|
+
info: info(format, decoded.size),
|
|
142
155
|
}));
|
|
143
156
|
if (images.length === 0)
|
|
144
157
|
return yield* invalid(`${route.name} returned no images`);
|
|
145
|
-
const state = {
|
|
158
|
+
const state = {
|
|
159
|
+
completed: images.length,
|
|
160
|
+
format,
|
|
161
|
+
size: decoded.size,
|
|
162
|
+
quality: decoded.quality,
|
|
163
|
+
background: decoded.background,
|
|
164
|
+
usage: usage(decoded.usage),
|
|
165
|
+
};
|
|
146
166
|
return [state, images.map((image, index) => ImageOutputEvent.make({ index, image }))];
|
|
147
167
|
});
|
|
148
168
|
const step = (state, frame) => (typeof frame === "string" ? onEvent(state, frame) : onDocument(frame));
|
|
@@ -150,7 +170,17 @@ const finish = (state) => {
|
|
|
150
170
|
if (state.completed === 0)
|
|
151
171
|
return Effect.fail(route.incomplete());
|
|
152
172
|
return Effect.succeed([
|
|
153
|
-
ImageFinishEvent.make({
|
|
173
|
+
ImageFinishEvent.make({
|
|
174
|
+
usage: state.usage,
|
|
175
|
+
providerMetadata: {
|
|
176
|
+
openai: {
|
|
177
|
+
outputFormat: state.format,
|
|
178
|
+
size: state.size,
|
|
179
|
+
quality: state.quality,
|
|
180
|
+
background: state.background,
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
}),
|
|
154
184
|
]);
|
|
155
185
|
};
|
|
156
186
|
// ---------------------------------------------------------------------------
|
|
@@ -364,8 +364,8 @@ export declare const protocol: Protocol<{
|
|
|
364
364
|
readonly type: "image_generation_call";
|
|
365
365
|
readonly status?: string | undefined;
|
|
366
366
|
readonly result?: string | null | undefined;
|
|
367
|
-
readonly revised_prompt?: string | null | undefined;
|
|
368
367
|
readonly output_format?: "png" | "jpeg" | "webp" | undefined;
|
|
368
|
+
readonly revised_prompt?: string | null | undefined;
|
|
369
369
|
})[];
|
|
370
370
|
readonly model: string;
|
|
371
371
|
readonly stream: true;
|
|
@@ -390,9 +390,9 @@ export declare const protocol: Protocol<{
|
|
|
390
390
|
readonly size?: string | undefined;
|
|
391
391
|
readonly action?: "generate" | "auto" | "edit" | undefined;
|
|
392
392
|
readonly output_format?: "png" | "jpeg" | "webp" | undefined;
|
|
393
|
-
readonly partial_images?: number | undefined;
|
|
394
393
|
readonly quality?: "auto" | "low" | "medium" | "high" | undefined;
|
|
395
394
|
readonly background?: "auto" | "opaque" | "transparent" | undefined;
|
|
395
|
+
readonly partial_images?: number | undefined;
|
|
396
396
|
readonly output_compression?: number | undefined;
|
|
397
397
|
readonly input_fidelity?: "low" | "high" | undefined;
|
|
398
398
|
} | {
|
|
@@ -647,8 +647,8 @@ export declare const httpTransport: HttpTransport.HttpJsonTransport<{
|
|
|
647
647
|
readonly type: "image_generation_call";
|
|
648
648
|
readonly status?: string | undefined;
|
|
649
649
|
readonly result?: string | null | undefined;
|
|
650
|
-
readonly revised_prompt?: string | null | undefined;
|
|
651
650
|
readonly output_format?: "png" | "jpeg" | "webp" | undefined;
|
|
651
|
+
readonly revised_prompt?: string | null | undefined;
|
|
652
652
|
})[];
|
|
653
653
|
readonly model: string;
|
|
654
654
|
readonly stream: true;
|
|
@@ -673,9 +673,9 @@ export declare const httpTransport: HttpTransport.HttpJsonTransport<{
|
|
|
673
673
|
readonly size?: string | undefined;
|
|
674
674
|
readonly action?: "generate" | "auto" | "edit" | undefined;
|
|
675
675
|
readonly output_format?: "png" | "jpeg" | "webp" | undefined;
|
|
676
|
-
readonly partial_images?: number | undefined;
|
|
677
676
|
readonly quality?: "auto" | "low" | "medium" | "high" | undefined;
|
|
678
677
|
readonly background?: "auto" | "opaque" | "transparent" | undefined;
|
|
678
|
+
readonly partial_images?: number | undefined;
|
|
679
679
|
readonly output_compression?: number | undefined;
|
|
680
680
|
readonly input_fidelity?: "low" | "high" | undefined;
|
|
681
681
|
} | {
|
|
@@ -875,8 +875,8 @@ export declare const channelTransport: (options: import("./open-responses-channe
|
|
|
875
875
|
readonly type: "image_generation_call";
|
|
876
876
|
readonly status?: string | undefined;
|
|
877
877
|
readonly result?: string | null | undefined;
|
|
878
|
-
readonly revised_prompt?: string | null | undefined;
|
|
879
878
|
readonly output_format?: "png" | "jpeg" | "webp" | undefined;
|
|
879
|
+
readonly revised_prompt?: string | null | undefined;
|
|
880
880
|
})[];
|
|
881
881
|
readonly model: string;
|
|
882
882
|
readonly stream: true;
|
|
@@ -901,9 +901,9 @@ export declare const channelTransport: (options: import("./open-responses-channe
|
|
|
901
901
|
readonly size?: string | undefined;
|
|
902
902
|
readonly action?: "generate" | "auto" | "edit" | undefined;
|
|
903
903
|
readonly output_format?: "png" | "jpeg" | "webp" | undefined;
|
|
904
|
-
readonly partial_images?: number | undefined;
|
|
905
904
|
readonly quality?: "auto" | "low" | "medium" | "high" | undefined;
|
|
906
905
|
readonly background?: "auto" | "opaque" | "transparent" | undefined;
|
|
906
|
+
readonly partial_images?: number | undefined;
|
|
907
907
|
readonly output_compression?: number | undefined;
|
|
908
908
|
readonly input_fidelity?: "low" | "high" | undefined;
|
|
909
909
|
} | {
|
|
@@ -1103,8 +1103,8 @@ export declare const transport: import("../route/transport/index.js").Transport<
|
|
|
1103
1103
|
readonly type: "image_generation_call";
|
|
1104
1104
|
readonly status?: string | undefined;
|
|
1105
1105
|
readonly result?: string | null | undefined;
|
|
1106
|
-
readonly revised_prompt?: string | null | undefined;
|
|
1107
1106
|
readonly output_format?: "png" | "jpeg" | "webp" | undefined;
|
|
1107
|
+
readonly revised_prompt?: string | null | undefined;
|
|
1108
1108
|
})[];
|
|
1109
1109
|
readonly model: string;
|
|
1110
1110
|
readonly stream: true;
|
|
@@ -1129,9 +1129,9 @@ export declare const transport: import("../route/transport/index.js").Transport<
|
|
|
1129
1129
|
readonly size?: string | undefined;
|
|
1130
1130
|
readonly action?: "generate" | "auto" | "edit" | undefined;
|
|
1131
1131
|
readonly output_format?: "png" | "jpeg" | "webp" | undefined;
|
|
1132
|
-
readonly partial_images?: number | undefined;
|
|
1133
1132
|
readonly quality?: "auto" | "low" | "medium" | "high" | undefined;
|
|
1134
1133
|
readonly background?: "auto" | "opaque" | "transparent" | undefined;
|
|
1134
|
+
readonly partial_images?: number | undefined;
|
|
1135
1135
|
readonly output_compression?: number | undefined;
|
|
1136
1136
|
readonly input_fidelity?: "low" | "high" | undefined;
|
|
1137
1137
|
} | {
|
|
@@ -1331,8 +1331,8 @@ export declare const route: Route<{
|
|
|
1331
1331
|
readonly type: "image_generation_call";
|
|
1332
1332
|
readonly status?: string | undefined;
|
|
1333
1333
|
readonly result?: string | null | undefined;
|
|
1334
|
-
readonly revised_prompt?: string | null | undefined;
|
|
1335
1334
|
readonly output_format?: "png" | "jpeg" | "webp" | undefined;
|
|
1335
|
+
readonly revised_prompt?: string | null | undefined;
|
|
1336
1336
|
})[];
|
|
1337
1337
|
readonly model: string;
|
|
1338
1338
|
readonly stream: true;
|
|
@@ -1357,9 +1357,9 @@ export declare const route: Route<{
|
|
|
1357
1357
|
readonly size?: string | undefined;
|
|
1358
1358
|
readonly action?: "generate" | "auto" | "edit" | undefined;
|
|
1359
1359
|
readonly output_format?: "png" | "jpeg" | "webp" | undefined;
|
|
1360
|
-
readonly partial_images?: number | undefined;
|
|
1361
1360
|
readonly quality?: "auto" | "low" | "medium" | "high" | undefined;
|
|
1362
1361
|
readonly background?: "auto" | "opaque" | "transparent" | undefined;
|
|
1362
|
+
readonly partial_images?: number | undefined;
|
|
1363
1363
|
readonly output_compression?: number | undefined;
|
|
1364
1364
|
readonly input_fidelity?: "low" | "high" | undefined;
|
|
1365
1365
|
} | {
|
|
@@ -41,6 +41,10 @@ declare const Transcript: Schema.Struct<{
|
|
|
41
41
|
readonly input_tokens: Schema.optional<Schema.Number>;
|
|
42
42
|
readonly output_tokens: Schema.optional<Schema.Number>;
|
|
43
43
|
readonly total_tokens: Schema.optional<Schema.Number>;
|
|
44
|
+
readonly input_token_details: Schema.optional<Schema.Struct<{
|
|
45
|
+
readonly audio_tokens: Schema.optional<Schema.Number>;
|
|
46
|
+
readonly text_tokens: Schema.optional<Schema.Number>;
|
|
47
|
+
}>>;
|
|
44
48
|
}>, Schema.Struct<{
|
|
45
49
|
readonly type: Schema.Literal<"duration">;
|
|
46
50
|
readonly seconds: Schema.Number;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Effect, Schema, Stream } from "effect";
|
|
2
|
+
import { classifyProviderFailure } from "../provider-error.js";
|
|
2
3
|
import { Framing } from "../route/framing.js";
|
|
3
4
|
import { MediaProtocol } from "../route/media-protocol.js";
|
|
4
5
|
import { MediaRoute } from "../route/media.js";
|
|
5
|
-
import { mergeJsonRecords } from "../schema/index.js";
|
|
6
|
+
import { AIError, mergeJsonRecords } from "../schema/index.js";
|
|
6
7
|
import { TranscriptionFinishEvent, TranscriptionModel, TranscriptionSegmentEvent, TranscriptionTextDeltaEvent, } from "../transcription.js";
|
|
7
8
|
import { mediaTypeExtension } from "../utils/media-type.js";
|
|
8
9
|
import { ProviderShared } from "./shared.js";
|
|
@@ -25,6 +26,7 @@ const Usage = Schema.Union([
|
|
|
25
26
|
input_tokens: Schema.optional(Schema.Number),
|
|
26
27
|
output_tokens: Schema.optional(Schema.Number),
|
|
27
28
|
total_tokens: Schema.optional(Schema.Number),
|
|
29
|
+
input_token_details: Schema.optional(Schema.Struct({ audio_tokens: Schema.optional(Schema.Number), text_tokens: Schema.optional(Schema.Number) })),
|
|
28
30
|
}),
|
|
29
31
|
Schema.Struct({ type: Schema.Literal("duration"), seconds: Schema.Number }),
|
|
30
32
|
]);
|
|
@@ -37,12 +39,21 @@ const transcriptFields = {
|
|
|
37
39
|
words: Schema.optional(Schema.Array(Schema.Struct({ word: Schema.String, start: Schema.Number, end: Schema.Number }))),
|
|
38
40
|
usage: Schema.optional(Usage),
|
|
39
41
|
};
|
|
42
|
+
/** OpenAI may add stream event types; frames outside `EVENT_TYPES` are ignored. */
|
|
43
|
+
const EventType = Schema.Struct({ type: Schema.String });
|
|
40
44
|
const Event = Schema.Union([
|
|
41
45
|
Schema.Struct({ type: Schema.Literal("transcript.text.delta"), delta: Schema.String }),
|
|
42
46
|
Schema.Struct({ type: Schema.Literal("transcript.text.segment"), ...Segment.fields }),
|
|
43
47
|
Schema.Struct({ type: Schema.Literal("transcript.text.done"), ...transcriptFields }),
|
|
48
|
+
Schema.Struct({
|
|
49
|
+
type: Schema.Literal("error"),
|
|
50
|
+
message: Schema.optional(Schema.String),
|
|
51
|
+
error: Schema.optional(Schema.Struct({ message: Schema.optional(Schema.String) })),
|
|
52
|
+
}),
|
|
44
53
|
]);
|
|
54
|
+
const EVENT_TYPES = new Set(["transcript.text.delta", "transcript.text.segment", "transcript.text.done", "error"]);
|
|
45
55
|
const Transcript = Schema.Struct(transcriptFields);
|
|
56
|
+
const decodeEventType = route.decodeFrame(EventType);
|
|
46
57
|
const decodeEvent = route.decodeFrame(Event);
|
|
47
58
|
const decodeTranscript = route.decodeFrame(Transcript);
|
|
48
59
|
const TRANSCRIBE = { stream: true, timestamps: [], diarize: false, languageField: "language" };
|
|
@@ -56,10 +67,10 @@ const capabilities = (model) => {
|
|
|
56
67
|
return { ...TRANSCRIBE, languageField: "languages" };
|
|
57
68
|
return TRANSCRIBE;
|
|
58
69
|
};
|
|
70
|
+
/** whisper-1 ignores `stream`, so its `stream` mode sends a plain request and emits only `finish`. */
|
|
71
|
+
const streamsEvents = (request) => request.mode === "stream" && capabilities(request.model.id).stream;
|
|
59
72
|
const validate = (request, model) => {
|
|
60
73
|
const id = request.model.id;
|
|
61
|
-
if (request.mode === "stream" && !model.stream)
|
|
62
|
-
return Effect.fail(route.unsupported("media.stream", `${id} does not stream; use Transcription.generate`));
|
|
63
74
|
if (request.diarize === true && !model.diarize)
|
|
64
75
|
return Effect.fail(route.unsupported("media.diarize", `${id} does not diarize; use gpt-4o-transcribe-diarize`));
|
|
65
76
|
if (request.prompt !== undefined && model.diarize)
|
|
@@ -103,7 +114,7 @@ const fromRequest = Effect.fn("OpenAITranscription.fromRequest")(function* (requ
|
|
|
103
114
|
timestamp_granularities: responseFormat === "verbose_json" ? [request.timestamps] : undefined,
|
|
104
115
|
// Diarizing audio longer than 30 seconds requires a chunking strategy.
|
|
105
116
|
chunking_strategy: model.diarize ? "auto" : undefined,
|
|
106
|
-
stream: request
|
|
117
|
+
stream: streamsEvents(request) ? true : undefined,
|
|
107
118
|
}, {
|
|
108
119
|
overlay: mergeJsonRecords(request.providerOptions, request.http?.body),
|
|
109
120
|
reserved: RESERVED_FORM_FIELDS,
|
|
@@ -121,7 +132,16 @@ const segment = (value) => ({
|
|
|
121
132
|
speaker: value.speaker,
|
|
122
133
|
});
|
|
123
134
|
const onEvent = Effect.fn("OpenAITranscription.onEvent")(function* (state, frame) {
|
|
135
|
+
if (!EVENT_TYPES.has((yield* decodeEventType(frame)).type))
|
|
136
|
+
return [state, []];
|
|
124
137
|
const event = yield* decodeEvent(frame);
|
|
138
|
+
if (event.type === "error")
|
|
139
|
+
return yield* new AIError({
|
|
140
|
+
reason: classifyProviderFailure({
|
|
141
|
+
message: `${route.name} stream failed: ${event.message ?? event.error?.message ?? "unknown error"}`,
|
|
142
|
+
rawBody: frame,
|
|
143
|
+
}),
|
|
144
|
+
});
|
|
125
145
|
if (event.type === "transcript.text.done")
|
|
126
146
|
return [{ ...state, transcript: event }, []];
|
|
127
147
|
if (event.type === "transcript.text.delta")
|
|
@@ -168,7 +188,7 @@ const finish = (state) => {
|
|
|
168
188
|
export const protocol = MediaProtocol.stream(route, {
|
|
169
189
|
unsupported: ["speakers"],
|
|
170
190
|
body: { from: fromRequest },
|
|
171
|
-
frames: (bytes, context) => context.request
|
|
191
|
+
frames: (bytes, context) => streamsEvents(context.request)
|
|
172
192
|
? Framing.sse.frame(bytes)
|
|
173
193
|
: Framing.document.frame(bytes).pipe(Stream.map((document) => ({ document }))),
|
|
174
194
|
initial: () => ({ segments: [] }),
|
|
@@ -88,7 +88,7 @@ const decodeResult = Effect.fn("ReplicateImages.decodeResult")(function* (respon
|
|
|
88
88
|
if (status === "failed" || status === "cancelled" || status === "expired")
|
|
89
89
|
return yield* output.ended(status, `${route.name} prediction ${context.token.id} ${prediction.status}${typeof prediction.error === "string" ? `: ${prediction.error}` : ""}`);
|
|
90
90
|
if (status !== "completed")
|
|
91
|
-
return yield* output.
|
|
91
|
+
return yield* output.pending(context.token.id);
|
|
92
92
|
if (prediction.data_removed === true)
|
|
93
93
|
return yield* output.ended("expired", `${route.name} removed the output of prediction ${context.token.id}`);
|
|
94
94
|
if (!isOutput(prediction.output))
|
|
@@ -94,7 +94,7 @@ const decodeResult = Effect.fn("RunwayVideo.decodeResult")(function* (response,
|
|
|
94
94
|
if (status === "cancelled")
|
|
95
95
|
return yield* output.ended("cancelled", `${route.name} task ${context.token.taskID} was cancelled`);
|
|
96
96
|
if (status !== "completed")
|
|
97
|
-
return yield* output.
|
|
97
|
+
return yield* output.pending(context.token.taskID);
|
|
98
98
|
const urls = task.output ?? [];
|
|
99
99
|
if (urls.length === 0)
|
|
100
100
|
return yield* output.invalid(`${route.name} task succeeded without any output`);
|
|
@@ -96,7 +96,7 @@ const decodeStatus = (response, context) => Effect.succeed({
|
|
|
96
96
|
const decodeUpscaleResult = Effect.fn("StabilityImages.decodeUpscaleResult")(function* (response, context) {
|
|
97
97
|
if (response.status === 202) {
|
|
98
98
|
const output = yield* upscaleRoute.text(response);
|
|
99
|
-
return yield* output.
|
|
99
|
+
return yield* output.pending(context.token.id);
|
|
100
100
|
}
|
|
101
101
|
return yield* decodeUpscaleImage(response);
|
|
102
102
|
});
|
|
@@ -60,7 +60,8 @@ const decodeResponse = Effect.fn("XAIImages.decodeResponse")(function* (response
|
|
|
60
60
|
if (images.length === 0)
|
|
61
61
|
return yield* output.invalid(`${route.name} returned no images`);
|
|
62
62
|
const usage = ProviderShared.isRecord(decoded.usage) ? decoded.usage : undefined;
|
|
63
|
-
// xAI reports
|
|
63
|
+
// xAI reports a USD cost (`cost_in_usd_ticks`) rather than tokens, seconds, or credits; the raw record stays in
|
|
64
|
+
// provider metadata.
|
|
64
65
|
return new ImageResponse({
|
|
65
66
|
images,
|
|
66
67
|
providerMetadata: usage === undefined ? undefined : { xai: { usage } },
|
|
@@ -84,7 +84,7 @@ const decodeResult = Effect.fn("XAIVideo.decodeResult")(function* (response, con
|
|
|
84
84
|
const decoded = output.value;
|
|
85
85
|
const status = yield* MediaProtocol.status(STATUS, decoded.status, output);
|
|
86
86
|
if (status === "running")
|
|
87
|
-
return yield* output.
|
|
87
|
+
return yield* output.pending(context.token.requestID);
|
|
88
88
|
if (status === "failed") {
|
|
89
89
|
const code = decoded.error?.code ?? undefined;
|
|
90
90
|
const message = decoded.error?.message ?? undefined;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { Effect, Schema } from "effect";
|
|
1
|
+
import { Duration, Effect, Schema } from "effect";
|
|
2
2
|
import { ImageModel, ImageResponse } from "../image.js";
|
|
3
|
-
import { Media } from "../media.js";
|
|
4
3
|
import { MediaProtocol } from "../route/media-protocol.js";
|
|
5
4
|
import { MediaRoute } from "../route/media.js";
|
|
6
5
|
import { mergeJsonRecords } from "../schema/index.js";
|
|
7
6
|
const route = MediaProtocol.identity({ id: "zai-images", name: "Z.ai Images", provider: "zai" });
|
|
8
7
|
export const DEFAULT_BASE_URL = "https://api.z.ai/api/paas/v4";
|
|
9
8
|
export const PATH = "/images/generations";
|
|
9
|
+
const OUTPUT_RETENTION = Duration.days(30);
|
|
10
10
|
// ---------------------------------------------------------------------------
|
|
11
11
|
// 2. Response schema
|
|
12
12
|
// ---------------------------------------------------------------------------
|
|
@@ -44,7 +44,7 @@ const decodeResponse = Effect.fn("ZAIImages.decodeResponse")(function* (response
|
|
|
44
44
|
const filters = decoded.content_filter ?? [];
|
|
45
45
|
return new ImageResponse({
|
|
46
46
|
// Z.ai returns only URLs and no content type; the media type resolves when the asset is materialized.
|
|
47
|
-
images: decoded.data
|
|
47
|
+
images: yield* Effect.forEach(decoded.data, (item) => MediaProtocol.expiringUrl(item.url, OUTPUT_RETENTION)),
|
|
48
48
|
// Z.ai reports applied content filters alongside a successful result; surface them instead of dropping them.
|
|
49
49
|
notices: filters.length === 0
|
|
50
50
|
? undefined
|
|
@@ -166,8 +166,8 @@ declare const responsesRoute: Route<{
|
|
|
166
166
|
readonly type: "image_generation_call";
|
|
167
167
|
readonly status?: string | undefined;
|
|
168
168
|
readonly result?: string | null | undefined;
|
|
169
|
-
readonly revised_prompt?: string | null | undefined;
|
|
170
169
|
readonly output_format?: "png" | "jpeg" | "webp" | undefined;
|
|
170
|
+
readonly revised_prompt?: string | null | undefined;
|
|
171
171
|
})[];
|
|
172
172
|
readonly model: string;
|
|
173
173
|
readonly stream: true;
|
|
@@ -192,9 +192,9 @@ declare const responsesRoute: Route<{
|
|
|
192
192
|
readonly size?: string | undefined;
|
|
193
193
|
readonly action?: "generate" | "auto" | "edit" | undefined;
|
|
194
194
|
readonly output_format?: "png" | "jpeg" | "webp" | undefined;
|
|
195
|
-
readonly partial_images?: number | undefined;
|
|
196
195
|
readonly quality?: "auto" | "low" | "medium" | "high" | undefined;
|
|
197
196
|
readonly background?: "auto" | "opaque" | "transparent" | undefined;
|
|
197
|
+
readonly partial_images?: number | undefined;
|
|
198
198
|
readonly output_compression?: number | undefined;
|
|
199
199
|
readonly input_fidelity?: "low" | "high" | undefined;
|
|
200
200
|
} | {
|
|
@@ -506,8 +506,8 @@ export declare const routes: (Route<{
|
|
|
506
506
|
readonly type: "image_generation_call";
|
|
507
507
|
readonly status?: string | undefined;
|
|
508
508
|
readonly result?: string | null | undefined;
|
|
509
|
-
readonly revised_prompt?: string | null | undefined;
|
|
510
509
|
readonly output_format?: "png" | "jpeg" | "webp" | undefined;
|
|
510
|
+
readonly revised_prompt?: string | null | undefined;
|
|
511
511
|
})[];
|
|
512
512
|
readonly model: string;
|
|
513
513
|
readonly stream: true;
|
|
@@ -532,9 +532,9 @@ export declare const routes: (Route<{
|
|
|
532
532
|
readonly size?: string | undefined;
|
|
533
533
|
readonly action?: "generate" | "auto" | "edit" | undefined;
|
|
534
534
|
readonly output_format?: "png" | "jpeg" | "webp" | undefined;
|
|
535
|
-
readonly partial_images?: number | undefined;
|
|
536
535
|
readonly quality?: "auto" | "low" | "medium" | "high" | undefined;
|
|
537
536
|
readonly background?: "auto" | "opaque" | "transparent" | undefined;
|
|
537
|
+
readonly partial_images?: number | undefined;
|
|
538
538
|
readonly output_compression?: number | undefined;
|
|
539
539
|
readonly input_fidelity?: "low" | "high" | undefined;
|
|
540
540
|
} | {
|
|
@@ -172,8 +172,8 @@ export declare const responsesRoute: import("../route/client.js").Route<{
|
|
|
172
172
|
readonly type: "image_generation_call";
|
|
173
173
|
readonly status?: string | undefined;
|
|
174
174
|
readonly result?: string | null | undefined;
|
|
175
|
-
readonly revised_prompt?: string | null | undefined;
|
|
176
175
|
readonly output_format?: "png" | "jpeg" | "webp" | undefined;
|
|
176
|
+
readonly revised_prompt?: string | null | undefined;
|
|
177
177
|
})[];
|
|
178
178
|
readonly model: string;
|
|
179
179
|
readonly stream: true;
|
|
@@ -198,9 +198,9 @@ export declare const responsesRoute: import("../route/client.js").Route<{
|
|
|
198
198
|
readonly size?: string | undefined;
|
|
199
199
|
readonly action?: "generate" | "auto" | "edit" | undefined;
|
|
200
200
|
readonly output_format?: "png" | "jpeg" | "webp" | undefined;
|
|
201
|
-
readonly partial_images?: number | undefined;
|
|
202
201
|
readonly quality?: "auto" | "low" | "medium" | "high" | undefined;
|
|
203
202
|
readonly background?: "auto" | "opaque" | "transparent" | undefined;
|
|
203
|
+
readonly partial_images?: number | undefined;
|
|
204
204
|
readonly output_compression?: number | undefined;
|
|
205
205
|
readonly input_fidelity?: "low" | "high" | undefined;
|
|
206
206
|
} | {
|
|
@@ -1169,8 +1169,8 @@ export declare const routes: (import("../route/client.js").Route<{
|
|
|
1169
1169
|
readonly type: "image_generation_call";
|
|
1170
1170
|
readonly status?: string | undefined;
|
|
1171
1171
|
readonly result?: string | null | undefined;
|
|
1172
|
-
readonly revised_prompt?: string | null | undefined;
|
|
1173
1172
|
readonly output_format?: "png" | "jpeg" | "webp" | undefined;
|
|
1173
|
+
readonly revised_prompt?: string | null | undefined;
|
|
1174
1174
|
})[];
|
|
1175
1175
|
readonly model: string;
|
|
1176
1176
|
readonly stream: true;
|
|
@@ -1195,9 +1195,9 @@ export declare const routes: (import("../route/client.js").Route<{
|
|
|
1195
1195
|
readonly size?: string | undefined;
|
|
1196
1196
|
readonly action?: "generate" | "auto" | "edit" | undefined;
|
|
1197
1197
|
readonly output_format?: "png" | "jpeg" | "webp" | undefined;
|
|
1198
|
-
readonly partial_images?: number | undefined;
|
|
1199
1198
|
readonly quality?: "auto" | "low" | "medium" | "high" | undefined;
|
|
1200
1199
|
readonly background?: "auto" | "opaque" | "transparent" | undefined;
|
|
1200
|
+
readonly partial_images?: number | undefined;
|
|
1201
1201
|
readonly output_compression?: number | undefined;
|
|
1202
1202
|
readonly input_fidelity?: "low" | "high" | undefined;
|
|
1203
1203
|
} | {
|
|
@@ -264,8 +264,8 @@ export declare const routes: (Route<{
|
|
|
264
264
|
readonly type: "image_generation_call";
|
|
265
265
|
readonly status?: string | undefined;
|
|
266
266
|
readonly result?: string | null | undefined;
|
|
267
|
-
readonly revised_prompt?: string | null | undefined;
|
|
268
267
|
readonly output_format?: "png" | "jpeg" | "webp" | undefined;
|
|
268
|
+
readonly revised_prompt?: string | null | undefined;
|
|
269
269
|
})[];
|
|
270
270
|
readonly model: string;
|
|
271
271
|
readonly stream: true;
|
|
@@ -290,9 +290,9 @@ export declare const routes: (Route<{
|
|
|
290
290
|
readonly size?: string | undefined;
|
|
291
291
|
readonly action?: "generate" | "auto" | "edit" | undefined;
|
|
292
292
|
readonly output_format?: "png" | "jpeg" | "webp" | undefined;
|
|
293
|
-
readonly partial_images?: number | undefined;
|
|
294
293
|
readonly quality?: "auto" | "low" | "medium" | "high" | undefined;
|
|
295
294
|
readonly background?: "auto" | "opaque" | "transparent" | undefined;
|
|
295
|
+
readonly partial_images?: number | undefined;
|
|
296
296
|
readonly output_compression?: number | undefined;
|
|
297
297
|
readonly input_fidelity?: "low" | "high" | undefined;
|
|
298
298
|
} | {
|
|
@@ -142,6 +142,7 @@ export declare const identity: (input: {
|
|
|
142
142
|
http: HttpContext;
|
|
143
143
|
invalid: (message: string, cause?: unknown) => AIError;
|
|
144
144
|
ended: (status: Exclude<Status, "queued" | "running" | "completed">, message: string) => AIError;
|
|
145
|
+
pending: (id: string) => AIError;
|
|
145
146
|
contentPolicy: (message: string) => AIError;
|
|
146
147
|
}, AIError, never>;
|
|
147
148
|
decodeJson: <A>(schema: Schema.Codec<A, unknown>) => (response: HttpClientResponse.HttpClientResponse) => Effect.Effect<{
|
|
@@ -150,6 +151,7 @@ export declare const identity: (input: {
|
|
|
150
151
|
http: HttpContext;
|
|
151
152
|
invalid: (message: string, cause?: unknown) => AIError;
|
|
152
153
|
ended: (status: Exclude<Status, "queued" | "running" | "completed">, message: string) => AIError;
|
|
154
|
+
pending: (id: string) => AIError;
|
|
153
155
|
contentPolicy: (message: string) => AIError;
|
|
154
156
|
}, AIError, never>;
|
|
155
157
|
/** Decode a submission response into the token and first snapshot. */
|
|
@@ -28,7 +28,8 @@ export const identity = (input) => {
|
|
|
28
28
|
/**
|
|
29
29
|
* Read a text body while retaining the original payload and HTTP context on every downstream error. `invalid` is a
|
|
30
30
|
* malformed provider document; `ended` is a generation that reached a terminal status without output (`failed` is
|
|
31
|
-
* provider-side, `cancelled`/`expired` mean the result will never exist); `
|
|
31
|
+
* provider-side, `cancelled`/`expired` mean the result will never exist); `pending` is a `result()` read before the
|
|
32
|
+
* generation finished, which is caller misuse; `contentPolicy` is a moderated result.
|
|
32
33
|
*/
|
|
33
34
|
const text = Effect.fn("MediaProtocol.text")(function* (response) {
|
|
34
35
|
const http = context(response);
|
|
@@ -49,6 +50,13 @@ export const identity = (input) => {
|
|
|
49
50
|
? new ProviderInternalError({ message, body, http })
|
|
50
51
|
: new InvalidRequestError({ message, body, http }),
|
|
51
52
|
}),
|
|
53
|
+
pending: (id) => new AIError({
|
|
54
|
+
reason: new InvalidRequestError({
|
|
55
|
+
message: `${input.name} generation ${id} has not finished; await it before reading the result`,
|
|
56
|
+
body,
|
|
57
|
+
http,
|
|
58
|
+
}),
|
|
59
|
+
}),
|
|
52
60
|
contentPolicy: (message) => new AIError({ reason: new ContentPolicyError({ message, body, http }) }),
|
|
53
61
|
};
|
|
54
62
|
});
|
|
@@ -91,10 +99,9 @@ export const identity = (input) => {
|
|
|
91
99
|
};
|
|
92
100
|
/** Map a provider status string through the protocol's table; unknown values are an invalid provider document. */
|
|
93
101
|
export const status = (table, raw, output) => {
|
|
94
|
-
|
|
95
|
-
if (normalized === undefined)
|
|
102
|
+
if (!Object.hasOwn(table, raw))
|
|
96
103
|
return Effect.fail(output.invalid(`Unknown generation status "${raw}"`));
|
|
97
|
-
return Effect.succeed(
|
|
104
|
+
return Effect.succeed(table[raw]);
|
|
98
105
|
};
|
|
99
106
|
/** A `url` asset whose provider-declared retention window starts now. */
|
|
100
107
|
export const expiringUrl = (url, retention, options) => Clock.currentTimeMillis.pipe(Effect.map((now) => Media.url(url, { ...options, expiresAt: now + Duration.toMillis(retention) })));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
|
-
"version": "0.0.0-dev-
|
|
3
|
+
"version": "0.0.0-dev-20183",
|
|
4
4
|
"name": "@opencode/ai",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@clack/prompts": "1.0.0-alpha.1",
|
|
36
36
|
"@effect/platform-node": "4.0.0-rc.112",
|
|
37
|
-
"@opencode/http-recorder": "0.0.0-dev-
|
|
37
|
+
"@opencode/http-recorder": "0.0.0-dev-20183",
|
|
38
38
|
"@tsconfig/bun": "1.0.9",
|
|
39
39
|
"@types/bun": "1.4.0",
|
|
40
40
|
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"@aws-sdk/credential-providers": "3.1057.0",
|
|
45
45
|
"@smithy/eventstream-codec": "4.2.14",
|
|
46
46
|
"@smithy/util-utf8": "4.2.2",
|
|
47
|
-
"@opencode/schema": "0.0.0-dev-
|
|
47
|
+
"@opencode/schema": "0.0.0-dev-20183",
|
|
48
48
|
"aws4fetch": "1.0.20",
|
|
49
49
|
"effect": "4.0.0-rc.112",
|
|
50
50
|
"google-auth-library": "10.5.0"
|