@opencode-ai/ai 0.0.0-beta-18387 → 0.0.0-beta-18593

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.
Files changed (54) hide show
  1. package/README.md +29 -11
  2. package/dist/image.js +5 -4
  3. package/dist/llm.d.ts +2 -0
  4. package/dist/llm.js +4 -7
  5. package/dist/protocols/anthropic-messages.js +7 -5
  6. package/dist/protocols/bedrock-converse.d.ts +1 -0
  7. package/dist/protocols/bedrock-converse.js +17 -7
  8. package/dist/protocols/bedrock-event-stream.js +16 -6
  9. package/dist/protocols/gemini.d.ts +1 -0
  10. package/dist/protocols/gemini.js +13 -1
  11. package/dist/protocols/google-images.js +8 -18
  12. package/dist/protocols/open-responses-channel.js +20 -9
  13. package/dist/protocols/open-responses-continuation.js +9 -8
  14. package/dist/protocols/open-responses.d.ts +9 -3
  15. package/dist/protocols/open-responses.js +139 -78
  16. package/dist/protocols/openai-chat.d.ts +3 -1
  17. package/dist/protocols/openai-chat.js +39 -28
  18. package/dist/protocols/openai-compatible-chat.js +1 -2
  19. package/dist/protocols/openai-images.js +11 -16
  20. package/dist/protocols/openai-responses.js +1 -1
  21. package/dist/protocols/shared.d.ts +11 -7
  22. package/dist/protocols/shared.js +31 -18
  23. package/dist/protocols/utils/image-input.d.ts +4 -4
  24. package/dist/protocols/utils/image-input.js +6 -8
  25. package/dist/protocols/utils/lifecycle.d.ts +6 -2
  26. package/dist/protocols/utils/lifecycle.js +11 -7
  27. package/dist/protocols/utils/tool-stream.d.ts +6 -0
  28. package/dist/protocols/xai-images.js +7 -12
  29. package/dist/protocols/zai-images.js +5 -10
  30. package/dist/provider-error.d.ts +4 -4
  31. package/dist/provider-error.js +39 -55
  32. package/dist/providers/groq.d.ts +1 -1
  33. package/dist/providers/groq.js +1 -2
  34. package/dist/providers/openrouter.d.ts +1 -1
  35. package/dist/providers/openrouter.js +1 -2
  36. package/dist/route/auth.js +3 -5
  37. package/dist/route/client.d.ts +2 -0
  38. package/dist/route/client.js +29 -11
  39. package/dist/route/executor.d.ts +9 -5
  40. package/dist/route/executor.js +33 -66
  41. package/dist/route/framing.d.ts +6 -2
  42. package/dist/route/framing.js +5 -0
  43. package/dist/route/transport/http.js +7 -2
  44. package/dist/route/transport/index.d.ts +3 -1
  45. package/dist/route/transport/websocket-channel.d.ts +2 -1
  46. package/dist/route/transport/websocket.d.ts +2 -1
  47. package/dist/route/transport/websocket.js +70 -30
  48. package/dist/schema/errors.d.ts +71 -89
  49. package/dist/schema/errors.js +35 -87
  50. package/dist/schema/events.d.ts +2835 -351
  51. package/dist/schema/events.js +32 -14
  52. package/dist/testing.d.ts +41 -2
  53. package/dist/testing.js +39 -18
  54. package/package.json +5 -5
@@ -2,7 +2,7 @@ import { Effect, Encoding, Schema } from "effect";
2
2
  import { Headers, HttpClientRequest, HttpClientResponse } from "effect/unstable/http";
3
3
  import { ImageModel, GeneratedImage, ImageResponse, } from "../image.js";
4
4
  import { Auth } from "../route/auth.js";
5
- import { InvalidProviderOutputReason, AIError, Usage, mergeHttpOptions, mergeJsonRecords, } from "../schema/index.js";
5
+ import { Usage, mergeHttpOptions, mergeJsonRecords } from "../schema/index.js";
6
6
  import { ProviderShared } from "./shared.js";
7
7
  import { ImageInputs } from "./utils/image-input.js";
8
8
  import { OpenAIImage } from "./utils/openai-image.js";
@@ -35,11 +35,6 @@ const nativeOptions = (options) => {
35
35
  ...native,
36
36
  };
37
37
  };
38
- const invalidOutput = (message) => new AIError({
39
- module: ADAPTER,
40
- method: "generate",
41
- reason: new InvalidProviderOutputReason({ message, route: ADAPTER }),
42
- });
43
38
  const applyQuery = (url, query) => {
44
39
  if (!query)
45
40
  return url;
@@ -53,14 +48,14 @@ export const model = (input) => {
53
48
  generate: Effect.fn("OpenAIImages.generate")(function* (request, execute) {
54
49
  const mask = request.options?.mask;
55
50
  if (mask !== undefined && (request.images?.length ?? 0) === 0)
56
- return yield* ImageInputs.invalid(ADAPTER, "An OpenAI image mask requires at least one input image");
51
+ return yield* ImageInputs.invalid("An OpenAI image mask requires at least one input image");
57
52
  const http = mergeHttpOptions(request.model.http, request.http);
58
53
  const sourceImages = request.images ?? [];
59
54
  const multipartImages = yield* Effect.forEach(sourceImages, (image) => {
60
55
  if (image.type === "bytes")
61
56
  return Effect.succeed({ data: image.data, mediaType: image.mediaType });
62
57
  if (image.type === "url")
63
- return ImageInputs.decodeDataUrl(image.url, ADAPTER);
58
+ return ImageInputs.decodeDataUrl(image.url);
64
59
  return Effect.undefined;
65
60
  });
66
61
  const multipartMask = mask === undefined
@@ -68,7 +63,7 @@ export const model = (input) => {
68
63
  : mask.type === "bytes"
69
64
  ? { data: mask.data, mediaType: mask.mediaType }
70
65
  : mask.type === "url"
71
- ? yield* ImageInputs.decodeDataUrl(mask.url, ADAPTER)
66
+ ? yield* ImageInputs.decodeDataUrl(mask.url)
72
67
  : undefined;
73
68
  const useMultipart = sourceImages.length > 0 &&
74
69
  multipartImages.every((image) => image !== undefined) &&
@@ -111,7 +106,7 @@ export const model = (input) => {
111
106
  return undefined;
112
107
  });
113
108
  if (references.some((image) => image === undefined))
114
- return yield* ImageInputs.invalid(ADAPTER, "OpenAI Images accepts image URLs, data URLs, bytes, and file IDs");
109
+ return yield* ImageInputs.invalid("OpenAI Images accepts image URLs, data URLs, bytes, and file IDs");
115
110
  const maskReference = mask === undefined
116
111
  ? undefined
117
112
  : mask.type === "bytes"
@@ -122,7 +117,7 @@ export const model = (input) => {
122
117
  ? { file_id: mask.id }
123
118
  : undefined;
124
119
  if (mask !== undefined && maskReference === undefined)
125
- return yield* ImageInputs.invalid(ADAPTER, "OpenAI Images accepts masks as URLs, data URLs, bytes, or file IDs");
120
+ return yield* ImageInputs.invalid("OpenAI Images accepts masks as URLs, data URLs, bytes, or file IDs");
126
121
  const requestBody = mergeJsonRecords({
127
122
  model: request.model.id,
128
123
  prompt: request.prompt,
@@ -144,13 +139,13 @@ export const model = (input) => {
144
139
  return ImageModel.make({ id: input.id, provider: "openai", route, http: input.http });
145
140
  };
146
141
  const parseResponse = Effect.fn("OpenAIImages.parseResponse")(function* (response, options, overlay) {
147
- const payload = yield* response.json.pipe(Effect.mapError(() => invalidOutput("Failed to read the OpenAI Images response")));
148
- const decoded = yield* Schema.decodeUnknownEffect(OpenAIImageResponse)(payload).pipe(Effect.mapError(() => invalidOutput("OpenAI Images returned an invalid response")));
142
+ const output = yield* ProviderShared.imageResponse(ADAPTER, "OpenAI Images", response);
143
+ const decoded = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(OpenAIImageResponse))(output.body).pipe(Effect.mapError((cause) => output.invalid("OpenAI Images returned an invalid response", cause)));
149
144
  const requestBody = mergeJsonRecords(nativeOptions(options), overlay);
150
145
  const format = decoded.output_format ?? (typeof requestBody?.output_format === "string" ? requestBody.output_format : "png");
151
146
  const images = yield* Effect.forEach(decoded.data, (item, index) => {
152
147
  if (item.b64_json)
153
- return Effect.fromResult(Encoding.decodeBase64(item.b64_json)).pipe(Effect.mapError(() => invalidOutput(`OpenAI Images result ${index} contains invalid base64 data`)), Effect.map((data) => new GeneratedImage({
148
+ return Effect.fromResult(Encoding.decodeBase64(item.b64_json)).pipe(Effect.mapError((cause) => output.invalid(`OpenAI Images result ${index} contains invalid base64 data`, cause)), Effect.map((data) => new GeneratedImage({
154
149
  mediaType: `image/${format}`,
155
150
  data,
156
151
  providerMetadata: item.revised_prompt === undefined ? undefined : { openai: { revisedPrompt: item.revised_prompt } },
@@ -161,10 +156,10 @@ const parseResponse = Effect.fn("OpenAIImages.parseResponse")(function* (respons
161
156
  data: item.url,
162
157
  providerMetadata: item.revised_prompt === undefined ? undefined : { openai: { revisedPrompt: item.revised_prompt } },
163
158
  }));
164
- return Effect.fail(invalidOutput(`OpenAI Images result ${index} has neither image data nor a URL`));
159
+ return Effect.fail(output.invalid(`OpenAI Images result ${index} has neither image data nor a URL`));
165
160
  });
166
161
  if (images.length === 0)
167
- return yield* invalidOutput("OpenAI Images returned no images");
162
+ return yield* output.invalid("OpenAI Images returned no images");
168
163
  return new ImageResponse({
169
164
  images,
170
165
  usage: decoded.usage === undefined
@@ -115,7 +115,7 @@ const fromRequest = Effect.fn("OpenAIResponses.fromRequest")(function* (request)
115
115
  const hostedToolResult = Effect.fn("OpenAIResponses.hostedToolResult")(function* (item) {
116
116
  const isError = item.error !== undefined && item.error !== null;
117
117
  if (item.type === "image_generation_call" && item.result) {
118
- yield* Effect.fromResult(Encoding.decodeBase64(item.result)).pipe(Effect.mapError(() => ProviderShared.eventError(ADAPTER, "OpenAI Responses returned invalid image base64")));
118
+ yield* Effect.fromResult(Encoding.decodeBase64(item.result)).pipe(Effect.mapError((cause) => ProviderShared.eventError(ADAPTER, "OpenAI Responses returned invalid image base64", undefined, cause)));
119
119
  const format = item.output_format ?? "png";
120
120
  return {
121
121
  type: "content",
@@ -1,6 +1,6 @@
1
1
  import { Tool } from "@opencode-ai/schema/tool";
2
2
  import { Effect, Schema, Stream } from "effect";
3
- import { Headers, HttpClientRequest } from "effect/unstable/http";
3
+ import { Headers, HttpClientRequest, HttpClientResponse } from "effect/unstable/http";
4
4
  import { AIError, type ContentPart, type LLMRequest, type MediaPart, type ToolResultPart } from "../schema/index.js";
5
5
  import { isRecord } from "../utils/record.js";
6
6
  export { isRecord };
@@ -54,7 +54,7 @@ export declare const subtractTokens: (total: number | undefined, subtrahend: num
54
54
  * (e.g. Anthropic, whose `input_tokens` is already non-cached only).
55
55
  */
56
56
  export declare const sumTokens: (...values: ReadonlyArray<number | undefined>) => number | undefined;
57
- export declare const eventError: (route: string, message: string, raw?: string) => AIError;
57
+ export declare const eventError: (route: string, message: string, body?: string, cause?: unknown) => AIError;
58
58
  export declare const parseJson: (route: string, input: string, message: string) => Effect.Effect<unknown, AIError, never>;
59
59
  /**
60
60
  * Join the `text` field of a list of parts with newlines. Used by routes
@@ -117,17 +117,21 @@ export declare const toolResultText: (part: ToolResultPart) => string;
117
117
  export declare const errorText: (error: unknown) => string;
118
118
  /**
119
119
  * `framing` step for Server-Sent Events. Decodes UTF-8, runs the SSE channel
120
- * decoder, optionally filters named events, and drops empty / `[DONE]`
121
- * keep-alive events so the protocol event schema sees one JSON string per
122
- * element. Retry control events are ignored without interrupting the stream.
120
+ * decoder, optionally filters named events, and drops empty events. `[DONE]`
121
+ * is dropped by default or retained for protocols that use it as their stream
122
+ * boundary. Retry control events are ignored without interrupting the stream.
123
123
  * Decoder failures become provider output errors so the public error channel
124
124
  * stays `AIError`.
125
125
  */
126
- export declare const sseFraming: (bytes: Stream.Stream<Uint8Array, AIError>, events?: ReadonlySet<string>) => Stream.Stream<string, AIError>;
126
+ export declare const sseFraming: (bytes: Stream.Stream<Uint8Array, AIError>, events?: ReadonlySet<string>, includeDone?: boolean) => Stream.Stream<string, AIError>;
127
127
  /**
128
128
  * Canonical invalid-request constructor shared by protocol lowering.
129
129
  */
130
- export declare const invalidRequest: (message: string) => AIError;
130
+ export declare const invalidRequest: (message: string, cause?: unknown) => AIError;
131
+ export declare const imageResponse: (route: string, name: string, response: HttpClientResponse.HttpClientResponse) => Effect.Effect<{
132
+ body: string;
133
+ invalid: (message: string, cause?: unknown) => AIError;
134
+ }, AIError, never>;
131
135
  export declare const matchToolChoice: <Auto, None, Required, Tool>(route: string, toolChoice: NonNullable<LLMRequest["toolChoice"]>, cases: {
132
136
  readonly auto: () => Auto;
133
137
  readonly none: () => None;
@@ -2,8 +2,8 @@ import { Buffer } from "node:buffer";
2
2
  import { Tool } from "@opencode-ai/schema/tool";
3
3
  import { Effect, Schema, Stream } from "effect";
4
4
  import * as Sse from "effect/unstable/encoding/Sse";
5
- import { Headers, HttpClientRequest } from "effect/unstable/http";
6
- import { InvalidProviderOutputReason, InvalidRequestReason, AIError, } from "../schema/index.js";
5
+ import { Headers, HttpClientRequest, HttpClientResponse } from "effect/unstable/http";
6
+ import { InvalidProviderOutputError, InvalidRequestError, AIError, HttpContext, } from "../schema/index.js";
7
7
  import { isRecord } from "../utils/record.js";
8
8
  export { isRecord };
9
9
  export const Json = Schema.fromJsonString(Schema.Unknown);
@@ -72,14 +72,12 @@ export const sumTokens = (...values) => {
72
72
  return undefined;
73
73
  return values.reduce((acc, value) => acc + (value ?? 0), 0);
74
74
  };
75
- export const eventError = (route, message, raw) => new AIError({
76
- module: "ProviderShared",
77
- method: "stream",
78
- reason: new InvalidProviderOutputReason({ route, message, raw }),
75
+ export const eventError = (route, message, body, cause) => new AIError({
76
+ reason: new InvalidProviderOutputError({ route, message, body, cause }),
79
77
  });
80
78
  export const parseJson = (route, input, message) => Effect.try({
81
79
  try: () => decodeJson(input),
82
- catch: () => eventError(route, message, input),
80
+ catch: (cause) => eventError(route, message, input, cause),
83
81
  });
84
82
  /**
85
83
  * Join the `text` field of a list of parts with newlines. Used by routes
@@ -160,13 +158,13 @@ export const errorText = (error) => {
160
158
  };
161
159
  /**
162
160
  * `framing` step for Server-Sent Events. Decodes UTF-8, runs the SSE channel
163
- * decoder, optionally filters named events, and drops empty / `[DONE]`
164
- * keep-alive events so the protocol event schema sees one JSON string per
165
- * element. Retry control events are ignored without interrupting the stream.
161
+ * decoder, optionally filters named events, and drops empty events. `[DONE]`
162
+ * is dropped by default or retained for protocols that use it as their stream
163
+ * boundary. Retry control events are ignored without interrupting the stream.
166
164
  * Decoder failures become provider output errors so the public error channel
167
165
  * stays `AIError`.
168
166
  */
169
- export const sseFraming = (bytes, events) => bytes.pipe(Stream.decodeText(), Stream.mapAccumEffect(() => {
167
+ export const sseFraming = (bytes, events, includeDone = false) => bytes.pipe(Stream.decodeText(), Stream.mapAccumEffect(() => {
170
168
  const output = [];
171
169
  return {
172
170
  output,
@@ -178,18 +176,33 @@ export const sseFraming = (bytes, events) => bytes.pipe(Stream.decodeText(), Str
178
176
  }, (state, chunk) => Effect.gen(function* () {
179
177
  const error = state.parser.feed(chunk);
180
178
  if (error)
181
- return yield* eventError("sse", error.message);
179
+ return yield* eventError("sse", error.message, chunk, error);
182
180
  return [state, state.output.splice(0)];
183
181
  })), Stream.filter((event) => (events === undefined || events.has(event.event)) &&
184
182
  event.data.length > 0 &&
185
- (event.data !== "[DONE]" || (events !== undefined && event.event !== "message"))), Stream.map((event) => event.data));
183
+ (event.data !== "[DONE]" || includeDone || (events !== undefined && event.event !== "message"))), Stream.map((event) => event.data));
186
184
  /**
187
185
  * Canonical invalid-request constructor shared by protocol lowering.
188
186
  */
189
- export const invalidRequest = (message) => new AIError({
190
- module: "ProviderShared",
191
- method: "request",
192
- reason: new InvalidRequestReason({ message }),
187
+ export const invalidRequest = (message, cause) => new AIError({
188
+ reason: new InvalidRequestError({ message, cause }),
189
+ });
190
+ export const imageResponse = Effect.fn("ProviderShared.imageResponse")(function* (route, name, response) {
191
+ const http = new HttpContext({ url: response.request.url, status: response.status, headers: response.headers });
192
+ const body = yield* response.text.pipe(Effect.mapError((cause) => new AIError({
193
+ reason: new InvalidProviderOutputError({
194
+ route,
195
+ message: `Failed to read the ${name} response`,
196
+ http,
197
+ cause,
198
+ }),
199
+ })));
200
+ return {
201
+ body,
202
+ invalid: (message, cause) => new AIError({
203
+ reason: new InvalidProviderOutputError({ route, message, body, http, cause }),
204
+ }),
205
+ };
193
206
  });
194
207
  export const matchToolChoice = (route, toolChoice, cases) => Effect.gen(function* () {
195
208
  if (toolChoice.type === "auto")
@@ -217,7 +230,7 @@ export const unsupportedContent = (route, role, types) => invalidRequest(`${rout
217
230
  * invalid(e.message)))`. Any decode error is translated into
218
231
  * `AIError` carrying the original parse-error message.
219
232
  */
220
- export const validateWith = (decode) => (payload) => decode(payload).pipe(Effect.mapError((error) => invalidRequest(error.message)));
233
+ export const validateWith = (decode) => (payload) => decode(payload).pipe(Effect.mapError((error) => invalidRequest(error.message, error)));
221
234
  /**
222
235
  * Build an HTTP POST with a JSON body. Sets `content-type: application/json`
223
236
  * automatically after caller-supplied headers so routes cannot accidentally
@@ -4,18 +4,18 @@ import { AIError } from "../../schema/index.js";
4
4
  export declare const dataUrl: (input: Extract<ImageInput, {
5
5
  readonly type: "bytes";
6
6
  }>) => string;
7
- export declare const decodeDataUrl: (url: string, module: string) => Effect.Effect<{
7
+ export declare const decodeDataUrl: (url: string) => Effect.Effect<{
8
8
  readonly mediaType: string;
9
9
  readonly data: Uint8Array;
10
10
  } | undefined, AIError>;
11
- export declare const invalidImageInput: (module: string, message: string) => AIError;
11
+ export declare const invalidImageInput: (message: string, cause?: unknown) => AIError;
12
12
  export declare const ImageInputs: {
13
13
  readonly dataUrl: (input: Extract<ImageInput, {
14
14
  readonly type: "bytes";
15
15
  }>) => string;
16
- readonly decodeDataUrl: (url: string, module: string) => Effect.Effect<{
16
+ readonly decodeDataUrl: (url: string) => Effect.Effect<{
17
17
  readonly mediaType: string;
18
18
  readonly data: Uint8Array;
19
19
  } | undefined, AIError>;
20
- readonly invalid: (module: string, message: string) => AIError;
20
+ readonly invalid: (message: string, cause?: unknown) => AIError;
21
21
  };
@@ -1,18 +1,16 @@
1
1
  import { Effect, Encoding } from "effect";
2
- import { InvalidRequestReason, AIError } from "../../schema/index.js";
3
- const invalid = (module, message) => new AIError({
4
- module,
5
- method: "generate",
6
- reason: new InvalidRequestReason({ message }),
2
+ import { InvalidRequestError, AIError } from "../../schema/index.js";
3
+ const invalid = (message, cause) => new AIError({
4
+ reason: new InvalidRequestError({ message, cause }),
7
5
  });
8
6
  export const dataUrl = (input) => `data:${input.mediaType};base64,${Encoding.encodeBase64(input.data)}`;
9
- export const decodeDataUrl = (url, module) => {
7
+ export const decodeDataUrl = (url) => {
10
8
  if (!url.startsWith("data:"))
11
9
  return Effect.undefined;
12
10
  const match = /^data:([^;,]+);base64,(.*)$/s.exec(url);
13
11
  if (!match)
14
- return Effect.fail(invalid(module, "Image data URLs must contain a MIME type and base64 data"));
15
- return Effect.fromResult(Encoding.decodeBase64(match[2])).pipe(Effect.mapError(() => invalid(module, "Image data URL contains invalid base64 data")), Effect.map((data) => ({ mediaType: match[1], data })));
12
+ return Effect.fail(invalid("Image data URLs must contain a MIME type and base64 data"));
13
+ return Effect.fromResult(Encoding.decodeBase64(match[2])).pipe(Effect.mapError((cause) => invalid("Image data URL contains invalid base64 data", cause)), Effect.map((data) => ({ mediaType: match[1], data })));
16
14
  };
17
15
  export const invalidImageInput = invalid;
18
16
  export const ImageInputs = {
@@ -10,8 +10,12 @@ export declare const textStart: (state: State, events: LLMEvent[], id: string, p
10
10
  export declare const textDelta: (state: State, events: LLMEvent[], id: string, text: string, providerMetadata?: ProviderMetadata) => State;
11
11
  export declare const reasoningStart: (state: State, events: LLMEvent[], id: string, providerMetadata?: ProviderMetadata) => State;
12
12
  export declare const reasoningDelta: (state: State, events: LLMEvent[], id: string, text: string, providerMetadata?: ProviderMetadata) => State;
13
- export declare const reasoningEnd: (state: State, events: LLMEvent[], id: string, providerMetadata?: ProviderMetadata) => State;
14
- export declare const textEnd: (state: State, events: LLMEvent[], id: string, providerMetadata?: ProviderMetadata) => State;
13
+ export declare const reasoningEnd: (state: State, events: LLMEvent[], id: string, providerMetadata?: ProviderMetadata,
14
+ /** Authoritative complete value; replaces accumulated deltas when present. */
15
+ text?: string) => State;
16
+ export declare const textEnd: (state: State, events: LLMEvent[], id: string, providerMetadata?: ProviderMetadata,
17
+ /** Authoritative complete value; replaces accumulated deltas when present. */
18
+ text?: string) => State;
15
19
  export declare const finish: (state: State, events: LLMEvent[], input: {
16
20
  readonly reason: FinishReasonDetails;
17
21
  readonly usage?: Usage;
@@ -30,23 +30,27 @@ export const reasoningDelta = (state, events, id, text, providerMetadata) => {
30
30
  events.push(LLMEvent.reasoningDelta({ id, text, providerMetadata }));
31
31
  return started;
32
32
  };
33
- export const reasoningEnd = (state, events, id, providerMetadata) => {
33
+ export const reasoningEnd = (state, events, id, providerMetadata,
34
+ /** Authoritative complete value; replaces accumulated deltas when present. */
35
+ text) => {
34
36
  if (!state.reasoning.has(id))
35
37
  return state;
36
38
  const stepped = stepStart(state, events);
37
- events.push(LLMEvent.reasoningEnd({ id, providerMetadata }));
39
+ events.push(LLMEvent.reasoningEnd({ id, text, providerMetadata }));
38
40
  const reasoning = new Set(stepped.reasoning);
39
41
  reasoning.delete(id);
40
42
  return { ...stepped, reasoning };
41
43
  };
42
- export const textEnd = (state, events, id, providerMetadata) => {
44
+ export const textEnd = (state, events, id, providerMetadata,
45
+ /** Authoritative complete value; replaces accumulated deltas when present. */
46
+ text) => {
43
47
  if (!state.text.has(id))
44
48
  return state;
45
49
  const stepped = stepStart(state, events);
46
- events.push(LLMEvent.textEnd({ id, providerMetadata }));
47
- const text = new Set(stepped.text);
48
- text.delete(id);
49
- return { ...stepped, text };
50
+ events.push(LLMEvent.textEnd({ id, text, providerMetadata }));
51
+ const open = new Set(stepped.text);
52
+ open.delete(id);
53
+ return { ...stepped, text: open };
50
54
  };
51
55
  const closeOpenBlocks = (state, events) => {
52
56
  for (const id of state.reasoning)
@@ -93,6 +93,7 @@ export declare const finish: <K extends StreamKey>(route: string, tools: State<K
93
93
  } | {
94
94
  readonly id: string;
95
95
  readonly type: "text-end";
96
+ readonly text?: string | undefined;
96
97
  readonly providerMetadata?: {
97
98
  readonly [x: string]: {
98
99
  readonly [x: string]: unknown;
@@ -118,6 +119,7 @@ export declare const finish: <K extends StreamKey>(route: string, tools: State<K
118
119
  } | {
119
120
  readonly id: string;
120
121
  readonly type: "reasoning-end";
122
+ readonly text?: string | undefined;
121
123
  readonly providerMetadata?: {
122
124
  readonly [x: string]: {
123
125
  readonly [x: string]: unknown;
@@ -287,6 +289,7 @@ export declare const finishWithInput: <K extends StreamKey>(route: string, tools
287
289
  } | {
288
290
  readonly id: string;
289
291
  readonly type: "text-end";
292
+ readonly text?: string | undefined;
290
293
  readonly providerMetadata?: {
291
294
  readonly [x: string]: {
292
295
  readonly [x: string]: unknown;
@@ -312,6 +315,7 @@ export declare const finishWithInput: <K extends StreamKey>(route: string, tools
312
315
  } | {
313
316
  readonly id: string;
314
317
  readonly type: "reasoning-end";
318
+ readonly text?: string | undefined;
315
319
  readonly providerMetadata?: {
316
320
  readonly [x: string]: {
317
321
  readonly [x: string]: unknown;
@@ -478,6 +482,7 @@ export declare const finishAll: <K extends StreamKey>(route: string, tools: Stat
478
482
  } | {
479
483
  readonly id: string;
480
484
  readonly type: "text-end";
485
+ readonly text?: string | undefined;
481
486
  readonly providerMetadata?: {
482
487
  readonly [x: string]: {
483
488
  readonly [x: string]: unknown;
@@ -503,6 +508,7 @@ export declare const finishAll: <K extends StreamKey>(route: string, tools: Stat
503
508
  } | {
504
509
  readonly id: string;
505
510
  readonly type: "reasoning-end";
511
+ readonly text?: string | undefined;
506
512
  readonly providerMetadata?: {
507
513
  readonly [x: string]: {
508
514
  readonly [x: string]: unknown;
@@ -2,7 +2,7 @@ import { Effect, Encoding, Schema } from "effect";
2
2
  import { Headers, HttpClientRequest } from "effect/unstable/http";
3
3
  import { GeneratedImage, ImageModel, ImageResponse } from "../image.js";
4
4
  import { Auth } from "../route/auth.js";
5
- import { InvalidProviderOutputReason, AIError, Usage, mergeHttpOptions, mergeJsonRecords, } from "../schema/index.js";
5
+ import { Usage, mergeHttpOptions, mergeJsonRecords } from "../schema/index.js";
6
6
  import { ProviderShared, optionalNull } from "./shared.js";
7
7
  import { ImageInputs } from "./utils/image-input.js";
8
8
  const ADAPTER = "xai-images";
@@ -28,11 +28,6 @@ const nativeOptions = (options) => {
28
28
  ...native,
29
29
  };
30
30
  };
31
- const invalidOutput = (message) => new AIError({
32
- module: ADAPTER,
33
- method: "generate",
34
- reason: new InvalidProviderOutputReason({ message, route: ADAPTER }),
35
- });
36
31
  const applyQuery = (url, query) => {
37
32
  if (!query)
38
33
  return url;
@@ -55,7 +50,7 @@ export const model = (input) => {
55
50
  return undefined;
56
51
  });
57
52
  if (imageReferences.some((image) => image === undefined))
58
- return yield* ImageInputs.invalid(ADAPTER, "xAI Images accepts image URLs, data URLs, bytes, and file IDs");
53
+ return yield* ImageInputs.invalid("xAI Images accepts image URLs, data URLs, bytes, and file IDs");
59
54
  const requestBody = mergeJsonRecords({
60
55
  model: request.model.id,
61
56
  prompt: request.prompt,
@@ -72,12 +67,12 @@ export const model = (input) => {
72
67
  headers: Headers.fromInput({ ...input.headers, ...http?.headers }),
73
68
  });
74
69
  const response = yield* execute(HttpClientRequest.post(url).pipe(HttpClientRequest.setHeaders(headers), HttpClientRequest.bodyText(text, "application/json")));
75
- const payload = yield* response.json.pipe(Effect.mapError(() => invalidOutput("Failed to read the xAI Images response")));
76
- const decoded = yield* Schema.decodeUnknownEffect(XAIImageResponse)(payload).pipe(Effect.mapError(() => invalidOutput("xAI Images returned an invalid response")));
70
+ const output = yield* ProviderShared.imageResponse(ADAPTER, "xAI Images", response);
71
+ const decoded = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(XAIImageResponse))(output.body).pipe(Effect.mapError((cause) => output.invalid("xAI Images returned an invalid response", cause)));
77
72
  const images = yield* Effect.forEach(decoded.data, (item, index) => {
78
73
  const mediaType = item.mime_type ?? "application/octet-stream";
79
74
  if (item.b64_json)
80
- return Effect.fromResult(Encoding.decodeBase64(item.b64_json)).pipe(Effect.mapError(() => invalidOutput(`xAI Images result ${index} contains invalid base64 data`)), Effect.map((data) => new GeneratedImage({
75
+ return Effect.fromResult(Encoding.decodeBase64(item.b64_json)).pipe(Effect.mapError((cause) => output.invalid(`xAI Images result ${index} contains invalid base64 data`, cause)), Effect.map((data) => new GeneratedImage({
81
76
  mediaType,
82
77
  data,
83
78
  providerMetadata: item.revised_prompt === undefined || item.revised_prompt === null
@@ -92,10 +87,10 @@ export const model = (input) => {
92
87
  ? undefined
93
88
  : { xai: { revisedPrompt: item.revised_prompt } },
94
89
  }));
95
- return Effect.fail(invalidOutput(`xAI Images result ${index} has neither image data nor a URL`));
90
+ return Effect.fail(output.invalid(`xAI Images result ${index} has neither image data nor a URL`));
96
91
  });
97
92
  if (images.length === 0)
98
- return yield* invalidOutput("xAI Images returned no images");
93
+ return yield* output.invalid("xAI Images returned no images");
99
94
  const usage = ProviderShared.isRecord(decoded.usage) ? decoded.usage : undefined;
100
95
  return new ImageResponse({
101
96
  images,
@@ -2,7 +2,7 @@ import { Effect, Schema } from "effect";
2
2
  import { Headers, HttpClientRequest } from "effect/unstable/http";
3
3
  import { GeneratedImage, ImageModel, ImageResponse } from "../image.js";
4
4
  import { Auth } from "../route/auth.js";
5
- import { InvalidProviderOutputReason, AIError, mergeHttpOptions, mergeJsonRecords, } from "../schema/index.js";
5
+ import { mergeHttpOptions, mergeJsonRecords } from "../schema/index.js";
6
6
  import { ProviderShared } from "./shared.js";
7
7
  import { ImageInputs } from "./utils/image-input.js";
8
8
  const ADAPTER = "zai-images";
@@ -27,11 +27,6 @@ const nativeOptions = (options) => {
27
27
  ...native,
28
28
  };
29
29
  };
30
- const invalidOutput = (message) => new AIError({
31
- module: ADAPTER,
32
- method: "generate",
33
- reason: new InvalidProviderOutputReason({ message, route: ADAPTER }),
34
- });
35
30
  const applyQuery = (url, query) => {
36
31
  if (!query)
37
32
  return url;
@@ -44,7 +39,7 @@ export const model = (input) => {
44
39
  id: ADAPTER,
45
40
  generate: Effect.fn("ZAIImages.generate")(function* (request, execute) {
46
41
  if ((request.images?.length ?? 0) > 0)
47
- return yield* ImageInputs.invalid(ADAPTER, "Z.ai hosted image generation does not support image inputs");
42
+ return yield* ImageInputs.invalid("Z.ai hosted image generation does not support image inputs");
48
43
  const http = mergeHttpOptions(request.model.http, request.http);
49
44
  const requestBody = mergeJsonRecords({ model: request.model.id, prompt: request.prompt }, nativeOptions(request.options), http?.body);
50
45
  const text = ProviderShared.encodeJson(requestBody);
@@ -57,10 +52,10 @@ export const model = (input) => {
57
52
  headers: Headers.fromInput({ ...input.headers, ...http?.headers }),
58
53
  });
59
54
  const response = yield* execute(HttpClientRequest.post(url).pipe(HttpClientRequest.setHeaders(headers), HttpClientRequest.bodyText(text, "application/json")));
60
- const payload = yield* response.json.pipe(Effect.mapError(() => invalidOutput("Failed to read the Z.ai Images response")));
61
- const decoded = yield* Schema.decodeUnknownEffect(ZAIImageResponse)(payload).pipe(Effect.mapError(() => invalidOutput("Z.ai Images returned an invalid response")));
55
+ const output = yield* ProviderShared.imageResponse(ADAPTER, "Z.ai Images", response);
56
+ const decoded = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(ZAIImageResponse))(output.body).pipe(Effect.mapError((cause) => output.invalid("Z.ai Images returned an invalid response", cause)));
62
57
  if (decoded.data.length === 0)
63
- return yield* invalidOutput("Z.ai Images returned no images");
58
+ return yield* output.invalid("Z.ai Images returned no images");
64
59
  return new ImageResponse({
65
60
  images: decoded.data.map((item) => new GeneratedImage({
66
61
  mediaType: "application/octet-stream",
@@ -1,15 +1,15 @@
1
- import { AIError, type HttpContext, type HttpRateLimitDetails, type ProviderMetadata } from "./schema/index.js";
1
+ import { AIError, type HttpContext, type HttpRateLimitDetails } from "./schema/index.js";
2
2
  export declare const isContextOverflow: (message: string) => boolean;
3
3
  export declare const isPayloadTooLarge: (message: string) => boolean;
4
4
  export declare const isContextOverflowFailure: (failure: unknown) => boolean;
5
5
  export interface ProviderFailure {
6
6
  readonly message: string;
7
7
  readonly status?: number | undefined;
8
- readonly code?: string | undefined;
9
8
  readonly rawBody?: string | undefined;
9
+ readonly data?: unknown;
10
+ readonly http?: HttpContext | undefined;
11
+ readonly cause?: unknown;
10
12
  readonly retryAfterMs?: number | undefined;
11
13
  readonly rateLimit?: HttpRateLimitDetails | undefined;
12
- readonly http?: HttpContext | undefined;
13
- readonly providerMetadata?: ProviderMetadata | undefined;
14
14
  }
15
15
  export declare function classifyProviderFailure(input: ProviderFailure): AIError["reason"];