@opencode-ai/ai 0.0.0-beta-17963 → 0.0.0-beta-18045

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 (39) hide show
  1. package/dist/protocols/anthropic-messages.d.ts +4 -0
  2. package/dist/protocols/anthropic-messages.js +23 -14
  3. package/dist/protocols/gemini.d.ts +40 -40
  4. package/dist/protocols/gemini.js +68 -50
  5. package/dist/protocols/open-responses-channel.js +3 -0
  6. package/dist/protocols/open-responses.d.ts +6 -6
  7. package/dist/protocols/open-responses.js +19 -7
  8. package/dist/protocols/openai-chat.d.ts +42 -2
  9. package/dist/protocols/openai-chat.js +37 -7
  10. package/dist/protocols/openai-compatible-responses.d.ts +1 -1
  11. package/dist/protocols/openai-responses.d.ts +6 -6
  12. package/dist/protocols/openai-responses.js +1 -2
  13. package/dist/protocols/shared.d.ts +2 -0
  14. package/dist/protocols/shared.js +11 -0
  15. package/dist/protocols/utils/open-responses-options.d.ts +4 -3
  16. package/dist/protocols/utils/open-responses-options.js +2 -1
  17. package/dist/protocols/utils/openai-options.d.ts +3 -3
  18. package/dist/protocols/utils/openai-options.js +1 -1
  19. package/dist/protocols/utils/tool-schema.js +1 -28
  20. package/dist/protocols/xai-responses.d.ts +1 -1
  21. package/dist/providers/amazon-bedrock-mantle.d.ts +11 -11
  22. package/dist/providers/anthropic-compatible.d.ts +1 -0
  23. package/dist/providers/anthropic.d.ts +1 -0
  24. package/dist/providers/azure.d.ts +8 -8
  25. package/dist/providers/cloudflare.d.ts +2 -2
  26. package/dist/providers/google-vertex-chat.d.ts +2 -2
  27. package/dist/providers/google-vertex-messages.d.ts +1 -0
  28. package/dist/providers/google-vertex-responses.d.ts +1 -1
  29. package/dist/providers/google-vertex.d.ts +7 -7
  30. package/dist/providers/google-vertex.js +13 -1
  31. package/dist/providers/google.d.ts +7 -7
  32. package/dist/providers/openai-compatible-responses.d.ts +1 -1
  33. package/dist/providers/openai-compatible.d.ts +9 -9
  34. package/dist/providers/openai-options.d.ts +6 -2
  35. package/dist/providers/openai.d.ts +12 -12
  36. package/dist/providers/openrouter.d.ts +22 -0
  37. package/dist/providers/openrouter.js +3 -2
  38. package/dist/providers/xai.d.ts +11 -11
  39. package/package.json +5 -5
@@ -15,6 +15,7 @@ export type ThinkingInput = {
15
15
  readonly type: "disabled";
16
16
  } | ({
17
17
  readonly type: "enabled";
18
+ readonly display?: "summarized" | "omitted";
18
19
  } & ({
19
20
  readonly budgetTokens: number;
20
21
  readonly budget_tokens?: number;
@@ -194,6 +195,7 @@ export declare const AnthropicMessagesBody: Schema.Struct<{
194
195
  thinking: Schema.optional<Schema.Union<readonly [Schema.Struct<{
195
196
  readonly type: Schema.tag<"enabled">;
196
197
  readonly budget_tokens: Schema.Number;
198
+ readonly display: Schema.optional<Schema.Literals<readonly ["summarized", "omitted"]>>;
197
199
  }>, Schema.Struct<{
198
200
  readonly type: Schema.tag<"adaptive">;
199
201
  readonly display: Schema.optional<Schema.Literals<readonly ["summarized", "omitted"]>>;
@@ -369,6 +371,7 @@ export declare const protocol: Protocol<{
369
371
  readonly thinking?: {
370
372
  readonly type: "enabled";
371
373
  readonly budget_tokens: number;
374
+ readonly display?: "summarized" | "omitted" | undefined;
372
375
  } | {
373
376
  readonly type: "adaptive";
374
377
  readonly display?: "summarized" | "omitted" | undefined;
@@ -611,6 +614,7 @@ export declare const route: Route<{
611
614
  readonly thinking?: {
612
615
  readonly type: "enabled";
613
616
  readonly budget_tokens: number;
617
+ readonly display?: "summarized" | "omitted" | undefined;
614
618
  } | {
615
619
  readonly type: "adaptive";
616
620
  readonly display?: "summarized" | "omitted" | undefined;
@@ -24,6 +24,7 @@ const SSE_EVENTS = new Set([
24
24
  "content_block_start",
25
25
  "content_block_delta",
26
26
  "content_block_stop",
27
+ "ping",
27
28
  "error",
28
29
  ]);
29
30
  export const framing = Framing.sseEvents(SSE_EVENTS);
@@ -145,6 +146,7 @@ const AnthropicThinking = Schema.Union([
145
146
  Schema.Struct({
146
147
  type: Schema.tag("enabled"),
147
148
  budget_tokens: Schema.Number,
149
+ display: Schema.optional(Schema.Literals(["summarized", "omitted"])),
148
150
  }),
149
151
  Schema.Struct({
150
152
  type: Schema.tag("adaptive"),
@@ -490,14 +492,11 @@ const resolveOptions = Effect.fn("AnthropicMessages.resolveOptions")(function* (
490
492
  const resolveThinking = Effect.fn("AnthropicMessages.resolveThinking")(function* (input) {
491
493
  if (!ProviderShared.isRecord(input))
492
494
  return undefined;
493
- if (input.type === "adaptive") {
494
- const display = input.display === "summarized"
495
- ? "summarized"
496
- : input.display === "omitted"
497
- ? "omitted"
498
- : undefined;
495
+ const display = input.display === "summarized" || input.display === "omitted"
496
+ ? input.display
497
+ : undefined;
498
+ if (input.type === "adaptive")
499
499
  return { type: "adaptive", ...(display === undefined ? {} : { display }) };
500
- }
501
500
  if (input.type === "disabled")
502
501
  return { type: "disabled" };
503
502
  if (input.type !== "enabled")
@@ -509,7 +508,7 @@ const resolveThinking = Effect.fn("AnthropicMessages.resolveThinking")(function*
509
508
  : undefined;
510
509
  if (budget === undefined)
511
510
  return yield* ProviderShared.invalidRequest("Anthropic thinking provider option requires budgetTokens");
512
- return { type: "enabled", budget_tokens: budget };
511
+ return { type: "enabled", budget_tokens: budget, ...(display === undefined ? {} : { display }) };
513
512
  });
514
513
  const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (request) {
515
514
  const generation = request.generation;
@@ -655,7 +654,9 @@ const onContentBlockStart = (state, event) => {
655
654
  const block = event.content_block;
656
655
  if (!block)
657
656
  return [state, NO_EVENTS];
658
- if ((block.type === "tool_use" || block.type === "server_tool_use") && event.index !== undefined) {
657
+ if (block.type === "tool_use" || block.type === "server_tool_use") {
658
+ if (event.index === undefined || !block.id)
659
+ return [state, NO_EVENTS];
659
660
  const events = [];
660
661
  const lifecycle = Lifecycle.stepStart(state.lifecycle, events);
661
662
  return [
@@ -663,7 +664,7 @@ const onContentBlockStart = (state, event) => {
663
664
  ...state,
664
665
  lifecycle,
665
666
  tools: ToolStream.start(state.tools, event.index, {
666
- id: block.id ?? String(event.index),
667
+ id: block.id,
667
668
  name: block.name ?? "",
668
669
  input: block.input !== undefined && (!ProviderShared.isRecord(block.input) || Object.keys(block.input).length > 0)
669
670
  ? ProviderShared.encodeJson(block.input)
@@ -674,7 +675,7 @@ const onContentBlockStart = (state, event) => {
674
675
  [
675
676
  ...events,
676
677
  LLMEvent.toolInputStart({
677
- id: block.id ?? String(event.index),
678
+ id: block.id,
678
679
  name: block.name ?? "",
679
680
  providerExecuted: block.type === "server_tool_use" ? true : undefined,
680
681
  }),
@@ -829,16 +830,24 @@ const providerErrorMessage = (event) => {
829
830
  return `${type}: ${message}`;
830
831
  return message || type || "Anthropic Messages stream error";
831
832
  };
832
- const onError = (event) => new AIError({
833
+ const onError = (event) => Effect.fail(new AIError({
833
834
  module: ADAPTER,
834
835
  method: "stream",
835
836
  reason: classifyProviderFailure({ message: providerErrorMessage(event), code: event.error?.type }),
836
- });
837
+ }));
837
838
  const step = (state, event) => {
838
839
  if (event.type === "message_start")
839
840
  return Effect.succeed(onMessageStart(state, event));
840
- if (event.type === "content_block_start")
841
+ if (event.type === "content_block_start") {
842
+ const block = event.content_block;
843
+ if (block && (block.type === "tool_use" || block.type === "server_tool_use")) {
844
+ if (event.index === undefined)
845
+ return Effect.fail(ProviderShared.eventError(ADAPTER, `Anthropic ${block.type} missing index`));
846
+ if (!block.id)
847
+ return Effect.fail(ProviderShared.eventError(ADAPTER, `Anthropic tool_use missing id at index ${event.index}`));
848
+ }
841
849
  return Effect.succeed(onContentBlockStart(state, event));
850
+ }
842
851
  if (event.type === "content_block_delta")
843
852
  return onContentBlockDelta(state, event);
844
853
  if (event.type === "content_block_stop")
@@ -21,11 +21,11 @@ export type ProviderOptionsInput = OptionsInput;
21
21
  declare const GeminiBody: Schema.Struct<{
22
22
  cachedContent: Schema.optional<Schema.String>;
23
23
  contents: Schema.$Array<Schema.Struct<{
24
- readonly role: Schema.Literals<readonly ["user", "model"]>;
25
- readonly parts: Schema.$Array<Schema.Union<readonly [Schema.Struct<{
24
+ readonly role: Schema.optional<Schema.NullOr<Schema.Literals<readonly ["user", "model"]>>>;
25
+ readonly parts: Schema.optional<Schema.NullOr<Schema.$Array<Schema.Union<readonly [Schema.Struct<{
26
26
  readonly text: Schema.String;
27
- readonly thought: Schema.optional<Schema.Boolean>;
28
- readonly thoughtSignature: Schema.optional<Schema.String>;
27
+ readonly thought: Schema.optional<Schema.NullOr<Schema.Boolean>>;
28
+ readonly thoughtSignature: Schema.optional<Schema.NullOr<Schema.String>>;
29
29
  }>, Schema.Struct<{
30
30
  readonly inlineData: Schema.Struct<{
31
31
  readonly mimeType: Schema.String;
@@ -33,11 +33,11 @@ declare const GeminiBody: Schema.Struct<{
33
33
  }>;
34
34
  }>, Schema.Struct<{
35
35
  readonly functionCall: Schema.Struct<{
36
- readonly id: Schema.optional<Schema.String>;
36
+ readonly id: Schema.optional<Schema.NullOr<Schema.String>>;
37
37
  readonly name: Schema.String;
38
38
  readonly args: Schema.optional<Schema.Unknown>;
39
39
  }>;
40
- readonly thoughtSignature: Schema.optional<Schema.String>;
40
+ readonly thoughtSignature: Schema.optional<Schema.NullOr<Schema.String>>;
41
41
  }>, Schema.Struct<{
42
42
  readonly functionResponse: Schema.Struct<{
43
43
  readonly id: Schema.optional<Schema.String>;
@@ -50,7 +50,7 @@ declare const GeminiBody: Schema.Struct<{
50
50
  }>;
51
51
  }>>>;
52
52
  }>;
53
- }>]>>;
53
+ }>]>>>>;
54
54
  }>>;
55
55
  labels: Schema.optional<Schema.$Record<Schema.String, Schema.String>>;
56
56
  safetySettings: Schema.optional<Schema.$Array<Schema.Struct<{
@@ -99,23 +99,22 @@ export type GeminiBody = Schema.Schema.Type<typeof GeminiBody>;
99
99
  */
100
100
  export declare const protocol: Protocol<{
101
101
  readonly contents: readonly {
102
- readonly role: "model" | "user";
103
- readonly parts: readonly ({
102
+ readonly parts?: readonly ({
104
103
  readonly inlineData: {
105
104
  readonly mimeType: string;
106
105
  readonly data: string;
107
106
  };
108
107
  } | {
109
108
  readonly text: string;
110
- readonly thought?: boolean | undefined;
111
- readonly thoughtSignature?: string | undefined;
109
+ readonly thought?: boolean | null | undefined;
110
+ readonly thoughtSignature?: string | null | undefined;
112
111
  } | {
113
112
  readonly functionCall: {
114
113
  readonly name: string;
115
- readonly id?: string | undefined;
114
+ readonly id?: string | null | undefined;
116
115
  readonly args?: unknown;
117
116
  };
118
- readonly thoughtSignature?: string | undefined;
117
+ readonly thoughtSignature?: string | null | undefined;
119
118
  } | {
120
119
  readonly functionResponse: {
121
120
  readonly name: string;
@@ -128,7 +127,8 @@ export declare const protocol: Protocol<{
128
127
  };
129
128
  }[] | undefined;
130
129
  };
131
- })[];
130
+ })[] | null | undefined;
131
+ readonly role?: "model" | "user" | null | undefined;
132
132
  }[];
133
133
  readonly tools?: readonly {
134
134
  readonly functionDeclarations: readonly {
@@ -177,23 +177,22 @@ export declare const protocol: Protocol<{
177
177
  }, string, {
178
178
  readonly candidates?: readonly {
179
179
  readonly content?: {
180
- readonly role: "model" | "user";
181
- readonly parts: readonly ({
180
+ readonly parts?: readonly ({
182
181
  readonly inlineData: {
183
182
  readonly mimeType: string;
184
183
  readonly data: string;
185
184
  };
186
185
  } | {
187
186
  readonly text: string;
188
- readonly thought?: boolean | undefined;
189
- readonly thoughtSignature?: string | undefined;
187
+ readonly thought?: boolean | null | undefined;
188
+ readonly thoughtSignature?: string | null | undefined;
190
189
  } | {
191
190
  readonly functionCall: {
192
191
  readonly name: string;
193
- readonly id?: string | undefined;
192
+ readonly id?: string | null | undefined;
194
193
  readonly args?: unknown;
195
194
  };
196
- readonly thoughtSignature?: string | undefined;
195
+ readonly thoughtSignature?: string | null | undefined;
197
196
  } | {
198
197
  readonly functionResponse: {
199
198
  readonly name: string;
@@ -206,46 +205,46 @@ export declare const protocol: Protocol<{
206
205
  };
207
206
  }[] | undefined;
208
207
  };
209
- })[];
210
- } | undefined;
211
- readonly finishReason?: string | undefined;
212
- }[] | undefined;
208
+ })[] | null | undefined;
209
+ readonly role?: "model" | "user" | null | undefined;
210
+ } | null | undefined;
211
+ readonly finishReason?: string | null | undefined;
212
+ }[] | null | undefined;
213
213
  readonly promptFeedback?: {
214
214
  readonly [x: string]: unknown;
215
- readonly blockReason?: string | undefined;
216
- readonly blockReasonMessage?: string | undefined;
215
+ readonly blockReason?: string | null | undefined;
216
+ readonly blockReasonMessage?: string | null | undefined;
217
217
  readonly safetyRatings?: unknown;
218
- } | undefined;
218
+ } | null | undefined;
219
219
  readonly usageMetadata?: {
220
- readonly cachedContentTokenCount?: number | undefined;
221
- readonly thoughtsTokenCount?: number | undefined;
222
- readonly promptTokenCount?: number | undefined;
223
- readonly candidatesTokenCount?: number | undefined;
224
- readonly totalTokenCount?: number | undefined;
225
- } | undefined;
220
+ readonly cachedContentTokenCount?: number | null | undefined;
221
+ readonly thoughtsTokenCount?: number | null | undefined;
222
+ readonly promptTokenCount?: number | null | undefined;
223
+ readonly candidatesTokenCount?: number | null | undefined;
224
+ readonly totalTokenCount?: number | null | undefined;
225
+ } | null | undefined;
226
226
  }, {
227
227
  hasToolCalls: boolean;
228
228
  lifecycle: Lifecycle.State;
229
229
  }>;
230
230
  export declare const route: Route<{
231
231
  readonly contents: readonly {
232
- readonly role: "model" | "user";
233
- readonly parts: readonly ({
232
+ readonly parts?: readonly ({
234
233
  readonly inlineData: {
235
234
  readonly mimeType: string;
236
235
  readonly data: string;
237
236
  };
238
237
  } | {
239
238
  readonly text: string;
240
- readonly thought?: boolean | undefined;
241
- readonly thoughtSignature?: string | undefined;
239
+ readonly thought?: boolean | null | undefined;
240
+ readonly thoughtSignature?: string | null | undefined;
242
241
  } | {
243
242
  readonly functionCall: {
244
243
  readonly name: string;
245
- readonly id?: string | undefined;
244
+ readonly id?: string | null | undefined;
246
245
  readonly args?: unknown;
247
246
  };
248
- readonly thoughtSignature?: string | undefined;
247
+ readonly thoughtSignature?: string | null | undefined;
249
248
  } | {
250
249
  readonly functionResponse: {
251
250
  readonly name: string;
@@ -258,7 +257,8 @@ export declare const route: Route<{
258
257
  };
259
258
  }[] | undefined;
260
259
  };
261
- })[];
260
+ })[] | null | undefined;
261
+ readonly role?: "model" | "user" | null | undefined;
262
262
  }[];
263
263
  readonly tools?: readonly {
264
264
  readonly functionDeclarations: readonly {
@@ -6,7 +6,7 @@ import { Endpoint } from "../route/endpoint.js";
6
6
  import { Framing } from "../route/framing.js";
7
7
  import { Protocol } from "../route/protocol.js";
8
8
  import { LLMEvent, Usage, } from "../schema/index.js";
9
- import { JsonObject, optionalArray, ProviderShared } from "./shared.js";
9
+ import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared.js";
10
10
  import { GeminiToolSchema } from "./utils/gemini-tool-schema.js";
11
11
  import { Lifecycle } from "./utils/lifecycle.js";
12
12
  import { ToolSchemaProjection } from "./utils/tool-schema.js";
@@ -29,13 +29,24 @@ const requiresThoughtSignatureFallback = (modelID) => {
29
29
  // Gemini 3 accepts media nested inside function responses; matched Gemini 2.5 variants reject it,
30
30
  // so their tool-result attachments lower as a separate user turn instead.
31
31
  const routesLegacyToolMedia = (modelID) => /gemini-2[.-]5(?:[.-]|$)/i.test(modelID);
32
+ // Blacklist: Gemini 1.x/2.x ignore or reject explicit function call ids.
33
+ // Every other model id (Gemini 3+, gemma, anything unrecognized) gets them.
34
+ const omitsFunctionCallIds = (modelID) => {
35
+ const match = /^gemini(?:-live)?-(\d+)/i.exec(modelID);
36
+ return match !== null && Number(match[1]) < 3;
37
+ };
32
38
  // =============================================================================
33
39
  // Request Body Schema
34
40
  // =============================================================================
41
+ // Gemini is known to send explicit `null` for optional streaming fields
42
+ // (usage counts, flags, whole subtrees), so every response-side optional uses
43
+ // `optionalNull` instead of bare `Schema.optional`. The same part/content
44
+ // schemas lower the outbound request body; encoding drops `undefined` keys,
45
+ // so the shared schemas stay safe there.
35
46
  const GeminiTextPart = Schema.Struct({
36
47
  text: Schema.String,
37
- thought: Schema.optional(Schema.Boolean),
38
- thoughtSignature: Schema.optional(Schema.String),
48
+ thought: optionalNull(Schema.Boolean),
49
+ thoughtSignature: optionalNull(Schema.String),
39
50
  });
40
51
  const GeminiInlineDataPart = Schema.Struct({
41
52
  inlineData: Schema.Struct({
@@ -45,11 +56,11 @@ const GeminiInlineDataPart = Schema.Struct({
45
56
  });
46
57
  const GeminiFunctionCallPart = Schema.Struct({
47
58
  functionCall: Schema.Struct({
48
- id: Schema.optional(Schema.String),
59
+ id: optionalNull(Schema.String),
49
60
  name: Schema.String,
50
61
  args: Schema.optional(Schema.Unknown),
51
62
  }),
52
- thoughtSignature: Schema.optional(Schema.String),
63
+ thoughtSignature: optionalNull(Schema.String),
53
64
  });
54
65
  const GeminiFunctionResponsePart = Schema.Struct({
55
66
  functionResponse: Schema.Struct({
@@ -66,8 +77,8 @@ const GeminiContentPart = Schema.Union([
66
77
  GeminiFunctionResponsePart,
67
78
  ]);
68
79
  const GeminiContent = Schema.Struct({
69
- role: Schema.Literals(["user", "model"]),
70
- parts: Schema.Array(GeminiContentPart),
80
+ role: optionalNull(Schema.Literals(["user", "model"])),
81
+ parts: optionalNull(Schema.Array(GeminiContentPart)),
71
82
  });
72
83
  const GeminiSystemInstruction = Schema.Struct({
73
84
  parts: Schema.Array(Schema.Struct({ text: Schema.String })),
@@ -119,25 +130,25 @@ const GeminiBodyFields = {
119
130
  };
120
131
  const GeminiBody = Schema.Struct(GeminiBodyFields);
121
132
  const GeminiUsage = Schema.Struct({
122
- cachedContentTokenCount: Schema.optional(Schema.Number),
123
- thoughtsTokenCount: Schema.optional(Schema.Number),
124
- promptTokenCount: Schema.optional(Schema.Number),
125
- candidatesTokenCount: Schema.optional(Schema.Number),
126
- totalTokenCount: Schema.optional(Schema.Number),
133
+ cachedContentTokenCount: optionalNull(Schema.Number),
134
+ thoughtsTokenCount: optionalNull(Schema.Number),
135
+ promptTokenCount: optionalNull(Schema.Number),
136
+ candidatesTokenCount: optionalNull(Schema.Number),
137
+ totalTokenCount: optionalNull(Schema.Number),
127
138
  });
128
139
  const GeminiCandidate = Schema.Struct({
129
- content: Schema.optional(GeminiContent),
130
- finishReason: Schema.optional(Schema.String),
140
+ content: optionalNull(GeminiContent),
141
+ finishReason: optionalNull(Schema.String),
131
142
  });
132
143
  const GeminiPromptFeedback = Schema.StructWithRest(Schema.Struct({
133
- blockReason: Schema.optional(Schema.String),
134
- blockReasonMessage: Schema.optional(Schema.String),
135
- safetyRatings: Schema.optional(Schema.Unknown),
144
+ blockReason: optionalNull(Schema.String),
145
+ blockReasonMessage: optionalNull(Schema.String),
146
+ safetyRatings: optionalNull(Schema.Unknown),
136
147
  }), [Schema.Record(Schema.String, Schema.Unknown)]);
137
148
  const GeminiEvent = Schema.Struct({
138
- candidates: optionalArray(GeminiCandidate),
139
- promptFeedback: Schema.optional(GeminiPromptFeedback),
140
- usageMetadata: Schema.optional(GeminiUsage),
149
+ candidates: optionalNull(Schema.Array(GeminiCandidate)),
150
+ promptFeedback: optionalNull(GeminiPromptFeedback),
151
+ usageMetadata: optionalNull(GeminiUsage),
141
152
  });
142
153
  // =============================================================================
143
154
  // Tool Schema Conversion
@@ -188,18 +199,13 @@ const thoughtSignature = (providerMetadata) => {
188
199
  ? google.thoughtSignature
189
200
  : undefined;
190
201
  };
191
- const functionCallId = (providerMetadata) => {
192
- const google = providerMetadata?.google;
193
- return ProviderShared.isRecord(google) && typeof google.functionCallId === "string"
194
- ? google.functionCallId
195
- : undefined;
196
- };
197
- const lowerToolCall = (part) => ({
198
- functionCall: { id: functionCallId(part.providerMetadata), name: part.name, args: part.input },
202
+ const lowerToolCall = (part, omitIds) => ({
203
+ functionCall: { ...(omitIds ? {} : { id: part.id }), name: part.name, args: part.input },
199
204
  thoughtSignature: thoughtSignature(part.providerMetadata),
200
205
  });
201
206
  const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request) {
202
207
  const contents = [];
208
+ const omitCallIds = omitsFunctionCallIds(request.model.id);
203
209
  const legacyToolMedia = routesLegacyToolMedia(request.model.id);
204
210
  let pendingMedia;
205
211
  const flushMedia = () => {
@@ -216,8 +222,8 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request) {
216
222
  const previous = contents.at(-1);
217
223
  // Gemini rejects a continuation whose function-response turn carries extra
218
224
  // parts, so an update after a tool result starts its own user turn.
219
- if (previous?.role === "user" && !previous.parts.some((item) => "functionResponse" in item))
220
- contents[contents.length - 1] = { role: "user", parts: [...previous.parts, { text: part.text }] };
225
+ if (previous?.role === "user" && !(previous.parts ?? []).some((item) => "functionResponse" in item))
226
+ contents[contents.length - 1] = { role: "user", parts: [...(previous.parts ?? []), { text: part.text }] };
221
227
  else
222
228
  contents.push({ role: "user", parts: [{ text: part.text }] });
223
229
  continue;
@@ -248,7 +254,7 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request) {
248
254
  continue;
249
255
  }
250
256
  if (part.type === "tool-call") {
251
- const lowered = lowerToolCall(part);
257
+ const lowered = lowerToolCall(part, omitCallIds);
252
258
  const signature = lowered.thoughtSignature;
253
259
  parts.push({
254
260
  ...lowered,
@@ -272,7 +278,7 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request) {
272
278
  if (part.result.type !== "content") {
273
279
  parts.push({
274
280
  functionResponse: {
275
- id: functionCallId(part.providerMetadata),
281
+ ...(omitCallIds ? {} : { id: part.id }),
276
282
  name: part.name,
277
283
  response: {
278
284
  name: part.name,
@@ -295,7 +301,7 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request) {
295
301
  (pendingMedia ??= []).push(...media);
296
302
  parts.push({
297
303
  functionResponse: {
298
- id: functionCallId(part.providerMetadata),
304
+ ...(omitCallIds ? {} : { id: part.id }),
299
305
  name: part.name,
300
306
  response: {
301
307
  name: part.name,
@@ -308,8 +314,8 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request) {
308
314
  // Gemini requires every response to a parallel call batch in one user turn,
309
315
  // so consecutive tool results join the open function-response turn.
310
316
  const previous = contents.at(-1);
311
- if (previous?.role === "user" && previous.parts.some((item) => "functionResponse" in item))
312
- contents[contents.length - 1] = { role: "user", parts: [...previous.parts, ...parts] };
317
+ if (previous?.role === "user" && (previous.parts ?? []).some((item) => "functionResponse" in item))
318
+ contents[contents.length - 1] = { role: "user", parts: [...(previous.parts ?? []), ...parts] };
313
319
  else
314
320
  contents.push({ role: "user", parts });
315
321
  }
@@ -388,20 +394,25 @@ const fromRequest = Effect.fn("Gemini.fromRequest")(function* (request) {
388
394
  const mapUsage = (usage) => {
389
395
  if (!usage)
390
396
  return undefined;
391
- const cached = usage.cachedContentTokenCount;
392
- const nonCached = ProviderShared.subtractTokens(usage.promptTokenCount, cached);
397
+ // Explicit provider nulls decode as `null`; normalize to `undefined` so the
398
+ // token arithmetic below treats them like absent counts.
399
+ const promptTokens = usage.promptTokenCount ?? undefined;
400
+ const cached = usage.cachedContentTokenCount ?? undefined;
401
+ const thoughts = usage.thoughtsTokenCount ?? undefined;
402
+ const visible = usage.candidatesTokenCount ?? undefined;
403
+ const nonCached = ProviderShared.subtractTokens(promptTokens, cached);
393
404
  // `candidatesTokenCount` is visible-only; sum with thoughts to produce the
394
405
  // inclusive `outputTokens` the contract expects. Only compute the total
395
406
  // when the visible component is reported — otherwise we'd fabricate an
396
407
  // inclusive number from a partial breakdown.
397
- const outputTokens = usage.candidatesTokenCount !== undefined ? usage.candidatesTokenCount + (usage.thoughtsTokenCount ?? 0) : undefined;
408
+ const outputTokens = visible !== undefined ? visible + (thoughts ?? 0) : undefined;
398
409
  return new Usage({
399
- inputTokens: usage.promptTokenCount,
410
+ inputTokens: promptTokens,
400
411
  outputTokens,
401
412
  nonCachedInputTokens: nonCached,
402
413
  cacheReadInputTokens: cached,
403
- reasoningTokens: usage.thoughtsTokenCount,
404
- totalTokens: ProviderShared.totalTokens(usage.promptTokenCount, outputTokens, usage.totalTokenCount),
414
+ reasoningTokens: thoughts,
415
+ totalTokens: ProviderShared.totalTokens(promptTokens, outputTokens, usage.totalTokenCount ?? undefined),
405
416
  providerMetadata: { google: usage },
406
417
  });
407
418
  };
@@ -433,7 +444,9 @@ const mapFinishReason = (finishReason, hasToolCalls) => {
433
444
  return "unknown";
434
445
  };
435
446
  const finish = (state) => {
436
- const promptBlockReason = state.finishReason === undefined ? state.promptFeedback?.blockReason : undefined;
447
+ // `?? undefined` normalizes an explicit `null` blockReason back to absent so
448
+ // the "nothing to finish" check below keeps its meaning.
449
+ const promptBlockReason = state.finishReason === undefined ? (state.promptFeedback?.blockReason ?? undefined) : undefined;
437
450
  const finishReason = state.finishReason ?? promptBlockReason;
438
451
  if (finishReason === undefined && state.usage === undefined)
439
452
  return [];
@@ -470,7 +483,9 @@ const step = (state, event) => {
470
483
  let lifecycle = nextState.lifecycle;
471
484
  let reasoningSignature = nextState.reasoningSignature;
472
485
  let textSignature = nextState.textSignature;
473
- for (const part of candidate.content.parts) {
486
+ // Supplier ids must be tracked across chunks of the same response, not just within one event's parts.
487
+ const seenCallIds = new Set(nextState.seenCallIds);
488
+ for (const part of candidate.content.parts ?? []) {
474
489
  const signature = "thoughtSignature" in part && part.thoughtSignature ? part.thoughtSignature : undefined;
475
490
  // Gemini attaches replay signatures to thought parts, visible text, or function calls;
476
491
  // each block kind must retain the signature attached to its own parts.
@@ -490,20 +505,22 @@ const step = (state, event) => {
490
505
  }
491
506
  if ("functionCall" in part) {
492
507
  const input = part.functionCall.args === undefined ? {} : part.functionCall.args;
493
- // Gemini 2.0+ and Vertex supply a unique function call ID on the part; when omitted (e.g. Gemini 1.5),
508
+ // Gemini 2.0+ supplies a unique function call ID on the part; when omitted (e.g. Gemini 1.5),
494
509
  // generate a globally unique ID rather than a per-request counter to prevent cross-request collisions in downstream registries.
495
- const id = part.functionCall.id ?? `tool_${crypto.randomUUID().replaceAll("-", "")}`;
496
- const metadata = {
497
- ...(part.functionCall.id === undefined ? {} : { functionCallId: part.functionCall.id }),
498
- ...(part.thoughtSignature === undefined ? {} : { thoughtSignature: part.thoughtSignature }),
499
- };
510
+ // A repeated supplier id would replay as two identical calls, so only the first occurrence keeps it.
511
+ // A `null` supplier id normalizes to absent so the generated-id fallback applies.
512
+ const supplied = part.functionCall.id ?? undefined;
513
+ const duplicate = supplied !== undefined && seenCallIds.has(supplied);
514
+ if (supplied !== undefined)
515
+ seenCallIds.add(supplied);
516
+ const id = supplied !== undefined && !duplicate ? supplied : `tool_${crypto.randomUUID().replaceAll("-", "")}`;
500
517
  lifecycle = Lifecycle.reasoningEnd(lifecycle, events, "reasoning-0", reasoningSignature ? googleMetadata({ thoughtSignature: reasoningSignature }) : undefined);
501
518
  lifecycle = Lifecycle.stepStart(lifecycle, events);
502
519
  events.push(LLMEvent.toolCall({
503
520
  id,
504
521
  name: part.functionCall.name,
505
522
  input,
506
- providerMetadata: Object.keys(metadata).length > 0 ? googleMetadata(metadata) : undefined,
523
+ providerMetadata: part.thoughtSignature ? googleMetadata({ thoughtSignature: part.thoughtSignature }) : undefined,
507
524
  }));
508
525
  hasToolCalls = true;
509
526
  }
@@ -515,6 +532,7 @@ const step = (state, event) => {
515
532
  lifecycle,
516
533
  reasoningSignature,
517
534
  textSignature,
535
+ seenCallIds,
518
536
  finishReason: candidate.finishReason ?? nextState.finishReason,
519
537
  },
520
538
  events,
@@ -57,6 +57,9 @@ const driver = (options, body) => {
57
57
  responseID = created;
58
58
  return { type: "frame", frame };
59
59
  }
60
+ // Keepalives carry no response state and may arrive before response.created.
61
+ if (event.type === "keepalive")
62
+ return { type: "frame", frame };
60
63
  if (!responseID)
61
64
  return yield* ProviderShared.eventError(options.id, `${options.name} emitted ${event.type} before response.created`, frame);
62
65
  if (event.response?.id && event.response.id !== responseID)
@@ -207,7 +207,7 @@ export declare const coreFields: {
207
207
  }>>;
208
208
  top_logprobs: Schema.optional<Schema.Int>;
209
209
  truncation: Schema.optional<Schema.Literals<readonly ["auto", "disabled"]>>;
210
- service_tier: Schema.optional<Schema.Literals<readonly ["auto", "default", "flex", "priority"]>>;
210
+ service_tier: Schema.optional<Schema.declare<OpenResponsesOptions.ServiceTier, OpenResponsesOptions.ServiceTier>>;
211
211
  prompt_cache_key: Schema.optional<Schema.String>;
212
212
  include: Schema.optional<Schema.$Array<Schema.declare<OpenResponsesOptions.ResponseIncludable, OpenResponsesOptions.ResponseIncludable>>>;
213
213
  reasoning: Schema.optional<Schema.Struct<{
@@ -320,7 +320,7 @@ declare const OpenResponsesBody: Schema.Struct<{
320
320
  }>>;
321
321
  readonly top_logprobs: Schema.optional<Schema.Int>;
322
322
  readonly truncation: Schema.optional<Schema.Literals<readonly ["auto", "disabled"]>>;
323
- readonly service_tier: Schema.optional<Schema.Literals<readonly ["auto", "default", "flex", "priority"]>>;
323
+ readonly service_tier: Schema.optional<Schema.declare<OpenResponsesOptions.ServiceTier, OpenResponsesOptions.ServiceTier>>;
324
324
  readonly prompt_cache_key: Schema.optional<Schema.String>;
325
325
  readonly include: Schema.optional<Schema.$Array<Schema.declare<OpenResponsesOptions.ResponseIncludable, OpenResponsesOptions.ResponseIncludable>>>;
326
326
  readonly reasoning: Schema.optional<Schema.Struct<{
@@ -479,7 +479,7 @@ export declare const fromRequestWithExtension: (request: LLMRequest, extension:
479
479
  truncation?: "auto" | "disabled" | undefined;
480
480
  parallel_tool_calls?: boolean | undefined;
481
481
  max_tool_calls?: number | undefined;
482
- service_tier?: "default" | "auto" | "flex" | "priority" | undefined;
482
+ service_tier?: OpenResponsesOptions.ServiceTier | undefined;
483
483
  text?: {
484
484
  verbosity: OpenResponsesOptions.TextVerbosity;
485
485
  } | undefined;
@@ -640,7 +640,7 @@ export declare const fromRequest: (request: LLMRequest) => Effect.Effect<{
640
640
  readonly presence_penalty?: number | undefined;
641
641
  readonly safety_identifier?: string | undefined;
642
642
  readonly top_logprobs?: number | undefined;
643
- readonly service_tier?: "default" | "auto" | "flex" | "priority" | undefined;
643
+ readonly service_tier?: OpenResponsesOptions.ServiceTier | undefined;
644
644
  readonly max_output_tokens?: number | undefined;
645
645
  readonly max_tool_calls?: number | undefined;
646
646
  readonly parallel_tool_calls?: boolean | undefined;
@@ -950,7 +950,7 @@ export declare const protocol: Protocol<{
950
950
  readonly presence_penalty?: number | undefined;
951
951
  readonly safety_identifier?: string | undefined;
952
952
  readonly top_logprobs?: number | undefined;
953
- readonly service_tier?: "default" | "auto" | "flex" | "priority" | undefined;
953
+ readonly service_tier?: OpenResponsesOptions.ServiceTier | undefined;
954
954
  readonly max_output_tokens?: number | undefined;
955
955
  readonly max_tool_calls?: number | undefined;
956
956
  readonly parallel_tool_calls?: boolean | undefined;
@@ -1121,7 +1121,7 @@ export declare const httpTransport: HttpTransport.HttpJsonTransport<{
1121
1121
  readonly presence_penalty?: number | undefined;
1122
1122
  readonly safety_identifier?: string | undefined;
1123
1123
  readonly top_logprobs?: number | undefined;
1124
- readonly service_tier?: "default" | "auto" | "flex" | "priority" | undefined;
1124
+ readonly service_tier?: OpenResponsesOptions.ServiceTier | undefined;
1125
1125
  readonly max_output_tokens?: number | undefined;
1126
1126
  readonly max_tool_calls?: number | undefined;
1127
1127
  readonly parallel_tool_calls?: boolean | undefined;