@opencode/ai 2.0.26 → 2.0.28

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 (108) hide show
  1. package/README.md +8 -2
  2. package/dist/cache-policy.js +20 -11
  3. package/dist/experimental/system-one.js +1 -1
  4. package/dist/media.d.ts +1 -1
  5. package/dist/media.js +7 -6
  6. package/dist/protocols/alibaba-chat.d.ts +2 -2
  7. package/dist/protocols/alibaba-messages.d.ts +1 -1
  8. package/dist/protocols/anthropic-messages.js +1 -1
  9. package/dist/protocols/bedrock-converse.js +4 -3
  10. package/dist/protocols/bedrock-event-stream.js +4 -3
  11. package/dist/protocols/gemini.d.ts +73 -9
  12. package/dist/protocols/gemini.js +111 -17
  13. package/dist/protocols/meta-responses.js +3 -2
  14. package/dist/protocols/mistral-chat.d.ts +1 -1
  15. package/dist/protocols/mistral-chat.js +4 -2
  16. package/dist/protocols/open-responses-channel.d.ts +1 -1
  17. package/dist/protocols/open-responses-channel.js +7 -2
  18. package/dist/protocols/openai-chat.js +6 -2
  19. package/dist/protocols/openai-responses.d.ts +5 -5
  20. package/dist/protocols/openai-responses.js +4 -3
  21. package/dist/protocols/shared.d.ts +6 -2
  22. package/dist/protocols/shared.js +29 -3
  23. package/dist/protocols/utils/bedrock-auth.js +1 -1
  24. package/dist/protocols/utils/bedrock-media.js +4 -3
  25. package/dist/protocols/utils/fal-queue.d.ts +1 -1
  26. package/dist/protocols/utils/media-input.js +4 -3
  27. package/dist/protocols/utils/responses-compaction.js +6 -1
  28. package/dist/protocols/utils/tool-schema.js +8 -2
  29. package/dist/providers/alibaba.d.ts +5 -5
  30. package/dist/providers/amazon-bedrock-mantle.js +2 -2
  31. package/dist/providers/anthropic-compatible.js +2 -2
  32. package/dist/providers/anthropic.js +2 -2
  33. package/dist/providers/azure.d.ts +2 -2
  34. package/dist/providers/azure.js +3 -3
  35. package/dist/providers/baseten.js +2 -2
  36. package/dist/providers/cerebras.js +2 -2
  37. package/dist/providers/cloudflare-ai-gateway.d.ts +2 -2
  38. package/dist/providers/cloudflare-ai-gateway.js +2 -2
  39. package/dist/providers/cloudflare-workers-ai.js +2 -2
  40. package/dist/providers/cohere/chat.js +1 -1
  41. package/dist/providers/cohere.js +1 -1
  42. package/dist/providers/deepinfra.js +2 -2
  43. package/dist/providers/deepseek.js +2 -2
  44. package/dist/providers/fireworks.js +2 -2
  45. package/dist/providers/google/interactions.js +2 -2
  46. package/dist/providers/google-vertex/interactions.d.ts +2 -0
  47. package/dist/providers/google-vertex/interactions.js +1 -0
  48. package/dist/providers/google-vertex/mistral.d.ts +2 -0
  49. package/dist/providers/google-vertex/mistral.js +1 -0
  50. package/dist/providers/google-vertex-chat.js +3 -3
  51. package/dist/providers/google-vertex-interactions.d.ts +112 -0
  52. package/dist/providers/google-vertex-interactions.js +64 -0
  53. package/dist/providers/google-vertex-messages.js +3 -3
  54. package/dist/providers/google-vertex-mistral.d.ts +124 -0
  55. package/dist/providers/google-vertex-mistral.js +69 -0
  56. package/dist/providers/google-vertex-responses.js +3 -3
  57. package/dist/providers/google-vertex-shared.d.ts +3 -1
  58. package/dist/providers/google-vertex-shared.js +29 -3
  59. package/dist/providers/google-vertex.d.ts +2 -1
  60. package/dist/providers/google-vertex.js +4 -5
  61. package/dist/providers/google.d.ts +2 -1
  62. package/dist/providers/google.js +2 -2
  63. package/dist/providers/groq.js +2 -2
  64. package/dist/providers/index.d.ts +3 -1
  65. package/dist/providers/index.js +3 -1
  66. package/dist/providers/meta.js +1 -1
  67. package/dist/providers/minimax/chat.js +1 -1
  68. package/dist/providers/minimax/responses.js +1 -1
  69. package/dist/providers/minimax.js +1 -1
  70. package/dist/providers/mistral.js +2 -2
  71. package/dist/providers/moonshot/messages.js +1 -1
  72. package/dist/providers/moonshot/responses.js +1 -1
  73. package/dist/providers/moonshot.js +1 -1
  74. package/dist/providers/openai-compatible-responses.js +2 -2
  75. package/dist/providers/openai-compatible.js +2 -2
  76. package/dist/providers/openai.d.ts +1 -1
  77. package/dist/providers/openai.js +2 -2
  78. package/dist/providers/{opencode-zen.d.ts → opencode.d.ts} +1 -1
  79. package/dist/providers/{opencode-zen.js → opencode.js} +2 -2
  80. package/dist/providers/openrouter.js +1 -1
  81. package/dist/providers/togetherai.js +2 -2
  82. package/dist/providers/venice.js +1 -1
  83. package/dist/providers/vercel-ai-gateway.js +2 -2
  84. package/dist/providers/xai.js +1 -1
  85. package/dist/providers/zai-coding-plan/messages.js +1 -1
  86. package/dist/providers/zai-coding-plan/responses.js +1 -1
  87. package/dist/providers/zai-coding-plan.js +1 -1
  88. package/dist/providers/zai.js +1 -1
  89. package/dist/route/auth.d.ts +1 -1
  90. package/dist/route/auth.js +2 -2
  91. package/dist/route/executor-service.d.ts +1 -1
  92. package/dist/route/executor.d.ts +1 -1
  93. package/dist/route/executor.js +1 -1
  94. package/dist/route/framing.js +1 -1
  95. package/dist/route/media-protocol.d.ts +1 -1
  96. package/dist/route/media-protocol.js +1 -1
  97. package/dist/route/media.js +1 -1
  98. package/dist/route/transport/http.d.ts +1 -1
  99. package/dist/route/transport/http.js +5 -4
  100. package/dist/route/transport/websocket-channel.d.ts +1 -1
  101. package/dist/route/transport/websocket.d.ts +4 -4
  102. package/dist/route/transport/websocket.js +4 -9
  103. package/dist/schema/messages.js +9 -8
  104. package/dist/schema/options.d.ts +15 -1
  105. package/dist/schema/options.js +25 -5
  106. package/dist/tool.d.ts +10 -0
  107. package/dist/tool.js +21 -2
  108. package/package.json +5 -5
package/README.md CHANGED
@@ -32,6 +32,11 @@ Anthropic Messages, Gemini, Bedrock Converse, and any OpenAI-compatible deployme
32
32
  `Google.configure({ apiKey }).interactions(modelID)` selects the Interactions API; `.model(modelID)` still selects
33
33
  GenerateContent. The package entrypoint is `@opencode/ai/providers/google/interactions`.
34
34
 
35
+ For Vertex AI, use `GoogleVertexInteractions.configure({ project, location: "global" }).model(modelID)` or
36
+ `@opencode/ai/providers/google-vertex/interactions`. It uses Application Default Credentials by default, accepts
37
+ an explicit `accessToken` or `auth` override, and supports Express mode with `apiKey`. Vertex metadata and raw usage
38
+ live under `providerMetadata.vertex`. The default is still full-history replay with `store: false`.
39
+
35
40
  ```ts
36
41
  const model = Google.configure({ apiKey }).interactions("gemini-3.8-flash")
37
42
  const response = yield* LLM.generate({
@@ -177,7 +182,7 @@ console.log(response.answers.department.choice)
177
182
  console.log(response.answers.refund.probability)
178
183
  ```
179
184
 
180
- `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
181
186
  `OPENCODE_API_KEY`. OpenRouter and Vercel AI Gateway use the same provider shape:
182
187
 
183
188
  ```ts
@@ -1316,13 +1321,14 @@ APIs have separate entrypoints:
1316
1321
  - `@opencode/ai/providers/openai-compatible/responses`
1317
1322
  - `@opencode/ai/providers/anthropic-compatible`
1318
1323
  - `@opencode/ai/providers/google-vertex/gemini`
1324
+ - `@opencode/ai/providers/google-vertex/interactions`
1319
1325
  - `@opencode/ai/providers/google-vertex/chat`
1320
1326
  - `@opencode/ai/providers/google-vertex/responses`
1321
1327
  - `@opencode/ai/providers/google-vertex/messages`
1322
1328
 
1323
1329
  OpenAI Responses has one semantic route and uses HTTP by default. Advanced callers may supply a per-call WebSocket channel executor through `StreamOptions`; transport policy does not change provider settings, model identity, or route identity. The provider-neutral Open Responses implementation owns the reusable WebSocket request and event contract, while each provider opts in with its own handshake and connection policy. Azure follows the same Chat/Responses split at `providers/azure/chat` and `providers/azure/responses`. Generic OpenAI-compatible Chat remains at `providers/openai-compatible`; the Responses adapter at `providers/openai-compatible/responses` uses the provider-neutral Open Responses protocol. OpenAI Responses extends that baseline with OpenAI tools, event variants, metadata, and defaults. Generic Anthropic Messages-compatible providers use `providers/anthropic-compatible`, which the named Anthropic provider composes. Google Gemini and Amazon Bedrock expose their single native API through their existing provider paths.
1324
1330
 
1325
- Vertex Gemini, Vertex Chat, Vertex Responses, and Vertex Messages are separate API entrypoints. All accept `project`, `location`, and an optional `accessToken`; when no explicit token or auth override is supplied they lazily use Google Application Default Credentials. Vertex Gemini instead selects express mode when `apiKey` or `GOOGLE_VERTEX_API_KEY` is present. Vertex Chat targets MaaS models through the OpenAI-compatible Chat Completions endpoint, while Vertex Responses targets Grok models and defaults `store` to `false` as required by Vertex. `providers/google-vertex` remains the default alias for `providers/google-vertex/gemini`.
1331
+ Vertex Gemini, Vertex Interactions, Vertex Chat, Vertex Responses, and Vertex Messages are separate API entrypoints. All accept `project`, `location`, and an optional `accessToken`; when no explicit token or auth override is supplied they lazily use Google Application Default Credentials. Vertex Gemini and Interactions instead select express mode when `apiKey` or `GOOGLE_VERTEX_API_KEY` is present (explicit `project` or `location` selects ADC over an ambient key). Vertex Chat targets MaaS models through the OpenAI-compatible Chat Completions endpoint, while Vertex Responses targets Grok models and defaults `store` to `false` as required by Vertex. `providers/google-vertex` remains the default alias for `providers/google-vertex/gemini`.
1326
1332
 
1327
1333
  Tuned Vertex Gemini deployments use model ids shaped like `endpoints/1234567890` and require OAuth or ADC; Vertex express-mode API keys support publisher models only.
1328
1334
 
@@ -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
+ };
@@ -1,5 +1,5 @@
1
1
  import { Effect, Schema } from "effect";
2
- import { Headers, HttpClientRequest } from "effect/unstable/http";
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
5
  import { AIError, HttpContext, HttpOptions, InvalidProviderOutputError, InvalidRequestError, ModelID, Usage, mergeJsonRecords, } from "../schema/index.js";
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]: {
package/dist/media.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export * as Media from "./media.js";
2
- import { Effect, Encoding, FileSystem, Schema, SchemaGetter } from "effect";
3
- import { HttpClientRequest } from "effect/unstable/http";
2
+ import { Effect, FileSystem, Schema, SchemaGetter } from "effect";
3
+ import { Base64 } from "effect/encoding";
4
+ import { HttpClientRequest } from "effect/http";
4
5
  import { ProviderID } from "./schema/ids.js";
5
6
  import { AIError, HttpContext, InvalidProviderOutputError, InvalidRequestError } from "./schema/errors.js";
6
7
  import { ProviderMetadata } from "./schema/options.js";
@@ -105,7 +106,7 @@ export class Asset {
105
106
  const source = this.source;
106
107
  if (source.type !== "bytes" && source.type !== "base64")
107
108
  return undefined;
108
- const base64 = source.type === "base64" ? source.data : (this.#base64 ??= Encoding.encodeBase64(source.data));
109
+ const base64 = source.type === "base64" ? source.data : (this.#base64 ??= Base64.encode(source.data));
109
110
  const mime = this.mediaType.toLowerCase();
110
111
  return { mime, base64, dataUrl: `data:${mime};base64,${base64}` };
111
112
  }
@@ -120,7 +121,7 @@ export class Asset {
120
121
  if (source.type === "ref")
121
122
  return Effect.fail(invalid(`Cannot materialize provider ref ${source.provider}:${source.id}`));
122
123
  const decoded = source.type === "base64"
123
- ? Effect.fromResult(Encoding.decodeBase64(source.data)).pipe(Effect.mapError((cause) => invalid(`Media asset contains invalid base64 data`, cause)))
124
+ ? Effect.fromResult(Base64.decode(source.data)).pipe(Effect.mapError((cause) => invalid(`Media asset contains invalid base64 data`, cause)))
124
125
  : download(source, this.headers);
125
126
  return decoded.pipe(Effect.tap((data) => Effect.sync(() => (this.#bytes = data))));
126
127
  });
@@ -132,7 +133,7 @@ export class Asset {
132
133
  return Effect.succeed(source.data);
133
134
  if (this.#base64 !== undefined)
134
135
  return Effect.succeed(this.#base64);
135
- return this.bytes().pipe(Effect.map((data) => (this.#base64 = Encoding.encodeBase64(data))));
136
+ return this.bytes().pipe(Effect.map((data) => (this.#base64 = Base64.encode(data))));
136
137
  });
137
138
  }
138
139
  dataUrl() {
@@ -145,7 +146,7 @@ export class Asset {
145
146
  toJSON() {
146
147
  const source = this.source;
147
148
  return {
148
- source: source.type === "bytes" ? { ...source, data: Encoding.encodeBase64(source.data) } : source,
149
+ source: source.type === "bytes" ? { ...source, data: Base64.encode(source.data) } : source,
149
150
  info: this.info,
150
151
  providerMetadata: this.providerMetadata,
151
152
  };
@@ -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
  }>>;
@@ -6,7 +6,7 @@ import { Auth } from "../route/auth.js";
6
6
  import { Endpoint } from "../route/endpoint.js";
7
7
  import { Framing } from "../route/framing.js";
8
8
  import { Protocol } from "../route/protocol.js";
9
- import { Headers } from "effect/unstable/http";
9
+ import { Headers } from "effect/http";
10
10
  import { HttpTransport } from "../route/transport/index.js";
11
11
  import { AIError, HttpOptions, LLMRequest, LLMEvent, mergeJsonRecords, Usage, } from "../schema/index.js";
12
12
  import { JsonObject, knownString, optionalArray, optionalNull, ProviderShared } from "./shared.js";
@@ -1,4 +1,5 @@
1
- import { Effect, Encoding, Schema } from "effect";
1
+ import { Effect, Schema } from "effect";
2
+ import { Base64 } from "effect/encoding";
2
3
  import { Route } from "../route/client.js";
3
4
  import { Endpoint } from "../route/endpoint.js";
4
5
  import { Protocol } from "../route/protocol.js";
@@ -489,7 +490,7 @@ const mapUsage = (usage, providerMetadataKey) => {
489
490
  providerMetadata: { [providerMetadataKey]: usage },
490
491
  });
491
492
  };
492
- const encodeRedactedContent = (chunks) => Encoding.encodeBase64(concatBytes(chunks));
493
+ const encodeRedactedContent = (chunks) => Base64.encode(concatBytes(chunks));
493
494
  const step = (state, event) => Effect.gen(function* () {
494
495
  if (event.contentBlockStart?.start?.toolUse) {
495
496
  const index = event.contentBlockStart.contentBlockIndex;
@@ -530,7 +531,7 @@ const step = (state, event) => Effect.gen(function* () {
530
531
  const redactedChunk = yield* (() => {
531
532
  if (reasoning.redactedContent === undefined)
532
533
  return Effect.succeed(undefined);
533
- return Effect.fromResult(Encoding.decodeBase64(reasoning.redactedContent)).pipe(Effect.mapError((cause) => ProviderShared.eventError(ADAPTER, "Bedrock Converse reasoningContent.redactedContent contains invalid base64 data", undefined, cause)));
534
+ return Effect.fromResult(Base64.decode(reasoning.redactedContent)).pipe(Effect.mapError((cause) => ProviderShared.eventError(ADAPTER, "Bedrock Converse reasoningContent.redactedContent contains invalid base64 data", undefined, cause)));
534
535
  })();
535
536
  const redactedChunks = state.reasoningRedactedContent[index] ?? [];
536
537
  if (redactedChunk !== undefined)
@@ -1,6 +1,7 @@
1
1
  import { EventStreamCodec } from "@smithy/eventstream-codec";
2
2
  import { fromUtf8, toUtf8 } from "@smithy/util-utf8";
3
- import { Effect, Encoding, Stream } from "effect";
3
+ import { Effect, Stream } from "effect";
4
+ import { Base64 } from "effect/encoding";
4
5
  import { AIError, AIErrorReason, InvalidProviderOutputError } from "../schema/index.js";
5
6
  import { Framing } from "../route/framing.js";
6
7
  import { ProviderShared } from "./shared.js";
@@ -31,7 +32,7 @@ const consumeFrames = (route) => (state, input) => Effect.gen(function* () {
31
32
  route,
32
33
  classification: "incomplete-stream",
33
34
  message: `Incomplete Bedrock Converse event-stream frame: ${remaining.length} buffered bytes remain at end of stream`,
34
- body: Encoding.encodeBase64(remaining),
35
+ body: Base64.encode(remaining),
35
36
  }),
36
37
  });
37
38
  return [state, []];
@@ -45,7 +46,7 @@ const consumeFrames = (route) => (state, input) => Effect.gen(function* () {
45
46
  break;
46
47
  const decoded = yield* Effect.try({
47
48
  try: () => eventCodec.decode(view.subarray(0, totalLength)),
48
- catch: (error) => ProviderShared.eventError(route, `Failed to decode Bedrock Converse event-stream frame: ${error instanceof Error ? error.message : String(error)}`, Encoding.encodeBase64(view.subarray(0, totalLength)), error),
49
+ catch: (error) => ProviderShared.eventError(route, `Failed to decode Bedrock Converse event-stream frame: ${error instanceof Error ? error.message : String(error)}`, Base64.encode(view.subarray(0, totalLength)), error),
49
50
  });
50
51
  cursor = { buffer: cursor.buffer, offset: cursor.offset + totalLength };
51
52
  const payload = utf8.decode(decoded.body);
@@ -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";
@@ -1,4 +1,5 @@
1
- import { Effect, Encoding, Schema } from "effect";
1
+ import { Effect, Schema } from "effect";
2
+ import { Base64 } from "effect/encoding";
2
3
  import { Protocol } from "../route/protocol.js";
3
4
  import { LLMEvent, LLMRequest, Message, ToolResultPart } from "../schema/index.js";
4
5
  import { OpenResponses } from "./open-responses.js";
@@ -80,7 +81,7 @@ const HOSTED_TOOLS = {
80
81
  return { type: "error", value: item.error };
81
82
  if (!item.result)
82
83
  return yield* ProviderShared.eventError(ADAPTER, "Meta returned an image without data", ProviderShared.encodeJson(raw));
83
- const data = yield* Effect.fromResult(Encoding.decodeBase64(item.result)).pipe(Effect.mapError((cause) => ProviderShared.eventError(ADAPTER, "Meta returned invalid image base64", ProviderShared.encodeJson(raw), cause)));
84
+ const data = yield* Effect.fromResult(Base64.decode(item.result)).pipe(Effect.mapError((cause) => ProviderShared.eventError(ADAPTER, "Meta returned invalid image base64", ProviderShared.encodeJson(raw), cause)));
84
85
  // Responses image items can omit output_format, including when PNG/JPEG was requested.
85
86
  const mime = item.output_format === undefined
86
87
  ? (detectMediaType(data) ?? "application/octet-stream")
@@ -32,7 +32,7 @@ export type ProviderOptionsInput = {
32
32
  readonly promptCacheKey?: string;
33
33
  readonly [key: string]: unknown;
34
34
  };
35
- declare const MistralBody: Schema.Struct<{
35
+ export declare const MistralBody: Schema.Struct<{
36
36
  readonly model: Schema.String;
37
37
  readonly messages: Schema.$Array<Schema.toTaggedUnion<"role", readonly [Schema.Struct<{
38
38
  readonly role: Schema.Literal<"system">;
@@ -78,7 +78,7 @@ const MistralOptions = Schema.Struct({
78
78
  promptMode: Schema.optional(Schema.Literal("reasoning")),
79
79
  promptCacheKey: Schema.optional(Schema.String),
80
80
  });
81
- const MistralBody = Schema.Struct({
81
+ export const MistralBody = Schema.Struct({
82
82
  model: Schema.String,
83
83
  messages: Schema.Array(MistralMessage),
84
84
  tools: optionalArray(MistralTool),
@@ -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,
@@ -1,4 +1,4 @@
1
- import { Headers } from "effect/unstable/http";
1
+ import { Headers } from "effect/http";
2
2
  import { HttpTransport, type Transport, type WebSocketChannelDriver } from "../route/transport/index.js";
3
3
  import { OpenResponsesContinuation } from "./open-responses-continuation.js";
4
4
  export interface Options {