@iislee/opencodex 2.11.0 → 2.35.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 (733) hide show
  1. package/AGENTS_INSTALL.md +109 -0
  2. package/README.md +117 -22
  3. package/bin/ocx.mjs +245 -88
  4. package/bin/package-main.mjs +1 -1
  5. package/gui/dist/assets/index-Be1sDpWw.css +1 -0
  6. package/gui/dist/assets/index-D4OKgUV4.js +114 -0
  7. package/gui/dist/index.html +2 -2
  8. package/gui/dist/provider-icons/alibaba-color.svg +1 -1
  9. package/gui/dist/provider-icons/antigravity-color.svg +1 -1
  10. package/gui/dist/provider-icons/claude-color.svg +1 -1
  11. package/gui/dist/provider-icons/cline-color.svg +16 -0
  12. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
  13. package/gui/dist/provider-icons/commandcode-color.svg +1 -0
  14. package/gui/dist/provider-icons/copilot-color.svg +1 -1
  15. package/gui/dist/provider-icons/cursor-color.svg +1 -1
  16. package/gui/dist/provider-icons/deepseek-color.svg +1 -1
  17. package/gui/dist/provider-icons/firepass-color.svg +1 -1
  18. package/gui/dist/provider-icons/fireworks-color.svg +1 -1
  19. package/gui/dist/provider-icons/gemini-color.svg +1 -1
  20. package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
  21. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
  22. package/gui/dist/provider-icons/grok.svg +1 -1
  23. package/gui/dist/provider-icons/groq-color.svg +1 -1
  24. package/gui/dist/provider-icons/huggingface-color.svg +1 -1
  25. package/gui/dist/provider-icons/kimi-color.svg +1 -1
  26. package/gui/dist/provider-icons/kiro-color.svg +2 -2
  27. package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
  28. package/gui/dist/provider-icons/mistral-color.svg +1 -1
  29. package/gui/dist/provider-icons/moonshot-color.svg +1 -1
  30. package/gui/dist/provider-icons/nvidia-color.svg +1 -1
  31. package/gui/dist/provider-icons/ollama-color.svg +1 -1
  32. package/gui/dist/provider-icons/openai.svg +1 -1
  33. package/gui/dist/provider-icons/opencode.svg +2 -1
  34. package/gui/dist/provider-icons/openrouter-color.svg +1 -1
  35. package/gui/dist/provider-icons/pi.svg +2 -2
  36. package/gui/dist/provider-icons/qianfan-color.svg +1 -1
  37. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
  38. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
  39. package/gui/dist/provider-icons/vllm-color.svg +1 -1
  40. package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
  41. package/package.json +21 -10
  42. package/src/adapters/anthropic-output-schema.ts +137 -0
  43. package/src/adapters/anthropic.ts +475 -62
  44. package/src/adapters/base.ts +82 -7
  45. package/src/adapters/client-fingerprint.ts +18 -12
  46. package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
  47. package/src/adapters/command-code.ts +637 -0
  48. package/src/adapters/cursor/call-id.ts +44 -0
  49. package/src/adapters/cursor/catalog.ts +541 -0
  50. package/src/adapters/cursor/checkpoint-store.ts +308 -0
  51. package/src/adapters/cursor/cursor-errors.ts +144 -6
  52. package/src/adapters/cursor/discovery.ts +122 -14
  53. package/src/adapters/cursor/effort-map.ts +106 -4
  54. package/src/adapters/cursor/envelope-echo.ts +290 -0
  55. package/src/adapters/cursor/framing.ts +39 -0
  56. package/src/adapters/cursor/h2-pool.ts +123 -0
  57. package/src/adapters/cursor/http1-bidi.ts +361 -0
  58. package/src/adapters/cursor/images.ts +704 -0
  59. package/src/adapters/cursor/live-models.ts +180 -59
  60. package/src/adapters/cursor/live-transport.ts +623 -170
  61. package/src/adapters/cursor/message-mapper.ts +4 -1
  62. package/src/adapters/cursor/native-exec-common.ts +23 -2
  63. package/src/adapters/cursor/native-exec-desktop.ts +23 -0
  64. package/src/adapters/cursor/native-exec-fs.ts +10 -7
  65. package/src/adapters/cursor/native-exec-network.ts +1 -1
  66. package/src/adapters/cursor/native-exec-shell.ts +5 -3
  67. package/src/adapters/cursor/native-exec.ts +121 -14
  68. package/src/adapters/cursor/protobuf-events.ts +829 -11
  69. package/src/adapters/cursor/protobuf-request.ts +447 -75
  70. package/src/adapters/cursor/request-builder.ts +271 -35
  71. package/src/adapters/cursor/tool-definitions.ts +252 -12
  72. package/src/adapters/cursor/tool-result-normalize.ts +115 -0
  73. package/src/adapters/cursor/transport.ts +22 -0
  74. package/src/adapters/cursor/types.ts +48 -1
  75. package/src/adapters/cursor.ts +336 -39
  76. package/src/adapters/exec-tool-result-normalize.ts +99 -0
  77. package/src/adapters/google-antigravity-replay.ts +713 -41
  78. package/src/adapters/google-antigravity-wire.ts +48 -10
  79. package/src/adapters/google-errors.ts +44 -12
  80. package/src/adapters/google-http.ts +38 -10
  81. package/src/adapters/google-tool-schema.ts +80 -15
  82. package/src/adapters/google-truncation.ts +11 -0
  83. package/src/adapters/google.ts +742 -86
  84. package/src/adapters/identity.ts +39 -6
  85. package/src/adapters/image.ts +1 -1
  86. package/src/adapters/kiro-constants.ts +27 -0
  87. package/src/adapters/kiro-errors.ts +11 -0
  88. package/src/adapters/kiro-events.ts +19 -1
  89. package/src/adapters/kiro-thinking.ts +18 -2
  90. package/src/adapters/kiro-tools.ts +65 -17
  91. package/src/adapters/kiro.ts +270 -73
  92. package/src/adapters/mimo-free.ts +17 -0
  93. package/src/adapters/openai-chat-url.ts +11 -0
  94. package/src/adapters/openai-chat.ts +1377 -344
  95. package/src/adapters/openai-responses-url.ts +16 -0
  96. package/src/adapters/openai-responses.ts +1164 -64
  97. package/src/adapters/registry.ts +175 -0
  98. package/src/adapters/responses-tool-schema.ts +67 -0
  99. package/src/adapters/run-turn-queue.ts +36 -1
  100. package/src/adapters/tool-call-id.ts +119 -0
  101. package/src/adapters/tool-catalog-nudge.ts +105 -21
  102. package/src/adapters/xai-tool-schema.ts +436 -0
  103. package/src/adapters/xai-web-search.ts +186 -0
  104. package/src/bridge.ts +512 -92
  105. package/src/chat/inbound.ts +57 -20
  106. package/src/chat/outbound.ts +97 -33
  107. package/src/claude/agents-inject.ts +40 -10
  108. package/src/claude/context-windows.ts +37 -14
  109. package/src/claude/desktop-3p.ts +243 -9
  110. package/src/claude/gateway-cache.ts +41 -4
  111. package/src/claude/inbound.ts +72 -3
  112. package/src/claude/model-info.ts +38 -15
  113. package/src/claude/outbound.ts +88 -21
  114. package/src/cli/access.ts +46 -3
  115. package/src/cli/account-api.ts +150 -19
  116. package/src/cli/account-auth.ts +33 -6
  117. package/src/cli/account-catalog-refresh.ts +14 -0
  118. package/src/cli/account-extended.ts +648 -28
  119. package/src/cli/account-main.ts +317 -0
  120. package/src/cli/account.ts +97 -19
  121. package/src/cli/agent.ts +53 -2
  122. package/src/cli/alias.ts +66 -0
  123. package/src/cli/capabilities-command.ts +94 -0
  124. package/src/cli/capabilities.ts +496 -0
  125. package/src/cli/claude-agent-startup-sync.ts +73 -0
  126. package/src/cli/claude-desktop.ts +60 -15
  127. package/src/cli/claude.ts +129 -35
  128. package/src/cli/codex-log-guard-doctor.ts +103 -0
  129. package/src/cli/codex-shim-autorestore.ts +2 -0
  130. package/src/cli/codex-shim-readiness.ts +76 -0
  131. package/src/cli/combo.ts +8 -0
  132. package/src/cli/config-command.ts +74 -10
  133. package/src/cli/dispatch.ts +821 -0
  134. package/src/cli/doctor.ts +440 -56
  135. package/src/cli/ensure-desired-integrations.ts +152 -0
  136. package/src/cli/export-command.ts +46 -20
  137. package/src/cli/help.ts +34 -274
  138. package/src/cli/index.ts +417 -528
  139. package/src/cli/init.ts +5 -17
  140. package/src/cli/inspect.ts +230 -0
  141. package/src/cli/integrations.ts +120 -2
  142. package/src/cli/lab.ts +607 -0
  143. package/src/cli/launcher-context.ts +77 -0
  144. package/src/cli/minimax.ts +497 -0
  145. package/src/cli/models-runtime.ts +130 -2
  146. package/src/cli/models.ts +118 -16
  147. package/src/cli/observe.ts +123 -11
  148. package/src/cli/opencode.ts +4 -2
  149. package/src/cli/provider-runtime.ts +34 -3
  150. package/src/cli/provider.ts +25 -3
  151. package/src/cli/ready.ts +301 -0
  152. package/src/cli/registry.ts +476 -0
  153. package/src/cli/root.ts +86 -0
  154. package/src/cli/route-policy.ts +92 -0
  155. package/src/cli/runtime-api.ts +57 -10
  156. package/src/cli/star-prompt.ts +71 -15
  157. package/src/cli/status.ts +51 -4
  158. package/src/cli/storage.ts +234 -0
  159. package/src/cli/system-command.ts +16 -0
  160. package/src/cli/system-restart-client.ts +146 -0
  161. package/src/cli/tray-proxy.ts +153 -6
  162. package/src/cli/usage-report.ts +184 -0
  163. package/src/cli/v2.ts +137 -18
  164. package/src/cli/version-skew.ts +46 -0
  165. package/src/cli.ts +1 -1
  166. package/src/clients/config-export.ts +1442 -23
  167. package/src/clients/effective-status.ts +30 -2
  168. package/src/clients/probes/cline.ts +81 -0
  169. package/src/codex/account-label.ts +35 -1
  170. package/src/codex/account-lifecycle.ts +130 -13
  171. package/src/codex/account-namespaces.ts +49 -3
  172. package/src/codex/account-pause.ts +2 -1
  173. package/src/codex/account-priority.ts +84 -0
  174. package/src/codex/account-store.ts +29 -2
  175. package/src/codex/account-usability.ts +25 -2
  176. package/src/codex/admission.ts +256 -0
  177. package/src/codex/affinity-debug.ts +162 -0
  178. package/src/codex/app-server-processes.ts +570 -107
  179. package/src/codex/app-server-restart-service.ts +232 -0
  180. package/src/codex/auth-api.ts +893 -246
  181. package/src/codex/auth-collision.ts +5 -3
  182. package/src/codex/auth-context.ts +348 -32
  183. package/src/codex/autostart-health.ts +24 -1
  184. package/src/codex/catalog/account-models.ts +67 -0
  185. package/src/codex/catalog/aggregation.ts +86 -10
  186. package/src/codex/catalog/bundled.ts +331 -33
  187. package/src/codex/catalog/effort.ts +138 -32
  188. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  189. package/src/codex/catalog/kinds.ts +2 -0
  190. package/src/codex/catalog/metadata.ts +555 -45
  191. package/src/codex/catalog/native-models.ts +75 -0
  192. package/src/codex/catalog/parsing.ts +269 -30
  193. package/src/codex/catalog/provider-fetch.ts +1626 -144
  194. package/src/codex/catalog/sync.ts +1576 -187
  195. package/src/codex/catalog-admission.ts +199 -0
  196. package/src/codex/catalog-refresh-status.ts +105 -0
  197. package/src/codex/catalog-write-serialization.ts +242 -0
  198. package/src/codex/catalog.ts +6 -3
  199. package/src/codex/codex-write-lock.ts +386 -0
  200. package/src/codex/convergence-types.ts +614 -0
  201. package/src/codex/convergence.ts +676 -0
  202. package/src/codex/coordinator-doctor.ts +332 -0
  203. package/src/codex/custom-model-catalog-migration.ts +176 -0
  204. package/src/codex/desired-state.ts +230 -0
  205. package/src/codex/desktop-app-restart.ts +355 -0
  206. package/src/codex/features.ts +636 -39
  207. package/src/codex/generation.ts +202 -0
  208. package/src/codex/history-job.ts +436 -0
  209. package/src/codex/history-lock.ts +242 -0
  210. package/src/codex/history-manifest.ts +112 -0
  211. package/src/codex/history-migration-guardian.ts +30 -24
  212. package/src/codex/history-provider.ts +1016 -235
  213. package/src/codex/history-transition.ts +105 -0
  214. package/src/codex/history-worker.ts +223 -0
  215. package/src/codex/inject-coordination.ts +373 -0
  216. package/src/codex/inject.ts +1114 -152
  217. package/src/codex/injected-marker.ts +37 -3
  218. package/src/codex/integration-record.ts +266 -0
  219. package/src/codex/internal/catalog-writer.ts +203 -0
  220. package/src/codex/internal/history-writer.ts +81 -0
  221. package/src/codex/journal.ts +66 -4
  222. package/src/codex/log-guard/inspect.ts +524 -0
  223. package/src/codex/log-guard/lock.ts +150 -0
  224. package/src/codex/log-guard/maintenance.ts +403 -0
  225. package/src/codex/log-guard/path-safety.ts +88 -0
  226. package/src/codex/log-guard/policy.ts +44 -0
  227. package/src/codex/log-guard/processes.ts +205 -0
  228. package/src/codex/log-guard/protection.ts +489 -0
  229. package/src/codex/log-guard/sqlite-errors.ts +9 -0
  230. package/src/codex/main-account-cache.ts +24 -0
  231. package/src/codex/main-account.ts +29 -1
  232. package/src/codex/management-convergence.ts +167 -0
  233. package/src/codex/model-cache.ts +56 -10
  234. package/src/codex/model-entitlements.ts +360 -0
  235. package/src/codex/native-main-admission.ts +47 -0
  236. package/src/codex/native-main-auth-temp.ts +187 -0
  237. package/src/codex/native-main-claim.ts +178 -0
  238. package/src/codex/native-main-lock-file.ts +162 -0
  239. package/src/codex/native-main-owner.ts +329 -0
  240. package/src/codex/native-profile-api.ts +247 -0
  241. package/src/codex/native-profile-manager.ts +1531 -0
  242. package/src/codex/native-profile-processes.ts +121 -0
  243. package/src/codex/native-profile-recovery.ts +99 -0
  244. package/src/codex/native-profile-stage-store.ts +387 -0
  245. package/src/codex/native-profile-startup.ts +622 -0
  246. package/src/codex/native-profile-store.ts +855 -0
  247. package/src/codex/native-profile-types.ts +120 -0
  248. package/src/codex/native-residue.ts +675 -0
  249. package/src/codex/paths.ts +80 -1
  250. package/src/codex/plan-from-token.ts +140 -0
  251. package/src/codex/plan.ts +40 -0
  252. package/src/codex/plugins-doctor.ts +1 -1
  253. package/src/codex/pool-rotation.ts +74 -4
  254. package/src/codex/project-config-warnings.ts +20 -6
  255. package/src/codex/prompt-journal.ts +352 -0
  256. package/src/codex/prompt-layers.ts +1313 -0
  257. package/src/codex/prompt-lock.ts +143 -0
  258. package/src/codex/prompt-text-probe.ts +238 -0
  259. package/src/codex/quota-rejection.ts +298 -0
  260. package/src/codex/quota.ts +264 -21
  261. package/src/codex/refresh.ts +11 -2
  262. package/src/codex/reset-credit-recovery.ts +1044 -0
  263. package/src/codex/routing.ts +514 -94
  264. package/src/codex/runtime.ts +159 -38
  265. package/src/codex/shim.ts +1065 -31
  266. package/src/codex/subagent-model-fallback.ts +437 -43
  267. package/src/codex/sync.ts +191 -2
  268. package/src/codex/transition-state.ts +720 -0
  269. package/src/codex/upstream-host-health.ts +368 -0
  270. package/src/codex/user-identity.ts +557 -0
  271. package/src/codex/warmup.ts +187 -81
  272. package/src/codex/write-coordination.ts +114 -0
  273. package/src/combos/failover.ts +47 -0
  274. package/src/combos/index.ts +4 -0
  275. package/src/combos/request.ts +32 -0
  276. package/src/combos/types.ts +91 -9
  277. package/src/compatibility/index.ts +26 -0
  278. package/src/compatibility/manifest.ts +253 -0
  279. package/src/compatibility/openai-responses.ts +121 -0
  280. package/src/config/atomic-write.ts +219 -0
  281. package/src/config/paths.ts +40 -0
  282. package/src/config/process-state.ts +309 -0
  283. package/src/config/provider-name.ts +24 -0
  284. package/src/config/provider-validation.ts +177 -0
  285. package/src/config/rebase-provenance.ts +68 -0
  286. package/src/config.ts +1713 -709
  287. package/src/generated/compatibility-version.json +3324 -0
  288. package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +19 -17
  289. package/src/grok/inject.ts +16 -5
  290. package/src/grok/inspect.ts +45 -0
  291. package/src/grok/sync.ts +2 -2
  292. package/src/images/loop.ts +157 -33
  293. package/src/images/plan.ts +24 -13
  294. package/src/integrations/config-io.ts +269 -0
  295. package/src/integrations/journal.ts +315 -0
  296. package/src/integrations/merge.ts +135 -0
  297. package/src/integrations/mutation-flight.ts +71 -0
  298. package/src/integrations/native/ownership-preflight.ts +202 -0
  299. package/src/integrations/omp-yaml-source.ts +358 -0
  300. package/src/integrations/owned-refresh.ts +74 -0
  301. package/src/integrations/ownership-policy.ts +160 -0
  302. package/src/integrations/ownership.ts +155 -0
  303. package/src/integrations/registry.ts +166 -0
  304. package/src/integrations/serialize.ts +314 -0
  305. package/src/integrations/state.ts +433 -0
  306. package/src/integrations/store.ts +103 -0
  307. package/src/integrations/writer-lock.ts +98 -0
  308. package/src/integrations/writer.ts +715 -0
  309. package/src/lab/artifacts/sanitize.ts +586 -0
  310. package/src/lab/artifacts/secure-fs.ts +475 -0
  311. package/src/lab/artifacts/store.ts +310 -0
  312. package/src/lab/automation/budgets.ts +78 -0
  313. package/src/lab/automation/config-persistence.ts +256 -0
  314. package/src/lab/automation/constants.ts +39 -0
  315. package/src/lab/automation/cooldown.ts +103 -0
  316. package/src/lab/automation/dispatch.ts +211 -0
  317. package/src/lab/automation/index.ts +13 -0
  318. package/src/lab/automation/orchestrator.ts +499 -0
  319. package/src/lab/automation/persistence.ts +512 -0
  320. package/src/lab/automation/planner.ts +371 -0
  321. package/src/lab/automation/policy.ts +136 -0
  322. package/src/lab/automation/queue.ts +191 -0
  323. package/src/lab/automation/recovery.ts +24 -0
  324. package/src/lab/automation/route-context.ts +21 -0
  325. package/src/lab/automation/run-key.ts +44 -0
  326. package/src/lab/automation/runs-query.ts +34 -0
  327. package/src/lab/automation/types.ts +160 -0
  328. package/src/lab/conformance/assertion.ts +325 -0
  329. package/src/lab/conformance/digest.ts +22 -0
  330. package/src/lab/conformance/executor.ts +741 -0
  331. package/src/lab/conformance/fixture-provider.ts +27 -0
  332. package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
  333. package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
  334. package/src/lab/conformance/harness-budget.ts +47 -0
  335. package/src/lab/conformance/index.ts +5 -0
  336. package/src/lab/conformance/jcs.ts +64 -0
  337. package/src/lab/conformance/json-pointer.ts +39 -0
  338. package/src/lab/conformance/manifest.ts +180 -0
  339. package/src/lab/conformance/mcp-stub.ts +179 -0
  340. package/src/lab/conformance/negative-controls.ts +164 -0
  341. package/src/lab/conformance/observation.ts +355 -0
  342. package/src/lab/conformance/runner.ts +68 -0
  343. package/src/lab/conformance/sse-normalize.ts +59 -0
  344. package/src/lab/conformance/suite-manifest.ts +78 -0
  345. package/src/lab/conformance/types.ts +214 -0
  346. package/src/lab/constants.ts +126 -0
  347. package/src/lab/digest.ts +64 -0
  348. package/src/lab/events/errors.ts +9 -0
  349. package/src/lab/events/limits.ts +117 -0
  350. package/src/lab/events/types.ts +229 -0
  351. package/src/lab/events/validate.ts +781 -0
  352. package/src/lab/fabric/constants.ts +40 -0
  353. package/src/lab/fabric/executor.ts +492 -0
  354. package/src/lab/fabric/index.ts +80 -0
  355. package/src/lab/fabric/manifest.ts +222 -0
  356. package/src/lab/fabric/observe.ts +489 -0
  357. package/src/lab/fabric/patch.ts +79 -0
  358. package/src/lab/fabric/producer-child.ts +139 -0
  359. package/src/lab/fabric/producer-isolate.ts +276 -0
  360. package/src/lab/fabric/producer-protocol.ts +61 -0
  361. package/src/lab/fabric/scratch.ts +439 -0
  362. package/src/lab/fabric/subject.ts +106 -0
  363. package/src/lab/fabric/types.ts +134 -0
  364. package/src/lab/fabric/verifier.ts +98 -0
  365. package/src/lab/index.ts +54 -0
  366. package/src/lab/ledger/artifact-refs.ts +127 -0
  367. package/src/lab/ledger/invalidation.ts +136 -0
  368. package/src/lab/ledger/purge.ts +310 -0
  369. package/src/lab/ledger/store.ts +532 -0
  370. package/src/lab/live/credential-lease.ts +53 -0
  371. package/src/lab/live/destination.ts +155 -0
  372. package/src/lab/live/executor.ts +336 -0
  373. package/src/lab/live/inert-tools.ts +56 -0
  374. package/src/lab/live/manifest.ts +85 -0
  375. package/src/lab/live/mcp-loopback.ts +57 -0
  376. package/src/lab/live/runner.ts +19 -0
  377. package/src/lab/live/sandbox.ts +61 -0
  378. package/src/lab/live/suite-manifest.ts +41 -0
  379. package/src/lab/live/transport.ts +118 -0
  380. package/src/lab/live/types.ts +197 -0
  381. package/src/lab/observe/from-conformance.ts +301 -0
  382. package/src/lab/observe/from-live.ts +117 -0
  383. package/src/lab/paths.ts +153 -0
  384. package/src/lab/projection/rebuild.ts +495 -0
  385. package/src/lab/projection/schema.ts +135 -0
  386. package/src/lab/projection/verdicts.ts +474 -0
  387. package/src/lab/projection/verification.ts +412 -0
  388. package/src/lab/public/bundle.ts +217 -0
  389. package/src/lab/public/community-authority.ts +175 -0
  390. package/src/lab/public/community-files.ts +29 -0
  391. package/src/lab/public/community.ts +479 -0
  392. package/src/lab/public/file-safety.ts +155 -0
  393. package/src/lab/public/ids.ts +26 -0
  394. package/src/lab/public/index.ts +16 -0
  395. package/src/lab/public/mutation-lock.ts +424 -0
  396. package/src/lab/public/operator.ts +353 -0
  397. package/src/lab/public/origin-purge.ts +79 -0
  398. package/src/lab/public/origin.ts +203 -0
  399. package/src/lab/public/privacy.ts +143 -0
  400. package/src/lab/public/private-file.ts +261 -0
  401. package/src/lab/public/project.ts +124 -0
  402. package/src/lab/public/purge-test-fault.ts +21 -0
  403. package/src/lab/public/purge.ts +223 -0
  404. package/src/lab/public/registry.ts +44 -0
  405. package/src/lab/public/revocation.ts +252 -0
  406. package/src/lab/public/signature.ts +243 -0
  407. package/src/lab/public/storage.ts +105 -0
  408. package/src/lab/public/strict-json.ts +206 -0
  409. package/src/lab/public/time.ts +26 -0
  410. package/src/lab/public/types.ts +172 -0
  411. package/src/lab/public/validate.ts +391 -0
  412. package/src/lab/query/catalog.ts +101 -0
  413. package/src/lab/query/connection.ts +107 -0
  414. package/src/lab/query/constants.ts +4 -0
  415. package/src/lab/query/cursor.ts +132 -0
  416. package/src/lab/query/dto-map.ts +277 -0
  417. package/src/lab/query/errors.ts +22 -0
  418. package/src/lab/query/freshness.ts +53 -0
  419. package/src/lab/query/index.ts +45 -0
  420. package/src/lab/query/latest-observation.ts +59 -0
  421. package/src/lab/query/passive-production.ts +159 -0
  422. package/src/lab/query/queries.ts +444 -0
  423. package/src/lab/query/types.ts +266 -0
  424. package/src/lab/subject/behavior-fingerprint.ts +77 -0
  425. package/src/lab/subject/installation-salt.ts +112 -0
  426. package/src/lab/subject/protocol-subject.ts +80 -0
  427. package/src/lab/subject/route-subject.ts +74 -0
  428. package/src/lib/admin-secrets.ts +24 -0
  429. package/src/lib/app-owned-memory-stores.ts +22 -0
  430. package/src/lib/bounded-body.ts +167 -11
  431. package/src/lib/bun-runtime.ts +125 -12
  432. package/src/lib/bun-stream-caps.ts +13 -9
  433. package/src/lib/codex-restart-contract.ts +120 -0
  434. package/src/lib/config-ownership.ts +26 -2
  435. package/src/lib/destination-policy.ts +65 -1
  436. package/src/lib/errors.ts +80 -5
  437. package/src/lib/fabric-task-execution-authority.ts +7 -0
  438. package/src/lib/fabric-task-host.ts +29 -0
  439. package/src/lib/lab-activation.ts +223 -0
  440. package/src/lib/lab-live-execution-authority.ts +13 -0
  441. package/src/lib/lab-live-host.ts +30 -0
  442. package/src/lib/lab-live-pinned-sender.ts +56 -0
  443. package/src/lib/lab-live-route-production.ts +130 -0
  444. package/src/lib/lab-passive-linker-registration.ts +26 -0
  445. package/src/lib/local-management-attestation.ts +51 -0
  446. package/src/lib/local-management-capability.ts +100 -0
  447. package/src/lib/local-provider-reload-contract.ts +100 -0
  448. package/src/lib/optional-shutdown-hooks.ts +57 -0
  449. package/src/lib/package-tree-integrity.ts +101 -0
  450. package/src/lib/pinned-http.ts +145 -26
  451. package/src/lib/process-control.ts +6 -2
  452. package/src/lib/provider-outbound.ts +49 -9
  453. package/src/lib/redact.ts +419 -3
  454. package/src/lib/self-launch-argv.ts +15 -0
  455. package/src/lib/server-resource-ownership.ts +71 -0
  456. package/src/lib/service-secrets.ts +15 -0
  457. package/src/lib/shadow-call.ts +35 -4
  458. package/src/lib/sse-decoder.ts +41 -0
  459. package/src/lib/state-store-registrations.ts +10 -2
  460. package/src/lib/system-restart-contract.ts +73 -0
  461. package/src/lib/token-estimate.ts +19 -2
  462. package/src/lib/tool-argument-integers.ts +253 -0
  463. package/src/lib/translator-budget.ts +44 -0
  464. package/src/lib/upstream-http-version.ts +57 -0
  465. package/src/lib/upstream-reachability.ts +95 -0
  466. package/src/lib/upstream-retry.ts +156 -3
  467. package/src/lib/windows-atomic-replace.ts +155 -0
  468. package/src/lib/windows-elevation.ts +70 -2
  469. package/src/lib/windows-secret-acl.ts +409 -69
  470. package/src/lib/windows-service-wrappers.ts +72 -0
  471. package/src/lib/windows-text.ts +106 -0
  472. package/src/lib/windows-user-principal.ts +341 -0
  473. package/src/lib/winsw.ts +33 -5
  474. package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
  475. package/src/oauth/account-import/index.ts +15 -0
  476. package/src/oauth/account-import/parser.ts +83 -0
  477. package/src/oauth/account-import/registry.ts +18 -0
  478. package/src/oauth/account-import/service.ts +75 -0
  479. package/src/oauth/account-import/types.ts +91 -0
  480. package/src/oauth/anthropic.ts +12 -1
  481. package/src/oauth/callback-server.ts +30 -4
  482. package/src/oauth/chatgpt.ts +12 -1
  483. package/src/oauth/cline.ts +203 -0
  484. package/src/oauth/command-code.ts +239 -0
  485. package/src/oauth/cursor.ts +46 -5
  486. package/src/oauth/generic-account-failover.ts +231 -0
  487. package/src/oauth/google-antigravity.ts +35 -3
  488. package/src/oauth/health.ts +20 -12
  489. package/src/oauth/index.ts +462 -71
  490. package/src/oauth/key-providers.ts +25 -0
  491. package/src/oauth/kimi.ts +25 -3
  492. package/src/oauth/kiro.ts +95 -6
  493. package/src/oauth/local-token-detect.ts +13 -2
  494. package/src/oauth/log.ts +3 -1
  495. package/src/oauth/login-cli.ts +88 -28
  496. package/src/oauth/nous.ts +798 -0
  497. package/src/oauth/open-browser-choice.ts +26 -0
  498. package/src/oauth/store.ts +133 -26
  499. package/src/oauth/token-guardian.ts +9 -3
  500. package/src/oauth/types.ts +15 -0
  501. package/src/pi/models.ts +2 -2
  502. package/src/providers/alibaba-region-migration.ts +1 -1
  503. package/src/providers/antigravity-models.ts +521 -31
  504. package/src/providers/auto-compact-budget.ts +65 -0
  505. package/src/providers/base-url-choices.ts +10 -0
  506. package/src/providers/codex-capacity.ts +292 -0
  507. package/src/providers/command-code-efforts.ts +176 -0
  508. package/src/providers/context-cap.ts +26 -8
  509. package/src/providers/cursor-pool.ts +72 -0
  510. package/src/providers/default-aliases.ts +65 -0
  511. package/src/providers/derive.ts +281 -6
  512. package/src/providers/fastwire.ts +507 -0
  513. package/src/providers/free-directory.ts +10 -7
  514. package/src/providers/google-vertex-location.ts +14 -0
  515. package/src/providers/key-failover.ts +71 -3
  516. package/src/providers/label.ts +35 -2
  517. package/src/providers/model-discovery-limits.ts +16 -0
  518. package/src/providers/model-discovery.ts +115 -22
  519. package/src/providers/model-presets.ts +119 -0
  520. package/src/providers/model-rename-migration.ts +255 -0
  521. package/src/providers/model-rename-startup.ts +28 -0
  522. package/src/providers/new-model-policy.ts +146 -0
  523. package/src/providers/openai-sidecar.ts +72 -4
  524. package/src/providers/openai-tier-startup.ts +31 -2
  525. package/src/providers/openai-tiers.ts +119 -4
  526. package/src/providers/openai-virtual-models.ts +1 -0
  527. package/src/providers/opencode-zen-rate-limit.ts +102 -0
  528. package/src/providers/provider-id-rewrite.ts +30 -0
  529. package/src/providers/quota.ts +1379 -40
  530. package/src/providers/registry.ts +1545 -112
  531. package/src/providers/request-pacing.ts +310 -0
  532. package/src/providers/service-tier.ts +277 -0
  533. package/src/providers/slug-codec.ts +94 -6
  534. package/src/providers/static-model-discovery.ts +86 -0
  535. package/src/providers/xai-responses-opt-in.ts +15 -0
  536. package/src/providers/xai-transport.ts +32 -4
  537. package/src/reasoning-effort.ts +68 -3
  538. package/src/responses/apply-patch-envelope.ts +63 -0
  539. package/src/responses/code-mode-helper-compat.ts +50 -0
  540. package/src/responses/compaction.ts +26 -1
  541. package/src/responses/custom-tool-compat.ts +384 -0
  542. package/src/responses/hosted-tool-policy.ts +9 -0
  543. package/src/responses/namespace-tool-compat.ts +435 -0
  544. package/src/responses/parser.ts +225 -38
  545. package/src/responses/provider-continuation.ts +98 -0
  546. package/src/responses/provider-opaque-metadata.ts +73 -0
  547. package/src/responses/reasoning-envelope.ts +9 -1
  548. package/src/responses/reasoning-replay-cache.ts +504 -0
  549. package/src/responses/schema.ts +12 -2
  550. package/src/responses/spill-store.ts +75 -10
  551. package/src/responses/state.ts +659 -31
  552. package/src/responses/thought-signature-replay.ts +364 -0
  553. package/src/responses/tool-search-compat.ts +301 -0
  554. package/src/responses/truncated-stop-reason.ts +60 -0
  555. package/src/responses/turn-termination.ts +107 -0
  556. package/src/router.ts +405 -31
  557. package/src/routing/analytics.ts +379 -0
  558. package/src/routing/capability.ts +244 -0
  559. package/src/routing/compatibility/assemble.ts +73 -0
  560. package/src/routing/compatibility/behavior.ts +278 -0
  561. package/src/routing/compatibility/catalog.ts +99 -0
  562. package/src/routing/compatibility/endpoint.ts +52 -0
  563. package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
  564. package/src/routing/compatibility/policy.ts +181 -0
  565. package/src/routing/compatibility/provider-slot.ts +56 -0
  566. package/src/routing/compatibility/reader.ts +110 -0
  567. package/src/routing/compatibility/subject.ts +191 -0
  568. package/src/routing/compatibility/types.ts +64 -0
  569. package/src/routing/compatibility/version.ts +104 -0
  570. package/src/routing/cost.ts +77 -0
  571. package/src/routing/evaluator.ts +495 -0
  572. package/src/routing/health.ts +412 -0
  573. package/src/routing/history/cursor.ts +43 -0
  574. package/src/routing/history/indexer.ts +605 -0
  575. package/src/routing/history/schema.ts +72 -0
  576. package/src/routing/profile-namespace.ts +15 -0
  577. package/src/routing/profile.ts +547 -0
  578. package/src/routing/quota.ts +155 -0
  579. package/src/routing/request-evidence.ts +45 -0
  580. package/src/routing/trace.ts +776 -0
  581. package/src/server/adapter-resolve.ts +2 -29
  582. package/src/server/auth-cors.ts +289 -48
  583. package/src/server/background-lifecycle.ts +182 -0
  584. package/src/server/chat-completions.ts +154 -72
  585. package/src/server/chat-native-sse.ts +331 -0
  586. package/src/server/chat-native.ts +445 -0
  587. package/src/server/claude-messages.ts +160 -44
  588. package/src/server/direct-local-http.ts +347 -0
  589. package/src/server/effort-policy.ts +18 -0
  590. package/src/server/github-copilot-responses-repair.ts +338 -0
  591. package/src/server/gui-static.ts +39 -10
  592. package/src/server/images.ts +94 -12
  593. package/src/server/index.ts +1021 -190
  594. package/src/server/lifecycle.ts +336 -14
  595. package/src/server/live.ts +136 -17
  596. package/src/server/local-management-read-client.ts +90 -0
  597. package/src/server/local-provider-reload-client.ts +137 -0
  598. package/src/server/management/agent-settings-routes.ts +433 -116
  599. package/src/server/management/api-key-usage.ts +31 -5
  600. package/src/server/management/body.ts +6 -0
  601. package/src/server/management/codex-prompt-routes.ts +570 -0
  602. package/src/server/management/combo-routes.ts +64 -25
  603. package/src/server/management/config-routes.ts +511 -51
  604. package/src/server/management/context.ts +89 -2
  605. package/src/server/management/integration-routes.ts +498 -0
  606. package/src/server/management/lab-automation-routes.ts +206 -0
  607. package/src/server/management/lab-routes.ts +563 -0
  608. package/src/server/management/logs-usage-routes.ts +127 -52
  609. package/src/server/management/model-routes.ts +461 -133
  610. package/src/server/management/model-rows.ts +167 -0
  611. package/src/server/management/native-integration-routes.ts +770 -0
  612. package/src/server/management/oauth-account-routes.ts +90 -8
  613. package/src/server/management/provider-capability-config.ts +48 -0
  614. package/src/server/management/provider-routes.ts +836 -159
  615. package/src/server/management/request-history-routes.ts +191 -0
  616. package/src/server/management/route-registry.ts +332 -0
  617. package/src/server/management/routing-analytics-routes.ts +74 -0
  618. package/src/server/management/routing-profile-routes.ts +380 -0
  619. package/src/server/management/shared.ts +27 -11
  620. package/src/server/management/sidebar-routes.ts +47 -31
  621. package/src/server/management/storage-log-guard-routes.ts +186 -0
  622. package/src/server/management/sync-response.ts +69 -0
  623. package/src/server/management/system-restart.ts +289 -33
  624. package/src/server/management/system-routes.ts +77 -0
  625. package/src/server/management/usage-summary-cache.ts +9 -1
  626. package/src/server/management/vision-sidecar-options.ts +167 -0
  627. package/src/server/management/web-search-sidecar-options.ts +120 -0
  628. package/src/server/management-api.ts +126 -23
  629. package/src/server/management-auth.ts +220 -5
  630. package/src/server/passive-route-linker.ts +66 -0
  631. package/src/server/port-reclaim.ts +1 -1
  632. package/src/server/ports.ts +41 -1
  633. package/src/server/proxy-liveness.ts +161 -10
  634. package/src/server/readiness.ts +99 -0
  635. package/src/server/relay-eager.ts +221 -54
  636. package/src/server/relay.ts +479 -87
  637. package/src/server/request-decompress.ts +113 -6
  638. package/src/server/request-log-conversation.ts +51 -0
  639. package/src/server/request-log.ts +298 -31
  640. package/src/server/responses/agent-task-recovery-cache.ts +143 -0
  641. package/src/server/responses/agent-task-recovery.ts +465 -0
  642. package/src/server/responses/codex-auth-error.ts +55 -0
  643. package/src/server/responses/collaboration.ts +239 -36
  644. package/src/server/responses/combo-stream-preflight.ts +178 -0
  645. package/src/server/responses/compact.ts +444 -72
  646. package/src/server/responses/core.ts +3579 -436
  647. package/src/server/responses/empty-completion-guard.ts +333 -0
  648. package/src/server/responses/encrypted-payload.ts +62 -39
  649. package/src/server/responses/fetch-helpers.ts +89 -99
  650. package/src/server/responses/input-admission.ts +187 -0
  651. package/src/server/responses/pacing-overload.ts +13 -0
  652. package/src/server/responses/passthrough-error.ts +33 -9
  653. package/src/server/responses/policy-fallback.ts +178 -0
  654. package/src/server/responses/responses-field-backfill.ts +343 -0
  655. package/src/server/responses/terminal-guard.ts +26 -5
  656. package/src/server/responses/upstream-error.ts +5 -0
  657. package/src/server/responses/ws-upstream.ts +381 -0
  658. package/src/server/responses-custom-tool-repair.ts +363 -0
  659. package/src/server/responses-item-id-repair.ts +54 -6
  660. package/src/server/responses-json-events.ts +90 -0
  661. package/src/server/responses-model-rewrite.ts +29 -0
  662. package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
  663. package/src/server/responses-snapshot-repair.ts +621 -0
  664. package/src/server/responses-terminal-repair.ts +363 -0
  665. package/src/server/responses-tool-search-repair.ts +267 -0
  666. package/src/server/responses-undeclared-tool-guard.ts +376 -0
  667. package/src/server/responses.ts +18 -2
  668. package/src/server/search.ts +78 -13
  669. package/src/server/sse-frame-buffer.ts +319 -0
  670. package/src/server/sse-payload-rewrite.ts +110 -22
  671. package/src/server/startup-action-control.ts +8 -1
  672. package/src/server/startup-health-cache.ts +19 -1
  673. package/src/server/system-env.ts +80 -9
  674. package/src/server/ws-bridge.ts +53 -40
  675. package/src/service-manager-probe.ts +892 -0
  676. package/src/service.ts +1153 -94
  677. package/src/sidecar/auth.ts +92 -0
  678. package/src/sidecar/candidates.ts +83 -0
  679. package/src/storage/cleanup.ts +2 -2
  680. package/src/storage/policy-job.ts +14 -4
  681. package/src/storage/policy.ts +88 -23
  682. package/src/storage/scanner.ts +1 -1
  683. package/src/storage/worker-lifecycle.ts +14 -14
  684. package/src/tray/windows-tray.ps1 +83 -9
  685. package/src/tray/windows.ts +43 -16
  686. package/src/types/accounts.ts +37 -0
  687. package/src/types/config.ts +911 -0
  688. package/src/types/provider.ts +614 -0
  689. package/src/types/request.ts +397 -0
  690. package/src/types/tools.ts +236 -0
  691. package/src/types/wire.ts +80 -0
  692. package/src/types.ts +107 -1237
  693. package/src/update/index.ts +37 -23
  694. package/src/update/job.ts +445 -68
  695. package/src/update/notify.ts +12 -6
  696. package/src/update/npm-cache-preflight.d.mts +47 -0
  697. package/src/update/npm-cache-preflight.mjs +201 -0
  698. package/src/update/transactional-install.d.mts +22 -0
  699. package/src/update/transactional-install.mjs +266 -0
  700. package/src/usage/cost.ts +0 -0
  701. package/src/usage/expected-prices.ts +287 -16
  702. package/src/usage/log.ts +666 -50
  703. package/src/usage/summary.ts +388 -17
  704. package/src/usage/user-cost-overlay-reconciler.ts +313 -0
  705. package/src/usage/user-cost-overlays.ts +314 -0
  706. package/src/vision/anthropic-describe.ts +10 -6
  707. package/src/vision/backends.ts +97 -0
  708. package/src/vision/describe.ts +27 -16
  709. package/src/vision/eligibility.ts +250 -0
  710. package/src/vision/index.ts +238 -24
  711. package/src/vision/reasoning.ts +55 -0
  712. package/src/vision/routed-describe.ts +175 -0
  713. package/src/vision/timeout-bounds.ts +9 -0
  714. package/src/web-search/anthropic-executor.ts +13 -7
  715. package/src/web-search/backends.ts +108 -0
  716. package/src/web-search/exa-executor.ts +119 -0
  717. package/src/web-search/executor.ts +21 -6
  718. package/src/web-search/gemini-executor.ts +141 -0
  719. package/src/web-search/index.ts +150 -15
  720. package/src/web-search/loop.ts +284 -54
  721. package/src/web-search/parse.ts +125 -30
  722. package/src/web-search/sources.ts +60 -0
  723. package/src/web-search/xai-executor.ts +219 -0
  724. package/gui/dist/assets/index-DTpMHS4F.js +0 -67
  725. package/gui/dist/assets/index-ZNVDE3C7.css +0 -1
  726. package/gui/dist/provider-icons/antigravity.svg +0 -1
  727. package/gui/dist/provider-icons/claude.svg +0 -1
  728. package/gui/dist/provider-icons/copilot.svg +0 -1
  729. package/gui/dist/provider-icons/cursor.svg +0 -2
  730. package/gui/dist/provider-icons/gemini.svg +0 -1
  731. package/gui/dist/provider-icons/grok-color.svg +0 -1
  732. package/gui/dist/provider-icons/kiro.svg +0 -14
  733. package/src/cli/internal-dispatch.ts +0 -20
@@ -1,6 +1,6 @@
1
1
  import type { AdapterFetchContext, AdapterRequest, ProviderAdapter } from "./base";
2
2
  import { debugDroppedFrame } from "../lib/debug";
3
- import { createHash } from "node:crypto";
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 {
6
6
  AdapterEvent,
@@ -8,28 +8,38 @@ import type {
8
8
  OcxContentPart,
9
9
  OcxParsedRequest,
10
10
  OcxProviderConfig,
11
+ OcxProviderOpaqueToolCallMetadata,
11
12
  OcxTextContent,
12
13
  OcxToolCall,
14
+ OcxToolResultMessage,
13
15
  OcxUsage,
14
16
  } from "../types";
15
- import { isAllowedToolChoice, namespacedToolName, toolAllowedByChoice } from "../types";
17
+ import { isAllowedToolChoice, namespacedToolName, resolveToolChoiceWireName, toolChoiceToolPredicate } from "../types";
16
18
  import { contentPartsToText, parseDataUrl } from "./image";
17
19
  import { getVertexAccessToken } from "../lib/gcp-adc";
18
20
  import { fetchAntigravityWithRetry, fetchVertexWithRetry } from "./google-http";
19
21
  import { safeAntigravityHttpErrorMessage, safeVertexHttpErrorMessage } from "./google-errors";
20
- import { isVertexTruncationReason, vertexTruncationErrorMessage } from "./google-truncation";
22
+ import { isVertexTruncatedTurn, vertexTruncationErrorMessage } from "./google-truncation";
21
23
  import { ANTIGRAVITY_REQUEST_UA, antigravitySessionId, isLikelyRealThoughtSignature, sanitizeAntigravityClaudeSignatures } from "./google-antigravity-wire";
22
24
  import { compileGoogleWireBody } from "./google-wire-compiler";
23
- import { neutralizeIdentity } from "./identity";
24
- import { antigravityUsesReplayCache, applyAntigravityReplay, clearAntigravityReplay, observeAntigravityReplay } from "./google-antigravity-replay";
25
+ import { identifyRoutedModel } from "./identity";
26
+ import {
27
+ antigravityUsesReplayCache,
28
+ applyAntigravityReplay,
29
+ applyAntigravityThoughtSignatureFallback,
30
+ clearAntigravityReplay,
31
+ observeAntigravityReplay,
32
+ } from "./google-antigravity-replay";
25
33
  import { resolveAntigravityEffortWireModel } from "../providers/antigravity-models";
34
+ import { googleVertexLocationConfigError } from "../providers/google-vertex-location";
35
+ import { forgetThoughtSignatureForReplay, lookupReplayThoughtSignature } from "../responses/thought-signature-replay";
26
36
  import {
27
37
  isTranslatorBudgetExceededError,
28
38
  retainTranslatedEventBatch,
29
39
  type TranslatorBudget,
30
40
  } from "../lib/translator-budget";
31
41
  import { buildNonOpenAIToolCatalogNudgeForTools } from "./tool-catalog-nudge";
32
- import { mapReasoningEffort } from "../reasoning-effort";
42
+ import { configuredReasoningEfforts, mapReasoningEffort } from "../reasoning-effort";
33
43
 
34
44
  // Google-family models (Gemini/Vertex/Antigravity) tend to emit long running commentary between
35
45
  // tool calls. This steers them to keep the BETWEEN-STEP text to one line and reason internally
@@ -44,12 +54,73 @@ const GOOGLE_BREVITY_INSTRUCTION = [
44
54
  "- This applies only to intermediate progress text. Your final answer after the work is done is exempt: write it in full and at whatever length the task requires.",
45
55
  ].join("\n");
46
56
 
57
+ /**
58
+ * Documented output ceiling for a Google-surface model, or `undefined` when the id is not
59
+ * recognized.
60
+ *
61
+ * Unknown ids return `undefined` deliberately. An earlier revision returned a 16,384 floor for
62
+ * anything unmatched, which silently truncated aliases, gateway ids, and any model added after
63
+ * this table was written — the operator asked for N tokens and got 16,384 with no signal. A cap
64
+ * we cannot justify is worse than no cap: `structure/02_config-and-codex-home.md` is explicit
65
+ * that an explicit request value wins, so an unrecognized model passes through untouched and the
66
+ * upstream remains the authority on its own limit.
67
+ *
68
+ * Matching is prefix/family based rather than substring based for the same reason: `includes("pro")`
69
+ * matched any id containing "pro" (`my-prototype-model`), and `includes("oss")` matched any id
70
+ * containing "oss" (`crossover-v2`).
71
+ */
72
+ export function maxOutputTokensForGoogleModel(modelId: string): number | undefined {
73
+ const lower = modelId.toLowerCase().trim();
74
+ if (lower.startsWith("gemini")) {
75
+ // Pro tops out one token below the flash/other Gemini ceiling; both are documented values.
76
+ return /(^|[-.])pro([-.]|$)/.test(lower) ? 65535 : 65536;
77
+ }
78
+ if (lower.startsWith("claude")) return 64000;
79
+ if (lower.startsWith("gpt-oss")) return 32768;
80
+ return undefined;
81
+ }
82
+
83
+ export function clampGoogleMaxOutputTokens(
84
+ modelId: string,
85
+ requestedTokens?: number,
86
+ ): number | undefined {
87
+ if (requestedTokens === undefined || requestedTokens <= 0) return undefined;
88
+ const modelMax = maxOutputTokensForGoogleModel(modelId);
89
+ // Unknown model: honour the request as-is rather than inventing a ceiling for it.
90
+ if (modelMax === undefined) return requestedTokens;
91
+ return Math.min(requestedTokens, modelMax);
92
+ }
93
+
94
+ /**
95
+ * Some Google direct deployments expose current Gemini Flash generations with a `-tiered`
96
+ * wire suffix (`gemini-3.7-flash` -> `gemini-3.7-flash-tiered`). Keep the picker-visible id
97
+ * stable and make the mapping configurable for deployments that still serve the bare id.
98
+ */
99
+ const GEMINI_DIRECT_WIRE_RENAMES: Record<string, string> = {
100
+ "gemini-3.7-flash": "gemini-3.7-flash-tiered",
101
+ "gemini-3.6-flash": "gemini-3.6-flash-tiered",
102
+ };
103
+
104
+ function resolveDirectGeminiWireModelId(modelId: string, applyRenames: boolean): string {
105
+ if (!applyRenames) return modelId;
106
+ return Object.hasOwn(GEMINI_DIRECT_WIRE_RENAMES, modelId)
107
+ ? GEMINI_DIRECT_WIRE_RENAMES[modelId]!
108
+ : modelId;
109
+ }
110
+
47
111
  /** Vertex API key: provider.apiKey if it looks real (not a sentinel), else GOOGLE_CLOUD_API_KEY env. */
48
112
  function resolveVertexApiKey(optKey?: string): string | undefined {
49
113
  const realKey = optKey && !optKey.startsWith("<") && optKey !== "N/A" ? optKey : undefined;
50
114
  return realKey || process.env.GOOGLE_CLOUD_API_KEY;
51
115
  }
52
116
 
117
+ /** Prefer Codex's stable opaque thread key; retain the existing deterministic fallback for clients
118
+ * that omit it. The replay store hashes this value and never retains the raw session identifier. */
119
+ function vertexReplaySessionId(parsed: OcxParsedRequest): string {
120
+ const threadId = parsed._clientThreadId?.trim();
121
+ return threadId || antigravitySessionId(parsed);
122
+ }
123
+
53
124
  /**
54
125
  * Stable tool-call id for the Gemini wire `functionCall.id` / `functionResponse.id` fields.
55
126
  *
@@ -67,15 +138,9 @@ function resolveVertexApiKey(optKey?: string): string | undefined {
67
138
  * the call/response pairing is preserved. Returns `undefined` for an empty id so the caller omits the
68
139
  * field entirely rather than inventing a non-matching one.
69
140
  */
70
- function geminiToolCallId(rawId: string | undefined): string | undefined {
71
- const raw = rawId ?? "";
72
- if (raw.length === 0) return undefined;
73
- const cleaned = raw.replace(/[^a-zA-Z0-9_-]/g, "_");
74
- if (cleaned === raw) return cleaned;
75
- // Lossy rewrite happened: disambiguate with a deterministic suffix derived from the raw id.
76
- const suffix = createHash("sha256").update(raw).digest("hex").slice(0, 8);
77
- return `${cleaned}_${suffix}`;
78
- }
141
+ // Aliasing the stateless transform here would reintroduce the collision it cannot prevent:
142
+ // a rewritten id can equal a distinct raw id that already conforms. Use a request-scoped
143
+ // allocator, exactly as the Anthropic adapter does, so call/response pairing stays injective.
79
144
 
80
145
  /**
81
146
  * Inline image parts (Gemini `inline_data`) extracted from tool-result content. Only base64 data URLs
@@ -104,6 +169,7 @@ function toolResultImageParts(content: string | OcxContentPart[]): unknown[] {
104
169
  */
105
170
  const GEMINI_EMPTY_PLACEHOLDER = "(empty)";
106
171
  const GEMINI_EMPTY_TOOL_OUTPUT_PLACEHOLDER = "(empty tool output)";
172
+ const GEMINI_MISSING_TOOL_RESULT = "[missing tool_result for this tool_use in history]";
107
173
 
108
174
  /** A Gemini text part, or undefined when the value cannot form a valid non-empty text block. */
109
175
  function geminiTextPart(text: unknown): { text: string } | undefined {
@@ -118,24 +184,75 @@ function geminiTextPart(text: unknown): { text: string } | undefined {
118
184
  */
119
185
  function geminiToolResultText(content: string | OcxContentPart[]): string {
120
186
  if (typeof content === "string") return content || GEMINI_EMPTY_TOOL_OUTPUT_PLACEHOLDER;
121
- const hasContent = content.some(p => p.type === "image" || (typeof p.text === "string" && p.text.length > 0));
187
+ const hasContent = content.some(p => p.type !== "text" || p.text.length > 0);
122
188
  return hasContent ? contentPartsToText(content) : GEMINI_EMPTY_TOOL_OUTPUT_PLACEHOLDER;
123
189
  }
124
190
 
125
- function messagesToGeminiFormat(parsed: OcxParsedRequest): { systemInstruction?: unknown; contents: unknown[] } {
191
+ function geminiToolResultParts(
192
+ msg: OcxToolResultMessage,
193
+ wireName: string,
194
+ wireCallId: string,
195
+ ): unknown[] {
196
+ const functionResponse: Record<string, unknown> = {
197
+ name: wireName,
198
+ response: { result: geminiToolResultText(msg.content) },
199
+ id: wireCallId,
200
+ };
201
+ return [{ functionResponse }, ...toolResultImageParts(msg.content)];
202
+ }
203
+
204
+ function geminiMissingToolResultPart(wireName: string, wireCallId: string): unknown {
205
+ return {
206
+ functionResponse: {
207
+ name: wireName,
208
+ response: { result: GEMINI_MISSING_TOOL_RESULT },
209
+ id: wireCallId,
210
+ },
211
+ };
212
+ }
213
+
214
+ function geminiUnrepresentableToolCallPart(tc: OcxToolCall, wireName: string): unknown {
215
+ const args = typeof tc.arguments === "string" ? tc.arguments : JSON.stringify(tc.arguments);
216
+ return { text: `[tool_use without a usable id: ${wireName}]\n${args}` };
217
+ }
218
+
219
+ function geminiOrphanToolResultParts(msg: OcxToolResultMessage): unknown[] {
220
+ const label = msg.toolName ? `${msg.toolName} (${msg.toolCallId})` : msg.toolCallId;
221
+ return [
222
+ { text: `[tool_result without adjacent tool_use: ${label}]\n${geminiToolResultText(msg.content)}` },
223
+ ...toolResultImageParts(msg.content),
224
+ ];
225
+ }
226
+
227
+ function messagesToGeminiFormat(
228
+ parsed: OcxParsedRequest,
229
+ identityModelId: string,
230
+ ): { systemInstruction?: unknown; contents: unknown[]; replayedCallIds: string[] } {
126
231
  // Neutralize Codex's GPT-5 identity line (Gemini/Antigravity share this path) so a routed model
127
232
  // never misreports as GPT-5/OpenAI, and never leaks the proxy identity upstream.
128
233
  const toolCatalogNudge = buildNonOpenAIToolCatalogNudgeForTools(parsed.context.tools, parsed.options.toolChoice);
129
- const systemText = neutralizeIdentity([
234
+ const systemText = identifyRoutedModel([
130
235
  ...(parsed.context.systemPrompt ?? []),
131
236
  ...(toolCatalogNudge ? [toolCatalogNudge] : []),
132
237
  GOOGLE_BREVITY_INSTRUCTION,
133
- ].join("\n\n"));
238
+ ].join("\n\n"), identityModelId);
134
239
  const systemInstruction = { parts: [{ text: systemText }] };
135
240
 
136
241
  const contents: unknown[] = [];
242
+ const replayedCallIds: string[] = [];
137
243
 
244
+ const callIds = createToolCallIdAllocator();
138
245
  for (const msg of parsed.context.messages) {
246
+ if (msg.role === "assistant") {
247
+ for (const part of (msg as OcxAssistantMessage).content) {
248
+ if (part.type === "toolCall") callIds.reserve((part as OcxToolCall).id);
249
+ }
250
+ } else if (msg.role === "toolResult") {
251
+ callIds.reserve((msg as OcxToolResultMessage).toolCallId);
252
+ }
253
+ }
254
+ for (let i = 0; i < parsed.context.messages.length; i++) {
255
+ const msg = parsed.context.messages[i];
139
256
  switch (msg.role) {
140
257
  case "user":
141
258
  case "developer": {
@@ -151,6 +268,13 @@ function messagesToGeminiFormat(parsed: OcxParsedRequest): { systemInstruction?:
151
268
  parts.push(data ? { inline_data: { mime_type: data.mediaType, data: data.base64 } } : { text: `[image: ${p.imageUrl}]` });
152
269
  continue;
153
270
  }
271
+ if (p.type === "video") {
272
+ const data = parseDataUrl(p.videoUrl);
273
+ // Gemini accepts inline video bytes in the same Part union as images. Arbitrary
274
+ // remote URLs are not valid fileData references, so retain only a short marker.
275
+ parts.push(data ? { inline_data: { mime_type: data.mediaType, data: data.base64 } } : { text: `[video: ${p.videoUrl}]` });
276
+ continue;
277
+ }
154
278
  // Drop empty/malformed text instead of emitting `{ text: "" }` or a bare `{}` part.
155
279
  const textPart = geminiTextPart(p.text);
156
280
  if (textPart) parts.push(textPart);
@@ -162,6 +286,7 @@ function messagesToGeminiFormat(parsed: OcxParsedRequest): { systemInstruction?:
162
286
  case "assistant": {
163
287
  const aMsg = msg as OcxAssistantMessage;
164
288
  const parts: unknown[] = [];
289
+ const toolCalls: Array<{ wireCallId: string; wireName: string }> = [];
165
290
  for (const p of aMsg.content) {
166
291
  if (p.type === "text") {
167
292
  const textPart = geminiTextPart((p as OcxTextContent).text);
@@ -173,13 +298,34 @@ function messagesToGeminiFormat(parsed: OcxParsedRequest): { systemInstruction?:
173
298
  // streaming covered by the replay cache. Only forward a REAL upstream signature — the
174
299
  // Responses parser also stashes synthetic item ids (`fc_...`) on this field, and sending
175
300
  // those as a thoughtSignature breaks continuity (the replay cache supplies the real one).
176
- const callId = geminiToolCallId(tc.id);
177
- const functionCall: Record<string, unknown> = { name: namespacedToolName(tc.namespace, tc.name), args: tc.arguments };
301
+ const callId = callIds.allocate(tc.id);
302
+ const wireName = namespacedToolName(tc.namespace, tc.name);
303
+ if (callId === undefined) {
304
+ // Claude-on-Antigravity requires a usable id for every translated tool_use. An empty
305
+ // source id cannot be paired safely, so preserve the call as text and let its result
306
+ // follow the same orphan-text path instead of emitting an invalid functionCall.
307
+ parts.push(geminiUnrepresentableToolCallPart(tc, wireName));
308
+ continue;
309
+ }
310
+ const functionCall: Record<string, unknown> = { name: wireName, args: tc.arguments };
178
311
  // Claude-on-Antigravity maps this id to Anthropic `tool_use.id`; without it the upstream
179
312
  // conversion 400s. Gemini accepts the optional id and pairs call/response by it.
180
- if (callId !== undefined) functionCall.id = callId;
313
+ functionCall.id = callId;
314
+ toolCalls.push({ wireCallId: callId, wireName });
181
315
  const part: Record<string, unknown> = { functionCall };
182
- if (isLikelyRealThoughtSignature(tc.thoughtSignature)) part.thoughtSignature = tc.thoughtSignature;
316
+ // Prefer the metadata that travelled with this exact call; fall back to the legacy
317
+ // field for callers that have not been migrated. Never merge or synthesize.
318
+ // Final fallback (#1926): the durable store, read AT SERIALIZATION TIME. The
319
+ // Responses parser runs before the route/credential scope is bound, so its
320
+ // parse-time lookup can never hit; by the time this adapter serializes, the
321
+ // credential-scoped identity is bound and the durable lookup is meaningful.
322
+ const signature = tc.providerMetadata?.google?.thoughtSignature
323
+ ?? tc.thoughtSignature
324
+ ?? lookupReplayThoughtSignature(tc.id, parsed._reasoningReplayScope);
325
+ if (isLikelyRealThoughtSignature(signature)) {
326
+ part.thoughtSignature = signature;
327
+ replayedCallIds.push(tc.id);
328
+ }
183
329
  parts.push(part);
184
330
  }
185
331
  }
@@ -188,36 +334,56 @@ function messagesToGeminiFormat(parsed: OcxParsedRequest): { systemInstruction?:
188
334
  // adapter does for its own empty assistant content.
189
335
  if (parts.length === 0) break;
190
336
  contents.push({ role: "model", parts });
337
+ if (toolCalls.length > 0) {
338
+ // Gemini/Claude-on-Antigravity requires one adjacent response batch for the whole
339
+ // function-call turn. Replayed histories can be interrupted, reversed, duplicated, or
340
+ // contain an orphan result; repair only this wire boundary without inventing success.
341
+ const requiredIds = new Set(toolCalls.map(call => call.wireCallId));
342
+ const resultsById = new Map<string, OcxToolResultMessage>();
343
+ const orphanResults: OcxToolResultMessage[] = [];
344
+ let j = i + 1;
345
+ while (j < parsed.context.messages.length && parsed.context.messages[j].role === "toolResult") {
346
+ const result = parsed.context.messages[j] as OcxToolResultMessage;
347
+ const wireResultId = callIds.lookup(result.toolCallId);
348
+ if (wireResultId !== undefined && requiredIds.has(wireResultId) && !resultsById.has(wireResultId)) {
349
+ resultsById.set(wireResultId, result);
350
+ } else {
351
+ orphanResults.push(result);
352
+ }
353
+ j++;
354
+ }
355
+
356
+ const responseParts: unknown[] = [];
357
+ for (const call of toolCalls) {
358
+ const result = resultsById.get(call.wireCallId);
359
+ if (result) responseParts.push(...geminiToolResultParts(result, call.wireName, call.wireCallId));
360
+ else responseParts.push(geminiMissingToolResultPart(call.wireName, call.wireCallId));
361
+ }
362
+ for (const orphan of orphanResults) {
363
+ responseParts.push(...geminiOrphanToolResultParts(orphan));
364
+ }
365
+ contents.push({ role: "user", parts: responseParts });
366
+ i = j - 1;
367
+ }
191
368
  break;
192
369
  }
193
370
  case "toolResult": {
194
- // The functionResponse part carries the textual result. Gemini cannot embed images inside a
195
- // functionResponse, but it does accept sibling inline_data parts in the same user turn, so
196
- // tool-result screenshots (e.g. Computer Use) ride along as inline_data instead of being
197
- // flattened to a "[image]" marker the model can't actually see.
198
- const responseId = geminiToolCallId(msg.toolCallId);
199
- const functionResponse: Record<string, unknown> = { name: namespacedToolName(msg.toolNamespace, msg.toolName), response: { result: geminiToolResultText(msg.content) } };
200
- // Mirror the matching functionCall id so Claude-on-Antigravity can pair this result with its
201
- // `tool_use` block (-> Anthropic `tool_result.tool_use_id`).
202
- if (responseId !== undefined) functionResponse.id = responseId;
203
- const parts: unknown[] = [{ functionResponse }];
204
- for (const part of toolResultImageParts(msg.content)) parts.push(part);
205
- contents.push({ role: "user", parts });
371
+ // A standalone functionResponse is invalid without an immediately preceding matching
372
+ // functionCall batch. Preserve the result as explicit user text (plus any representable
373
+ // image siblings) rather than manufacturing a successful call or sending a 400-prone shape.
374
+ contents.push({ role: "user", parts: geminiOrphanToolResultParts(msg as OcxToolResultMessage) });
206
375
  break;
207
376
  }
208
377
  }
209
378
  }
210
379
 
211
- return { systemInstruction, contents };
380
+ return { systemInstruction, contents, replayedCallIds };
212
381
  }
213
382
 
214
383
  function toolsToGeminiFormat(parsed: OcxParsedRequest): unknown[] | undefined {
215
384
  if (!parsed.context.tools?.length) return undefined;
216
- const allowed = isAllowedToolChoice(parsed.options.toolChoice)
217
- ? new Set(parsed.options.toolChoice.allowedTools)
218
- : undefined;
219
- const tools = allowed
220
- ? parsed.context.tools.filter(t => toolAllowedByChoice(t, allowed))
385
+ const tools = isAllowedToolChoice(parsed.options.toolChoice)
386
+ ? parsed.context.tools.filter(toolChoiceToolPredicate(parsed.options.toolChoice, parsed.context.tools))
221
387
  : parsed.context.tools;
222
388
  if (tools.length === 0) return undefined;
223
389
  return [{
@@ -229,6 +395,28 @@ function toolsToGeminiFormat(parsed: OcxParsedRequest): unknown[] | undefined {
229
395
  }];
230
396
  }
231
397
 
398
+ /**
399
+ * Client tool_choice enforcement on the wire. The catalog nudge states the same contract in
400
+ * prose, but without functionCallingConfig the model is free to ignore it. "auto" stays absent
401
+ * so the common case is byte-identical. The allowedTools variant already filters the
402
+ * declarations in toolsToGeminiFormat; only its "required" half needs a wire mode.
403
+ */
404
+ function toolChoiceToGeminiToolConfig(parsed: OcxParsedRequest): Record<string, unknown> | undefined {
405
+ const choice = parsed.options.toolChoice;
406
+ if (!choice || choice === "auto") return undefined;
407
+ if (choice === "none") return { functionCallingConfig: { mode: "NONE" } };
408
+ if (choice === "required") return { functionCallingConfig: { mode: "ANY" } };
409
+ if (isAllowedToolChoice(choice)) {
410
+ return choice.mode === "required" ? { functionCallingConfig: { mode: "ANY" } } : undefined;
411
+ }
412
+ return {
413
+ functionCallingConfig: {
414
+ mode: "ANY",
415
+ allowedFunctionNames: [resolveToolChoiceWireName(parsed.context.tools, choice.name)],
416
+ },
417
+ };
418
+ }
419
+
232
420
  function usageFromGemini(usage: Record<string, number> | undefined): OcxUsage | undefined {
233
421
  if (!usage) return undefined;
234
422
  return {
@@ -271,17 +459,259 @@ function artifactMarkdownUrl(filePath: string): string {
271
459
  return artifactHttpUrl(filePath).replace(/([()])/g, "\\$1");
272
460
  }
273
461
 
462
+ interface GoogleResponsePart {
463
+ text?: unknown;
464
+ thought?: boolean;
465
+ thoughtSignature?: string;
466
+ thought_signature?: string;
467
+ extra_content?: { google?: { thought_signature?: unknown } };
468
+ functionCall?: unknown;
469
+ }
470
+
471
+ interface GoogleFunctionCall {
472
+ name: string;
473
+ args?: unknown;
474
+ }
475
+
476
+ /**
477
+ * Read a Gemini/Antigravity thought signature from a response part. Antigravity can place it
478
+ * either directly on the part (`thoughtSignature` / `thought_signature`) or inside the same
479
+ * nested `extra_content.google.thought_signature` shape used on the Responses wire.
480
+ */
481
+ function googlePartThoughtSignature(part: GoogleResponsePart): string | undefined {
482
+ const direct = part.thoughtSignature ?? part.thought_signature;
483
+ if (typeof direct === "string" && direct.length > 0) return direct;
484
+ const nested = part.extra_content?.google?.thought_signature;
485
+ return typeof nested === "string" && nested.length > 0 ? nested : undefined;
486
+ }
487
+
488
+ /**
489
+ * Carry a Gemini thought signature with the exact function-call part that produced it. Google
490
+ * validates the signature against that specific part, so it must ride the individual tool call
491
+ * rather than be re-matched by name/arguments later (issue #1735).
492
+ */
493
+ function googleToolCallMetadataFromPart(
494
+ part: GoogleResponsePart,
495
+ fallbackSignature?: string,
496
+ ): { providerMetadata: OcxProviderOpaqueToolCallMetadata } | undefined {
497
+ const signature = googlePartThoughtSignature(part) ?? fallbackSignature;
498
+ if (!isLikelyRealThoughtSignature(signature)) return undefined;
499
+ return { providerMetadata: { google: { thoughtSignature: signature } } };
500
+ }
501
+
502
+ /**
503
+ * Google marks model-internal reasoning as a normal text-bearing part plus `thought: true`.
504
+ * Keep that provider visibility bit authoritative here so the streaming and buffered parsers
505
+ * cannot accidentally expose the same hidden reasoning through different event types.
506
+ */
507
+ function googlePartTextEvent(part: GoogleResponsePart): AdapterEvent | undefined {
508
+ // A malformed scalar/object is not text and must not cross the AdapterEvent boundary. Dropping
509
+ // only this optional field preserves the rest of the part without inventing assistant output by
510
+ // coercion; an empty string keeps its existing no-event behavior.
511
+ if (typeof part.text !== "string" || part.text.length === 0) return undefined;
512
+ return part.thought === true
513
+ ? { type: "reasoning_raw_delta", text: part.text }
514
+ : { type: "text_delta", text: part.text };
515
+ }
516
+
517
+ interface InvalidGoogleFunctionCallDiagnostic {
518
+ reason:
519
+ | "function_call_not_object"
520
+ | "function_call_name_invalid"
521
+ | "function_call_name_blank";
522
+ partIndex: number;
523
+ valueType: string;
524
+ }
525
+
526
+ interface InvalidGoogleShapeDiagnostic {
527
+ reason:
528
+ | "candidates_not_array"
529
+ | "candidate_not_object"
530
+ | "content_not_object"
531
+ | "parts_not_array"
532
+ | "part_not_object";
533
+ partIndex?: number;
534
+ valueType: string;
535
+ }
536
+
537
+ function isGoogleRecord(value: unknown): value is Record<string, unknown> {
538
+ return value !== null && typeof value === "object" && !Array.isArray(value);
539
+ }
540
+
541
+ function googleStructuralValueType(value: unknown): string {
542
+ if (value === null) return "null";
543
+ return Array.isArray(value) ? "array" : typeof value;
544
+ }
545
+
546
+ /**
547
+ * Gemini delivers one complete functionCall per part, so a missing name cannot be repaired by a
548
+ * later delta. Validate the whole parts array before observing signatures or emitting content: a
549
+ * malformed call must terminate the claimed response rather than enter replay state or reach the
550
+ * bridge as a nameless dispatch. Null remains an absence encoding, matching other optional fields.
551
+ */
552
+ function diagnoseGoogleFunctionCalls(
553
+ parts: readonly GoogleResponsePart[],
554
+ ): InvalidGoogleFunctionCallDiagnostic | undefined {
555
+ for (let partIndex = 0; partIndex < parts.length; partIndex++) {
556
+ const functionCall = parts[partIndex]?.functionCall;
557
+ if (functionCall === undefined || functionCall === null) continue;
558
+ if (!isGoogleRecord(functionCall)) {
559
+ return {
560
+ reason: "function_call_not_object",
561
+ partIndex,
562
+ valueType: googleStructuralValueType(functionCall),
563
+ };
564
+ }
565
+ if (typeof functionCall.name !== "string") {
566
+ return {
567
+ reason: "function_call_name_invalid",
568
+ partIndex,
569
+ valueType: googleStructuralValueType(functionCall.name),
570
+ };
571
+ }
572
+ if (functionCall.name.trim().length === 0) {
573
+ return {
574
+ reason: "function_call_name_blank",
575
+ partIndex,
576
+ valueType: "string",
577
+ };
578
+ }
579
+ }
580
+ return undefined;
581
+ }
582
+
583
+ function googleFunctionCall(part: GoogleResponsePart): GoogleFunctionCall | undefined {
584
+ const functionCall = part.functionCall;
585
+ if (!isGoogleRecord(functionCall) || typeof functionCall.name !== "string") return undefined;
586
+ return { name: functionCall.name, args: functionCall.args };
587
+ }
588
+
589
+ function invalidGoogleFunctionCallEvent(
590
+ diagnostic: InvalidGoogleFunctionCallDiagnostic,
591
+ ): Extract<AdapterEvent, { type: "error" }> {
592
+ const subject = diagnostic.reason === "function_call_not_object"
593
+ ? "invalid function call"
594
+ : diagnostic.reason === "function_call_name_blank"
595
+ ? "blank function call name"
596
+ : "invalid function call name";
597
+ return {
598
+ type: "error",
599
+ message: `google response contained ${subject} (${diagnostic.reason}; partIndex=${diagnostic.partIndex}; valueType=${diagnostic.valueType}) — cannot dispatch`,
600
+ };
601
+ }
602
+
603
+ /**
604
+ * A candidate's `content` is claimed model output inside a well-formed frame, so it is governed by
605
+ * the #1332 nested-shape rule (fail closed) rather than #1240's root-frame padding rule (skip).
606
+ *
607
+ * Absence stays legal, and so does one encoding of it: an empty array is how a JSON writer with no
608
+ * distinct empty-object form spells an empty `content`, and it already behaves as "no parts". A
609
+ * NON-empty array is the opposite case — `content?.parts` silently reads `undefined` from it, so a
610
+ * candidate shaped `content: [{ parts: [...] }]` dropped its own text and completed as an empty
611
+ * turn.
612
+ */
613
+ function diagnoseGoogleContent(content: unknown): InvalidGoogleShapeDiagnostic | undefined {
614
+ if (content === undefined || content === null || isGoogleRecord(content)) return undefined;
615
+ if (Array.isArray(content) && content.length === 0) return undefined;
616
+ return { reason: "content_not_object", valueType: googleStructuralValueType(content) };
617
+ }
618
+
619
+ /**
620
+ * `content.parts` sits one rung below the candidate guard added in #1332, and both parsers
621
+ * consumed it unchecked: `for (const part of {})` throws `{} is not iterable`, and a `[null]`
622
+ * element throws on `part.thoughtSignature`. A `parts` that is absent or `null` keeps its existing
623
+ * skip — only a present, non-null container is validated.
624
+ */
625
+ function diagnoseGoogleParts(parts: unknown): InvalidGoogleShapeDiagnostic | undefined {
626
+ if (!Array.isArray(parts)) {
627
+ return { reason: "parts_not_array", valueType: googleStructuralValueType(parts) };
628
+ }
629
+ for (let partIndex = 0; partIndex < parts.length; partIndex++) {
630
+ const part: unknown = parts[partIndex];
631
+ if (!isGoogleRecord(part)) {
632
+ return { reason: "part_not_object", partIndex, valueType: googleStructuralValueType(part) };
633
+ }
634
+ }
635
+ return undefined;
636
+ }
637
+
638
+ function invalidGoogleShapeEvent(
639
+ diagnostic: InvalidGoogleShapeDiagnostic,
640
+ ): Extract<AdapterEvent, { type: "error" }> {
641
+ const at = diagnostic.partIndex !== undefined ? `; partIndex=${diagnostic.partIndex}` : "";
642
+ // The subject names the rung that failed, so an operator reading a log can tell a broken
643
+ // candidate list from a well-formed candidate whose parts are broken. The candidate subject keeps
644
+ // the exact wording #1332 introduced as its prefix, so an existing grep still matches.
645
+ const subject = diagnostic.reason === "candidates_not_array" || diagnostic.reason === "candidate_not_object"
646
+ ? "candidates"
647
+ : diagnostic.reason === "content_not_object"
648
+ ? "content"
649
+ : "content parts";
650
+ return {
651
+ type: "error",
652
+ message: `google response contained invalid ${subject} (${diagnostic.reason}${at}; valueType=${diagnostic.valueType})`,
653
+ };
654
+ }
655
+
274
656
  export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapter {
275
657
  // Per-request closure: resolveAdapter builds a fresh adapter per request (server.ts), so buildRequest
276
658
  // can stash the CCA model/session for parseStream's reasoning-replay observation.
277
659
  let antigravityModel: string | undefined;
278
660
  let antigravitySession: string | undefined;
661
+ // Vertex returns the same opaque Gemini thought signatures as CCA, but its replay namespace
662
+ // must stay transport-scoped: a signature minted by one Google backend must never be sent to
663
+ // another merely because the public model id and first prompt happen to match.
664
+ let vertexReplayModel: string | undefined;
665
+ let vertexReplaySession: string | undefined;
279
666
  let restoreGoogleToolName = (name: string): string => name;
667
+ let lastInjectedCallIds: string[] = [];
668
+ let lastReasoningReplayScope: OcxParsedRequest["_reasoningReplayScope"];
669
+
670
+ // Conservative batch invalidation: upstream Gemini/Antigravity errors (e.g.
671
+ // "Function call is missing a thought_signature in functionCall parts") do not specify which
672
+ // specific call_id was rejected. When a request containing replayed signatures is rejected,
673
+ // we evict all callIds injected in that turn (lastInjectedCallIds) from the durable store
674
+ // and clear the session replay cache, preventing poisoned-signature loops while allowing
675
+ // subsequent turns to re-accumulate valid signatures. Unrelated calls from other turns remain intact.
676
+ //
677
+ // Memory-cache clearing stays broad (any invalid-argument/signature error can poison the
678
+ // session replay cache), but durable-store eviction is intentionally narrower: it only runs
679
+ // when the error text explicitly mentions a signature, so a generic tool-schema
680
+ // INVALID_ARGUMENT does not destroy valid durable signatures.
681
+ function handleSignatureRejection(errorMessage?: string) {
682
+ const replayModel = provider.googleMode === "cloud-code-assist" ? antigravityModel : vertexReplayModel;
683
+ const replaySession = provider.googleMode === "cloud-code-assist" ? antigravitySession : vertexReplaySession;
684
+ const text = errorMessage ?? "";
685
+ const isInvalidArgument = /invalid_argument|invalid argument/i.test(text);
686
+ const isSignatureError = /signature|thought_signature|thoughtSignature/i.test(text);
687
+ // The in-memory Antigravity replay cache only exists for CCA/Vertex, so clearing it stays
688
+ // scoped to those modes (replayModel/replaySession are undefined elsewhere anyway).
689
+ if (
690
+ (provider.googleMode === "cloud-code-assist" || provider.googleMode === "vertex")
691
+ && replayModel && replaySession && (isInvalidArgument || isSignatureError)
692
+ ) {
693
+ clearAntigravityReplay(replayModel, replaySession);
694
+ }
695
+ // The DURABLE store is not mode-scoped: signatures are remembered through
696
+ // rememberAndSerializeExtraContent and read back by lookupReplayThoughtSignature on every
697
+ // Google mode, including AI Studio. Gating eviction on CCA/Vertex therefore left AI Studio
698
+ // with rejected signatures cached forever, replaying them into every subsequent turn — the
699
+ // store poisons itself and the request keeps failing. Eviction follows the same scope the
700
+ // write does.
701
+ if (isSignatureError) {
702
+ for (const callId of lastInjectedCallIds) {
703
+ forgetThoughtSignatureForReplay(callId, lastReasoningReplayScope);
704
+ }
705
+ }
706
+ }
707
+
280
708
  return {
281
709
  name: "google",
282
710
 
283
- // Vertex + Antigravity get Kiro-style retry/timeout + classified, redacted errors. AI-Studio
284
- // Gemini keeps the default server fetch path (fetchResponse stays undefined so server.ts falls back).
711
+ // Vertex + Antigravity get Kiro-style retry/timeout + classified, redacted errors.
712
+ // Direct AI-Studio uses the canonical server transport (fetchWithTransientRetry), which
713
+ // retries transient 5xx responses through providerFetch while preserving multi-key pool
714
+ // 429 rotation and raw error formatting.
285
715
  ...(provider.googleMode === "vertex" || provider.googleMode === "cloud-code-assist"
286
716
  ? {
287
717
  fetchResponse: (request: AdapterRequest, ctx?: AdapterFetchContext): Promise<Response> =>
@@ -292,24 +722,52 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
292
722
  : {}),
293
723
 
294
724
  async buildRequest(parsed: OcxParsedRequest) {
295
- const { systemInstruction, contents } = messagesToGeminiFormat(parsed);
725
+ const routedModelId = provider.googleMode === "cloud-code-assist"
726
+ ? resolveAntigravityEffortWireModel(
727
+ parsed.modelId,
728
+ mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning),
729
+ provider.baseUrl,
730
+ ).wireModelId
731
+ : provider.googleMode === "vertex"
732
+ ? parsed.modelId
733
+ : resolveDirectGeminiWireModelId(parsed.modelId, provider.directGeminiWireRenames !== false);
734
+ // AI Studio's `-tiered` spelling is wire-only; CCA aliases may migrate to another generation.
735
+ const identityModelId = provider.googleMode === "cloud-code-assist" ? routedModelId : parsed.modelId;
736
+ const { systemInstruction, contents, replayedCallIds } = messagesToGeminiFormat(parsed, identityModelId);
737
+ lastInjectedCallIds = [...replayedCallIds];
738
+ lastReasoningReplayScope = parsed._reasoningReplayScope;
296
739
  const tools = toolsToGeminiFormat(parsed);
297
740
 
298
741
  const body: Record<string, unknown> = { contents };
299
742
  if (systemInstruction) body.systemInstruction = systemInstruction;
300
743
  if (tools) body.tools = tools;
744
+ // Only meaningful with declarations on the wire: mode ANY with an empty
745
+ // catalog is a guaranteed upstream 400.
746
+ const toolConfig = tools ? toolChoiceToGeminiToolConfig(parsed) : undefined;
747
+ if (toolConfig) body.toolConfig = toolConfig;
301
748
 
302
749
  const generationConfig: Record<string, unknown> = {};
303
- if (parsed.options.maxOutputTokens) generationConfig.maxOutputTokens = parsed.options.maxOutputTokens;
750
+ const clampedMaxOutputTokens = clampGoogleMaxOutputTokens(identityModelId, parsed.options.maxOutputTokens);
751
+ if (clampedMaxOutputTokens !== undefined) generationConfig.maxOutputTokens = clampedMaxOutputTokens;
304
752
  if (parsed.options.temperature !== undefined) generationConfig.temperature = parsed.options.temperature;
305
753
  if (parsed.options.topP !== undefined) generationConfig.topP = parsed.options.topP;
306
754
  if (parsed.options.stopSequences) generationConfig.stopSequences = parsed.options.stopSequences;
307
- const directFlashThinking = provider.googleMode !== "vertex"
308
- && provider.googleMode !== "cloud-code-assist"
309
- && (parsed.modelId === "gemini-3.5-flash" || parsed.modelId === "gemini-3.6-flash")
755
+ // Effort thinkingLevel follows the configured ladder: any model advertising reasoning
756
+ // efforts (registry preset or user config) sends the mapped level, so a picker-selected
757
+ // effort actually reaches the wire (gemini-3.1-pro-preview ships a ladder). The original
758
+ // gemini-3.5/3.6-flash direct-mode slice stays hardcoded so unladdered configs keep their
759
+ // current behavior; Vertex participates only through an explicitly configured ladder (the
760
+ // seed google-vertex entry ships none). Image models are excluded — thinkingConfig would
761
+ // suppress the responseModalities fallback below. CCA maps effort on its envelope path.
762
+ const thinkingEligible = provider.googleMode !== "cloud-code-assist"
763
+ && !isImageCapableModel(parsed.modelId)
764
+ && (configuredReasoningEfforts(provider, parsed.modelId) !== undefined
765
+ || (provider.googleMode !== "vertex"
766
+ && (parsed.modelId === "gemini-3.5-flash" || parsed.modelId === "gemini-3.6-flash")));
767
+ const thinkingLevel = thinkingEligible
310
768
  ? mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning)
311
769
  : undefined;
312
- if (directFlashThinking) generationConfig.thinkingConfig = { thinkingLevel: directFlashThinking };
770
+ if (thinkingLevel) generationConfig.thinkingConfig = { thinkingLevel };
313
771
  if (!generationConfig.thinkingConfig && isImageCapableModel(parsed.modelId)) {
314
772
  generationConfig.responseModalities = ["TEXT", "IMAGE"];
315
773
  }
@@ -331,7 +789,11 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
331
789
  if (!project) throw new Error("Antigravity requires a discovered Cloud Code Assist project id (re-run `ocx login google-antigravity`).");
332
790
  const sessionId = antigravitySessionId(parsed);
333
791
  const mappedEffort = mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning);
334
- const { wireModelId, thinkingLevel } = resolveAntigravityEffortWireModel(parsed.modelId, mappedEffort);
792
+ const { wireModelId, thinkingLevel } = resolveAntigravityEffortWireModel(
793
+ parsed.modelId,
794
+ mappedEffort,
795
+ provider.baseUrl,
796
+ );
335
797
  antigravityModel = wireModelId;
336
798
  antigravitySession = sessionId;
337
799
  // Effort → thinkingConfig for CCA (CLIProxyAPI proven: request.generationConfig.thinkingConfig).
@@ -349,6 +811,12 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
349
811
  const draftRequest: Record<string, unknown> = { ...body, sessionId };
350
812
  // Claude-on-Antigravity forces VALIDATED function calling (the real client always sets it).
351
813
  if (/claude/i.test(wireModelId)) {
814
+ // VALIDATED would defeat a client's tool_choice "none": honor it by dropping the
815
+ // declarations instead, the wire shape of a tool-less Claude turn.
816
+ if (parsed.options.toolChoice === "none") {
817
+ delete draftRequest.tools;
818
+ delete draftRequest.toolConfig;
819
+ }
352
820
  const existing = (draftRequest.toolConfig ?? {}) as Record<string, unknown>;
353
821
  const fcc = (existing.functionCallingConfig ?? {}) as Record<string, unknown>;
354
822
  draftRequest.toolConfig = { ...existing, functionCallingConfig: { ...fcc, mode: "VALIDATED" } };
@@ -364,11 +832,26 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
364
832
  } else {
365
833
  sanitizeAntigravityClaudeSignatures(contents);
366
834
  }
835
+ // After replay, not instead of it: a real signature always wins, and the sentinel only
836
+ // fills a first functionCall that replay could not sign. Outside the cache branch too,
837
+ // because the turn still needs a signature when no session was ever recorded.
838
+ applyAntigravityThoughtSignatureFallback(wireModelId, contents);
839
+ // Claude-on-Antigravity rejects assistant-tail (model-tail in Gemini terms) histories
840
+ // as prefill: "This model does not support assistant message prefill. The conversation
841
+ // must end with a user message." Context compaction, previous_response_id expansion,
842
+ // and interrupted-turn replay can all produce a model-tail history. Append a user
843
+ // "(continue)" nudge, mirroring the anthropic adapter's tail guard (src/adapters/anthropic.ts).
844
+ if (/claude/i.test(wireModelId)) {
845
+ const last = contents.length > 0 ? contents[contents.length - 1] as { role?: string } : undefined;
846
+ if (!last || last.role === "model") {
847
+ contents.push({ role: "user", parts: [{ text: "(continue)" }] });
848
+ }
849
+ }
367
850
  }
368
851
  const envelope = {
369
852
  model: wireModelId,
370
853
  // The envelope's `userAgent` field is a protocol constant ("antigravity"), distinct from
371
- // the HTTP `User-Agent` header (the real CLI UA). CLIProxyAPI `geminiToAntigravity` hardcodes
854
+ // the HTTP `User-Agent` header (the real IDE UA). CLIProxyAPI `geminiToAntigravity` hardcodes
372
855
  // the body field; only the header carries the versioned client string.
373
856
  userAgent: "antigravity",
374
857
  requestType: "agent",
@@ -384,6 +867,24 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
384
867
  if (provider.googleMode === "vertex") {
385
868
  const compiled = compileGoogleWireBody(body);
386
869
  restoreGoogleToolName = compiled.restoreToolName;
870
+ const vertexProject = provider.project || process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT || "api-key";
871
+ const vertexLocation = provider.location || process.env.GOOGLE_CLOUD_LOCATION || "global";
872
+ vertexReplayModel = `vertex:${vertexProject}:${vertexLocation}:${parsed.modelId}`;
873
+ vertexReplaySession = vertexReplaySessionId(parsed);
874
+ // Compile names before replay so the cache matches the exact provider-visible
875
+ // functionCall identity. This is the same bounded TTL/LRU store used by CCA, with the
876
+ // transport prefix above preventing cross-backend signature reuse (#1254).
877
+ if (Array.isArray((compiled.body as { contents?: unknown[] }).contents)) {
878
+ applyAntigravityReplay(
879
+ vertexReplayModel,
880
+ vertexReplaySession,
881
+ (compiled.body as { contents: unknown[] }).contents,
882
+ );
883
+ applyAntigravityThoughtSignatureFallback(
884
+ vertexReplayModel,
885
+ (compiled.body as { contents: unknown[] }).contents,
886
+ );
887
+ }
387
888
  // Vertex AI: project/location endpoint with GCP ADC, or x-goog-api-key fast path.
388
889
  const apiKey = resolveVertexApiKey(provider.apiKey);
389
890
  if (apiKey) {
@@ -395,6 +896,8 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
395
896
  if (!project) throw new Error("Vertex AI requires a project id (provider.project or GOOGLE_CLOUD_PROJECT/GCLOUD_PROJECT).");
396
897
  const location = provider.location || process.env.GOOGLE_CLOUD_LOCATION;
397
898
  if (!location) throw new Error("Vertex AI requires a location (provider.location or GOOGLE_CLOUD_LOCATION).");
899
+ const locationError = googleVertexLocationConfigError(location);
900
+ if (locationError) throw new Error(locationError);
398
901
  const host = location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com`;
399
902
  const url = `https://${host}/v1/projects/${project}/locations/${location}/publishers/google/models/${parsed.modelId}:${method}${streamParam}`;
400
903
  const token = await getVertexAccessToken();
@@ -403,7 +906,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
403
906
  }
404
907
 
405
908
  // ai-studio (default): Generative Language API + x-goog-api-key.
406
- const url = `${provider.baseUrl}/v1beta/models/${parsed.modelId}:${method}${streamParam}`;
909
+ const url = `${provider.baseUrl}/v1beta/models/${routedModelId}:${method}${streamParam}`;
407
910
  const apiKey = provider.apiKey?.trim();
408
911
  if (!apiKey) throw new Error("google (AI Studio) requires a non-empty API key");
409
912
  headers["x-goog-api-key"] = apiKey;
@@ -433,6 +936,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
433
936
  let lastFinishReason: string | undefined;
434
937
  let sawAnyFrame = false;
435
938
  let sawTerminalSignal = false;
939
+ let pendingStreamThoughtSig: string | undefined;
436
940
 
437
941
  const handleDataLine = async function* (line: string): AsyncGenerator<AdapterEvent, "continue" | "content" | "terminate"> {
438
942
  const payload = line.slice(5).trim();
@@ -443,24 +947,32 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
443
947
  }
444
948
  let emittedContentEvent = false;
445
949
 
446
- let chunk: Record<string, unknown>;
950
+ let parsed: unknown;
447
951
  try {
448
- chunk = JSON.parse(payload);
952
+ parsed = JSON.parse(payload);
449
953
  } catch {
450
954
  yield { type: "error", message: "malformed upstream SSE data frame" };
451
955
  return "terminate";
452
956
  }
957
+ // `JSON.parse("null")` returns null rather than throwing, so the catch above cannot cover
958
+ // it and the `chunk.error` read below crashed the stream (see openai-chat.ts). Skip such a
959
+ // frame rather than terminating, for the reason given there: it is padding between real
960
+ // frames, not a broken stream. Deliberately returns BEFORE `sawAnyFrame`, so a stream made
961
+ // only of non-record frames still fails the terminal-signal check below instead of
962
+ // completing empty. An unparseable frame stays terminal.
963
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
964
+ return "continue";
965
+ }
966
+ const chunk = parsed as Record<string, unknown>;
453
967
  sawAnyFrame = true;
454
968
 
455
969
  // Inline provider error inside a 200 stream → terminal error (see openai-chat.ts).
456
970
  if (chunk.error) {
457
971
  const err = chunk.error as { message?: string } | undefined;
458
972
  // Clear-on-invalid: a signature rejection means our replayed thoughtSignatures are stale.
459
- // Drop the cache entry so the next turn starts clean instead of re-injecting a bad sig.
460
- if (provider.googleMode === "cloud-code-assist" && antigravityModel && antigravitySession
461
- && /signature|invalid_argument|invalid argument/i.test(err?.message ?? "")) {
462
- clearAntigravityReplay(antigravityModel, antigravitySession);
463
- }
973
+ // Drop the cache entry and durable store entry for rejected calls so the next turn
974
+ // starts clean instead of re-injecting a bad sig.
975
+ handleSignatureRejection(err?.message);
464
976
  yield { type: "error", message: err?.message ?? "upstream error" };
465
977
  return "terminate";
466
978
  }
@@ -484,24 +996,91 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
484
996
  pendingUsage = usageFromGemini(usageMeta);
485
997
  sawTerminalSignal = true;
486
998
  }
487
- const candidates = root.candidates as { content?: { parts?: unknown[] }; finishReason?: string }[] | undefined;
488
- if (!candidates?.length) return "continue";
999
+ const rawCandidates = root.candidates;
1000
+ // `null` is an absence encoding, not corruption, and terminating on it is the #1219
1001
+ // failure mode one rung in: a `{"candidates":null}` frame arriving between a content
1002
+ // delta and the finish chunk killed a turn whose answer had already fully arrived. An
1003
+ // absent key and an empty array are already skipped here; `null` joins them. A non-null
1004
+ // non-array container is still claimed structure the parser cannot read, and stays
1005
+ // terminal.
1006
+ if (rawCandidates === undefined || rawCandidates === null) return "continue";
1007
+ if (!Array.isArray(rawCandidates)) {
1008
+ yield invalidGoogleShapeEvent({
1009
+ reason: "candidates_not_array",
1010
+ valueType: googleStructuralValueType(rawCandidates),
1011
+ });
1012
+ return "terminate";
1013
+ }
1014
+ if (rawCandidates.length === 0) return "continue";
1015
+ const rawCandidate = rawCandidates[0];
1016
+ if (!isGoogleRecord(rawCandidate)) {
1017
+ // Unlike a root `data: null` keepalive, this is a claimed response candidate. Treat it
1018
+ // as terminal protocol corruption so the turn cannot complete after silently losing
1019
+ // a candidate or tool call (#1325).
1020
+ yield invalidGoogleShapeEvent({
1021
+ reason: "candidate_not_object",
1022
+ valueType: googleStructuralValueType(rawCandidate),
1023
+ });
1024
+ return "terminate";
1025
+ }
1026
+ const candidate = rawCandidate as {
1027
+ content?: unknown;
1028
+ finishReason?: string;
1029
+ };
489
1030
 
490
- if (typeof candidates[0].finishReason === "string" && candidates[0].finishReason) {
491
- lastFinishReason = candidates[0].finishReason;
1031
+ if (typeof candidate.finishReason === "string" && candidate.finishReason) {
1032
+ lastFinishReason = candidate.finishReason;
492
1033
  sawTerminalSignal = true;
493
1034
  }
494
1035
 
495
- const parts = candidates[0].content?.parts as { text?: string; functionCall?: { name: string; args: unknown } }[] | undefined;
496
- // Antigravity reasoning-replay: record thoughtSignatures from the model parts for the next turn.
497
- if (provider.googleMode === "cloud-code-assist" && parts && antigravityModel && antigravitySession) {
498
- observeAntigravityReplay(antigravityModel, antigravitySession, parts as unknown[]);
1036
+ // One rung below the candidate guard above, same rule: this is claimed content, not
1037
+ // padding, so it fails closed rather than being iterated or silently dropped (#1325).
1038
+ const rawContent: unknown = candidate.content;
1039
+ const invalidContent = diagnoseGoogleContent(rawContent);
1040
+ if (invalidContent) {
1041
+ yield invalidGoogleShapeEvent(invalidContent);
1042
+ return "terminate";
1043
+ }
1044
+ const rawParts: unknown = isGoogleRecord(rawContent) ? rawContent.parts : undefined;
1045
+ let parts: GoogleResponsePart[] | undefined;
1046
+ if (rawParts !== undefined && rawParts !== null) {
1047
+ const invalidParts = diagnoseGoogleParts(rawParts);
1048
+ if (invalidParts) {
1049
+ yield invalidGoogleShapeEvent(invalidParts);
1050
+ return "terminate";
1051
+ }
1052
+ parts = rawParts as GoogleResponsePart[];
1053
+ const invalidFunctionCall = diagnoseGoogleFunctionCalls(parts);
1054
+ if (invalidFunctionCall) {
1055
+ yield invalidGoogleFunctionCallEvent(invalidFunctionCall);
1056
+ return "terminate";
1057
+ }
1058
+ }
1059
+ // Record Gemini thought signatures for the next stateless tool-result turn. Vertex and
1060
+ // Antigravity use separate model namespaces so opaque provider state cannot cross routes.
1061
+ const replayModel = provider.googleMode === "cloud-code-assist" ? antigravityModel : vertexReplayModel;
1062
+ const replaySession = provider.googleMode === "cloud-code-assist" ? antigravitySession : vertexReplaySession;
1063
+ if ((provider.googleMode === "cloud-code-assist" || provider.googleMode === "vertex")
1064
+ && parts && replayModel && replaySession) {
1065
+ // Observation may scan the whole frame, so use it only for replay-cache side effects.
1066
+ // The source-order loop below exclusively owns stream carry and cannot pair backwards.
1067
+ observeAntigravityReplay(
1068
+ replayModel,
1069
+ replaySession,
1070
+ parts as unknown[],
1071
+ pendingStreamThoughtSig,
1072
+ );
499
1073
  }
500
1074
  if (parts) {
501
1075
  for (const part of parts) {
502
- if (part.text) {
1076
+ const sig = googlePartThoughtSignature(part);
1077
+ if (part.thought === true && sig && isLikelyRealThoughtSignature(sig)) {
1078
+ pendingStreamThoughtSig = sig;
1079
+ }
1080
+ const textEvent = googlePartTextEvent(part);
1081
+ if (textEvent) {
503
1082
  emittedContentEvent = true;
504
- yield { type: "text_delta", text: part.text };
1083
+ yield textEvent;
505
1084
  }
506
1085
  const inline = (part as { inlineData?: { mimeType?: string; data?: string } }).inlineData;
507
1086
  if (inline && typeof inline.data === "string") {
@@ -518,12 +1097,19 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
518
1097
  }
519
1098
  }
520
1099
  }
521
- if (part.functionCall) {
1100
+ const functionCall = googleFunctionCall(part);
1101
+ if (functionCall) {
522
1102
  const id = `call_${crypto.randomUUID().slice(0, 8)}`;
523
1103
  toolCallsStarted++;
524
1104
  emittedContentEvent = true;
525
- yield { type: "tool_call_start", id, name: restoreGoogleToolName(part.functionCall.name) };
526
- yield { type: "tool_call_delta", arguments: JSON.stringify(part.functionCall.args ?? {}) };
1105
+ const restoredName = restoreGoogleToolName(functionCall.name);
1106
+ yield {
1107
+ type: "tool_call_start",
1108
+ id,
1109
+ name: restoredName,
1110
+ ...googleToolCallMetadataFromPart(part, pendingStreamThoughtSig),
1111
+ };
1112
+ yield { type: "tool_call_delta", arguments: JSON.stringify(functionCall.args ?? {}) };
527
1113
  yield { type: "tool_call_end" };
528
1114
  }
529
1115
  }
@@ -587,7 +1173,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
587
1173
  // Fail-closed: a turn cut off mid tool call (MAX_TOKENS / MALFORMED_FUNCTION_CALL) surfaces
588
1174
  // an error instead of a silently-incomplete done. Mirrors kiro-truncation.
589
1175
  if ((provider.googleMode === "vertex" || provider.googleMode === "cloud-code-assist")
590
- && toolCallsStarted > 0 && isVertexTruncationReason(lastFinishReason)) {
1176
+ && isVertexTruncatedTurn(lastFinishReason, toolCallsStarted)) {
591
1177
  yield { type: "error", message: vertexTruncationErrorMessage(lastFinishReason) };
592
1178
  return;
593
1179
  }
@@ -673,7 +1259,18 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
673
1259
  let raw: Record<string, unknown>;
674
1260
  let rawBytes = 0;
675
1261
  try {
676
- raw = JSON.parse(rawText) as Record<string, unknown>;
1262
+ const parsedRaw: unknown = JSON.parse(rawText);
1263
+ // `JSON.parse("null")` returns null instead of throwing, so the catch below cannot see it
1264
+ // and the `raw.error` read crashed the turn — #1219 at the buffered body root, which #1240
1265
+ // never reached because that audit swept SSE frame parsers only. There is no next frame to
1266
+ // recover into here, so unlike a stream frame this fails closed, matching the
1267
+ // unparseable-body branch just below and the buffered candidate guards added in #2232.
1268
+ if (!isGoogleRecord(parsedRaw)) {
1269
+ budget.releaseRetained(rawTextBytes, { kind: "retained_collectors" });
1270
+ const valueType = googleStructuralValueType(parsedRaw);
1271
+ return [{ type: "error", message: `google response was not a JSON object (${valueType})` }];
1272
+ }
1273
+ raw = parsedRaw;
677
1274
  rawBytes = new TextEncoder().encode(JSON.stringify(raw)).byteLength;
678
1275
  const rawReservation = budget.reserveTransient(rawBytes, { kind: "retained_collectors" });
679
1276
  rawReservation.commitRetained();
@@ -690,6 +1287,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
690
1287
  };
691
1288
  if (raw.error) {
692
1289
  const err = raw.error as { message?: string };
1290
+ handleSignatureRejection(err.message);
693
1291
  return finish([{ type: "error", message: err.message ?? "upstream error" }]);
694
1292
  }
695
1293
  // Antigravity (CCA) nests the standard Gemini payload under `response`; unwrap it.
@@ -703,19 +1301,60 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
703
1301
  }
704
1302
  const events: AdapterEvent[] = [];
705
1303
 
706
- const candidates = json.candidates as { content?: { parts?: { text?: string; functionCall?: { name: string; args: unknown } }[] }; finishReason?: string }[] | undefined;
1304
+ const rawCandidates: unknown = json.candidates;
1305
+ // Parity with the streaming path, which has rejected a non-array `candidates` since #1332.
1306
+ // Buffered accepted `"abc"` outright (`"abc".length` is 3, so the emptiness check below
1307
+ // passed and `candidates[0]` was the character `"a"`), and reported `{}`/`5` as an absent
1308
+ // candidate list rather than a malformed one.
1309
+ if (rawCandidates !== undefined && rawCandidates !== null && !Array.isArray(rawCandidates)) {
1310
+ return finish([invalidGoogleShapeEvent({
1311
+ reason: "candidates_not_array",
1312
+ valueType: googleStructuralValueType(rawCandidates),
1313
+ })]);
1314
+ }
1315
+ const candidates = rawCandidates as { finishReason?: string }[] | undefined;
707
1316
  if (!candidates?.length) {
708
1317
  return finish([{ type: "error", message: "google response contained no candidates" }]);
709
1318
  }
1319
+ const rawCandidate: unknown = candidates[0];
1320
+ if (!isGoogleRecord(rawCandidate)) {
1321
+ // The streaming parser already treats this as terminal protocol corruption (#1325/#1332).
1322
+ // Buffered returned a bare `done`, so a claimed-but-malformed candidate was reported to
1323
+ // the caller as a successful empty turn.
1324
+ return finish([invalidGoogleShapeEvent({
1325
+ reason: "candidate_not_object",
1326
+ valueType: googleStructuralValueType(rawCandidate),
1327
+ })]);
1328
+ }
1329
+ const candidate = rawCandidate as { content?: unknown; finishReason?: string };
710
1330
  let toolCallsStarted = 0;
711
1331
  const imageBudget = createImageBudget();
712
- if (candidates?.[0]?.content?.parts) {
713
- // Non-streaming CCA: observe thoughtSignatures for the next turn, same as the stream path.
714
- if (provider.googleMode === "cloud-code-assist" && antigravityModel && antigravitySession) {
715
- observeAntigravityReplay(antigravityModel, antigravitySession, candidates[0].content.parts as unknown[]);
1332
+ const rawContent: unknown = candidate.content;
1333
+ const invalidContent = diagnoseGoogleContent(rawContent);
1334
+ if (invalidContent) return finish([invalidGoogleShapeEvent(invalidContent)]);
1335
+ const rawParts: unknown = isGoogleRecord(rawContent) ? rawContent.parts : undefined;
1336
+ if (rawParts !== undefined && rawParts !== null) {
1337
+ const invalidParts = diagnoseGoogleParts(rawParts);
1338
+ if (invalidParts) return finish([invalidGoogleShapeEvent(invalidParts)]);
1339
+ const parts = rawParts as GoogleResponsePart[];
1340
+ const invalidFunctionCall = diagnoseGoogleFunctionCalls(parts);
1341
+ if (invalidFunctionCall) return finish([invalidGoogleFunctionCallEvent(invalidFunctionCall)]);
1342
+ // Non-streaming Google-family response: observe thought signatures for the next turn,
1343
+ // using the same transport-scoped namespace as the streaming path.
1344
+ const replayModel = provider.googleMode === "cloud-code-assist" ? antigravityModel : vertexReplayModel;
1345
+ const replaySession = provider.googleMode === "cloud-code-assist" ? antigravitySession : vertexReplaySession;
1346
+ if ((provider.googleMode === "cloud-code-assist" || provider.googleMode === "vertex")
1347
+ && replayModel && replaySession) {
1348
+ observeAntigravityReplay(replayModel, replaySession, parts as unknown[]);
716
1349
  }
717
- for (const part of candidates[0].content.parts) {
718
- if (part.text) events.push({ type: "text_delta", text: part.text });
1350
+ let pendingThoughtSig: string | undefined;
1351
+ for (const part of parts) {
1352
+ const sig = googlePartThoughtSignature(part);
1353
+ if (part.thought === true && sig && isLikelyRealThoughtSignature(sig)) {
1354
+ pendingThoughtSig = sig;
1355
+ }
1356
+ const textEvent = googlePartTextEvent(part);
1357
+ if (textEvent) events.push(textEvent);
719
1358
  const inline = (part as { inlineData?: { mimeType?: string; data?: string } }).inlineData;
720
1359
  if (inline && typeof inline.data === "string") {
721
1360
  if (inline.data.length > MAX_ENCODED_BYTES_PER_IMAGE) {
@@ -730,11 +1369,17 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
730
1369
  }
731
1370
  }
732
1371
  }
733
- if (part.functionCall) {
1372
+ const functionCall = googleFunctionCall(part);
1373
+ if (functionCall) {
734
1374
  const id = `call_${crypto.randomUUID().slice(0, 8)}`;
735
1375
  toolCallsStarted++;
736
- events.push({ type: "tool_call_start", id, name: restoreGoogleToolName(part.functionCall.name) });
737
- events.push({ type: "tool_call_delta", arguments: JSON.stringify(part.functionCall.args ?? {}) });
1376
+ events.push({
1377
+ type: "tool_call_start",
1378
+ id,
1379
+ name: restoreGoogleToolName(functionCall.name),
1380
+ ...googleToolCallMetadataFromPart(part, pendingThoughtSig),
1381
+ });
1382
+ events.push({ type: "tool_call_delta", arguments: JSON.stringify(functionCall.args ?? {}) });
738
1383
  events.push({ type: "tool_call_end" });
739
1384
  }
740
1385
  }
@@ -743,14 +1388,25 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
743
1388
  // Fail-closed truncation, same as the stream path: a non-stream turn cut off mid tool call
744
1389
  // (MAX_TOKENS / MALFORMED_FUNCTION_CALL) surfaces an error instead of a silent done.
745
1390
  if ((provider.googleMode === "vertex" || provider.googleMode === "cloud-code-assist")
746
- && toolCallsStarted > 0 && isVertexTruncationReason(candidates?.[0]?.finishReason)) {
747
- return finish([{ type: "error", message: vertexTruncationErrorMessage(candidates?.[0]?.finishReason) }]);
1391
+ && isVertexTruncatedTurn(candidate.finishReason, toolCallsStarted)) {
1392
+ return finish([{ type: "error", message: vertexTruncationErrorMessage(candidate.finishReason) }]);
748
1393
  }
749
1394
 
750
1395
  const usage = json.usageMetadata as Record<string, number> | undefined;
1396
+ // Mirror the streaming path: a buffered turn cut off by the token limit or a content filter
1397
+ // must carry its stop reason, or the bridge sees a clean `done` and reports the truncated
1398
+ // turn as completed — and, on a compaction turn, installs the half-written summary as
1399
+ // replacement history (#422).
1400
+ const finishReason = candidate.finishReason as string | undefined;
1401
+ const stopReason = finishReason === "MAX_TOKENS"
1402
+ ? "max_tokens"
1403
+ : ["SAFETY", "RECITATION", "BLOCKLIST", "PROHIBITED_CONTENT", "SPII"].includes(finishReason ?? "")
1404
+ ? "content_filter"
1405
+ : undefined;
751
1406
  events.push({
752
1407
  type: "done",
753
1408
  usage: usageFromGemini(usage),
1409
+ ...(stopReason ? { stopReason } : {}),
754
1410
  });
755
1411
  return finish(events);
756
1412
  },