@opencode/ai 2.0.27 → 2.0.29

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.
package/README.md CHANGED
@@ -182,7 +182,7 @@ console.log(response.answers.department.choice)
182
182
  console.log(response.answers.refund.probability)
183
183
  ```
184
184
 
185
- `TypeSafeAI` reads `TYPESAFE_API_KEY`. `OpenCodeZen` exposes the same selector and reads
185
+ `TypeSafeAI` reads `TYPESAFE_API_KEY`. `OpenCode` exposes the same selector and reads
186
186
  `OPENCODE_API_KEY`. OpenRouter and Vercel AI Gateway use the same provider shape:
187
187
 
188
188
  ```ts
@@ -143,17 +143,7 @@ const countHints = (request) => countToolHints(request.tools) +
143
143
  request.messages.reduce((count, message) => count +
144
144
  message.content.reduce((contentCount, part) => contentCount + ("cache" in part && part.cache !== undefined ? 1 : 0), 0), 0);
145
145
  export const applyCachePolicy = (request) => {
146
- const route = request.model.route.id;
147
- if (!RESPECTS_INLINE_HINTS.has(route))
148
- return request;
149
- const policy = (route === "openrouter" || route === "vercel-ai-gateway-messages") &&
150
- (request.cache === undefined || request.cache === "auto")
151
- ? gatewayPolicy(request.model.id)
152
- : route === "alibaba-chat" && (request.cache === undefined || request.cache === "auto")
153
- ? request.model.id.toLowerCase().startsWith("qwen")
154
- ? QWEN
155
- : NONE
156
- : resolve(request.cache);
146
+ const policy = selectPolicy(request);
157
147
  if (!policy.tools && !policy.system && !policy.messages)
158
148
  return request;
159
149
  const hint = makeHint(policy.ttlSeconds);
@@ -165,3 +155,22 @@ export const applyCachePolicy = (request) => {
165
155
  return request;
166
156
  return LLMRequest.update(request, { tools, system, messages });
167
157
  };
158
+ // A declared cache format overrides the route allowlist and its model-ID heuristics.
159
+ const selectPolicy = (request) => {
160
+ const format = request.model.compatibility?.cacheFormat;
161
+ const route = request.model.route.id;
162
+ const auto = request.cache === undefined || request.cache === "auto";
163
+ if (format === "alibaba")
164
+ return auto ? QWEN : { ...resolve(request.cache), tools: false };
165
+ if (format === "anthropic")
166
+ return resolve(request.cache);
167
+ if (!RESPECTS_INLINE_HINTS.has(route))
168
+ return NONE;
169
+ if (!auto)
170
+ return resolve(request.cache);
171
+ if (route === "openrouter" || route === "vercel-ai-gateway-messages")
172
+ return gatewayPolicy(request.model.id);
173
+ if (route === "alibaba-chat")
174
+ return request.model.id.toLowerCase().startsWith("qwen") ? QWEN : NONE;
175
+ return AUTO;
176
+ };
package/dist/media.d.ts CHANGED
@@ -100,12 +100,12 @@ export declare class Asset {
100
100
  };
101
101
  info: {
102
102
  readonly format?: string | undefined;
103
+ readonly encoding?: string | undefined;
103
104
  readonly width?: number | undefined;
104
105
  readonly height?: number | undefined;
105
106
  readonly durationSeconds?: number | undefined;
106
107
  readonly sampleRate?: number | undefined;
107
108
  readonly channels?: number | undefined;
108
- readonly encoding?: string | undefined;
109
109
  } | undefined;
110
110
  providerMetadata: {
111
111
  readonly [x: string]: {
@@ -10,7 +10,7 @@ declare const Options: Schema.Struct<{
10
10
  readonly preserveThinking: Schema.optional<Schema.Boolean>;
11
11
  readonly clearThinking: Schema.optional<Schema.Boolean>;
12
12
  readonly thinking: Schema.optional<Schema.Struct<{
13
- readonly type: Schema.declare<"disabled" | (string & {}) | "adaptive", "disabled" | (string & {}) | "adaptive">;
13
+ readonly type: Schema.declare<(string & {}) | "disabled" | "adaptive", (string & {}) | "disabled" | "adaptive">;
14
14
  }>>;
15
15
  readonly toolStream: Schema.optional<Schema.Boolean>;
16
16
  readonly parallelToolCalls: Schema.optional<Schema.Boolean>;
@@ -140,7 +140,7 @@ export declare const protocol: Protocol<{
140
140
  readonly temperature?: number | undefined;
141
141
  readonly seed?: number | undefined;
142
142
  readonly thinking?: {
143
- readonly type: "disabled" | (string & {}) | "adaptive";
143
+ readonly type: (string & {}) | "disabled" | "adaptive";
144
144
  } | undefined;
145
145
  readonly tool_choice?: "required" | "auto" | "none" | {
146
146
  readonly type: "function";
@@ -5,7 +5,7 @@ import { OpenResponsesOptions } from "./utils/open-responses-options.js";
5
5
  declare const Options: Schema.Struct<{
6
6
  readonly effort: Schema.optional<Schema.declare<OpenResponsesOptions.ReasoningEffort, OpenResponsesOptions.ReasoningEffort>>;
7
7
  readonly thinking: Schema.optional<Schema.Struct<{
8
- readonly type: Schema.declare<"enabled" | "disabled" | (string & {}), "enabled" | "disabled" | (string & {})>;
8
+ readonly type: Schema.declare<(string & {}) | "enabled" | "disabled", (string & {}) | "enabled" | "disabled">;
9
9
  readonly budgetTokens: Schema.optional<Schema.Int>;
10
10
  readonly budget_tokens: Schema.optional<Schema.Int>;
11
11
  }>>;
@@ -1,6 +1,7 @@
1
1
  import { Schema } from "effect";
2
2
  import { Route } from "../route/client.js";
3
3
  import { Protocol } from "../route/protocol.js";
4
+ import { HttpTransport, type Transport, type TransportPrepareInput } from "../route/transport/index.js";
4
5
  import { type ProviderOptions } from "../schema/index.js";
5
6
  import { Lifecycle } from "./utils/lifecycle.js";
6
7
  export declare const DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
@@ -100,8 +101,54 @@ declare const GeminiBody: Schema.Struct<{
100
101
  readonly thinkingLevel: Schema.optional<Schema.declare<import("../schema/options.js").OpenString<"minimal" | "low" | "medium" | "high">, import("../schema/options.js").OpenString<"minimal" | "low" | "medium" | "high">>>;
101
102
  }>>;
102
103
  }>>;
104
+ continuationToken: Schema.optional<Schema.String>;
103
105
  }>;
104
106
  export type GeminiBody = Schema.Schema.Type<typeof GeminiBody>;
107
+ declare const GeminiUsage: Schema.Struct<{
108
+ readonly cachedContentTokenCount: Schema.optional<Schema.NullOr<Schema.Number>>;
109
+ readonly thoughtsTokenCount: Schema.optional<Schema.NullOr<Schema.Number>>;
110
+ readonly promptTokenCount: Schema.optional<Schema.NullOr<Schema.Number>>;
111
+ readonly candidatesTokenCount: Schema.optional<Schema.NullOr<Schema.Number>>;
112
+ readonly totalTokenCount: Schema.optional<Schema.NullOr<Schema.Number>>;
113
+ }>;
114
+ type GeminiUsage = Schema.Schema.Type<typeof GeminiUsage>;
115
+ declare const GeminiPromptFeedback: Schema.StructWithRest<Schema.Struct<{
116
+ readonly blockReason: Schema.optional<Schema.NullOr<Schema.String>>;
117
+ readonly blockReasonMessage: Schema.optional<Schema.NullOr<Schema.String>>;
118
+ readonly safetyRatings: Schema.optional<Schema.NullOr<Schema.Unknown>>;
119
+ }>, readonly [Schema.$Record<Schema.String, Schema.Unknown>]>;
120
+ type GeminiPromptFeedback = Schema.Schema.Type<typeof GeminiPromptFeedback>;
121
+ interface ParserState {
122
+ readonly route: string;
123
+ readonly providerMetadataKey: string;
124
+ readonly finishReason?: string;
125
+ readonly hasToolCalls: boolean;
126
+ readonly promptFeedback?: GeminiPromptFeedback;
127
+ readonly usage?: GeminiUsage;
128
+ /** Usage of the earlier requests of a response Gemini continued across requests. */
129
+ readonly continuedUsage?: ReadonlyArray<GeminiUsage>;
130
+ readonly lifecycle: Lifecycle.State;
131
+ readonly reasoningSignature?: string;
132
+ readonly textSignature?: string;
133
+ readonly reasoningId?: string;
134
+ readonly textId?: string;
135
+ readonly nextReasoningId: number;
136
+ readonly nextTextId: number;
137
+ readonly seenCallIds?: ReadonlySet<string>;
138
+ }
139
+ /** Marks where the transport starts another request for the same response; provider JSON can never match it. */
140
+ declare class NextRequest {
141
+ }
142
+ export interface Prepared {
143
+ readonly http: HttpTransport.HttpPrepared<string>;
144
+ readonly input: TransportPrepareInput<GeminiBody>;
145
+ }
146
+ /**
147
+ * SSE transport that continues responses Gemini splits across requests. Chunks carry `continuationToken` checkpoints,
148
+ * and a request that reaches its output cap finishes with `CONTINUATION`. The transport resends the original body with
149
+ * the latest token, which also resumes a request that ended or dropped before finishing.
150
+ */
151
+ export declare const transport: Transport<GeminiBody, Prepared, unknown>;
105
152
  /**
106
153
  * The Gemini protocol — request body construction, body schema, and the
107
154
  * streaming-event state machine shared by Google AI Studio and Vertex Gemini.
@@ -190,7 +237,8 @@ export declare const protocol: Protocol<{
190
237
  } | undefined;
191
238
  readonly maxOutputTokens?: number | undefined;
192
239
  } | undefined;
193
- }, string, {
240
+ readonly continuationToken?: string | undefined;
241
+ }, NextRequest | {
194
242
  readonly error?: unknown;
195
243
  readonly candidates?: readonly {
196
244
  readonly content?: {
@@ -213,13 +261,28 @@ export declare const protocol: Protocol<{
213
261
  readonly thoughtsTokenCount?: number | null | undefined;
214
262
  } | null | undefined;
215
263
  }, {
216
- route: string;
217
- providerMetadataKey: string;
218
- hasToolCalls: boolean;
219
- lifecycle: Lifecycle.State;
220
- nextReasoningId: number;
221
- nextTextId: number;
222
- }>;
264
+ readonly error?: unknown;
265
+ readonly candidates?: readonly {
266
+ readonly content?: {
267
+ readonly parts?: readonly unknown[] | null | undefined;
268
+ readonly role?: "model" | "user" | null | undefined;
269
+ } | null | undefined;
270
+ readonly finishReason?: string | null | undefined;
271
+ }[] | null | undefined;
272
+ readonly promptFeedback?: {
273
+ readonly [x: string]: unknown;
274
+ readonly blockReason?: string | null | undefined;
275
+ readonly blockReasonMessage?: string | null | undefined;
276
+ readonly safetyRatings?: unknown;
277
+ } | null | undefined;
278
+ readonly usageMetadata?: {
279
+ readonly promptTokenCount?: number | null | undefined;
280
+ readonly candidatesTokenCount?: number | null | undefined;
281
+ readonly totalTokenCount?: number | null | undefined;
282
+ readonly cachedContentTokenCount?: number | null | undefined;
283
+ readonly thoughtsTokenCount?: number | null | undefined;
284
+ } | null | undefined;
285
+ } | NextRequest, ParserState>;
223
286
  export declare const route: Route<{
224
287
  readonly contents: readonly {
225
288
  readonly parts?: readonly ({
@@ -304,5 +367,6 @@ export declare const route: Route<{
304
367
  } | undefined;
305
368
  readonly maxOutputTokens?: number | undefined;
306
369
  } | undefined;
307
- }, import("../route/transport/http.js").HttpPrepared<string>, import("../route/client.js").CompactionOperations | undefined>;
370
+ readonly continuationToken?: string | undefined;
371
+ }, Prepared, import("../route/client.js").CompactionOperations | undefined>;
308
372
  export * as Gemini from "./gemini.js";
@@ -1,10 +1,10 @@
1
- import { Effect, Option, Schema } from "effect";
1
+ import { Effect, Option, Schema, Stream } from "effect";
2
2
  import { Tool } from "@opencode/schema/tool";
3
3
  import { Route } from "../route/client.js";
4
4
  import { Auth } from "../route/auth.js";
5
5
  import { Endpoint } from "../route/endpoint.js";
6
- import { Framing } from "../route/framing.js";
7
6
  import { Protocol } from "../route/protocol.js";
7
+ import { HttpTransport, } from "../route/transport/index.js";
8
8
  import { AIError, LLMEvent, Usage, } from "../schema/index.js";
9
9
  import { classifyProviderFailure } from "../provider-error.js";
10
10
  import { Media } from "../media.js";
@@ -12,6 +12,7 @@ import { JsonObject, knownString, lenient, optionalArray, optionalNull, Provider
12
12
  import { GeminiGenerateContent } from "./utils/gemini-generate-content.js";
13
13
  import { Lifecycle } from "./utils/lifecycle.js";
14
14
  const ADAPTER = "gemini";
15
+ const CONTINUATION = "CONTINUATION";
15
16
  // Google documents this sentinel for replaying Gemini 3 function calls after their original signature was lost.
16
17
  const SKIP_THOUGHT_SIGNATURE_VALIDATOR = "skip_thought_signature_validator";
17
18
  // Gemini 2.5 rejects a budget under the model's minimum: 512 on Flash-Lite, the highest, and 128 on Pro.
@@ -162,6 +163,7 @@ const GeminiBodyFields = {
162
163
  tools: optionalArray(GeminiTool),
163
164
  toolConfig: Schema.optional(GeminiToolConfig),
164
165
  generationConfig: Schema.optional(GeminiGenerationConfig),
166
+ continuationToken: Schema.optional(Schema.String),
165
167
  };
166
168
  const GeminiBody = Schema.Struct(GeminiBodyFields);
167
169
  const GeminiUsage = Schema.Struct({
@@ -403,7 +405,7 @@ const fromRequest = Effect.fn("Gemini.fromRequest")(function* (request) {
403
405
  // `cachedContentTokenCount` subset. `candidatesTokenCount` is *exclusive*
404
406
  // of `thoughtsTokenCount` — visible-only, not a total — so we sum the two
405
407
  // to produce the inclusive `outputTokens` the rest of the contract expects.
406
- const mapUsage = (usage, metadataKey) => {
408
+ const mapUsage = (usage, metadataKey, metadata = usage ?? {}) => {
407
409
  if (!usage)
408
410
  return undefined;
409
411
  // Explicit provider nulls decode as `null`; normalize to `undefined` so the
@@ -425,7 +427,26 @@ const mapUsage = (usage, metadataKey) => {
425
427
  cacheReadInputTokens: cached,
426
428
  reasoningTokens: thoughts,
427
429
  totalTokens: ProviderShared.totalTokens(promptTokens, outputTokens, usage.totalTokenCount ?? undefined),
428
- providerMetadata: providerMetadata(metadataKey, usage),
430
+ providerMetadata: providerMetadata(metadataKey, metadata),
431
+ });
432
+ };
433
+ // Each continued request re-bills the output so far as input. Reporting the first request's input with all output keeps
434
+ // input plus output equal to the response's final context size, under-reporting billed input; the summed billed counts
435
+ // remain in provider metadata.
436
+ const mapContinuedUsage = (requests, metadataKey) => {
437
+ const sum = (field) => ProviderShared.sumTokens(...requests.map((usage) => usage[field] ?? undefined));
438
+ return mapUsage({
439
+ promptTokenCount: requests[0].promptTokenCount,
440
+ cachedContentTokenCount: requests[0].cachedContentTokenCount,
441
+ candidatesTokenCount: sum("candidatesTokenCount"),
442
+ thoughtsTokenCount: sum("thoughtsTokenCount"),
443
+ }, metadataKey, {
444
+ promptTokenCount: sum("promptTokenCount"),
445
+ cachedContentTokenCount: sum("cachedContentTokenCount"),
446
+ candidatesTokenCount: sum("candidatesTokenCount"),
447
+ thoughtsTokenCount: sum("thoughtsTokenCount"),
448
+ totalTokenCount: sum("totalTokenCount"),
449
+ requests: requests.length,
429
450
  });
430
451
  };
431
452
  const mapFinishReason = (finishReason, hasToolCalls) => {
@@ -451,7 +472,7 @@ const finish = (state) => {
451
472
  // the "nothing to finish" check below keeps its meaning.
452
473
  const promptBlockReason = state.finishReason === undefined ? (state.promptFeedback?.blockReason ?? undefined) : undefined;
453
474
  const finishReason = state.finishReason ?? promptBlockReason;
454
- if (finishReason === undefined && state.usage === undefined)
475
+ if (finishReason === undefined && state.usage === undefined && state.continuedUsage === undefined)
455
476
  return [];
456
477
  const events = [];
457
478
  let lifecycle = state.lifecycle;
@@ -468,7 +489,9 @@ const finish = (state) => {
468
489
  normalized: promptBlockReason === undefined ? mapFinishReason(finishReason, state.hasToolCalls) : "content-filter",
469
490
  raw: finishReason,
470
491
  },
471
- usage: state.usage,
492
+ usage: state.continuedUsage === undefined
493
+ ? mapUsage(state.usage, state.providerMetadataKey)
494
+ : mapContinuedUsage([...state.continuedUsage, state.usage ?? {}], state.providerMetadataKey),
472
495
  providerMetadata: state.promptFeedback === undefined
473
496
  ? undefined
474
497
  : providerMetadata(state.providerMetadataKey, { promptFeedback: state.promptFeedback }),
@@ -476,6 +499,18 @@ const finish = (state) => {
476
499
  return events;
477
500
  };
478
501
  const step = (state, event) => {
502
+ // Usage is cumulative within one request, so the next request of the same response counts from zero.
503
+ if (event instanceof NextRequest)
504
+ return Effect.succeed([
505
+ {
506
+ ...state,
507
+ finishReason: undefined,
508
+ promptFeedback: undefined,
509
+ usage: undefined,
510
+ continuedUsage: [...(state.continuedUsage ?? []), state.usage ?? {}],
511
+ },
512
+ [],
513
+ ]);
479
514
  if (ProviderShared.isRecord(event.error)) {
480
515
  const body = ProviderShared.encodeJson(event);
481
516
  return Effect.fail(new AIError({
@@ -492,21 +527,17 @@ const step = (state, event) => {
492
527
  }
493
528
  if ("error" in event)
494
529
  return Effect.fail(ProviderShared.eventError(state.route, `Invalid ${state.route} stream event`, ProviderShared.encodeJson(event)));
530
+ const candidate = event.candidates?.[0];
495
531
  const nextState = {
496
532
  ...state,
497
533
  promptFeedback: event.promptFeedback ?? state.promptFeedback,
498
- usage: event.usageMetadata
499
- ? (mapUsage(event.usageMetadata, state.providerMetadataKey) ?? state.usage)
500
- : state.usage,
534
+ usage: event.usageMetadata ?? state.usage,
535
+ finishReason: candidate?.finishReason ?? state.finishReason,
501
536
  };
502
- const candidate = event.candidates?.[0];
503
537
  if (candidate?.finishReason && mapFinishReason(candidate.finishReason, state.hasToolCalls) === "error")
504
538
  return Effect.fail(ProviderShared.eventError(state.route, `Gemini stopped with ${candidate.finishReason}`, ProviderShared.encodeJson(event)));
505
539
  if (!candidate?.content)
506
- return Effect.succeed([
507
- { ...nextState, finishReason: candidate?.finishReason ?? nextState.finishReason },
508
- [],
509
- ]);
540
+ return Effect.succeed([nextState, []]);
510
541
  const events = [];
511
542
  let hasToolCalls = nextState.hasToolCalls;
512
543
  let lifecycle = nextState.lifecycle;
@@ -625,12 +656,75 @@ const step = (state, event) => {
625
656
  nextReasoningId,
626
657
  nextTextId,
627
658
  seenCallIds,
628
- finishReason: candidate.finishReason ?? nextState.finishReason,
629
659
  },
630
660
  events,
631
661
  ]);
632
662
  };
633
663
  // =============================================================================
664
+ // Transport
665
+ // =============================================================================
666
+ const GeminiCheckpoint = Schema.Struct({
667
+ candidates: optionalNull(Schema.Array(Schema.Struct({
668
+ finishReason: optionalNull(Schema.String),
669
+ continuationToken: optionalNull(Schema.String),
670
+ }))),
671
+ });
672
+ const decodeCheckpoint = Schema.decodeUnknownOption(GeminiCheckpoint);
673
+ /** Marks where the transport starts another request for the same response; provider JSON can never match it. */
674
+ class NextRequest {
675
+ }
676
+ const sse = HttpTransport.sseJson.with();
677
+ /**
678
+ * SSE transport that continues responses Gemini splits across requests. Chunks carry `continuationToken` checkpoints,
679
+ * and a request that reaches its output cap finishes with `CONTINUATION`. The transport resends the original body with
680
+ * the latest token, which also resumes a request that ended or dropped before finishing.
681
+ */
682
+ export const transport = {
683
+ id: sse.id,
684
+ prepare: (input) => sse.prepare(input).pipe(Effect.map((http) => ({ http, input }))),
685
+ execute: (prepared, request, runtime) => Effect.gen(function* () {
686
+ const route = request.model.route.id;
687
+ const label = `${request.model.provider}/${route}`;
688
+ // Frames are parsed once here, so errors recover each frame's raw text for their body.
689
+ const raw = new WeakMap();
690
+ const respond = (execution) => Stream.suspend(() => {
691
+ let token;
692
+ let finishReason;
693
+ let dropped;
694
+ return execution.frames.pipe(Stream.mapEffect((frame) => ProviderShared.parseJson(route, frame, `Invalid ${label} stream event`).pipe(Effect.map((event) => {
695
+ if (typeof event === "object" && event !== null)
696
+ raw.set(event, frame);
697
+ const candidate = Option.getOrUndefined(decodeCheckpoint(event))?.candidates?.[0];
698
+ token = candidate?.continuationToken ?? (candidate?.finishReason ? undefined : token);
699
+ finishReason = candidate?.finishReason ?? finishReason;
700
+ return event;
701
+ }))), Stream.catchIf(
702
+ // A connection failure may resume; a timeout keeps its meaning and ends the response.
703
+ (error) => error.reason._tag === "Transport" && error.reason.code !== "Timeout", (error) => {
704
+ dropped = error;
705
+ return Stream.empty;
706
+ }), Stream.concat(Stream.suspend(() => {
707
+ if (token !== undefined && (finishReason === undefined || finishReason === CONTINUATION))
708
+ return Stream.concat(Stream.succeed(new NextRequest()), next(token));
709
+ if (dropped)
710
+ return Stream.fail(dropped);
711
+ if (finishReason === CONTINUATION)
712
+ return Stream.fail(ProviderShared.eventError(label, "Gemini continued the response without a token"));
713
+ return Stream.empty;
714
+ })));
715
+ });
716
+ const next = (continuationToken) => Stream.unwrap(sse
717
+ .prepare({ ...prepared.input, body: { ...prepared.input.body, continuationToken } })
718
+ .pipe(Effect.flatMap((continued) => sse.execute(continued, request, runtime)), Effect.map(respond)));
719
+ const first = yield* sse.execute(prepared.http, request, runtime);
720
+ return {
721
+ frames: respond(first),
722
+ http: first.http,
723
+ body: (frame) => (typeof frame === "object" && frame !== null ? raw.get(frame) : undefined),
724
+ };
725
+ }),
726
+ };
727
+ // =============================================================================
634
728
  // Protocol And Gemini Route
635
729
  // =============================================================================
636
730
  /**
@@ -646,7 +740,7 @@ export const protocol = Protocol.make({
646
740
  // Gemini's schema rules are this API's default, including for tuned endpoints whose IDs do not name Gemini.
647
741
  sanitizer: "gemini",
648
742
  stream: {
649
- event: Protocol.jsonEvent(GeminiEvent),
743
+ event: Schema.Union([Schema.instanceOf(NextRequest), GeminiEvent]),
650
744
  initial: (request) => ({
651
745
  route: `${request.model.provider}/${request.model.route.id}`,
652
746
  providerMetadataKey: request.model.route.providerMetadataKey ?? String(request.model.provider),
@@ -669,6 +763,6 @@ export const route = Route.make({
669
763
  baseURL: DEFAULT_BASE_URL,
670
764
  }),
671
765
  auth: Auth.none,
672
- framing: Framing.sse,
766
+ transport,
673
767
  });
674
768
  export * as Gemini from "./gemini.js";
@@ -317,7 +317,9 @@ export const fromRequest = Effect.fn("MistralChat.fromRequest")(function* (reque
317
317
  frequency_penalty: request.generation?.frequencyPenalty,
318
318
  presence_penalty: request.generation?.presencePenalty,
319
319
  stop: request.generation?.stop,
320
- prompt_cache_key: request.cache === "none" ? undefined : (options.promptCacheKey ?? request.promptCacheKey),
320
+ prompt_cache_key: ProviderShared.sendsPromptCacheKey(request)
321
+ ? (options.promptCacheKey ?? request.promptCacheKey)
322
+ : undefined,
321
323
  safe_prompt: options.safePrompt,
322
324
  document_image_limit: options.documentImageLimit,
323
325
  document_page_limit: options.documentPageLimit,
@@ -106,7 +106,12 @@ export const transport = (options) => {
106
106
  });
107
107
  return {
108
108
  http: {
109
- request: ProviderShared.jsonPost({ url: parts.url, body: parts.bodyText, headers: parts.headers }),
109
+ request: ProviderShared.jsonPost({
110
+ url: parts.url,
111
+ body: parts.bodyText,
112
+ headers: parts.headers,
113
+ encoding: input.request.http?.encoding,
114
+ }),
110
115
  framing: Framing.sse,
111
116
  middleware: input.middleware,
112
117
  },
@@ -9,6 +9,7 @@ import { Protocol } from "../route/protocol.js";
9
9
  import { AIError, AIErrorReason, InvalidProviderOutputError, LLMEvent, ProviderInternalError, UnknownProviderError, Usage, } from "../schema/index.js";
10
10
  import { classifyProviderFailure } from "../provider-error.js";
11
11
  import { isRecord, JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared.js";
12
+ import { cacheControl } from "./utils/cache.js";
12
13
  import { OpenAIOptions } from "./utils/openai-options.js";
13
14
  import { Lifecycle } from "./utils/lifecycle.js";
14
15
  import { ToolStream } from "./utils/tool-stream.js";
@@ -646,14 +647,15 @@ export const fromRequest = Effect.fn("OpenAIChat.fromRequest")(function* (reques
646
647
  const zaiToolStream = request.model.compatibility?.zaiToolStream ?? detectZaiToolStream(provider, baseURL, request.model.id);
647
648
  const hasHistory = hasToolHistory(request.messages);
648
649
  const hasActiveTools = flattened.tools.length > 0;
650
+ const lowering = { ...options, cacheControl: options.cacheControl ?? declaredCacheControl(request) };
649
651
  return {
650
652
  model: request.model.id,
651
- messages: yield* lowerMessages(flattened.request, options),
653
+ messages: yield* lowerMessages(flattened.request, lowering),
652
654
  tools: flattened.tools.length === 0
653
655
  ? hasHistory
654
656
  ? []
655
657
  : undefined
656
- : flattened.tools.map((tool) => lowerTool(tool, options, supportsStrictMode)),
658
+ : flattened.tools.map((tool) => lowerTool(tool, lowering, supportsStrictMode)),
657
659
  tool_choice: hasActiveTools && request.toolChoice ? yield* lowerToolChoice(request.toolChoice) : undefined,
658
660
  stream: true,
659
661
  ...(supportsUsageInStreaming ? { stream_options: { include_usage: true } } : {}),
@@ -670,6 +672,8 @@ export const fromRequest = Effect.fn("OpenAIChat.fromRequest")(function* (reques
670
672
  ...lowerOptions(request, supportsStore),
671
673
  };
672
674
  });
675
+ // Provider wrappers that pass their own marker writer keep it; otherwise a declared cache format adds one.
676
+ const declaredCacheControl = (request) => request.model.compatibility?.cacheFormat === undefined ? undefined : cacheControl();
673
677
  // =============================================================================
674
678
  // Stream Parsing
675
679
  // =============================================================================
@@ -2,7 +2,7 @@ import { Tool } from "@opencode/schema/tool";
2
2
  import { Effect, Schema } from "effect";
3
3
  import { Headers, HttpClientRequest } from "effect/http";
4
4
  import { Media } from "../media.js";
5
- import { AIError, LLMRequest, Message, ToolDefinition, type ContentPart, type MediaPart, type OpenString, type ProviderID, type ToolEntry, type ToolResultPart } from "../schema/index.js";
5
+ import { AIError, LLMRequest, Message, ToolDefinition, type ContentPart, type MediaPart, type OpenString, type ProviderID, type RequestEncoding, type ToolEntry, type ToolResultPart } from "../schema/index.js";
6
6
  import { Json, decodeJson, encodeJson } from "../utils/json.js";
7
7
  import { isRecord } from "../utils/record.js";
8
8
  export { Json, decodeJson, encodeJson, isRecord };
@@ -14,6 +14,7 @@ export declare const lenient: <const S extends Schema.Top>(schema: S) => Schema.
14
14
  /** Provider-defined string enum: known values for autocomplete, any string accepted at runtime. */
15
15
  export declare const knownString: <Known extends string>() => Schema.declare<OpenString<Known>, OpenString<Known>>;
16
16
  export declare const OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH = 64;
17
+ export declare const sendsPromptCacheKey: (request: LLMRequest) => boolean;
17
18
  export declare const promptCacheKey: (request: LLMRequest) => string | undefined;
18
19
  /**
19
20
  * Streaming tool-call accumulator. Adapters that build a tool call across
@@ -192,10 +193,13 @@ export declare const validateWith: <A, I, E extends {
192
193
  * send JSON with a stale content type. The body is passed pre-encoded so
193
194
  * routes can choose between
194
195
  * `Schema.encodeSync(payload)` and `ProviderShared.encodeJson(payload)`.
196
+ * A `zstd` or `gzip` encoding compresses the body and sets `content-encoding`;
197
+ * it falls back to plain JSON when the runtime has no zlib.
195
198
  */
196
199
  export declare const jsonPost: (input: {
197
200
  readonly url: string;
198
201
  readonly body: string;
199
202
  readonly headers?: Headers.Input;
203
+ readonly encoding?: RequestEncoding;
200
204
  }) => HttpClientRequest.HttpClientRequest;
201
205
  export * as ProviderShared from "./shared.js";
@@ -17,10 +17,13 @@ export const knownString = () => Schema.declare((value) => typeof value === "str
17
17
  expected: "string",
18
18
  });
19
19
  export const OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH = 64;
20
+ // Body-level `prompt_cache_key` is off when caching is disabled or the model
21
+ // explicitly rejects the field. Header-based affinity is unaffected.
22
+ export const sendsPromptCacheKey = (request) => request.cache !== "none" && request.model.compatibility?.supportsPromptCacheKey !== false;
20
23
  // OpenAI limits `prompt_cache_key` to 64 chars; DeepSeek and Zai inherit the same
21
24
  // limit via their OpenAI-compatible APIs. Clamp with unicode-aware slicing.
22
25
  export const promptCacheKey = (request) => {
23
- if (request.cache === "none" || request.promptCacheKey === undefined)
26
+ if (!sendsPromptCacheKey(request) || request.promptCacheKey === undefined)
24
27
  return undefined;
25
28
  const chars = Array.from(request.promptCacheKey);
26
29
  if (chars.length <= OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH)
@@ -279,6 +282,29 @@ export const validateWith = (decode) => (payload) => decode(payload).pipe(Effect
279
282
  * send JSON with a stale content type. The body is passed pre-encoded so
280
283
  * routes can choose between
281
284
  * `Schema.encodeSync(payload)` and `ProviderShared.encodeJson(payload)`.
285
+ * A `zstd` or `gzip` encoding compresses the body and sets `content-encoding`;
286
+ * it falls back to plain JSON when the runtime has no zlib.
282
287
  */
283
- export const jsonPost = (input) => HttpClientRequest.post(input.url).pipe(HttpClientRequest.setHeaders(Headers.set(Headers.fromInput(input.headers), "content-type", "application/json")), HttpClientRequest.bodyText(input.body, "application/json"));
288
+ export const jsonPost = (input) => {
289
+ const headers = Headers.set(Headers.fromInput(input.headers), "content-type", "application/json");
290
+ const compressed = compressBody(input.body, input.encoding);
291
+ if (!compressed)
292
+ return HttpClientRequest.post(input.url).pipe(HttpClientRequest.setHeaders(headers), HttpClientRequest.bodyText(input.body, "application/json"));
293
+ return HttpClientRequest.post(input.url).pipe(HttpClientRequest.setHeaders(Headers.set(headers, "content-encoding", compressed.encoding)), HttpClientRequest.bodyUint8Array(compressed.body, "application/json"));
294
+ };
295
+ function compressBody(text, encoding) {
296
+ if (encoding === undefined || encoding === "none")
297
+ return undefined;
298
+ // Looked up at call time instead of imported: a runtime without node:zlib or its zstd export
299
+ // (browsers, older Node, some workerd setups) sends plain JSON rather than failing to load this module.
300
+ const zlib = globalThis.process?.getBuiltinModule?.("node:zlib");
301
+ try {
302
+ const body = encoding === "zstd" ? zlib?.zstdCompressSync?.(text) : zlib?.gzipSync(text);
303
+ return body && { encoding, body };
304
+ }
305
+ catch {
306
+ // Compression is an optimization, so a failure sends plain JSON instead of failing the request.
307
+ return undefined;
308
+ }
309
+ }
284
310
  export * as ProviderShared from "./shared.js";
@@ -87,7 +87,12 @@ export const make = (adapter, lowerTools) => Effect.fn("ResponsesCompaction.exec
87
87
  auth: route.auth,
88
88
  encodeBody: Schema.encodeSync(Schema.fromJsonString(Body)),
89
89
  });
90
- const response = yield* executor.execute(ProviderShared.jsonPost({ url: parts.url, body: parts.bodyText, headers: parts.headers }), options?.http);
90
+ const response = yield* executor.execute(ProviderShared.jsonPost({
91
+ url: parts.url,
92
+ body: parts.bodyText,
93
+ headers: parts.headers,
94
+ encoding: request.http?.encoding,
95
+ }), options?.http);
91
96
  const text = yield* RequestExecutor.responseStream(response).pipe(Stream.decodeText(), Stream.runFold(() => "", (text, chunk) => text + chunk));
92
97
  const invalid = (message, cause) => new AIError({
93
98
  reason: new InvalidProviderOutputError({
@@ -129,7 +129,7 @@ export declare const routes: (Route<{
129
129
  readonly temperature?: number | undefined;
130
130
  readonly seed?: number | undefined;
131
131
  readonly thinking?: {
132
- readonly type: "disabled" | (string & {}) | "adaptive";
132
+ readonly type: (string & {}) | "disabled" | "adaptive";
133
133
  } | undefined;
134
134
  readonly tool_choice?: "required" | "auto" | "none" | {
135
135
  readonly type: "function";
@@ -611,7 +611,7 @@ export declare const configure: (input: Config) => {
611
611
  id: string & import("effect/Brand").Brand<"AI.ProviderID">;
612
612
  model: (id: string | ModelID) => import("../schema/options.js").LanguageModel<{
613
613
  readonly thinking?: {
614
- readonly type: "disabled" | (string & {}) | "adaptive";
614
+ readonly type: (string & {}) | "disabled" | "adaptive";
615
615
  } | undefined;
616
616
  readonly thinkingBudget?: number | undefined;
617
617
  readonly parallelToolCalls?: boolean | undefined;
@@ -636,7 +636,7 @@ export declare const configure: (input: Config) => {
636
636
  }, import("../route/client.js").CompactionOperations | undefined>;
637
637
  chat: (id: string | ModelID) => import("../schema/options.js").LanguageModel<{
638
638
  readonly thinking?: {
639
- readonly type: "disabled" | (string & {}) | "adaptive";
639
+ readonly type: (string & {}) | "disabled" | "adaptive";
640
640
  } | undefined;
641
641
  readonly thinkingBudget?: number | undefined;
642
642
  readonly parallelToolCalls?: boolean | undefined;
@@ -675,7 +675,7 @@ export declare const provider: {
675
675
  id: string & import("effect/Brand").Brand<"AI.ProviderID">;
676
676
  model: (id: string | ModelID) => import("../schema/options.js").LanguageModel<{
677
677
  readonly thinking?: {
678
- readonly type: "disabled" | (string & {}) | "adaptive";
678
+ readonly type: (string & {}) | "disabled" | "adaptive";
679
679
  } | undefined;
680
680
  readonly thinkingBudget?: number | undefined;
681
681
  readonly parallelToolCalls?: boolean | undefined;
@@ -700,7 +700,7 @@ export declare const provider: {
700
700
  }, import("../route/client.js").CompactionOperations | undefined>;
701
701
  chat: (id: string | ModelID) => import("../schema/options.js").LanguageModel<{
702
702
  readonly thinking?: {
703
- readonly type: "disabled" | (string & {}) | "adaptive";
703
+ readonly type: (string & {}) | "disabled" | "adaptive";
704
704
  } | undefined;
705
705
  readonly thinkingBudget?: number | undefined;
706
706
  readonly parallelToolCalls?: boolean | undefined;
@@ -109,7 +109,8 @@ export declare const routes: Route<{
109
109
  } | undefined;
110
110
  readonly maxOutputTokens?: number | undefined;
111
111
  } | undefined;
112
- }, import("../route/transport/http.js").HttpPrepared<string>, import("../route/client.js").CompactionOperations | undefined>[];
112
+ readonly continuationToken?: string | undefined;
113
+ }, Gemini.Prepared, import("../route/client.js").CompactionOperations | undefined>[];
113
114
  export declare const configure: (input?: Config) => {
114
115
  id: string & import("effect/Brand").Brand<"AI.ProviderID">;
115
116
  model: (modelID: string | ModelID) => import("../schema/options.js").LanguageModel<GeminiOptionsInput, import("../route/client.js").CompactionOperations | undefined>;
@@ -4,7 +4,6 @@ import { ProviderShared } from "../protocols/shared.js";
4
4
  import { Auth } from "../route/auth.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 { ProviderConfigurationError, ProviderID } from "../schema/index.js";
9
8
  import { GoogleVertexShared } from "./google-vertex-shared.js";
10
9
  export const id = ProviderID.make("google-vertex");
@@ -37,7 +36,7 @@ const route = Route.make({
37
36
  const serviceTier = request.providerOptions?.serviceTier;
38
37
  return typeof serviceTier === "string" ? { "x-vertex-ai-llm-shared-request-type": serviceTier } : {};
39
38
  },
40
- framing: Framing.sse,
39
+ transport: Gemini.transport,
41
40
  });
42
41
  export const routes = [route];
43
42
  const configuredRoute = (input, modelID) => {
@@ -96,7 +96,8 @@ export declare const routes: (import("../route/client.js").Route<{
96
96
  } | undefined;
97
97
  readonly maxOutputTokens?: number | undefined;
98
98
  } | undefined;
99
- }, import("../route/transport/http.js").HttpPrepared<string>, import("../route/client.js").CompactionOperations | undefined> | import("../route/client.js").Route<{
99
+ readonly continuationToken?: string | undefined;
100
+ }, Gemini.Prepared, import("../route/client.js").CompactionOperations | undefined> | import("../route/client.js").Route<{
100
101
  readonly input: readonly ({
101
102
  readonly type: "user_input" | "model_output";
102
103
  readonly content: readonly ({
@@ -34,7 +34,7 @@ export * as Moonshot from "./moonshot.js";
34
34
  export * as OpenAI from "./openai.js";
35
35
  export * as OpenAICompatible from "./openai-compatible.js";
36
36
  export * as OpenAICompatibleResponses from "./openai-compatible-responses.js";
37
- export * as OpenCodeZen from "./opencode-zen.js";
37
+ export * as OpenCode from "./opencode.js";
38
38
  export * as OpenRouter from "./openrouter.js";
39
39
  export * as Replicate from "./replicate.js";
40
40
  export * as Runway from "./runway.js";
@@ -34,7 +34,7 @@ export * as Moonshot from "./moonshot.js";
34
34
  export * as OpenAI from "./openai.js";
35
35
  export * as OpenAICompatible from "./openai-compatible.js";
36
36
  export * as OpenAICompatibleResponses from "./openai-compatible-responses.js";
37
- export * as OpenCodeZen from "./opencode-zen.js";
37
+ export * as OpenCode from "./opencode.js";
38
38
  export * as OpenRouter from "./openrouter.js";
39
39
  export * as Replicate from "./replicate.js";
40
40
  export * as Runway from "./runway.js";
@@ -29,4 +29,4 @@ export declare const provider: {
29
29
  export declare const experimental: {
30
30
  evaluation: (modelID: string | ModelID) => import("../experimental.js").EvaluationModel<import("../experimental.js").EvaluationOptions>;
31
31
  };
32
- export * as OpenCodeZen from "./opencode-zen.js";
32
+ export * as OpenCode from "./opencode.js";
@@ -2,7 +2,7 @@ import { HttpOptions, ProviderID } from "../schema/index.js";
2
2
  import { AuthOptions } from "../route/auth-options.js";
3
3
  import { SystemOne } from "../experimental/system-one.js";
4
4
  export const id = ProviderID.make("opencode");
5
- const baseURL = "https://opencode.ai/zen/v1";
5
+ const baseURL = "https://opencode.ai/inference/v1";
6
6
  export const configure = (input = {}) => {
7
7
  const evaluation = (modelID) => SystemOne.model({
8
8
  id: modelID,
@@ -17,4 +17,4 @@ export const configure = (input = {}) => {
17
17
  };
18
18
  export const provider = configure();
19
19
  export const experimental = provider.experimental;
20
- export * as OpenCodeZen from "./opencode-zen.js";
20
+ export * as OpenCode from "./opencode.js";
@@ -4,7 +4,7 @@ import { Auth } from "../auth.js";
4
4
  import { render as renderEndpoint } from "../endpoint.js";
5
5
  import { Framing } from "../framing.js";
6
6
  import * as ProviderShared from "../../protocols/shared.js";
7
- import { AIError, DEFAULT_HTTP_TIMEOUT_MS, mergeJsonRecords, TransportError, } from "../../schema/index.js";
7
+ import { AIError, DEFAULT_CHUNK_TIMEOUT_MS, DEFAULT_HTTP_TIMEOUT_MS, mergeJsonRecords, TransportError, } from "../../schema/index.js";
8
8
  import { RequestExecutor } from "../executor.js";
9
9
  const applyQuery = (url, query) => {
10
10
  if (!query)
@@ -46,6 +46,7 @@ export const httpJson = (input) => ({
46
46
  url: parts.url,
47
47
  body: parts.bodyText,
48
48
  headers: parts.headers,
49
+ encoding: prepareInput.request.http?.encoding,
49
50
  });
50
51
  return {
51
52
  request,
@@ -75,7 +76,7 @@ export const httpJson = (input) => ({
75
76
  const remaining = Duration.subtract(total, Duration.millis((yield* Clock.currentTimeMillis) - started));
76
77
  return {
77
78
  frames: prepared.framing.frame(RequestExecutor.responseStream(response).pipe(Stream.timeoutOrElse({
78
- duration: timeoutDuration(request.http?.chunkTimeout),
79
+ duration: timeoutDuration(request.http?.chunkTimeout, DEFAULT_CHUNK_TIMEOUT_MS),
79
80
  orElse: () => Stream.fail(timeout("read", "Timed out waiting for response data", http)),
80
81
  }), Stream.interruptWhen(Effect.sleep(remaining).pipe(Effect.andThen(Effect.fail(timeout("read", "Timed out waiting for the response to complete", http))))))),
81
82
  http,
@@ -83,7 +84,7 @@ export const httpJson = (input) => ({
83
84
  };
84
85
  }),
85
86
  });
86
- const timeoutDuration = (value) => value === false ? Duration.infinity : Duration.millis(value ?? DEFAULT_HTTP_TIMEOUT_MS);
87
+ const timeoutDuration = (value, fallback = DEFAULT_HTTP_TIMEOUT_MS) => value === false ? Duration.infinity : Duration.millis(value ?? fallback);
87
88
  export const sseJson = {
88
89
  id: "http-json/sse",
89
90
  with: () => httpJson({ framing: Framing.sse }),
@@ -12,8 +12,16 @@ export declare const mergeProviderOptions: (...items: ReadonlyArray<ProviderOpti
12
12
  /** Milliseconds for an HTTP timeout, or `false` to disable it. */
13
13
  export declare const HttpTimeout: Schema.Union<readonly [Schema.Number, Schema.Literal<false>]>;
14
14
  export type HttpTimeout = Schema.Schema.Type<typeof HttpTimeout>;
15
- /** Applied to `headerTimeout` and `chunkTimeout` when a request leaves them unset. */
15
+ /** Applied to `headerTimeout` when a request leaves it unset. */
16
16
  export declare const DEFAULT_HTTP_TIMEOUT_MS = 300000;
17
+ /**
18
+ * Applied to `chunkTimeout` when a request leaves it unset. Some providers, such as DeepSeek, have specifically asked
19
+ * clients to allow at least 15 minutes, because their models can stream nothing for minutes while they reason.
20
+ */
21
+ export declare const DEFAULT_CHUNK_TIMEOUT_MS = 900000;
22
+ /** `Content-Encoding` for JSON request bodies; `none` sends plain JSON. */
23
+ export declare const RequestEncoding: Schema.Literals<readonly ["zstd", "gzip", "none"]>;
24
+ export type RequestEncoding = Schema.Schema.Type<typeof RequestEncoding>;
17
25
  declare const HttpOptions_base: Schema.Class<HttpOptions, Schema.Struct<{
18
26
  readonly body: Schema.optional<Schema.$Record<Schema.String, Schema.Unknown>>;
19
27
  readonly headers: Schema.optional<Schema.$Record<Schema.String, Schema.String>>;
@@ -24,6 +32,8 @@ declare const HttpOptions_base: Schema.Class<HttpOptions, Schema.Struct<{
24
32
  readonly headerTimeout: Schema.optional<Schema.Union<readonly [Schema.Number, Schema.Literal<false>]>>;
25
33
  /** Time allowed between streamed response chunks once headers have arrived. */
26
34
  readonly chunkTimeout: Schema.optional<Schema.Union<readonly [Schema.Number, Schema.Literal<false>]>>;
35
+ /** Compression for JSON request bodies. Only set it for endpoints known to decode it. */
36
+ readonly encoding: Schema.optional<Schema.Literals<readonly ["zstd", "gzip", "none"]>>;
27
37
  }>, {}>;
28
38
  export declare class HttpOptions extends HttpOptions_base {
29
39
  }
@@ -89,6 +99,8 @@ export declare const LanguageModelSanitizerCompatibility: Schema.Literals<readon
89
99
  export type LanguageModelSanitizerCompatibility = Schema.Schema.Type<typeof LanguageModelSanitizerCompatibility>;
90
100
  export declare const LanguageModelMaxTokensFieldCompatibility: Schema.Literals<readonly ["max_completion_tokens", "max_tokens"]>;
91
101
  export type LanguageModelMaxTokensFieldCompatibility = Schema.Schema.Type<typeof LanguageModelMaxTokensFieldCompatibility>;
102
+ export declare const LanguageModelCacheFormatCompatibility: Schema.Literals<readonly ["anthropic", "alibaba"]>;
103
+ export type LanguageModelCacheFormatCompatibility = Schema.Schema.Type<typeof LanguageModelCacheFormatCompatibility>;
92
104
  declare const LanguageModelCompatibility_base: Schema.Class<LanguageModelCompatibility, Schema.Struct<{
93
105
  readonly sanitizer: Schema.optional<Schema.Literals<readonly ["gemini", "moonshot", "none"]>>;
94
106
  readonly reasoningField: Schema.optional<Schema.String>;
@@ -101,6 +113,8 @@ declare const LanguageModelCompatibility_base: Schema.Class<LanguageModelCompati
101
113
  readonly supportsUsageInStreaming: Schema.optional<Schema.Boolean>;
102
114
  readonly supportsStrictMode: Schema.optional<Schema.Boolean>;
103
115
  readonly supportsPromptCacheKey: Schema.optional<Schema.Boolean>;
116
+ /** Prompt-cache marker syntax the endpoint accepts. Overrides the route default; both send `cache_control`, and `alibaba` skips tool definitions. */
117
+ readonly cacheFormat: Schema.optional<Schema.Literals<readonly ["anthropic", "alibaba"]>>;
104
118
  readonly zaiToolStream: Schema.optional<Schema.Boolean>;
105
119
  readonly requireSignature: Schema.optional<Schema.Boolean>;
106
120
  /** Supports Anthropic's thinking-prefix mismatch controls. Overrides model-ID detection. */
@@ -34,8 +34,15 @@ export const ProviderMetadata = Schema.Record(Schema.String, Schema.Record(Schem
34
34
  export const mergeProviderOptions = (...items) => mergeJsonRecords(...items);
35
35
  /** Milliseconds for an HTTP timeout, or `false` to disable it. */
36
36
  export const HttpTimeout = Schema.Union([Schema.Number.check(Schema.isGreaterThan(0)), Schema.Literal(false)]);
37
- /** Applied to `headerTimeout` and `chunkTimeout` when a request leaves them unset. */
37
+ /** Applied to `headerTimeout` when a request leaves it unset. */
38
38
  export const DEFAULT_HTTP_TIMEOUT_MS = 300_000;
39
+ /**
40
+ * Applied to `chunkTimeout` when a request leaves it unset. Some providers, such as DeepSeek, have specifically asked
41
+ * clients to allow at least 15 minutes, because their models can stream nothing for minutes while they reason.
42
+ */
43
+ export const DEFAULT_CHUNK_TIMEOUT_MS = 900_000;
44
+ /** `Content-Encoding` for JSON request bodies; `none` sends plain JSON. */
45
+ export const RequestEncoding = Schema.Literals(["zstd", "gzip", "none"]);
39
46
  export class HttpOptions extends Schema.Class("AI.HttpOptions")({
40
47
  body: Schema.optional(JsonSchema),
41
48
  headers: Schema.optional(Schema.Record(Schema.String, Schema.String)),
@@ -46,6 +53,8 @@ export class HttpOptions extends Schema.Class("AI.HttpOptions")({
46
53
  headerTimeout: Schema.optional(HttpTimeout),
47
54
  /** Time allowed between streamed response chunks once headers have arrived. */
48
55
  chunkTimeout: Schema.optional(HttpTimeout),
56
+ /** Compression for JSON request bodies. Only set it for endpoints known to decode it. */
57
+ encoding: Schema.optional(RequestEncoding),
49
58
  }) {
50
59
  }
51
60
  (function (HttpOptions) {
@@ -63,9 +72,16 @@ export const mergeHttpOptions = (...items) => {
63
72
  const timeout = items.findLast((item) => item?.timeout !== undefined)?.timeout;
64
73
  const headerTimeout = items.findLast((item) => item?.headerTimeout !== undefined)?.headerTimeout;
65
74
  const chunkTimeout = items.findLast((item) => item?.chunkTimeout !== undefined)?.chunkTimeout;
66
- if (!body && !headers && !query && timeout === undefined && headerTimeout === undefined && chunkTimeout === undefined)
75
+ const encoding = items.findLast((item) => item?.encoding !== undefined)?.encoding;
76
+ if (!body &&
77
+ !headers &&
78
+ !query &&
79
+ timeout === undefined &&
80
+ headerTimeout === undefined &&
81
+ chunkTimeout === undefined &&
82
+ encoding === undefined)
67
83
  return undefined;
68
- return new HttpOptions({ body, headers, query, timeout, headerTimeout, chunkTimeout });
84
+ return new HttpOptions({ body, headers, query, timeout, headerTimeout, chunkTimeout, encoding });
69
85
  };
70
86
  export class GenerationOptions extends Schema.Class("LLM.GenerationOptions")({
71
87
  maxTokens: Schema.optional(Schema.Number),
@@ -119,6 +135,7 @@ export const ReasoningEffort = Schema.declare((value) => typeof value === "strin
119
135
  /** Tool schema sanitizer for a model family. `none` opts out of the protocol and model-name defaults. */
120
136
  export const LanguageModelSanitizerCompatibility = Schema.Literals(["gemini", "moonshot", "none"]);
121
137
  export const LanguageModelMaxTokensFieldCompatibility = Schema.Literals(["max_completion_tokens", "max_tokens"]);
138
+ export const LanguageModelCacheFormatCompatibility = Schema.Literals(["anthropic", "alibaba"]);
122
139
  export class LanguageModelCompatibility extends Schema.Class("LLM.LanguageModelCompatibility")({
123
140
  sanitizer: Schema.optional(LanguageModelSanitizerCompatibility),
124
141
  reasoningField: Schema.optional(Schema.String),
@@ -130,9 +147,12 @@ export class LanguageModelCompatibility extends Schema.Class("LLM.LanguageModelC
130
147
  supportsStore: Schema.optional(Schema.Boolean),
131
148
  supportsUsageInStreaming: Schema.optional(Schema.Boolean),
132
149
  supportsStrictMode: Schema.optional(Schema.Boolean),
133
- // Accepts `prompt_cache_key` in the Chat Completions body. Chat omits the
134
- // key unless this is set; session-affinity headers still flow regardless.
150
+ // Accepts `prompt_cache_key` in the request body. Chat omits the key unless
151
+ // this is true; other protocols omit it only when this is false.
152
+ // Session-affinity headers still flow regardless.
135
153
  supportsPromptCacheKey: Schema.optional(Schema.Boolean),
154
+ /** Prompt-cache marker syntax the endpoint accepts. Overrides the route default; both send `cache_control`, and `alibaba` skips tool definitions. */
155
+ cacheFormat: Schema.optional(LanguageModelCacheFormatCompatibility),
136
156
  zaiToolStream: Schema.optional(Schema.Boolean),
137
157
  requireSignature: Schema.optional(Schema.Boolean),
138
158
  /** Supports Anthropic's thinking-prefix mismatch controls. Overrides model-ID detection. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
- "version": "2.0.27",
3
+ "version": "2.0.29",
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.0.27",
37
+ "@opencode/http-recorder": "2.0.29",
38
38
  "@tsconfig/bun": "1.0.9",
39
39
  "@types/bun": "1.4.0",
40
40
  "@typescript/native-preview": "7.0.0-dev.20251207.1",
@@ -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.0.27",
47
+ "@opencode/schema": "2.0.29",
48
48
  "aws4fetch": "1.0.20",
49
49
  "effect": "4.0.1",
50
50
  "google-auth-library": "10.5.0"