@bitkyc08/opencodex 2.87.0 → 2.89.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 (246) hide show
  1. package/README.md +11 -1
  2. package/bin/ocx.mjs +15 -4
  3. package/gui/dist/assets/App-BnRSujOq.js +58 -0
  4. package/gui/dist/assets/App-dnkJ85te.css +1 -0
  5. package/gui/dist/assets/{Tray-DCd-A1az.js → Tray-7z1COnmz.js} +1 -1
  6. package/gui/dist/assets/index-uchD-ngf.js +94 -0
  7. package/gui/dist/assets/{quota-summary-ChvmKGOk.js → quota-summary-BY_10IEz.js} +1 -1
  8. package/gui/dist/index.html +1 -1
  9. package/package.json +1 -1
  10. package/src/adapters/anthropic/cache-retention.ts +117 -0
  11. package/src/adapters/anthropic/passthrough.ts +7 -4
  12. package/src/adapters/anthropic-rejection-memo.ts +71 -0
  13. package/src/adapters/anthropic-replay.ts +135 -0
  14. package/src/adapters/anthropic-tool-catalog.ts +11 -0
  15. package/src/adapters/anthropic.ts +86 -48
  16. package/src/adapters/coding-agent/protocol.ts +6 -0
  17. package/src/adapters/google-http.ts +3 -2
  18. package/src/adapters/google-progress-history.ts +53 -0
  19. package/src/adapters/google-vertex-endpoint.ts +67 -0
  20. package/src/adapters/google.ts +52 -30
  21. package/src/adapters/openai-responses/compaction-search-history.ts +27 -0
  22. package/src/adapters/openai-responses/passthrough.ts +17 -12
  23. package/src/adapters/openai-responses/reasoning.ts +68 -1
  24. package/src/adapters/registry.ts +1 -1
  25. package/src/adapters/upstream-http-error.ts +3 -2
  26. package/src/adapters/zed.ts +7 -4
  27. package/src/bridge/internal.ts +17 -3
  28. package/src/bridge/response-json.ts +4 -4
  29. package/src/bridge/sse.ts +17 -10
  30. package/src/claude/inbound-content-options.ts +66 -0
  31. package/src/claude/inbound.ts +12 -26
  32. package/src/claude/outbound.ts +18 -5
  33. package/src/cli/account-api.ts +13 -1
  34. package/src/cli/account-extended.ts +17 -5
  35. package/src/cli/account-key-quota.ts +4 -1
  36. package/src/cli/account.ts +1 -1
  37. package/src/cli/capabilities-agents-routing.ts +45 -8
  38. package/src/cli/capabilities-base.ts +5 -5
  39. package/src/cli/capabilities-provider-models.ts +5 -4
  40. package/src/cli/combo-result.ts +8 -2
  41. package/src/cli/combo.ts +17 -0
  42. package/src/cli/cross-home-owner.ts +16 -2
  43. package/src/cli/help.ts +1 -1
  44. package/src/cli/loopback-family.ts +31 -0
  45. package/src/cli/message-command.ts +4 -1
  46. package/src/cli/message-remote-args.ts +69 -0
  47. package/src/cli/message-remote-command.ts +77 -0
  48. package/src/cli/provider-runtime.ts +69 -1
  49. package/src/cli/registry.ts +3 -3
  50. package/src/codex/account-pause.ts +6 -1
  51. package/src/codex/account-runtime-state.ts +5 -0
  52. package/src/codex/account-store.ts +30 -3
  53. package/src/codex/auth-api/account-list.ts +3 -0
  54. package/src/codex/auth-api/main-account-probe.ts +3 -2
  55. package/src/codex/auth-context.ts +9 -4
  56. package/src/codex/catalog/aggregation.ts +15 -0
  57. package/src/codex/catalog/auto-review.ts +2 -1
  58. package/src/codex/catalog/build-entries.ts +17 -7
  59. package/src/codex/catalog/derive-entry.ts +3 -1
  60. package/src/codex/catalog/metadata.ts +2 -0
  61. package/src/codex/catalog/model-visibility.ts +0 -2
  62. package/src/codex/catalog/ownership.ts +71 -0
  63. package/src/codex/catalog/parsing.ts +12 -3
  64. package/src/codex/catalog/retained-sync.ts +2 -1
  65. package/src/codex/catalog/routed-gather.ts +3 -1
  66. package/src/codex/catalog/routed-removal.ts +4 -4
  67. package/src/codex/convergence.ts +2 -1
  68. package/src/codex/low-quota-desktop-notifier.ts +125 -0
  69. package/src/codex/low-quota-protection.ts +57 -31
  70. package/src/codex/main-account-cache.ts +77 -3
  71. package/src/codex/main-account.ts +6 -2
  72. package/src/codex/native-residue.ts +2 -3
  73. package/src/codex/quota.ts +26 -2
  74. package/src/codex/websocket-activity.ts +161 -0
  75. package/src/codex/websocket-registry.ts +25 -1
  76. package/src/combos/failover.ts +2 -1
  77. package/src/combos/jev-model-backend.ts +30 -7
  78. package/src/combos/jev-model-request.ts +20 -0
  79. package/src/combos/jev-quota-activation.ts +153 -0
  80. package/src/combos/jev-quota-config.ts +62 -0
  81. package/src/combos/jev-quota-route.ts +26 -0
  82. package/src/combos/jev-quota.ts +88 -0
  83. package/src/combos/jev.ts +25 -12
  84. package/src/combos/types.ts +27 -0
  85. package/src/compatibility/index.ts +3 -0
  86. package/src/compatibility/openai-responses.ts +8 -1
  87. package/src/compatibility/opengateway-responses.ts +61 -0
  88. package/src/config/account-auto-pause-revisions.ts +136 -0
  89. package/src/config/diagnostics.ts +13 -0
  90. package/src/config/live-reconcile.ts +9 -0
  91. package/src/config/opencode-zen-upgrade.ts +9 -8
  92. package/src/config/persist-unlocked.ts +10 -1
  93. package/src/config/removed-opencode-free.ts +135 -0
  94. package/src/config/schema/account-auto-pause.ts +14 -0
  95. package/src/config/schema/config-schema.ts +15 -0
  96. package/src/config/schema/leaf-validators.ts +7 -0
  97. package/src/config.ts +10 -3
  98. package/src/generated/compatibility-version.json +411 -175
  99. package/src/images/loop.ts +101 -27
  100. package/src/lib/account-auto-pause-response.ts +16 -0
  101. package/src/lib/bun-runtime.ts +18 -1
  102. package/src/lib/bun-transpiler-cache-scope.d.mts +8 -0
  103. package/src/lib/bun-transpiler-cache-scope.mjs +68 -0
  104. package/src/lib/decision-quota-sink.ts +76 -0
  105. package/src/lib/request-failure-model.ts +2 -0
  106. package/src/lib/state-store-registrations.ts +25 -0
  107. package/src/link/ssh-argv.ts +3 -0
  108. package/src/messaging/envelope.ts +6 -3
  109. package/src/messaging/remote-auth.ts +83 -0
  110. package/src/messaging/remote-bridge.ts +270 -0
  111. package/src/messaging/remote-contract.ts +108 -0
  112. package/src/messaging/remote-enrollment-recovery.ts +59 -0
  113. package/src/messaging/remote-enrollment.ts +244 -0
  114. package/src/messaging/remote-envelope.ts +40 -0
  115. package/src/messaging/remote-files.ts +117 -0
  116. package/src/messaging/remote-owner.ts +169 -0
  117. package/src/messaging/remote-ports.ts +56 -0
  118. package/src/messaging/remote-process.ts +131 -0
  119. package/src/messaging/remote-replies.ts +98 -0
  120. package/src/messaging/remote-rpc-admission.ts +32 -0
  121. package/src/messaging/remote-send.ts +72 -0
  122. package/src/messaging/remote-store.ts +111 -0
  123. package/src/messaging/remote-tunnels.ts +65 -0
  124. package/src/messaging/rpc.ts +13 -1
  125. package/src/oauth/account-auto-pause-policy.ts +66 -0
  126. package/src/oauth/account-auto-pause.ts +171 -0
  127. package/src/oauth/account-quota-rank.ts +2 -1
  128. package/src/oauth/anthropic-account-refusal.ts +6 -0
  129. package/src/oauth/anthropic-model-family.ts +8 -0
  130. package/src/oauth/anthropic-model-quota.ts +2 -6
  131. package/src/oauth/api-key-pool-preserve.ts +37 -0
  132. package/src/oauth/generic-account-failover.ts +7 -4
  133. package/src/oauth/index.ts +5 -35
  134. package/src/oauth/store.ts +35 -8
  135. package/src/oauth/types.ts +2 -0
  136. package/src/plugins/upstream-hooks.ts +14 -1
  137. package/src/protocols/encoders/adapter-events.ts +5 -5
  138. package/src/protocols/encoders/messages.ts +2 -2
  139. package/src/providers/api-key-pause.ts +32 -0
  140. package/src/providers/api-key-resolve.ts +10 -4
  141. package/src/providers/api-key-selection.ts +6 -1
  142. package/src/providers/api-keys.ts +8 -4
  143. package/src/providers/claude-cli-usage.ts +5 -0
  144. package/src/providers/key-failover.ts +11 -11
  145. package/src/providers/model-rename-fields.ts +1 -0
  146. package/src/providers/opencode-zen-rate-limit.ts +12 -73
  147. package/src/providers/quota/account-cache.ts +15 -1
  148. package/src/providers/quota/anthropic-account-quota.ts +2 -1
  149. package/src/providers/quota/anthropic-cooldown-recovery.ts +22 -4
  150. package/src/providers/quota/anthropic-decision-windows.ts +47 -0
  151. package/src/providers/quota/antigravity.ts +39 -6
  152. package/src/providers/quota/report-cache.ts +14 -0
  153. package/src/providers/quota/vendor-probes-oauth.ts +3 -3
  154. package/src/providers/quota-decision-publication.ts +77 -0
  155. package/src/providers/quota-decision-snapshot.ts +145 -0
  156. package/src/providers/quota-routing-cache.ts +3 -0
  157. package/src/providers/quota-types.ts +6 -1
  158. package/src/providers/quota.ts +3 -2
  159. package/src/providers/registry/entries-extended.ts +8 -52
  160. package/src/providers/registry/model-seeds.ts +1 -1
  161. package/src/providers/registry.ts +1 -2
  162. package/src/providers/stale-vision-classification-migration.ts +1 -1
  163. package/src/responses/parser.ts +18 -10
  164. package/src/responses/reasoning-replay-cache.ts +50 -4
  165. package/src/responses/web-search-replay.ts +138 -0
  166. package/src/routing/analytics.ts +7 -15
  167. package/src/routing/compatibility/behavior.ts +4 -1
  168. package/src/server/auth-cors.ts +7 -0
  169. package/src/server/background-lifecycle.ts +2 -1
  170. package/src/server/chat-native.ts +28 -13
  171. package/src/server/claude-messages.ts +4 -2
  172. package/src/server/index/live-sideband.ts +3 -2
  173. package/src/server/index/optional-listeners.ts +6 -0
  174. package/src/server/index/serve-options.ts +11 -5
  175. package/src/server/index/websocket-handler.ts +18 -4
  176. package/src/server/index.ts +7 -7
  177. package/src/server/lifecycle.ts +13 -1
  178. package/src/server/management/combo-routes.ts +17 -2
  179. package/src/server/management/oauth-account-routes.ts +10 -1
  180. package/src/server/management/provider-completion-test.ts +289 -0
  181. package/src/server/management/provider-routes.ts +9 -1
  182. package/src/server/management/route-registry.ts +2 -0
  183. package/src/server/messages-native.ts +34 -7
  184. package/src/server/messages-shared.ts +4 -1
  185. package/src/server/request-log.ts +7 -2
  186. package/src/server/responses/account-auto-pause-recovery.ts +26 -0
  187. package/src/server/responses/adapter-continuation.ts +34 -18
  188. package/src/server/responses/adapter-delivery.ts +53 -6
  189. package/src/server/responses/adapter-dispatch.ts +104 -29
  190. package/src/server/responses/anthropic-thinking-recovery.ts +153 -0
  191. package/src/server/responses/combo-session-recall.ts +25 -0
  192. package/src/server/responses/combo-stream-preflight.ts +6 -3
  193. package/src/server/responses/combo-tool-loop-pin.ts +70 -0
  194. package/src/server/responses/compact.ts +8 -5
  195. package/src/server/responses/compaction-recovery.ts +2 -0
  196. package/src/server/responses/context-overflow.ts +141 -2
  197. package/src/server/responses/core-codex-account.ts +9 -3
  198. package/src/server/responses/core-combo.ts +34 -7
  199. package/src/server/responses/core-lifetime.ts +3 -2
  200. package/src/server/responses/core-options.ts +2 -0
  201. package/src/server/responses/core-replay.ts +5 -1
  202. package/src/server/responses/core.ts +12 -9
  203. package/src/server/responses/empty-completion-guard.ts +2 -0
  204. package/src/server/responses/fetch-helpers.ts +35 -4
  205. package/src/server/responses/jev-model-invoke.ts +13 -14
  206. package/src/server/responses/native-steering-log.ts +2 -2
  207. package/src/server/responses/passthrough-delivery.ts +3 -2
  208. package/src/server/responses/passthrough-dispatch.ts +30 -33
  209. package/src/server/responses/passthrough-execution.ts +3 -2
  210. package/src/server/responses/passthrough-observations.ts +38 -0
  211. package/src/server/responses/request-compression.ts +98 -0
  212. package/src/server/responses/request-prepare.ts +1 -0
  213. package/src/server/responses/request-transport.ts +32 -3
  214. package/src/server/responses/response-effects.ts +1 -0
  215. package/src/server/responses/response-tool-ownership.ts +21 -0
  216. package/src/server/responses/run-turn-execution.ts +2 -0
  217. package/src/server/responses/sidecar-execution.ts +59 -10
  218. package/src/server/responses/sidecar-send-budget.ts +2 -2
  219. package/src/server/responses/terminal-guard.ts +2 -0
  220. package/src/server/responses-undeclared-tool-guard.ts +1 -1
  221. package/src/server/startup-action-control.ts +2 -2
  222. package/src/server/startup-health-cache.ts +2 -2
  223. package/src/server/windows-tray-control.ts +2 -2
  224. package/src/server/ws-bridge.ts +2 -0
  225. package/src/service/definition.ts +9 -1
  226. package/src/types/account-auto-pause.ts +15 -0
  227. package/src/types/config.ts +23 -1
  228. package/src/types/opencode-zen.ts +9 -3
  229. package/src/types/provider.ts +9 -5
  230. package/src/types/request.ts +30 -4
  231. package/src/types/wire.ts +4 -2
  232. package/src/types.ts +2 -0
  233. package/src/usage/cost.ts +17 -1
  234. package/src/usage/expected-prices.ts +3 -1
  235. package/src/usage/jev-stats.ts +4 -0
  236. package/src/usage/log.ts +2 -0
  237. package/src/usage/summary.ts +16 -3
  238. package/src/usage/telemetry-contract.ts +1 -0
  239. package/src/web-search/anthropic-history.ts +59 -0
  240. package/src/web-search/loop.ts +212 -73
  241. package/src/web-search/progress-stream.ts +14 -1
  242. package/src/web-search/run-turn-loop.ts +9 -3
  243. package/src/web-search/sidecar-search-replay.ts +96 -0
  244. package/gui/dist/assets/App-ZfYbLHkB.js +0 -58
  245. package/gui/dist/assets/App-wT3mXqFz.css +0 -1
  246. package/gui/dist/assets/index-CfYDMj34.js +0 -94
@@ -36,7 +36,7 @@ import {
36
36
  observeAntigravityReplay,
37
37
  } from "./google-antigravity-replay";
38
38
  import { canonicalAntigravityUsageModel, resolveAntigravityEffortWireModel } from "../providers/antigravity-models";
39
- import { googleVertexLocationConfigError } from "../providers/google-vertex-location";
39
+ import { selectVertexEndpoint, vertexInferenceUrl, vertexReplayModelKey } from "./google-vertex-endpoint";
40
40
  import { forgetThoughtSignatureForReplay, lookupReplayThoughtSignature } from "../responses/thought-signature-replay";
41
41
  import {
42
42
  isTranslatorBudgetExceededError,
@@ -44,6 +44,7 @@ import {
44
44
  type TranslatorBudget,
45
45
  } from "../lib/translator-budget";
46
46
  import { buildNonOpenAIToolCatalogNudgeForTools } from "./tool-catalog-nudge";
47
+ import { geminiProgressLead, trimsGeminiProgressHistory } from "./google-progress-history";
47
48
  import { configuredReasoningEfforts, mapReasoningEffort } from "../reasoning-effort";
48
49
 
49
50
  // Google-family models (Gemini/Vertex/Antigravity) tend to emit long running commentary between
@@ -332,13 +333,20 @@ function geminiToolResultParts(
332
333
  msg: OcxToolResultMessage,
333
334
  wireName: string,
334
335
  wireCallId: string,
336
+ nestToolResultImages: boolean,
335
337
  ): unknown[] {
336
338
  const functionResponse: Record<string, unknown> = {
337
339
  name: wireName,
338
340
  response: { result: geminiToolResultText(msg.content) },
339
341
  id: wireCallId,
340
342
  };
341
- return [{ functionResponse }, ...toolResultImageParts(msg.content)];
343
+ const images = toolResultImageParts(msg.content);
344
+ // Vertex Argon rejects image siblings beside a functionResponse as an unsupported model tail.
345
+ if (nestToolResultImages && images.length > 0) {
346
+ functionResponse.parts = images;
347
+ return [{ functionResponse }];
348
+ }
349
+ return [{ functionResponse }, ...images];
342
350
  }
343
351
 
344
352
  function geminiMissingToolResultPart(wireName: string, wireCallId: string): unknown {
@@ -369,6 +377,7 @@ function messagesToGeminiFormat(
369
377
  identityModelId: string,
370
378
  stripRejectedClaudeSdkParagraph = false,
371
379
  isCloudCodeAssist = false,
380
+ nestToolResultImages = false,
372
381
  ): { systemInstruction?: unknown; contents: unknown[]; replayedCallIds: string[] } {
373
382
  // Neutralize Codex's GPT-5 identity line (Gemini/Antigravity share this path) so a routed model
374
383
  // never misreports as GPT-5/OpenAI, and never leaks the proxy identity upstream.
@@ -466,11 +475,26 @@ function messagesToGeminiFormat(
466
475
  const aMsg = msg as OcxAssistantMessage;
467
476
  const parts: unknown[] = [];
468
477
  const toolCalls: Array<{ wireCallId: string; wireName: string }> = [];
478
+ // Progress text before a Gemini tool call replays as its lead sentence, so planning the
479
+ // model leaked into visible text is not copied forward (google-progress-history.ts).
480
+ const trimProgress = trimsGeminiProgressHistory(identityModelId)
481
+ && aMsg.phase !== "final_answer"
482
+ && aMsg.content.some(part => part.type === "toolCall");
483
+ let progressLeadKept = false;
484
+ let afterToolCall = false;
469
485
  for (const p of aMsg.content) {
470
486
  if (p.type === "text") {
471
- const textPart = geminiTextPart((p as OcxTextContent).text);
487
+ let text: unknown = (p as OcxTextContent).text;
488
+ if (trimProgress && !afterToolCall) {
489
+ if (progressLeadKept || typeof text !== "string") continue;
490
+ text = geminiProgressLead(text);
491
+ if (text === undefined) continue;
492
+ progressLeadKept = true;
493
+ }
494
+ const textPart = geminiTextPart(text);
472
495
  if (textPart) parts.push(textPart);
473
496
  } else if (p.type === "toolCall") {
497
+ afterToolCall = true;
474
498
  const tc = p as OcxToolCall;
475
499
  // Preserve the thought signature on the function-call part so Antigravity/Gemini-3
476
500
  // reasoning continuity survives history-driven (stateless) turns, not just same-process
@@ -535,13 +559,17 @@ function messagesToGeminiFormat(
535
559
  const responseParts: unknown[] = [];
536
560
  for (const call of toolCalls) {
537
561
  const result = resultsById.get(call.wireCallId);
538
- if (result) responseParts.push(...geminiToolResultParts(result, call.wireName, call.wireCallId));
562
+ if (result) responseParts.push(...geminiToolResultParts(result, call.wireName, call.wireCallId, nestToolResultImages));
539
563
  else responseParts.push(geminiMissingToolResultPart(call.wireName, call.wireCallId));
540
564
  }
541
- for (const orphan of orphanResults) {
542
- responseParts.push(...geminiOrphanToolResultParts(orphan));
543
- }
565
+ const orphanParts = orphanResults.flatMap(geminiOrphanToolResultParts);
544
566
  contents.push({ role: "user", parts: responseParts });
567
+ // Orphan images cannot share Argon's response batch or claim another call's ownership.
568
+ if (nestToolResultImages && orphanParts.some(part => typeof part === "object" && part !== null && "inline_data" in part)) {
569
+ contents.push({ role: "user", parts: orphanParts });
570
+ } else {
571
+ responseParts.push(...orphanParts);
572
+ }
545
573
  i = j - 1;
546
574
  }
547
575
  break;
@@ -1010,6 +1038,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
1010
1038
  identityModelId,
1011
1039
  stripRejectedClaudeSdkParagraph,
1012
1040
  isCloudCodeAssist,
1041
+ provider.googleMode === "vertex" && parsed.modelId === "gemini-4-argon",
1013
1042
  );
1014
1043
  lastInjectedCallIds = [...replayedCallIds];
1015
1044
  lastReasoningReplayScope = parsed._reasoningReplayScope;
@@ -1176,9 +1205,10 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
1176
1205
  const compiled = compileGoogleWireBody(body, toolSchemaProfile, toolSchemaPolicy);
1177
1206
  reportToolSchemaLoss(compiled.toolSchemaLossReport);
1178
1207
  restoreGoogleToolName = compiled.restoreToolName;
1179
- const vertexProject = provider.project || process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT || "api-key";
1180
- const vertexLocation = provider.location || process.env.GOOGLE_CLOUD_LOCATION || "global";
1181
- vertexReplayModel = `vertex:${vertexProject}:${vertexLocation}:${parsed.modelId}`;
1208
+ const apiKey = resolveVertexApiKey(provider.apiKey);
1209
+ const vertexEndpoint = selectVertexEndpoint(provider, Boolean(apiKey));
1210
+ const url = vertexInferenceUrl(vertexEndpoint, parsed.modelId, method, streamParam);
1211
+ vertexReplayModel = vertexReplayModelKey(vertexEndpoint, parsed.modelId);
1182
1212
  vertexReplaySession = vertexReplaySessionId(parsed);
1183
1213
  // Compile names before replay so the cache matches the exact provider-visible
1184
1214
  // functionCall identity. This is the same bounded TTL/LRU store used by CCA, with the
@@ -1194,27 +1224,13 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
1194
1224
  (compiled.body as { contents: unknown[] }).contents,
1195
1225
  );
1196
1226
  }
1197
- // Vertex AI: project/location endpoint with GCP ADC, or x-goog-api-key fast path.
1198
- const apiKey = resolveVertexApiKey(provider.apiKey);
1199
- if (apiKey) {
1200
- const url = `https://aiplatform.googleapis.com/v1/publishers/google/models/${parsed.modelId}:${method}${streamParam}`;
1201
- headers["x-goog-api-key"] = apiKey;
1202
- return { url, method: "POST", headers, body: JSON.stringify(compiled.body) };
1227
+ // Endpoint, location and model path were validated above, before any credential is attached.
1228
+ if (apiKey) headers["x-goog-api-key"] = apiKey;
1229
+ else {
1230
+ // Resolve each marker per request; never share a process-global active account.
1231
+ const markerAccount = gcpCredentialMarkerAccount(provider.apiKey);
1232
+ headers["Authorization"] = `Bearer ${await getVertexAccessToken({ markerAccount })}`;
1203
1233
  }
1204
- const project = provider.project || process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT;
1205
- if (!project) throw new Error("Vertex AI requires a project id (provider.project or GOOGLE_CLOUD_PROJECT/GCLOUD_PROJECT).");
1206
- const location = provider.location || process.env.GOOGLE_CLOUD_LOCATION;
1207
- if (!location) throw new Error("Vertex AI requires a location (provider.location or GOOGLE_CLOUD_LOCATION).");
1208
- const locationError = googleVertexLocationConfigError(location);
1209
- if (locationError) throw new Error(locationError);
1210
- const host = location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com`;
1211
- const url = `https://${host}/v1/projects/${project}/locations/${location}/publishers/google/models/${parsed.modelId}:${method}${streamParam}`;
1212
- // The marker account travels per-request as an argument (never process-global state): a
1213
- // module-level "active marker" written at routing time would race across concurrent
1214
- // requests for different providers and could resolve one call with another's credential.
1215
- const markerAccount = gcpCredentialMarkerAccount(provider.apiKey);
1216
- const token = await getVertexAccessToken({ markerAccount });
1217
- headers["Authorization"] = `Bearer ${token}`;
1218
1234
  return { url, method: "POST", headers, body: JSON.stringify(compiled.body) };
1219
1235
  }
1220
1236
 
@@ -1238,6 +1254,12 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
1238
1254
  const compiled = compileGoogleWireBody(body, toolSchemaProfile, toolSchemaPolicy);
1239
1255
  reportToolSchemaLoss(compiled.toolSchemaLossReport);
1240
1256
  restoreGoogleToolName = compiled.restoreToolName;
1257
+ // A functionCall issued by another model (a combo hop, #7298) has no Gemini signature, and
1258
+ // Gemini 3 refuses the turn without one. The same Gemini-only sentinel CCA and Vertex
1259
+ // apply; a real signature carried by history always wins.
1260
+ if (Array.isArray((compiled.body as { contents?: unknown[] }).contents)) {
1261
+ applyAntigravityThoughtSignatureFallback(routedModelId, (compiled.body as { contents: unknown[] }).contents);
1262
+ }
1241
1263
  return { url, method: "POST", headers, body: JSON.stringify(compiled.body) };
1242
1264
  },
1243
1265
 
@@ -114,3 +114,30 @@ export function renderCompactionSearchHistoryItems(items: readonly unknown[], re
114
114
  if (omissionIndex >= 0) out[omissionIndex] = omissionNote(omitted);
115
115
  return out;
116
116
  }
117
+
118
+ function isHostedSearchTool(tool: unknown): boolean {
119
+ return isPlainObject(tool) && typeof tool.type === "string" && tool.type.startsWith("web_search");
120
+ }
121
+
122
+ /**
123
+ * True when the outbound body declares a hosted search tool, either top-level or inside a Lite
124
+ * `additional_tools` input wrapper. Read on the final body, after unsupported hosted tools were
125
+ * stripped, so a declaration the destination never sees does not count.
126
+ */
127
+ export function declaresHostedWebSearch(body: Record<string, unknown>): boolean {
128
+ if (Array.isArray(body.tools) && body.tools.some(isHostedSearchTool)) return true;
129
+ return Array.isArray(body.input) && body.input.some(item => isPlainObject(item)
130
+ && item.type === "additional_tools" && Array.isArray(item.tools) && item.tools.some(isHostedSearchTool));
131
+ }
132
+
133
+ /**
134
+ * Project unreferenced hosted `web_search_call` history into reference notes. A same-input
135
+ * `item_reference` to a hosted cell keeps that cell structural. Returns the original body when
136
+ * there is nothing to project.
137
+ */
138
+ export function projectHostedSearchHistory(body: Record<string, unknown>): Record<string, unknown> {
139
+ if (!Array.isArray(body.input) || !body.input.some(item => isPlainObject(item) && item.type === "web_search_call")) return body;
140
+ const referencedIds = new Set(body.input.flatMap(item => isPlainObject(item)
141
+ && item.type === "item_reference" && typeof item.id === "string" ? [item.id] : []));
142
+ return { ...body, input: renderCompactionSearchHistoryItems(body.input, referencedIds) };
143
+ }
@@ -53,7 +53,7 @@ import { stripMuseSparkUnsupportedWebSearchFields, stripOpenAiOnlyWebSearchField
53
53
  import { observeOutbound } from "../../usage/cache-diagnostic";
54
54
 
55
55
  import { normalizeMuseToolChoice } from "./muse-tool-choice";
56
- import { renderCompactionSearchHistoryItems } from "./compaction-search-history";
56
+ import { declaresHostedWebSearch, projectHostedSearchHistory, renderCompactionSearchHistoryItems } from "./compaction-search-history";
57
57
  import { stripKimiEmptyAssistantMessages } from "./empty-assistant";
58
58
 
59
59
  /**
@@ -135,7 +135,8 @@ function usageFromResponsesPayload(payload: unknown): OcxUsage | undefined {
135
135
  const knownKeys = new Set(["input_tokens", "output_tokens", "total_tokens", "input_tokens_details", "output_tokens_details"]);
136
136
  const hasExtras = Object.keys(usage).some(key => !knownKeys.has(key))
137
137
  || (isPlainObject(usage.input_tokens_details)
138
- && Object.keys(usage.input_tokens_details).some(key => key !== "cached_tokens" && key !== "cache_write_tokens"))
138
+ && Object.keys(usage.input_tokens_details).some(key => key !== "cached_tokens" && key !== "cache_write_tokens"
139
+ && key !== "cache_write_1h_tokens"))
139
140
  || (isPlainObject(usage.output_tokens_details)
140
141
  && Object.keys(usage.output_tokens_details).some(key => key !== "reasoning_tokens"));
141
142
  if (inputTokens === 0 && outputTokens === 0 && !hasExtras) return undefined;
@@ -147,6 +148,8 @@ function usageFromResponsesPayload(payload: unknown): OcxUsage | undefined {
147
148
  ...(typeof usage.total_tokens === "number" ? { totalTokens: usage.total_tokens } : {}),
148
149
  ...(typeof inputDetails?.cached_tokens === "number" ? { cachedInputTokens: inputDetails.cached_tokens } : {}),
149
150
  ...(typeof inputDetails?.cache_write_tokens === "number" ? { cacheCreationInputTokens: inputDetails.cache_write_tokens } : {}),
151
+ ...(typeof inputDetails?.cache_write_1h_tokens === "number"
152
+ ? { cacheCreation1hInputTokens: inputDetails.cache_write_1h_tokens } : {}),
150
153
  ...(typeof outputDetails?.reasoning_tokens === "number" ? { reasoningOutputTokens: outputDetails.reasoning_tokens } : {}),
151
154
  ...(hasExtras ? { rawUsage: { ...usage } } : {}),
152
155
  };
@@ -492,16 +495,18 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
492
495
  // here, on the serialized body, not on the parsed selector. One place covers both the
493
496
  // HTTP and the WebSocket outbound, because the WS path transports this same request
494
497
  // instead of rebuilding it.
495
- // Opt-in Codex Lite history policy: replay remaining hosted actions as untrusted metadata,
496
- // after bridge restoration/tool validation. Tool declarations and other input stay intact.
497
- if (provider.projectResponsesLiteHostedSearchHistory === true && url === `${CODEX_FORWARD_BASE_URL}/responses`
498
- && !parsed._compactionRequest && !parsed._portableCompaction
499
- && codexResponsesLiteSelected(headers, finalBody)
500
- && isPlainObject(finalBody) && Array.isArray(finalBody.input)
501
- && finalBody.input.some(item => isPlainObject(item) && item.type === "web_search_call")) {
502
- const referencedIds = new Set(finalBody.input.flatMap(item => isPlainObject(item)
503
- && item.type === "item_reference" && typeof item.id === "string" ? [item.id] : []));
504
- finalBody = { ...finalBody, input: renderCompactionSearchHistoryItems(finalBody.input, referencedIds) };
498
+ // Hosted-search history projection, after bridge restoration and tool validation. Two
499
+ // triggers share one bounded renderer, and tool declarations and other input stay intact:
500
+ // 1. The opt-in Codex Lite policy covers ordinary turns and native v2 compaction triggers
501
+ // (#7268). Portable compaction was already projected by the summary builder above.
502
+ // 2. Any request whose final tool surface declares no hosted search. Native Sol answers a
503
+ // replayed web_search_call without a matching declaration with a generic server_error
504
+ // (#7250), so the cells become untrusted reference notes instead of a doomed request.
505
+ if (isPlainObject(finalBody)
506
+ && ((provider.projectResponsesLiteHostedSearchHistory === true && url === `${CODEX_FORWARD_BASE_URL}/responses`
507
+ && !parsed._portableCompaction && codexResponsesLiteSelected(headers, finalBody))
508
+ || !declaresHostedWebSearch(finalBody))) {
509
+ finalBody = projectHostedSearchHistory(finalBody);
505
510
  }
506
511
  observeOutbound(parsed._rawBody, finalBody, headers);
507
512
  if (!isCanonicalOpenAiForwardProvider(provider)) {
@@ -163,7 +163,74 @@ export function sanitizeReasoningInputContent(
163
163
  });
164
164
 
165
165
  if (blankedPlaintext) opts?.onPlaintextReasoningBlanked?.();
166
- return changed ? { ...raw, input } : body;
166
+ // Runs after the per-item pass so an item it emptied has already been backfilled, and a
167
+ // placeholder is only invented for a turn that carries no reasoning item at all.
168
+ const withTurnReasoning = opts?.requirePlaintextReasoning === true
169
+ ? backfillBareToolCallTurnReasoning(input)
170
+ : input;
171
+ if (withTurnReasoning !== input) changed = true;
172
+ return changed ? { ...raw, input: withTurnReasoning } : body;
173
+ }
174
+
175
+ /** Assistant-authored history items: what DeepSeek merges into one assistant message. */
176
+ function isAssistantTurnItem(item: unknown): boolean {
177
+ if (!isPlainObject(item) || typeof item.type !== "string") return false;
178
+ if (item.type === "reasoning") return true;
179
+ if (item.type === "message") return item.role === "assistant";
180
+ return item.type.endsWith("_call");
181
+ }
182
+
183
+ function isClientToolCallItem(item: unknown): boolean {
184
+ return isPlainObject(item)
185
+ && (item.type === "function_call" || item.type === "custom_tool_call" || item.type === "local_shell_call");
186
+ }
187
+
188
+ /**
189
+ * Give every assistant tool-call turn that has NO reasoning item a minimal plaintext one (#7298).
190
+ *
191
+ * A Combo can serve the turn that issued a tool call on one model and the continuation on
192
+ * another. When the issuing model was a GPT turn that produced no reasoning item (or the item
193
+ * was dropped upstream), the strict destination sees a bare `function_call` and DeepSeek's
194
+ * thinking mode refuses the whole request with `The reasoning_text in the thinking mode must be
195
+ * passed back to the API`. The per-item backfill above cannot help, because there is no item to
196
+ * backfill. This is the Responses-wire twin of the Chat adapter's `reasoning_content: " "`
197
+ * placeholder (#5421): the same single-space text, never fabricated reasoning, inserted at the
198
+ * head of the turn so it precedes the call exactly where a native turn's reasoning would be.
199
+ *
200
+ * A turn is a maximal run of assistant-authored items; any user message or tool output ends it.
201
+ * Turns that already carry a reasoning item, and turns with no client tool call, are untouched.
202
+ */
203
+ export function backfillBareToolCallTurnReasoning(input: unknown[]): unknown[] {
204
+ const bareTurnStarts = new Set<number>();
205
+ let turnStart = -1;
206
+ let turnHasReasoning = false;
207
+ let turnHasToolCall = false;
208
+ const closeTurn = (): void => {
209
+ if (turnStart >= 0 && turnHasToolCall && !turnHasReasoning) bareTurnStarts.add(turnStart);
210
+ turnStart = -1;
211
+ turnHasReasoning = false;
212
+ turnHasToolCall = false;
213
+ };
214
+ for (let index = 0; index < input.length; index++) {
215
+ const item = input[index];
216
+ if (!isAssistantTurnItem(item)) {
217
+ closeTurn();
218
+ continue;
219
+ }
220
+ if (turnStart < 0) turnStart = index;
221
+ if ((item as Record<string, unknown>).type === "reasoning") turnHasReasoning = true;
222
+ if (isClientToolCallItem(item)) turnHasToolCall = true;
223
+ }
224
+ closeTurn();
225
+ if (bareTurnStarts.size === 0) return input;
226
+ const result: unknown[] = [];
227
+ for (let index = 0; index < input.length; index++) {
228
+ if (bareTurnStarts.has(index)) {
229
+ result.push({ type: "reasoning", summary: [], content: [{ type: "reasoning_text", text: " " }] });
230
+ }
231
+ result.push(input[index]);
232
+ }
233
+ return result;
167
234
  }
168
235
 
169
236
  /**
@@ -138,7 +138,7 @@ export const ADAPTER_REGISTRY = {
138
138
  zed: {
139
139
  wire: "zed",
140
140
  mutation: "codex-owned",
141
- create: (provider: OcxProviderConfig, _context: AdapterFactoryContext) => createZedAdapter(provider),
141
+ create: (provider: OcxProviderConfig, context: AdapterFactoryContext) => createZedAdapter(provider, context.cacheRetention),
142
142
  },
143
143
  "mimo-free": {
144
144
  contractParent: "openai-chat",
@@ -1,3 +1,4 @@
1
+ import { inheritAccountAutoPauseResponse } from "../lib/account-auto-pause-response";
1
2
  import { readBoundedResponseBody } from "../lib/bounded-body";
2
3
  import { redactSecretString } from "../lib/redact";
3
4
 
@@ -40,9 +41,9 @@ export async function normalizeUpstreamHttpErrorResponse(
40
41
  const headers = new Headers(res.headers);
41
42
  headers.delete("content-encoding");
42
43
  headers.delete("content-length");
43
- return new Response(await opts.formatMessage(payloadText), {
44
+ return inheritAccountAutoPauseResponse(res, new Response(await opts.formatMessage(payloadText), {
44
45
  status: res.status,
45
46
  statusText: res.statusText,
46
47
  headers,
47
- });
48
+ }));
48
49
  }
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import type { AdapterRequest, IncomingMeta, ProviderAdapter } from "./base";
3
3
  import { createAnthropicAdapter } from "./anthropic";
4
+ import type { AnthropicCacheRetention } from "./anthropic/cache-retention";
4
5
  import { createGoogleAdapter } from "./google";
5
6
  import { createOpenAIChatAdapter } from "./openai-chat";
6
7
  import { createResponsesPassthroughAdapter } from "./openai-responses";
@@ -218,15 +219,17 @@ export function zedEventStream(
218
219
  return body.pipeThrough(output);
219
220
  }
220
221
 
221
- function createDelegate(provider: OcxProviderConfig, zedProvider: ZedProvider): ProviderAdapter {
222
+ function createDelegate(provider: OcxProviderConfig, zedProvider: ZedProvider, cacheRetention?: AnthropicCacheRetention): ProviderAdapter {
222
223
  const config = delegateProvider(provider, zedProvider);
223
- if (zedProvider === "anthropic") return createAnthropicAdapter(config);
224
+ // The delegate only borrows the api.anthropic.com body builder; the wire goes to Zed's
225
+ // gateway, which does not document the 1h TTL, so an omitted retention stays 5m here.
226
+ if (zedProvider === "anthropic") return createAnthropicAdapter(config, cacheRetention ?? "short");
224
227
  if (zedProvider === "google") return createGoogleAdapter(config);
225
228
  if (zedProvider === "open_ai") return createResponsesPassthroughAdapter(config);
226
229
  return createOpenAIChatAdapter(config);
227
230
  }
228
231
 
229
- export function createZedAdapter(provider: OcxProviderConfig): ProviderAdapter {
232
+ export function createZedAdapter(provider: OcxProviderConfig, cacheRetention?: AnthropicCacheRetention): ProviderAdapter {
230
233
  let delegate: ZedDelegate | undefined;
231
234
  let credentials: ZedCredentials | undefined;
232
235
 
@@ -246,7 +249,7 @@ export function createZedAdapter(provider: OcxProviderConfig): ProviderAdapter {
246
249
  /* Model inference fallback below; a transient catalog outage must not block passthrough. */
247
250
  }
248
251
  const zedProvider = providerFromCatalog(catalog, parsed.modelId);
249
- const selected = createDelegate(provider, zedProvider);
252
+ const selected = createDelegate(provider, zedProvider, cacheRetention);
250
253
  const safeIncoming: IncomingMeta = {
251
254
  headers: incoming?.headers ?? new Headers(),
252
255
  translatorBudget: incoming?.translatorBudget ?? createTranslatorBudget(),
@@ -1,5 +1,6 @@
1
1
  import type {
2
2
  AdapterEvent,
3
+ AnthropicServerBlocks,
3
4
  OcxMessagePhase,
4
5
  OcxProviderContinuationState,
5
6
  OcxProviderOpaqueToolCallMetadata,
@@ -22,6 +23,11 @@ export function uuid(): string {
22
23
  return crypto.randomUUID().replace(/-/g, "");
23
24
  }
24
25
 
26
+ /** The client-visible id of a hosted search cell: the producer's `ws_` id when well-formed, else a fresh one. */
27
+ export function webSearchCellItemId(candidate: string | undefined): string {
28
+ return typeof candidate === "string" && /^ws_[A-Za-z0-9_-]{1,128}$/.test(candidate) ? candidate : `ws_${uuid()}`;
29
+ }
30
+
25
31
  /** Test-only: bound the abandoned-owned-budget watchdog delay (null restores). */
26
32
  export let ownedBudgetAbandonedMs = 10 * 60 * 1000;
27
33
  const OWNED_BUDGET_ABANDONED_DEFAULT_MS = ownedBudgetAbandonedMs;
@@ -63,7 +69,7 @@ export function responsesUsage(usage: OcxUsage | undefined): Record<string, unkn
63
69
  // normalized contract).
64
70
  const rawInputDetails = isRecord(raw.input_tokens_details)
65
71
  ? Object.fromEntries(Object.entries(raw.input_tokens_details as Record<string, unknown>)
66
- .filter(([key]) => key !== "cache_write_tokens"))
72
+ .filter(([key]) => key !== "cache_write_tokens" && key !== "cache_write_1h_tokens"))
67
73
  : {} as Record<string, unknown>;
68
74
  const rawOutputDetails = isRecord(raw.output_tokens_details)
69
75
  ? raw.output_tokens_details as Record<string, unknown>
@@ -91,6 +97,11 @@ export function responsesUsage(usage: OcxUsage | undefined): Record<string, unkn
91
97
  usage.cacheCreationInputTokens,
92
98
  Math.max(0, inputTokens - cacheRead),
93
99
  );
100
+ // The 1-hour-TTL share of the write (Anthropic cache_creation split), only when reported,
101
+ // so a translated Messages client can price it at the 1h rate.
102
+ if (usage.cacheCreation1hInputTokens !== undefined) {
103
+ inputDetails.cache_write_1h_tokens = Math.min(usage.cacheCreation1hInputTokens, inputDetails.cache_write_tokens as number);
104
+ }
94
105
  }
95
106
  out.input_tokens_details = inputDetails;
96
107
  out.output_tokens_details = { ...rawOutputDetails, reasoning_tokens: usage.reasoningOutputTokens ?? 0 };
@@ -186,9 +197,12 @@ export function adapterFailureFromEvent(event: Extract<AdapterEvent, { type: "er
186
197
  * This fixes items created from here on. History recorded before it is repaired at the
187
198
  * replay boundary by `backfillWebSearchQueries()` in the Responses adapter.
188
199
  */
189
- export function webSearchAction(queries: string[], fetch?: { url: string }): Record<string, unknown> {
200
+ export function webSearchAction(queries: string[], anthropic?: AnthropicServerBlocks): Record<string, unknown> {
190
201
  // Anthropic's server-side web_fetch opens one page instead of searching.
191
- if (fetch) return { type: "open_page", url: fetch.url };
202
+ if (anthropic?.use.name === "web_fetch") {
203
+ const url = (anthropic.result.content as { url?: unknown } | undefined)?.url ?? (anthropic.use.input as { url?: unknown }).url;
204
+ return { type: "open_page", url: typeof url === "string" ? url : "" };
205
+ }
192
206
  const first = queries[0] ?? "";
193
207
  return { type: "search", query: first, queries: queries.length > 0 ? queries : [first] };
194
208
  }
@@ -42,7 +42,7 @@ import {
42
42
  type TranslatorBudget,
43
43
  type TranslatorBufferKind,
44
44
  } from "../lib/translator-budget";
45
- import { adapterFailureFromEvent, emptyChunks, joinChunks, responsesUsage, toolCallArgumentsUsable, uuid, webSearchAction } from "./internal";
45
+ import { adapterFailureFromEvent, emptyChunks, joinChunks, responsesUsage, toolCallArgumentsUsable, uuid, webSearchAction, webSearchCellItemId } from "./internal";
46
46
  import type { OutputItem, StringChunks } from "./internal";
47
47
  import { bridgeToResponsesSSE } from "./sse";
48
48
 
@@ -546,10 +546,10 @@ function buildResponseJSONWithBudget(
546
546
  flushToolCall();
547
547
  const safeSources = safeWebSearchSources(e.sources);
548
548
  pushOutput({
549
- type: "web_search_call", id: `ws_${uuid()}`, status: e.status ?? "completed",
550
- action: webSearchAction(e.queries, e.fetch),
549
+ type: "web_search_call", id: webSearchCellItemId(e.itemId), status: e.status ?? "completed",
550
+ action: webSearchAction(e.queries, e.anthropic),
551
551
  ...(safeSources.length > 0 ? { sources: safeSources } : {}),
552
- ...(e.fetch ? { fetch_result: e.fetch.result } : {}),
552
+ ...(e.anthropic ? { anthropic_blocks: e.anthropic } : {}),
553
553
  });
554
554
  if (safeSources.length > 0) {
555
555
  for (const source of safeSources) {
package/src/bridge/sse.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { anthropicProgressItems } from "../responses/anthropic-progress";
2
2
  import type {
3
3
  AdapterEvent,
4
+ AnthropicServerBlocks,
4
5
  OcxMessagePhase,
5
6
  OcxProviderContinuationState,
6
7
  OcxProviderOpaqueToolCallMetadata,
@@ -49,7 +50,7 @@ import {
49
50
  type TranslatorBudget,
50
51
  type TranslatorBufferKind,
51
52
  } from "../lib/translator-budget";
52
- import { adapterFailureFromEvent, emptyChunks, joinChunks, ownedBudgetAbandonedMs, responsesUsage, toolCallArgumentsCouldBeJson, toolCallArgumentsUsable, uuid, webSearchAction } from "./internal";
53
+ import { adapterFailureFromEvent, emptyChunks, joinChunks, ownedBudgetAbandonedMs, responsesUsage, toolCallArgumentsCouldBeJson, toolCallArgumentsUsable, uuid, webSearchAction, webSearchCellItemId } from "./internal";
53
54
  import type { OutputItem, StringChunks } from "./internal";
54
55
  import { terminalRefusalFailure } from "./terminal-refusal";
55
56
 
@@ -685,15 +686,15 @@ export function bridgeToResponsesSSE(
685
686
  // leaves a "Searching the web" spinner spinning forever.
686
687
  // `sources` rides on the done item (additive field; codex-rs serde ignores unknown fields) so
687
688
  // downstream translators (claude outbound) can fill web_search_tool_result content.
688
- // `fetch` (Anthropic's server-side web_fetch) makes the cell a page open and rides its result
689
- // verbatim as `fetch_result`, for the same claude outbound translator.
690
- const closeCurrentWebSearch = (status: "completed" | "failed", queries: string[], sources?: { url: string; title?: string }[], fetch?: { url: string; result: unknown }) => {
689
+ // `anthropic` (Anthropic's server-side call) rides verbatim as `anthropic_blocks`, for the same
690
+ // claude outbound translator; a web_fetch makes the cell a page open.
691
+ const closeCurrentWebSearch = (status: "completed" | "failed", queries: string[], sources?: { url: string; title?: string }[], anthropic?: AnthropicServerBlocks) => {
691
692
  if (!currentWebSearch) return;
692
693
  const item = {
693
694
  type: "web_search_call", id: currentWebSearch.itemId, status,
694
- action: webSearchAction(queries, fetch),
695
+ action: webSearchAction(queries, anthropic),
695
696
  ...(sources && sources.length > 0 ? { sources } : {}),
696
- ...(fetch ? { fetch_result: fetch.result } : {}),
697
+ ...(anthropic ? { anthropic_blocks: anthropic } : {}),
697
698
  };
698
699
  emit("response.output_item.done", { output_index: currentWebSearch.outputIndex, item });
699
700
  retainFinishedItem(item as OutputItem);
@@ -769,9 +770,14 @@ export function bridgeToResponsesSSE(
769
770
  returnIterator();
770
771
  };
771
772
  let handlingTranslatorOverflow = false;
772
- terminateForTranslatorOverflow = _error => {
773
+ terminateForTranslatorOverflow = error => {
773
774
  if (handlingTranslatorOverflow || terminated || clientCancelled || closed) return;
774
775
  handlingTranslatorOverflow = true;
776
+ // An adapter-raised overflow event carries the usage the turn already consumed; the
777
+ // provider billed it, so it must not be logged as zero.
778
+ const usage = (error as AdapterEvent | null | undefined)?.type === "error"
779
+ ? (error as Extract<AdapterEvent, { type: "error" }>).usage : undefined;
780
+ if (usage) options?.onUsage?.(usage);
775
781
  abortCurrentToolCallForTranslatorOverflow();
776
782
  currentWebSearch = null;
777
783
  releasePendingWebSources();
@@ -787,6 +793,7 @@ export function bridgeToResponsesSSE(
787
793
  sequence_number: seq++,
788
794
  response: {
789
795
  ...responseSnapshot("failed", finishedItems),
796
+ ...(usage ? { usage: responsesUsage(usage) } : {}),
790
797
  error: failure,
791
798
  last_error: failure,
792
799
  },
@@ -1210,7 +1217,7 @@ export function bridgeToResponsesSSE(
1210
1217
  flushHiddenRawReasoning();
1211
1218
  if (currentToolCall) closeCurrentToolCall();
1212
1219
  if (currentWebSearch) closeCurrentWebSearch("completed", []);
1213
- const wsItemId = `ws_${uuid()}`;
1220
+ const wsItemId = webSearchCellItemId(event.itemId);
1214
1221
  emit("response.output_item.added", {
1215
1222
  output_index: outputIndex,
1216
1223
  item: { type: "web_search_call", id: wsItemId, status: "in_progress" },
@@ -1223,7 +1230,7 @@ export function bridgeToResponsesSSE(
1223
1230
  // (defensive), synthesize the added frame first so the done has a matching item.
1224
1231
  if (!currentWebSearch || currentWebSearch.eventId !== event.id) {
1225
1232
  if (currentWebSearch) closeCurrentWebSearch("completed", []);
1226
- const wsItemId2 = `ws_${uuid()}`;
1233
+ const wsItemId2 = webSearchCellItemId(event.itemId);
1227
1234
  emit("response.output_item.added", {
1228
1235
  output_index: outputIndex,
1229
1236
  item: { type: "web_search_call", id: wsItemId2, status: "in_progress" },
@@ -1231,7 +1238,7 @@ export function bridgeToResponsesSSE(
1231
1238
  currentWebSearch = { itemId: wsItemId2, eventId: event.id, outputIndex };
1232
1239
  }
1233
1240
  const safeSources = safeWebSearchSources(event.sources);
1234
- closeCurrentWebSearch(event.status ?? "completed", event.queries, safeSources, event.fetch);
1241
+ closeCurrentWebSearch(event.status ?? "completed", event.queries, safeSources, event.anthropic);
1235
1242
  // Queue this search's sources for the next assistant message (dedup by URL).
1236
1243
  if (safeSources.length > 0) {
1237
1244
  for (const source of safeSources) {
@@ -87,6 +87,72 @@ export function declaredAnthropicServerTools(raw: unknown): Rec[] | undefined {
87
87
  return tools.length > 0 ? tools : undefined;
88
88
  }
89
89
 
90
+ /** Validate original call/result order without inventing inputs or pairing a later call backwards. */
91
+ function replayableServerRun(run: readonly Rec[]): boolean {
92
+ const pending = new Map<string, Rec>();
93
+ const completed = new Set<string>();
94
+ for (const block of run) {
95
+ if (block.type === "server_tool_use") {
96
+ const { id, name, input } = block;
97
+ if (typeof id !== "string" || !id.startsWith("srvtoolu_") || pending.has(id) || completed.has(id)
98
+ || (name !== "web_search" && name !== "web_fetch") || !isRec(input)) return false;
99
+ const prototype = Object.getPrototypeOf(input);
100
+ const required = input[name === "web_search" ? "query" : "url"];
101
+ if ((prototype !== Object.prototype && prototype !== null)
102
+ || typeof required !== "string" || required.trim().length === 0) return false;
103
+ pending.set(id, block);
104
+ } else {
105
+ const id = block.tool_use_id;
106
+ if (typeof id !== "string") return false;
107
+ const call = pending.get(id);
108
+ if (!call || block.type !== `${call.name}_tool_result`
109
+ || (Array.isArray(block.content) && !block.content.every(h => isRec(h) && typeof h.encrypted_content === "string"))) return false;
110
+ pending.delete(id);
111
+ completed.add(id);
112
+ }
113
+ }
114
+ return completed.size > 0 && pending.size === 0;
115
+ }
116
+
117
+ /**
118
+ * Completed Anthropic server-tool exchanges (`server_tool_use` + `web_search_tool_result` /
119
+ * `web_fetch_tool_result`) in the caller's assistant history. The Responses body drops them, so
120
+ * an Anthropic destination re-inserts them out of body, keyed by the block that follows each run
121
+ * in its message: `call:<tool_use id>`, `text:<text>`, `thinking:<signature>` or
122
+ * `redacted:<data>`, all of which survive translation. An anchor that occurs more than once in the
123
+ * assistant history (string content included) cannot name one owner, so its run stays dropped,
124
+ * as it was before replay existed. A run with none, an unpaired call, a
125
+ * non-Anthropic id, or a search hit without `encrypted_content` is not replayable and stays dropped.
126
+ */
127
+ export function replayedAnthropicServerBlocks(raw: unknown): Map<string, Rec[]> | undefined {
128
+ if (!isRec(raw) || !Array.isArray(raw.messages)) return undefined;
129
+ const out = new Map<string, Rec[]>();
130
+ const seen = new Map<string, number>();
131
+ const count = (anchor: string) => seen.set(anchor, (seen.get(anchor) ?? 0) + 1);
132
+ for (const message of raw.messages) {
133
+ if (!isRec(message) || message.role !== "assistant") continue;
134
+ if (typeof message.content === "string") count(`text:${message.content}`);
135
+ if (!Array.isArray(message.content)) continue;
136
+ let run: Rec[] = [];
137
+ for (const block of message.content.filter(isRec)) {
138
+ if (block.type === "server_tool_use" || block.type === "web_search_tool_result" || block.type === "web_fetch_tool_result") {
139
+ // The caller's breakpoints are dropped on this lane, as for the declared tools above.
140
+ run.push({ ...block, cache_control: undefined });
141
+ continue;
142
+ }
143
+ const anchor = block.type === "tool_use" ? `call:${String(block.id)}`
144
+ : block.type === "text" ? `text:${String(block.text)}`
145
+ : block.type === "thinking" ? `thinking:${String(block.signature)}`
146
+ : block.type === "redacted_thinking" ? `redacted:${String(block.data)}` : undefined;
147
+ if (anchor) count(anchor);
148
+ if (anchor && replayableServerRun(run)) out.set(anchor, run);
149
+ run = [];
150
+ }
151
+ }
152
+ for (const anchor of out.keys()) if ((seen.get(anchor) ?? 0) > 1) out.delete(anchor);
153
+ return out.size > 0 ? out : undefined;
154
+ }
155
+
90
156
  export function toolChoiceToResponses(choice: unknown, body: Rec): void {
91
157
  if (!isRec(choice)) return;
92
158
  if (choice.disable_parallel_tool_use === true) body.parallel_tool_calls = false;