@iislee/opencodex 2.11.0 → 2.31.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 (675) hide show
  1. package/AGENTS_INSTALL.md +109 -0
  2. package/README.md +114 -19
  3. package/bin/ocx.mjs +164 -36
  4. package/bin/package-main.mjs +1 -1
  5. package/gui/dist/assets/index-BF38heuV.js +104 -0
  6. package/gui/dist/assets/index-DMiI18Kv.css +1 -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 +19 -10
  42. package/src/adapters/anthropic-output-schema.ts +137 -0
  43. package/src/adapters/anthropic.ts +376 -52
  44. package/src/adapters/base.ts +54 -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 +601 -0
  48. package/src/adapters/cursor/checkpoint-store.ts +303 -0
  49. package/src/adapters/cursor/cursor-errors.ts +129 -6
  50. package/src/adapters/cursor/discovery.ts +61 -4
  51. package/src/adapters/cursor/effort-map.ts +27 -3
  52. package/src/adapters/cursor/framing.ts +39 -0
  53. package/src/adapters/cursor/h2-pool.ts +123 -0
  54. package/src/adapters/cursor/http1-bidi.ts +361 -0
  55. package/src/adapters/cursor/images.ts +704 -0
  56. package/src/adapters/cursor/live-models.ts +174 -58
  57. package/src/adapters/cursor/live-transport.ts +609 -170
  58. package/src/adapters/cursor/native-exec-common.ts +23 -2
  59. package/src/adapters/cursor/native-exec-fs.ts +10 -7
  60. package/src/adapters/cursor/native-exec-network.ts +1 -1
  61. package/src/adapters/cursor/native-exec-shell.ts +0 -1
  62. package/src/adapters/cursor/native-exec.ts +101 -14
  63. package/src/adapters/cursor/protobuf-events.ts +829 -11
  64. package/src/adapters/cursor/protobuf-request.ts +383 -65
  65. package/src/adapters/cursor/request-builder.ts +237 -31
  66. package/src/adapters/cursor/tool-definitions.ts +242 -12
  67. package/src/adapters/cursor/tool-result-normalize.ts +92 -0
  68. package/src/adapters/cursor/transport.ts +22 -0
  69. package/src/adapters/cursor/types.ts +28 -1
  70. package/src/adapters/cursor.ts +133 -7
  71. package/src/adapters/google-antigravity-replay.ts +558 -34
  72. package/src/adapters/google-antigravity-wire.ts +43 -10
  73. package/src/adapters/google-http.ts +38 -10
  74. package/src/adapters/google-tool-schema.ts +80 -15
  75. package/src/adapters/google-truncation.ts +11 -0
  76. package/src/adapters/google.ts +618 -74
  77. package/src/adapters/identity.ts +39 -6
  78. package/src/adapters/kiro-errors.ts +11 -0
  79. package/src/adapters/kiro-events.ts +19 -1
  80. package/src/adapters/kiro-thinking.ts +18 -2
  81. package/src/adapters/kiro-tools.ts +10 -1
  82. package/src/adapters/kiro.ts +92 -54
  83. package/src/adapters/mimo-free.ts +17 -0
  84. package/src/adapters/openai-chat-url.ts +11 -0
  85. package/src/adapters/openai-chat.ts +1311 -336
  86. package/src/adapters/openai-responses-url.ts +16 -0
  87. package/src/adapters/openai-responses.ts +830 -56
  88. package/src/adapters/registry.ts +175 -0
  89. package/src/adapters/responses-tool-schema.ts +67 -0
  90. package/src/adapters/tool-call-id.ts +119 -0
  91. package/src/adapters/tool-catalog-nudge.ts +104 -21
  92. package/src/adapters/xai-web-search.ts +185 -0
  93. package/src/bridge.ts +428 -61
  94. package/src/chat/inbound.ts +43 -19
  95. package/src/chat/outbound.ts +82 -26
  96. package/src/claude/agents-inject.ts +32 -9
  97. package/src/claude/context-windows.ts +21 -5
  98. package/src/claude/desktop-3p.ts +243 -9
  99. package/src/claude/gateway-cache.ts +41 -4
  100. package/src/claude/inbound.ts +72 -3
  101. package/src/claude/model-info.ts +38 -15
  102. package/src/claude/outbound.ts +70 -16
  103. package/src/cli/account-api.ts +35 -1
  104. package/src/cli/account-auth.ts +33 -6
  105. package/src/cli/account-catalog-refresh.ts +14 -0
  106. package/src/cli/account-extended.ts +389 -2
  107. package/src/cli/account-main.ts +317 -0
  108. package/src/cli/account.ts +30 -6
  109. package/src/cli/agent.ts +45 -1
  110. package/src/cli/claude-agent-startup-sync.ts +73 -0
  111. package/src/cli/claude-desktop.ts +29 -4
  112. package/src/cli/claude.ts +103 -34
  113. package/src/cli/codex-log-guard-doctor.ts +103 -0
  114. package/src/cli/codex-shim-autorestore.ts +2 -0
  115. package/src/cli/codex-shim-readiness.ts +76 -0
  116. package/src/cli/combo.ts +8 -0
  117. package/src/cli/config-command.ts +74 -10
  118. package/src/cli/dispatch.ts +593 -0
  119. package/src/cli/doctor.ts +315 -43
  120. package/src/cli/ensure-desired-integrations.ts +152 -0
  121. package/src/cli/export-command.ts +46 -20
  122. package/src/cli/help.ts +20 -271
  123. package/src/cli/index.ts +378 -519
  124. package/src/cli/init.ts +4 -17
  125. package/src/cli/integrations.ts +120 -2
  126. package/src/cli/lab.ts +607 -0
  127. package/src/cli/launcher-context.ts +77 -0
  128. package/src/cli/minimax.ts +497 -0
  129. package/src/cli/models-runtime.ts +35 -2
  130. package/src/cli/models.ts +100 -14
  131. package/src/cli/observe.ts +92 -3
  132. package/src/cli/opencode.ts +4 -2
  133. package/src/cli/provider-runtime.ts +18 -1
  134. package/src/cli/provider.ts +24 -3
  135. package/src/cli/ready.ts +301 -0
  136. package/src/cli/registry.ts +437 -0
  137. package/src/cli/root.ts +86 -0
  138. package/src/cli/route-policy.ts +92 -0
  139. package/src/cli/runtime-api.ts +6 -3
  140. package/src/cli/star-prompt.ts +71 -15
  141. package/src/cli/status.ts +10 -3
  142. package/src/cli/system-restart-client.ts +146 -0
  143. package/src/cli/tray-proxy.ts +153 -6
  144. package/src/cli/v2.ts +105 -10
  145. package/src/cli.ts +1 -1
  146. package/src/clients/config-export.ts +1358 -21
  147. package/src/codex/account-label.ts +14 -1
  148. package/src/codex/account-lifecycle.ts +130 -13
  149. package/src/codex/account-namespaces.ts +49 -3
  150. package/src/codex/account-priority.ts +83 -0
  151. package/src/codex/account-store.ts +29 -2
  152. package/src/codex/account-usability.ts +25 -2
  153. package/src/codex/admission.ts +256 -0
  154. package/src/codex/affinity-debug.ts +162 -0
  155. package/src/codex/app-server-processes.ts +493 -106
  156. package/src/codex/app-server-restart-service.ts +232 -0
  157. package/src/codex/auth-api.ts +849 -242
  158. package/src/codex/auth-collision.ts +5 -3
  159. package/src/codex/auth-context.ts +345 -32
  160. package/src/codex/autostart-health.ts +8 -1
  161. package/src/codex/catalog/account-models.ts +67 -0
  162. package/src/codex/catalog/aggregation.ts +68 -10
  163. package/src/codex/catalog/bundled.ts +331 -33
  164. package/src/codex/catalog/effort.ts +121 -30
  165. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  166. package/src/codex/catalog/kinds.ts +2 -0
  167. package/src/codex/catalog/metadata.ts +529 -45
  168. package/src/codex/catalog/native-models.ts +72 -0
  169. package/src/codex/catalog/parsing.ts +224 -30
  170. package/src/codex/catalog/provider-fetch.ts +1460 -134
  171. package/src/codex/catalog/sync.ts +1449 -186
  172. package/src/codex/catalog-admission.ts +199 -0
  173. package/src/codex/catalog-refresh-status.ts +105 -0
  174. package/src/codex/catalog-write-serialization.ts +242 -0
  175. package/src/codex/catalog.ts +6 -3
  176. package/src/codex/codex-write-lock.ts +384 -0
  177. package/src/codex/convergence-types.ts +614 -0
  178. package/src/codex/convergence.ts +651 -0
  179. package/src/codex/coordinator-doctor.ts +332 -0
  180. package/src/codex/custom-model-catalog-migration.ts +176 -0
  181. package/src/codex/desired-state.ts +230 -0
  182. package/src/codex/features.ts +636 -39
  183. package/src/codex/generation.ts +202 -0
  184. package/src/codex/history-job.ts +407 -0
  185. package/src/codex/history-lock.ts +242 -0
  186. package/src/codex/history-migration-guardian.ts +26 -20
  187. package/src/codex/history-provider.ts +231 -28
  188. package/src/codex/history-transition.ts +105 -0
  189. package/src/codex/history-worker.ts +220 -0
  190. package/src/codex/inject-coordination.ts +290 -0
  191. package/src/codex/inject.ts +1073 -152
  192. package/src/codex/injected-marker.ts +37 -3
  193. package/src/codex/integration-record.ts +266 -0
  194. package/src/codex/internal/catalog-writer.ts +203 -0
  195. package/src/codex/internal/history-writer.ts +80 -0
  196. package/src/codex/journal.ts +66 -4
  197. package/src/codex/log-guard/inspect.ts +506 -0
  198. package/src/codex/log-guard/lock.ts +150 -0
  199. package/src/codex/log-guard/maintenance.ts +403 -0
  200. package/src/codex/log-guard/path-safety.ts +88 -0
  201. package/src/codex/log-guard/policy.ts +44 -0
  202. package/src/codex/log-guard/processes.ts +205 -0
  203. package/src/codex/log-guard/protection.ts +489 -0
  204. package/src/codex/log-guard/sqlite-errors.ts +9 -0
  205. package/src/codex/main-account-cache.ts +24 -0
  206. package/src/codex/main-account.ts +29 -1
  207. package/src/codex/management-convergence.ts +167 -0
  208. package/src/codex/model-cache.ts +56 -10
  209. package/src/codex/model-entitlements.ts +353 -0
  210. package/src/codex/native-main-admission.ts +47 -0
  211. package/src/codex/native-main-auth-temp.ts +187 -0
  212. package/src/codex/native-main-claim.ts +178 -0
  213. package/src/codex/native-main-lock-file.ts +162 -0
  214. package/src/codex/native-main-owner.ts +329 -0
  215. package/src/codex/native-profile-api.ts +247 -0
  216. package/src/codex/native-profile-manager.ts +1531 -0
  217. package/src/codex/native-profile-processes.ts +121 -0
  218. package/src/codex/native-profile-recovery.ts +99 -0
  219. package/src/codex/native-profile-stage-store.ts +387 -0
  220. package/src/codex/native-profile-startup.ts +492 -0
  221. package/src/codex/native-profile-store.ts +855 -0
  222. package/src/codex/native-profile-types.ts +120 -0
  223. package/src/codex/native-residue.ts +682 -0
  224. package/src/codex/paths.ts +80 -1
  225. package/src/codex/plan-from-token.ts +140 -0
  226. package/src/codex/plan.ts +40 -0
  227. package/src/codex/plugins-doctor.ts +1 -1
  228. package/src/codex/pool-rotation.ts +74 -4
  229. package/src/codex/project-config-warnings.ts +20 -6
  230. package/src/codex/prompt-journal.ts +352 -0
  231. package/src/codex/prompt-layers.ts +967 -0
  232. package/src/codex/prompt-lock.ts +143 -0
  233. package/src/codex/quota-rejection.ts +298 -0
  234. package/src/codex/quota.ts +175 -13
  235. package/src/codex/refresh.ts +11 -2
  236. package/src/codex/reset-credit-recovery.ts +1044 -0
  237. package/src/codex/routing.ts +505 -94
  238. package/src/codex/runtime.ts +159 -38
  239. package/src/codex/shim.ts +1009 -28
  240. package/src/codex/subagent-model-fallback.ts +350 -35
  241. package/src/codex/sync.ts +191 -2
  242. package/src/codex/transition-state.ts +612 -0
  243. package/src/codex/upstream-host-health.ts +368 -0
  244. package/src/codex/user-identity.ts +557 -0
  245. package/src/codex/warmup.ts +187 -81
  246. package/src/codex/write-coordination.ts +114 -0
  247. package/src/combos/failover.ts +20 -0
  248. package/src/combos/index.ts +4 -0
  249. package/src/combos/request.ts +32 -0
  250. package/src/combos/types.ts +81 -9
  251. package/src/config/provider-name.ts +24 -0
  252. package/src/config.ts +1762 -140
  253. package/src/generated/compatibility-version.json +3164 -0
  254. package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +19 -17
  255. package/src/grok/inject.ts +16 -5
  256. package/src/grok/inspect.ts +45 -0
  257. package/src/grok/sync.ts +2 -2
  258. package/src/images/loop.ts +152 -29
  259. package/src/images/plan.ts +23 -13
  260. package/src/integrations/config-io.ts +269 -0
  261. package/src/integrations/journal.ts +315 -0
  262. package/src/integrations/merge.ts +135 -0
  263. package/src/integrations/mutation-flight.ts +71 -0
  264. package/src/integrations/native/ownership-preflight.ts +202 -0
  265. package/src/integrations/omp-yaml-source.ts +358 -0
  266. package/src/integrations/owned-refresh.ts +74 -0
  267. package/src/integrations/ownership.ts +111 -0
  268. package/src/integrations/registry.ts +159 -0
  269. package/src/integrations/serialize.ts +314 -0
  270. package/src/integrations/state.ts +361 -0
  271. package/src/integrations/store.ts +103 -0
  272. package/src/integrations/writer-lock.ts +98 -0
  273. package/src/integrations/writer.ts +691 -0
  274. package/src/lab/artifacts/sanitize.ts +586 -0
  275. package/src/lab/artifacts/secure-fs.ts +475 -0
  276. package/src/lab/artifacts/store.ts +310 -0
  277. package/src/lab/automation/budgets.ts +78 -0
  278. package/src/lab/automation/config-persistence.ts +256 -0
  279. package/src/lab/automation/constants.ts +39 -0
  280. package/src/lab/automation/cooldown.ts +103 -0
  281. package/src/lab/automation/dispatch.ts +211 -0
  282. package/src/lab/automation/index.ts +13 -0
  283. package/src/lab/automation/orchestrator.ts +499 -0
  284. package/src/lab/automation/persistence.ts +512 -0
  285. package/src/lab/automation/planner.ts +371 -0
  286. package/src/lab/automation/policy.ts +136 -0
  287. package/src/lab/automation/queue.ts +191 -0
  288. package/src/lab/automation/recovery.ts +24 -0
  289. package/src/lab/automation/route-context.ts +21 -0
  290. package/src/lab/automation/run-key.ts +44 -0
  291. package/src/lab/automation/runs-query.ts +34 -0
  292. package/src/lab/automation/types.ts +160 -0
  293. package/src/lab/conformance/assertion.ts +325 -0
  294. package/src/lab/conformance/digest.ts +22 -0
  295. package/src/lab/conformance/executor.ts +741 -0
  296. package/src/lab/conformance/fixture-provider.ts +27 -0
  297. package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
  298. package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
  299. package/src/lab/conformance/harness-budget.ts +47 -0
  300. package/src/lab/conformance/index.ts +5 -0
  301. package/src/lab/conformance/jcs.ts +64 -0
  302. package/src/lab/conformance/json-pointer.ts +39 -0
  303. package/src/lab/conformance/manifest.ts +180 -0
  304. package/src/lab/conformance/mcp-stub.ts +179 -0
  305. package/src/lab/conformance/negative-controls.ts +164 -0
  306. package/src/lab/conformance/observation.ts +355 -0
  307. package/src/lab/conformance/runner.ts +68 -0
  308. package/src/lab/conformance/sse-normalize.ts +59 -0
  309. package/src/lab/conformance/suite-manifest.ts +78 -0
  310. package/src/lab/conformance/types.ts +214 -0
  311. package/src/lab/constants.ts +126 -0
  312. package/src/lab/digest.ts +64 -0
  313. package/src/lab/events/errors.ts +9 -0
  314. package/src/lab/events/limits.ts +117 -0
  315. package/src/lab/events/types.ts +229 -0
  316. package/src/lab/events/validate.ts +781 -0
  317. package/src/lab/fabric/constants.ts +40 -0
  318. package/src/lab/fabric/executor.ts +492 -0
  319. package/src/lab/fabric/index.ts +80 -0
  320. package/src/lab/fabric/manifest.ts +222 -0
  321. package/src/lab/fabric/observe.ts +489 -0
  322. package/src/lab/fabric/patch.ts +79 -0
  323. package/src/lab/fabric/producer-child.ts +139 -0
  324. package/src/lab/fabric/producer-isolate.ts +276 -0
  325. package/src/lab/fabric/producer-protocol.ts +61 -0
  326. package/src/lab/fabric/scratch.ts +439 -0
  327. package/src/lab/fabric/subject.ts +106 -0
  328. package/src/lab/fabric/types.ts +134 -0
  329. package/src/lab/fabric/verifier.ts +98 -0
  330. package/src/lab/index.ts +54 -0
  331. package/src/lab/ledger/artifact-refs.ts +127 -0
  332. package/src/lab/ledger/invalidation.ts +136 -0
  333. package/src/lab/ledger/purge.ts +310 -0
  334. package/src/lab/ledger/store.ts +532 -0
  335. package/src/lab/live/credential-lease.ts +53 -0
  336. package/src/lab/live/destination.ts +155 -0
  337. package/src/lab/live/executor.ts +336 -0
  338. package/src/lab/live/inert-tools.ts +56 -0
  339. package/src/lab/live/manifest.ts +85 -0
  340. package/src/lab/live/mcp-loopback.ts +57 -0
  341. package/src/lab/live/runner.ts +19 -0
  342. package/src/lab/live/sandbox.ts +61 -0
  343. package/src/lab/live/suite-manifest.ts +41 -0
  344. package/src/lab/live/transport.ts +118 -0
  345. package/src/lab/live/types.ts +197 -0
  346. package/src/lab/observe/from-conformance.ts +301 -0
  347. package/src/lab/observe/from-live.ts +117 -0
  348. package/src/lab/paths.ts +153 -0
  349. package/src/lab/projection/rebuild.ts +495 -0
  350. package/src/lab/projection/schema.ts +135 -0
  351. package/src/lab/projection/verdicts.ts +474 -0
  352. package/src/lab/projection/verification.ts +412 -0
  353. package/src/lab/public/bundle.ts +217 -0
  354. package/src/lab/public/community-authority.ts +175 -0
  355. package/src/lab/public/community-files.ts +29 -0
  356. package/src/lab/public/community.ts +479 -0
  357. package/src/lab/public/file-safety.ts +155 -0
  358. package/src/lab/public/ids.ts +26 -0
  359. package/src/lab/public/index.ts +16 -0
  360. package/src/lab/public/mutation-lock.ts +424 -0
  361. package/src/lab/public/operator.ts +353 -0
  362. package/src/lab/public/origin-purge.ts +79 -0
  363. package/src/lab/public/origin.ts +203 -0
  364. package/src/lab/public/privacy.ts +143 -0
  365. package/src/lab/public/private-file.ts +261 -0
  366. package/src/lab/public/project.ts +124 -0
  367. package/src/lab/public/purge-test-fault.ts +21 -0
  368. package/src/lab/public/purge.ts +223 -0
  369. package/src/lab/public/registry.ts +44 -0
  370. package/src/lab/public/revocation.ts +252 -0
  371. package/src/lab/public/signature.ts +243 -0
  372. package/src/lab/public/storage.ts +105 -0
  373. package/src/lab/public/strict-json.ts +206 -0
  374. package/src/lab/public/time.ts +26 -0
  375. package/src/lab/public/types.ts +172 -0
  376. package/src/lab/public/validate.ts +391 -0
  377. package/src/lab/query/catalog.ts +101 -0
  378. package/src/lab/query/connection.ts +107 -0
  379. package/src/lab/query/constants.ts +4 -0
  380. package/src/lab/query/cursor.ts +132 -0
  381. package/src/lab/query/dto-map.ts +277 -0
  382. package/src/lab/query/errors.ts +22 -0
  383. package/src/lab/query/freshness.ts +53 -0
  384. package/src/lab/query/index.ts +45 -0
  385. package/src/lab/query/latest-observation.ts +59 -0
  386. package/src/lab/query/passive-production.ts +159 -0
  387. package/src/lab/query/queries.ts +444 -0
  388. package/src/lab/query/types.ts +266 -0
  389. package/src/lab/subject/behavior-fingerprint.ts +77 -0
  390. package/src/lab/subject/installation-salt.ts +112 -0
  391. package/src/lab/subject/protocol-subject.ts +80 -0
  392. package/src/lab/subject/route-subject.ts +74 -0
  393. package/src/lib/app-owned-memory-stores.ts +22 -0
  394. package/src/lib/bounded-body.ts +153 -9
  395. package/src/lib/bun-runtime.ts +125 -12
  396. package/src/lib/bun-stream-caps.ts +13 -9
  397. package/src/lib/codex-restart-contract.ts +120 -0
  398. package/src/lib/config-ownership.ts +6 -2
  399. package/src/lib/destination-policy.ts +65 -1
  400. package/src/lib/errors.ts +44 -2
  401. package/src/lib/fabric-task-execution-authority.ts +7 -0
  402. package/src/lib/fabric-task-host.ts +29 -0
  403. package/src/lib/lab-activation.ts +223 -0
  404. package/src/lib/lab-live-execution-authority.ts +13 -0
  405. package/src/lib/lab-live-host.ts +30 -0
  406. package/src/lib/lab-live-pinned-sender.ts +56 -0
  407. package/src/lib/lab-live-route-production.ts +130 -0
  408. package/src/lib/lab-passive-linker-registration.ts +26 -0
  409. package/src/lib/local-management-attestation.ts +51 -0
  410. package/src/lib/local-management-capability.ts +100 -0
  411. package/src/lib/local-provider-reload-contract.ts +100 -0
  412. package/src/lib/optional-shutdown-hooks.ts +57 -0
  413. package/src/lib/pinned-http.ts +145 -26
  414. package/src/lib/process-control.ts +4 -1
  415. package/src/lib/provider-outbound.ts +49 -9
  416. package/src/lib/redact.ts +419 -3
  417. package/src/lib/self-launch-argv.ts +15 -0
  418. package/src/lib/server-resource-ownership.ts +71 -0
  419. package/src/lib/shadow-call.ts +35 -4
  420. package/src/lib/sse-decoder.ts +41 -0
  421. package/src/lib/state-store-registrations.ts +10 -2
  422. package/src/lib/system-restart-contract.ts +73 -0
  423. package/src/lib/token-estimate.ts +19 -2
  424. package/src/lib/tool-argument-integers.ts +202 -0
  425. package/src/lib/translator-budget.ts +44 -0
  426. package/src/lib/upstream-http-version.ts +57 -0
  427. package/src/lib/upstream-reachability.ts +95 -0
  428. package/src/lib/upstream-retry.ts +156 -3
  429. package/src/lib/windows-atomic-replace.ts +155 -0
  430. package/src/lib/windows-elevation.ts +70 -2
  431. package/src/lib/windows-secret-acl.ts +409 -69
  432. package/src/lib/windows-service-wrappers.ts +72 -0
  433. package/src/lib/windows-text.ts +106 -0
  434. package/src/lib/windows-user-principal.ts +341 -0
  435. package/src/lib/winsw.ts +33 -5
  436. package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
  437. package/src/oauth/account-import/index.ts +15 -0
  438. package/src/oauth/account-import/parser.ts +83 -0
  439. package/src/oauth/account-import/registry.ts +18 -0
  440. package/src/oauth/account-import/service.ts +75 -0
  441. package/src/oauth/account-import/types.ts +91 -0
  442. package/src/oauth/anthropic.ts +12 -1
  443. package/src/oauth/callback-server.ts +8 -2
  444. package/src/oauth/chatgpt.ts +12 -1
  445. package/src/oauth/command-code.ts +239 -0
  446. package/src/oauth/cursor.ts +46 -5
  447. package/src/oauth/google-antigravity.ts +35 -3
  448. package/src/oauth/health.ts +20 -12
  449. package/src/oauth/index.ts +398 -66
  450. package/src/oauth/key-providers.ts +16 -0
  451. package/src/oauth/kimi.ts +16 -2
  452. package/src/oauth/kiro.ts +50 -6
  453. package/src/oauth/local-token-detect.ts +11 -2
  454. package/src/oauth/log.ts +3 -1
  455. package/src/oauth/login-cli.ts +88 -28
  456. package/src/oauth/nous.ts +798 -0
  457. package/src/oauth/store.ts +119 -21
  458. package/src/oauth/token-guardian.ts +9 -3
  459. package/src/pi/models.ts +2 -2
  460. package/src/providers/alibaba-region-migration.ts +1 -1
  461. package/src/providers/antigravity-models.ts +521 -31
  462. package/src/providers/base-url-choices.ts +10 -0
  463. package/src/providers/codex-capacity.ts +292 -0
  464. package/src/providers/command-code-efforts.ts +144 -0
  465. package/src/providers/context-cap.ts +22 -5
  466. package/src/providers/cursor-pool.ts +72 -0
  467. package/src/providers/derive.ts +253 -6
  468. package/src/providers/fastwire.ts +501 -0
  469. package/src/providers/free-directory.ts +10 -7
  470. package/src/providers/google-vertex-location.ts +14 -0
  471. package/src/providers/key-failover.ts +71 -3
  472. package/src/providers/label.ts +1 -1
  473. package/src/providers/model-discovery-limits.ts +16 -0
  474. package/src/providers/model-discovery.ts +115 -22
  475. package/src/providers/model-rename-migration.ts +255 -0
  476. package/src/providers/model-rename-startup.ts +28 -0
  477. package/src/providers/openai-sidecar.ts +72 -4
  478. package/src/providers/openai-tier-startup.ts +31 -2
  479. package/src/providers/openai-tiers.ts +119 -4
  480. package/src/providers/openai-virtual-models.ts +1 -0
  481. package/src/providers/opencode-zen-rate-limit.ts +102 -0
  482. package/src/providers/provider-id-rewrite.ts +29 -0
  483. package/src/providers/quota.ts +1319 -38
  484. package/src/providers/registry.ts +1429 -111
  485. package/src/providers/request-pacing.ts +310 -0
  486. package/src/providers/service-tier.ts +277 -0
  487. package/src/providers/slug-codec.ts +42 -6
  488. package/src/providers/static-model-discovery.ts +86 -0
  489. package/src/providers/xai-responses-opt-in.ts +15 -0
  490. package/src/providers/xai-transport.ts +11 -4
  491. package/src/reasoning-effort.ts +49 -1
  492. package/src/responses/compaction.ts +26 -1
  493. package/src/responses/custom-tool-compat.ts +266 -0
  494. package/src/responses/hosted-tool-policy.ts +9 -0
  495. package/src/responses/namespace-tool-compat.ts +355 -0
  496. package/src/responses/parser.ts +220 -38
  497. package/src/responses/provider-continuation.ts +98 -0
  498. package/src/responses/provider-opaque-metadata.ts +73 -0
  499. package/src/responses/reasoning-envelope.ts +9 -1
  500. package/src/responses/reasoning-replay-cache.ts +426 -0
  501. package/src/responses/schema.ts +7 -1
  502. package/src/responses/spill-store.ts +75 -10
  503. package/src/responses/state.ts +565 -27
  504. package/src/responses/thought-signature-replay.ts +347 -0
  505. package/src/responses/tool-search-compat.ts +301 -0
  506. package/src/responses/truncated-stop-reason.ts +60 -0
  507. package/src/router.ts +366 -30
  508. package/src/routing/analytics.ts +378 -0
  509. package/src/routing/capability.ts +244 -0
  510. package/src/routing/compatibility/assemble.ts +73 -0
  511. package/src/routing/compatibility/behavior.ts +278 -0
  512. package/src/routing/compatibility/catalog.ts +99 -0
  513. package/src/routing/compatibility/endpoint.ts +52 -0
  514. package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
  515. package/src/routing/compatibility/policy.ts +181 -0
  516. package/src/routing/compatibility/provider-slot.ts +56 -0
  517. package/src/routing/compatibility/reader.ts +110 -0
  518. package/src/routing/compatibility/subject.ts +191 -0
  519. package/src/routing/compatibility/types.ts +64 -0
  520. package/src/routing/compatibility/version.ts +104 -0
  521. package/src/routing/cost.ts +77 -0
  522. package/src/routing/evaluator.ts +495 -0
  523. package/src/routing/health.ts +412 -0
  524. package/src/routing/history/cursor.ts +43 -0
  525. package/src/routing/history/indexer.ts +605 -0
  526. package/src/routing/history/schema.ts +72 -0
  527. package/src/routing/profile-namespace.ts +15 -0
  528. package/src/routing/profile.ts +547 -0
  529. package/src/routing/quota.ts +145 -0
  530. package/src/routing/request-evidence.ts +45 -0
  531. package/src/routing/trace.ts +776 -0
  532. package/src/server/adapter-resolve.ts +2 -29
  533. package/src/server/auth-cors.ts +267 -46
  534. package/src/server/background-lifecycle.ts +182 -0
  535. package/src/server/chat-completions.ts +130 -56
  536. package/src/server/chat-native-sse.ts +331 -0
  537. package/src/server/chat-native.ts +426 -0
  538. package/src/server/claude-messages.ts +159 -43
  539. package/src/server/direct-local-http.ts +347 -0
  540. package/src/server/effort-policy.ts +18 -0
  541. package/src/server/github-copilot-responses-repair.ts +338 -0
  542. package/src/server/gui-static.ts +39 -10
  543. package/src/server/images.ts +94 -12
  544. package/src/server/index.ts +865 -181
  545. package/src/server/lifecycle.ts +284 -13
  546. package/src/server/live.ts +136 -17
  547. package/src/server/local-management-read-client.ts +90 -0
  548. package/src/server/local-provider-reload-client.ts +137 -0
  549. package/src/server/management/agent-settings-routes.ts +398 -103
  550. package/src/server/management/api-key-usage.ts +31 -5
  551. package/src/server/management/body.ts +6 -0
  552. package/src/server/management/combo-routes.ts +62 -24
  553. package/src/server/management/config-routes.ts +464 -51
  554. package/src/server/management/context.ts +80 -2
  555. package/src/server/management/integration-routes.ts +498 -0
  556. package/src/server/management/lab-automation-routes.ts +206 -0
  557. package/src/server/management/lab-routes.ts +563 -0
  558. package/src/server/management/logs-usage-routes.ts +101 -32
  559. package/src/server/management/model-routes.ts +189 -131
  560. package/src/server/management/model-rows.ts +163 -0
  561. package/src/server/management/native-integration-routes.ts +769 -0
  562. package/src/server/management/oauth-account-routes.ts +80 -4
  563. package/src/server/management/provider-capability-config.ts +48 -0
  564. package/src/server/management/provider-routes.ts +764 -157
  565. package/src/server/management/request-history-routes.ts +191 -0
  566. package/src/server/management/routing-analytics-routes.ts +74 -0
  567. package/src/server/management/routing-profile-routes.ts +380 -0
  568. package/src/server/management/shared.ts +27 -11
  569. package/src/server/management/sidebar-routes.ts +47 -31
  570. package/src/server/management/storage-log-guard-routes.ts +186 -0
  571. package/src/server/management/sync-response.ts +69 -0
  572. package/src/server/management/system-restart.ts +288 -32
  573. package/src/server/management/system-routes.ts +77 -0
  574. package/src/server/management/usage-summary-cache.ts +9 -1
  575. package/src/server/management/vision-sidecar-options.ts +167 -0
  576. package/src/server/management/web-search-sidecar-options.ts +120 -0
  577. package/src/server/management-api.ts +115 -14
  578. package/src/server/management-auth.ts +220 -5
  579. package/src/server/passive-route-linker.ts +66 -0
  580. package/src/server/ports.ts +41 -1
  581. package/src/server/proxy-liveness.ts +132 -5
  582. package/src/server/readiness.ts +99 -0
  583. package/src/server/relay-eager.ts +82 -42
  584. package/src/server/relay.ts +236 -76
  585. package/src/server/request-decompress.ts +113 -6
  586. package/src/server/request-log.ts +235 -22
  587. package/src/server/responses/agent-task-recovery-cache.ts +143 -0
  588. package/src/server/responses/agent-task-recovery.ts +465 -0
  589. package/src/server/responses/collaboration.ts +204 -35
  590. package/src/server/responses/compact.ts +442 -55
  591. package/src/server/responses/core.ts +2872 -331
  592. package/src/server/responses/empty-completion-guard.ts +276 -0
  593. package/src/server/responses/encrypted-payload.ts +62 -39
  594. package/src/server/responses/fetch-helpers.ts +79 -4
  595. package/src/server/responses/input-admission.ts +185 -0
  596. package/src/server/responses/pacing-overload.ts +13 -0
  597. package/src/server/responses/policy-fallback.ts +178 -0
  598. package/src/server/responses/responses-field-backfill.ts +251 -0
  599. package/src/server/responses/terminal-guard.ts +26 -5
  600. package/src/server/responses/upstream-error.ts +5 -0
  601. package/src/server/responses/ws-upstream.ts +308 -0
  602. package/src/server/responses-custom-tool-repair.ts +282 -0
  603. package/src/server/responses-item-id-repair.ts +54 -6
  604. package/src/server/responses-json-events.ts +90 -0
  605. package/src/server/responses-model-rewrite.ts +29 -0
  606. package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
  607. package/src/server/responses-snapshot-repair.ts +621 -0
  608. package/src/server/responses-terminal-repair.ts +342 -0
  609. package/src/server/responses-tool-search-repair.ts +267 -0
  610. package/src/server/responses-undeclared-tool-guard.ts +153 -0
  611. package/src/server/responses.ts +18 -2
  612. package/src/server/search.ts +78 -13
  613. package/src/server/sse-frame-buffer.ts +292 -0
  614. package/src/server/sse-payload-rewrite.ts +110 -22
  615. package/src/server/startup-action-control.ts +8 -1
  616. package/src/server/startup-health-cache.ts +19 -1
  617. package/src/server/system-env.ts +80 -9
  618. package/src/server/ws-bridge.ts +39 -38
  619. package/src/service-manager-probe.ts +892 -0
  620. package/src/service.ts +1111 -90
  621. package/src/sidecar/auth.ts +92 -0
  622. package/src/sidecar/candidates.ts +83 -0
  623. package/src/storage/cleanup.ts +2 -2
  624. package/src/storage/scanner.ts +1 -1
  625. package/src/storage/worker-lifecycle.ts +14 -14
  626. package/src/tray/windows-tray.ps1 +83 -9
  627. package/src/tray/windows.ts +43 -16
  628. package/src/types/accounts.ts +37 -0
  629. package/src/types/config.ts +845 -0
  630. package/src/types/provider.ts +545 -0
  631. package/src/types/request.ts +384 -0
  632. package/src/types/tools.ts +131 -0
  633. package/src/types/wire.ts +80 -0
  634. package/src/types.ts +104 -1236
  635. package/src/update/index.ts +32 -19
  636. package/src/update/job.ts +442 -67
  637. package/src/update/notify.ts +12 -6
  638. package/src/update/npm-cache-preflight.d.mts +47 -0
  639. package/src/update/npm-cache-preflight.mjs +201 -0
  640. package/src/update/transactional-install.d.mts +22 -0
  641. package/src/update/transactional-install.mjs +259 -0
  642. package/src/usage/cost.ts +0 -0
  643. package/src/usage/expected-prices.ts +268 -16
  644. package/src/usage/log.ts +606 -41
  645. package/src/usage/summary.ts +177 -9
  646. package/src/usage/user-cost-overlay-reconciler.ts +313 -0
  647. package/src/usage/user-cost-overlays.ts +314 -0
  648. package/src/vision/anthropic-describe.ts +10 -6
  649. package/src/vision/backends.ts +97 -0
  650. package/src/vision/describe.ts +9 -3
  651. package/src/vision/eligibility.ts +250 -0
  652. package/src/vision/index.ts +238 -24
  653. package/src/vision/reasoning.ts +55 -0
  654. package/src/vision/routed-describe.ts +175 -0
  655. package/src/vision/timeout-bounds.ts +9 -0
  656. package/src/web-search/anthropic-executor.ts +13 -7
  657. package/src/web-search/backends.ts +108 -0
  658. package/src/web-search/exa-executor.ts +88 -0
  659. package/src/web-search/executor.ts +11 -3
  660. package/src/web-search/gemini-executor.ts +141 -0
  661. package/src/web-search/index.ts +150 -15
  662. package/src/web-search/loop.ts +279 -50
  663. package/src/web-search/parse.ts +125 -30
  664. package/src/web-search/sources.ts +60 -0
  665. package/src/web-search/xai-executor.ts +219 -0
  666. package/gui/dist/assets/index-DTpMHS4F.js +0 -67
  667. package/gui/dist/assets/index-ZNVDE3C7.css +0 -1
  668. package/gui/dist/provider-icons/antigravity.svg +0 -1
  669. package/gui/dist/provider-icons/claude.svg +0 -1
  670. package/gui/dist/provider-icons/copilot.svg +0 -1
  671. package/gui/dist/provider-icons/cursor.svg +0 -2
  672. package/gui/dist/provider-icons/gemini.svg +0 -1
  673. package/gui/dist/provider-icons/grok-color.svg +0 -1
  674. package/gui/dist/provider-icons/kiro.svg +0 -14
  675. 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,32 @@ 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, toolAllowedByChoice } 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";
25
+ import { identifyRoutedModel } from "./identity";
24
26
  import { antigravityUsesReplayCache, applyAntigravityReplay, clearAntigravityReplay, observeAntigravityReplay } from "./google-antigravity-replay";
25
27
  import { resolveAntigravityEffortWireModel } from "../providers/antigravity-models";
28
+ import { googleVertexLocationConfigError } from "../providers/google-vertex-location";
29
+ import { lookupReplayThoughtSignature } from "../responses/thought-signature-replay";
26
30
  import {
27
31
  isTranslatorBudgetExceededError,
28
32
  retainTranslatedEventBatch,
29
33
  type TranslatorBudget,
30
34
  } from "../lib/translator-budget";
31
35
  import { buildNonOpenAIToolCatalogNudgeForTools } from "./tool-catalog-nudge";
32
- import { mapReasoningEffort } from "../reasoning-effort";
36
+ import { configuredReasoningEfforts, mapReasoningEffort } from "../reasoning-effort";
33
37
 
34
38
  // Google-family models (Gemini/Vertex/Antigravity) tend to emit long running commentary between
35
39
  // tool calls. This steers them to keep the BETWEEN-STEP text to one line and reason internally
@@ -44,12 +48,36 @@ const GOOGLE_BREVITY_INSTRUCTION = [
44
48
  "- 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
49
  ].join("\n");
46
50
 
51
+ /**
52
+ * Some Google direct deployments expose current Gemini Flash generations with a `-tiered`
53
+ * wire suffix (`gemini-3.7-flash` -> `gemini-3.7-flash-tiered`). Keep the picker-visible id
54
+ * stable and make the mapping configurable for deployments that still serve the bare id.
55
+ */
56
+ const GEMINI_DIRECT_WIRE_RENAMES: Record<string, string> = {
57
+ "gemini-3.7-flash": "gemini-3.7-flash-tiered",
58
+ "gemini-3.6-flash": "gemini-3.6-flash-tiered",
59
+ };
60
+
61
+ function resolveDirectGeminiWireModelId(modelId: string, applyRenames: boolean): string {
62
+ if (!applyRenames) return modelId;
63
+ return Object.hasOwn(GEMINI_DIRECT_WIRE_RENAMES, modelId)
64
+ ? GEMINI_DIRECT_WIRE_RENAMES[modelId]!
65
+ : modelId;
66
+ }
67
+
47
68
  /** Vertex API key: provider.apiKey if it looks real (not a sentinel), else GOOGLE_CLOUD_API_KEY env. */
48
69
  function resolveVertexApiKey(optKey?: string): string | undefined {
49
70
  const realKey = optKey && !optKey.startsWith("<") && optKey !== "N/A" ? optKey : undefined;
50
71
  return realKey || process.env.GOOGLE_CLOUD_API_KEY;
51
72
  }
52
73
 
74
+ /** Prefer Codex's stable opaque thread key; retain the existing deterministic fallback for clients
75
+ * that omit it. The replay store hashes this value and never retains the raw session identifier. */
76
+ function vertexReplaySessionId(parsed: OcxParsedRequest): string {
77
+ const threadId = parsed._clientThreadId?.trim();
78
+ return threadId || antigravitySessionId(parsed);
79
+ }
80
+
53
81
  /**
54
82
  * Stable tool-call id for the Gemini wire `functionCall.id` / `functionResponse.id` fields.
55
83
  *
@@ -67,15 +95,9 @@ function resolveVertexApiKey(optKey?: string): string | undefined {
67
95
  * the call/response pairing is preserved. Returns `undefined` for an empty id so the caller omits the
68
96
  * field entirely rather than inventing a non-matching one.
69
97
  */
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
- }
98
+ // Aliasing the stateless transform here would reintroduce the collision it cannot prevent:
99
+ // a rewritten id can equal a distinct raw id that already conforms. Use a request-scoped
100
+ // allocator, exactly as the Anthropic adapter does, so call/response pairing stays injective.
79
101
 
80
102
  /**
81
103
  * Inline image parts (Gemini `inline_data`) extracted from tool-result content. Only base64 data URLs
@@ -104,6 +126,7 @@ function toolResultImageParts(content: string | OcxContentPart[]): unknown[] {
104
126
  */
105
127
  const GEMINI_EMPTY_PLACEHOLDER = "(empty)";
106
128
  const GEMINI_EMPTY_TOOL_OUTPUT_PLACEHOLDER = "(empty tool output)";
129
+ const GEMINI_MISSING_TOOL_RESULT = "[missing tool_result for this tool_use in history]";
107
130
 
108
131
  /** A Gemini text part, or undefined when the value cannot form a valid non-empty text block. */
109
132
  function geminiTextPart(text: unknown): { text: string } | undefined {
@@ -122,20 +145,70 @@ function geminiToolResultText(content: string | OcxContentPart[]): string {
122
145
  return hasContent ? contentPartsToText(content) : GEMINI_EMPTY_TOOL_OUTPUT_PLACEHOLDER;
123
146
  }
124
147
 
125
- function messagesToGeminiFormat(parsed: OcxParsedRequest): { systemInstruction?: unknown; contents: unknown[] } {
148
+ function geminiToolResultParts(
149
+ msg: OcxToolResultMessage,
150
+ wireName: string,
151
+ wireCallId: string,
152
+ ): unknown[] {
153
+ const functionResponse: Record<string, unknown> = {
154
+ name: wireName,
155
+ response: { result: geminiToolResultText(msg.content) },
156
+ id: wireCallId,
157
+ };
158
+ return [{ functionResponse }, ...toolResultImageParts(msg.content)];
159
+ }
160
+
161
+ function geminiMissingToolResultPart(wireName: string, wireCallId: string): unknown {
162
+ return {
163
+ functionResponse: {
164
+ name: wireName,
165
+ response: { result: GEMINI_MISSING_TOOL_RESULT },
166
+ id: wireCallId,
167
+ },
168
+ };
169
+ }
170
+
171
+ function geminiUnrepresentableToolCallPart(tc: OcxToolCall, wireName: string): unknown {
172
+ const args = typeof tc.arguments === "string" ? tc.arguments : JSON.stringify(tc.arguments);
173
+ return { text: `[tool_use without a usable id: ${wireName}]\n${args}` };
174
+ }
175
+
176
+ function geminiOrphanToolResultParts(msg: OcxToolResultMessage): unknown[] {
177
+ const label = msg.toolName ? `${msg.toolName} (${msg.toolCallId})` : msg.toolCallId;
178
+ return [
179
+ { text: `[tool_result without adjacent tool_use: ${label}]\n${geminiToolResultText(msg.content)}` },
180
+ ...toolResultImageParts(msg.content),
181
+ ];
182
+ }
183
+
184
+ function messagesToGeminiFormat(
185
+ parsed: OcxParsedRequest,
186
+ identityModelId: string,
187
+ ): { systemInstruction?: unknown; contents: unknown[] } {
126
188
  // Neutralize Codex's GPT-5 identity line (Gemini/Antigravity share this path) so a routed model
127
189
  // never misreports as GPT-5/OpenAI, and never leaks the proxy identity upstream.
128
190
  const toolCatalogNudge = buildNonOpenAIToolCatalogNudgeForTools(parsed.context.tools, parsed.options.toolChoice);
129
- const systemText = neutralizeIdentity([
191
+ const systemText = identifyRoutedModel([
130
192
  ...(parsed.context.systemPrompt ?? []),
131
193
  ...(toolCatalogNudge ? [toolCatalogNudge] : []),
132
194
  GOOGLE_BREVITY_INSTRUCTION,
133
- ].join("\n\n"));
195
+ ].join("\n\n"), identityModelId);
134
196
  const systemInstruction = { parts: [{ text: systemText }] };
135
197
 
136
198
  const contents: unknown[] = [];
137
199
 
200
+ const callIds = createToolCallIdAllocator();
138
201
  for (const msg of parsed.context.messages) {
202
+ if (msg.role === "assistant") {
203
+ for (const part of (msg as OcxAssistantMessage).content) {
204
+ if (part.type === "toolCall") callIds.reserve((part as OcxToolCall).id);
205
+ }
206
+ } else if (msg.role === "toolResult") {
207
+ callIds.reserve((msg as OcxToolResultMessage).toolCallId);
208
+ }
209
+ }
210
+ for (let i = 0; i < parsed.context.messages.length; i++) {
211
+ const msg = parsed.context.messages[i];
139
212
  switch (msg.role) {
140
213
  case "user":
141
214
  case "developer": {
@@ -162,6 +235,7 @@ function messagesToGeminiFormat(parsed: OcxParsedRequest): { systemInstruction?:
162
235
  case "assistant": {
163
236
  const aMsg = msg as OcxAssistantMessage;
164
237
  const parts: unknown[] = [];
238
+ const toolCalls: Array<{ wireCallId: string; wireName: string }> = [];
165
239
  for (const p of aMsg.content) {
166
240
  if (p.type === "text") {
167
241
  const textPart = geminiTextPart((p as OcxTextContent).text);
@@ -173,13 +247,31 @@ function messagesToGeminiFormat(parsed: OcxParsedRequest): { systemInstruction?:
173
247
  // streaming covered by the replay cache. Only forward a REAL upstream signature — the
174
248
  // Responses parser also stashes synthetic item ids (`fc_...`) on this field, and sending
175
249
  // 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 };
250
+ const callId = callIds.allocate(tc.id);
251
+ const wireName = namespacedToolName(tc.namespace, tc.name);
252
+ if (callId === undefined) {
253
+ // Claude-on-Antigravity requires a usable id for every translated tool_use. An empty
254
+ // source id cannot be paired safely, so preserve the call as text and let its result
255
+ // follow the same orphan-text path instead of emitting an invalid functionCall.
256
+ parts.push(geminiUnrepresentableToolCallPart(tc, wireName));
257
+ continue;
258
+ }
259
+ const functionCall: Record<string, unknown> = { name: wireName, args: tc.arguments };
178
260
  // Claude-on-Antigravity maps this id to Anthropic `tool_use.id`; without it the upstream
179
261
  // conversion 400s. Gemini accepts the optional id and pairs call/response by it.
180
- if (callId !== undefined) functionCall.id = callId;
262
+ functionCall.id = callId;
263
+ toolCalls.push({ wireCallId: callId, wireName });
181
264
  const part: Record<string, unknown> = { functionCall };
182
- if (isLikelyRealThoughtSignature(tc.thoughtSignature)) part.thoughtSignature = tc.thoughtSignature;
265
+ // Prefer the metadata that travelled with this exact call; fall back to the legacy
266
+ // field for callers that have not been migrated. Never merge or synthesize.
267
+ // Final fallback (#1926): the durable store, read AT SERIALIZATION TIME. The
268
+ // Responses parser runs before the route/credential scope is bound, so its
269
+ // parse-time lookup can never hit; by the time this adapter serializes, the
270
+ // credential-scoped identity is bound and the durable lookup is meaningful.
271
+ const signature = tc.providerMetadata?.google?.thoughtSignature
272
+ ?? tc.thoughtSignature
273
+ ?? lookupReplayThoughtSignature(tc.id, parsed._reasoningReplayScope);
274
+ if (isLikelyRealThoughtSignature(signature)) part.thoughtSignature = signature;
183
275
  parts.push(part);
184
276
  }
185
277
  }
@@ -188,21 +280,44 @@ function messagesToGeminiFormat(parsed: OcxParsedRequest): { systemInstruction?:
188
280
  // adapter does for its own empty assistant content.
189
281
  if (parts.length === 0) break;
190
282
  contents.push({ role: "model", parts });
283
+ if (toolCalls.length > 0) {
284
+ // Gemini/Claude-on-Antigravity requires one adjacent response batch for the whole
285
+ // function-call turn. Replayed histories can be interrupted, reversed, duplicated, or
286
+ // contain an orphan result; repair only this wire boundary without inventing success.
287
+ const requiredIds = new Set(toolCalls.map(call => call.wireCallId));
288
+ const resultsById = new Map<string, OcxToolResultMessage>();
289
+ const orphanResults: OcxToolResultMessage[] = [];
290
+ let j = i + 1;
291
+ while (j < parsed.context.messages.length && parsed.context.messages[j].role === "toolResult") {
292
+ const result = parsed.context.messages[j] as OcxToolResultMessage;
293
+ const wireResultId = callIds.lookup(result.toolCallId);
294
+ if (wireResultId !== undefined && requiredIds.has(wireResultId) && !resultsById.has(wireResultId)) {
295
+ resultsById.set(wireResultId, result);
296
+ } else {
297
+ orphanResults.push(result);
298
+ }
299
+ j++;
300
+ }
301
+
302
+ const responseParts: unknown[] = [];
303
+ for (const call of toolCalls) {
304
+ const result = resultsById.get(call.wireCallId);
305
+ if (result) responseParts.push(...geminiToolResultParts(result, call.wireName, call.wireCallId));
306
+ else responseParts.push(geminiMissingToolResultPart(call.wireName, call.wireCallId));
307
+ }
308
+ for (const orphan of orphanResults) {
309
+ responseParts.push(...geminiOrphanToolResultParts(orphan));
310
+ }
311
+ contents.push({ role: "user", parts: responseParts });
312
+ i = j - 1;
313
+ }
191
314
  break;
192
315
  }
193
316
  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 });
317
+ // A standalone functionResponse is invalid without an immediately preceding matching
318
+ // functionCall batch. Preserve the result as explicit user text (plus any representable
319
+ // image siblings) rather than manufacturing a successful call or sending a 400-prone shape.
320
+ contents.push({ role: "user", parts: geminiOrphanToolResultParts(msg as OcxToolResultMessage) });
206
321
  break;
207
322
  }
208
323
  }
@@ -217,7 +332,7 @@ function toolsToGeminiFormat(parsed: OcxParsedRequest): unknown[] | undefined {
217
332
  ? new Set(parsed.options.toolChoice.allowedTools)
218
333
  : undefined;
219
334
  const tools = allowed
220
- ? parsed.context.tools.filter(t => toolAllowedByChoice(t, allowed))
335
+ ? parsed.context.tools.filter(t => toolAllowedByChoice(t, allowed, parsed.context.tools))
221
336
  : parsed.context.tools;
222
337
  if (tools.length === 0) return undefined;
223
338
  return [{
@@ -229,6 +344,28 @@ function toolsToGeminiFormat(parsed: OcxParsedRequest): unknown[] | undefined {
229
344
  }];
230
345
  }
231
346
 
347
+ /**
348
+ * Client tool_choice enforcement on the wire. The catalog nudge states the same contract in
349
+ * prose, but without functionCallingConfig the model is free to ignore it. "auto" stays absent
350
+ * so the common case is byte-identical. The allowedTools variant already filters the
351
+ * declarations in toolsToGeminiFormat; only its "required" half needs a wire mode.
352
+ */
353
+ function toolChoiceToGeminiToolConfig(parsed: OcxParsedRequest): Record<string, unknown> | undefined {
354
+ const choice = parsed.options.toolChoice;
355
+ if (!choice || choice === "auto") return undefined;
356
+ if (choice === "none") return { functionCallingConfig: { mode: "NONE" } };
357
+ if (choice === "required") return { functionCallingConfig: { mode: "ANY" } };
358
+ if (isAllowedToolChoice(choice)) {
359
+ return choice.mode === "required" ? { functionCallingConfig: { mode: "ANY" } } : undefined;
360
+ }
361
+ return {
362
+ functionCallingConfig: {
363
+ mode: "ANY",
364
+ allowedFunctionNames: [resolveToolChoiceWireName(parsed.context.tools, choice.name)],
365
+ },
366
+ };
367
+ }
368
+
232
369
  function usageFromGemini(usage: Record<string, number> | undefined): OcxUsage | undefined {
233
370
  if (!usage) return undefined;
234
371
  return {
@@ -271,17 +408,218 @@ function artifactMarkdownUrl(filePath: string): string {
271
408
  return artifactHttpUrl(filePath).replace(/([()])/g, "\\$1");
272
409
  }
273
410
 
411
+ interface GoogleResponsePart {
412
+ text?: unknown;
413
+ thought?: boolean;
414
+ thoughtSignature?: string;
415
+ thought_signature?: string;
416
+ extra_content?: { google?: { thought_signature?: unknown } };
417
+ functionCall?: unknown;
418
+ }
419
+
420
+ interface GoogleFunctionCall {
421
+ name: string;
422
+ args?: unknown;
423
+ }
424
+
425
+ /**
426
+ * Read a Gemini/Antigravity thought signature from a response part. Antigravity can place it
427
+ * either directly on the part (`thoughtSignature` / `thought_signature`) or inside the same
428
+ * nested `extra_content.google.thought_signature` shape used on the Responses wire.
429
+ */
430
+ function googlePartThoughtSignature(part: GoogleResponsePart): string | undefined {
431
+ const direct = part.thoughtSignature ?? part.thought_signature;
432
+ if (typeof direct === "string" && direct.length > 0) return direct;
433
+ const nested = part.extra_content?.google?.thought_signature;
434
+ return typeof nested === "string" && nested.length > 0 ? nested : undefined;
435
+ }
436
+
437
+ /**
438
+ * Carry a Gemini thought signature with the exact function-call part that produced it. Google
439
+ * validates the signature against that specific part, so it must ride the individual tool call
440
+ * rather than be re-matched by name/arguments later (issue #1735).
441
+ */
442
+ function googleToolCallMetadataFromPart(
443
+ part: GoogleResponsePart,
444
+ fallbackSignature?: string,
445
+ ): { providerMetadata: OcxProviderOpaqueToolCallMetadata } | undefined {
446
+ const signature = googlePartThoughtSignature(part) ?? fallbackSignature;
447
+ if (!isLikelyRealThoughtSignature(signature)) return undefined;
448
+ return { providerMetadata: { google: { thoughtSignature: signature } } };
449
+ }
450
+
451
+ /**
452
+ * Google marks model-internal reasoning as a normal text-bearing part plus `thought: true`.
453
+ * Keep that provider visibility bit authoritative here so the streaming and buffered parsers
454
+ * cannot accidentally expose the same hidden reasoning through different event types.
455
+ */
456
+ function googlePartTextEvent(part: GoogleResponsePart): AdapterEvent | undefined {
457
+ // A malformed scalar/object is not text and must not cross the AdapterEvent boundary. Dropping
458
+ // only this optional field preserves the rest of the part without inventing assistant output by
459
+ // coercion; an empty string keeps its existing no-event behavior.
460
+ if (typeof part.text !== "string" || part.text.length === 0) return undefined;
461
+ return part.thought === true
462
+ ? { type: "reasoning_raw_delta", text: part.text }
463
+ : { type: "text_delta", text: part.text };
464
+ }
465
+
466
+ interface InvalidGoogleFunctionCallDiagnostic {
467
+ reason:
468
+ | "function_call_not_object"
469
+ | "function_call_name_invalid"
470
+ | "function_call_name_blank";
471
+ partIndex: number;
472
+ valueType: string;
473
+ }
474
+
475
+ interface InvalidGoogleShapeDiagnostic {
476
+ reason:
477
+ | "candidates_not_array"
478
+ | "candidate_not_object"
479
+ | "content_not_object"
480
+ | "parts_not_array"
481
+ | "part_not_object";
482
+ partIndex?: number;
483
+ valueType: string;
484
+ }
485
+
486
+ function isGoogleRecord(value: unknown): value is Record<string, unknown> {
487
+ return value !== null && typeof value === "object" && !Array.isArray(value);
488
+ }
489
+
490
+ function googleStructuralValueType(value: unknown): string {
491
+ if (value === null) return "null";
492
+ return Array.isArray(value) ? "array" : typeof value;
493
+ }
494
+
495
+ /**
496
+ * Gemini delivers one complete functionCall per part, so a missing name cannot be repaired by a
497
+ * later delta. Validate the whole parts array before observing signatures or emitting content: a
498
+ * malformed call must terminate the claimed response rather than enter replay state or reach the
499
+ * bridge as a nameless dispatch. Null remains an absence encoding, matching other optional fields.
500
+ */
501
+ function diagnoseGoogleFunctionCalls(
502
+ parts: readonly GoogleResponsePart[],
503
+ ): InvalidGoogleFunctionCallDiagnostic | undefined {
504
+ for (let partIndex = 0; partIndex < parts.length; partIndex++) {
505
+ const functionCall = parts[partIndex]?.functionCall;
506
+ if (functionCall === undefined || functionCall === null) continue;
507
+ if (!isGoogleRecord(functionCall)) {
508
+ return {
509
+ reason: "function_call_not_object",
510
+ partIndex,
511
+ valueType: googleStructuralValueType(functionCall),
512
+ };
513
+ }
514
+ if (typeof functionCall.name !== "string") {
515
+ return {
516
+ reason: "function_call_name_invalid",
517
+ partIndex,
518
+ valueType: googleStructuralValueType(functionCall.name),
519
+ };
520
+ }
521
+ if (functionCall.name.trim().length === 0) {
522
+ return {
523
+ reason: "function_call_name_blank",
524
+ partIndex,
525
+ valueType: "string",
526
+ };
527
+ }
528
+ }
529
+ return undefined;
530
+ }
531
+
532
+ function googleFunctionCall(part: GoogleResponsePart): GoogleFunctionCall | undefined {
533
+ const functionCall = part.functionCall;
534
+ if (!isGoogleRecord(functionCall) || typeof functionCall.name !== "string") return undefined;
535
+ return { name: functionCall.name, args: functionCall.args };
536
+ }
537
+
538
+ function invalidGoogleFunctionCallEvent(
539
+ diagnostic: InvalidGoogleFunctionCallDiagnostic,
540
+ ): Extract<AdapterEvent, { type: "error" }> {
541
+ const subject = diagnostic.reason === "function_call_not_object"
542
+ ? "invalid function call"
543
+ : diagnostic.reason === "function_call_name_blank"
544
+ ? "blank function call name"
545
+ : "invalid function call name";
546
+ return {
547
+ type: "error",
548
+ message: `google response contained ${subject} (${diagnostic.reason}; partIndex=${diagnostic.partIndex}; valueType=${diagnostic.valueType}) — cannot dispatch`,
549
+ };
550
+ }
551
+
552
+ /**
553
+ * A candidate's `content` is claimed model output inside a well-formed frame, so it is governed by
554
+ * the #1332 nested-shape rule (fail closed) rather than #1240's root-frame padding rule (skip).
555
+ *
556
+ * Absence stays legal, and so does one encoding of it: an empty array is how a JSON writer with no
557
+ * distinct empty-object form spells an empty `content`, and it already behaves as "no parts". A
558
+ * NON-empty array is the opposite case — `content?.parts` silently reads `undefined` from it, so a
559
+ * candidate shaped `content: [{ parts: [...] }]` dropped its own text and completed as an empty
560
+ * turn.
561
+ */
562
+ function diagnoseGoogleContent(content: unknown): InvalidGoogleShapeDiagnostic | undefined {
563
+ if (content === undefined || content === null || isGoogleRecord(content)) return undefined;
564
+ if (Array.isArray(content) && content.length === 0) return undefined;
565
+ return { reason: "content_not_object", valueType: googleStructuralValueType(content) };
566
+ }
567
+
568
+ /**
569
+ * `content.parts` sits one rung below the candidate guard added in #1332, and both parsers
570
+ * consumed it unchecked: `for (const part of {})` throws `{} is not iterable`, and a `[null]`
571
+ * element throws on `part.thoughtSignature`. A `parts` that is absent or `null` keeps its existing
572
+ * skip — only a present, non-null container is validated.
573
+ */
574
+ function diagnoseGoogleParts(parts: unknown): InvalidGoogleShapeDiagnostic | undefined {
575
+ if (!Array.isArray(parts)) {
576
+ return { reason: "parts_not_array", valueType: googleStructuralValueType(parts) };
577
+ }
578
+ for (let partIndex = 0; partIndex < parts.length; partIndex++) {
579
+ const part: unknown = parts[partIndex];
580
+ if (!isGoogleRecord(part)) {
581
+ return { reason: "part_not_object", partIndex, valueType: googleStructuralValueType(part) };
582
+ }
583
+ }
584
+ return undefined;
585
+ }
586
+
587
+ function invalidGoogleShapeEvent(
588
+ diagnostic: InvalidGoogleShapeDiagnostic,
589
+ ): Extract<AdapterEvent, { type: "error" }> {
590
+ const at = diagnostic.partIndex !== undefined ? `; partIndex=${diagnostic.partIndex}` : "";
591
+ // The subject names the rung that failed, so an operator reading a log can tell a broken
592
+ // candidate list from a well-formed candidate whose parts are broken. The candidate subject keeps
593
+ // the exact wording #1332 introduced as its prefix, so an existing grep still matches.
594
+ const subject = diagnostic.reason === "candidates_not_array" || diagnostic.reason === "candidate_not_object"
595
+ ? "candidates"
596
+ : diagnostic.reason === "content_not_object"
597
+ ? "content"
598
+ : "content parts";
599
+ return {
600
+ type: "error",
601
+ message: `google response contained invalid ${subject} (${diagnostic.reason}${at}; valueType=${diagnostic.valueType})`,
602
+ };
603
+ }
604
+
274
605
  export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapter {
275
606
  // Per-request closure: resolveAdapter builds a fresh adapter per request (server.ts), so buildRequest
276
607
  // can stash the CCA model/session for parseStream's reasoning-replay observation.
277
608
  let antigravityModel: string | undefined;
278
609
  let antigravitySession: string | undefined;
610
+ // Vertex returns the same opaque Gemini thought signatures as CCA, but its replay namespace
611
+ // must stay transport-scoped: a signature minted by one Google backend must never be sent to
612
+ // another merely because the public model id and first prompt happen to match.
613
+ let vertexReplayModel: string | undefined;
614
+ let vertexReplaySession: string | undefined;
279
615
  let restoreGoogleToolName = (name: string): string => name;
280
616
  return {
281
617
  name: "google",
282
618
 
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).
619
+ // Vertex + Antigravity get Kiro-style retry/timeout + classified, redacted errors.
620
+ // Direct AI-Studio uses the canonical server transport (fetchWithTransientRetry), which
621
+ // retries transient 5xx responses through providerFetch while preserving multi-key pool
622
+ // 429 rotation and raw error formatting.
285
623
  ...(provider.googleMode === "vertex" || provider.googleMode === "cloud-code-assist"
286
624
  ? {
287
625
  fetchResponse: (request: AdapterRequest, ctx?: AdapterFetchContext): Promise<Response> =>
@@ -292,24 +630,49 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
292
630
  : {}),
293
631
 
294
632
  async buildRequest(parsed: OcxParsedRequest) {
295
- const { systemInstruction, contents } = messagesToGeminiFormat(parsed);
633
+ const routedModelId = provider.googleMode === "cloud-code-assist"
634
+ ? resolveAntigravityEffortWireModel(
635
+ parsed.modelId,
636
+ mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning),
637
+ provider.baseUrl,
638
+ ).wireModelId
639
+ : provider.googleMode === "vertex"
640
+ ? parsed.modelId
641
+ : resolveDirectGeminiWireModelId(parsed.modelId, provider.directGeminiWireRenames !== false);
642
+ // AI Studio's `-tiered` spelling is wire-only; CCA aliases may migrate to another generation.
643
+ const identityModelId = provider.googleMode === "cloud-code-assist" ? routedModelId : parsed.modelId;
644
+ const { systemInstruction, contents } = messagesToGeminiFormat(parsed, identityModelId);
296
645
  const tools = toolsToGeminiFormat(parsed);
297
646
 
298
647
  const body: Record<string, unknown> = { contents };
299
648
  if (systemInstruction) body.systemInstruction = systemInstruction;
300
649
  if (tools) body.tools = tools;
650
+ // Only meaningful with declarations on the wire: mode ANY with an empty
651
+ // catalog is a guaranteed upstream 400.
652
+ const toolConfig = tools ? toolChoiceToGeminiToolConfig(parsed) : undefined;
653
+ if (toolConfig) body.toolConfig = toolConfig;
301
654
 
302
655
  const generationConfig: Record<string, unknown> = {};
303
656
  if (parsed.options.maxOutputTokens) generationConfig.maxOutputTokens = parsed.options.maxOutputTokens;
304
657
  if (parsed.options.temperature !== undefined) generationConfig.temperature = parsed.options.temperature;
305
658
  if (parsed.options.topP !== undefined) generationConfig.topP = parsed.options.topP;
306
659
  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")
660
+ // Effort thinkingLevel follows the configured ladder: any model advertising reasoning
661
+ // efforts (registry preset or user config) sends the mapped level, so a picker-selected
662
+ // effort actually reaches the wire (gemini-3.1-pro-preview ships a ladder). The original
663
+ // gemini-3.5/3.6-flash direct-mode slice stays hardcoded so unladdered configs keep their
664
+ // current behavior; Vertex participates only through an explicitly configured ladder (the
665
+ // seed google-vertex entry ships none). Image models are excluded — thinkingConfig would
666
+ // suppress the responseModalities fallback below. CCA maps effort on its envelope path.
667
+ const thinkingEligible = provider.googleMode !== "cloud-code-assist"
668
+ && !isImageCapableModel(parsed.modelId)
669
+ && (configuredReasoningEfforts(provider, parsed.modelId) !== undefined
670
+ || (provider.googleMode !== "vertex"
671
+ && (parsed.modelId === "gemini-3.5-flash" || parsed.modelId === "gemini-3.6-flash")));
672
+ const thinkingLevel = thinkingEligible
310
673
  ? mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning)
311
674
  : undefined;
312
- if (directFlashThinking) generationConfig.thinkingConfig = { thinkingLevel: directFlashThinking };
675
+ if (thinkingLevel) generationConfig.thinkingConfig = { thinkingLevel };
313
676
  if (!generationConfig.thinkingConfig && isImageCapableModel(parsed.modelId)) {
314
677
  generationConfig.responseModalities = ["TEXT", "IMAGE"];
315
678
  }
@@ -331,7 +694,11 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
331
694
  if (!project) throw new Error("Antigravity requires a discovered Cloud Code Assist project id (re-run `ocx login google-antigravity`).");
332
695
  const sessionId = antigravitySessionId(parsed);
333
696
  const mappedEffort = mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning);
334
- const { wireModelId, thinkingLevel } = resolveAntigravityEffortWireModel(parsed.modelId, mappedEffort);
697
+ const { wireModelId, thinkingLevel } = resolveAntigravityEffortWireModel(
698
+ parsed.modelId,
699
+ mappedEffort,
700
+ provider.baseUrl,
701
+ );
335
702
  antigravityModel = wireModelId;
336
703
  antigravitySession = sessionId;
337
704
  // Effort → thinkingConfig for CCA (CLIProxyAPI proven: request.generationConfig.thinkingConfig).
@@ -349,6 +716,12 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
349
716
  const draftRequest: Record<string, unknown> = { ...body, sessionId };
350
717
  // Claude-on-Antigravity forces VALIDATED function calling (the real client always sets it).
351
718
  if (/claude/i.test(wireModelId)) {
719
+ // VALIDATED would defeat a client's tool_choice "none": honor it by dropping the
720
+ // declarations instead, the wire shape of a tool-less Claude turn.
721
+ if (parsed.options.toolChoice === "none") {
722
+ delete draftRequest.tools;
723
+ delete draftRequest.toolConfig;
724
+ }
352
725
  const existing = (draftRequest.toolConfig ?? {}) as Record<string, unknown>;
353
726
  const fcc = (existing.functionCallingConfig ?? {}) as Record<string, unknown>;
354
727
  draftRequest.toolConfig = { ...existing, functionCallingConfig: { ...fcc, mode: "VALIDATED" } };
@@ -364,11 +737,22 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
364
737
  } else {
365
738
  sanitizeAntigravityClaudeSignatures(contents);
366
739
  }
740
+ // Claude-on-Antigravity rejects assistant-tail (model-tail in Gemini terms) histories
741
+ // as prefill: "This model does not support assistant message prefill. The conversation
742
+ // must end with a user message." Context compaction, previous_response_id expansion,
743
+ // and interrupted-turn replay can all produce a model-tail history. Append a user
744
+ // "(continue)" nudge, mirroring the anthropic adapter's tail guard (src/adapters/anthropic.ts).
745
+ if (/claude/i.test(wireModelId)) {
746
+ const last = contents.length > 0 ? contents[contents.length - 1] as { role?: string } : undefined;
747
+ if (!last || last.role === "model") {
748
+ contents.push({ role: "user", parts: [{ text: "(continue)" }] });
749
+ }
750
+ }
367
751
  }
368
752
  const envelope = {
369
753
  model: wireModelId,
370
754
  // 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
755
+ // the HTTP `User-Agent` header (the real IDE UA). CLIProxyAPI `geminiToAntigravity` hardcodes
372
756
  // the body field; only the header carries the versioned client string.
373
757
  userAgent: "antigravity",
374
758
  requestType: "agent",
@@ -384,6 +768,20 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
384
768
  if (provider.googleMode === "vertex") {
385
769
  const compiled = compileGoogleWireBody(body);
386
770
  restoreGoogleToolName = compiled.restoreToolName;
771
+ const vertexProject = provider.project || process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT || "api-key";
772
+ const vertexLocation = provider.location || process.env.GOOGLE_CLOUD_LOCATION || "global";
773
+ vertexReplayModel = `vertex:${vertexProject}:${vertexLocation}:${parsed.modelId}`;
774
+ vertexReplaySession = vertexReplaySessionId(parsed);
775
+ // Compile names before replay so the cache matches the exact provider-visible
776
+ // functionCall identity. This is the same bounded TTL/LRU store used by CCA, with the
777
+ // transport prefix above preventing cross-backend signature reuse (#1254).
778
+ if (Array.isArray((compiled.body as { contents?: unknown[] }).contents)) {
779
+ applyAntigravityReplay(
780
+ vertexReplayModel,
781
+ vertexReplaySession,
782
+ (compiled.body as { contents: unknown[] }).contents,
783
+ );
784
+ }
387
785
  // Vertex AI: project/location endpoint with GCP ADC, or x-goog-api-key fast path.
388
786
  const apiKey = resolveVertexApiKey(provider.apiKey);
389
787
  if (apiKey) {
@@ -395,6 +793,8 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
395
793
  if (!project) throw new Error("Vertex AI requires a project id (provider.project or GOOGLE_CLOUD_PROJECT/GCLOUD_PROJECT).");
396
794
  const location = provider.location || process.env.GOOGLE_CLOUD_LOCATION;
397
795
  if (!location) throw new Error("Vertex AI requires a location (provider.location or GOOGLE_CLOUD_LOCATION).");
796
+ const locationError = googleVertexLocationConfigError(location);
797
+ if (locationError) throw new Error(locationError);
398
798
  const host = location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com`;
399
799
  const url = `https://${host}/v1/projects/${project}/locations/${location}/publishers/google/models/${parsed.modelId}:${method}${streamParam}`;
400
800
  const token = await getVertexAccessToken();
@@ -403,7 +803,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
403
803
  }
404
804
 
405
805
  // ai-studio (default): Generative Language API + x-goog-api-key.
406
- const url = `${provider.baseUrl}/v1beta/models/${parsed.modelId}:${method}${streamParam}`;
806
+ const url = `${provider.baseUrl}/v1beta/models/${routedModelId}:${method}${streamParam}`;
407
807
  const apiKey = provider.apiKey?.trim();
408
808
  if (!apiKey) throw new Error("google (AI Studio) requires a non-empty API key");
409
809
  headers["x-goog-api-key"] = apiKey;
@@ -433,6 +833,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
433
833
  let lastFinishReason: string | undefined;
434
834
  let sawAnyFrame = false;
435
835
  let sawTerminalSignal = false;
836
+ let pendingStreamThoughtSig: string | undefined;
436
837
 
437
838
  const handleDataLine = async function* (line: string): AsyncGenerator<AdapterEvent, "continue" | "content" | "terminate"> {
438
839
  const payload = line.slice(5).trim();
@@ -443,13 +844,23 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
443
844
  }
444
845
  let emittedContentEvent = false;
445
846
 
446
- let chunk: Record<string, unknown>;
847
+ let parsed: unknown;
447
848
  try {
448
- chunk = JSON.parse(payload);
849
+ parsed = JSON.parse(payload);
449
850
  } catch {
450
851
  yield { type: "error", message: "malformed upstream SSE data frame" };
451
852
  return "terminate";
452
853
  }
854
+ // `JSON.parse("null")` returns null rather than throwing, so the catch above cannot cover
855
+ // it and the `chunk.error` read below crashed the stream (see openai-chat.ts). Skip such a
856
+ // frame rather than terminating, for the reason given there: it is padding between real
857
+ // frames, not a broken stream. Deliberately returns BEFORE `sawAnyFrame`, so a stream made
858
+ // only of non-record frames still fails the terminal-signal check below instead of
859
+ // completing empty. An unparseable frame stays terminal.
860
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
861
+ return "continue";
862
+ }
863
+ const chunk = parsed as Record<string, unknown>;
453
864
  sawAnyFrame = true;
454
865
 
455
866
  // Inline provider error inside a 200 stream → terminal error (see openai-chat.ts).
@@ -457,9 +868,12 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
457
868
  const err = chunk.error as { message?: string } | undefined;
458
869
  // Clear-on-invalid: a signature rejection means our replayed thoughtSignatures are stale.
459
870
  // 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
871
+ const replayModel = provider.googleMode === "cloud-code-assist" ? antigravityModel : vertexReplayModel;
872
+ const replaySession = provider.googleMode === "cloud-code-assist" ? antigravitySession : vertexReplaySession;
873
+ if ((provider.googleMode === "cloud-code-assist" || provider.googleMode === "vertex")
874
+ && replayModel && replaySession
461
875
  && /signature|invalid_argument|invalid argument/i.test(err?.message ?? "")) {
462
- clearAntigravityReplay(antigravityModel, antigravitySession);
876
+ clearAntigravityReplay(replayModel, replaySession);
463
877
  }
464
878
  yield { type: "error", message: err?.message ?? "upstream error" };
465
879
  return "terminate";
@@ -484,24 +898,89 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
484
898
  pendingUsage = usageFromGemini(usageMeta);
485
899
  sawTerminalSignal = true;
486
900
  }
487
- const candidates = root.candidates as { content?: { parts?: unknown[] }; finishReason?: string }[] | undefined;
488
- if (!candidates?.length) return "continue";
901
+ const rawCandidates = root.candidates;
902
+ // `null` is an absence encoding, not corruption, and terminating on it is the #1219
903
+ // failure mode one rung in: a `{"candidates":null}` frame arriving between a content
904
+ // delta and the finish chunk killed a turn whose answer had already fully arrived. An
905
+ // absent key and an empty array are already skipped here; `null` joins them. A non-null
906
+ // non-array container is still claimed structure the parser cannot read, and stays
907
+ // terminal.
908
+ if (rawCandidates === undefined || rawCandidates === null) return "continue";
909
+ if (!Array.isArray(rawCandidates)) {
910
+ yield invalidGoogleShapeEvent({
911
+ reason: "candidates_not_array",
912
+ valueType: googleStructuralValueType(rawCandidates),
913
+ });
914
+ return "terminate";
915
+ }
916
+ if (rawCandidates.length === 0) return "continue";
917
+ const rawCandidate = rawCandidates[0];
918
+ if (!isGoogleRecord(rawCandidate)) {
919
+ // Unlike a root `data: null` keepalive, this is a claimed response candidate. Treat it
920
+ // as terminal protocol corruption so the turn cannot complete after silently losing
921
+ // a candidate or tool call (#1325).
922
+ yield invalidGoogleShapeEvent({
923
+ reason: "candidate_not_object",
924
+ valueType: googleStructuralValueType(rawCandidate),
925
+ });
926
+ return "terminate";
927
+ }
928
+ const candidate = rawCandidate as {
929
+ content?: unknown;
930
+ finishReason?: string;
931
+ };
489
932
 
490
- if (typeof candidates[0].finishReason === "string" && candidates[0].finishReason) {
491
- lastFinishReason = candidates[0].finishReason;
933
+ if (typeof candidate.finishReason === "string" && candidate.finishReason) {
934
+ lastFinishReason = candidate.finishReason;
492
935
  sawTerminalSignal = true;
493
936
  }
494
937
 
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[]);
938
+ // One rung below the candidate guard above, same rule: this is claimed content, not
939
+ // padding, so it fails closed rather than being iterated or silently dropped (#1325).
940
+ const rawContent: unknown = candidate.content;
941
+ const invalidContent = diagnoseGoogleContent(rawContent);
942
+ if (invalidContent) {
943
+ yield invalidGoogleShapeEvent(invalidContent);
944
+ return "terminate";
945
+ }
946
+ const rawParts: unknown = isGoogleRecord(rawContent) ? rawContent.parts : undefined;
947
+ let parts: GoogleResponsePart[] | undefined;
948
+ if (rawParts !== undefined && rawParts !== null) {
949
+ const invalidParts = diagnoseGoogleParts(rawParts);
950
+ if (invalidParts) {
951
+ yield invalidGoogleShapeEvent(invalidParts);
952
+ return "terminate";
953
+ }
954
+ parts = rawParts as GoogleResponsePart[];
955
+ const invalidFunctionCall = diagnoseGoogleFunctionCalls(parts);
956
+ if (invalidFunctionCall) {
957
+ yield invalidGoogleFunctionCallEvent(invalidFunctionCall);
958
+ return "terminate";
959
+ }
960
+ }
961
+ // Record Gemini thought signatures for the next stateless tool-result turn. Vertex and
962
+ // Antigravity use separate model namespaces so opaque provider state cannot cross routes.
963
+ const replayModel = provider.googleMode === "cloud-code-assist" ? antigravityModel : vertexReplayModel;
964
+ const replaySession = provider.googleMode === "cloud-code-assist" ? antigravitySession : vertexReplaySession;
965
+ if ((provider.googleMode === "cloud-code-assist" || provider.googleMode === "vertex")
966
+ && parts && replayModel && replaySession) {
967
+ pendingStreamThoughtSig = observeAntigravityReplay(
968
+ replayModel,
969
+ replaySession,
970
+ parts as unknown[],
971
+ pendingStreamThoughtSig,
972
+ );
499
973
  }
500
974
  if (parts) {
501
975
  for (const part of parts) {
502
- if (part.text) {
976
+ const sig = googlePartThoughtSignature(part);
977
+ if (part.thought === true && sig && isLikelyRealThoughtSignature(sig)) {
978
+ pendingStreamThoughtSig = sig;
979
+ }
980
+ const textEvent = googlePartTextEvent(part);
981
+ if (textEvent) {
503
982
  emittedContentEvent = true;
504
- yield { type: "text_delta", text: part.text };
983
+ yield textEvent;
505
984
  }
506
985
  const inline = (part as { inlineData?: { mimeType?: string; data?: string } }).inlineData;
507
986
  if (inline && typeof inline.data === "string") {
@@ -518,12 +997,19 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
518
997
  }
519
998
  }
520
999
  }
521
- if (part.functionCall) {
1000
+ const functionCall = googleFunctionCall(part);
1001
+ if (functionCall) {
522
1002
  const id = `call_${crypto.randomUUID().slice(0, 8)}`;
523
1003
  toolCallsStarted++;
524
1004
  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 ?? {}) };
1005
+ const restoredName = restoreGoogleToolName(functionCall.name);
1006
+ yield {
1007
+ type: "tool_call_start",
1008
+ id,
1009
+ name: restoredName,
1010
+ ...googleToolCallMetadataFromPart(part, pendingStreamThoughtSig),
1011
+ };
1012
+ yield { type: "tool_call_delta", arguments: JSON.stringify(functionCall.args ?? {}) };
527
1013
  yield { type: "tool_call_end" };
528
1014
  }
529
1015
  }
@@ -587,7 +1073,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
587
1073
  // Fail-closed: a turn cut off mid tool call (MAX_TOKENS / MALFORMED_FUNCTION_CALL) surfaces
588
1074
  // an error instead of a silently-incomplete done. Mirrors kiro-truncation.
589
1075
  if ((provider.googleMode === "vertex" || provider.googleMode === "cloud-code-assist")
590
- && toolCallsStarted > 0 && isVertexTruncationReason(lastFinishReason)) {
1076
+ && isVertexTruncatedTurn(lastFinishReason, toolCallsStarted)) {
591
1077
  yield { type: "error", message: vertexTruncationErrorMessage(lastFinishReason) };
592
1078
  return;
593
1079
  }
@@ -703,19 +1189,60 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
703
1189
  }
704
1190
  const events: AdapterEvent[] = [];
705
1191
 
706
- const candidates = json.candidates as { content?: { parts?: { text?: string; functionCall?: { name: string; args: unknown } }[] }; finishReason?: string }[] | undefined;
1192
+ const rawCandidates: unknown = json.candidates;
1193
+ // Parity with the streaming path, which has rejected a non-array `candidates` since #1332.
1194
+ // Buffered accepted `"abc"` outright (`"abc".length` is 3, so the emptiness check below
1195
+ // passed and `candidates[0]` was the character `"a"`), and reported `{}`/`5` as an absent
1196
+ // candidate list rather than a malformed one.
1197
+ if (rawCandidates !== undefined && rawCandidates !== null && !Array.isArray(rawCandidates)) {
1198
+ return finish([invalidGoogleShapeEvent({
1199
+ reason: "candidates_not_array",
1200
+ valueType: googleStructuralValueType(rawCandidates),
1201
+ })]);
1202
+ }
1203
+ const candidates = rawCandidates as { finishReason?: string }[] | undefined;
707
1204
  if (!candidates?.length) {
708
1205
  return finish([{ type: "error", message: "google response contained no candidates" }]);
709
1206
  }
1207
+ const rawCandidate: unknown = candidates[0];
1208
+ if (!isGoogleRecord(rawCandidate)) {
1209
+ // The streaming parser already treats this as terminal protocol corruption (#1325/#1332).
1210
+ // Buffered returned a bare `done`, so a claimed-but-malformed candidate was reported to
1211
+ // the caller as a successful empty turn.
1212
+ return finish([invalidGoogleShapeEvent({
1213
+ reason: "candidate_not_object",
1214
+ valueType: googleStructuralValueType(rawCandidate),
1215
+ })]);
1216
+ }
1217
+ const candidate = rawCandidate as { content?: unknown; finishReason?: string };
710
1218
  let toolCallsStarted = 0;
711
1219
  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[]);
1220
+ const rawContent: unknown = candidate.content;
1221
+ const invalidContent = diagnoseGoogleContent(rawContent);
1222
+ if (invalidContent) return finish([invalidGoogleShapeEvent(invalidContent)]);
1223
+ const rawParts: unknown = isGoogleRecord(rawContent) ? rawContent.parts : undefined;
1224
+ if (rawParts !== undefined && rawParts !== null) {
1225
+ const invalidParts = diagnoseGoogleParts(rawParts);
1226
+ if (invalidParts) return finish([invalidGoogleShapeEvent(invalidParts)]);
1227
+ const parts = rawParts as GoogleResponsePart[];
1228
+ const invalidFunctionCall = diagnoseGoogleFunctionCalls(parts);
1229
+ if (invalidFunctionCall) return finish([invalidGoogleFunctionCallEvent(invalidFunctionCall)]);
1230
+ // Non-streaming Google-family response: observe thought signatures for the next turn,
1231
+ // using the same transport-scoped namespace as the streaming path.
1232
+ const replayModel = provider.googleMode === "cloud-code-assist" ? antigravityModel : vertexReplayModel;
1233
+ const replaySession = provider.googleMode === "cloud-code-assist" ? antigravitySession : vertexReplaySession;
1234
+ if ((provider.googleMode === "cloud-code-assist" || provider.googleMode === "vertex")
1235
+ && replayModel && replaySession) {
1236
+ observeAntigravityReplay(replayModel, replaySession, parts as unknown[]);
716
1237
  }
717
- for (const part of candidates[0].content.parts) {
718
- if (part.text) events.push({ type: "text_delta", text: part.text });
1238
+ let pendingThoughtSig: string | undefined;
1239
+ for (const part of parts) {
1240
+ const sig = googlePartThoughtSignature(part);
1241
+ if (part.thought === true && sig && isLikelyRealThoughtSignature(sig)) {
1242
+ pendingThoughtSig = sig;
1243
+ }
1244
+ const textEvent = googlePartTextEvent(part);
1245
+ if (textEvent) events.push(textEvent);
719
1246
  const inline = (part as { inlineData?: { mimeType?: string; data?: string } }).inlineData;
720
1247
  if (inline && typeof inline.data === "string") {
721
1248
  if (inline.data.length > MAX_ENCODED_BYTES_PER_IMAGE) {
@@ -730,11 +1257,17 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
730
1257
  }
731
1258
  }
732
1259
  }
733
- if (part.functionCall) {
1260
+ const functionCall = googleFunctionCall(part);
1261
+ if (functionCall) {
734
1262
  const id = `call_${crypto.randomUUID().slice(0, 8)}`;
735
1263
  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 ?? {}) });
1264
+ events.push({
1265
+ type: "tool_call_start",
1266
+ id,
1267
+ name: restoreGoogleToolName(functionCall.name),
1268
+ ...googleToolCallMetadataFromPart(part, pendingThoughtSig),
1269
+ });
1270
+ events.push({ type: "tool_call_delta", arguments: JSON.stringify(functionCall.args ?? {}) });
738
1271
  events.push({ type: "tool_call_end" });
739
1272
  }
740
1273
  }
@@ -743,14 +1276,25 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
743
1276
  // Fail-closed truncation, same as the stream path: a non-stream turn cut off mid tool call
744
1277
  // (MAX_TOKENS / MALFORMED_FUNCTION_CALL) surfaces an error instead of a silent done.
745
1278
  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) }]);
1279
+ && isVertexTruncatedTurn(candidate.finishReason, toolCallsStarted)) {
1280
+ return finish([{ type: "error", message: vertexTruncationErrorMessage(candidate.finishReason) }]);
748
1281
  }
749
1282
 
750
1283
  const usage = json.usageMetadata as Record<string, number> | undefined;
1284
+ // Mirror the streaming path: a buffered turn cut off by the token limit or a content filter
1285
+ // must carry its stop reason, or the bridge sees a clean `done` and reports the truncated
1286
+ // turn as completed — and, on a compaction turn, installs the half-written summary as
1287
+ // replacement history (#422).
1288
+ const finishReason = candidate.finishReason as string | undefined;
1289
+ const stopReason = finishReason === "MAX_TOKENS"
1290
+ ? "max_tokens"
1291
+ : ["SAFETY", "RECITATION", "BLOCKLIST", "PROHIBITED_CONTENT", "SPII"].includes(finishReason ?? "")
1292
+ ? "content_filter"
1293
+ : undefined;
751
1294
  events.push({
752
1295
  type: "done",
753
1296
  usage: usageFromGemini(usage),
1297
+ ...(stopReason ? { stopReason } : {}),
754
1298
  });
755
1299
  return finish(events);
756
1300
  },