@bitkyc08/opencodex 2.58.0 → 2.60.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 (303) hide show
  1. package/README.md +28 -10
  2. package/gui/dist/assets/index-BTuCbqQd.css +1 -0
  3. package/gui/dist/assets/index-DoBVdPHP.js +134 -0
  4. package/gui/dist/index.html +2 -2
  5. package/gui/dist/provider-icons/crusoe.svg +1 -0
  6. package/gui/dist/provider-icons/opper.svg +3 -0
  7. package/package.json +4 -1
  8. package/src/adapters/anthropic-image-codec.ts +16 -2
  9. package/src/adapters/anthropic-image-normalize.ts +49 -2
  10. package/src/adapters/anthropic.ts +4 -1
  11. package/src/adapters/base.ts +34 -1
  12. package/src/adapters/coding-agent/turn.ts +22 -2
  13. package/src/adapters/command-code.ts +50 -3
  14. package/src/adapters/cursor/catalog.ts +11 -0
  15. package/src/adapters/cursor/checkpoint-store.ts +3 -0
  16. package/src/adapters/cursor/discovery.ts +11 -8
  17. package/src/adapters/cursor/effort-map.ts +16 -2
  18. package/src/adapters/cursor/envelope-echo.ts +55 -2
  19. package/src/adapters/cursor/live-transport.ts +26 -9
  20. package/src/adapters/cursor/message-mapper.ts +3 -2
  21. package/src/adapters/cursor/protobuf-request.ts +8 -5
  22. package/src/adapters/cursor/request-builder.ts +21 -4
  23. package/src/adapters/cursor/thread-continuity.ts +105 -31
  24. package/src/adapters/cursor/tool-guidance.ts +5 -4
  25. package/src/adapters/cursor/transport.ts +19 -0
  26. package/src/adapters/cursor.ts +45 -2
  27. package/src/adapters/devin/cloud-direct/chat.ts +14 -3
  28. package/src/adapters/devin/cloud-direct/index.ts +7 -0
  29. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +140 -0
  30. package/src/adapters/devin.ts +125 -26
  31. package/src/adapters/google-antigravity-replay.ts +1 -1
  32. package/src/adapters/google-antigravity-wire.ts +55 -4
  33. package/src/adapters/google-http.ts +57 -11
  34. package/src/adapters/google-tool-schema.ts +595 -31
  35. package/src/adapters/google-wire-compiler.ts +93 -10
  36. package/src/adapters/google-wire-shape.ts +461 -0
  37. package/src/adapters/google.ts +60 -10
  38. package/src/adapters/openai-chat/response-events.ts +61 -0
  39. package/src/adapters/openai-chat-images.ts +3 -1
  40. package/src/adapters/openai-chat.ts +10 -11
  41. package/src/adapters/openai-responses/image-gen.ts +8 -6
  42. package/src/adapters/openai-responses/passthrough.ts +25 -4
  43. package/src/adapters/openai-responses/reasoning.ts +7 -0
  44. package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
  45. package/src/adapters/openai-responses/tool-schema.ts +19 -7
  46. package/src/adapters/opencode-go-additional-tools.ts +12 -2
  47. package/src/adapters/responses-tool-schema.ts +76 -46
  48. package/src/adapters/run-turn-queue.ts +17 -4
  49. package/src/bridge/response-json.ts +1 -1
  50. package/src/bridge/sse.ts +28 -35
  51. package/src/claude/context-windows.ts +22 -0
  52. package/src/claude/outbound.ts +35 -4
  53. package/src/cli/account-api.ts +4 -3
  54. package/src/cli/account-extended.ts +26 -6
  55. package/src/cli/account-orca-import.ts +63 -0
  56. package/src/cli/account.ts +32 -4
  57. package/src/cli/capabilities.ts +40 -0
  58. package/src/cli/claude.ts +29 -1
  59. package/src/cli/codex-cli-update.ts +97 -2
  60. package/src/cli/dispatch.ts +57 -3
  61. package/src/cli/doctor.ts +218 -4
  62. package/src/cli/help.ts +4 -1
  63. package/src/cli/hub.ts +3 -2
  64. package/src/cli/index.ts +95 -22
  65. package/src/cli/models-runtime.ts +33 -4
  66. package/src/cli/opencode.ts +2 -2
  67. package/src/cli/provider.ts +13 -1
  68. package/src/cli/registry.ts +11 -1
  69. package/src/cli/runtime-api.ts +44 -0
  70. package/src/cli/start-args.ts +94 -0
  71. package/src/cli/system-command.ts +2 -0
  72. package/src/client/machine-api.ts +6 -5
  73. package/src/client/machine-listener.ts +16 -3
  74. package/src/client/runtime.ts +26 -2
  75. package/src/clients/config-export/constants.ts +2 -3
  76. package/src/clients/config-export.ts +5 -5
  77. package/src/codex/account-store.ts +146 -5
  78. package/src/codex/auth-api/account-list.ts +19 -11
  79. package/src/codex/auth-api/pool-quota-probe.ts +44 -10
  80. package/src/codex/auth-api/routes.ts +17 -2
  81. package/src/codex/auth-context.ts +16 -12
  82. package/src/codex/catalog/build-entries.ts +25 -4
  83. package/src/codex/catalog/derive-entry.ts +8 -1
  84. package/src/codex/catalog/effort.ts +10 -6
  85. package/src/codex/catalog/gather-capture.ts +22 -2
  86. package/src/codex/catalog/model-hints.ts +66 -33
  87. package/src/codex/catalog/parsing.ts +90 -5
  88. package/src/codex/catalog/provider-models.ts +19 -2
  89. package/src/codex/catalog/reserve-warn.ts +96 -0
  90. package/src/codex/catalog/retained-sync.ts +41 -26
  91. package/src/codex/catalog/routed-gather.ts +61 -3
  92. package/src/codex/cli-installation-identity.ts +210 -0
  93. package/src/codex/cli-installation-targets.ts +158 -0
  94. package/src/codex/context-compat.ts +5 -2
  95. package/src/codex/convergence.ts +5 -0
  96. package/src/codex/desired-state.ts +4 -1
  97. package/src/codex/history-job.ts +6 -6
  98. package/src/codex/history-provider.ts +24 -167
  99. package/src/codex/history-rollout-read.ts +174 -0
  100. package/src/codex/history-state-open.ts +105 -0
  101. package/src/codex/inject/config-toml.ts +44 -2
  102. package/src/codex/inject.ts +3 -2
  103. package/src/codex/internal/catalog-writer.ts +33 -1
  104. package/src/codex/lineage.ts +83 -32
  105. package/src/codex/loopback-target.ts +31 -0
  106. package/src/codex/main-account-hard-lock.ts +2 -1
  107. package/src/codex/main-account.ts +10 -3
  108. package/src/codex/main-device-reauth.ts +17 -9
  109. package/src/codex/model-cache.ts +47 -0
  110. package/src/codex/model-entitlements.ts +80 -2
  111. package/src/codex/observed-model-denials.ts +230 -0
  112. package/src/codex/orca-auth-source.ts +94 -0
  113. package/src/codex/orca-import.ts +219 -0
  114. package/src/codex/prompt-text-probe.ts +289 -16
  115. package/src/codex/quota-401-recovery.ts +12 -0
  116. package/src/codex/quota-types.ts +65 -0
  117. package/src/codex/quota.ts +24 -19
  118. package/src/codex/routing/cooldown-math.ts +8 -47
  119. package/src/codex/routing/pin-drain.ts +57 -0
  120. package/src/codex/routing.ts +20 -16
  121. package/src/codex/shim.ts +1 -1
  122. package/src/codex/subagent-model-fallback.ts +114 -2
  123. package/src/codex/windows-installation-files.ts +224 -0
  124. package/src/combos/failover.ts +125 -5
  125. package/src/config/admitted-identity.ts +222 -0
  126. package/src/config/diagnostics.ts +43 -1
  127. package/src/config/feature-flags.ts +5 -0
  128. package/src/config/load-degrade.ts +33 -0
  129. package/src/config/pending-teardown.ts +8 -0
  130. package/src/config/process-state.ts +36 -3
  131. package/src/config/provider-relative-send-path.ts +16 -0
  132. package/src/config/proxy-env.ts +31 -7
  133. package/src/config/schema/compaction-triggers.ts +11 -0
  134. package/src/config/schema/config-schema.ts +25 -0
  135. package/src/config/schema/leaf-validators.ts +76 -17
  136. package/src/config.ts +2 -2
  137. package/src/generated/compatibility-version.json +410 -258
  138. package/src/generated/model-metadata.ts +1 -1
  139. package/src/grok/reset-coupons.ts +38 -19
  140. package/src/images/loop.ts +6 -1
  141. package/src/integrations/aside-profile-context.ts +37 -3
  142. package/src/integrations/aside-profile-journal.ts +68 -3
  143. package/src/integrations/aside-profiles.ts +128 -3
  144. package/src/integrations/mutation-plan.ts +815 -0
  145. package/src/integrations/writer.ts +85 -99
  146. package/src/lab/live/transport.ts +4 -0
  147. package/src/lab/live/types.ts +5 -0
  148. package/src/lab/subject/behavior-fingerprint.ts +1 -1
  149. package/src/lib/admin-secrets.ts +9 -1
  150. package/src/lib/bounded-body.ts +4 -2
  151. package/src/lib/debug-log-buffer.ts +6 -1
  152. package/src/lib/debug.ts +23 -0
  153. package/src/lib/destination-policy.ts +48 -6
  154. package/src/lib/errors.ts +82 -15
  155. package/src/lib/http-response-semantics.ts +57 -0
  156. package/src/lib/lab-live-pinned-sender.ts +26 -12
  157. package/src/lib/local-destinations.ts +32 -5
  158. package/src/lib/pinned-http.ts +142 -2
  159. package/src/lib/plain-data.ts +103 -0
  160. package/src/lib/process-control.ts +13 -5
  161. package/src/lib/provider-outbound.ts +53 -5
  162. package/src/lib/proxy-env.ts +70 -3
  163. package/src/lib/request-execution-budget.ts +11 -3
  164. package/src/lib/response-body-inactivity.ts +193 -0
  165. package/src/lib/retry-delay.ts +69 -0
  166. package/src/lib/socks5-fetch.ts +741 -0
  167. package/src/lib/spend-ledger-owner.ts +364 -0
  168. package/src/lib/spend-reservation-ledger.ts +332 -35
  169. package/src/lib/windows-system-proxy.ts +16 -11
  170. package/src/lib/workflow-budget.ts +145 -8
  171. package/src/oauth/account-quota-rank.ts +72 -15
  172. package/src/oauth/callback-server.ts +4 -3
  173. package/src/oauth/generic-account-failover.ts +41 -27
  174. package/src/oauth/health.ts +12 -1
  175. package/src/oauth/index.ts +3 -107
  176. package/src/oauth/login-flow-state.ts +127 -0
  177. package/src/oauth/orcarouter.ts +15 -2
  178. package/src/oauth/store.ts +8 -0
  179. package/src/providers/codex-capacity.ts +9 -0
  180. package/src/providers/derive.ts +34 -17
  181. package/src/providers/devin-cli-authmode-migration.ts +14 -10
  182. package/src/providers/devin-provider-merge-migration.ts +33 -12
  183. package/src/providers/free-directory.ts +20 -2
  184. package/src/providers/key-failover.ts +327 -25
  185. package/src/providers/model-rename-migration.ts +56 -1
  186. package/src/providers/model-rename-startup.ts +7 -5
  187. package/src/providers/openai-sidecar.ts +4 -0
  188. package/src/providers/openai-virtual-models.ts +42 -2
  189. package/src/providers/opencode-go-transport.ts +14 -5
  190. package/src/providers/quota/antigravity.ts +22 -2
  191. package/src/providers/quota/report-cache.ts +3 -0
  192. package/src/providers/quota/vendor-probes-key.ts +1 -1
  193. package/src/providers/registry/entries-core.ts +39 -17
  194. package/src/providers/registry/entries-extended.ts +131 -1
  195. package/src/providers/registry/model-ids.ts +168 -0
  196. package/src/providers/registry/model-seeds.ts +87 -21
  197. package/src/providers/registry/types.ts +2 -0
  198. package/src/providers/resolved-model-policy-merge.ts +167 -0
  199. package/src/providers/resolved-model-policy.ts +406 -0
  200. package/src/providers/stale-vision-classification-migration.ts +137 -0
  201. package/src/responses/apply-patch-envelope.ts +32 -11
  202. package/src/responses/bridge-search-replay-cache.ts +152 -0
  203. package/src/responses/code-mode-helper-compat.ts +26 -16
  204. package/src/responses/custom-tool-compat.ts +1 -1
  205. package/src/responses/freeform-wrapper-scan.ts +279 -0
  206. package/src/responses/hosted-tool-policy.ts +85 -2
  207. package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
  208. package/src/responses/progressive-freeform-input.ts +130 -0
  209. package/src/responses/reasoning-envelope.ts +30 -0
  210. package/src/responses/schema.ts +9 -2
  211. package/src/responses/state.ts +5 -12
  212. package/src/responses/tool-name-aliases.ts +15 -1
  213. package/src/router.ts +91 -115
  214. package/src/routing/compatibility/behavior.ts +9 -0
  215. package/src/routing/compatibility/subject.ts +16 -1
  216. package/src/server/adapter-resolve.ts +9 -0
  217. package/src/server/auth-cors.ts +29 -0
  218. package/src/server/chat-completions.ts +13 -5
  219. package/src/server/chat-native-sse.ts +26 -9
  220. package/src/server/chat-native.ts +10 -4
  221. package/src/server/claude-messages.ts +30 -5
  222. package/src/server/effort-row.ts +11 -3
  223. package/src/server/grok-responses-control-frame.ts +160 -1
  224. package/src/server/gui-static.ts +36 -2
  225. package/src/server/inbound-body-admission.ts +187 -0
  226. package/src/server/index/serve-options.ts +86 -29
  227. package/src/server/index/spend-ledger-lifecycle.ts +66 -0
  228. package/src/server/index/websocket-handler.ts +6 -1
  229. package/src/server/index.ts +24 -25
  230. package/src/server/management/api-access.ts +3 -4
  231. package/src/server/management/aside-profile-routes.ts +266 -7
  232. package/src/server/management/config-routes.ts +48 -7
  233. package/src/server/management/context.ts +3 -0
  234. package/src/server/management/integration-routes.ts +287 -5
  235. package/src/server/management/metrics-routes.ts +20 -0
  236. package/src/server/management/model-rows.ts +224 -12
  237. package/src/server/management/provider-capability-config.ts +35 -7
  238. package/src/server/management/provider-routes.ts +70 -18
  239. package/src/server/management/route-registry.ts +14 -0
  240. package/src/server/management/shared.ts +10 -3
  241. package/src/server/management/system-restart.ts +7 -2
  242. package/src/server/management/system-routes.ts +2 -0
  243. package/src/server/management/usage-aggregate-cache.ts +4 -0
  244. package/src/server/management-api.ts +2 -0
  245. package/src/server/management-auth.ts +15 -1
  246. package/src/server/proxy-liveness.ts +97 -2
  247. package/src/server/readiness.ts +29 -10
  248. package/src/server/relay-eager.ts +24 -2
  249. package/src/server/relay.ts +136 -34
  250. package/src/server/request-log.ts +80 -3
  251. package/src/server/request-metrics.ts +236 -0
  252. package/src/server/responses/adapter-continuation.ts +74 -30
  253. package/src/server/responses/adapter-delivery.ts +39 -8
  254. package/src/server/responses/adapter-dispatch.ts +63 -30
  255. package/src/server/responses/compact.ts +89 -18
  256. package/src/server/responses/compaction-routing.ts +111 -0
  257. package/src/server/responses/core-codex-account.ts +90 -24
  258. package/src/server/responses/core-combo.ts +7 -7
  259. package/src/server/responses/core-normalize.ts +16 -15
  260. package/src/server/responses/core-opaque-recovery.ts +1 -0
  261. package/src/server/responses/core-options.ts +4 -0
  262. package/src/server/responses/encrypted-payload.ts +20 -2
  263. package/src/server/responses/fetch-helpers.ts +68 -2
  264. package/src/server/responses/passthrough-delivery.ts +41 -15
  265. package/src/server/responses/passthrough-dispatch.ts +145 -53
  266. package/src/server/responses/passthrough-execution.ts +11 -1
  267. package/src/server/responses/policy-fallback.ts +5 -13
  268. package/src/server/responses/request-prepare.ts +96 -17
  269. package/src/server/responses/request-send-budget.ts +89 -8
  270. package/src/server/responses/request-sidecar-auth.ts +17 -9
  271. package/src/server/responses/request-spend.ts +38 -9
  272. package/src/server/responses/request-transport.ts +15 -12
  273. package/src/server/responses/run-turn-execution.ts +45 -8
  274. package/src/server/responses/sidecar-execution.ts +19 -2
  275. package/src/server/responses/ws-upstream.ts +16 -28
  276. package/src/server/responses-custom-tool-repair.ts +29 -56
  277. package/src/server/responses-undeclared-tool-guard.ts +31 -1
  278. package/src/server/sse-frame-buffer.ts +12 -10
  279. package/src/server/sse-payload-rewrite.ts +37 -10
  280. package/src/server/system-env-shell.ts +5 -1
  281. package/src/server/system-env.ts +7 -1
  282. package/src/server/workflow-refusal.ts +56 -2
  283. package/src/service/cli.ts +16 -6
  284. package/src/service/guards.ts +10 -0
  285. package/src/service/health.ts +43 -0
  286. package/src/service/state.ts +7 -2
  287. package/src/tray/windows-tray.ps1 +155 -3
  288. package/src/types/accounts.ts +4 -0
  289. package/src/types/config.ts +111 -6
  290. package/src/types/provider.ts +37 -0
  291. package/src/types/request.ts +9 -1
  292. package/src/types/tools.ts +14 -0
  293. package/src/types/wire.ts +9 -1
  294. package/src/types.ts +1 -0
  295. package/src/usage/expected-prices.ts +28 -0
  296. package/src/usage/log.ts +87 -4
  297. package/src/vision/eligibility.ts +88 -9
  298. package/src/vision/plan.ts +34 -10
  299. package/src/web-search/executor.ts +41 -2
  300. package/src/web-search/loop.ts +6 -1
  301. package/src/web-search/passthrough-bridge.ts +39 -5
  302. package/gui/dist/assets/index-BbrHOIY0.js +0 -128
  303. package/gui/dist/assets/index-C5-RdDmD.css +0 -1
@@ -1,5 +1,5 @@
1
1
  import type { AdapterFetchContext, AdapterRequest, ProviderAdapter } from "./base";
2
- import { debugDroppedFrame } from "../lib/debug";
2
+ import { debugDroppedFrame, debugProviderDiagnosticLazy } from "../lib/debug";
3
3
  import { createToolCallIdAllocator } from "./tool-call-id";
4
4
  import { createImageBudget, materializeInlineImage, MAX_ENCODED_BYTES_PER_IMAGE, artifactHttpUrl } from "../images/artifacts";
5
5
  import type {
@@ -20,8 +20,10 @@ import { getVertexAccessToken } from "../lib/gcp-adc";
20
20
  import { fetchAntigravityWithRetry, fetchVertexWithRetry } from "./google-http";
21
21
  import { safeAntigravityHttpErrorMessage, safeVertexHttpErrorMessage } from "./google-errors";
22
22
  import { isVertexTruncatedTurn, vertexTruncationErrorMessage } from "./google-truncation";
23
- import { ANTIGRAVITY_REQUEST_UA, antigravitySessionId, isLikelyRealThoughtSignature, sanitizeAntigravityClaudeSignatures } from "./google-antigravity-wire";
23
+ import { ANTIGRAVITY_REQUEST_UA, antigravitySessionAnchor, antigravitySessionId, isLikelyRealThoughtSignature, sanitizeAntigravityClaudeSignatures } from "./google-antigravity-wire";
24
+ import { summarizeGoogleWireShape } from "./google-wire-shape";
24
25
  import { compileGoogleWireBody } from "./google-wire-compiler";
26
+ import type { GoogleToolSchemaLossReport, GoogleToolSchemaProfile } from "./google-tool-schema";
25
27
  import { identifyRoutedModel } from "./identity";
26
28
  import {
27
29
  antigravityUsesReplayCache,
@@ -57,7 +59,6 @@ const GOOGLE_BREVITY_INSTRUCTION = [
57
59
 
58
60
  const ANTIGRAVITY_REJECTED_CLAUDE_SDK_PARAGRAPH =
59
61
  "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
60
-
61
62
  /**
62
63
  * CCA Flash generations that reject the Claude-Agent identity paragraph.
63
64
  *
@@ -102,6 +103,20 @@ function stripAntigravityRejectedClaudeSdkParagraph(systemText: string): string
102
103
  .join("\n\n");
103
104
  }
104
105
 
106
+ /**
107
+ * Strips Claude Code CLI's internal billing header (`x-anthropic-billing-header: ...`)
108
+ * at the start of the system prompt, because Cloud Code Assist / Google Antigravity inspects
109
+ * `systemInstruction` and rejects requests containing Anthropic billing metadata with
110
+ * HTTP 429 RESOURCE_EXHAUSTED.
111
+ *
112
+ * Matching is restricted to the prompt start (`^` without the `/m` multiline flag) so that
113
+ * user prompts discussing billing headers in intermediate lines are never modified, and
114
+ * prompts without a billing header preserve their leading whitespace untouched.
115
+ */
116
+ function stripAntigravityBillingHeader(systemText: string): string {
117
+ return systemText.replace(/^x-anthropic-billing-header:[^\n]*\n*/, "");
118
+ }
119
+
105
120
  /**
106
121
  * Documented output ceiling for a Google-surface model, or `undefined` when the id is not
107
122
  * recognized.
@@ -276,6 +291,7 @@ function messagesToGeminiFormat(
276
291
  parsed: OcxParsedRequest,
277
292
  identityModelId: string,
278
293
  stripRejectedClaudeSdkParagraph = false,
294
+ isCloudCodeAssist = false,
279
295
  ): { systemInstruction?: unknown; contents: unknown[]; replayedCallIds: string[] } {
280
296
  // Neutralize Codex's GPT-5 identity line (Gemini/Antigravity share this path) so a routed model
281
297
  // never misreports as GPT-5/OpenAI, and never leaks the proxy identity upstream.
@@ -285,9 +301,12 @@ function messagesToGeminiFormat(
285
301
  ...(toolCatalogNudge ? [toolCatalogNudge] : []),
286
302
  GOOGLE_BREVITY_INSTRUCTION,
287
303
  ].join("\n\n"), identityModelId);
288
- const systemText = stripRejectedClaudeSdkParagraph
289
- ? stripAntigravityRejectedClaudeSdkParagraph(identifiedSystemText)
304
+ let systemText = isCloudCodeAssist
305
+ ? stripAntigravityBillingHeader(identifiedSystemText)
290
306
  : identifiedSystemText;
307
+ if (stripRejectedClaudeSdkParagraph) {
308
+ systemText = stripAntigravityRejectedClaudeSdkParagraph(systemText);
309
+ }
291
310
  const systemInstruction = { parts: [{ text: systemText }] };
292
311
 
293
312
  const contents: unknown[] = [];
@@ -720,6 +739,14 @@ function invalidGoogleShapeEvent(
720
739
  }
721
740
 
722
741
  export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapter {
742
+ const toolSchemaPolicy = provider.googleToolSchemaPolicy ?? "compatible";
743
+ const toolSchemaProfile = {
744
+ endpointClass: provider.googleMode ?? "ai-studio",
745
+ } satisfies GoogleToolSchemaProfile;
746
+ const reportToolSchemaLoss = (report: GoogleToolSchemaLossReport): void => {
747
+ if (!report.lossy && report.uncertainComparisons === 0) return;
748
+ debugProviderDiagnosticLazy("google", "google-tool-schema-loss", () => ({ ...report }));
749
+ };
723
750
  // Per-request closure: resolveAdapter builds a fresh adapter per request (server.ts), so buildRequest
724
751
  // can stash the CCA model/session for parseStream's reasoning-replay observation.
725
752
  let antigravityModel: string | undefined;
@@ -782,7 +809,11 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
782
809
  ...(provider.googleMode === "vertex" || provider.googleMode === "cloud-code-assist"
783
810
  ? {
784
811
  fetchResponse: (request: AdapterRequest, ctx?: AdapterFetchContext): Promise<Response> =>
785
- (provider.googleMode === "cloud-code-assist" ? fetchAntigravityWithRetry : fetchVertexWithRetry)(request, ctx),
812
+ (provider.googleMode === "cloud-code-assist" ? fetchAntigravityWithRetry : fetchVertexWithRetry)(
813
+ request,
814
+ ctx,
815
+ { toolSchemaProfile, toolSchemaPolicy },
816
+ ),
786
817
  formatErrorBody: (status: number, _headers: Headers, payloadText: string): string =>
787
818
  (provider.googleMode === "cloud-code-assist" ? safeAntigravityHttpErrorMessage : safeVertexHttpErrorMessage)(status, payloadText),
788
819
  }
@@ -833,12 +864,14 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
833
864
  && /^gemini-/.test(routedModelId) && !isImageCapableModel(parsed.modelId);
834
865
  // AI Studio's `-tiered` spelling is wire-only; CCA aliases may migrate to another generation.
835
866
  const identityModelId = provider.googleMode === "cloud-code-assist" ? routedModelId : parsed.modelId;
836
- const stripRejectedClaudeSdkParagraph = provider.googleMode === "cloud-code-assist"
867
+ const isCloudCodeAssist = provider.googleMode === "cloud-code-assist";
868
+ const stripRejectedClaudeSdkParagraph = isCloudCodeAssist
837
869
  && rejectsClaudeSdkParagraph(parsed.modelId, routedModelId);
838
870
  const { systemInstruction, contents, replayedCallIds } = messagesToGeminiFormat(
839
871
  parsed,
840
872
  identityModelId,
841
873
  stripRejectedClaudeSdkParagraph,
874
+ isCloudCodeAssist,
842
875
  );
843
876
  lastInjectedCallIds = [...replayedCallIds];
844
877
  lastReasoningReplayScope = parsed._reasoningReplayScope;
@@ -951,7 +984,8 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
951
984
  const fcc = (existing.functionCallingConfig ?? {}) as Record<string, unknown>;
952
985
  draftRequest.toolConfig = { ...existing, functionCallingConfig: { ...fcc, mode: "VALIDATED" } };
953
986
  }
954
- const compiled = compileGoogleWireBody(draftRequest);
987
+ const compiled = compileGoogleWireBody(draftRequest, toolSchemaProfile, toolSchemaPolicy);
988
+ reportToolSchemaLoss(compiled.toolSchemaLossReport);
955
989
  const request = compiled.body;
956
990
  restoreGoogleToolName = compiled.restoreToolName;
957
991
  // Compile names before replay: signatures are keyed by the exact provider-visible name.
@@ -970,6 +1004,20 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
970
1004
  // Vertex and AI Studio share one decision. A second check here would append a
971
1005
  // duplicate nudge whenever signature sanitization reshapes the tail afterwards.
972
1006
  }
1007
+ // Opt-in structural description of the request that is about to leave (#5008).
1008
+ //
1009
+ // Passed as a BUILDER, not a value: the lazy form gates before invoking it, so a session
1010
+ // with provider debug off never pays the walk, and it evaluates the projection inside its
1011
+ // own try/catch, so a throw in here cannot turn a built request into a rejected one. The
1012
+ // projection only reads `request`, so the bytes below are the same either way.
1013
+ debugProviderDiagnosticLazy("google", "antigravity-wire-shape", () => summarizeGoogleWireShape(request, {
1014
+ sessionAnchor: antigravitySessionAnchor(parsed),
1015
+ // Signed at translation time, from client history or the durable store. The
1016
+ // Antigravity session cache signs afterwards, inside applyAntigravityReplay, and the
1017
+ // projection attributes that remainder to the cache rather than to this count.
1018
+ historySignedCalls: replayedCallIds.length,
1019
+ replayScopeBound: parsed._reasoningReplayScope !== undefined,
1020
+ }));
973
1021
  const envelope = {
974
1022
  model: wireModelId,
975
1023
  // The envelope's `userAgent` field is a protocol constant ("antigravity"), distinct from
@@ -987,7 +1035,8 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
987
1035
  }
988
1036
 
989
1037
  if (provider.googleMode === "vertex") {
990
- const compiled = compileGoogleWireBody(body);
1038
+ const compiled = compileGoogleWireBody(body, toolSchemaProfile, toolSchemaPolicy);
1039
+ reportToolSchemaLoss(compiled.toolSchemaLossReport);
991
1040
  restoreGoogleToolName = compiled.restoreToolName;
992
1041
  const vertexProject = provider.project || process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT || "api-key";
993
1042
  const vertexLocation = provider.location || process.env.GOOGLE_CLOUD_LOCATION || "global";
@@ -1033,7 +1082,8 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
1033
1082
  if (!apiKey) throw new Error("google (AI Studio) requires a non-empty API key");
1034
1083
  headers["x-goog-api-key"] = apiKey;
1035
1084
 
1036
- const compiled = compileGoogleWireBody(body);
1085
+ const compiled = compileGoogleWireBody(body, toolSchemaProfile, toolSchemaPolicy);
1086
+ reportToolSchemaLoss(compiled.toolSchemaLossReport);
1037
1087
  restoreGoogleToolName = compiled.restoreToolName;
1038
1088
  return { url, method: "POST", headers, body: JSON.stringify(compiled.body) };
1039
1089
  },
@@ -1,4 +1,5 @@
1
1
  import { diagnoseInvalidToolCalls, isRecord, type InvalidToolCallDiagnostic } from "./tool-call-validation";
2
+ import { TranslatorBudgetExceededError, type TranslatorBudget } from "../../lib/translator-budget";
2
3
  import type { AdapterEvent, OcxUsage } from "../../types";
3
4
 
4
5
  export function stopReasonFor(finishReason: unknown): "max_tokens" | "content_filter" | undefined {
@@ -22,6 +23,9 @@ export interface ReasoningDetailSegment {
22
23
  text: string;
23
24
  }
24
25
 
26
+ const MAX_REASONING_DETAIL_KEY_BYTES = 1024;
27
+ const MAX_REASONING_DETAIL_SEGMENTS = 1024;
28
+
25
29
  /**
26
30
  * Structured `reasoning_details` array (MiniMax M-series with `reasoning_split`).
27
31
  * Each segment's key scopes cumulative-snapshot tracking: upstream repeats the
@@ -35,6 +39,11 @@ export function reasoningDetailSegmentsFrom(record: Record<string, unknown>): Re
35
39
  const item: unknown = raw[i];
36
40
  if (!isRecord(item)) continue;
37
41
  if (typeof item.text !== "string" || item.text.length === 0) continue;
42
+ // Parsing retains nothing, so it rejects nothing. The non-streaming
43
+ // parseResponse path shares this function and reads only `text`; failing a
44
+ // whole valid response there because an opaque upstream id is long would be
45
+ // a new rejection unrelated to the retention bound. The key cap lives with
46
+ // the map that holds the key; see the tracker below.
38
47
  const key = typeof item.id === "string" && item.id.length > 0
39
48
  ? `id:${item.id}`
40
49
  : typeof item.index === "number"
@@ -45,6 +54,58 @@ export function reasoningDetailSegmentsFrom(record: Record<string, unknown>): Re
45
54
  return segments;
46
55
  }
47
56
 
57
+ /**
58
+ * Per-stream cumulative-snapshot store for structured `reasoning_details`. Each stream chunk
59
+ * repeats a detail's full text-so-far, so deltas are derived by prefix-diffing per segment key;
60
+ * a piece that does not extend the previous snapshot is appended whole, which keeps incremental
61
+ * senders parseable on the same path. Retained key+text bytes are charged to the translator
62
+ * budget under the `reasoning` kind, and both the key length and the segment count are capped
63
+ * here, where the map actually retains them, so a hostile upstream cannot grow it without bound.
64
+ */
65
+ export function createReasoningDetailSnapshotTracker(budget: TranslatorBudget): {
66
+ ingest(segment: ReasoningDetailSegment): string | null;
67
+ release(): void;
68
+ } {
69
+ const snapshots = new Map<string, string>();
70
+ const encoder = new TextEncoder();
71
+ let retainedBytes = 0;
72
+ return {
73
+ ingest(segment) {
74
+ const existing = snapshots.get(segment.key);
75
+ if (existing === undefined && encoder.encode(segment.key).byteLength > MAX_REASONING_DETAIL_KEY_BYTES) {
76
+ throw new TranslatorBudgetExceededError("reasoning", MAX_REASONING_DETAIL_KEY_BYTES);
77
+ }
78
+ if (existing === undefined && snapshots.size >= MAX_REASONING_DETAIL_SEGMENTS) {
79
+ throw new TranslatorBudgetExceededError("reasoning", MAX_REASONING_DETAIL_SEGMENTS);
80
+ }
81
+ const prev = existing ?? "";
82
+ if (segment.text === prev) return null;
83
+ const extendsPrev = segment.text.startsWith(prev);
84
+ const next = extendsPrev ? segment.text : prev + segment.text;
85
+ const previousBytes = existing === undefined
86
+ ? 0
87
+ : encoder.encode(segment.key).byteLength + encoder.encode(prev).byteLength;
88
+ const nextBytes = encoder.encode(segment.key).byteLength + encoder.encode(next).byteLength;
89
+ const reservation = budget.reserveTransient(nextBytes, { kind: "reasoning" });
90
+ try {
91
+ snapshots.set(segment.key, next);
92
+ reservation.commitRetained();
93
+ budget.releaseRetained(previousBytes, { kind: "reasoning" });
94
+ retainedBytes += nextBytes - previousBytes;
95
+ } catch (error) {
96
+ reservation.release();
97
+ throw error;
98
+ }
99
+ return extendsPrev ? segment.text.slice(prev.length) : segment.text;
100
+ },
101
+ release() {
102
+ budget.releaseRetained(retainedBytes, { kind: "reasoning" });
103
+ retainedBytes = 0;
104
+ snapshots.clear();
105
+ },
106
+ };
107
+ }
108
+
48
109
  /** Single-segment `reasoning_details` entry for replaying preserved reasoning (MiniMax wire shape). */
49
110
  export function reasoningDetailSegmentForWire(text: string): Record<string, unknown> {
50
111
  return { type: "reasoning.text", id: "reasoning-text-1", format: "MiniMax-response-v1", index: 0, text };
@@ -13,7 +13,9 @@ import {
13
13
  export const OPENAI_CHAT_IMAGE_BASE64_BUDGET = 3_670_016; // 3.5MiB
14
14
 
15
15
  export interface NormalizeOpenAIChatImagesOptions
16
- extends Pick<NormalizeOptions, "encode" | "tierBias" | "validate"> {}
16
+ extends Pick<NormalizeOptions, "encode" | "tierBias" | "validate"> {
17
+ abortSignal?: AbortSignal;
18
+ }
17
19
 
18
20
  /** Whether `value` is a plain object, so message and part shapes can be walked safely. */
19
21
  function isRecord(value: unknown): value is Record<string, unknown> {
@@ -23,6 +23,7 @@ import {
23
23
  type InvalidToolCallDiagnostic,
24
24
  } from "./openai-chat/tool-call-validation";
25
25
  import {
26
+ createReasoningDetailSnapshotTracker,
26
27
  invalidChoicesEvent,
27
28
  invalidToolCallsEvent,
28
29
  reasoningDetailSegmentsFrom,
@@ -296,7 +297,11 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
296
297
  };
297
298
  };
298
299
  if (hasShrinkableOpenAIChatImages(messages)) {
299
- return normalizeOpenAIChatImages(messages, { tierBias: incoming?.imageTierBias }).then(finish, finish);
300
+ const imageOptions = { tierBias: incoming?.imageTierBias, abortSignal: incoming?.abortSignal };
301
+ return normalizeOpenAIChatImages(messages, imageOptions).then(finish, error => {
302
+ if (incoming?.abortSignal?.aborted) throw error;
303
+ return finish();
304
+ });
300
305
  }
301
306
  return finish();
302
307
  },
@@ -385,7 +390,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
385
390
  // full text-so-far, so deltas are derived by prefix-diffing per segment key.
386
391
  // A piece that does not extend the previous snapshot is appended whole, which
387
392
  // keeps incremental senders parseable on the same path.
388
- const reasoningDetailSnapshots = new Map<string, string>();
393
+ const reasoningDetailTracker = createReasoningDetailSnapshotTracker(budget);
389
394
  // Gate on the routed model, not list length: a mixed openai-chat provider
390
395
  // can list MiniMax ids without putting every sibling on MiniMax semantics.
391
396
  const reasoningDetailsOptIn = modelInList(provider.reasoningDetailsModels, lastRequestedModelId ?? "");
@@ -448,15 +453,8 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
448
453
  const detailSegments = reasoningDetailsOptIn ? reasoningDetailSegmentsFrom(delta) : [];
449
454
  if (detailSegments.length > 0) {
450
455
  for (const segment of detailSegments) {
451
- const prev = reasoningDetailSnapshots.get(segment.key) ?? "";
452
- if (segment.text === prev) continue;
453
- if (segment.text.startsWith(prev)) {
454
- reasoningDetailSnapshots.set(segment.key, segment.text);
455
- yield { type: "reasoning_raw_delta", text: segment.text.slice(prev.length) };
456
- } else {
457
- reasoningDetailSnapshots.set(segment.key, prev + segment.text);
458
- yield { type: "reasoning_raw_delta", text: segment.text };
459
- }
456
+ const reasoningDelta = reasoningDetailTracker.ingest(segment);
457
+ if (reasoningDelta !== null) yield { type: "reasoning_raw_delta", text: reasoningDelta };
460
458
  }
461
459
  } else {
462
460
  const reasoningText = reasoningTextFrom(delta);
@@ -692,6 +690,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
692
690
  throw error;
693
691
  } finally {
694
692
  budget.releaseRetained(bufferBytes, { kind: "live_transient" });
693
+ reasoningDetailTracker.release();
695
694
  closeToolCalls();
696
695
  reader.releaseLock();
697
696
  }
@@ -101,14 +101,17 @@ export function preferConfiguredHostedTools(
101
101
  }
102
102
 
103
103
  let input = body.input;
104
- const strippedAdditionalToolsIndices = new Set<number>();
104
+ // Indices arrive in increasing order, so the first stripped container is already
105
+ // the minimum — tracking it directly avoids spreading an attacker-sized Set into
106
+ // Math.min's argument list.
107
+ let firstStrippedAdditionalToolsIndex: number | undefined;
105
108
  if (Array.isArray(body.input)) {
106
109
  let nestedChanged = false;
107
110
  const mappedInput = body.input.map((item, index) => {
108
111
  if (!isPlainObject(item) || item.type !== "additional_tools" || !Array.isArray(item.tools)) return item;
109
112
  const nestedTools = stripGroup(item.tools);
110
113
  if (nestedTools === item.tools) return item;
111
- strippedAdditionalToolsIndices.add(index);
114
+ firstStrippedAdditionalToolsIndex ??= index;
112
115
  nestedChanged = true;
113
116
  return { ...item, tools: nestedTools };
114
117
  });
@@ -127,17 +130,16 @@ export function preferConfiguredHostedTools(
127
130
  || (Array.isArray(input) && input.some(item => isPlainObject(item)
128
131
  && item.type === "additional_tools"
129
132
  && hasHostedImageGenTool(item.tools)));
130
- if ((strippedTopLevelImageGenTool || strippedAdditionalToolsIndices.size > 0) && !hasHostedImageGenDeclaration) {
133
+ if ((strippedTopLevelImageGenTool || firstStrippedAdditionalToolsIndex !== undefined) && !hasHostedImageGenDeclaration) {
131
134
  if (strippedTopLevelImageGenTool && Array.isArray(tools)) {
132
135
  tools = [...tools, { type: HOSTED_IMAGE_GENERATION_TOOL }];
133
- } else if (strippedAdditionalToolsIndices.size > 0 && Array.isArray(input)) {
136
+ } else if (firstStrippedAdditionalToolsIndex !== undefined && Array.isArray(input)) {
134
137
  // Restore into the FIRST stripped container only. Tool declarations are
135
138
  // request-scoped, not container-scoped — the containers are separate carriers for
136
139
  // one tool set, so a single hosted declaration covers the request. An earlier
137
140
  // revision restored into every stripped container and put `image_generation` on
138
141
  // the wire twice; review caught it.
139
- const firstStripped = Math.min(...strippedAdditionalToolsIndices);
140
- input = input.map((item, index) => index === firstStripped
142
+ input = input.map((item, index) => index === firstStrippedAdditionalToolsIndex
141
143
  && isPlainObject(item)
142
144
  && Array.isArray(item.tools)
143
145
  ? { ...item, tools: [...item.tools, { type: HOSTED_IMAGE_GENERATION_TOOL }] }
@@ -18,6 +18,7 @@ import type { TranslatorBudget } from "../../lib/translator-budget";
18
18
  import { rewriteRoutedCustomToolsForUpstream } from "../../responses/custom-tool-compat";
19
19
  import { rewriteRoutedToolSearchForUpstream } from "../../responses/tool-search-compat";
20
20
  import { rewriteRoutedNamespaceToolsForUpstream } from "../../responses/namespace-tool-compat";
21
+ import { repairLegacyDottedToolCallNames } from "../../responses/legacy-dotted-tool-name-repair";
21
22
  import { preparePlaintextV2AgentMessages } from "../../responses/plaintext-v2-agent-messages";
22
23
  import { isMetaAiResponsesDestination, rewriteMuseToolNamesForUpstream } from "../../responses/muse-tool-name-alias";
23
24
  import { openaiResponsesUrl } from "../openai-responses-url";
@@ -31,12 +32,13 @@ import {
31
32
  import {
32
33
  createAdapterTierMetadata,
33
34
  } from "../../providers/fastwire";
34
- import { mapRoutedResponsesReasoningEffort, normalizeConfiguredReasoningSummaryDelivery, sanitizeReasoningInputContent, stripDisabledReasoningSummaries, stripDisabledVerbosity, stripUnsupportedReasoningSummaryDelivery } from "./reasoning";
35
+ import { dropResponsesReasoningInputItems, mapRoutedResponsesReasoningEffort, normalizeConfiguredReasoningSummaryDelivery, sanitizeReasoningInputContent, stripDisabledReasoningSummaries, stripDisabledVerbosity, stripUnsupportedReasoningSummaryDelivery } from "./reasoning";
35
36
  import { scrubOcxCompactionItems, stripCanonicalOnlyToolFields, stripCanonicalOnlyTopLevelFields, stripInternalChatMessageMetadataPassthrough, stripInvalidItemIds, stripItemIdsWhenUnstored } from "./request-strips";
36
37
  import { stripCanonicalForwardPromptCacheOptions, stripDeprecatedPromptCacheRetention } from "./prompt-cache";
37
38
  import { isPlainObject } from "./internal";
38
39
  import { normalizeToolSchemas, promoteClientLoadedTools, stripUnsupportedHostedTools } from "./tool-schema";
39
- import { annotateEmptyResponsesToolOutputs, backfillWebSearchQueries, normalizeResponsesToolResultAdjacency, repairOrphanedInputItems, repairOversizedReplayCallIds, repairUnidentifiedToolOutputItems } from "./tool-output-recovery";
40
+ import { annotateEmptyResponsesToolOutputs, backfillWebSearchQueries, normalizeResponsesToolResultAdjacency, repairOrphanedInputItems, repairOversizedReplayCallIds, repairUnidentifiedToolOutputItems, restoreBridgedWebSearchCalls } from "./tool-output-recovery";
41
+ import { bridgeSearchReplayScope } from "../../responses/bridge-search-replay-cache";
40
42
  import { applyTierDecisionToResponsesBody, normalizeCanonicalForwardContinuationEnvelope, normalizeCanonicalForwardPromptEnvelope, stripCanonicalForwardSamplingParams, stripPreviousResponseId, stripStatefulResponsesParams, stripUnsupportedForwardParams } from "./canonical-forward";
41
43
  import { normalizeImageGenClientTools, preferConfiguredHostedTools } from "./image-gen";
42
44
  import { stripMuseSparkUnsupportedWebSearchFields, stripOpenAiOnlyWebSearchFields } from "./web-search";
@@ -294,6 +296,9 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
294
296
  if (forward || stateless || pairedToolResults) {
295
297
  outBody = repairOrphanedInputItems(outBody, unexpandedMiss, synthesizeMissingCallOutputs);
296
298
  }
299
+ if (provider.dropResponsesReasoningItems === true) {
300
+ outBody = dropResponsesReasoningInputItems(outBody);
301
+ }
297
302
  if (adjacentToolResults) {
298
303
  outBody = normalizeResponsesToolResultAdjacency(outBody);
299
304
  }
@@ -321,11 +326,25 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
321
326
  outBody = repairOversizedReplayCallIds(outBody);
322
327
  }
323
328
  outBody = stripUnsupportedReasoningSummaryDelivery(outBody, parsed.modelId);
329
+ // #4587: on a bridged provider, hand the destination back the search call and result the
330
+ // proxy executed on its behalf, in place of the hosted cell the caller replays. Scoped to
331
+ // this destination and recorded by the bridge itself, so a provider without the opt-in
332
+ // computes no identity and keeps the body reference it already had. This runs before the
333
+ // query backfill below because a restored cell is no longer a web_search_call to repair.
334
+ if (provider.webSearchBridge?.enabled === true) {
335
+ outBody = restoreBridgedWebSearchCalls(outBody, bridgeSearchReplayScope(provider.baseUrl));
336
+ }
324
337
  // Repair stored history from before the bridge emitted both keys, in either
325
338
  // direction: a conversation that already recorded a web_search_call replays it
326
339
  // every turn, and a strict parser rejects the whole request over the missing key —
327
340
  // `queries` for DeepSeek (#930), `query` for Console Go (#3071).
328
341
  outBody = backfillWebSearchQueries(outBody);
342
+ // #5095: a conversation that already contains a `default.`-prefixed call name is refused by
343
+ // the upstream `^[a-zA-Z0-9_-]+$` name pattern on every later turn that replays it, so the
344
+ // task cannot be compacted or continued at all. Repair the replayed item here, before the
345
+ // canonical-destination split below, because the reported failure was a side chat on a plain
346
+ // OpenAI model inheriting history a routed provider had damaged.
347
+ outBody = repairLegacyDottedToolCallNames(outBody);
329
348
  if (!isCanonicalOpenAiForwardProvider(provider)) {
330
349
  outBody = stripInternalChatMessageMetadataPassthrough(outBody);
331
350
  // The same class of private field, one level up, but keyed on the DESTINATION rather than
@@ -388,14 +407,16 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
388
407
  // Last, so promoted namespace children are also cleared of Codex-private fields.
389
408
  outBody = stripCanonicalOnlyToolFields(outBody, provider.supportsOpenAiWebSearchToolFields === false);
390
409
  }
391
- if (!forward) outBody = normalizeOpenCodeGoAdditionalTools(outBody, url);
410
+ if (!forward) {
411
+ outBody = normalizeOpenCodeGoAdditionalTools(outBody, url, parsed._replayPrefixLen);
412
+ }
392
413
  // Same predicate as the routedCompaction gate in handleResponses(): an authMode check would
393
414
  // let a noncanonical custom forward provider skip this rewrite while the server still routes
394
415
  // it as a summarizer turn (#422). The compaction body build removes the tool surface and must
395
416
  // therefore be the last routed transform that may depend on those declarations. Structural
396
417
  // sanitizers below can still run after it.
397
418
  outBody = normalizeResponsesCodeMode(outBody, parsed, provider);
398
- if (parsed._compactionRequest === true && !isCanonicalOpenAiForwardProvider(provider)) {
419
+ if (parsed._compactionRequest === true && (!isCanonicalOpenAiForwardProvider(provider) || parsed._portableCompaction === true)) {
399
420
  outBody = buildRoutedCompactionBody(outBody);
400
421
  }
401
422
  // Run after routed compaction so nested input_image parts are replaced before a malformed
@@ -4,6 +4,13 @@ import { OCX_REASONING_PREFIX } from "../../responses/reasoning-envelope";
4
4
  import { configuredReasoningEfforts, mapReasoningEffort, modelRecordValue } from "../../reasoning-effort";
5
5
  import { isPlainObject } from "./internal";
6
6
 
7
+ /** Drop only replayed Responses reasoning items; all other continuation input stays untouched. */
8
+ export function dropResponsesReasoningInputItems(body: unknown): unknown {
9
+ if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
10
+ const input = body.input.filter(item => !isPlainObject(item) || item.type !== "reasoning");
11
+ return input.length === body.input.length ? body : { ...body, input };
12
+ }
13
+
7
14
  /**
8
15
  * Sanitize reasoning input by field policy, not by preserving each item's shape. Retaining a
9
16
  * native `encrypted_content` guarantees only that blob value: `status` is always removed;
@@ -1,6 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { EMPTY_TOOL_OUTPUT_ANNOTATION, isWhitespaceOnlyTextPartArray } from "../empty-tool-output-annotation";
3
3
  import { isPlainObject } from "./internal";
4
+ import { peekBridgeSearchReplay } from "../../responses/bridge-search-replay-cache";
4
5
 
5
6
  const MAX_RESPONSES_CALL_ID_LENGTH = 64;
6
7
 
@@ -265,6 +266,80 @@ export function backfillWebSearchQueries(body: unknown): unknown {
265
266
  return changed ? { ...body, input } : body;
266
267
  }
267
268
 
269
+ /**
270
+ * Give a bridged destination back its own search call and result (issue #4587).
271
+ *
272
+ * When `providers.<name>.webSearchBridge` is armed, the proxy intercepts the destination's
273
+ * `function_call` named `web_search`, runs the search, and shows the CALLER a hosted
274
+ * `web_search_call` cell. The caller stores that cell and replays it on every later turn, so the
275
+ * destination receives an item type it never produced, carrying a query and sources but no result.
276
+ * It typically responds by searching again.
277
+ *
278
+ * This restores the exchange the destination actually had: the cell becomes the destination's own
279
+ * `function_call`, immediately followed by the `function_call_output` the bridge produced for
280
+ * it, in the cell's original position. It runs before the first leg of the next turn is
281
+ * dispatched, which is the only place it can run — by the time the bridge wraps a turn, that
282
+ * turn's first leg is already on the wire.
283
+ *
284
+ * Three things it deliberately does not do:
285
+ * - It never re-runs a search. A missing memo entry means the result is gone, and paying for a
286
+ * second search would answer the model with a different search than its history claims.
287
+ * - It never invents result text. A miss leaves the item exactly as the caller sent it, which is
288
+ * the behaviour every unbridged conversation already has.
289
+ * - It never restores a call id the body already carries. If the history somehow holds that
290
+ * `function_call` too, emitting a second one would be a duplicate the upstream must reject.
291
+ *
292
+ * Entries are scoped to the upstream destination, so a history replayed against a different
293
+ * provider cannot resurrect a call that provider never made. Callers pass `undefined` for any
294
+ * provider without the bridge armed, and the common path then returns the original reference.
295
+ */
296
+ export function restoreBridgedWebSearchCalls(body: unknown, destinationScope: string | undefined): unknown {
297
+ if (destinationScope === undefined) return body;
298
+ if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
299
+ const input = body.input;
300
+
301
+ // Cheap pre-check: nothing to do for a conversation that carries no hosted search cell at all,
302
+ // which is every turn before the model's first bridged search.
303
+ let hasCell = false;
304
+ for (const item of input) {
305
+ if (isPlainObject(item) && item.type === "web_search_call" && typeof item.id === "string") {
306
+ hasCell = true;
307
+ break;
308
+ }
309
+ }
310
+ if (!hasCell) return body;
311
+
312
+ const occupiedCallIds = new Set<string>();
313
+ for (const item of input) {
314
+ if (isPlainObject(item) && typeof item.call_id === "string") occupiedCallIds.add(item.call_id);
315
+ }
316
+
317
+ let changed = false;
318
+ const restored: unknown[] = [];
319
+ for (const item of input) {
320
+ if (isPlainObject(item) && item.type === "web_search_call" && typeof item.id === "string") {
321
+ const memo = peekBridgeSearchReplay(destinationScope, item.id);
322
+ if (memo && !occupiedCallIds.has(memo.callId)) {
323
+ changed = true;
324
+ occupiedCallIds.add(memo.callId);
325
+ restored.push({
326
+ type: "function_call",
327
+ ...(memo.sourceItemId ? { id: memo.sourceItemId } : {}),
328
+ call_id: memo.callId,
329
+ name: memo.name,
330
+ // The bridge records the complete arguments text from the call's own done frame; the
331
+ // empty-object fallback matches what a continuation leg would have sent.
332
+ arguments: memo.argumentsText.length > 0 ? memo.argumentsText : "{}",
333
+ });
334
+ restored.push({ type: "function_call_output", call_id: memo.callId, output: memo.output });
335
+ continue;
336
+ }
337
+ }
338
+ restored.push(item);
339
+ }
340
+ return changed ? { ...body, input: restored } : body;
341
+ }
342
+
268
343
  export function repairOrphanedInputItems(body: unknown, dropReasoning: boolean, synthesizeMissingCallOutputs = false): unknown {
269
344
  if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
270
345
  const input = body.input;
@@ -1,5 +1,5 @@
1
1
  import { namespacedToolName, type AdapterEvent, type OcxParsedRequest, type OcxProviderConfig, type OcxUsage, type TierDecision } from "../../types";
2
- import { isHostedToolUnsupportedForModel } from "../../responses/hosted-tool-policy";
2
+ import { declaredUnsupportedHostedTools, isHostedToolUnsupportedForModel } from "../../responses/hosted-tool-policy";
3
3
  import { debugProviderDiagnostic } from "../../lib/debug";
4
4
  import { stripUnicodePropertyPatterns } from "../responses-tool-schema";
5
5
  import {
@@ -225,17 +225,29 @@ export function promoteClientLoadedTools(body: unknown): unknown {
225
225
  }
226
226
 
227
227
  /**
228
- * Remove hosted tool entries the target native slug rejects, so the OAuth-passthrough body never
229
- * carries a tool the upstream model 400s on. No-op (returns the original reference) when nothing
230
- * matches, keeping the common path allocation-free.
228
+ * Remove hosted tool entries the destination rejects, so the OAuth-passthrough body never
229
+ * carries a tool the upstream 400s on. Two sources of truth are consulted: the built-in
230
+ * table of known-broken native slugs and destinations, and the routed provider's own
231
+ * `unsupportedHostedTools` declaration. The declaration is what lets an OpenAI-compatible
232
+ * Responses gateway with a narrower capability set be described in config instead of
233
+ * requiring a hard-coded destination rule per vendor (#5002).
234
+ *
235
+ * No-op (returns the original reference) when nothing matches, keeping the common path
236
+ * allocation-free.
231
237
  */
232
- export function stripUnsupportedHostedTools(body: unknown, provider: Pick<OcxProviderConfig, "baseUrl">): unknown {
238
+ export function stripUnsupportedHostedTools(
239
+ body: unknown,
240
+ provider: Pick<OcxProviderConfig, "baseUrl" | "unsupportedHostedTools">,
241
+ ): unknown {
233
242
  if (!isPlainObject(body)) return body;
234
243
  const model = typeof body.model === "string" ? body.model : "";
244
+ // Expanded once per request rather than per tool: the alias walk is the only
245
+ // non-lookup work in this filter.
246
+ const declaredUnsupported = declaredUnsupportedHostedTools(provider);
235
247
  const filterTools = (tools: unknown[]): unknown[] => {
236
248
  const filtered = tools.filter(t => {
237
249
  const type = isPlainObject(t) && typeof t.type === "string" ? t.type : undefined;
238
- return !type || !isHostedToolUnsupportedForModel(model, type, provider.baseUrl);
250
+ return !type || !isHostedToolUnsupportedForModel(model, type, provider.baseUrl, declaredUnsupported);
239
251
  });
240
252
  return filtered.length === tools.length ? tools : filtered;
241
253
  };
@@ -274,7 +286,7 @@ export function stripUnsupportedHostedTools(body: unknown, provider: Pick<OcxPro
274
286
  } else if (
275
287
  isPlainObject(toolChoice)
276
288
  && typeof toolChoice.type === "string"
277
- && isHostedToolUnsupportedForModel(model, toolChoice.type, provider.baseUrl)
289
+ && isHostedToolUnsupportedForModel(model, toolChoice.type, provider.baseUrl, declaredUnsupported)
278
290
  ) {
279
291
  next = { ...next, tool_choice: "none" };
280
292
  changed = true;
@@ -3,7 +3,11 @@ function isRecord(value: unknown): value is Record<string, unknown> {
3
3
  }
4
4
 
5
5
  /** Console Go accepts public tools but rejects the private additional_tools input wrapper. */
6
- export function normalizeOpenCodeGoAdditionalTools(body: unknown, responseUrl: string): unknown {
6
+ export function normalizeOpenCodeGoAdditionalTools(
7
+ body: unknown,
8
+ responseUrl: string,
9
+ replayPrefixLength = 0,
10
+ ): unknown {
7
11
  let destination: URL;
8
12
  try {
9
13
  destination = new URL(responseUrl);
@@ -20,10 +24,16 @@ export function normalizeOpenCodeGoAdditionalTools(body: unknown, responseUrl: s
20
24
 
21
25
  const input: unknown[] = [];
22
26
  const promoted: unknown[] = [];
27
+ const currentTurnStart = Number.isFinite(replayPrefixLength)
28
+ ? Math.min(body.input.length, Math.max(0, Math.trunc(replayPrefixLength)))
29
+ : 0;
23
30
  let changed = false;
24
- for (const item of body.input) {
31
+ for (const [index, item] of body.input.entries()) {
25
32
  if (isRecord(item) && item.type === "additional_tools" && Array.isArray(item.tools)) {
26
33
  changed = true;
34
+ // Replayed wrappers are conversation history, not authority for the current request.
35
+ // Go cannot accept the wrapper itself, so remove it without promoting its catalog.
36
+ if (index < currentTurnStart) continue;
27
37
  // Custom/search/namespace lowering already owns identity and deduplication. This pass
28
38
  // only moves declarations, including hosted tools that intentionally have no name.
29
39
  for (const tool of item.tools) promoted.push(tool);