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

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.
@@ -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",
@@ -452,16 +470,23 @@ const step = (state, event) => {
452
470
  let lifecycle = nextState.lifecycle;
453
471
  let nextToolCallId = nextState.nextToolCallId;
454
472
  let reasoningSignature = nextState.reasoningSignature;
473
+ let textSignature = nextState.textSignature;
455
474
  for (const part of candidate.content.parts) {
456
- if ("thoughtSignature" in part && part.thoughtSignature && "thought" in part && part.thought)
457
- reasoningSignature = part.thoughtSignature;
475
+ const signature = "thoughtSignature" in part && part.thoughtSignature ? part.thoughtSignature : undefined;
476
+ // Gemini attaches replay signatures to thought parts, visible text, or function calls;
477
+ // each block kind must retain the signature attached to its own parts.
478
+ if (signature !== undefined && "thought" in part && part.thought)
479
+ reasoningSignature = signature;
480
+ else if (signature !== undefined && "text" in part)
481
+ textSignature = signature;
458
482
  if ("text" in part && part.text.length > 0) {
459
483
  if (part.thought) {
460
- lifecycle = Lifecycle.reasoningDelta(lifecycle, events, "reasoning-0", part.text, part.thoughtSignature ? googleMetadata({ thoughtSignature: part.thoughtSignature }) : undefined);
484
+ lifecycle = Lifecycle.reasoningDelta(lifecycle, events, "reasoning-0", part.text, signature ? googleMetadata({ thoughtSignature: signature }) : undefined);
461
485
  continue;
462
486
  }
463
487
  lifecycle = Lifecycle.reasoningEnd(lifecycle, events, "reasoning-0", reasoningSignature ? googleMetadata({ thoughtSignature: reasoningSignature }) : undefined);
464
- lifecycle = Lifecycle.textDelta(lifecycle, events, "text-0", part.text);
488
+ lifecycle = Lifecycle.textDelta(lifecycle, events, "text-0", part.text, textSignature ? googleMetadata({ thoughtSignature: textSignature }) : undefined);
489
+ textSignature = undefined;
465
490
  continue;
466
491
  }
467
492
  if ("functionCall" in part) {
@@ -489,6 +514,7 @@ const step = (state, event) => {
489
514
  lifecycle,
490
515
  nextToolCallId,
491
516
  reasoningSignature,
517
+ textSignature,
492
518
  finishReason: candidate.finishReason ?? nextState.finishReason,
493
519
  },
494
520
  events,
@@ -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-17887",
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-17887",
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-17887",
43
43
  "aws4fetch": "1.0.20",
44
44
  "effect": "4.0.0-rc.110",
45
45
  "google-auth-library": "10.5.0"