@opencode-ai/ai 0.0.0-dev-17987 → 0.0.0-dev-18000

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.
@@ -29,6 +29,12 @@ 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
  // =============================================================================
@@ -188,18 +194,13 @@ const thoughtSignature = (providerMetadata) => {
188
194
  ? google.thoughtSignature
189
195
  : undefined;
190
196
  };
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 },
197
+ const lowerToolCall = (part, omitIds) => ({
198
+ functionCall: { ...(omitIds ? {} : { id: part.id }), name: part.name, args: part.input },
199
199
  thoughtSignature: thoughtSignature(part.providerMetadata),
200
200
  });
201
201
  const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request) {
202
202
  const contents = [];
203
+ const omitCallIds = omitsFunctionCallIds(request.model.id);
203
204
  const legacyToolMedia = routesLegacyToolMedia(request.model.id);
204
205
  let pendingMedia;
205
206
  const flushMedia = () => {
@@ -248,7 +249,7 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request) {
248
249
  continue;
249
250
  }
250
251
  if (part.type === "tool-call") {
251
- const lowered = lowerToolCall(part);
252
+ const lowered = lowerToolCall(part, omitCallIds);
252
253
  const signature = lowered.thoughtSignature;
253
254
  parts.push({
254
255
  ...lowered,
@@ -272,7 +273,7 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request) {
272
273
  if (part.result.type !== "content") {
273
274
  parts.push({
274
275
  functionResponse: {
275
- id: functionCallId(part.providerMetadata),
276
+ ...(omitCallIds ? {} : { id: part.id }),
276
277
  name: part.name,
277
278
  response: {
278
279
  name: part.name,
@@ -295,7 +296,7 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request) {
295
296
  (pendingMedia ??= []).push(...media);
296
297
  parts.push({
297
298
  functionResponse: {
298
- id: functionCallId(part.providerMetadata),
299
+ ...(omitCallIds ? {} : { id: part.id }),
299
300
  name: part.name,
300
301
  response: {
301
302
  name: part.name,
@@ -470,6 +471,8 @@ const step = (state, event) => {
470
471
  let lifecycle = nextState.lifecycle;
471
472
  let reasoningSignature = nextState.reasoningSignature;
472
473
  let textSignature = nextState.textSignature;
474
+ // Supplier ids must be tracked across chunks of the same response, not just within one event's parts.
475
+ const seenCallIds = new Set(nextState.seenCallIds);
473
476
  for (const part of candidate.content.parts) {
474
477
  const signature = "thoughtSignature" in part && part.thoughtSignature ? part.thoughtSignature : undefined;
475
478
  // Gemini attaches replay signatures to thought parts, visible text, or function calls;
@@ -490,20 +493,21 @@ const step = (state, event) => {
490
493
  }
491
494
  if ("functionCall" in part) {
492
495
  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),
496
+ // Gemini 2.0+ supplies a unique function call ID on the part; when omitted (e.g. Gemini 1.5),
494
497
  // 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
- };
498
+ // A repeated supplier id would replay as two identical calls, so only the first occurrence keeps it.
499
+ const supplied = part.functionCall.id;
500
+ const duplicate = supplied !== undefined && seenCallIds.has(supplied);
501
+ if (supplied !== undefined)
502
+ seenCallIds.add(supplied);
503
+ const id = supplied !== undefined && !duplicate ? supplied : `tool_${crypto.randomUUID().replaceAll("-", "")}`;
500
504
  lifecycle = Lifecycle.reasoningEnd(lifecycle, events, "reasoning-0", reasoningSignature ? googleMetadata({ thoughtSignature: reasoningSignature }) : undefined);
501
505
  lifecycle = Lifecycle.stepStart(lifecycle, events);
502
506
  events.push(LLMEvent.toolCall({
503
507
  id,
504
508
  name: part.functionCall.name,
505
509
  input,
506
- providerMetadata: Object.keys(metadata).length > 0 ? googleMetadata(metadata) : undefined,
510
+ providerMetadata: part.thoughtSignature === undefined ? undefined : googleMetadata({ thoughtSignature: part.thoughtSignature }),
507
511
  }));
508
512
  hasToolCalls = true;
509
513
  }
@@ -515,6 +519,7 @@ const step = (state, event) => {
515
519
  lifecycle,
516
520
  reasoningSignature,
517
521
  textSignature,
522
+ seenCallIds,
518
523
  finishReason: candidate.finishReason ?? nextState.finishReason,
519
524
  },
520
525
  events,
@@ -10,11 +10,23 @@ import { GoogleVertexShared } from "./google-vertex-shared.js";
10
10
  export const id = ProviderID.make("google-vertex");
11
11
  const fromRequest = Effect.fn("GoogleVertex.fromRequest")(function* (request) {
12
12
  const body = yield* Gemini.protocol.body.from(request);
13
+ // Vertex's native REST schema rejects `id` on FunctionCall/FunctionResponse parts with HTTP 400,
14
+ // unlike AI Studio, so history minted there cannot be lowered verbatim.
15
+ const contents = body.contents.map((content) => ({
16
+ ...content,
17
+ parts: content.parts.map((part) => {
18
+ if ("functionCall" in part)
19
+ return { ...part, functionCall: { ...part.functionCall, id: undefined } };
20
+ if ("functionResponse" in part)
21
+ return { ...part, functionResponse: { ...part.functionResponse, id: undefined } };
22
+ return part;
23
+ }),
24
+ }));
13
25
  const value = request.providerOptions?.labels;
14
26
  const labels = ProviderShared.isRecord(value)
15
27
  ? Object.fromEntries(Object.entries(value).filter((entry) => typeof entry[1] === "string"))
16
28
  : undefined;
17
- return { ...body, labels };
29
+ return { ...body, contents, labels };
18
30
  });
19
31
  const protocol = {
20
32
  ...Gemini.protocol,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
- "version": "0.0.0-dev-17987",
3
+ "version": "0.0.0-dev-18000",
4
4
  "name": "@opencode-ai/ai",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -30,7 +30,7 @@
30
30
  "devDependencies": {
31
31
  "@clack/prompts": "1.0.0-alpha.1",
32
32
  "@effect/platform-node": "4.0.0-rc.110",
33
- "@opencode-ai/http-recorder": "0.0.0-dev-17987",
33
+ "@opencode-ai/http-recorder": "0.0.0-dev-18000",
34
34
  "@tsconfig/bun": "1.0.9",
35
35
  "@types/bun": "1.3.13",
36
36
  "@typescript/native-preview": "7.0.0-dev.20251207.1",
@@ -39,7 +39,7 @@
39
39
  "dependencies": {
40
40
  "@smithy/eventstream-codec": "4.2.14",
41
41
  "@smithy/util-utf8": "4.2.2",
42
- "@opencode-ai/schema": "0.0.0-dev-17987",
42
+ "@opencode-ai/schema": "0.0.0-dev-18000",
43
43
  "aws4fetch": "1.0.20",
44
44
  "effect": "4.0.0-rc.110",
45
45
  "google-auth-library": "10.5.0"