@bitkyc08/opencodex 2.26.0 → 2.28.0

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 (83) hide show
  1. package/gui/dist/assets/{index-RL6b1bTV.js → index-D2sP-biU.js} +14 -14
  2. package/gui/dist/index.html +1 -1
  3. package/package.json +1 -1
  4. package/src/adapters/anthropic.ts +60 -1
  5. package/src/adapters/base.ts +16 -2
  6. package/src/adapters/command-code.ts +4 -3
  7. package/src/adapters/cursor/cursor-errors.ts +15 -0
  8. package/src/adapters/cursor/live-transport.ts +14 -1
  9. package/src/adapters/google-antigravity-replay.ts +16 -8
  10. package/src/adapters/google.ts +22 -5
  11. package/src/adapters/openai-chat.ts +189 -60
  12. package/src/adapters/openai-responses.ts +37 -0
  13. package/src/adapters/tool-catalog-nudge.ts +1 -1
  14. package/src/bridge.ts +11 -5
  15. package/src/cli/doctor.ts +76 -0
  16. package/src/cli/help.ts +2 -0
  17. package/src/cli/index.ts +19 -6
  18. package/src/cli/models.ts +13 -6
  19. package/src/codex/account-usability.ts +3 -0
  20. package/src/codex/app-server-processes.ts +269 -37
  21. package/src/codex/auth-api.ts +22 -5
  22. package/src/codex/auth-context.ts +108 -3
  23. package/src/codex/catalog/aggregation.ts +3 -0
  24. package/src/codex/catalog/metadata.ts +17 -3
  25. package/src/codex/catalog/native-models.ts +22 -14
  26. package/src/codex/catalog/parsing.ts +20 -3
  27. package/src/codex/catalog/provider-fetch.ts +8 -0
  28. package/src/codex/catalog/sync.ts +63 -15
  29. package/src/codex/convergence.ts +61 -13
  30. package/src/codex/log-guard/path-safety.ts +52 -3
  31. package/src/codex/model-entitlements.ts +353 -0
  32. package/src/codex/native-profile-startup.ts +100 -2
  33. package/src/codex/quota.ts +28 -3
  34. package/src/codex/routing.ts +14 -8
  35. package/src/codex/user-identity.ts +21 -1
  36. package/src/config/provider-name.ts +24 -0
  37. package/src/config.ts +11 -24
  38. package/src/generated/compatibility-version.json +110 -70
  39. package/src/images/loop.ts +11 -4
  40. package/src/lib/destination-policy.ts +47 -0
  41. package/src/lib/shadow-call.ts +15 -0
  42. package/src/lib/state-store-registrations.ts +8 -2
  43. package/src/oauth/index.ts +33 -5
  44. package/src/oauth/store.ts +11 -5
  45. package/src/providers/antigravity-models.ts +70 -5
  46. package/src/providers/derive.ts +12 -2
  47. package/src/providers/fastwire.ts +39 -8
  48. package/src/providers/quota.ts +9 -2
  49. package/src/providers/registry.ts +120 -6
  50. package/src/providers/service-tier.ts +50 -15
  51. package/src/responses/parser.ts +59 -11
  52. package/src/responses/state.ts +162 -5
  53. package/src/responses/tool-search-compat.ts +301 -0
  54. package/src/router.ts +17 -3
  55. package/src/routing/capability.ts +26 -9
  56. package/src/routing/compatibility/behavior.ts +44 -6
  57. package/src/routing/profile.ts +1 -1
  58. package/src/server/chat-native.ts +11 -2
  59. package/src/server/index.ts +59 -8
  60. package/src/server/management/agent-settings-routes.ts +16 -2
  61. package/src/server/management/shared.ts +3 -1
  62. package/src/server/request-log.ts +31 -0
  63. package/src/server/responses/collaboration.ts +34 -9
  64. package/src/server/responses/compact.ts +54 -7
  65. package/src/server/responses/core.ts +259 -43
  66. package/src/server/responses/input-admission.ts +7 -2
  67. package/src/server/responses/responses-field-backfill.ts +88 -6
  68. package/src/server/responses/terminal-guard.ts +10 -0
  69. package/src/server/responses-tool-search-repair.ts +217 -0
  70. package/src/server/system-env.ts +74 -5
  71. package/src/service-manager-probe.ts +99 -0
  72. package/src/service.ts +86 -6
  73. package/src/tray/windows.ts +25 -5
  74. package/src/types/accounts.ts +37 -0
  75. package/src/types/config.ts +818 -0
  76. package/src/types/provider.ts +521 -0
  77. package/src/types/request.ts +358 -0
  78. package/src/types/tools.ts +131 -0
  79. package/src/types/wire.ts +80 -0
  80. package/src/types.ts +103 -1883
  81. package/src/usage/cost.ts +37 -1
  82. package/src/usage/log.ts +4 -0
  83. package/src/web-search/loop.ts +11 -4
@@ -16,7 +16,7 @@
16
16
  } catch (e) {}
17
17
  })();
18
18
  </script>
19
- <script type="module" crossorigin src="/assets/index-RL6b1bTV.js"></script>
19
+ <script type="module" crossorigin src="/assets/index-D2sP-biU.js"></script>
20
20
  <link rel="stylesheet" crossorigin href="/assets/index-DQsMZzI5.css">
21
21
  </head>
22
22
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitkyc08/opencodex",
3
- "version": "2.26.0",
3
+ "version": "2.28.0",
4
4
  "description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code",
5
5
  "type": "module",
6
6
  "main": "./bin/package-main.mjs",
@@ -594,6 +594,63 @@ function orphanToolResultText(msg: OcxToolResultMessage): string {
594
594
  return `[tool_result without adjacent tool_use: ${label}]\n${content}`;
595
595
  }
596
596
 
597
+ /**
598
+ * AgentRouter answers 400 `content-blocked` when the first user message is not in English
599
+ * (#2074), while the same request in English returns 200. The gateway is inspecting the opening
600
+ * user content, so an Anthropic `system` string cannot reach it — the framing has to sit in the
601
+ * first user turn.
602
+ */
603
+ const AGENTROUTER_LANGUAGE_PREAMBLE =
604
+ "[Instruction: Process the user request below and respond in the appropriate language.]";
605
+
606
+ /**
607
+ * Exact host match, not a substring.
608
+ *
609
+ * A `hostname.includes("agentrouter")` test also matches `notagentrouter.example` and
610
+ * `agentrouter.org.attacker.example`, which would let an unrelated destination silently
611
+ * receive an injected instruction block. A prompt mutation keyed on a provider's identity
612
+ * must be keyed on that identity exactly.
613
+ */
614
+ function isAgentRouterEndpoint(baseUrl: string): boolean {
615
+ try {
616
+ const { hostname } = new URL(baseUrl);
617
+ return hostname === "agentrouter.org" || hostname.endsWith(".agentrouter.org");
618
+ } catch {
619
+ return false;
620
+ }
621
+ }
622
+
623
+ /**
624
+ * Prepend the framing as its OWN text block instead of splicing it into the user's string.
625
+ *
626
+ * The distinction matters: rewriting `content` to `${marker}\n\n${original}` edits what the
627
+ * user wrote, and every downstream consumer — logs, retries, an upstream that echoes the turn —
628
+ * then sees a sentence the user never typed as if they had. A separate leading block carries the
629
+ * same signal to the filter while the original text survives byte-for-byte.
630
+ *
631
+ * Only the first user turn is framed, because only the first is what the gateway rejects.
632
+ */
633
+ function applyAgentRouterLanguageFraming(messages: unknown[]): void {
634
+ const firstUser = messages.find(
635
+ (m): m is { role: string; content: unknown } =>
636
+ typeof m === "object" && m !== null && (m as { role?: unknown }).role === "user",
637
+ );
638
+ if (!firstUser) return;
639
+ const preamble = { type: "text", text: AGENTROUTER_LANGUAGE_PREAMBLE };
640
+ if (typeof firstUser.content === "string") {
641
+ firstUser.content = firstUser.content === ""
642
+ ? [preamble]
643
+ : [preamble, { type: "text", text: firstUser.content }];
644
+ return;
645
+ }
646
+ if (!Array.isArray(firstUser.content)) return;
647
+ // Idempotence is keyed on the LEADING block being exactly the marker. A substring test would
648
+ // let a user who quotes the marker later in their own prompt suppress the framing entirely.
649
+ const [head] = firstUser.content as { type?: unknown; text?: unknown }[];
650
+ if (head?.type === "text" && head.text === AGENTROUTER_LANGUAGE_PREAMBLE) return;
651
+ (firstUser.content as unknown[]).unshift(preamble);
652
+ }
653
+
597
654
  function messagesToAnthropicFormat(
598
655
  parsed: OcxParsedRequest,
599
656
  toolNames: { toWire: (name: string) => string },
@@ -743,7 +800,7 @@ function toolsToAnthropicFormat(parsed: OcxParsedRequest, toolNames: { toWire: (
743
800
  ? new Set(parsed.options.toolChoice.allowedTools)
744
801
  : undefined;
745
802
  const tools = allowed
746
- ? parsed.context.tools.filter(t => toolAllowedByChoice(t, allowed))
803
+ ? parsed.context.tools.filter(t => toolAllowedByChoice(t, allowed, parsed.context.tools))
747
804
  : parsed.context.tools;
748
805
  if (tools.length === 0) return undefined;
749
806
  const converted = tools.map(t => ({
@@ -833,6 +890,8 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
833
890
  }
834
891
 
835
892
  const { system, messages } = messagesToAnthropicFormat(parsed, toolNames);
893
+ // Before image normalization, so the framing block is present for every downstream pass.
894
+ if (isAgentRouterEndpoint(provider.baseUrl)) applyAgentRouterLanguageFraming(messages);
836
895
  // Primary image layer: resize/re-encode to fit Anthropic limits without dropping
837
896
  // (anthropic-image-normalize.ts); the guard below remains the deterministic backstop.
838
897
  // imageTierBias > 0 = upstream-413 tightened retry (030): start every image one tier lower.
@@ -39,8 +39,20 @@ export interface ProviderAdapter {
39
39
 
40
40
  fetchResponse?(request: AdapterRequest, ctx?: AdapterFetchContext): Promise<Response>;
41
41
 
42
- parseStream(response: Response, budget: TranslatorBudget): AsyncGenerator<AdapterEvent>;
43
- parseResponse?(response: Response, budget: TranslatorBudget): Promise<AdapterEvent[]>;
42
+ /**
43
+ * Parse one upstream response. `tierMetadata` is the same live observer returned on the
44
+ * corresponding AdapterRequest; adapters that receive a documented tier echo may update it.
45
+ */
46
+ parseStream(
47
+ response: Response,
48
+ budget: TranslatorBudget,
49
+ tierMetadata?: AdapterTierMetadata,
50
+ ): AsyncGenerator<AdapterEvent>;
51
+ parseResponse?(
52
+ response: Response,
53
+ budget: TranslatorBudget,
54
+ tierMetadata?: AdapterTierMetadata,
55
+ ): Promise<AdapterEvent[]>;
44
56
  runTurn?(
45
57
  parsed: OcxParsedRequest,
46
58
  incoming: IncomingMeta,
@@ -58,6 +70,8 @@ export interface AdapterRequest {
58
70
  body: string;
59
71
  /** Custom-tool names actually lowered to upstream function calls while building this request. */
60
72
  convertedRoutedCustomToolNames?: ReadonlySet<string>;
73
+ /** Client tool-search names actually lowered to upstream function calls for this request. */
74
+ convertedRoutedToolSearchNames?: ReadonlySet<string>;
61
75
  /** Releases observation of a serialized request body after its final fetch attempt settles. */
62
76
  releaseBodyObservation?: () => void;
63
77
  /** Exact reasoning parameter emitted by the adapter, for request-log diagnostics only. */
@@ -3,7 +3,7 @@ import { execFile as execFileCallback } from "node:child_process";
3
3
  import { promisify } from "node:util";
4
4
  import { opendir } from "node:fs/promises";
5
5
  import type { AdapterEvent, OcxContentPart, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxTool, OcxUsage } from "../types";
6
- import { isAllowedToolChoice, namespacedToolName, resolveToolChoiceWireName, toolAllowedByChoice, toolChoiceAliases } from "../types";
6
+ import { isAllowedToolChoice, namespacedToolName, resolveToolChoiceWireName, toolAllowedByChoice } from "../types";
7
7
  import type { AdapterFetchContext, AdapterRequest, ProviderAdapter } from "./base";
8
8
  import type { TranslatorBudget } from "../lib/translator-budget";
9
9
  import { readBoundedResponseBody } from "../lib/bounded-body";
@@ -157,10 +157,11 @@ function visibleTools(parsed: OcxParsedRequest): OcxTool[] {
157
157
  const tools = parsed.context.tools ?? [];
158
158
  if (isAllowedToolChoice(choice)) {
159
159
  const allowed = new Set(choice.allowedTools);
160
- return tools.filter(tool => toolAllowedByChoice(tool, allowed));
160
+ return tools.filter(tool => toolAllowedByChoice(tool, allowed, tools));
161
161
  }
162
162
  if (choice && typeof choice !== "string") {
163
- return tools.filter(tool => toolChoiceAliases(tool).includes(choice.name));
163
+ const selected = resolveToolChoiceWireName(tools, choice.name);
164
+ return tools.filter(tool => namespacedToolName(tool.namespace, tool.name) === selected);
164
165
  }
165
166
  return tools;
166
167
  }
@@ -85,6 +85,21 @@ export function isCursorBenignCancelError(value: unknown): boolean {
85
85
  return false;
86
86
  }
87
87
 
88
+ /**
89
+ * True when the turn was torn down by an `AbortSignal` rather than by a transport fault.
90
+ *
91
+ * This is deliberately NOT part of `isCursorBenignCancelError`: an abort mid-turn is a real
92
+ * failure and must still surface. It is only meaningful in combination with a terminal frame
93
+ * having already been emitted, where it means "the answer landed and then the connection went
94
+ * away" (#1527).
95
+ */
96
+ export function isCursorAbortError(value: unknown): boolean {
97
+ const message = errorMessage(value).toLowerCase();
98
+ if (message.includes("cursor request was aborted")) return true;
99
+ const name = (value as { name?: unknown })?.name;
100
+ return typeof name === "string" && name === "AbortError";
101
+ }
102
+
88
103
  /**
89
104
  * True when Cursor Connect rejected the turn with invalid_argument.
90
105
  * Seen after stepCompleted on brittle external-model continuations.
@@ -48,7 +48,7 @@ import {
48
48
  type InteractionResponse,
49
49
  } from "./gen/agent_pb";
50
50
  import { debugProviderDiagnostic } from "../../lib/debug";
51
- import { classifyCursorError, CursorUnexpectedCancelError, isCursorBenignCancelError, safeCursorErrorMessage } from "./cursor-errors";
51
+ import { classifyCursorError, CursorUnexpectedCancelError, isCursorAbortError, isCursorBenignCancelError, safeCursorErrorMessage } from "./cursor-errors";
52
52
  import { mcpArgsFromToolCall } from "./protobuf-events";
53
53
  import { OCX_RESPONSES_TOOL_PROVIDER } from "./tool-definitions";
54
54
  import {
@@ -650,6 +650,18 @@ class LiveCursorTransport implements CursorTransport {
650
650
  // A CANCEL is benign only on the client-tool suspend path (expectedClose); an
651
651
  // unexpected server-side NGHTTP2_CANCEL must surface as a real transport error.
652
652
  if (this.expectedClose && isCursorBenignCancelError(failure)) return;
653
+ // A teardown error arriving AFTER the turn's terminal frame describes the connection,
654
+ // not the turn: the answer is committed and every queued message has been yielded.
655
+ //
656
+ // Narrow on purpose. A benign cancel after a terminal is already swallowed one layer
657
+ // up (`cursor.ts:183`), so widening this to every post-terminal error would change
658
+ // what the adapter sees for genuine faults. What it does cover is the abort case
659
+ // from #1527: `signal.abort` fires `failAndClear(new Error("Cursor request was
660
+ // aborted"))`, which is NOT benign (`cursor-errors.ts:74`), so an ordinary completed
661
+ // turn that is then torn down still surfaced as `turn-failed` with
662
+ // `expectedClose:false`. Only `cancelCursorRun()` sets `expectedClose`, so a normal
663
+ // completion never qualified for the branch above.
664
+ if (this.emittedTerminal && isCursorAbortError(failure)) return;
653
665
  throw attachPartialUsage(classifyTurnFailure(failure), state);
654
666
  }
655
667
  if (done) break;
@@ -659,6 +671,7 @@ class LiveCursorTransport implements CursorTransport {
659
671
  }
660
672
  if (failure) {
661
673
  if (this.expectedClose && isCursorBenignCancelError(failure)) return;
674
+ if (this.emittedTerminal && isCursorAbortError(failure)) return;
662
675
  throw attachPartialUsage(classifyTurnFailure(failure), state);
663
676
  }
664
677
  }
@@ -599,12 +599,20 @@ export function antigravityUsesReplayCache(model: string): boolean {
599
599
  * Observe a parsed CCA chunk's `candidates[0].content.parts` and record thought signatures keyed by
600
600
  * the functionCall identity (name + args). Accumulates across the whole session so a sequential
601
601
  * multi-step tool loop keeps EVERY prior call's signature, not just the latest part-index slot.
602
- * A signature on a standalone thought part is paired with the next functionCall in the same
603
- * array (#897); a call's own signature takes precedence and an unpaired one is dropped.
602
+ * A signature on a standalone thought part applies to the functionCall parts that follow it in
603
+ * the same array AND to later arrays of the same turn: streaming splits a thought part and its
604
+ * calls across SSE chunks, so `carriedThoughtSig` threads the still-unpaired signature from the
605
+ * previous chunk and the return value hands the remainder to the next one (#897, #2125). A call's
606
+ * own signature always takes precedence over a carried one.
604
607
  * `parts` is the already-unwrapped `response.candidates[0].content.parts`.
605
608
  */
606
- export function observeAntigravityReplay(model: string, sessionId: string, parts: unknown[]): void {
607
- if (!antigravityUsesReplayCache(model) || !Array.isArray(parts) || parts.length === 0) return;
609
+ export function observeAntigravityReplay(
610
+ model: string,
611
+ sessionId: string,
612
+ parts: unknown[],
613
+ carriedThoughtSig?: string,
614
+ ): string | undefined {
615
+ if (!antigravityUsesReplayCache(model) || !Array.isArray(parts) || parts.length === 0) return carriedThoughtSig;
608
616
  ensureReplaySnapshotLoaded();
609
617
  const now = Date.now();
610
618
  deleteExpiredReplaySessionsThrottled(now);
@@ -618,7 +626,7 @@ export function observeAntigravityReplay(model: string, sessionId: string, parts
618
626
  lastActiveAtMs: 0,
619
627
  };
620
628
  let inserted = false;
621
- let pendingThoughtSig: string | undefined;
629
+ let pendingThoughtSig: string | undefined = carriedThoughtSig;
622
630
  for (const raw of parts) {
623
631
  if (!raw || typeof raw !== "object") continue;
624
632
  const part = raw as Record<string, unknown>;
@@ -632,7 +640,6 @@ export function observeAntigravityReplay(model: string, sessionId: string, parts
632
640
  continue;
633
641
  }
634
642
  const callSig = sig ?? pendingThoughtSig; // a signature on the call part itself wins
635
- pendingThoughtSig = undefined;
636
643
  if (!callSig) continue;
637
644
  const ck = functionCallKey(fc.name, fc.args);
638
645
  if (!ck) continue; // only function-call signatures are replayable by identity
@@ -645,7 +652,7 @@ export function observeAntigravityReplay(model: string, sessionId: string, parts
645
652
  replayBytes += sizeBytes;
646
653
  inserted = true;
647
654
  }
648
- if (!inserted) return;
655
+ if (!inserted) return pendingThoughtSig;
649
656
  // Charge the fixed outer key only when the session is actually stored.
650
657
  if (!existing) replayBytes += REPLAY_SESSION_KEY_BYTES;
651
658
  evictInnerCalls(entry);
@@ -659,7 +666,7 @@ export function observeAntigravityReplay(model: string, sessionId: string, parts
659
666
  } else {
660
667
  replayBytes -= REPLAY_SESSION_KEY_BYTES;
661
668
  }
662
- return;
669
+ return pendingThoughtSig;
663
670
  }
664
671
  entry.expiresAtMs = now + REPLAY_TTL_MS;
665
672
  entry.lastActiveAtMs = now;
@@ -668,6 +675,7 @@ export function observeAntigravityReplay(model: string, sessionId: string, parts
668
675
  evictIfNeeded();
669
676
  enforceAppOwnedMemoryBudget();
670
677
  markReplayDirty();
678
+ return pendingThoughtSig;
671
679
  }
672
680
 
673
681
  /**
@@ -262,7 +262,7 @@ function toolsToGeminiFormat(parsed: OcxParsedRequest): unknown[] | undefined {
262
262
  ? new Set(parsed.options.toolChoice.allowedTools)
263
263
  : undefined;
264
264
  const tools = allowed
265
- ? parsed.context.tools.filter(t => toolAllowedByChoice(t, allowed))
265
+ ? parsed.context.tools.filter(t => toolAllowedByChoice(t, allowed, parsed.context.tools))
266
266
  : parsed.context.tools;
267
267
  if (tools.length === 0) return undefined;
268
268
  return [{
@@ -342,6 +342,7 @@ interface GoogleResponsePart {
342
342
  text?: string;
343
343
  thought?: boolean;
344
344
  thoughtSignature?: string;
345
+ thought_signature?: string;
345
346
  functionCall?: { name: string; args: unknown };
346
347
  }
347
348
 
@@ -352,8 +353,9 @@ interface GoogleResponsePart {
352
353
  */
353
354
  function googleToolCallMetadataFromPart(
354
355
  part: GoogleResponsePart,
356
+ fallbackSignature?: string,
355
357
  ): { providerMetadata: OcxProviderOpaqueToolCallMetadata } | undefined {
356
- const signature = part.thoughtSignature;
358
+ const signature = part.thoughtSignature ?? part.thought_signature ?? fallbackSignature;
357
359
  if (!isLikelyRealThoughtSignature(signature)) return undefined;
358
360
  return { providerMetadata: { google: { thoughtSignature: signature } } };
359
361
  }
@@ -601,6 +603,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
601
603
  let lastFinishReason: string | undefined;
602
604
  let sawAnyFrame = false;
603
605
  let sawTerminalSignal = false;
606
+ let pendingStreamThoughtSig: string | undefined;
604
607
 
605
608
  const handleDataLine = async function* (line: string): AsyncGenerator<AdapterEvent, "continue" | "content" | "terminate"> {
606
609
  const payload = line.slice(5).trim();
@@ -697,10 +700,19 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
697
700
  const replaySession = provider.googleMode === "cloud-code-assist" ? antigravitySession : vertexReplaySession;
698
701
  if ((provider.googleMode === "cloud-code-assist" || provider.googleMode === "vertex")
699
702
  && parts && replayModel && replaySession) {
700
- observeAntigravityReplay(replayModel, replaySession, parts as unknown[]);
703
+ pendingStreamThoughtSig = observeAntigravityReplay(
704
+ replayModel,
705
+ replaySession,
706
+ parts as unknown[],
707
+ pendingStreamThoughtSig,
708
+ );
701
709
  }
702
710
  if (parts) {
703
711
  for (const part of parts) {
712
+ const sig = part.thoughtSignature ?? part.thought_signature;
713
+ if (part.thought === true && sig && isLikelyRealThoughtSignature(sig)) {
714
+ pendingStreamThoughtSig = sig;
715
+ }
704
716
  const textEvent = googlePartTextEvent(part);
705
717
  if (textEvent) {
706
718
  emittedContentEvent = true;
@@ -729,7 +741,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
729
741
  type: "tool_call_start",
730
742
  id,
731
743
  name: restoreGoogleToolName(part.functionCall.name),
732
- ...googleToolCallMetadataFromPart(part),
744
+ ...googleToolCallMetadataFromPart(part, pendingStreamThoughtSig),
733
745
  };
734
746
  yield { type: "tool_call_delta", arguments: JSON.stringify(part.functionCall.args ?? {}) };
735
747
  yield { type: "tool_call_end" };
@@ -926,7 +938,12 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
926
938
  && replayModel && replaySession) {
927
939
  observeAntigravityReplay(replayModel, replaySession, candidates[0].content.parts as unknown[]);
928
940
  }
941
+ let pendingThoughtSig: string | undefined;
929
942
  for (const part of candidates[0].content.parts) {
943
+ const sig = part.thoughtSignature ?? part.thought_signature;
944
+ if (part.thought === true && sig && isLikelyRealThoughtSignature(sig)) {
945
+ pendingThoughtSig = sig;
946
+ }
930
947
  const textEvent = googlePartTextEvent(part);
931
948
  if (textEvent) events.push(textEvent);
932
949
  const inline = (part as { inlineData?: { mimeType?: string; data?: string } }).inlineData;
@@ -950,7 +967,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
950
967
  type: "tool_call_start",
951
968
  id,
952
969
  name: restoreGoogleToolName(part.functionCall.name),
953
- ...googleToolCallMetadataFromPart(part),
970
+ ...googleToolCallMetadataFromPart(part, pendingThoughtSig),
954
971
  });
955
972
  events.push({ type: "tool_call_delta", arguments: JSON.stringify(part.functionCall.args ?? {}) });
956
973
  events.push({ type: "tool_call_end" });