@opencode-ai/ai 0.0.0-beta-17823 → 0.0.0-beta-17898

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.
@@ -225,7 +225,6 @@ export declare const protocol: Protocol<{
225
225
  } | undefined;
226
226
  }, {
227
227
  hasToolCalls: boolean;
228
- nextToolCallId: number;
229
228
  lifecycle: Lifecycle.State;
230
229
  }>;
231
230
  export declare const route: Route<{
@@ -26,6 +26,9 @@ const requiresThoughtSignatureFallback = (modelID) => {
26
26
  return false;
27
27
  return !/(^|\/)gemini-robotics-er-1\.5(?:[.-]|$)/i.test(modelID);
28
28
  };
29
+ // Gemini 3 accepts media nested inside function responses; matched Gemini 2.5 variants reject it,
30
+ // so their tool-result attachments lower as a separate user turn instead.
31
+ const routesLegacyToolMedia = (modelID) => /gemini-2[.-]5(?:[.-]|$)/i.test(modelID);
29
32
  // =============================================================================
30
33
  // Request Body Schema
31
34
  // =============================================================================
@@ -197,7 +200,17 @@ const lowerToolCall = (part) => ({
197
200
  });
198
201
  const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request) {
199
202
  const contents = [];
203
+ const legacyToolMedia = routesLegacyToolMedia(request.model.id);
204
+ let pendingMedia;
205
+ const flushMedia = () => {
206
+ if (!pendingMedia)
207
+ return;
208
+ contents.push({ role: "user", parts: [{ text: "Attached media from tool result:" }, ...pendingMedia] });
209
+ pendingMedia = undefined;
210
+ };
200
211
  for (const message of request.messages) {
212
+ if (message.role !== "tool")
213
+ flushMedia();
201
214
  if (message.role === "system") {
202
215
  const part = yield* ProviderShared.wrappedSystemUpdate("Gemini", message);
203
216
  const previous = contents.at(-1);
@@ -227,7 +240,7 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request) {
227
240
  if (!ProviderShared.supportsContent(part, ["text", "reasoning", "tool-call"]))
228
241
  return yield* ProviderShared.unsupportedContent("Gemini", "assistant", ["text", "reasoning", "tool-call"]);
229
242
  if (part.type === "text") {
230
- parts.push({ text: part.text });
243
+ parts.push({ text: part.text, thoughtSignature: thoughtSignature(part.providerMetadata) });
231
244
  continue;
232
245
  }
233
246
  if (part.type === "reasoning") {
@@ -278,6 +291,8 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request) {
278
291
  const value = ProviderShared.normalizeToolFile(item);
279
292
  media.push({ inlineData: { mimeType: value.mime, data: value.base64 } });
280
293
  }
294
+ if (legacyToolMedia && media.length > 0)
295
+ (pendingMedia ??= []).push(...media);
281
296
  parts.push({
282
297
  functionResponse: {
283
298
  id: functionCallId(part.providerMetadata),
@@ -286,7 +301,7 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request) {
286
301
  name: part.name,
287
302
  content: text.join("\n"),
288
303
  },
289
- parts: media.length > 0 ? media : undefined,
304
+ parts: legacyToolMedia || media.length === 0 ? undefined : media,
290
305
  },
291
306
  });
292
307
  }
@@ -298,6 +313,7 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request) {
298
313
  else
299
314
  contents.push({ role: "user", parts });
300
315
  }
316
+ flushMedia();
301
317
  return contents;
302
318
  });
303
319
  const resolveOptions = (request) => {
@@ -422,9 +438,11 @@ const finish = (state) => {
422
438
  if (finishReason === undefined && state.usage === undefined)
423
439
  return [];
424
440
  const events = [];
425
- const lifecycle = state.reasoningSignature
426
- ? Lifecycle.reasoningEnd(state.lifecycle, events, "reasoning-0", googleMetadata({ thoughtSignature: state.reasoningSignature }))
427
- : state.lifecycle;
441
+ let lifecycle = state.lifecycle;
442
+ if (state.reasoningSignature !== undefined)
443
+ lifecycle = Lifecycle.reasoningEnd(lifecycle, events, "reasoning-0", googleMetadata({ thoughtSignature: state.reasoningSignature }));
444
+ if (state.textSignature !== undefined)
445
+ lifecycle = Lifecycle.textEnd(lifecycle, events, "text-0", googleMetadata({ thoughtSignature: state.textSignature }));
428
446
  Lifecycle.finish(lifecycle, events, {
429
447
  reason: {
430
448
  normalized: promptBlockReason === undefined ? mapFinishReason(finishReason, state.hasToolCalls) : "content-filter",
@@ -450,23 +468,31 @@ const step = (state, event) => {
450
468
  const events = [];
451
469
  let hasToolCalls = nextState.hasToolCalls;
452
470
  let lifecycle = nextState.lifecycle;
453
- let nextToolCallId = nextState.nextToolCallId;
454
471
  let reasoningSignature = nextState.reasoningSignature;
472
+ let textSignature = nextState.textSignature;
455
473
  for (const part of candidate.content.parts) {
456
- if ("thoughtSignature" in part && part.thoughtSignature && "thought" in part && part.thought)
457
- reasoningSignature = part.thoughtSignature;
474
+ const signature = "thoughtSignature" in part && part.thoughtSignature ? part.thoughtSignature : undefined;
475
+ // Gemini attaches replay signatures to thought parts, visible text, or function calls;
476
+ // each block kind must retain the signature attached to its own parts.
477
+ if (signature !== undefined && "thought" in part && part.thought)
478
+ reasoningSignature = signature;
479
+ else if (signature !== undefined && "text" in part)
480
+ textSignature = signature;
458
481
  if ("text" in part && part.text.length > 0) {
459
482
  if (part.thought) {
460
- lifecycle = Lifecycle.reasoningDelta(lifecycle, events, "reasoning-0", part.text, part.thoughtSignature ? googleMetadata({ thoughtSignature: part.thoughtSignature }) : undefined);
483
+ lifecycle = Lifecycle.reasoningDelta(lifecycle, events, "reasoning-0", part.text, signature ? googleMetadata({ thoughtSignature: signature }) : undefined);
461
484
  continue;
462
485
  }
463
486
  lifecycle = Lifecycle.reasoningEnd(lifecycle, events, "reasoning-0", reasoningSignature ? googleMetadata({ thoughtSignature: reasoningSignature }) : undefined);
464
- lifecycle = Lifecycle.textDelta(lifecycle, events, "text-0", part.text);
487
+ lifecycle = Lifecycle.textDelta(lifecycle, events, "text-0", part.text, textSignature ? googleMetadata({ thoughtSignature: textSignature }) : undefined);
488
+ textSignature = undefined;
465
489
  continue;
466
490
  }
467
491
  if ("functionCall" in part) {
468
492
  const input = part.functionCall.args === undefined ? {} : part.functionCall.args;
469
- const id = `tool_${nextToolCallId++}`;
493
+ // Gemini 2.0+ and Vertex supply a unique function call ID on the part; when omitted (e.g. Gemini 1.5),
494
+ // 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("-", "")}`;
470
496
  const metadata = {
471
497
  ...(part.functionCall.id === undefined ? {} : { functionCallId: part.functionCall.id }),
472
498
  ...(part.thoughtSignature === undefined ? {} : { thoughtSignature: part.thoughtSignature }),
@@ -487,8 +513,8 @@ const step = (state, event) => {
487
513
  ...nextState,
488
514
  hasToolCalls,
489
515
  lifecycle,
490
- nextToolCallId,
491
516
  reasoningSignature,
517
+ textSignature,
492
518
  finishReason: candidate.finishReason ?? nextState.finishReason,
493
519
  },
494
520
  events,
@@ -509,7 +535,7 @@ export const protocol = Protocol.make({
509
535
  },
510
536
  stream: {
511
537
  event: Protocol.jsonEvent(GeminiEvent),
512
- initial: () => ({ hasToolCalls: false, nextToolCallId: 0, lifecycle: Lifecycle.initial() }),
538
+ initial: () => ({ hasToolCalls: false, lifecycle: Lifecycle.initial() }),
513
539
  step,
514
540
  onHalt: finish,
515
541
  },
@@ -61,7 +61,7 @@ export const model = (input) => {
61
61
  return Effect.succeed({ data: image.data, mediaType: image.mediaType });
62
62
  if (image.type === "url")
63
63
  return ImageInputs.decodeDataUrl(image.url, ADAPTER);
64
- return Effect.succeed(undefined);
64
+ return Effect.undefined;
65
65
  });
66
66
  const multipartMask = mask === undefined
67
67
  ? undefined
@@ -8,7 +8,7 @@ const invalid = (module, message) => new AIError({
8
8
  export const dataUrl = (input) => `data:${input.mediaType};base64,${Encoding.encodeBase64(input.data)}`;
9
9
  export const decodeDataUrl = (url, module) => {
10
10
  if (!url.startsWith("data:"))
11
- return Effect.succeed(undefined);
11
+ return Effect.undefined;
12
12
  const match = /^data:([^;,]+);base64,(.*)$/s.exec(url);
13
13
  if (!match)
14
14
  return Effect.fail(invalid(module, "Image data URLs must contain a MIME type and base64 data"));
@@ -7,7 +7,7 @@ export interface State {
7
7
  export declare const initial: () => State;
8
8
  export declare const stepStart: (state: State, events: LLMEvent[]) => State;
9
9
  export declare const textStart: (state: State, events: LLMEvent[], id: string, providerMetadata?: ProviderMetadata) => State;
10
- export declare const textDelta: (state: State, events: LLMEvent[], id: string, text: string) => State;
10
+ export declare const textDelta: (state: State, events: LLMEvent[], id: string, text: string, providerMetadata?: ProviderMetadata) => State;
11
11
  export declare const reasoningStart: (state: State, events: LLMEvent[], id: string, providerMetadata?: ProviderMetadata) => State;
12
12
  export declare const reasoningDelta: (state: State, events: LLMEvent[], id: string, text: string, providerMetadata?: ProviderMetadata) => State;
13
13
  export declare const reasoningEnd: (state: State, events: LLMEvent[], id: string, providerMetadata?: ProviderMetadata) => State;
@@ -13,9 +13,9 @@ export const textStart = (state, events, id, providerMetadata) => {
13
13
  events.push(LLMEvent.textStart({ id, providerMetadata }));
14
14
  return { ...stepped, text: new Set([...stepped.text, id]) };
15
15
  };
16
- export const textDelta = (state, events, id, text) => {
16
+ export const textDelta = (state, events, id, text, providerMetadata) => {
17
17
  const started = textStart(state, events, id);
18
- events.push(LLMEvent.textDelta({ id, text }));
18
+ events.push(LLMEvent.textDelta({ id, text, providerMetadata }));
19
19
  return started;
20
20
  };
21
21
  export const reasoningStart = (state, events, id, providerMetadata) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
- "version": "0.0.0-beta-17823",
3
+ "version": "0.0.0-beta-17898",
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-beta-17823",
33
+ "@opencode-ai/http-recorder": "0.0.0-beta-17898",
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-beta-17823",
42
+ "@opencode-ai/schema": "0.0.0-beta-17898",
43
43
  "aws4fetch": "1.0.20",
44
44
  "effect": "4.0.0-rc.110",
45
45
  "google-auth-library": "10.5.0"