@opencode-ai/ai 0.0.0-dev-18409 → 0.0.0-dev-18412

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 (36) hide show
  1. package/dist/image.js +5 -4
  2. package/dist/llm.js +4 -7
  3. package/dist/protocols/anthropic-messages.js +7 -5
  4. package/dist/protocols/bedrock-converse.js +4 -6
  5. package/dist/protocols/bedrock-event-stream.js +16 -6
  6. package/dist/protocols/gemini.d.ts +1 -0
  7. package/dist/protocols/gemini.js +13 -1
  8. package/dist/protocols/google-images.js +8 -18
  9. package/dist/protocols/open-responses-channel.js +20 -9
  10. package/dist/protocols/open-responses-continuation.js +9 -8
  11. package/dist/protocols/open-responses.d.ts +1 -1
  12. package/dist/protocols/open-responses.js +7 -16
  13. package/dist/protocols/openai-chat.js +23 -18
  14. package/dist/protocols/openai-images.js +11 -16
  15. package/dist/protocols/openai-responses.js +1 -1
  16. package/dist/protocols/shared.d.ts +7 -3
  17. package/dist/protocols/shared.js +26 -13
  18. package/dist/protocols/utils/image-input.d.ts +4 -4
  19. package/dist/protocols/utils/image-input.js +6 -8
  20. package/dist/protocols/xai-images.js +7 -12
  21. package/dist/protocols/zai-images.js +5 -10
  22. package/dist/provider-error.d.ts +4 -4
  23. package/dist/provider-error.js +31 -32
  24. package/dist/route/auth.js +3 -5
  25. package/dist/route/client.js +29 -11
  26. package/dist/route/executor.d.ts +9 -5
  27. package/dist/route/executor.js +33 -66
  28. package/dist/route/framing.d.ts +2 -0
  29. package/dist/route/transport/http.js +7 -2
  30. package/dist/route/transport/index.d.ts +3 -1
  31. package/dist/route/transport/websocket-channel.d.ts +2 -1
  32. package/dist/route/transport/websocket.d.ts +2 -1
  33. package/dist/route/transport/websocket.js +70 -30
  34. package/dist/schema/errors.d.ts +71 -88
  35. package/dist/schema/errors.js +36 -85
  36. package/package.json +3 -3
package/dist/image.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Effect, Schema } from "effect";
2
- import { HttpOptions, InvalidRequestReason, AIError, ModelID, ProviderID, ProviderMetadata, Usage, } from "./schema/index.js";
2
+ import { HttpOptions, InvalidRequestError, AIError, ModelID, ProviderID, ProviderMetadata, Usage, } from "./schema/index.js";
3
3
  import { ImageClient, Service } from "./image-client.js";
4
4
  export class ImageModel {
5
5
  id;
@@ -90,9 +90,10 @@ export function generate(input) {
90
90
  return Effect.try({
91
91
  try: () => (input instanceof ImageRequest ? input : request(input)),
92
92
  catch: (error) => new AIError({
93
- module: "Image",
94
- method: "generate",
95
- reason: new InvalidRequestReason({ message: error instanceof Error ? error.message : String(error) }),
93
+ reason: new InvalidRequestError({
94
+ message: error instanceof Error ? error.message : String(error),
95
+ cause: error,
96
+ }),
96
97
  }),
97
98
  }).pipe(Effect.flatMap((request) => ImageClient.generate(request)));
98
99
  }
package/dist/llm.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Effect, JsonSchema, Schema } from "effect";
2
2
  import { LLMClient, Service } from "./route/client.js";
3
- import { GenerationOptions, HttpOptions, InvalidProviderOutputReason, AIError, LLMEvent, LLMRequest, LLMResponse, Message, LanguageModel, SystemPart, ToolChoice, ToolDefinition, } from "./schema/index.js";
3
+ import { GenerationOptions, HttpOptions, InvalidProviderOutputError, AIError, LLMEvent, LLMRequest, LLMResponse, Message, LanguageModel, SystemPart, ToolChoice, ToolDefinition, } from "./schema/index.js";
4
4
  import { make as makeTool, toDefinitions } from "./tool.js";
5
5
  export const generate = LLMClient.generate;
6
6
  export const stream = LLMClient.stream;
@@ -43,17 +43,14 @@ const runGenerateObject = Effect.fn("LLM.generateObject")(function* (options, to
43
43
  const call = response.toolCalls.find((event) => LLMEvent.is.toolCall(event) && event.name === GENERATE_OBJECT_TOOL_NAME);
44
44
  if (!call || !LLMEvent.is.toolCall(call))
45
45
  return yield* new AIError({
46
- module: "LLM",
47
- method: "generateObject",
48
- reason: new InvalidProviderOutputReason({
46
+ reason: new InvalidProviderOutputError({
49
47
  message: `generateObject: model did not call the forced \`${GENERATE_OBJECT_TOOL_NAME}\` tool`,
50
48
  }),
51
49
  });
52
50
  const object = yield* tool._decode(call.input).pipe(Effect.mapError((error) => new AIError({
53
- module: "LLM",
54
- method: "generateObject",
55
- reason: new InvalidProviderOutputReason({
51
+ reason: new InvalidProviderOutputError({
56
52
  message: `generateObject: tool input failed schema decode: ${error.message}`,
53
+ cause: error,
57
54
  }),
58
55
  })));
59
56
  return new GenerateObjectResponse(object, response);
@@ -1167,11 +1167,13 @@ const providerErrorMessage = (event) => {
1167
1167
  return `${type}: ${message}`;
1168
1168
  return message || type || "Anthropic Messages stream error";
1169
1169
  };
1170
- const onError = (event) => Effect.fail(new AIError({
1171
- module: ADAPTER,
1172
- method: "stream",
1173
- reason: classifyProviderFailure({ message: providerErrorMessage(event), code: event.error?.type }),
1174
- }));
1170
+ const onError = (event) => {
1171
+ const message = providerErrorMessage(event);
1172
+ const body = ProviderShared.encodeJson(event);
1173
+ return Effect.fail(new AIError({
1174
+ reason: classifyProviderFailure({ message, rawBody: body }),
1175
+ }));
1176
+ };
1175
1177
  const isKnownStreamBlockType = (type) => type === "text" ||
1176
1178
  type === "thinking" ||
1177
1179
  type === "redacted_thinking" ||
@@ -507,14 +507,12 @@ const step = (state, event) => Effect.gen(function* () {
507
507
  ];
508
508
  }
509
509
  if (event.exception) {
510
+ const message = event.exception.details.message ?? event.exception.details.originalMessage ?? "Bedrock Converse stream error";
511
+ const body = ProviderShared.encodeJson(event);
510
512
  return yield* new AIError({
511
- module: ADAPTER,
512
- method: "stream",
513
513
  reason: classifyProviderFailure({
514
- message: event.exception.details.message ??
515
- event.exception.details.originalMessage ??
516
- "Bedrock Converse stream error",
517
- code: event.exception.type,
514
+ message,
515
+ rawBody: body,
518
516
  }),
519
517
  });
520
518
  }
@@ -1,6 +1,7 @@
1
1
  import { EventStreamCodec } from "@smithy/eventstream-codec";
2
2
  import { fromUtf8, toUtf8 } from "@smithy/util-utf8";
3
- import { Effect, Stream } from "effect";
3
+ import { Effect, Encoding, Stream } from "effect";
4
+ import { AIError, AIErrorReason } from "../schema/index.js";
4
5
  import { Framing } from "../route/framing.js";
5
6
  import { ProviderShared } from "./shared.js";
6
7
  // Bedrock streams responses using the AWS event stream binary protocol — each
@@ -30,15 +31,17 @@ const consumeFrames = (route) => (state, chunk) => Effect.gen(function* () {
30
31
  break;
31
32
  const decoded = yield* Effect.try({
32
33
  try: () => eventCodec.decode(view.subarray(0, totalLength)),
33
- catch: (error) => ProviderShared.eventError(route, `Failed to decode Bedrock Converse event-stream frame: ${error instanceof Error ? error.message : String(error)}`),
34
+ catch: (error) => ProviderShared.eventError(route, `Failed to decode Bedrock Converse event-stream frame: ${error instanceof Error ? error.message : String(error)}`, Encoding.encodeBase64(view.subarray(0, totalLength)), error),
34
35
  });
35
36
  cursor = { buffer: cursor.buffer, offset: cursor.offset + totalLength };
37
+ const payload = utf8.decode(decoded.body);
38
+ const body = ProviderShared.encodeJson({ headers: decoded.headers, body: payload });
36
39
  const messageType = decoded.headers[":message-type"]?.value;
37
40
  if (messageType === "error") {
38
41
  const code = decoded.headers[":error-code"]?.value;
39
42
  const message = decoded.headers[":error-message"]?.value;
40
43
  return yield* ProviderShared.eventError(route, [code, message].filter((value) => typeof value === "string").join(": ") ||
41
- "Bedrock Converse event-stream error");
44
+ "Bedrock Converse event-stream error", body);
42
45
  }
43
46
  const eventType = messageType === "event"
44
47
  ? decoded.headers[":event-type"]?.value
@@ -47,16 +50,22 @@ const consumeFrames = (route) => (state, chunk) => Effect.gen(function* () {
47
50
  : undefined;
48
51
  if (typeof eventType !== "string")
49
52
  continue;
50
- const payload = utf8.decode(decoded.body);
51
53
  if (!payload)
52
54
  continue;
53
55
  // The AWS event stream pads short payloads with a `p` field. Drop it
54
56
  // before handing the object to the chunk schema. JSON decode goes
55
57
  // through the shared Schema-driven codec to satisfy the package rule
56
58
  // against ad-hoc `JSON.parse` calls.
57
- const parsed = (yield* ProviderShared.parseJson(route, payload, "Failed to parse Bedrock Converse event-stream payload"));
59
+ const parsed = (yield* ProviderShared.parseJson(route, payload, "Failed to parse Bedrock Converse event-stream payload").pipe(Effect.mapError((error) => new AIError({
60
+ reason: AIErrorReason.make({ ...error.reason, message: error.message, cause: error.reason.cause, body }),
61
+ }))));
58
62
  delete parsed.p;
59
- out.push(messageType === "exception" ? { exception: { type: eventType, details: parsed } } : { [eventType]: parsed });
63
+ out.push({
64
+ ...(messageType === "exception"
65
+ ? { exception: { type: eventType, details: parsed } }
66
+ : { [eventType]: parsed }),
67
+ rawBody: body,
68
+ });
60
69
  }
61
70
  return [cursor, out];
62
71
  });
@@ -68,6 +77,7 @@ const consumeFrames = (route) => (state, chunk) => Effect.gen(function* () {
68
77
  */
69
78
  export const framing = (route) => ({
70
79
  id: "aws-event-stream",
80
+ body: (frame) => ("rawBody" in frame && typeof frame.rawBody === "string" ? frame.rawBody : undefined),
71
81
  frame: (bytes) => bytes.pipe(Stream.mapAccumEffect(() => initialFrameBuffer, consumeFrames(route))),
72
82
  });
73
83
  export * as BedrockEventStream from "./bedrock-event-stream.js";
@@ -175,6 +175,7 @@ export declare const protocol: Protocol<{
175
175
  readonly maxOutputTokens?: number | undefined;
176
176
  } | undefined;
177
177
  }, string, {
178
+ readonly error?: unknown;
178
179
  readonly candidates?: readonly {
179
180
  readonly content?: {
180
181
  readonly parts?: readonly unknown[] | null | undefined;
@@ -5,7 +5,8 @@ import { Auth } from "../route/auth.js";
5
5
  import { Endpoint } from "../route/endpoint.js";
6
6
  import { Framing } from "../route/framing.js";
7
7
  import { Protocol } from "../route/protocol.js";
8
- import { LLMEvent, Usage, } from "../schema/index.js";
8
+ import { AIError, LLMEvent, Usage, } from "../schema/index.js";
9
+ import { classifyProviderFailure } from "../provider-error.js";
9
10
  import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared.js";
10
11
  import { GeminiToolSchema } from "./utils/gemini-tool-schema.js";
11
12
  import { Lifecycle } from "./utils/lifecycle.js";
@@ -151,6 +152,7 @@ const GeminiPromptFeedback = Schema.StructWithRest(Schema.Struct({
151
152
  safetyRatings: optionalNull(Schema.Unknown),
152
153
  }), [Schema.Record(Schema.String, Schema.Unknown)]);
153
154
  const GeminiEvent = Schema.Struct({
155
+ error: Schema.optional(Schema.Unknown),
154
156
  candidates: optionalNull(Schema.Array(GeminiCandidate)),
155
157
  promptFeedback: optionalNull(GeminiPromptFeedback),
156
158
  usageMetadata: optionalNull(GeminiUsage),
@@ -479,6 +481,16 @@ const finish = (state) => {
479
481
  return events;
480
482
  };
481
483
  const step = (state, event) => {
484
+ if (ProviderShared.isRecord(event.error) && typeof event.error.message === "string") {
485
+ const body = ProviderShared.encodeJson(event);
486
+ return Effect.fail(new AIError({
487
+ reason: classifyProviderFailure({
488
+ message: event.error.message,
489
+ status: typeof event.error.code === "number" ? event.error.code : undefined,
490
+ rawBody: body,
491
+ }),
492
+ }));
493
+ }
482
494
  const nextState = {
483
495
  ...state,
484
496
  promptFeedback: event.promptFeedback ?? state.promptFeedback,
@@ -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 { AIError, 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
  const ADAPTER = "google-images";
@@ -58,11 +58,6 @@ const nativeOptions = (options) => {
58
58
  thinkingConfig: Object.values(thinkingConfig).some((value) => value !== undefined) ? thinkingConfig : undefined,
59
59
  }, native) ?? { responseModalities: ["IMAGE"] });
60
60
  };
61
- const invalidOutput = (message, providerMetadata) => new AIError({
62
- module: ADAPTER,
63
- method: "generate",
64
- reason: new InvalidProviderOutputReason({ message, route: ADAPTER, providerMetadata }),
65
- });
66
61
  const applyQuery = (url, query) => {
67
62
  if (!query)
68
63
  return url;
@@ -90,8 +85,8 @@ export const model = (input) => {
90
85
  headers: Headers.fromInput({ ...input.headers, ...http?.headers }),
91
86
  });
92
87
  const response = yield* execute(HttpClientRequest.post(url).pipe(HttpClientRequest.setHeaders(headers), HttpClientRequest.bodyText(text, "application/json")));
93
- const payload = yield* response.json.pipe(Effect.mapError(() => invalidOutput("Failed to read the Google Images response")));
94
- const decoded = yield* Schema.decodeUnknownEffect(GoogleImageResponse)(payload).pipe(Effect.mapError(() => invalidOutput("Google Images returned an invalid response")));
88
+ const output = yield* ProviderShared.imageResponse(ADAPTER, "Google Images", response);
89
+ const decoded = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(GoogleImageResponse))(output.body).pipe(Effect.mapError((cause) => output.invalid("Google Images returned an invalid response", cause)));
95
90
  const candidates = decoded.candidates ?? [];
96
91
  const candidateMetadata = candidates.map((candidate, candidateIndex) => ({
97
92
  index: candidate.index ?? candidateIndex,
@@ -117,7 +112,7 @@ export const model = (input) => {
117
112
  const encoded = candidates.flatMap((candidate, candidateIndex) => (candidate.content?.parts ?? []).flatMap((part, partIndex) => part.inlineData === undefined || part.thought === true
118
113
  ? []
119
114
  : [{ candidate, candidateIndex, partIndex, inlineData: part.inlineData }]));
120
- const images = yield* Effect.forEach(encoded, (item) => Effect.fromResult(Encoding.decodeBase64(item.inlineData.data)).pipe(Effect.mapError(() => invalidOutput(`Google Images candidate ${item.candidateIndex} part ${item.partIndex} contains invalid base64 data`)), Effect.map((data) => new GeneratedImage({
115
+ const images = yield* Effect.forEach(encoded, (item) => Effect.fromResult(Encoding.decodeBase64(item.inlineData.data)).pipe(Effect.mapError((cause) => output.invalid(`Google Images candidate ${item.candidateIndex} part ${item.partIndex} contains invalid base64 data`, cause)), Effect.map((data) => new GeneratedImage({
121
116
  mediaType: item.inlineData.mimeType,
122
117
  data,
123
118
  providerMetadata: {
@@ -134,12 +129,7 @@ export const model = (input) => {
134
129
  }))));
135
130
  if (images.length === 0) {
136
131
  const finishReasons = candidates.flatMap((candidate) => candidate.finishReason === undefined ? [] : [candidate.finishReason]);
137
- return yield* invalidOutput(`Google Images returned no final images${finishReasons.length === 0 ? "" : ` (finish reasons: ${finishReasons.join(", ")})`}; inspect reason.providerMetadata.google for prompt feedback and candidate details`, {
138
- google: {
139
- promptFeedback: decoded.promptFeedback,
140
- candidates: candidateMetadata,
141
- },
142
- });
132
+ return yield* output.invalid(`Google Images returned no final images${finishReasons.length === 0 ? "" : ` (finish reasons: ${finishReasons.join(", ")})`}; inspect body for prompt feedback and candidate details`);
143
133
  }
144
134
  const usage = decoded.usageMetadata;
145
135
  const outputTokens = usage?.candidatesTokenCount === undefined
@@ -177,14 +167,14 @@ const googleImagePart = (image) => {
177
167
  if (image.type === "file-uri")
178
168
  return Effect.succeed({ fileData: { mimeType: image.mediaType, fileUri: image.uri } });
179
169
  if (image.type === "url")
180
- return ImageInputs.decodeDataUrl(image.url, ADAPTER).pipe(Effect.flatMap((decoded) => {
170
+ return ImageInputs.decodeDataUrl(image.url).pipe(Effect.flatMap((decoded) => {
181
171
  if (decoded === undefined)
182
- return Effect.fail(ImageInputs.invalid(ADAPTER, "Google generateContent does not fetch public image URLs; use bytes, a data URL, or a Gemini file URI"));
172
+ return Effect.fail(ImageInputs.invalid("Google generateContent does not fetch public image URLs; use bytes, a data URL, or a Gemini file URI"));
183
173
  return Effect.succeed({
184
174
  inlineData: { mimeType: decoded.mediaType, data: Encoding.encodeBase64(decoded.data) },
185
175
  });
186
176
  }));
187
- return Effect.fail(ImageInputs.invalid(ADAPTER, "Google generateContent requires Gemini file URIs rather than provider file IDs"));
177
+ return Effect.fail(ImageInputs.invalid("Google generateContent requires Gemini file URIs rather than provider file IDs"));
188
178
  };
189
179
  export const GoogleImages = {
190
180
  model,
@@ -28,15 +28,15 @@ const driver = (options, body) => {
28
28
  return { message: body, mode: "full" };
29
29
  }),
30
30
  observe: (_create, frame) => Effect.gen(function* () {
31
- const event = yield* decodeEvent(frame).pipe(Effect.mapError(() => ProviderShared.eventError(options.id, `Invalid ${options.name} WebSocket event`, frame)));
31
+ const event = yield* decodeEvent(frame).pipe(Effect.mapError((cause) => ProviderShared.eventError(options.id, `Invalid ${options.name} WebSocket event`, frame, cause)));
32
32
  if (terminal)
33
33
  return yield* ProviderShared.eventError(options.id, `${options.name} emitted ${event.type} after a terminal event`, frame);
34
34
  if (event.type === "error") {
35
35
  terminal = true;
36
- yield* OpenResponses.decodeKnownErrorEvent(event).pipe(Effect.mapError(() => ProviderShared.eventError(options.id, `${options.name} returned a malformed error event`, frame)));
36
+ yield* OpenResponses.decodeKnownErrorEvent(event).pipe(Effect.mapError((cause) => ProviderShared.eventError(options.id, `${options.name} returned a malformed error event`, frame, cause)));
37
37
  return {
38
38
  type: "provider-failure",
39
- error: OpenResponses.providerFailure(options.id, event, `${options.name} stream error`),
39
+ error: OpenResponses.providerFailure(event, `${options.name} stream error`, frame),
40
40
  };
41
41
  }
42
42
  if (event.type === "response.failed") {
@@ -45,7 +45,7 @@ const driver = (options, body) => {
45
45
  return yield* ProviderShared.eventError(options.id, `${options.name} response ID changed during execution`, frame);
46
46
  return {
47
47
  type: "provider-failure",
48
- error: OpenResponses.providerFailure(options.id, event, `${options.name} response failed`),
48
+ error: OpenResponses.providerFailure(event, `${options.name} response failed`, frame),
49
49
  };
50
50
  }
51
51
  if (event.type === "response.created") {
@@ -110,9 +110,10 @@ export const transport = (options) => {
110
110
  channel,
111
111
  };
112
112
  }),
113
- execute: (prepared, request, runtime, executeOptions) => {
113
+ execute: (prepared, request, runtime, executeOptions) => Effect.gen(function* () {
114
114
  if (!executeOptions?.webSocket || !prepared.channel)
115
- return http.execute(prepared.http, request, runtime);
115
+ return yield* http.execute(prepared.http, request, runtime);
116
+ let fallbackHttp;
116
117
  const exchange = {
117
118
  id: request.id ?? "request",
118
119
  connect: {
@@ -120,11 +121,21 @@ export const transport = (options) => {
120
121
  headers: prepared.channel.headers,
121
122
  rotateAfterMs: prepared.channel.rotateAfterMs,
122
123
  },
123
- fallback: () => Stream.unwrap(http.execute(prepared.http, request, runtime).pipe(Effect.map((execution) => execution.frames))),
124
+ fallback: () => Stream.unwrap(http.execute(prepared.http, request, runtime).pipe(Effect.map((execution) => {
125
+ fallbackHttp = execution.http;
126
+ return execution.frames;
127
+ }))),
124
128
  driver: prepared.channel.driver,
125
129
  };
126
- return executeOptions.webSocket.execute(exchange);
127
- },
130
+ const execution = yield* executeOptions.webSocket.execute(exchange);
131
+ return {
132
+ frames: execution.frames,
133
+ complete: execution.complete,
134
+ get http() {
135
+ return fallbackHttp ?? execution.http;
136
+ },
137
+ };
138
+ }),
128
139
  };
129
140
  };
130
141
  export const OpenResponsesChannel = { transport };
@@ -1,4 +1,4 @@
1
- import { AIError, TransportReason } from "../schema/index.js";
1
+ import { AIError, TransportError } from "../schema/index.js";
2
2
  import { Effect, Option, Schema } from "effect";
3
3
  import * as ProviderShared from "./shared.js";
4
4
  import { OpenResponses } from "./open-responses.js";
@@ -81,14 +81,15 @@ const incremental = (request, checkpoint) => {
81
81
  return input.slice(baseline.length);
82
82
  };
83
83
  const code = (event) => event.code || event.error?.code || event.response?.error?.code || undefined;
84
- const rejected = (input, observation, recovery) => ({
84
+ const rejected = (observation, recovery) => ({
85
85
  type: "rejected",
86
86
  recovery,
87
87
  error: new AIError({
88
- module: input.id,
89
- method: "stream",
90
- reason: new TransportReason({
88
+ reason: new TransportError({
91
89
  message: observation.error.message,
90
+ body: observation.error.reason.body,
91
+ http: observation.error.reason.http,
92
+ cause: observation.error.reason.cause,
92
93
  transport: "websocket",
93
94
  operation: "read",
94
95
  phase: "receive",
@@ -113,16 +114,16 @@ export const driver = (input) => {
113
114
  };
114
115
  }),
115
116
  observe: (create, frame) => Effect.gen(function* () {
116
- const event = yield* decodeEvent(frame).pipe(Effect.mapError(() => ProviderShared.eventError(input.id, `Invalid ${input.name} WebSocket event`, frame)));
117
+ const event = yield* decodeEvent(frame).pipe(Effect.mapError((cause) => ProviderShared.eventError(input.id, `Invalid ${input.name} WebSocket event`, frame, cause)));
117
118
  const observation = yield* input.base.observe(create, frame);
118
119
  if (event.type === "response.output_item.done" && event.item)
119
120
  output.push(event.item);
120
121
  if (observation.type === "provider-failure") {
121
122
  const rejection = code(event);
122
123
  if (rejection === "previous_response_not_found")
123
- return rejected(input, observation, "retry-full");
124
+ return rejected(observation, "retry-full");
124
125
  if (rejection === "websocket_connection_limit_reached")
125
- return rejected(input, observation, "rotate-and-retry-full");
126
+ return rejected(observation, "rotate-and-retry-full");
126
127
  }
127
128
  if (observation.type !== "completed")
128
129
  return observation;
@@ -804,7 +804,7 @@ export declare const terminal: (event: Event) => boolean;
804
804
  export declare const outputItemID: (state: ParserState, event: Event) => string | undefined;
805
805
  export declare const onReasoningDelta: (state: ParserState, event: Event, itemID: string) => StepResult;
806
806
  export declare const onReasoningDone: (state: ParserState, event: Event, itemID: string) => StepResult;
807
- export declare const providerFailure: (id: string, event: Event, fallback: string) => AIError;
807
+ export declare const providerFailure: (event: Event, fallback: string, body?: string) => AIError;
808
808
  export declare const step: (state: ParserState, input: Event) => AIError | Effect.Effect<[ParserState, readonly ({
809
809
  readonly type: "step-start";
810
810
  readonly index: number;
@@ -1,7 +1,7 @@
1
1
  import { Effect, Schema } from "effect";
2
2
  import { HttpTransport } from "../route/transport/index.js";
3
3
  import { Protocol } from "../route/protocol.js";
4
- import { AIError, LLMEvent, ProviderInternalReason, Usage, } from "../schema/index.js";
4
+ import { AIError, LLMEvent, ProviderInternalError, Usage, } from "../schema/index.js";
5
5
  import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared.js";
6
6
  import { classifyProviderFailure } from "../provider-error.js";
7
7
  import { OpenResponsesOptions } from "./utils/open-responses-options.js";
@@ -918,11 +918,8 @@ const providerErrorMessage = (event, nested) => {
918
918
  return `${code}: ${message}`;
919
919
  return message || code;
920
920
  };
921
- export const providerFailure = (id, event, fallback) => {
921
+ export const providerFailure = (event, fallback, body = ProviderShared.encodeJson(event)) => {
922
922
  const nested = event.error ?? event.response?.error ?? undefined;
923
- const code = event.code || nested?.code || undefined;
924
- // Keep the full raw payload on the error even when the message is a summary.
925
- const body = JSON.stringify(nested ?? event) ?? "";
926
923
  const summary = providerErrorMessage(event, nested);
927
924
  const message = summary ?? (body === "{}" ? fallback : body);
928
925
  const status = typeof event.status === "number"
@@ -935,16 +932,10 @@ export const providerFailure = (id, event, fallback) => {
935
932
  event.response === undefined &&
936
933
  summary === undefined &&
937
934
  status === undefined
938
- ? new ProviderInternalReason({ message })
939
- : classifyProviderFailure({ message, code, status, rawBody: body });
940
- return new AIError({
941
- module: id,
942
- method: "stream",
943
- body,
944
- reason,
945
- });
935
+ ? new ProviderInternalError({ message, body })
936
+ : classifyProviderFailure({ message, status, rawBody: body });
937
+ return new AIError({ reason });
946
938
  };
947
- const providerError = (state, event, fallback) => providerFailure(state.id, event, fallback);
948
939
  export const step = (state, input) => {
949
940
  // The OpenAPI requires string IDs but imposes no minLength; empty is not missing.
950
941
  const event = input.item_id !== undefined && outputItemID(state, input) !== input.item_id
@@ -1005,9 +996,9 @@ export const step = (state, input) => {
1005
996
  if (event.type === "response.completed" || event.type === "response.incomplete")
1006
997
  return onResponseFinish(state, event);
1007
998
  if (event.type === "response.failed")
1008
- return providerError(state, event, `${state.name} response failed`);
999
+ return providerFailure(event, `${state.name} response failed`);
1009
1000
  if (event.type === "error")
1010
- return decodeKnownErrorEvent(event).pipe(Effect.mapError(() => ProviderShared.eventError(state.id, `${state.name} returned a malformed error event`)), Effect.flatMap(() => providerError(state, event, `${state.name} stream error`)));
1001
+ return decodeKnownErrorEvent(event).pipe(Effect.mapError((cause) => ProviderShared.eventError(state.id, `${state.name} returned a malformed error event`, ProviderShared.encodeJson(event), cause)), Effect.flatMap(() => providerFailure(event, `${state.name} stream error`)));
1011
1002
  return Effect.succeed([state, NO_EVENTS]);
1012
1003
  };
1013
1004
  // =============================================================================
@@ -5,7 +5,7 @@ import { Auth } from "../route/auth.js";
5
5
  import { Endpoint } from "../route/endpoint.js";
6
6
  import { HttpTransport } from "../route/transport/index.js";
7
7
  import { Protocol } from "../route/protocol.js";
8
- import { AIError, InvalidProviderOutputReason, LLMEvent, ProviderInternalReason, UnknownProviderReason, Usage, } from "../schema/index.js";
8
+ import { AIError, AIErrorReason, InvalidProviderOutputError, LLMEvent, ProviderInternalError, UnknownProviderError, Usage, } from "../schema/index.js";
9
9
  import { classifyProviderFailure } from "../provider-error.js";
10
10
  import { isRecord, JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared.js";
11
11
  import { OpenAIOptions } from "./utils/openai-options.js";
@@ -613,18 +613,22 @@ export const fromRequest = Effect.fn("OpenAIChat.fromRequest")(function* (reques
613
613
  // Streaming parsers are small state machines: every event returns a new state
614
614
  // plus the common `LLMEvent`s produced by that event. Tool calls are accumulated
615
615
  // because OpenAI streams JSON arguments across multiple deltas.
616
- const finishReasonError = (event, reason) => new AIError({
617
- module: ADAPTER,
618
- method: "stream",
619
- body: ProviderShared.encodeJson(event),
620
- reason,
621
- });
622
616
  const mapFinishReason = Effect.fn("OpenAIChat.mapFinishReason")(function* (event, reason) {
623
617
  switch (reason) {
624
618
  case "error":
625
- return yield* finishReasonError(event, new UnknownProviderReason({ message: "Provider reported an error (finish_reason: error)" }));
619
+ return yield* new AIError({
620
+ reason: new UnknownProviderError({
621
+ message: "Provider reported an error (finish_reason: error)",
622
+ body: ProviderShared.encodeJson(event),
623
+ }),
624
+ });
626
625
  case "network_error":
627
- return yield* finishReasonError(event, new ProviderInternalReason({ message: "Provider reported a network error (finish_reason: network_error)" }));
626
+ return yield* new AIError({
627
+ reason: new ProviderInternalError({
628
+ message: "Provider reported a network error (finish_reason: network_error)",
629
+ body: ProviderShared.encodeJson(event),
630
+ }),
631
+ });
628
632
  case "stop":
629
633
  case "end":
630
634
  return "stop";
@@ -738,12 +742,8 @@ const step = (state, event) => Effect.gen(function* () {
738
742
  if (event.error) {
739
743
  const body = ProviderShared.encodeJson(event);
740
744
  return yield* new AIError({
741
- module: ADAPTER,
742
- method: "stream",
743
- body,
744
745
  reason: classifyProviderFailure({
745
746
  message: event.error.message,
746
- code: event.error.code === undefined || event.error.code === null ? undefined : String(event.error.code),
747
747
  status: typeof event.error.code === "number" ? event.error.code : undefined,
748
748
  rawBody: body,
749
749
  }),
@@ -831,7 +831,14 @@ const step = (state, event) => Effect.gen(function* () {
831
831
  }
832
832
  const result = ToolStream.appendOrStart(ADAPTER, tools, index, { id: id || undefined, name: name || undefined, text }, "OpenAI Chat tool call delta is missing id or name");
833
833
  if (ToolStream.isError(result))
834
- return yield* ProviderShared.eventError(ADAPTER, result.reason.message, ProviderShared.encodeJson(event));
834
+ return yield* new AIError({
835
+ reason: AIErrorReason.make({
836
+ ...result.reason,
837
+ message: result.message,
838
+ cause: result.reason.cause,
839
+ body: ProviderShared.encodeJson(event),
840
+ }),
841
+ });
835
842
  tools = result.tools;
836
843
  if (result.events.length)
837
844
  lifecycle = Lifecycle.stepStart(lifecycle, events);
@@ -867,11 +874,9 @@ const step = (state, event) => Effect.gen(function* () {
867
874
  const finishEvents = Effect.fn("OpenAIChat.finishEvents")(function* (state) {
868
875
  if (state.finishReason === undefined && state.requireFinishReason)
869
876
  return yield* new AIError({
870
- module: ADAPTER,
871
- method: "stream",
872
- reason: new InvalidProviderOutputReason({
873
- classification: "incomplete-stream",
877
+ reason: new InvalidProviderOutputError({
874
878
  message: "OpenAI Chat stream ended without finish_reason",
879
+ classification: "incomplete-stream",
875
880
  route: ADAPTER,
876
881
  }),
877
882
  });
@@ -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",