@opencode/ai 0.0.0-dev-20180 → 0.0.0-dev-20184

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 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.invalid(`${route.name} transcript ${context.token.transcriptID} has not finished`);
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" || document.result === undefined || document.result === null)
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("image.n", `${route.name} generates one image per request; call it once per image instead of n=${request.n}`);
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.invalid(`${route.name} operation ${context.token.operation} has not finished`);
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
- const OpenAIImageResponse = Schema.Struct({
25
- data: Schema.Array(Schema.Struct({
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
- const eventImage = (frame, label, data, format) => MediaInput.decodedAsset((message, cause) => route.frameError(message, frame, cause), label, data, `image/${format}`, {
119
- info: { format },
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
- { ...state, completed: state.completed + 1, format, usage: usage(event.usage) },
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 mediaType = `image/${format}`;
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 } },
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 = { completed: images.length, format, usage: usage(decoded.usage) };
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({ usage: state.usage, providerMetadata: { openai: { outputFormat: state.format } } }),
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.mode === "stream" ? true : undefined,
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.mode === "stream"
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.invalid(`${route.name} prediction ${context.token.id} has not finished`);
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.invalid(`${route.name} task ${context.token.taskID} has not finished`);
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.invalid(`${upscaleRoute.name} upscale ${context.token.id} has not finished`);
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 image counts rather than tokens, seconds, or credits; the raw record stays in provider metadata.
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.invalid(`${route.name} request ${context.token.requestID} has not finished`);
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.map((item) => Media.url(item.url)),
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); `contentPolicy` is a moderated result.
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
- const normalized = table[raw];
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(normalized);
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-20180",
3
+ "version": "0.0.0-dev-20184",
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-20180",
37
+ "@opencode/http-recorder": "0.0.0-dev-20184",
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-20180",
47
+ "@opencode/schema": "0.0.0-dev-20184",
48
48
  "aws4fetch": "1.0.20",
49
49
  "effect": "4.0.0-rc.112",
50
50
  "google-auth-library": "10.5.0"