@opencode-ai/ai 0.0.0-dev-18407 → 0.0.0-dev-18411

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
@@ -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
@@ -127,7 +127,11 @@ export declare const sseFraming: (bytes: Stream.Stream<Uint8Array, AIError>, eve
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
@@ -178,7 +176,7 @@ 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 &&
@@ -186,10 +184,25 @@ export const sseFraming = (bytes, events) => bytes.pipe(Stream.decodeText(), Str
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 = {
@@ -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"];
@@ -1,5 +1,5 @@
1
1
  import { Option, Schema } from "effect";
2
- import { AuthenticationReason, ContentPolicyReason, InvalidRequestReason, AIError, ProviderErrorEvent, ProviderInternalReason, QuotaExceededReason, RateLimitReason, UnknownProviderReason, } from "./schema/index.js";
2
+ import { AuthenticationError, ContentPolicyError, InvalidRequestError, AIError, ProviderErrorEvent, ProviderInternalError, QuotaExceededError, RateLimitError, UnknownProviderError, } from "./schema/index.js";
3
3
  const patterns = [
4
4
  /prompt is too long/i,
5
5
  /input is too long for requested model/i,
@@ -57,80 +57,79 @@ const NETWORK_ERROR_TEXT = /network[-_\s]error/i;
57
57
  // Keep HTTP failures and provider-reported stream failures on one typed path so
58
58
  // session retry policy never needs provider-specific string matching.
59
59
  export function classifyProviderFailure(input) {
60
- const body = input.http?.body ?? input.rawBody ?? "";
61
- const codes = [input.code, ...providerCodes(body), ...providerCodes(input.message)]
62
- .filter((code) => code !== undefined)
63
- .map((code) => code.toLowerCase());
60
+ const details = { message: input.message, body: input.rawBody, http: input.http, cause: input.cause };
61
+ const body = input.rawBody ?? "";
62
+ const codes = [...providerCodes(input.data), ...providerCodes(body), ...providerCodes(input.message)].map((code) => code.toLowerCase());
64
63
  // Scan the raw payload too so signals missing from the summary message
65
64
  // (e.g. overflow phrases nested in a JSON error body) still classify.
66
65
  const text = [input.message, body].filter((value) => value.length > 0).join("\n");
67
- const common = { message: input.message, providerMetadata: input.providerMetadata, http: input.http };
68
66
  const clientScoped = input.status === undefined || (input.status >= 400 && input.status < 500);
69
67
  if (clientScoped &&
70
68
  (codes.includes("context_length_exceeded") ||
71
69
  codes.includes("model_context_window_exceeded") ||
72
70
  codes.includes("request_too_large") ||
73
71
  isContextOverflow(text)))
74
- return new InvalidRequestReason({ ...common, classification: "context-overflow" });
72
+ return new InvalidRequestError({ ...details, classification: "context-overflow" });
75
73
  if (input.status === 413 || isPayloadTooLarge(text))
76
- return new InvalidRequestReason({ ...common, classification: "payload-too-large" });
74
+ return new InvalidRequestError({ ...details, classification: "payload-too-large" });
77
75
  if (CONTENT_POLICY_TEXT.test(text))
78
- return new ContentPolicyReason(common);
76
+ return new ContentPolicyError(details);
79
77
  if (codes.some((code) => QUOTA_CODES.has(code)) || (input.status === 429 && QUOTA_TEXT.test(text)))
80
- return new QuotaExceededReason(common);
78
+ return new QuotaExceededError(details);
81
79
  if (input.status === 401)
82
- return new AuthenticationReason({ ...common, kind: "invalid" });
80
+ return new AuthenticationError({ ...details, kind: "invalid" });
83
81
  if (input.status === 403)
84
- return new AuthenticationReason({ ...common, kind: "insufficient-permissions" });
82
+ return new AuthenticationError({ ...details, kind: "insufficient-permissions" });
85
83
  if (codes.includes("authentication_error"))
86
- return new AuthenticationReason({ ...common, kind: "invalid" });
84
+ return new AuthenticationError({ ...details, kind: "invalid" });
87
85
  if (codes.includes("permission_error"))
88
- return new AuthenticationReason({ ...common, kind: "insufficient-permissions" });
86
+ return new AuthenticationError({ ...details, kind: "insufficient-permissions" });
89
87
  if (codes.some((code) => code.includes("rate_limit") || code === "too_many_requests" || code === "throttlingexception"))
90
- return new RateLimitReason({
91
- ...common,
88
+ return new RateLimitError({
89
+ ...details,
92
90
  retryAfterMs: input.retryAfterMs,
93
91
  rateLimit: input.rateLimit,
94
92
  });
95
93
  if (RATE_LIMIT_TEXT.test(text))
96
- return new RateLimitReason({
97
- ...common,
94
+ return new RateLimitError({
95
+ ...details,
98
96
  retryAfterMs: input.retryAfterMs,
99
97
  rateLimit: input.rateLimit,
100
98
  });
101
99
  if (NETWORK_ERROR_TEXT.test(text))
102
- return new ProviderInternalReason({ ...common, status: input.status });
100
+ return new ProviderInternalError(details);
103
101
  if (codes.some((code) => SERVER_CODES.has(code) || code.includes("exhausted") || code.includes("unavailable")))
104
- return new ProviderInternalReason({
105
- ...common,
106
- status: input.status,
102
+ return new ProviderInternalError({
103
+ ...details,
107
104
  retryAfterMs: input.retryAfterMs,
108
105
  });
109
106
  if (input.status === 429) {
110
- return new RateLimitReason({
111
- ...common,
107
+ return new RateLimitError({
108
+ ...details,
112
109
  retryAfterMs: input.retryAfterMs,
113
110
  rateLimit: input.rateLimit,
114
111
  });
115
112
  }
116
113
  if (input.status === 408 || input.status === 409 || (input.status !== undefined && input.status >= 500))
117
- return new ProviderInternalReason({
118
- ...common,
119
- status: input.status,
114
+ return new ProviderInternalError({
115
+ ...details,
120
116
  retryAfterMs: input.retryAfterMs,
121
117
  });
122
118
  if (codes.some((code) => INVALID_REQUEST_CODES.has(code)))
123
- return new InvalidRequestReason(common);
119
+ return new InvalidRequestError(details);
124
120
  if (input.status === 400 || input.status === 404 || input.status === 413 || input.status === 422)
125
- return new InvalidRequestReason(common);
126
- return new UnknownProviderReason({ ...common, status: input.status });
121
+ return new InvalidRequestError(details);
122
+ return new UnknownProviderError(details);
127
123
  }
128
124
  function providerCodes(value) {
129
- const decoded = Option.getOrUndefined(decodeJson(value));
125
+ const decoded = typeof value === "string" ? Option.getOrUndefined(decodeJson(value)) : value;
130
126
  if (!isRecord(decoded))
131
127
  return [];
132
128
  const error = isRecord(decoded.error) ? decoded.error : undefined;
133
- return [decoded.code, error?.code, error?.type].filter((value) => typeof value === "string");
129
+ const response = isRecord(decoded.response) ? decoded.response : undefined;
130
+ const responseError = response && isRecord(response.error) ? response.error : undefined;
131
+ const exception = isRecord(decoded.exception) ? decoded.exception : undefined;
132
+ return [decoded.code, error?.code, error?.type, error?.status, responseError?.code, exception?.type].filter((value) => typeof value === "string");
134
133
  }
135
134
  function isRecord(value) {
136
135
  return typeof value === "object" && value !== null;
@@ -1,6 +1,6 @@
1
1
  import { Config, Effect, Redacted } from "effect";
2
2
  import { Headers } from "effect/unstable/http";
3
- import { AuthenticationReason, InvalidRequestReason, AIError } from "../schema/index.js";
3
+ import { AuthenticationError, InvalidRequestError, AIError } from "../schema/index.js";
4
4
  export class MissingCredentialError extends Error {
5
5
  source;
6
6
  _tag = "MissingCredentialError";
@@ -76,11 +76,9 @@ export function bearerHeader(name, source) {
76
76
  const toAIError = (error) => {
77
77
  if (error instanceof MissingCredentialError || error instanceof Config.ConfigError) {
78
78
  return new AIError({
79
- module: "Auth",
80
- method: "apply",
81
79
  reason: error instanceof MissingCredentialError
82
- ? new AuthenticationReason({ message: error.message, kind: "missing" })
83
- : new InvalidRequestReason({ message: `Failed to resolve auth config: ${error.message}` }),
80
+ ? new AuthenticationError({ message: error.message, cause: error, kind: "missing" })
81
+ : new InvalidRequestError({ message: `Failed to resolve auth config: ${error.message}`, cause: error }),
84
82
  });
85
83
  }
86
84
  return error;
@@ -7,7 +7,7 @@ import { HttpTransport } from "./transport/index.js";
7
7
  import { applyCachePolicy } from "../cache-policy.js";
8
8
  import { sanitizeSurrogates } from "../utils/sanitize.js";
9
9
  import * as ProviderShared from "../protocols/shared.js";
10
- import { AIError, GenerationOptions, HttpOptions, LLMRequest, LLMResponse, LanguageModel, LLMEvent, InvalidProviderOutputReason, ProviderID, mergeGenerationOptions, mergeHttpOptions, mergeProviderOptions, } from "../schema/index.js";
10
+ import { AIError, AIErrorReason, GenerationOptions, HttpOptions, LLMRequest, LLMResponse, LanguageModel, LLMEvent, InvalidProviderOutputError, ProviderID, mergeGenerationOptions, mergeHttpOptions, mergeProviderOptions, } from "../schema/index.js";
11
11
  const makeRouteLanguageModel = (route, mapped) => {
12
12
  const provider = route.provider ?? ("provider" in mapped ? mapped.provider : undefined);
13
13
  if (!provider)
@@ -60,14 +60,12 @@ const streamError = (route, message, cause) => {
60
60
  const failed = cause.reasons.find(Cause.isFailReason)?.error;
61
61
  if (failed instanceof AIError)
62
62
  return failed;
63
- return ProviderShared.eventError(route, message, Cause.pretty(cause));
63
+ return ProviderShared.eventError(route, message, undefined, cause);
64
64
  };
65
65
  const incompleteStreamError = (route) => new AIError({
66
- module: "LLMClient",
67
- method: "stream",
68
- reason: new InvalidProviderOutputReason({
69
- classification: "incomplete-stream",
66
+ reason: new InvalidProviderOutputError({
70
67
  message: "The provider response ended unexpectedly.",
68
+ classification: "incomplete-stream",
71
69
  route,
72
70
  }),
73
71
  });
@@ -85,7 +83,7 @@ function makeFromTransport(input) {
85
83
  const protocol = input.protocol;
86
84
  const encodeBody = Schema.encodeSync(Schema.fromJsonString(protocol.body.schema));
87
85
  const decodeEventEffect = Schema.decodeUnknownEffect(protocol.stream.event);
88
- const decodeEvent = (route) => (frame) => decodeEventEffect(frame).pipe(Effect.mapError(() => ProviderShared.eventError(input.id, `Invalid ${route} stream event`, typeof frame === "string" ? frame : ProviderShared.encodeJson(frame))));
86
+ const decodeEvent = (route) => (frame) => decodeEventEffect(frame).pipe(Effect.mapError((cause) => ProviderShared.eventError(input.id, `Invalid ${route} stream event`, typeof frame === "string" ? frame : ProviderShared.encodeJson(frame), cause)));
89
87
  const build = (routeInput) => {
90
88
  const route = {
91
89
  id: routeInput.id,
@@ -127,18 +125,38 @@ function makeFromTransport(input) {
127
125
  streamPrepared: (prepared, request, runtime, options) => {
128
126
  const route = `${request.model.provider}/${request.model.route.id}`;
129
127
  return Stream.unwrap(routeInput.transport.execute(prepared, request, runtime, options).pipe(Effect.map((execution) => {
130
- const events = execution.frames.pipe(Stream.mapEffect(decodeEvent(route)), protocol.stream.terminal ? Stream.takeUntil(protocol.stream.terminal) : (stream) => stream);
128
+ const terminal = protocol.stream.terminal;
129
+ // Preserve assembled inputs; replace only serialized event fallbacks with their original wire data.
130
+ const frameError = (frame, event = frame) => (error) => new AIError({
131
+ reason: AIErrorReason.make({
132
+ ...error.reason,
133
+ message: error.reason.message,
134
+ cause: error.reason.cause,
135
+ body: error.reason.body !== undefined && error.reason.body !== ProviderShared.encodeJson(event)
136
+ ? error.reason.body
137
+ : (execution.body?.(frame) ??
138
+ (typeof frame === "string" ? frame : ProviderShared.encodeJson(frame))),
139
+ }),
140
+ });
141
+ const events = execution.frames.pipe(Stream.mapEffect((frame) => decodeEvent(route)(frame).pipe(Effect.catchCause((cause) => Effect.fail(streamError(route, `Failed to decode ${route} event`, cause))), Effect.map((event) => ({ event, frame })), Effect.mapError(frameError(frame)))), terminal ? Stream.takeUntil(({ event }) => terminal(event)) : (stream) => stream);
131
142
  const stream = Stream.suspend(() => {
132
143
  let state = protocol.stream.initial(request);
133
- const parsed = events.pipe(Stream.mapEffect((event) => protocol.stream.step(state, event).pipe(Effect.map(([next, output]) => {
144
+ const parsed = events.pipe(Stream.mapEffect(({ event, frame }) => protocol.stream.step(state, event).pipe(Effect.catchCause((cause) => Effect.fail(streamError(route, `Failed to parse ${route} event`, cause))), Effect.map(([next, output]) => {
134
145
  state = next;
135
146
  return output;
136
- }))), Stream.flatMap(Stream.fromIterable));
147
+ }), Effect.mapError(frameError(frame, event)))), Stream.flatMap(Stream.fromIterable));
137
148
  const onHalt = protocol.stream.onHalt;
138
149
  return onHalt
139
150
  ? parsed.pipe(Stream.concat(Stream.suspend(() => Stream.unwrap(onHalt(state).pipe(Effect.map(Stream.fromIterable))))))
140
151
  : parsed;
141
- }).pipe(Stream.catchCause((cause) => Stream.fail(streamError(route, `Failed to read ${route} stream`, cause))), requireTerminalEvent(route));
152
+ }).pipe(Stream.catchCause((cause) => Stream.fail(streamError(route, `Failed to read ${route} stream`, cause))), requireTerminalEvent(route), Stream.mapError((error) => new AIError({
153
+ reason: AIErrorReason.make({
154
+ ...error.reason,
155
+ message: error.reason.message,
156
+ cause: error.reason.cause,
157
+ http: error.reason.http ?? execution.http,
158
+ }),
159
+ })));
142
160
  return execution.complete ? stream.pipe(Stream.onEnd(execution.complete)) : stream;
143
161
  })));
144
162
  },
@@ -1,6 +1,6 @@
1
1
  import { Context, Effect, Layer, Stream } from "effect";
2
2
  import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http";
3
- import { AIError, TransportReason } from "../schema/index.js";
3
+ import { HttpContext, AIError } from "../schema/index.js";
4
4
  export interface Interface {
5
5
  readonly execute: (request: HttpClientRequest.HttpClientRequest, middleware?: HttpMiddleware) => Effect.Effect<HttpClientResponse.HttpClientResponse, AIError>;
6
6
  }
@@ -9,14 +9,18 @@ export type HttpMiddleware = (request: HttpClientRequest.HttpClientRequest, hand
9
9
  declare const Service_base: Context.ServiceClass<Service, "@opencode/AI/RequestExecutor", Interface>;
10
10
  export declare class Service extends Service_base {
11
11
  }
12
- export declare const classifyHttpFailure: (input: {
12
+ export declare const responseHttp: (response: HttpClientResponse.HttpClientResponse) => HttpContext;
13
+ /** Preserve HTTP diagnostics for executor and externally captured failures alike. */
14
+ export declare const httpFailure: (input: {
13
15
  readonly message: string;
14
- readonly url: string;
16
+ readonly url?: string | undefined;
15
17
  readonly status?: number | undefined;
16
- readonly code?: string | undefined;
18
+ readonly data?: unknown;
17
19
  readonly responseHeaders?: Record<string, string> | undefined;
18
20
  readonly responseBody?: string | undefined;
19
- }) => import("../schema/errors.js").InvalidRequestReason | import("../schema/errors.js").NoRouteReason | import("../schema/errors.js").AuthenticationReason | import("../schema/errors.js").RateLimitReason | import("../schema/errors.js").QuotaExceededReason | import("../schema/errors.js").ContentPolicyReason | import("../schema/errors.js").ProviderInternalReason | TransportReason | import("../schema/errors.js").InvalidProviderOutputReason | import("../schema/errors.js").UnknownProviderReason;
21
+ readonly cause?: unknown;
22
+ }) => AIError;
23
+ export declare const responseStream: (response: HttpClientResponse.HttpClientResponse) => Stream.Stream<Uint8Array, AIError>;
20
24
  export declare const stream: (executor: Interface, request: HttpClientRequest.HttpClientRequest, middleware?: HttpMiddleware) => Stream.Stream<Uint8Array, AIError>;
21
25
  export declare const layer: Layer.Layer<Service, never, HttpClient.HttpClient>;
22
26
  export declare const fetchLayer: Layer.Layer<Service, never, never>;