@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
@@ -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,
@@ -24,6 +24,7 @@ const patterns = [
24
24
  /too large for model with \d+ maximum context length/i,
25
25
  /prompt has [\d,]+ tokens?, but the configured context size is [\d,]+ tokens?/i,
26
26
  /model_context_window_exceeded/i,
27
+ /range of input length should be/i,
27
28
  /too many tokens/i,
28
29
  /token limit exceeded/i,
29
30
  /request_too_large/i,
@@ -38,6 +39,7 @@ export const isContextOverflowFailure = (failure) => failure instanceof AIError
38
39
  : Schema.is(ProviderErrorEvent)(failure) && failure.classification === "context-overflow";
39
40
  const decodeJson = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Unknown));
40
41
  const QUOTA_CODES = new Set(["insufficient_quota", "usage_not_included", "billing_error"]);
42
+ const AUTH_CODES = new Set(["authentication_error", "permission_error"]);
41
43
  const SERVER_CODES = new Set([
42
44
  "api_error",
43
45
  "internal_error",
@@ -53,84 +55,66 @@ const INVALID_REQUEST_CODES = new Set(["invalid_prompt", "invalid_request_error"
53
55
  const RATE_LIMIT_TEXT = /rate increased too quickly|rate[-_\s]?limit|too[_\s]?many[_\s]?requests/i;
54
56
  const QUOTA_TEXT = /insufficient[-_\s]?quota|quota[-_\s]?exceeded/i;
55
57
  const CONTENT_POLICY_TEXT = /content[-_\s]?policy|content_filter|safety/i;
56
- const NETWORK_ERROR_TEXT = /network[-_\s]error/i;
57
- // Keep HTTP failures and provider-reported stream failures on one typed path so
58
- // session retry policy never needs provider-specific string matching.
58
+ // Classification records affirmative evidence about a failure. Deterministic
59
+ // failures need positive identification (a 4xx status, quota/auth/policy
60
+ // signals); anything unrecognized stays UnknownProvider, which the session
61
+ // retry policy treats as retry-eligible because transient failures arrive in
62
+ // unpredictable shapes while deterministic rejections almost always carry a
63
+ // status or known code.
59
64
  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());
65
+ const details = { message: input.message, body: input.rawBody, http: input.http, cause: input.cause };
66
+ const body = input.rawBody ?? "";
67
+ const codes = [...providerCodes(input.data), ...providerCodes(body), ...providerCodes(input.message)].map((code) => code.toLowerCase());
64
68
  // Scan the raw payload too so signals missing from the summary message
65
69
  // (e.g. overflow phrases nested in a JSON error body) still classify.
66
70
  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
71
  const clientScoped = input.status === undefined || (input.status >= 400 && input.status < 500);
69
72
  if (clientScoped &&
70
73
  (codes.includes("context_length_exceeded") ||
71
74
  codes.includes("model_context_window_exceeded") ||
72
75
  codes.includes("request_too_large") ||
73
76
  isContextOverflow(text)))
74
- return new InvalidRequestReason({ ...common, classification: "context-overflow" });
77
+ return new InvalidRequestError({ ...details, classification: "context-overflow" });
75
78
  if (input.status === 413 || isPayloadTooLarge(text))
76
- return new InvalidRequestReason({ ...common, classification: "payload-too-large" });
79
+ return new InvalidRequestError({ ...details, classification: "payload-too-large" });
77
80
  if (CONTENT_POLICY_TEXT.test(text))
78
- return new ContentPolicyReason(common);
81
+ return new ContentPolicyError(details);
79
82
  if (codes.some((code) => QUOTA_CODES.has(code)) || (input.status === 429 && QUOTA_TEXT.test(text)))
80
- return new QuotaExceededReason(common);
81
- if (input.status === 401)
82
- return new AuthenticationReason({ ...common, kind: "invalid" });
83
- if (input.status === 403)
84
- return new AuthenticationReason({ ...common, kind: "insufficient-permissions" });
85
- if (codes.includes("authentication_error"))
86
- return new AuthenticationReason({ ...common, kind: "invalid" });
87
- if (codes.includes("permission_error"))
88
- return new AuthenticationReason({ ...common, kind: "insufficient-permissions" });
89
- if (codes.some((code) => code.includes("rate_limit") || code === "too_many_requests" || code === "throttlingexception"))
90
- return new RateLimitReason({
91
- ...common,
83
+ return new QuotaExceededError(details);
84
+ if (input.status === 401 || input.status === 403 || codes.some((code) => AUTH_CODES.has(code)))
85
+ return new AuthenticationError(details);
86
+ if (input.status === 429 ||
87
+ codes.some((code) => code.includes("rate_limit") || code === "too_many_requests" || code === "throttlingexception") ||
88
+ RATE_LIMIT_TEXT.test(text))
89
+ return new RateLimitError({
90
+ ...details,
92
91
  retryAfterMs: input.retryAfterMs,
93
92
  rateLimit: input.rateLimit,
94
93
  });
95
- if (RATE_LIMIT_TEXT.test(text))
96
- return new RateLimitReason({
97
- ...common,
98
- retryAfterMs: input.retryAfterMs,
99
- rateLimit: input.rateLimit,
100
- });
101
- if (NETWORK_ERROR_TEXT.test(text))
102
- return new ProviderInternalReason({ ...common, status: input.status });
103
- if (codes.some((code) => SERVER_CODES.has(code) || code.includes("exhausted") || code.includes("unavailable")))
104
- return new ProviderInternalReason({
105
- ...common,
106
- status: input.status,
107
- retryAfterMs: input.retryAfterMs,
108
- });
109
- if (input.status === 429) {
110
- return new RateLimitReason({
111
- ...common,
112
- retryAfterMs: input.retryAfterMs,
113
- rateLimit: input.rateLimit,
114
- });
115
- }
116
- if (input.status === 408 || input.status === 409 || (input.status !== undefined && input.status >= 500))
117
- return new ProviderInternalReason({
118
- ...common,
119
- status: input.status,
94
+ if (input.status === 408 ||
95
+ input.status === 409 ||
96
+ (input.status !== undefined && input.status >= 500) ||
97
+ codes.some((code) => SERVER_CODES.has(code) || code.includes("exhausted") || code.includes("unavailable")))
98
+ return new ProviderInternalError({
99
+ ...details,
120
100
  retryAfterMs: input.retryAfterMs,
121
101
  });
122
102
  if (codes.some((code) => INVALID_REQUEST_CODES.has(code)))
123
- return new InvalidRequestReason(common);
124
- 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 });
103
+ return new InvalidRequestError(details);
104
+ // Any remaining 4xx is a deterministic rejection of this request.
105
+ if (input.status !== undefined && input.status >= 400 && input.status < 500)
106
+ return new InvalidRequestError(details);
107
+ return new UnknownProviderError(details);
127
108
  }
128
109
  function providerCodes(value) {
129
- const decoded = Option.getOrUndefined(decodeJson(value));
110
+ const decoded = typeof value === "string" ? Option.getOrUndefined(decodeJson(value)) : value;
130
111
  if (!isRecord(decoded))
131
112
  return [];
132
113
  const error = isRecord(decoded.error) ? decoded.error : undefined;
133
- return [decoded.code, error?.code, error?.type].filter((value) => typeof value === "string");
114
+ const response = isRecord(decoded.response) ? decoded.response : undefined;
115
+ const responseError = response && isRecord(response.error) ? response.error : undefined;
116
+ const exception = isRecord(decoded.exception) ? decoded.exception : undefined;
117
+ return [decoded.code, error?.code, error?.type, error?.status, responseError?.code, exception?.type].filter((value) => typeof value === "string");
134
118
  }
135
119
  function isRecord(value) {
136
120
  return typeof value === "object" && value !== null;
@@ -125,7 +125,7 @@ export declare const protocol: Protocol<{
125
125
  readonly parallel_tool_calls?: boolean | undefined;
126
126
  readonly reasoning_format?: "parsed" | undefined;
127
127
  readonly include_reasoning?: boolean | undefined;
128
- }, string, {
128
+ }, string, "[DONE]" | {
129
129
  readonly [x: string]: unknown;
130
130
  readonly error?: {
131
131
  readonly [x: string]: unknown;
@@ -4,7 +4,6 @@ import { ProviderShared } from "../protocols/shared.js";
4
4
  import { AuthOptions } from "../route/auth-options.js";
5
5
  import { Route } from "../route/client.js";
6
6
  import { Endpoint } from "../route/endpoint.js";
7
- import { Framing } from "../route/framing.js";
8
7
  import { Protocol } from "../route/protocol.js";
9
8
  import { ProviderID } from "../schema/index.js";
10
9
  import { profiles } from "./openai-compatible-profile.js";
@@ -47,7 +46,7 @@ export const route = Route.make({
47
46
  providerMetadataKey: "openai",
48
47
  protocol,
49
48
  endpoint: Endpoint.path("/chat/completions", { baseURL: profiles.groq.baseURL }),
50
- framing: Framing.sse,
49
+ framing: OpenAIChat.framing,
51
50
  });
52
51
  export const configure = (input = {}) => {
53
52
  const { apiKey: _apiKey, auth: _auth, baseURL, ...defaults } = input;
@@ -282,7 +282,7 @@ export declare const protocol: Protocol<{
282
282
  readonly tool_stream?: boolean | undefined;
283
283
  readonly frequency_penalty?: number | undefined;
284
284
  readonly presence_penalty?: number | undefined;
285
- }, string, {
285
+ }, string, "[DONE]" | {
286
286
  readonly [x: string]: unknown;
287
287
  readonly error?: {
288
288
  readonly [x: string]: unknown;
@@ -1,7 +1,6 @@
1
1
  import { Effect, Schema } from "effect";
2
2
  import { Route } from "../route/client.js";
3
3
  import { Endpoint } from "../route/endpoint.js";
4
- import { Framing } from "../route/framing.js";
5
4
  import { Protocol } from "../route/protocol.js";
6
5
  import { AuthOptions } from "../route/auth-options.js";
7
6
  import { ProviderID } from "../schema/index.js";
@@ -87,7 +86,7 @@ export const route = Route.make({
87
86
  providerMetadataKey: "openrouter",
88
87
  protocol,
89
88
  endpoint: Endpoint.path("/chat/completions", { baseURL: profile.baseURL }),
90
- framing: Framing.sse,
89
+ framing: OpenAIChat.framing,
91
90
  });
92
91
  export const routes = [route];
93
92
  const configuredRoute = (input) => {
@@ -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 })
81
+ : new InvalidRequestError({ message: `Failed to resolve auth config: ${error.message}`, cause: error }),
84
82
  });
85
83
  }
86
84
  return error;
@@ -173,6 +173,7 @@ export declare const streamRequest: (request: LLMRequest, options?: StreamOption
173
173
  } | {
174
174
  readonly id: string;
175
175
  readonly type: "text-end";
176
+ readonly text?: string | undefined;
176
177
  readonly providerMetadata?: {
177
178
  readonly [x: string]: {
178
179
  readonly [x: string]: unknown;
@@ -198,6 +199,7 @@ export declare const streamRequest: (request: LLMRequest, options?: StreamOption
198
199
  } | {
199
200
  readonly id: string;
200
201
  readonly type: "reasoning-end";
202
+ readonly text?: string | undefined;
201
203
  readonly providerMetadata?: {
202
204
  readonly [x: string]: {
203
205
  readonly [x: string]: unknown;
@@ -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>;
@@ -1,6 +1,6 @@
1
1
  import { Cause, Context, Effect, Layer, Option, Schema, Stream } from "effect";
2
2
  import { FetchHttpClient, Headers, HttpClient, HttpClientError, HttpClientRequest, HttpClientResponse, } from "effect/unstable/http";
3
- import { HttpContext, HttpRateLimitDetails, HttpRequestDetails, HttpResponseDetails, AIError, TransportReason, } from "../schema/index.js";
3
+ import { HttpContext, HttpRateLimitDetails, AIError, TransportError } from "../schema/index.js";
4
4
  import { classifyProviderFailure } from "../provider-error.js";
5
5
  export class Service extends Context.Service()("@opencode/AI/RequestExecutor") {
6
6
  }
@@ -60,20 +60,11 @@ const rateLimitDetails = (headers, retryAfter) => {
60
60
  reset: Object.keys(reset).length === 0 ? undefined : reset,
61
61
  });
62
62
  };
63
- const requestDetails = (request) => new HttpRequestDetails({
64
- method: request.method,
65
- url: request.url,
66
- headers: headerDetails(request.headers),
67
- });
68
- const responseDetails = (response) => new HttpResponseDetails({
63
+ export const responseHttp = (response) => new HttpContext({
64
+ url: response.request.url,
69
65
  status: response.status,
70
66
  headers: headerDetails(response.headers),
71
67
  });
72
- const responseBody = (body) => {
73
- if (body === undefined)
74
- return {};
75
- return { body };
76
- };
77
68
  const decodeProviderBody = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Struct({
78
69
  message: Schema.optionalKey(Schema.String),
79
70
  error: Schema.optionalKey(Schema.Struct({ message: Schema.optionalKey(Schema.String) })),
@@ -83,60 +74,36 @@ const providerMessage = (status, body) => {
83
74
  return ([decoded?.error?.message, decoded?.message].find((message) => message?.trim()) ??
84
75
  `Provider request failed with HTTP ${status}`);
85
76
  };
86
- const responseHttp = (input) => new HttpContext({
87
- request: requestDetails(input.request),
88
- response: responseDetails(input.response),
89
- ...input.body,
90
- rateLimit: input.rateLimit,
91
- });
92
- const statusError = (request) => (response) => Effect.gen(function* () {
77
+ const statusError = (response) => Effect.gen(function* () {
93
78
  if (response.status < 400)
94
79
  return response;
95
- const body = yield* response.text.pipe(Effect.catch(() => Effect.void));
96
- const headers = normalizedHeaders(response.headers);
97
- const retryAfter = retryAfterMs(headers);
98
- const rateLimit = rateLimitDetails(headers, retryAfter);
99
- const details = responseBody(body);
100
- return yield* new AIError({
101
- module: "RequestExecutor",
102
- method: "execute",
103
- reason: classifyProviderFailure({
104
- status: response.status,
105
- message: providerMessage(response.status, body),
106
- retryAfterMs: retryAfter,
107
- rateLimit,
108
- http: responseHttp({
109
- request,
110
- response,
111
- body: details,
112
- rateLimit,
113
- }),
114
- }),
80
+ const result = yield* response.text.pipe(Effect.result);
81
+ return yield* httpFailure({
82
+ message: providerMessage(response.status, result._tag === "Success" ? result.success : undefined),
83
+ url: response.request.url,
84
+ status: response.status,
85
+ responseHeaders: headerDetails(response.headers),
86
+ responseBody: result._tag === "Success" ? result.success : undefined,
87
+ cause: result._tag === "Failure" ? (result.failure.cause ?? result.failure) : undefined,
115
88
  });
116
89
  });
117
- // Classifies an HTTP failure captured outside the executor (for example by the
118
- // AI SDK's own fetch) onto the same reason types and HttpContext that
119
- // executor-driven requests produce. The originating request is not available on
120
- // that path, so the method is assumed (language model calls are always POST),
121
- // request headers are empty.
122
- export const classifyHttpFailure = (input) => {
90
+ /** Preserve HTTP diagnostics for executor and externally captured failures alike. */
91
+ export const httpFailure = (input) => {
123
92
  const headers = normalizedHeaders(Headers.fromInput(input.responseHeaders));
124
93
  const retryAfter = retryAfterMs(headers);
125
94
  const rateLimit = rateLimitDetails(headers, retryAfter);
126
- const details = responseBody(input.responseBody);
127
- return classifyProviderFailure({
128
- message: input.message,
129
- status: input.status,
130
- code: input.code,
131
- retryAfterMs: retryAfter,
132
- rateLimit,
133
- http: new HttpContext({
134
- request: new HttpRequestDetails({ method: "POST", url: input.url, headers: {} }),
135
- response: input.status === undefined
136
- ? undefined
137
- : new HttpResponseDetails({ status: input.status, headers: headerDetails(Headers.fromInput(headers)) }),
138
- ...details,
95
+ return new AIError({
96
+ reason: classifyProviderFailure({
97
+ message: input.message,
98
+ status: input.status,
99
+ data: input.data,
100
+ rawBody: input.responseBody,
101
+ retryAfterMs: retryAfter,
139
102
  rateLimit,
103
+ cause: input.cause,
104
+ http: input.status === undefined || input.url === undefined
105
+ ? undefined
106
+ : new HttpContext({ url: input.url, status: input.status, headers }),
140
107
  }),
141
108
  });
142
109
  };
@@ -160,19 +127,18 @@ const nativeTransportFailure = (error) => {
160
127
  const httpError = (input) => {
161
128
  const request = HttpClientError.isHttpClientError(input.error) ? input.error.request : input.request;
162
129
  const transportError = (failure) => new AIError({
163
- module: "RequestExecutor",
164
- method: input.operation,
165
- reason: new TransportReason({
130
+ reason: new TransportError({
166
131
  message: failure.message,
132
+ cause: source,
133
+ http: input.http,
167
134
  transport: "http",
168
135
  operation: input.operation,
169
136
  code: failure.code,
170
137
  url: request.url,
171
- http: new HttpContext({ request: requestDetails(request) }),
172
138
  }),
173
139
  });
174
140
  const source = HttpClientError.isHttpClientError(input.error) && "cause" in input.error.reason
175
- ? input.error.reason.cause
141
+ ? (input.error.reason.cause ?? input.error)
176
142
  : input.error;
177
143
  const native = nativeTransportFailure(source);
178
144
  const code = native?.code;
@@ -194,19 +160,20 @@ const httpError = (input) => {
194
160
  code: code ?? input.error.reason._tag,
195
161
  });
196
162
  };
163
+ export const responseStream = (response) => response.stream.pipe(Stream.mapError((error) => httpError({ error, request: response.request, operation: "read", http: responseHttp(response) })));
197
164
  export const stream = (executor, request, middleware) => Stream.unwrap(Effect.gen(function* () {
198
165
  const response = yield* executor.execute(request, middleware);
199
- return response.stream.pipe(Stream.mapError((error) => httpError({ error, request: response.request, operation: "read" })));
166
+ return responseStream(response);
200
167
  }));
201
168
  export const layer = Layer.effect(Service, Effect.gen(function* () {
202
169
  const http = yield* HttpClient.HttpClient;
203
170
  const executeOnce = (request, middleware) => Effect.gen(function* () {
204
171
  if (!middleware)
205
- return yield* http.execute(request).pipe(Effect.mapError((error) => httpError({ error, request, operation: "request" })), Effect.flatMap(statusError(request)));
172
+ return yield* http.execute(request).pipe(Effect.mapError((error) => httpError({ error, request, operation: "request" })), Effect.flatMap(statusError));
206
173
  const response = yield* middleware(request, (input) => http
207
174
  .execute(input)
208
175
  .pipe(Effect.mapError((cause) => (cause instanceof Error ? cause : new Error(String(cause)))))).pipe(Effect.mapError((error) => httpError({ error, request, operation: "request" })));
209
- return yield* statusError(response.request)(response);
176
+ return yield* statusError(response);
210
177
  });
211
178
  return Service.of({
212
179
  execute: executeOnce,
@@ -6,8 +6,8 @@ import type { AIError } from "../schema/index.js";
6
6
  * `Framing` is the byte-stream-shaped seam between transport and protocol:
7
7
  *
8
8
  * - SSE (`Framing.sse`) — UTF-8 decode the body, run the SSE channel decoder,
9
- * drop empty / `[DONE]` keep-alives. Each emitted frame is the JSON `data:`
10
- * payload of one event.
9
+ * and emit the `data:` payload of each non-empty event. The default drops
10
+ * `[DONE]`; protocols that use it as a terminal select `sseWithDone`.
11
11
  * - AWS event stream — length-prefixed binary frames with CRC checksums.
12
12
  * Each emitted frame is one parsed binary event record.
13
13
  *
@@ -17,9 +17,13 @@ import type { AIError } from "../schema/index.js";
17
17
  export interface Definition<Frame> {
18
18
  readonly id: string;
19
19
  readonly frame: (bytes: Stream.Stream<Uint8Array, AIError>) => Stream.Stream<Frame, AIError>;
20
+ /** Original wire representation when framing transforms the provider payload. */
21
+ readonly body?: (frame: Frame) => string | undefined;
20
22
  }
21
23
  /** Server-Sent Events framing. Used by every JSON-streaming HTTP provider. */
22
24
  export declare const sse: Definition<string>;
25
+ /** Server-Sent Events framing that retains the conventional `[DONE]` sentinel. */
26
+ export declare const sseWithDone: Definition<string>;
23
27
  /** SSE framing restricted to protocol-recognized event names. */
24
28
  export declare const sseEvents: (events: ReadonlySet<string>) => Definition<string>;
25
29
  export * as Framing from "./framing.js";
@@ -1,6 +1,11 @@
1
1
  import * as ProviderShared from "../protocols/shared.js";
2
2
  /** Server-Sent Events framing. Used by every JSON-streaming HTTP provider. */
3
3
  export const sse = { id: "sse", frame: ProviderShared.sseFraming };
4
+ /** Server-Sent Events framing that retains the conventional `[DONE]` sentinel. */
5
+ export const sseWithDone = {
6
+ id: "sse",
7
+ frame: (bytes) => ProviderShared.sseFraming(bytes, undefined, true),
8
+ };
4
9
  /** SSE framing restricted to protocol-recognized event names. */
5
10
  export const sseEvents = (events) => ({
6
11
  id: "sse",
@@ -53,8 +53,13 @@ export const httpJson = (input) => ({
53
53
  middleware: prepareInput.middleware,
54
54
  };
55
55
  }),
56
- execute: (prepared, _request, runtime) => Effect.succeed({
57
- frames: prepared.framing.frame(RequestExecutor.stream(runtime.http, prepared.request, prepared.middleware)),
56
+ execute: (prepared, _request, runtime) => Effect.gen(function* () {
57
+ const response = yield* runtime.http.execute(prepared.request, prepared.middleware);
58
+ return {
59
+ frames: prepared.framing.frame(RequestExecutor.responseStream(response)),
60
+ http: RequestExecutor.responseHttp(response),
61
+ body: prepared.framing.body,
62
+ };
58
63
  }),
59
64
  });
60
65
  export const sseJson = {
@@ -3,12 +3,14 @@ import { Endpoint } from "../endpoint.js";
3
3
  import { Auth } from "../auth.js";
4
4
  import type { HttpMiddleware, Interface as RequestExecutorInterface } from "../executor.js";
5
5
  import type { WebSocketChannelExecutor } from "./websocket-channel.js";
6
- import type { AIError, LLMRequest } from "../../schema/index.js";
6
+ import type { AIError, HttpContext, LLMRequest } from "../../schema/index.js";
7
7
  export interface TransportRuntime {
8
8
  readonly http: RequestExecutorInterface;
9
9
  }
10
10
  export interface TransportExecution<Frame> {
11
11
  readonly frames: Stream.Stream<Frame, AIError>;
12
+ readonly http?: HttpContext;
13
+ body?(frame: Frame): string | undefined;
12
14
  /** Optional successful-consumption acknowledgement. HTTP leaves this absent. */
13
15
  readonly complete?: Effect.Effect<void>;
14
16
  }
@@ -1,11 +1,12 @@
1
1
  import type { Effect, Scope, Stream } from "effect";
2
2
  import type { Headers } from "effect/unstable/http";
3
- import type { AIError } from "../../schema/index.js";
3
+ import type { AIError, HttpContext } from "../../schema/index.js";
4
4
  export interface WebSocketChannelExecutor {
5
5
  readonly execute: (exchange: WebSocketChannelExchange) => Effect.Effect<WebSocketChannelExecution, AIError, Scope.Scope>;
6
6
  }
7
7
  export interface WebSocketChannelExecution {
8
8
  readonly frames: Stream.Stream<string, AIError>;
9
+ readonly http?: HttpContext;
9
10
  /** Commits staged state after the decoded Route stream ends successfully. */
10
11
  readonly complete: Effect.Effect<void>;
11
12
  }
@@ -1,7 +1,7 @@
1
1
  import { Effect, Stream } from "effect";
2
2
  import { Headers } from "effect/unstable/http";
3
3
  import { Socket } from "effect/unstable/socket";
4
- import { AIError } from "../../schema/index.js";
4
+ import { AIError, type HttpContext } from "../../schema/index.js";
5
5
  import type { Transport } from "./index.js";
6
6
  import type { WebSocketChannelExecutor } from "./websocket-channel.js";
7
7
  export interface WebSocketRequest {
@@ -9,6 +9,7 @@ export interface WebSocketRequest {
9
9
  readonly headers: Headers.Headers;
10
10
  }
11
11
  export interface WebSocketConnection {
12
+ readonly http?: HttpContext;
12
13
  readonly sendText: (message: string) => Effect.Effect<void, AIError>;
13
14
  readonly messages: Stream.Stream<string | Uint8Array, AIError>;
14
15
  readonly close: Effect.Effect<void, never>;