@opencode/ai 2.1.0 → 2.1.2

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.
@@ -1,7 +1,7 @@
1
1
  import { Clock, Effect, Redacted, Schema } from "effect";
2
2
  import { HttpClientRequest } from "effect/http";
3
3
  import { RequestExecutor } from "../route/executor.js";
4
- import { AIError, HttpContext, InvalidProviderOutputError, InvalidRequestError } from "../schema/index.js";
4
+ import { AIError, InvalidProviderOutputError, InvalidRequestError } from "../schema/index.js";
5
5
  import { encodeJson } from "../utils/json.js";
6
6
  const OPENAI_BASE_URL = "https://api.openai.com/v1";
7
7
  const XAI_BASE_URL = "https://api.x.ai/v1";
@@ -28,7 +28,10 @@ export const connect = Effect.fn("Realtime.connect")(function* (input) {
28
28
  }
29
29
  case "openai-realtime": {
30
30
  if (input.transport.type === "websocket")
31
- return yield* clientSecret(resolved.id, OPENAI_BASE_URL, input.model, input.apiKey, session);
31
+ return yield* clientSecret(resolved.id, OPENAI_BASE_URL, input.model, input.apiKey, {
32
+ expires_after: { anchor: "created_at", seconds: 60 },
33
+ session,
34
+ });
32
35
  const form = new FormData();
33
36
  form.append("sdp", input.transport.sdp);
34
37
  form.append("session", encodeJson(session));
@@ -39,7 +42,9 @@ export const connect = Effect.fn("Realtime.connect")(function* (input) {
39
42
  case "xai-realtime": {
40
43
  if (input.transport.type !== "websocket")
41
44
  return yield* invalidRequest("xAI Grok Voice only supports WebSocket transport");
42
- return yield* clientSecret(resolved.id, XAI_BASE_URL, input.model, input.apiKey);
45
+ return yield* clientSecret(resolved.id, XAI_BASE_URL, input.model, input.apiKey, {
46
+ expires_after: { seconds: 60 },
47
+ });
43
48
  }
44
49
  case "gemini-live": {
45
50
  if (input.transport.type !== "websocket")
@@ -69,7 +74,7 @@ const LiveSessionResponse = Schema.fromJsonString(Schema.Struct({
69
74
  transport: Schema.Struct({ sdp: Schema.String }),
70
75
  }));
71
76
  // xAI's ephemeral-token docs (https://docs.x.ai/developers/model-capabilities/audio/ephemeral-tokens)
72
- // document the request body (`{ expires_after: { seconds } }`) but not the response payload; we assume
77
+ // document the request body (`{ expires_after: { seconds } }`, no `anchor` or `session`) but not the response payload; we assume
73
78
  // it mirrors OpenAI's `{ value, expires_at }` response shape.
74
79
  const ClientSecretResponse = Schema.fromJsonString(Schema.Struct({
75
80
  value: Schema.String,
@@ -86,12 +91,12 @@ const post = (route, url, apiKey, body, response, authHeader) => Effect.gen(func
86
91
  : HttpClientRequest.bearerToken(apiKey), body instanceof FormData
87
92
  ? HttpClientRequest.bodyFormData(body)
88
93
  : HttpClientRequest.bodyText(encodeJson(body), "application/json")));
89
- const http = new HttpContext({ url: res.request.url, status: res.status, headers: res.headers });
94
+ const http = RequestExecutor.responseHttp(res);
90
95
  const fail = (message, cause, raw) => new AIError({ reason: new InvalidProviderOutputError({ route, message, body: raw, http, cause }) });
91
96
  const text = yield* res.text.pipe(Effect.mapError((cause) => fail(`Failed to read ${route} response`, cause)));
92
97
  return yield* Schema.decodeEffect(response)(text).pipe(Effect.mapError((cause) => fail(`${route} returned an invalid response`, cause, text)));
93
98
  });
94
- const clientSecret = (route, baseURL, model, apiKey, session) => post(route, `${baseURL}/realtime/client_secrets`, apiKey, { expires_after: { seconds: 60 }, session }, ClientSecretResponse).pipe(Effect.map((data) => ({
99
+ const clientSecret = (route, baseURL, model, apiKey, body) => post(route, `${baseURL}/realtime/client_secrets`, apiKey, body, ClientSecretResponse).pipe(Effect.map((data) => ({
95
100
  type: "websocket",
96
101
  url: `${baseURL.replace(/^http/, "ws")}/realtime?model=${encodeURIComponent(model)}`,
97
102
  token: data.value,
@@ -2,7 +2,9 @@ import { Effect, Schema } from "effect";
2
2
  import { Headers, HttpClientRequest } from "effect/http";
3
3
  import { ChoiceQuestion, EvaluationInput, EvaluationModel, EvaluationResponse, EvaluationRounding, ScoreQuestion, } from "./evaluation.js";
4
4
  import { Auth } from "../route/auth.js";
5
- import { AIError, HttpContext, HttpOptions, InvalidProviderOutputError, InvalidRequestError, ModelID, Usage, mergeJsonRecords, } from "../schema/index.js";
5
+ import { RequestExecutor } from "../route/executor.js";
6
+ import { JsonObject, ProviderShared } from "../protocols/shared.js";
7
+ import { AIError, HttpOptions, InvalidProviderOutputError, InvalidRequestError, ModelID, ProviderMetadata, Usage, mergeJsonRecords, } from "../schema/index.js";
6
8
  const Noul = Schema.Struct({
7
9
  type: Schema.Literal("noul"),
8
10
  instructions: EvaluationInput,
@@ -24,7 +26,7 @@ const Request = Schema.StructWithRest(Schema.Struct({
24
26
  model: Schema.String,
25
27
  state: EvaluationInput,
26
28
  questions: Schema.Record(Schema.String, Question),
27
- }), [Schema.Record(Schema.String, Schema.Any)]);
29
+ }), [JsonObject]);
28
30
  const Probability = Schema.Number.check(Schema.isBetween({ minimum: 0, maximum: 1 }));
29
31
  const NoulAnswer = Schema.Struct({ type: Schema.Literal("noul"), noul: Probability });
30
32
  const Choice = Schema.Struct({
@@ -44,14 +46,14 @@ const Answer = Schema.Union([NoulAnswer, Choice, Score]).pipe(Schema.toTaggedUni
44
46
  const NativeUsage = Schema.StructWithRest(Schema.Struct({
45
47
  input_tokens: Schema.optional(Schema.Number),
46
48
  output_tokens: Schema.optional(Schema.Number),
47
- }), [Schema.Record(Schema.String, Schema.Unknown)]);
49
+ }), [JsonObject]);
48
50
  const Response = Schema.Struct({
49
51
  model: Schema.String,
50
52
  answers: Schema.Record(Schema.String, Answer),
51
53
  usage: Schema.optional(NativeUsage),
52
54
  id: Schema.optional(Schema.String),
53
55
  provider: Schema.optional(Schema.String),
54
- provider_metadata: Schema.optional(Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.Unknown))),
56
+ provider_metadata: Schema.optional(ProviderMetadata),
55
57
  });
56
58
  export const model = (cfg) => EvaluationModel.make({
57
59
  id: cfg.id,
@@ -76,7 +78,7 @@ export const model = (cfg) => EvaluationModel.make({
76
78
  headers: Headers.fromInput({ ...cfg.headers, ...req.http?.headers }),
77
79
  });
78
80
  const res = yield* send(HttpClientRequest.post(url).pipe(HttpClientRequest.setHeaders(headers), HttpClientRequest.bodyText(body, "application/json")));
79
- const http = new HttpContext({ url: res.request.url, status: res.status, headers: res.headers });
81
+ const http = RequestExecutor.responseHttp(res);
80
82
  const fail = (message, cause, body) => new AIError({ reason: new InvalidProviderOutputError({ route: "system-one", message, body, http, cause }) });
81
83
  const text = yield* res.text.pipe(Effect.mapError((cause) => fail("Failed to read the System One response", cause)));
82
84
  const data = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(Response))(text).pipe(Effect.mapError((cause) => fail("System One returned an invalid response", cause, text)));
@@ -120,9 +122,7 @@ export const model = (cfg) => EvaluationModel.make({
120
122
  ? new Usage({
121
123
  inputTokens: data.usage.input_tokens,
122
124
  outputTokens: data.usage.output_tokens,
123
- totalTokens: data.usage.input_tokens === undefined && data.usage.output_tokens === undefined
124
- ? undefined
125
- : (data.usage.input_tokens ?? 0) + (data.usage.output_tokens ?? 0),
125
+ totalTokens: ProviderShared.totalTokens(data.usage.input_tokens, data.usage.output_tokens, undefined),
126
126
  providerMetadata: { [cfg.providerMetadataKey]: data.usage },
127
127
  })
128
128
  : undefined,
package/dist/media.js CHANGED
@@ -3,9 +3,9 @@ import { Effect, FileSystem, Schema, SchemaGetter } from "effect";
3
3
  import { Base64 } from "effect/encoding";
4
4
  import { HttpClientRequest } from "effect/http";
5
5
  import { ProviderID } from "./schema/ids.js";
6
- import { AIError, HttpContext, InvalidProviderOutputError, InvalidRequestError } from "./schema/errors.js";
6
+ import { AIError, InvalidProviderOutputError, InvalidRequestError } from "./schema/errors.js";
7
7
  import { ProviderMetadata } from "./schema/options.js";
8
- import { Service } from "./route/executor-service.js";
8
+ import { responseHttp, Service } from "./route/executor-service.js";
9
9
  import { detectMediaType, fileMediaType } from "./utils/media-type.js";
10
10
  export { detectMediaType } from "./utils/media-type.js";
11
11
  const OCTET_STREAM = "application/octet-stream";
@@ -183,7 +183,7 @@ const download = Effect.fn("Media.download")(function* (source, headers) {
183
183
  const buffer = yield* response.arrayBuffer.pipe(Effect.mapError((cause) => new AIError({
184
184
  reason: new InvalidProviderOutputError({
185
185
  message: `Failed to read media from ${source.url}`,
186
- http: new HttpContext({ url: response.request.url, status: response.status, headers: response.headers }),
186
+ http: responseHttp(response),
187
187
  cause,
188
188
  }),
189
189
  })));
@@ -49,7 +49,7 @@ const Assistant = Schema.StructWithRest(Schema.Struct({
49
49
  ...OpenAIChat.OpenAIChatMessage.cases.assistant.schema.fields,
50
50
  tool_calls: optionalArray(ToolCall),
51
51
  thought_signature: Schema.optional(Schema.String),
52
- }), [Schema.Record(Schema.String, Schema.Unknown)]);
52
+ }), [JsonObject]);
53
53
  const Messages = Schema.Array(Schema.Union([
54
54
  OpenAIChat.OpenAIChatMessage.cases.system,
55
55
  OpenAIChat.OpenAIChatMessage.cases.user,
@@ -85,16 +85,16 @@ const Delta = Schema.StructWithRest(Schema.Struct({
85
85
  ...OpenAIChat.OpenAIChatDelta.schema.fields,
86
86
  tool_calls: optionalNull(Schema.Array(ToolDelta)),
87
87
  thought_signature: optionalNull(Schema.String),
88
- }), [Schema.Record(Schema.String, Schema.Unknown)]);
89
- const Choice = Schema.StructWithRest(Schema.Struct({ ...OpenAIChat.OpenAIChatChoice.schema.fields, delta: optionalNull(Delta) }), [Schema.Record(Schema.String, Schema.Unknown)]);
88
+ }), [JsonObject]);
89
+ const Choice = Schema.StructWithRest(Schema.Struct({ ...OpenAIChat.OpenAIChatChoice.schema.fields, delta: optionalNull(Delta) }), [JsonObject]);
90
90
  const Usage = Schema.StructWithRest(Schema.Struct({
91
91
  ...OpenAIChat.OpenAIChatUsage.schema.fields,
92
92
  prompt_tokens_details: optionalNull(Schema.StructWithRest(Schema.Struct({
93
93
  cached_tokens: optionalNull(Schema.Number),
94
94
  cache_write_tokens: optionalNull(Schema.Number),
95
95
  cache_creation_input_tokens: optionalNull(Schema.Number),
96
- }), [Schema.Record(Schema.String, Schema.Unknown)])),
97
- }), [Schema.Record(Schema.String, Schema.Unknown)]);
96
+ }), [JsonObject])),
97
+ }), [JsonObject]);
98
98
  const Event = Schema.StructWithRest(Schema.Struct({
99
99
  ...OpenAIChat.OpenAIChatEvent.schema.fields,
100
100
  choices: optionalNull(Schema.Array(Choice)),
@@ -104,7 +104,7 @@ const Event = Schema.StructWithRest(Schema.Struct({
104
104
  message: Schema.String,
105
105
  path: Schema.optional(Schema.Array(Schema.Union([Schema.String, Schema.Number]))),
106
106
  })),
107
- }), [Schema.Record(Schema.String, Schema.Unknown)]);
107
+ }), [JsonObject]);
108
108
  const MARKER = "__ENCRYPTED_REASONING__";
109
109
  // ---------------------------------------------------------------------------
110
110
  // Request lowering
@@ -201,7 +201,7 @@ export function classifyProviderFailure(input) {
201
201
  if (input.status === 402 ||
202
202
  codes.some((code) => QUOTA_CODES.has(code)) ||
203
203
  (input.status === 429 && QUOTA_TEXT.test(text)))
204
- return new QuotaExceededError(details);
204
+ return new QuotaExceededError({ ...details, retryAfterMs: input.retryAfterMs });
205
205
  if (input.status === 401 ||
206
206
  input.status === 403 ||
207
207
  codes.some((code) => AUTH_CODES.has(code)) ||
@@ -4,19 +4,20 @@ import { EvaluationAnswer, EvaluationInput, EvaluationModel, EvaluationQuestion,
4
4
  import { AnthropicMessages } from "../protocols/anthropic-messages.js";
5
5
  import { OpenAIChat } from "../protocols/openai-chat.js";
6
6
  import { OpenResponses } from "../protocols/open-responses.js";
7
- import { optionalNull, ProviderShared } from "../protocols/shared.js";
7
+ import { JsonObject, optionalNull, ProviderShared } from "../protocols/shared.js";
8
8
  import { gatewayProtocol } from "../protocols/utils/gateway-protocol.js";
9
9
  import { Auth } from "../route/auth.js";
10
10
  import { AuthOptions } from "../route/auth-options.js";
11
11
  import { Route } from "../route/client.js";
12
12
  import { Endpoint } from "../route/endpoint.js";
13
+ import { RequestExecutor } from "../route/executor.js";
13
14
  import { Framing } from "../route/framing.js";
14
- import { AIError, HttpContext, HttpOptions, InvalidProviderOutputError, InvalidRequestError, LLMRequest, ModelID, ProviderID, ProviderMetadata, ReasoningEffort, Usage, } from "../schema/index.js";
15
+ import { AIError, HttpOptions, InvalidProviderOutputError, InvalidRequestError, LLMRequest, ModelID, ProviderID, ProviderMetadata, ProviderOptions, ReasoningEffort, Usage, } from "../schema/index.js";
15
16
  export const id = ProviderID.make("vercel-ai-gateway");
16
17
  const baseURL = "https://ai-gateway.vercel.sh/v1";
17
18
  const GatewayOptionsSchema = Schema.Struct({
18
19
  gateway: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
19
- upstream: Schema.optional(Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.Unknown))),
20
+ upstream: Schema.optional(ProviderMetadata),
20
21
  reasoningEffort: Schema.optional(ReasoningEffort),
21
22
  thinking: Schema.optional(Schema.Struct({
22
23
  type: Schema.String,
@@ -110,8 +111,8 @@ const Request = Schema.StructWithRest(Schema.Struct({
110
111
  model: Schema.String,
111
112
  state: EvaluationInput,
112
113
  questions: Schema.Record(Schema.String, EvaluationQuestion),
113
- providerOptions: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
114
- }), [Schema.Record(Schema.String, Schema.Any)]);
114
+ providerOptions: Schema.optional(ProviderOptions),
115
+ }), [JsonObject]);
115
116
  const Response = Schema.Struct({
116
117
  model: optionalNull(Schema.String),
117
118
  answers: Schema.Record(Schema.String, EvaluationAnswer),
@@ -170,7 +171,7 @@ export const configure = (input = {}) => {
170
171
  headers: Headers.fromInput({ ...input.headers, ...req.http?.headers }),
171
172
  });
172
173
  const res = yield* send(HttpClientRequest.post(url).pipe(HttpClientRequest.setHeaders(headers), HttpClientRequest.bodyText(body, "application/json")));
173
- const http = new HttpContext({ url: res.request.url, status: res.status, headers: res.headers });
174
+ const http = RequestExecutor.responseHttp(res);
174
175
  const fail = (message, cause, body) => new AIError({
175
176
  reason: new InvalidProviderOutputError({
176
177
  route: "vercel-evaluation",
@@ -15,7 +15,7 @@ const makeRouteLanguageModel = (route, mapped) => {
15
15
  const provider = route.provider ?? ("provider" in mapped ? mapped.provider : undefined);
16
16
  if (!provider)
17
17
  throw new Error(`Route.model(${route.id}) requires a provider`);
18
- if (!endpointBaseURL(route.endpoint))
18
+ if (!route.endpoint.baseURL)
19
19
  throw new ProviderConfigurationError({
20
20
  provider: ProviderID.make(provider),
21
21
  message: `Route.model(${route.id}) requires an endpoint baseURL — configure it on the route first`,
@@ -37,7 +37,6 @@ const mergeRouteDefaults = (base, patch) => {
37
37
  http: mergeHttpOptions(base?.http, HttpOptions.make(patch.http), headers === undefined ? undefined : new HttpOptions({ headers })),
38
38
  };
39
39
  };
40
- const endpointBaseURL = (endpoint) => typeof endpoint.baseURL === "string" ? endpoint.baseURL : undefined;
41
40
  const mergeHeaders = (...items) => {
42
41
  const entries = items.flatMap((item) => item === undefined ? [] : Object.entries(item).filter((entry) => entry[1] !== undefined));
43
42
  if (entries.length === 0)
@@ -1,6 +1,6 @@
1
1
  import { Context, type Effect } from "effect";
2
- import type { HttpClientRequest, HttpClientResponse } from "effect/http";
3
- import type { AIError } from "../schema/errors.js";
2
+ import type { Headers, HttpClientRequest, HttpClientResponse } from "effect/http";
3
+ import { HttpContext, type AIError } from "../schema/errors.js";
4
4
  export interface Interface {
5
5
  readonly execute: (request: HttpClientRequest.HttpClientRequest, middleware?: HttpMiddleware) => Effect.Effect<HttpClientResponse.HttpClientResponse, AIError>;
6
6
  }
@@ -11,4 +11,8 @@ export declare class RequestExecutorService extends RequestExecutorService_base
11
11
  }
12
12
  export declare const Service: typeof RequestExecutorService;
13
13
  export type Service = RequestExecutorService;
14
+ export declare const headerDetails: (headers: Headers.Headers) => {
15
+ [k: string]: string;
16
+ };
17
+ export declare const responseHttp: (response: HttpClientResponse.HttpClientResponse) => HttpContext;
14
18
  export {};
@@ -1,4 +1,11 @@
1
1
  import { Context } from "effect";
2
+ import { HttpContext } from "../schema/errors.js";
2
3
  export class RequestExecutorService extends Context.Service()("@opencode/AI/RequestExecutor") {
3
4
  }
4
5
  export const Service = RequestExecutorService;
6
+ export const headerDetails = (headers) => Object.fromEntries(Object.entries(headers).map(([name, value]) => [name, String(value)]));
7
+ export const responseHttp = (response) => new HttpContext({
8
+ url: response.request.url,
9
+ status: response.status,
10
+ headers: headerDetails(response.headers),
11
+ });
@@ -1,10 +1,9 @@
1
1
  import { Layer, Stream } from "effect";
2
2
  import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http";
3
- import { HttpContext, AIError } from "../schema/index.js";
3
+ import { AIError } from "../schema/index.js";
4
4
  import { Service, type HttpMiddleware, type Interface } from "./executor-service.js";
5
- export { Service } from "./executor-service.js";
5
+ export { responseHttp, Service } from "./executor-service.js";
6
6
  export type { HttpHandler, HttpMiddleware, Interface } from "./executor-service.js";
7
- export declare const responseHttp: (response: HttpClientResponse.HttpClientResponse) => HttpContext;
8
7
  /** Preserve HTTP diagnostics for executor and externally captured failures alike. */
9
8
  export declare const httpFailure: (input: {
10
9
  readonly message: string;
@@ -2,23 +2,23 @@ import { Cause, Effect, Layer, Option, Schema, Stream } from "effect";
2
2
  import { FetchHttpClient, Headers, HttpClient, HttpClientError, HttpClientRequest, HttpClientResponse, } from "effect/http";
3
3
  import { HttpContext, HttpRateLimitDetails, AIError, TransportError } from "../schema/index.js";
4
4
  import { classifyProviderFailure, providerErrorMessage } from "../provider-error.js";
5
- import { Service } from "./executor-service.js";
6
- export { Service } from "./executor-service.js";
7
- const headerDetails = (headers) => Object.fromEntries(Object.entries(headers).map(([name, value]) => [name, String(value)]));
5
+ import { headerDetails, responseHttp, Service } from "./executor-service.js";
6
+ export { responseHttp, Service } from "./executor-service.js";
8
7
  const normalizedHeaders = (headers) => Object.fromEntries(Object.entries(headers).map(([key, value]) => [key.toLowerCase(), value]));
8
+ const clampRetryAfterMs = (millis) => Math.min(Number.MAX_SAFE_INTEGER, Math.max(0, Math.ceil(millis)));
9
9
  const retryAfterMs = (headers) => {
10
10
  const millis = Number(headers["retry-after-ms"]);
11
11
  if (Number.isFinite(millis))
12
- return Math.max(0, millis);
12
+ return clampRetryAfterMs(millis);
13
13
  const value = headers["retry-after"];
14
14
  if (!value)
15
15
  return undefined;
16
16
  const seconds = Number(value);
17
17
  if (Number.isFinite(seconds))
18
- return Math.max(0, seconds * 1000);
18
+ return clampRetryAfterMs(seconds * 1000);
19
19
  const date = Date.parse(value);
20
20
  if (!Number.isNaN(date))
21
- return Math.max(0, date - Date.now());
21
+ return clampRetryAfterMs(date - Date.now());
22
22
  return undefined;
23
23
  };
24
24
  const addRateLimitValue = (target, key, value) => {
@@ -60,11 +60,6 @@ const rateLimitDetails = (headers, retryAfter) => {
60
60
  reset: Object.keys(reset).length === 0 ? undefined : reset,
61
61
  });
62
62
  };
63
- export const responseHttp = (response) => new HttpContext({
64
- url: response.request.url,
65
- status: response.status,
66
- headers: headerDetails(response.headers),
67
- });
68
63
  const MAX_BODY_CHARS = 2000;
69
64
  // Without a recognized message, show the raw body so the provider's explanation is never dropped.
70
65
  const providerMessage = (status, body) => {
@@ -3,7 +3,7 @@ import { HttpClientResponse } from "effect/http";
3
3
  import type { Snapshot, Status } from "../generation.js";
4
4
  import { Media } from "../media.js";
5
5
  import type { AuthInput } from "./auth.js";
6
- import { AIError, AuthenticationError, HttpContext, InvalidRequestError, ProviderID, ProviderInternalError, RateLimitError } from "../schema/index.js";
6
+ import { AIError, AuthenticationError, type HttpContext, InvalidRequestError, ProviderID, ProviderInternalError, RateLimitError } from "../schema/index.js";
7
7
  /** Array values become repeated parameters (`keyterm=a&keyterm=b`). */
8
8
  export type Query = Readonly<Record<string, string | ReadonlyArray<string>>>;
9
9
  /** `query` is appended to the endpoint URL before the route and caller `http.query` overlays. */
@@ -1,7 +1,8 @@
1
1
  import { Clock, Duration, Effect, Schema } from "effect";
2
2
  import { HttpClientResponse } from "effect/http";
3
3
  import { Media } from "../media.js";
4
- import { AIError, AuthenticationError, ContentPolicyError, HttpContext, InvalidProviderOutputError, InvalidRequestError, ProviderID, ProviderInternalError, RateLimitError, UnsupportedOperationError, } from "../schema/index.js";
4
+ import { RequestExecutor } from "./executor.js";
5
+ import { AIError, AuthenticationError, ContentPolicyError, InvalidProviderOutputError, InvalidRequestError, ProviderID, ProviderInternalError, RateLimitError, UnsupportedOperationError, } from "../schema/index.js";
5
6
  export const json = (value, query) => ({
6
7
  type: "json",
7
8
  value,
@@ -27,7 +28,6 @@ const FAILURES = {
27
28
  RateLimit: RateLimitError,
28
29
  ProviderInternal: ProviderInternalError,
29
30
  };
30
- const context = (response) => new HttpContext({ url: response.request.url, status: response.status, headers: response.headers });
31
31
  /** One protocol's route id, display name, and provider, with the decoders and errors that carry them. */
32
32
  export const identity = (input) => {
33
33
  const provider = ProviderID.make(input.provider);
@@ -40,7 +40,7 @@ export const identity = (input) => {
40
40
  * `contentPolicy` is a moderated result.
41
41
  */
42
42
  const text = Effect.fn("MediaProtocol.text")(function* (response) {
43
- const http = context(response);
43
+ const http = RequestExecutor.responseHttp(response);
44
44
  const body = yield* response.text.pipe(Effect.mapError((cause) => new AIError({
45
45
  reason: new InvalidProviderOutputError({
46
46
  route: input.id,
@@ -2,7 +2,6 @@ import { Effect, Stream } from "effect";
2
2
  import { Headers } from "effect/http";
3
3
  import { Socket } from "effect/socket";
4
4
  import { AIError, type HttpContext } from "../../schema/index.js";
5
- import type { Transport } from "./index.js";
6
5
  import type { WebSocketChannelExecutor } from "./websocket-channel.js";
7
6
  export interface WebSocketRequest {
8
7
  readonly url: string;
@@ -23,30 +22,7 @@ export declare const fromWebSocket: (ws: Socket.WebSocketLike, input: WebSocketR
23
22
  export declare const messageText: (message: string | Uint8Array, decoder: TextDecoder) => string;
24
23
  export declare const makeDirect: (connector: WebSocketConnector) => WebSocketChannelExecutor;
25
24
  export declare const direct: Effect.Effect<WebSocketChannelExecutor, never, Socket.WebSocketConstructor>;
26
- export interface JsonPrepared {
27
- readonly url: string;
28
- readonly headers: Headers.Headers;
29
- readonly message: string;
30
- }
31
- export interface JsonInput<Body, Message> {
32
- readonly toMessage: (body: Body | Record<string, unknown>) => Effect.Effect<Message, AIError>;
33
- readonly encodeMessage: (message: Message) => string;
34
- }
35
- export type JsonPatch<Body, Message> = Partial<JsonInput<Body, Message>>;
36
- export interface JsonTransport<Body, Message> extends Transport<Body, JsonPrepared, string> {
37
- readonly with: (patch: JsonPatch<Body, Message>) => JsonTransport<Body, Message>;
38
- }
39
- export declare const json: <Body, Message>(input: JsonInput<Body, Message>) => JsonTransport<Body, Message>;
40
- export declare const jsonTransport: {
41
- readonly id: "websocket-json";
42
- readonly with: typeof json;
43
- };
44
25
  export declare const WebSocketTransport: {
45
- readonly json: typeof json;
46
- readonly jsonTransport: {
47
- readonly id: "websocket-json";
48
- readonly with: typeof json;
49
- };
50
26
  readonly direct: Effect.Effect<WebSocketChannelExecutor, never, Socket.WebSocketConstructor>;
51
27
  readonly makeDirect: typeof makeDirect;
52
28
  readonly open: typeof open;
@@ -2,7 +2,6 @@ import { Cause, Effect, Queue, Stream } from "effect";
2
2
  import { Headers } from "effect/http";
3
3
  import { Socket } from "effect/socket";
4
4
  import { AIError, AIErrorReason, TransportError, } from "../../schema/index.js";
5
- import * as HttpTransport from "./http.js";
6
5
  const MAX_FRAME_BYTES = 16 * 1024 * 1024;
7
6
  const transportError = (message, input) => new AIError({
8
7
  reason: new TransportError({
@@ -300,56 +299,7 @@ export const direct = Effect.gen(function* () {
300
299
  open: (input) => open(input).pipe(Effect.provideService(Socket.WebSocketConstructor, constructor)),
301
300
  });
302
301
  });
303
- export const json = (input) => ({
304
- id: "websocket-json",
305
- with: (patch) => json({ ...input, ...patch }),
306
- prepare: (prepareInput) => Effect.gen(function* () {
307
- const parts = yield* HttpTransport.jsonRequestParts({
308
- ...prepareInput,
309
- });
310
- return {
311
- url: yield* toWebSocketUrl(parts.url),
312
- headers: parts.headers,
313
- message: input.encodeMessage(yield* input.toMessage(parts.jsonBody)),
314
- };
315
- }),
316
- execute: (prepared, request, _runtime, options) => {
317
- const webSocket = options?.webSocket;
318
- if (!webSocket) {
319
- return Effect.fail(transportError("WebSocket JSON transport requires StreamOptions.webSocket", {
320
- url: prepared.url,
321
- operation: "request",
322
- code: "unavailable",
323
- phase: "prepare",
324
- delivery: "not-sent",
325
- }));
326
- }
327
- const driver = {
328
- create: () => Effect.succeed({ message: prepared.message, mode: "full" }),
329
- observe: (_create, frame) => Effect.succeed({ type: "frame", frame }),
330
- };
331
- const exchange = {
332
- id: request.id ?? "request",
333
- connect: { url: prepared.url, headers: prepared.headers },
334
- fallback: () => Stream.fail(transportError("WebSocket JSON transport does not provide HTTP fallback", {
335
- url: prepared.url,
336
- operation: "request",
337
- code: "websocket",
338
- phase: "fallback",
339
- delivery: "not-sent",
340
- })),
341
- driver,
342
- };
343
- return webSocket.execute(exchange);
344
- },
345
- });
346
- export const jsonTransport = {
347
- id: "websocket-json",
348
- with: json,
349
- };
350
302
  export const WebSocketTransport = {
351
- json,
352
- jsonTransport,
353
303
  direct,
354
304
  makeDirect,
355
305
  open,
@@ -10,7 +10,7 @@ declare const HttpContext_base: Schema.Class<HttpContext, Schema.Struct<{
10
10
  export declare class HttpContext extends HttpContext_base {
11
11
  }
12
12
  declare const HttpRateLimitDetails_base: Schema.Class<HttpRateLimitDetails, Schema.Struct<{
13
- readonly retryAfterMs: Schema.optional<Schema.Number>;
13
+ readonly retryAfterMs: Schema.optional<Schema.Int>;
14
14
  readonly limit: Schema.optional<Schema.$Record<Schema.String, Schema.String>>;
15
15
  readonly remaining: Schema.optional<Schema.$Record<Schema.String, Schema.String>>;
16
16
  readonly reset: Schema.optional<Schema.$Record<Schema.String, Schema.String>>;
@@ -80,16 +80,17 @@ declare const RateLimitError_base: Schema.Class<RateLimitError, Schema.TaggedStr
80
80
  readonly body: Schema.optional<Schema.String>;
81
81
  readonly http: Schema.optional<typeof HttpContext>;
82
82
  readonly cause: Schema.optional<Schema.Defect>;
83
- readonly retryAfterMs: Schema.optional<Schema.Number>;
83
+ readonly retryAfterMs: Schema.optional<Schema.Int>;
84
84
  readonly rateLimit: Schema.optional<typeof HttpRateLimitDetails>;
85
85
  }>, import("effect/Cause").YieldableError>;
86
86
  export declare class RateLimitError extends RateLimitError_base {
87
87
  }
88
88
  declare const QuotaExceededError_base: Schema.Class<QuotaExceededError, Schema.TaggedStruct<"QuotaExceeded", {
89
- message: Schema.String;
90
- body: Schema.optional<Schema.String>;
91
- http: Schema.optional<typeof HttpContext>;
92
- cause: Schema.optional<Schema.Defect>;
89
+ readonly message: Schema.String;
90
+ readonly body: Schema.optional<Schema.String>;
91
+ readonly http: Schema.optional<typeof HttpContext>;
92
+ readonly cause: Schema.optional<Schema.Defect>;
93
+ readonly retryAfterMs: Schema.optional<Schema.Int>;
93
94
  }>, import("effect/Cause").YieldableError>;
94
95
  export declare class QuotaExceededError extends QuotaExceededError_base {
95
96
  }
@@ -106,7 +107,7 @@ declare const ProviderInternalError_base: Schema.Class<ProviderInternalError, Sc
106
107
  readonly body: Schema.optional<Schema.String>;
107
108
  readonly http: Schema.optional<typeof HttpContext>;
108
109
  readonly cause: Schema.optional<Schema.Defect>;
109
- readonly retryAfterMs: Schema.optional<Schema.Number>;
110
+ readonly retryAfterMs: Schema.optional<Schema.Int>;
110
111
  }>, import("effect/Cause").YieldableError>;
111
112
  export declare class ProviderInternalError extends ProviderInternalError_base {
112
113
  }
@@ -1,4 +1,5 @@
1
1
  import { Schema } from "effect";
2
+ import { NonNegativeInt } from "@opencode/schema";
2
3
  import { Tool } from "@opencode/schema/tool";
3
4
  import { ModelID, ProviderID, RouteID } from "./ids.js";
4
5
  export const ProviderFailureClassification = Schema.Literals(["context-overflow", "payload-too-large"]);
@@ -9,7 +10,7 @@ export class HttpContext extends Schema.Class("AI.HttpContext")({
9
10
  }) {
10
11
  }
11
12
  export class HttpRateLimitDetails extends Schema.Class("AI.HttpRateLimitDetails")({
12
- retryAfterMs: Schema.optional(Schema.Number),
13
+ retryAfterMs: Schema.optional(NonNegativeInt),
13
14
  limit: Schema.optional(Schema.Record(Schema.String, Schema.String)),
14
15
  remaining: Schema.optional(Schema.Record(Schema.String, Schema.String)),
15
16
  reset: Schema.optional(Schema.Record(Schema.String, Schema.String)),
@@ -63,17 +64,20 @@ export class AuthenticationError extends Schema.TaggedError("AI.Error.Authentica
63
64
  }
64
65
  export class RateLimitError extends Schema.TaggedError("AI.Error.RateLimit")("RateLimit", {
65
66
  ...ReasonFields,
66
- retryAfterMs: Schema.optional(Schema.Number),
67
+ retryAfterMs: Schema.optional(NonNegativeInt),
67
68
  rateLimit: Schema.optional(HttpRateLimitDetails),
68
69
  }) {
69
70
  }
70
- export class QuotaExceededError extends Schema.TaggedError("AI.Error.QuotaExceeded")("QuotaExceeded", ReasonFields) {
71
+ export class QuotaExceededError extends Schema.TaggedError("AI.Error.QuotaExceeded")("QuotaExceeded", {
72
+ ...ReasonFields,
73
+ retryAfterMs: Schema.optional(NonNegativeInt),
74
+ }) {
71
75
  }
72
76
  export class ContentPolicyError extends Schema.TaggedError("AI.Error.ContentPolicy")("ContentPolicy", ReasonFields) {
73
77
  }
74
78
  export class ProviderInternalError extends Schema.TaggedError("AI.Error.ProviderInternal")("ProviderInternal", {
75
79
  ...ReasonFields,
76
- retryAfterMs: Schema.optional(Schema.Number),
80
+ retryAfterMs: Schema.optional(NonNegativeInt),
77
81
  }) {
78
82
  }
79
83
  export const TransportType = Schema.Literals(["http", "websocket"]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
4
4
  "name": "@opencode/ai",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -34,7 +34,7 @@
34
34
  "devDependencies": {
35
35
  "@clack/prompts": "1.0.0-alpha.1",
36
36
  "@effect/platform-node": "4.0.1",
37
- "@opencode/http-recorder": "2.1.0",
37
+ "@opencode/http-recorder": "2.1.2",
38
38
  "@tsconfig/bun": "1.0.9",
39
39
  "@types/bun": "1.4.0",
40
40
  "@typescript/native": "npm:typescript@7.0.2",
@@ -44,7 +44,7 @@
44
44
  "@aws-sdk/credential-providers": "3.1057.0",
45
45
  "@smithy/eventstream-codec": "4.2.14",
46
46
  "@smithy/util-utf8": "4.2.2",
47
- "@opencode/schema": "2.1.0",
47
+ "@opencode/schema": "2.1.2",
48
48
  "aws4fetch": "1.0.20",
49
49
  "effect": "4.0.1",
50
50
  "google-auth-library": "10.5.0"