@yansigit/opencodex 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 (826) hide show
  1. package/AGENTS_INSTALL.md +109 -0
  2. package/LICENSE +21 -0
  3. package/README.md +303 -0
  4. package/assets/architecture.png +0 -0
  5. package/assets/banner.png +0 -0
  6. package/assets/claude-code-models.gif +0 -0
  7. package/assets/codex-app-picker.png +0 -0
  8. package/bin/ocx.mjs +587 -0
  9. package/bin/package-main.mjs +9 -0
  10. package/gui/dist/assets/index-BNJ7r4Gd.js +102 -0
  11. package/gui/dist/assets/index-CGoDO3uO.css +1 -0
  12. package/gui/dist/favicon.png +0 -0
  13. package/gui/dist/icons.svg +24 -0
  14. package/gui/dist/index.html +25 -0
  15. package/gui/dist/logo.png +0 -0
  16. package/gui/dist/provider-icons/alibaba-color.svg +1 -0
  17. package/gui/dist/provider-icons/antigravity-color.svg +1 -0
  18. package/gui/dist/provider-icons/claude-color.svg +1 -0
  19. package/gui/dist/provider-icons/cline-color.svg +16 -0
  20. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
  21. package/gui/dist/provider-icons/commandcode-color.svg +1 -0
  22. package/gui/dist/provider-icons/copilot-color.svg +1 -0
  23. package/gui/dist/provider-icons/cursor-color.svg +2 -0
  24. package/gui/dist/provider-icons/deepseek-color.svg +1 -0
  25. package/gui/dist/provider-icons/discord.svg +1 -0
  26. package/gui/dist/provider-icons/firepass-color.svg +1 -0
  27. package/gui/dist/provider-icons/fireworks-color.svg +1 -0
  28. package/gui/dist/provider-icons/gemini-color.svg +1 -0
  29. package/gui/dist/provider-icons/github-copilot-color.svg +1 -0
  30. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -0
  31. package/gui/dist/provider-icons/grok.svg +1 -0
  32. package/gui/dist/provider-icons/groq-color.svg +1 -0
  33. package/gui/dist/provider-icons/huggingface-color.svg +1 -0
  34. package/gui/dist/provider-icons/kimi-color.svg +1 -0
  35. package/gui/dist/provider-icons/kiro-color.svg +15 -0
  36. package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
  37. package/gui/dist/provider-icons/mistral-color.svg +1 -0
  38. package/gui/dist/provider-icons/moonshot-color.svg +1 -0
  39. package/gui/dist/provider-icons/nvidia-color.svg +1 -0
  40. package/gui/dist/provider-icons/ollama-color.svg +1 -0
  41. package/gui/dist/provider-icons/openai.svg +1 -0
  42. package/gui/dist/provider-icons/opencode.svg +2 -0
  43. package/gui/dist/provider-icons/openrouter-color.svg +1 -0
  44. package/gui/dist/provider-icons/pi.svg +21 -0
  45. package/gui/dist/provider-icons/qianfan-color.svg +1 -0
  46. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
  47. package/gui/dist/provider-icons/telegram.svg +1 -0
  48. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
  49. package/gui/dist/provider-icons/vllm-color.svg +1 -0
  50. package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
  51. package/package.json +108 -0
  52. package/src/AGENTS.md +28 -0
  53. package/src/adapters/anthropic-image-guard.ts +251 -0
  54. package/src/adapters/anthropic-image-normalize.ts +518 -0
  55. package/src/adapters/anthropic-output-schema.ts +137 -0
  56. package/src/adapters/anthropic.ts +1327 -0
  57. package/src/adapters/azure.ts +36 -0
  58. package/src/adapters/base.ts +121 -0
  59. package/src/adapters/client-fingerprint.ts +65 -0
  60. package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
  61. package/src/adapters/command-code.ts +601 -0
  62. package/src/adapters/cursor/arg-codec.ts +38 -0
  63. package/src/adapters/cursor/arg-normalize.ts +104 -0
  64. package/src/adapters/cursor/checkpoint-store.ts +303 -0
  65. package/src/adapters/cursor/cursor-errors.ts +288 -0
  66. package/src/adapters/cursor/discovery.ts +333 -0
  67. package/src/adapters/cursor/effort-map.ts +151 -0
  68. package/src/adapters/cursor/exec-policy.ts +88 -0
  69. package/src/adapters/cursor/framing.ts +250 -0
  70. package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
  71. package/src/adapters/cursor/h2-pool.ts +123 -0
  72. package/src/adapters/cursor/http1-bidi.ts +361 -0
  73. package/src/adapters/cursor/images.ts +704 -0
  74. package/src/adapters/cursor/kv-store.ts +52 -0
  75. package/src/adapters/cursor/live-models.ts +269 -0
  76. package/src/adapters/cursor/live-smoke-gate.ts +41 -0
  77. package/src/adapters/cursor/live-transport.ts +1653 -0
  78. package/src/adapters/cursor/mcp-config.ts +42 -0
  79. package/src/adapters/cursor/mcp-manager.ts +333 -0
  80. package/src/adapters/cursor/message-mapper.ts +49 -0
  81. package/src/adapters/cursor/native-exec-common.ts +76 -0
  82. package/src/adapters/cursor/native-exec-desktop.ts +184 -0
  83. package/src/adapters/cursor/native-exec-fs.ts +332 -0
  84. package/src/adapters/cursor/native-exec-mcp.ts +153 -0
  85. package/src/adapters/cursor/native-exec-network.ts +43 -0
  86. package/src/adapters/cursor/native-exec-shell.ts +547 -0
  87. package/src/adapters/cursor/native-exec-tools.ts +118 -0
  88. package/src/adapters/cursor/native-exec.ts +663 -0
  89. package/src/adapters/cursor/protobuf-events.ts +1381 -0
  90. package/src/adapters/cursor/protobuf-request.ts +1032 -0
  91. package/src/adapters/cursor/request-builder.ts +461 -0
  92. package/src/adapters/cursor/thread-continuity.ts +67 -0
  93. package/src/adapters/cursor/tool-definitions.ts +735 -0
  94. package/src/adapters/cursor/tool-result-normalize.ts +92 -0
  95. package/src/adapters/cursor/transport-retry.ts +132 -0
  96. package/src/adapters/cursor/transport.ts +79 -0
  97. package/src/adapters/cursor/types.ts +79 -0
  98. package/src/adapters/cursor.ts +322 -0
  99. package/src/adapters/google-antigravity-hosts.ts +48 -0
  100. package/src/adapters/google-antigravity-replay.ts +827 -0
  101. package/src/adapters/google-antigravity-tools.ts +105 -0
  102. package/src/adapters/google-antigravity-wire.ts +141 -0
  103. package/src/adapters/google-errors.ts +92 -0
  104. package/src/adapters/google-http.ts +460 -0
  105. package/src/adapters/google-tool-schema.ts +238 -0
  106. package/src/adapters/google-truncation.ts +24 -0
  107. package/src/adapters/google-wire-compiler.ts +232 -0
  108. package/src/adapters/google.ts +1377 -0
  109. package/src/adapters/identity.ts +77 -0
  110. package/src/adapters/image.ts +23 -0
  111. package/src/adapters/kiro-constants.ts +16 -0
  112. package/src/adapters/kiro-errors.ts +208 -0
  113. package/src/adapters/kiro-events.ts +197 -0
  114. package/src/adapters/kiro-images.ts +129 -0
  115. package/src/adapters/kiro-retry.ts +312 -0
  116. package/src/adapters/kiro-thinking.ts +112 -0
  117. package/src/adapters/kiro-tool-fallback.ts +36 -0
  118. package/src/adapters/kiro-tools.ts +224 -0
  119. package/src/adapters/kiro-truncation.ts +33 -0
  120. package/src/adapters/kiro-wire.ts +129 -0
  121. package/src/adapters/kiro.ts +1936 -0
  122. package/src/adapters/mimo-free.ts +280 -0
  123. package/src/adapters/openai-chat-url.ts +11 -0
  124. package/src/adapters/openai-chat.ts +1980 -0
  125. package/src/adapters/openai-responses-url.ts +16 -0
  126. package/src/adapters/openai-responses.ts +1911 -0
  127. package/src/adapters/registry.ts +175 -0
  128. package/src/adapters/responses-tool-schema.ts +67 -0
  129. package/src/adapters/run-turn-queue.ts +114 -0
  130. package/src/adapters/tool-call-id.ts +119 -0
  131. package/src/adapters/tool-catalog-nudge.ts +154 -0
  132. package/src/adapters/upstream-http-error.ts +48 -0
  133. package/src/adapters/xai-web-search.ts +185 -0
  134. package/src/bridge.ts +1986 -0
  135. package/src/chat/inbound.ts +319 -0
  136. package/src/chat/outbound.ts +821 -0
  137. package/src/claude/agents-inject.ts +266 -0
  138. package/src/claude/alias.ts +149 -0
  139. package/src/claude/auth-detect.ts +229 -0
  140. package/src/claude/auth-mode-migration.ts +32 -0
  141. package/src/claude/auth-mode.ts +62 -0
  142. package/src/claude/context-windows.ts +205 -0
  143. package/src/claude/desktop-3p-guard.ts +35 -0
  144. package/src/claude/desktop-3p-paths.ts +84 -0
  145. package/src/claude/desktop-3p.ts +615 -0
  146. package/src/claude/desktop-health.ts +26 -0
  147. package/src/claude/desktop-profile.ts +263 -0
  148. package/src/claude/gateway-cache.ts +107 -0
  149. package/src/claude/inbound-debug.ts +163 -0
  150. package/src/claude/inbound.ts +578 -0
  151. package/src/claude/model-info.ts +174 -0
  152. package/src/claude/outbound.ts +926 -0
  153. package/src/cli/access.ts +108 -0
  154. package/src/cli/account-api.ts +302 -0
  155. package/src/cli/account-auth.ts +250 -0
  156. package/src/cli/account-catalog-refresh.ts +14 -0
  157. package/src/cli/account-extended.ts +737 -0
  158. package/src/cli/account-main.ts +317 -0
  159. package/src/cli/account.ts +299 -0
  160. package/src/cli/agent-driven.ts +70 -0
  161. package/src/cli/agent.ts +290 -0
  162. package/src/cli/catalog-prewarm.ts +27 -0
  163. package/src/cli/claude-agent-startup-sync.ts +73 -0
  164. package/src/cli/claude-desktop.ts +213 -0
  165. package/src/cli/claude.ts +355 -0
  166. package/src/cli/codex-log-guard-doctor.ts +103 -0
  167. package/src/cli/codex-shim-autorestore.ts +47 -0
  168. package/src/cli/codex-shim-readiness.ts +76 -0
  169. package/src/cli/combo.ts +127 -0
  170. package/src/cli/config-command.ts +209 -0
  171. package/src/cli/debug.ts +228 -0
  172. package/src/cli/dispatch.ts +585 -0
  173. package/src/cli/doctor.ts +1202 -0
  174. package/src/cli/ensure-desired-integrations.ts +152 -0
  175. package/src/cli/export-command.ts +213 -0
  176. package/src/cli/help.ts +101 -0
  177. package/src/cli/index.ts +973 -0
  178. package/src/cli/init.ts +211 -0
  179. package/src/cli/integrations.ts +260 -0
  180. package/src/cli/interactive-confirm.ts +133 -0
  181. package/src/cli/lab.ts +607 -0
  182. package/src/cli/launcher-context.ts +77 -0
  183. package/src/cli/minimax.ts +497 -0
  184. package/src/cli/models-runtime.ts +245 -0
  185. package/src/cli/models.ts +422 -0
  186. package/src/cli/observe.ts +206 -0
  187. package/src/cli/opencode.ts +588 -0
  188. package/src/cli/provider-replit.ts +232 -0
  189. package/src/cli/provider-runtime.ts +179 -0
  190. package/src/cli/provider.ts +492 -0
  191. package/src/cli/ready.ts +301 -0
  192. package/src/cli/registry.ts +422 -0
  193. package/src/cli/replit-gateway-key-input.ts +138 -0
  194. package/src/cli/root.ts +86 -0
  195. package/src/cli/route-policy.ts +92 -0
  196. package/src/cli/runtime-api.ts +328 -0
  197. package/src/cli/star-prompt.ts +211 -0
  198. package/src/cli/status-oauth.ts +78 -0
  199. package/src/cli/status.ts +328 -0
  200. package/src/cli/system-command.ts +112 -0
  201. package/src/cli/system-restart-client.ts +146 -0
  202. package/src/cli/tray-proxy.ts +199 -0
  203. package/src/cli/v2.ts +268 -0
  204. package/src/cli.ts +10 -0
  205. package/src/clients/config-export.ts +1704 -0
  206. package/src/codex/account-id.ts +34 -0
  207. package/src/codex/account-label.ts +47 -0
  208. package/src/codex/account-lifecycle.ts +172 -0
  209. package/src/codex/account-namespace-match.ts +63 -0
  210. package/src/codex/account-namespaces.ts +195 -0
  211. package/src/codex/account-pause.ts +20 -0
  212. package/src/codex/account-priority.ts +83 -0
  213. package/src/codex/account-runtime-state.ts +31 -0
  214. package/src/codex/account-store.ts +544 -0
  215. package/src/codex/account-usability.ts +43 -0
  216. package/src/codex/admission.ts +256 -0
  217. package/src/codex/affinity-debug.ts +162 -0
  218. package/src/codex/agent-roles-sync.ts +225 -0
  219. package/src/codex/agent-roles.ts +238 -0
  220. package/src/codex/app-server-processes.ts +1143 -0
  221. package/src/codex/app-server-restart-service.ts +232 -0
  222. package/src/codex/auth-api.ts +2147 -0
  223. package/src/codex/auth-collision.ts +109 -0
  224. package/src/codex/auth-context.ts +665 -0
  225. package/src/codex/autostart-health.ts +156 -0
  226. package/src/codex/catalog/account-models.ts +67 -0
  227. package/src/codex/catalog/aggregation.ts +436 -0
  228. package/src/codex/catalog/bundled.ts +549 -0
  229. package/src/codex/catalog/effort.ts +446 -0
  230. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  231. package/src/codex/catalog/kinds.ts +2 -0
  232. package/src/codex/catalog/metadata.ts +664 -0
  233. package/src/codex/catalog/native-models.ts +72 -0
  234. package/src/codex/catalog/parsing.ts +650 -0
  235. package/src/codex/catalog/provider-fetch.ts +2064 -0
  236. package/src/codex/catalog/sync.ts +1883 -0
  237. package/src/codex/catalog-admission.ts +199 -0
  238. package/src/codex/catalog-refresh-status.ts +105 -0
  239. package/src/codex/catalog-write-serialization.ts +242 -0
  240. package/src/codex/catalog.ts +14 -0
  241. package/src/codex/codex-write-lock.ts +384 -0
  242. package/src/codex/convergence-types.ts +614 -0
  243. package/src/codex/convergence.ts +651 -0
  244. package/src/codex/coordinator-doctor.ts +332 -0
  245. package/src/codex/custom-model-catalog-migration.ts +176 -0
  246. package/src/codex/data/upstream-models.json +830 -0
  247. package/src/codex/desired-state.ts +230 -0
  248. package/src/codex/exec-invocation.ts +22 -0
  249. package/src/codex/features.ts +1566 -0
  250. package/src/codex/generation.ts +202 -0
  251. package/src/codex/history-job.ts +407 -0
  252. package/src/codex/history-lock.ts +242 -0
  253. package/src/codex/history-migration-guardian.ts +108 -0
  254. package/src/codex/history-provider.ts +979 -0
  255. package/src/codex/history-transition.ts +105 -0
  256. package/src/codex/history-worker.ts +220 -0
  257. package/src/codex/home.ts +206 -0
  258. package/src/codex/inject-coordination.ts +290 -0
  259. package/src/codex/inject.ts +1733 -0
  260. package/src/codex/injected-marker.ts +106 -0
  261. package/src/codex/integration-record.ts +266 -0
  262. package/src/codex/internal/catalog-writer.ts +203 -0
  263. package/src/codex/internal/history-writer.ts +80 -0
  264. package/src/codex/journal.ts +225 -0
  265. package/src/codex/log-guard/inspect.ts +506 -0
  266. package/src/codex/log-guard/lock.ts +150 -0
  267. package/src/codex/log-guard/maintenance.ts +403 -0
  268. package/src/codex/log-guard/path-safety.ts +88 -0
  269. package/src/codex/log-guard/policy.ts +44 -0
  270. package/src/codex/log-guard/processes.ts +205 -0
  271. package/src/codex/log-guard/protection.ts +489 -0
  272. package/src/codex/log-guard/sqlite-errors.ts +9 -0
  273. package/src/codex/main-account-cache.ts +56 -0
  274. package/src/codex/main-account.ts +68 -0
  275. package/src/codex/management-convergence.ts +167 -0
  276. package/src/codex/model-cache.ts +273 -0
  277. package/src/codex/model-entitlements.ts +353 -0
  278. package/src/codex/native-main-admission.ts +47 -0
  279. package/src/codex/native-main-auth-temp.ts +187 -0
  280. package/src/codex/native-main-claim.ts +178 -0
  281. package/src/codex/native-main-lock-file.ts +162 -0
  282. package/src/codex/native-main-owner.ts +329 -0
  283. package/src/codex/native-profile-api.ts +247 -0
  284. package/src/codex/native-profile-manager.ts +1531 -0
  285. package/src/codex/native-profile-processes.ts +121 -0
  286. package/src/codex/native-profile-recovery.ts +99 -0
  287. package/src/codex/native-profile-stage-store.ts +387 -0
  288. package/src/codex/native-profile-startup.ts +492 -0
  289. package/src/codex/native-profile-store.ts +855 -0
  290. package/src/codex/native-profile-types.ts +120 -0
  291. package/src/codex/native-residue.ts +682 -0
  292. package/src/codex/paths.ts +144 -0
  293. package/src/codex/plan-from-token.ts +140 -0
  294. package/src/codex/plan.ts +40 -0
  295. package/src/codex/plugins-doctor.ts +242 -0
  296. package/src/codex/pool-rotation.ts +295 -0
  297. package/src/codex/project-config-warnings.ts +425 -0
  298. package/src/codex/prompt-journal.ts +352 -0
  299. package/src/codex/prompt-layers.ts +967 -0
  300. package/src/codex/prompt-lock.ts +143 -0
  301. package/src/codex/quota-rejection.ts +298 -0
  302. package/src/codex/quota.ts +573 -0
  303. package/src/codex/refresh.ts +62 -0
  304. package/src/codex/reset-credit-recovery.ts +1044 -0
  305. package/src/codex/routing.ts +1888 -0
  306. package/src/codex/runtime.ts +659 -0
  307. package/src/codex/shim.ts +2170 -0
  308. package/src/codex/subagent-defaults.ts +550 -0
  309. package/src/codex/subagent-model-fallback.ts +784 -0
  310. package/src/codex/sync.ts +319 -0
  311. package/src/codex/transition-state.ts +612 -0
  312. package/src/codex/upstream-host-health.ts +368 -0
  313. package/src/codex/user-identity.ts +557 -0
  314. package/src/codex/warmup.ts +298 -0
  315. package/src/codex/websocket-registry.ts +100 -0
  316. package/src/codex/write-coordination.ts +114 -0
  317. package/src/combos/failover.ts +160 -0
  318. package/src/combos/index.ts +45 -0
  319. package/src/combos/request.ts +94 -0
  320. package/src/combos/resolve.ts +232 -0
  321. package/src/combos/types.ts +398 -0
  322. package/src/config/provider-name.ts +24 -0
  323. package/src/config.ts +4041 -0
  324. package/src/fork/register.ts +3 -0
  325. package/src/generated/compatibility-version.json +3116 -0
  326. package/src/generated/model-metadata.ts +106 -0
  327. package/src/github/star-state.ts +203 -0
  328. package/src/grok/inject.ts +530 -0
  329. package/src/grok/inspect.ts +45 -0
  330. package/src/grok/status.ts +121 -0
  331. package/src/grok/sync.ts +66 -0
  332. package/src/images/artifacts.ts +516 -0
  333. package/src/images/fulfill-video.ts +163 -0
  334. package/src/images/fulfill.ts +149 -0
  335. package/src/images/index.ts +4 -0
  336. package/src/images/loop.ts +955 -0
  337. package/src/images/plan.ts +143 -0
  338. package/src/images/synthetic-tool.ts +133 -0
  339. package/src/images/types.ts +41 -0
  340. package/src/images/xai-client.ts +141 -0
  341. package/src/images/xai-video-client.ts +163 -0
  342. package/src/index.ts +22 -0
  343. package/src/integrations/config-io.ts +269 -0
  344. package/src/integrations/journal.ts +315 -0
  345. package/src/integrations/merge.ts +135 -0
  346. package/src/integrations/mutation-flight.ts +71 -0
  347. package/src/integrations/native/ownership-preflight.ts +202 -0
  348. package/src/integrations/omp-yaml-source.ts +358 -0
  349. package/src/integrations/owned-refresh.ts +74 -0
  350. package/src/integrations/ownership.ts +111 -0
  351. package/src/integrations/registry.ts +159 -0
  352. package/src/integrations/serialize.ts +314 -0
  353. package/src/integrations/state.ts +361 -0
  354. package/src/integrations/store.ts +103 -0
  355. package/src/integrations/writer-lock.ts +98 -0
  356. package/src/integrations/writer.ts +691 -0
  357. package/src/lab/artifacts/sanitize.ts +586 -0
  358. package/src/lab/artifacts/secure-fs.ts +475 -0
  359. package/src/lab/artifacts/store.ts +310 -0
  360. package/src/lab/automation/budgets.ts +78 -0
  361. package/src/lab/automation/config-persistence.ts +256 -0
  362. package/src/lab/automation/constants.ts +39 -0
  363. package/src/lab/automation/cooldown.ts +103 -0
  364. package/src/lab/automation/dispatch.ts +211 -0
  365. package/src/lab/automation/index.ts +13 -0
  366. package/src/lab/automation/orchestrator.ts +499 -0
  367. package/src/lab/automation/persistence.ts +512 -0
  368. package/src/lab/automation/planner.ts +371 -0
  369. package/src/lab/automation/policy.ts +136 -0
  370. package/src/lab/automation/queue.ts +191 -0
  371. package/src/lab/automation/recovery.ts +24 -0
  372. package/src/lab/automation/route-context.ts +21 -0
  373. package/src/lab/automation/run-key.ts +44 -0
  374. package/src/lab/automation/runs-query.ts +34 -0
  375. package/src/lab/automation/types.ts +160 -0
  376. package/src/lab/conformance/assertion.ts +325 -0
  377. package/src/lab/conformance/digest.ts +22 -0
  378. package/src/lab/conformance/executor.ts +741 -0
  379. package/src/lab/conformance/fixture-provider.ts +27 -0
  380. package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
  381. package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
  382. package/src/lab/conformance/harness-budget.ts +47 -0
  383. package/src/lab/conformance/index.ts +5 -0
  384. package/src/lab/conformance/jcs.ts +64 -0
  385. package/src/lab/conformance/json-pointer.ts +39 -0
  386. package/src/lab/conformance/manifest.ts +180 -0
  387. package/src/lab/conformance/mcp-stub.ts +179 -0
  388. package/src/lab/conformance/negative-controls.ts +164 -0
  389. package/src/lab/conformance/observation.ts +355 -0
  390. package/src/lab/conformance/runner.ts +68 -0
  391. package/src/lab/conformance/sse-normalize.ts +59 -0
  392. package/src/lab/conformance/suite-manifest.ts +78 -0
  393. package/src/lab/conformance/types.ts +214 -0
  394. package/src/lab/constants.ts +126 -0
  395. package/src/lab/digest.ts +64 -0
  396. package/src/lab/events/errors.ts +9 -0
  397. package/src/lab/events/limits.ts +117 -0
  398. package/src/lab/events/types.ts +229 -0
  399. package/src/lab/events/validate.ts +781 -0
  400. package/src/lab/fabric/constants.ts +40 -0
  401. package/src/lab/fabric/executor.ts +492 -0
  402. package/src/lab/fabric/index.ts +80 -0
  403. package/src/lab/fabric/manifest.ts +222 -0
  404. package/src/lab/fabric/observe.ts +489 -0
  405. package/src/lab/fabric/patch.ts +79 -0
  406. package/src/lab/fabric/producer-child.ts +139 -0
  407. package/src/lab/fabric/producer-isolate.ts +276 -0
  408. package/src/lab/fabric/producer-protocol.ts +61 -0
  409. package/src/lab/fabric/scratch.ts +439 -0
  410. package/src/lab/fabric/subject.ts +106 -0
  411. package/src/lab/fabric/types.ts +134 -0
  412. package/src/lab/fabric/verifier.ts +98 -0
  413. package/src/lab/index.ts +54 -0
  414. package/src/lab/ledger/artifact-refs.ts +127 -0
  415. package/src/lab/ledger/invalidation.ts +136 -0
  416. package/src/lab/ledger/purge.ts +310 -0
  417. package/src/lab/ledger/store.ts +532 -0
  418. package/src/lab/live/credential-lease.ts +53 -0
  419. package/src/lab/live/destination.ts +155 -0
  420. package/src/lab/live/executor.ts +336 -0
  421. package/src/lab/live/inert-tools.ts +56 -0
  422. package/src/lab/live/manifest.ts +85 -0
  423. package/src/lab/live/mcp-loopback.ts +57 -0
  424. package/src/lab/live/runner.ts +19 -0
  425. package/src/lab/live/sandbox.ts +61 -0
  426. package/src/lab/live/suite-manifest.ts +41 -0
  427. package/src/lab/live/transport.ts +118 -0
  428. package/src/lab/live/types.ts +197 -0
  429. package/src/lab/observe/from-conformance.ts +301 -0
  430. package/src/lab/observe/from-live.ts +117 -0
  431. package/src/lab/paths.ts +153 -0
  432. package/src/lab/projection/rebuild.ts +495 -0
  433. package/src/lab/projection/schema.ts +135 -0
  434. package/src/lab/projection/verdicts.ts +474 -0
  435. package/src/lab/projection/verification.ts +412 -0
  436. package/src/lab/public/bundle.ts +217 -0
  437. package/src/lab/public/community-authority.ts +175 -0
  438. package/src/lab/public/community-files.ts +29 -0
  439. package/src/lab/public/community.ts +479 -0
  440. package/src/lab/public/file-safety.ts +155 -0
  441. package/src/lab/public/ids.ts +26 -0
  442. package/src/lab/public/index.ts +16 -0
  443. package/src/lab/public/mutation-lock.ts +424 -0
  444. package/src/lab/public/operator.ts +353 -0
  445. package/src/lab/public/origin-purge.ts +79 -0
  446. package/src/lab/public/origin.ts +203 -0
  447. package/src/lab/public/privacy.ts +143 -0
  448. package/src/lab/public/private-file.ts +261 -0
  449. package/src/lab/public/project.ts +124 -0
  450. package/src/lab/public/purge-test-fault.ts +21 -0
  451. package/src/lab/public/purge.ts +223 -0
  452. package/src/lab/public/registry.ts +44 -0
  453. package/src/lab/public/revocation.ts +252 -0
  454. package/src/lab/public/signature.ts +243 -0
  455. package/src/lab/public/storage.ts +105 -0
  456. package/src/lab/public/strict-json.ts +206 -0
  457. package/src/lab/public/time.ts +26 -0
  458. package/src/lab/public/types.ts +172 -0
  459. package/src/lab/public/validate.ts +391 -0
  460. package/src/lab/query/catalog.ts +101 -0
  461. package/src/lab/query/connection.ts +107 -0
  462. package/src/lab/query/constants.ts +4 -0
  463. package/src/lab/query/cursor.ts +132 -0
  464. package/src/lab/query/dto-map.ts +277 -0
  465. package/src/lab/query/errors.ts +22 -0
  466. package/src/lab/query/freshness.ts +53 -0
  467. package/src/lab/query/index.ts +45 -0
  468. package/src/lab/query/latest-observation.ts +59 -0
  469. package/src/lab/query/passive-production.ts +159 -0
  470. package/src/lab/query/queries.ts +444 -0
  471. package/src/lab/query/types.ts +266 -0
  472. package/src/lab/subject/behavior-fingerprint.ts +77 -0
  473. package/src/lab/subject/installation-salt.ts +112 -0
  474. package/src/lab/subject/protocol-subject.ts +80 -0
  475. package/src/lab/subject/route-subject.ts +74 -0
  476. package/src/lib/abort.ts +146 -0
  477. package/src/lib/admin-secrets.ts +25 -0
  478. package/src/lib/admission.ts +83 -0
  479. package/src/lib/app-owned-memory-stores.ts +195 -0
  480. package/src/lib/app-owned-memory.ts +265 -0
  481. package/src/lib/bounded-body.ts +346 -0
  482. package/src/lib/bun-binary-validator.d.mts +3 -0
  483. package/src/lib/bun-binary-validator.mjs +18 -0
  484. package/src/lib/bun-runtime.ts +184 -0
  485. package/src/lib/bun-stream-caps.ts +130 -0
  486. package/src/lib/codex-restart-contract.ts +120 -0
  487. package/src/lib/config-ownership.ts +364 -0
  488. package/src/lib/crash-guard.ts +344 -0
  489. package/src/lib/debug-log-buffer.ts +83 -0
  490. package/src/lib/debug-settings.ts +108 -0
  491. package/src/lib/debug.ts +31 -0
  492. package/src/lib/destination-policy.ts +380 -0
  493. package/src/lib/errors.ts +406 -0
  494. package/src/lib/eventstream-decoder.ts +253 -0
  495. package/src/lib/fabric-task-execution-authority.ts +7 -0
  496. package/src/lib/fabric-task-host.ts +29 -0
  497. package/src/lib/gcp-adc.ts +341 -0
  498. package/src/lib/injection-debug-log.ts +58 -0
  499. package/src/lib/lab-activation.ts +223 -0
  500. package/src/lib/lab-live-execution-authority.ts +13 -0
  501. package/src/lib/lab-live-host.ts +30 -0
  502. package/src/lib/lab-live-pinned-sender.ts +56 -0
  503. package/src/lib/lab-live-route-production.ts +130 -0
  504. package/src/lib/lab-passive-linker-registration.ts +26 -0
  505. package/src/lib/local-management-attestation.ts +51 -0
  506. package/src/lib/local-management-capability.ts +100 -0
  507. package/src/lib/local-provider-reload-contract.ts +100 -0
  508. package/src/lib/open-url.ts +25 -0
  509. package/src/lib/optional-shutdown-hooks.ts +57 -0
  510. package/src/lib/pinned-http.ts +270 -0
  511. package/src/lib/privacy.ts +20 -0
  512. package/src/lib/process-control.ts +168 -0
  513. package/src/lib/provider-outbound.ts +210 -0
  514. package/src/lib/provider-url.ts +14 -0
  515. package/src/lib/proxy-env.ts +18 -0
  516. package/src/lib/redact.ts +521 -0
  517. package/src/lib/retry-after.ts +55 -0
  518. package/src/lib/self-launch-argv.ts +15 -0
  519. package/src/lib/server-resource-ownership.ts +71 -0
  520. package/src/lib/service-secrets.ts +25 -0
  521. package/src/lib/shadow-call.ts +61 -0
  522. package/src/lib/sidecar-tracker.ts +52 -0
  523. package/src/lib/sse-decoder.ts +364 -0
  524. package/src/lib/state-store-registrations.ts +119 -0
  525. package/src/lib/state-store-sweeper.ts +184 -0
  526. package/src/lib/system-restart-contract.ts +73 -0
  527. package/src/lib/test-home-guard.ts +90 -0
  528. package/src/lib/token-estimate.ts +86 -0
  529. package/src/lib/tool-argument-integers.ts +202 -0
  530. package/src/lib/translator-budget.ts +400 -0
  531. package/src/lib/upstream-http-version.ts +57 -0
  532. package/src/lib/upstream-reachability.ts +95 -0
  533. package/src/lib/upstream-retry.ts +392 -0
  534. package/src/lib/win-exec.ts +115 -0
  535. package/src/lib/win-paths.ts +68 -0
  536. package/src/lib/windows-atomic-replace.ts +156 -0
  537. package/src/lib/windows-elevation.ts +773 -0
  538. package/src/lib/windows-secret-acl.ts +854 -0
  539. package/src/lib/windows-service-wrappers.ts +72 -0
  540. package/src/lib/windows-text.ts +106 -0
  541. package/src/lib/windows-user-principal.ts +341 -0
  542. package/src/lib/winsw.ts +403 -0
  543. package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
  544. package/src/oauth/account-import/index.ts +15 -0
  545. package/src/oauth/account-import/parser.ts +83 -0
  546. package/src/oauth/account-import/registry.ts +18 -0
  547. package/src/oauth/account-import/service.ts +75 -0
  548. package/src/oauth/account-import/types.ts +91 -0
  549. package/src/oauth/anthropic-routing.ts +594 -0
  550. package/src/oauth/anthropic.ts +188 -0
  551. package/src/oauth/antigravity-routing.ts +151 -0
  552. package/src/oauth/callback-server.ts +300 -0
  553. package/src/oauth/chatgpt.ts +161 -0
  554. package/src/oauth/command-code.ts +239 -0
  555. package/src/oauth/cursor.ts +252 -0
  556. package/src/oauth/github-copilot.ts +428 -0
  557. package/src/oauth/google-antigravity.ts +262 -0
  558. package/src/oauth/health.ts +407 -0
  559. package/src/oauth/index.ts +1504 -0
  560. package/src/oauth/key-providers.ts +124 -0
  561. package/src/oauth/kimi.ts +227 -0
  562. package/src/oauth/kiro-credentials.ts +726 -0
  563. package/src/oauth/kiro.ts +621 -0
  564. package/src/oauth/local-token-detect.ts +130 -0
  565. package/src/oauth/log.ts +50 -0
  566. package/src/oauth/login-cli.ts +223 -0
  567. package/src/oauth/nous.ts +798 -0
  568. package/src/oauth/pkce.ts +15 -0
  569. package/src/oauth/store.ts +728 -0
  570. package/src/oauth/token-guardian.ts +309 -0
  571. package/src/oauth/types.ts +62 -0
  572. package/src/oauth/xai.ts +241 -0
  573. package/src/providers/alibaba-region-backup.ts +75 -0
  574. package/src/providers/alibaba-region-migration.ts +156 -0
  575. package/src/providers/alibaba-region-startup.ts +36 -0
  576. package/src/providers/antigravity-models.ts +695 -0
  577. package/src/providers/antigravity-quota.ts +216 -0
  578. package/src/providers/api-keys.ts +140 -0
  579. package/src/providers/base-url-choices.ts +74 -0
  580. package/src/providers/codex-capacity.ts +292 -0
  581. package/src/providers/command-code-efforts.ts +144 -0
  582. package/src/providers/context-cap.ts +82 -0
  583. package/src/providers/cursor-pool.ts +72 -0
  584. package/src/providers/derive.ts +586 -0
  585. package/src/providers/fastwire.ts +501 -0
  586. package/src/providers/free-directory.ts +187 -0
  587. package/src/providers/github-copilot-transport.ts +56 -0
  588. package/src/providers/google-vertex-location.ts +14 -0
  589. package/src/providers/key-failover.ts +271 -0
  590. package/src/providers/kiro-models.ts +67 -0
  591. package/src/providers/label.ts +19 -0
  592. package/src/providers/model-discovery-limits.ts +16 -0
  593. package/src/providers/model-discovery.ts +449 -0
  594. package/src/providers/model-rename-migration.ts +255 -0
  595. package/src/providers/model-rename-startup.ts +28 -0
  596. package/src/providers/openai-sidecar.ts +243 -0
  597. package/src/providers/openai-tier-startup.ts +56 -0
  598. package/src/providers/openai-tiers.ts +423 -0
  599. package/src/providers/openai-virtual-models.ts +83 -0
  600. package/src/providers/opencode-zen-rate-limit.ts +102 -0
  601. package/src/providers/openrouter-routing.ts +102 -0
  602. package/src/providers/provider-id-rewrite.ts +185 -0
  603. package/src/providers/quota.ts +2345 -0
  604. package/src/providers/registry.ts +2918 -0
  605. package/src/providers/replit/constants.ts +27 -0
  606. package/src/providers/replit/derive.ts +85 -0
  607. package/src/providers/replit/headers.ts +28 -0
  608. package/src/providers/replit/origin.ts +55 -0
  609. package/src/providers/replit/pair-install-response.ts +72 -0
  610. package/src/providers/replit/probe.ts +199 -0
  611. package/src/providers/replit/setup.ts +350 -0
  612. package/src/providers/request-pacing.ts +310 -0
  613. package/src/providers/service-tier.ts +277 -0
  614. package/src/providers/slug-codec.ts +103 -0
  615. package/src/providers/static-model-discovery.ts +86 -0
  616. package/src/providers/xai-responses-opt-in.ts +15 -0
  617. package/src/providers/xai-transport.ts +148 -0
  618. package/src/reasoning-effort.ts +183 -0
  619. package/src/responses/compaction.ts +142 -0
  620. package/src/responses/custom-tool-compat.ts +266 -0
  621. package/src/responses/hosted-tool-policy.ts +9 -0
  622. package/src/responses/namespace-tool-compat.ts +355 -0
  623. package/src/responses/parser.ts +838 -0
  624. package/src/responses/provider-continuation.ts +98 -0
  625. package/src/responses/provider-opaque-metadata.ts +73 -0
  626. package/src/responses/reasoning-envelope.ts +60 -0
  627. package/src/responses/reasoning-replay-cache.ts +426 -0
  628. package/src/responses/schema.ts +165 -0
  629. package/src/responses/spill-store.ts +459 -0
  630. package/src/responses/state.ts +1433 -0
  631. package/src/responses/thought-signature-replay.ts +347 -0
  632. package/src/responses/tool-groups.ts +19 -0
  633. package/src/responses/tool-search-compat.ts +301 -0
  634. package/src/responses/truncated-stop-reason.ts +60 -0
  635. package/src/router.ts +761 -0
  636. package/src/routing/analytics.ts +378 -0
  637. package/src/routing/capability.ts +244 -0
  638. package/src/routing/compatibility/assemble.ts +73 -0
  639. package/src/routing/compatibility/behavior.ts +278 -0
  640. package/src/routing/compatibility/catalog.ts +99 -0
  641. package/src/routing/compatibility/endpoint.ts +52 -0
  642. package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
  643. package/src/routing/compatibility/policy.ts +181 -0
  644. package/src/routing/compatibility/provider-slot.ts +56 -0
  645. package/src/routing/compatibility/reader.ts +110 -0
  646. package/src/routing/compatibility/subject.ts +191 -0
  647. package/src/routing/compatibility/types.ts +64 -0
  648. package/src/routing/compatibility/version.ts +104 -0
  649. package/src/routing/cost.ts +77 -0
  650. package/src/routing/evaluator.ts +495 -0
  651. package/src/routing/health.ts +412 -0
  652. package/src/routing/history/cursor.ts +43 -0
  653. package/src/routing/history/indexer.ts +605 -0
  654. package/src/routing/history/schema.ts +72 -0
  655. package/src/routing/profile-namespace.ts +15 -0
  656. package/src/routing/profile.ts +547 -0
  657. package/src/routing/quota.ts +145 -0
  658. package/src/routing/request-evidence.ts +45 -0
  659. package/src/routing/trace.ts +776 -0
  660. package/src/server/adapter-resolve.ts +53 -0
  661. package/src/server/auth-cors.ts +751 -0
  662. package/src/server/background-lifecycle.ts +182 -0
  663. package/src/server/chat-completions.ts +442 -0
  664. package/src/server/chat-native-sse.ts +331 -0
  665. package/src/server/chat-native.ts +426 -0
  666. package/src/server/claude-messages.ts +1030 -0
  667. package/src/server/direct-local-http.ts +347 -0
  668. package/src/server/effort-policy.ts +190 -0
  669. package/src/server/github-copilot-responses-repair.ts +338 -0
  670. package/src/server/gui-static.ts +152 -0
  671. package/src/server/image-retry.ts +42 -0
  672. package/src/server/images.ts +568 -0
  673. package/src/server/index.ts +1813 -0
  674. package/src/server/lifecycle.ts +498 -0
  675. package/src/server/live.ts +717 -0
  676. package/src/server/local-management-read-client.ts +90 -0
  677. package/src/server/local-provider-reload-client.ts +137 -0
  678. package/src/server/management/agent-settings-routes.ts +1433 -0
  679. package/src/server/management/api-access.ts +141 -0
  680. package/src/server/management/api-key-usage.ts +193 -0
  681. package/src/server/management/body.ts +41 -0
  682. package/src/server/management/combo-routes.ts +263 -0
  683. package/src/server/management/config-routes.ts +835 -0
  684. package/src/server/management/context.ts +113 -0
  685. package/src/server/management/integration-routes.ts +498 -0
  686. package/src/server/management/lab-automation-routes.ts +206 -0
  687. package/src/server/management/lab-routes.ts +563 -0
  688. package/src/server/management/logs-usage-routes.ts +586 -0
  689. package/src/server/management/model-routes.ts +560 -0
  690. package/src/server/management/model-rows.ts +163 -0
  691. package/src/server/management/native-integration-routes.ts +769 -0
  692. package/src/server/management/oauth-account-routes.ts +637 -0
  693. package/src/server/management/provider-capability-config.ts +48 -0
  694. package/src/server/management/provider-routes.ts +1033 -0
  695. package/src/server/management/replit-provider-routes.ts +86 -0
  696. package/src/server/management/request-history-routes.ts +191 -0
  697. package/src/server/management/routing-analytics-routes.ts +74 -0
  698. package/src/server/management/routing-profile-routes.ts +385 -0
  699. package/src/server/management/shared.ts +286 -0
  700. package/src/server/management/sidebar-routes.ts +106 -0
  701. package/src/server/management/storage-log-guard-routes.ts +186 -0
  702. package/src/server/management/sync-response.ts +69 -0
  703. package/src/server/management/system-restart.ts +435 -0
  704. package/src/server/management/system-routes.ts +194 -0
  705. package/src/server/management/usage-summary-cache.ts +94 -0
  706. package/src/server/management/vision-sidecar-options.ts +167 -0
  707. package/src/server/management/web-search-sidecar-options.ts +120 -0
  708. package/src/server/management-api.ts +314 -0
  709. package/src/server/management-auth.ts +482 -0
  710. package/src/server/memory-watchdog.ts +156 -0
  711. package/src/server/passive-route-linker.ts +66 -0
  712. package/src/server/port-reclaim.ts +307 -0
  713. package/src/server/ports.ts +156 -0
  714. package/src/server/proxy-liveness.ts +328 -0
  715. package/src/server/readiness.ts +99 -0
  716. package/src/server/relay-eager.ts +353 -0
  717. package/src/server/relay.ts +1209 -0
  718. package/src/server/request-decompress.ts +239 -0
  719. package/src/server/request-log-conversation.ts +168 -0
  720. package/src/server/request-log.ts +1259 -0
  721. package/src/server/responses/agent-task-recovery-cache.ts +143 -0
  722. package/src/server/responses/agent-task-recovery.ts +465 -0
  723. package/src/server/responses/collaboration.ts +551 -0
  724. package/src/server/responses/compact.ts +771 -0
  725. package/src/server/responses/core.ts +5389 -0
  726. package/src/server/responses/empty-completion-guard.ts +276 -0
  727. package/src/server/responses/encrypted-payload.ts +331 -0
  728. package/src/server/responses/fetch-helpers.ts +232 -0
  729. package/src/server/responses/input-admission.ts +185 -0
  730. package/src/server/responses/pacing-overload.ts +13 -0
  731. package/src/server/responses/passthrough-error.ts +78 -0
  732. package/src/server/responses/policy-fallback.ts +178 -0
  733. package/src/server/responses/responses-field-backfill.ts +251 -0
  734. package/src/server/responses/terminal-guard.ts +251 -0
  735. package/src/server/responses/upstream-error.ts +53 -0
  736. package/src/server/responses/ws-upstream.ts +308 -0
  737. package/src/server/responses-custom-tool-repair.ts +282 -0
  738. package/src/server/responses-image-gen-repair.ts +132 -0
  739. package/src/server/responses-item-id-repair.ts +272 -0
  740. package/src/server/responses-json-events.ts +90 -0
  741. package/src/server/responses-model-rewrite.ts +29 -0
  742. package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
  743. package/src/server/responses-snapshot-repair.ts +621 -0
  744. package/src/server/responses-terminal-repair.ts +342 -0
  745. package/src/server/responses-tool-search-repair.ts +267 -0
  746. package/src/server/responses-undeclared-tool-guard.ts +153 -0
  747. package/src/server/responses.ts +25 -0
  748. package/src/server/search.ts +201 -0
  749. package/src/server/sse-frame-buffer.ts +292 -0
  750. package/src/server/sse-payload-rewrite.ts +263 -0
  751. package/src/server/startup-action-control.ts +315 -0
  752. package/src/server/startup-health-cache.ts +131 -0
  753. package/src/server/system-env.ts +484 -0
  754. package/src/server/windows-tcp-drop.ts +184 -0
  755. package/src/server/windows-tray-control.ts +41 -0
  756. package/src/server/ws-bridge.ts +472 -0
  757. package/src/service-manager-probe.ts +892 -0
  758. package/src/service.ts +3575 -0
  759. package/src/sidecar/auth.ts +92 -0
  760. package/src/sidecar/candidates.ts +83 -0
  761. package/src/stall-timeout.ts +20 -0
  762. package/src/storage/cleanup-job.ts +57 -0
  763. package/src/storage/cleanup.ts +3085 -0
  764. package/src/storage/policy-job.ts +457 -0
  765. package/src/storage/policy-scheduler.ts +40 -0
  766. package/src/storage/policy-worker.ts +59 -0
  767. package/src/storage/policy.ts +527 -0
  768. package/src/storage/restore-job.ts +299 -0
  769. package/src/storage/restore-worker.ts +58 -0
  770. package/src/storage/scanner.ts +238 -0
  771. package/src/storage/storage-mutation-coordinator.ts +139 -0
  772. package/src/storage/worker-lifecycle.ts +215 -0
  773. package/src/tray/assets/opencodex-tray-offline.ico +0 -0
  774. package/src/tray/assets/opencodex-tray-online.ico +0 -0
  775. package/src/tray/assets/opencodex-tray-warning.ico +0 -0
  776. package/src/tray/assets/opencodex-tray.png +0 -0
  777. package/src/tray/windows-tray.ps1 +364 -0
  778. package/src/tray/windows.ts +757 -0
  779. package/src/types/accounts.ts +37 -0
  780. package/src/types/config.ts +876 -0
  781. package/src/types/provider.ts +545 -0
  782. package/src/types/request.ts +384 -0
  783. package/src/types/tools.ts +131 -0
  784. package/src/types/wire.ts +80 -0
  785. package/src/types.ts +106 -0
  786. package/src/update/badge.ts +72 -0
  787. package/src/update/index.ts +415 -0
  788. package/src/update/job.ts +1887 -0
  789. package/src/update/notify.ts +263 -0
  790. package/src/update/npm-cache-preflight.d.mts +47 -0
  791. package/src/update/npm-cache-preflight.mjs +201 -0
  792. package/src/update/npm-invocation.d.mts +23 -0
  793. package/src/update/npm-invocation.mjs +94 -0
  794. package/src/update/transactional-install.d.mts +22 -0
  795. package/src/update/transactional-install.mjs +259 -0
  796. package/src/update/tray-update-plan.d.mts +18 -0
  797. package/src/update/tray-update-plan.mjs +38 -0
  798. package/src/usage/cost.ts +625 -0
  799. package/src/usage/debug.ts +97 -0
  800. package/src/usage/expected-prices.ts +416 -0
  801. package/src/usage/log.ts +1223 -0
  802. package/src/usage/summary.ts +753 -0
  803. package/src/usage/totals.ts +14 -0
  804. package/src/usage/user-cost-overlay-reconciler.ts +313 -0
  805. package/src/usage/user-cost-overlays.ts +314 -0
  806. package/src/vision/anthropic-describe.ts +189 -0
  807. package/src/vision/backends.ts +97 -0
  808. package/src/vision/describe.ts +131 -0
  809. package/src/vision/eligibility.ts +250 -0
  810. package/src/vision/index.ts +681 -0
  811. package/src/vision/reasoning.ts +55 -0
  812. package/src/vision/routed-describe.ts +175 -0
  813. package/src/vision/timeout-bounds.ts +9 -0
  814. package/src/web-search/anthropic-executor.ts +195 -0
  815. package/src/web-search/backends.ts +108 -0
  816. package/src/web-search/exa-executor.ts +88 -0
  817. package/src/web-search/executor.ts +113 -0
  818. package/src/web-search/format-result.ts +89 -0
  819. package/src/web-search/gemini-executor.ts +141 -0
  820. package/src/web-search/index.ts +331 -0
  821. package/src/web-search/loop.ts +896 -0
  822. package/src/web-search/parse.ts +315 -0
  823. package/src/web-search/progress-stream.ts +342 -0
  824. package/src/web-search/sources.ts +60 -0
  825. package/src/web-search/synthetic-tool.ts +47 -0
  826. package/src/web-search/xai-executor.ts +219 -0
@@ -0,0 +1,1377 @@
1
+ import type { AdapterFetchContext, AdapterRequest, ProviderAdapter } from "./base";
2
+ import { debugDroppedFrame } from "../lib/debug";
3
+ import { createToolCallIdAllocator } from "./tool-call-id";
4
+ import { createImageBudget, materializeInlineImage, MAX_ENCODED_BYTES_PER_IMAGE, artifactHttpUrl } from "../images/artifacts";
5
+ import type {
6
+ AdapterEvent,
7
+ OcxAssistantMessage,
8
+ OcxContentPart,
9
+ OcxParsedRequest,
10
+ OcxProviderConfig,
11
+ OcxProviderOpaqueToolCallMetadata,
12
+ OcxTextContent,
13
+ OcxToolCall,
14
+ OcxToolResultMessage,
15
+ OcxUsage,
16
+ } from "../types";
17
+ import { isAllowedToolChoice, namespacedToolName, resolveToolChoiceWireName, toolAllowedByChoice } from "../types";
18
+ import { contentPartsToText, parseDataUrl } from "./image";
19
+ import { getVertexAccessToken } from "../lib/gcp-adc";
20
+ import { fetchAntigravityWithRetry, fetchVertexWithRetry } from "./google-http";
21
+ import { safeAntigravityHttpErrorMessage, safeVertexHttpErrorMessage } from "./google-errors";
22
+ import { isVertexTruncatedTurn, vertexTruncationErrorMessage } from "./google-truncation";
23
+ import { ANTIGRAVITY_REQUEST_UA, antigravitySessionId, isLikelyRealThoughtSignature, sanitizeAntigravityClaudeSignatures } from "./google-antigravity-wire";
24
+ import { repairGoogleToolPairs, stripTrailingClaudePrefill } from "./google-antigravity-tools";
25
+ import { canonicalAntigravityHttpsHost, isAntigravityHttpsHost } from "./google-antigravity-hosts";
26
+ import { compileGoogleWireBody } from "./google-wire-compiler";
27
+ import { identifyRoutedModel } from "./identity";
28
+ import { antigravityUsesReplayCache, applyAntigravityReplay, clearAntigravityReplay, observeAntigravityReplay } from "./google-antigravity-replay";
29
+ import { resolveAntigravityEffortWireModel } from "../providers/antigravity-models";
30
+ import { googleVertexLocationConfigError } from "../providers/google-vertex-location";
31
+ import { lookupReplayThoughtSignature } from "../responses/thought-signature-replay";
32
+ import {
33
+ isTranslatorBudgetExceededError,
34
+ releaseTranslatedEvent,
35
+ retainTranslatedEvent,
36
+ retainTranslatedEventBatch,
37
+ type TranslatorBudget,
38
+ } from "../lib/translator-budget";
39
+ import { buildNonOpenAIToolCatalogNudgeForTools } from "./tool-catalog-nudge";
40
+ import { configuredReasoningEfforts, mapReasoningEffort } from "../reasoning-effort";
41
+
42
+ // Google-family models (Gemini/Vertex/Antigravity) tend to emit long running commentary between
43
+ // tool calls. This steers them to keep the BETWEEN-STEP text to one line and reason internally
44
+ // while still driving tools to completion. The FINAL answer is explicitly exempt so task output is
45
+ // not truncated. Appended to systemInstruction for the `google` adapter only, so non-Google
46
+ // providers are unaffected.
47
+ const GOOGLE_BREVITY_INSTRUCTION = [
48
+ "Output style for this session:",
49
+ "- While you are still working (between tool calls), keep any text you emit to a single short line; do not narrate at length.",
50
+ "- Do detailed reasoning internally, not as visible intermediate output.",
51
+ "- Prefer taking the next tool action over explaining; keep calling tools until the task is complete.",
52
+ "- 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.",
53
+ ].join("\n");
54
+
55
+ /**
56
+ * Some Google direct deployments expose current Gemini Flash generations with a `-tiered`
57
+ * wire suffix (`gemini-3.7-flash` -> `gemini-3.7-flash-tiered`). Keep the picker-visible id
58
+ * stable and make the mapping configurable for deployments that still serve the bare id.
59
+ */
60
+ const GEMINI_DIRECT_WIRE_RENAMES: Record<string, string> = {
61
+ "gemini-3.7-flash": "gemini-3.7-flash-tiered",
62
+ "gemini-3.6-flash": "gemini-3.6-flash-tiered",
63
+ };
64
+
65
+ function resolveDirectGeminiWireModelId(modelId: string, applyRenames: boolean): string {
66
+ if (!applyRenames) return modelId;
67
+ return Object.hasOwn(GEMINI_DIRECT_WIRE_RENAMES, modelId)
68
+ ? GEMINI_DIRECT_WIRE_RENAMES[modelId]!
69
+ : modelId;
70
+ }
71
+
72
+ /** Vertex API key: provider.apiKey if it looks real (not a sentinel), else GOOGLE_CLOUD_API_KEY env. */
73
+ function resolveVertexApiKey(optKey?: string): string | undefined {
74
+ const realKey = optKey && !optKey.startsWith("<") && optKey !== "N/A" ? optKey : undefined;
75
+ return realKey || process.env.GOOGLE_CLOUD_API_KEY;
76
+ }
77
+
78
+ /** Prefer Codex's stable opaque thread key; retain the existing deterministic fallback for clients
79
+ * that omit it. The replay store hashes this value and never retains the raw session identifier. */
80
+ function vertexReplaySessionId(parsed: OcxParsedRequest): string {
81
+ const threadId = parsed._clientThreadId?.trim();
82
+ return threadId || antigravitySessionId(parsed);
83
+ }
84
+
85
+ /**
86
+ * Stable tool-call id for the Gemini wire `functionCall.id` / `functionResponse.id` fields.
87
+ *
88
+ * Gemini treats these ids as optional and pairs a call with its response by id when present, so
89
+ * emitting them is harmless for Gemini models. They are REQUIRED, however, for Claude-on-Antigravity:
90
+ * the backend converts the Gemini-shaped request into Anthropic `messages`, mapping
91
+ * `functionCall.id -> tool_use.id` and `functionResponse.id -> tool_result.tool_use_id`. With no id
92
+ * the conversion fails upstream with `messages.N.content.M.tool_use.id: Field required` (HTTP 400).
93
+ *
94
+ * Anthropic's `tool_use.id` only accepts `[a-zA-Z0-9_-]`, so non-conforming characters are mapped to
95
+ * `_`. To keep the mapping injective (so two distinct raw ids like `call:a` and `call/a` cannot
96
+ * collide into one `tool_use.id` within a request), a short hash of the original raw id is appended
97
+ * whenever any character had to be rewritten. The transform is deterministic, so a call id and its
98
+ * matching result id — equal at the source, since Codex pairs them — still normalize identically and
99
+ * the call/response pairing is preserved. Returns `undefined` for an empty id so the caller omits the
100
+ * field entirely rather than inventing a non-matching one.
101
+ */
102
+ // Aliasing the stateless transform here would reintroduce the collision it cannot prevent:
103
+ // a rewritten id can equal a distinct raw id that already conforms. Use a request-scoped
104
+ // allocator, exactly as the Anthropic adapter does, so call/response pairing stays injective.
105
+
106
+ /**
107
+ * Inline image parts (Gemini `inline_data`) extracted from tool-result content. Only base64 data URLs
108
+ * can be inlined; a remote URL has no mime type we can supply, so it is skipped here (the textual
109
+ * result already carries an "[image]" marker via contentPartsToText).
110
+ */
111
+ function toolResultImageParts(content: string | OcxContentPart[]): unknown[] {
112
+ if (typeof content === "string") return [];
113
+ const parts: unknown[] = [];
114
+ for (const p of content) {
115
+ if (p.type !== "image") continue;
116
+ const data = parseDataUrl(p.imageUrl);
117
+ if (data) parts.push({ inline_data: { mime_type: data.mediaType, data: data.base64 } });
118
+ }
119
+ return parts;
120
+ }
121
+
122
+ /**
123
+ * Antigravity translates these Gemini `contents` into Anthropic `messages` for Claude models, and
124
+ * Anthropic rejects a text block whose `text` is empty or absent. An empty Gemini text part reaches
125
+ * that upstream as `{"type":"text"}` — a proto3 empty string is omitted from the translated JSON —
126
+ * and 400s with `messages.N.content.M.text.text: Field required` (issue #420). An empty `parts: []`
127
+ * model turn fails the same way. Gemini itself accepts both shapes, which is why this only ever
128
+ * surfaced on Claude-on-Antigravity; the guard lives here because this is where the parts are
129
+ * built. Mirrors the Anthropic adapter's own empty-block guard (src/adapters/anthropic.ts).
130
+ */
131
+ const GEMINI_EMPTY_PLACEHOLDER = "(empty)";
132
+ const GEMINI_EMPTY_TOOL_OUTPUT_PLACEHOLDER = "(empty tool output)";
133
+ const GEMINI_MISSING_TOOL_RESULT = "[missing tool_result for this tool_use in history]";
134
+
135
+ /** A Gemini text part, or undefined when the value cannot form a valid non-empty text block. */
136
+ function geminiTextPart(text: unknown): { text: string } | undefined {
137
+ return typeof text === "string" && text.length > 0 ? { text } : undefined;
138
+ }
139
+
140
+ /**
141
+ * Text for `functionResponse.response.result`. `contentPartsToText` collapses an empty array — or one
142
+ * holding only empty text — to its "[image]" marker, which would claim an image the turn does not
143
+ * actually carry (`toolResultImageParts` adds none). Fall back to the placeholder unless the content
144
+ * has something representable.
145
+ */
146
+ function geminiToolResultText(content: string | OcxContentPart[]): string {
147
+ if (typeof content === "string") return content || GEMINI_EMPTY_TOOL_OUTPUT_PLACEHOLDER;
148
+ const hasContent = content.some(p => p.type === "image" || (typeof p.text === "string" && p.text.length > 0));
149
+ return hasContent ? contentPartsToText(content) : GEMINI_EMPTY_TOOL_OUTPUT_PLACEHOLDER;
150
+ }
151
+
152
+ function geminiToolResultParts(
153
+ msg: OcxToolResultMessage,
154
+ wireName: string,
155
+ wireCallId: string,
156
+ ): unknown[] {
157
+ const functionResponse: Record<string, unknown> = {
158
+ name: wireName,
159
+ response: { result: geminiToolResultText(msg.content) },
160
+ id: wireCallId,
161
+ };
162
+ return [{ functionResponse }, ...toolResultImageParts(msg.content)];
163
+ }
164
+
165
+ function geminiMissingToolResultPart(wireName: string, wireCallId: string): unknown {
166
+ return {
167
+ functionResponse: {
168
+ name: wireName,
169
+ response: { result: GEMINI_MISSING_TOOL_RESULT },
170
+ id: wireCallId,
171
+ },
172
+ };
173
+ }
174
+
175
+ function geminiUnrepresentableToolCallPart(tc: OcxToolCall, wireName: string): unknown {
176
+ const args = typeof tc.arguments === "string" ? tc.arguments : JSON.stringify(tc.arguments);
177
+ return { text: `[tool_use without a usable id: ${wireName}]\n${args}` };
178
+ }
179
+
180
+ function geminiOrphanToolResultParts(msg: OcxToolResultMessage): unknown[] {
181
+ const label = msg.toolName ? `${msg.toolName} (${msg.toolCallId})` : msg.toolCallId;
182
+ return [
183
+ { text: `[tool_result without adjacent tool_use: ${label}]\n${geminiToolResultText(msg.content)}` },
184
+ ...toolResultImageParts(msg.content),
185
+ ];
186
+ }
187
+
188
+ function messagesToGeminiFormat(
189
+ parsed: OcxParsedRequest,
190
+ identityModelId: string,
191
+ repairToolPairs: boolean,
192
+ ): { systemInstruction?: unknown; contents: unknown[] } {
193
+ // Neutralize Codex's GPT-5 identity line (Gemini/Antigravity share this path) so a routed model
194
+ // never misreports as GPT-5/OpenAI, and never leaks the proxy identity upstream.
195
+ const toolCatalogNudge = buildNonOpenAIToolCatalogNudgeForTools(parsed.context.tools, parsed.options.toolChoice);
196
+ const systemText = identifyRoutedModel([
197
+ ...(parsed.context.systemPrompt ?? []),
198
+ ...(toolCatalogNudge ? [toolCatalogNudge] : []),
199
+ GOOGLE_BREVITY_INSTRUCTION,
200
+ ].join("\n\n"), identityModelId);
201
+ const systemInstruction = { parts: [{ text: systemText }] };
202
+
203
+ const contents: unknown[] = [];
204
+ const messages = repairGoogleToolPairs(parsed.context.messages, { dropUnmatchedCalls: repairToolPairs });
205
+
206
+ const callIds = createToolCallIdAllocator();
207
+ for (const msg of messages) {
208
+ if (msg.role === "assistant") {
209
+ for (const part of (msg as OcxAssistantMessage).content) {
210
+ if (part.type === "toolCall") callIds.reserve((part as OcxToolCall).id);
211
+ }
212
+ } else if (msg.role === "toolResult") {
213
+ callIds.reserve((msg as OcxToolResultMessage).toolCallId);
214
+ }
215
+ }
216
+ for (let i = 0; i < parsed.context.messages.length; i++) {
217
+ const msg = parsed.context.messages[i];
218
+ switch (msg.role) {
219
+ case "user":
220
+ case "developer": {
221
+ if (typeof msg.content === "string") {
222
+ contents.push({ role: "user", parts: [{ text: msg.content || GEMINI_EMPTY_PLACEHOLDER }] });
223
+ } else {
224
+ const parts: unknown[] = [];
225
+ for (const p of msg.content as OcxContentPart[]) {
226
+ if (p.type === "image") {
227
+ const data = parseDataUrl(p.imageUrl);
228
+ // Gemini takes base64 via inline_data; a remote URL needs a mime type we don't have, so
229
+ // fall back to a short marker rather than inlining the URL as a huge text blob.
230
+ parts.push(data ? { inline_data: { mime_type: data.mediaType, data: data.base64 } } : { text: `[image: ${p.imageUrl}]` });
231
+ continue;
232
+ }
233
+ // Drop empty/malformed text instead of emitting `{ text: "" }` or a bare `{}` part.
234
+ const textPart = geminiTextPart(p.text);
235
+ if (textPart) parts.push(textPart);
236
+ }
237
+ contents.push({ role: "user", parts: parts.length > 0 ? parts : [{ text: GEMINI_EMPTY_PLACEHOLDER }] });
238
+ }
239
+ break;
240
+ }
241
+ case "assistant": {
242
+ const aMsg = msg as OcxAssistantMessage;
243
+ const parts: unknown[] = [];
244
+ const toolCalls: Array<{ wireCallId: string; wireName: string }> = [];
245
+ for (const p of aMsg.content) {
246
+ if (p.type === "text") {
247
+ const textPart = geminiTextPart((p as OcxTextContent).text);
248
+ if (textPart) parts.push(textPart);
249
+ } else if (p.type === "toolCall") {
250
+ const tc = p as OcxToolCall;
251
+ // Preserve the thought signature on the function-call part so Antigravity/Gemini-3
252
+ // reasoning continuity survives history-driven (stateless) turns, not just same-process
253
+ // streaming covered by the replay cache. Only forward a REAL upstream signature — the
254
+ // Responses parser also stashes synthetic item ids (`fc_...`) on this field, and sending
255
+ // those as a thoughtSignature breaks continuity (the replay cache supplies the real one).
256
+ const callId = callIds.allocate(tc.id);
257
+ const wireName = namespacedToolName(tc.namespace, tc.name);
258
+ if (callId === undefined) {
259
+ // Claude-on-Antigravity requires a usable id for every translated tool_use. An empty
260
+ // source id cannot be paired safely, so preserve the call as text and let its result
261
+ // follow the same orphan-text path instead of emitting an invalid functionCall.
262
+ parts.push(geminiUnrepresentableToolCallPart(tc, wireName));
263
+ continue;
264
+ }
265
+ const functionCall: Record<string, unknown> = { name: wireName, args: tc.arguments };
266
+ // Claude-on-Antigravity maps this id to Anthropic `tool_use.id`; without it the upstream
267
+ // conversion 400s. Gemini accepts the optional id and pairs call/response by it.
268
+ functionCall.id = callId;
269
+ toolCalls.push({ wireCallId: callId, wireName });
270
+ const part: Record<string, unknown> = { functionCall };
271
+ // Prefer the metadata that travelled with this exact call; fall back to the legacy
272
+ // field for callers that have not been migrated. Never merge or synthesize.
273
+ // Final fallback (#1926): the durable store, read AT SERIALIZATION TIME. The
274
+ // Responses parser runs before the route/credential scope is bound, so its
275
+ // parse-time lookup can never hit; by the time this adapter serializes, the
276
+ // credential-scoped identity is bound and the durable lookup is meaningful.
277
+ const signature = tc.providerMetadata?.google?.thoughtSignature
278
+ ?? tc.thoughtSignature
279
+ ?? lookupReplayThoughtSignature(tc.id, parsed._reasoningReplayScope);
280
+ if (isLikelyRealThoughtSignature(signature)) part.thoughtSignature = signature;
281
+ parts.push(part);
282
+ }
283
+ }
284
+ // A turn with nothing Gemini can represent (e.g. thinking-only) would serialize as
285
+ // `parts: []`, which the Anthropic translation rejects. Skip it, as the Anthropic
286
+ // adapter does for its own empty assistant content.
287
+ if (parts.length === 0) break;
288
+ contents.push({ role: "model", parts });
289
+ if (toolCalls.length > 0) {
290
+ // Gemini/Claude-on-Antigravity requires one adjacent response batch for the whole
291
+ // function-call turn. Replayed histories can be interrupted, reversed, duplicated, or
292
+ // contain an orphan result; repair only this wire boundary without inventing success.
293
+ const requiredIds = new Set(toolCalls.map(call => call.wireCallId));
294
+ const resultsById = new Map<string, OcxToolResultMessage>();
295
+ const orphanResults: OcxToolResultMessage[] = [];
296
+ let j = i + 1;
297
+ while (j < parsed.context.messages.length && parsed.context.messages[j].role === "toolResult") {
298
+ const result = parsed.context.messages[j] as OcxToolResultMessage;
299
+ const wireResultId = callIds.lookup(result.toolCallId);
300
+ if (wireResultId !== undefined && requiredIds.has(wireResultId) && !resultsById.has(wireResultId)) {
301
+ resultsById.set(wireResultId, result);
302
+ } else {
303
+ orphanResults.push(result);
304
+ }
305
+ j++;
306
+ }
307
+
308
+ const responseParts: unknown[] = [];
309
+ for (const call of toolCalls) {
310
+ const result = resultsById.get(call.wireCallId);
311
+ if (result) responseParts.push(...geminiToolResultParts(result, call.wireName, call.wireCallId));
312
+ else responseParts.push(geminiMissingToolResultPart(call.wireName, call.wireCallId));
313
+ }
314
+ for (const orphan of orphanResults) {
315
+ responseParts.push(...geminiOrphanToolResultParts(orphan));
316
+ }
317
+ contents.push({ role: "user", parts: responseParts });
318
+ i = j - 1;
319
+ }
320
+ break;
321
+ }
322
+ case "toolResult": {
323
+ // repairGoogleToolPairs already dropped this result from pairing/id allocation, so a
324
+ // lone functionResponse cannot be emitted. Consecutive results after a functionCall
325
+ // batch never reach here (the assistant branch consumes them). Standalone or
326
+ // barrier-delayed results still have to stay visible — especially image-bearing
327
+ // screenshots — as explicit user text rather than vanishing or 400ing CCA.
328
+ contents.push({ role: "user", parts: geminiOrphanToolResultParts(msg as OcxToolResultMessage) });
329
+ break;
330
+ }
331
+ }
332
+ }
333
+
334
+ return { systemInstruction, contents };
335
+ }
336
+
337
+ function toolsToGeminiFormat(parsed: OcxParsedRequest): unknown[] | undefined {
338
+ if (!parsed.context.tools?.length) return undefined;
339
+ const allowed = isAllowedToolChoice(parsed.options.toolChoice)
340
+ ? new Set(parsed.options.toolChoice.allowedTools)
341
+ : undefined;
342
+ const tools = allowed
343
+ ? parsed.context.tools.filter(t => toolAllowedByChoice(t, allowed, parsed.context.tools))
344
+ : parsed.context.tools;
345
+ if (tools.length === 0) return undefined;
346
+ return [{
347
+ functionDeclarations: tools.map(t => ({
348
+ name: namespacedToolName(t.namespace, t.name),
349
+ description: t.description,
350
+ parameters: t.parameters,
351
+ })),
352
+ }];
353
+ }
354
+
355
+ /**
356
+ * Client tool_choice enforcement on the wire. The catalog nudge states the same contract in
357
+ * prose, but without functionCallingConfig the model is free to ignore it. "auto" stays absent
358
+ * so the common case is byte-identical. The allowedTools variant already filters the
359
+ * declarations in toolsToGeminiFormat; only its "required" half needs a wire mode.
360
+ */
361
+ function toolChoiceToGeminiToolConfig(parsed: OcxParsedRequest): Record<string, unknown> | undefined {
362
+ const choice = parsed.options.toolChoice;
363
+ if (!choice || choice === "auto") return undefined;
364
+ if (choice === "none") return { functionCallingConfig: { mode: "NONE" } };
365
+ if (choice === "required") return { functionCallingConfig: { mode: "ANY" } };
366
+ if (isAllowedToolChoice(choice)) {
367
+ return choice.mode === "required" ? { functionCallingConfig: { mode: "ANY" } } : undefined;
368
+ }
369
+ return {
370
+ functionCallingConfig: {
371
+ mode: "ANY",
372
+ allowedFunctionNames: [resolveToolChoiceWireName(parsed.context.tools, choice.name)],
373
+ },
374
+ };
375
+ }
376
+
377
+ function usageFromGemini(usage: Record<string, number> | undefined): OcxUsage | undefined {
378
+ if (!usage) return undefined;
379
+ return {
380
+ inputTokens: usage.promptTokenCount ?? 0,
381
+ outputTokens: usage.candidatesTokenCount ?? 0,
382
+ ...(usage.cachedContentTokenCount !== undefined ? { cachedInputTokens: usage.cachedContentTokenCount } : {}),
383
+ ...(usage.thoughtsTokenCount !== undefined ? { reasoningOutputTokens: usage.thoughtsTokenCount } : {}),
384
+ };
385
+ }
386
+
387
+ /**
388
+ * Cap on the buffered non-streaming response body (100 MiB), matching
389
+ * IMAGES_RESPONSE_MAX_BYTES in src/server/images.ts. Enforced by streaming the
390
+ * body with a hard byte cap before JSON.parse — Content-Length alone is not
391
+ * trusted (missing/lying headers must still reject oversized payloads).
392
+ * Streaming SSE responses also cap each data frame before JSON.parse.
393
+ */
394
+ const MAX_RESPONSE_BYTES = 100 * 1024 * 1024;
395
+ const MAX_SSE_FRAME_BYTES = MAX_RESPONSE_BYTES;
396
+ let sseFrameMaxBytes = MAX_SSE_FRAME_BYTES;
397
+
398
+ /** Test-only: lower the SSE frame byte cap without allocating a 100 MiB fixture. */
399
+ export function setGoogleSseFrameMaxBytesForTests(bytes?: number): void {
400
+ sseFrameMaxBytes = bytes ?? MAX_SSE_FRAME_BYTES;
401
+ }
402
+
403
+ function scanSseLineBytes(incompleteLineBytes: number, incoming: Uint8Array): {
404
+ maximum: number;
405
+ residual: number;
406
+ } {
407
+ let lineBytes = incompleteLineBytes;
408
+ let maximum = lineBytes;
409
+ for (let index = 0; index < incoming.length; index++) {
410
+ if (incoming[index] === 0x0a) {
411
+ lineBytes = 0;
412
+ continue;
413
+ }
414
+ lineBytes += 1;
415
+ maximum = Math.max(maximum, lineBytes);
416
+ }
417
+ return { maximum, residual: lineBytes };
418
+ }
419
+
420
+ // Note: imagen-* models use a different API surface (prediction/image-generation
421
+ // schema) and must NOT be treated as responseModalities-capable Gemini models.
422
+ // Explicit allowlist only — never `/gemini/ && /image/` (resurrects media-gen IDs).
423
+ const IMAGE_CAPABLE_MODELS = new Set([
424
+ "gemini-3.1-flash-image",
425
+ "gemini-2.0-flash-preview-image-generation",
426
+ "gemini-3-pro-image-preview",
427
+ ]);
428
+
429
+ function isImageCapableModel(modelId: string): boolean {
430
+ return IMAGE_CAPABLE_MODELS.has(modelId);
431
+ }
432
+
433
+ /**
434
+ * Model-visible markdown link for a materialized artifact. Uses the authenticated
435
+ * opaque HTTP route so remote/container clients can fetch the image without host
436
+ * filesystem paths leaking into the transcript.
437
+ */
438
+ function artifactMarkdownUrl(filePath: string): string {
439
+ return artifactHttpUrl(filePath).replace(/([()])/g, "\\$1");
440
+ }
441
+
442
+ interface GoogleResponsePart {
443
+ text?: unknown;
444
+ thought?: boolean;
445
+ thoughtSignature?: string;
446
+ thought_signature?: string;
447
+ extra_content?: { google?: { thought_signature?: unknown } };
448
+ functionCall?: unknown;
449
+ }
450
+
451
+ interface GoogleFunctionCall {
452
+ name: string;
453
+ args?: unknown;
454
+ }
455
+
456
+ /**
457
+ * Read a Gemini/Antigravity thought signature from a response part. Antigravity can place it
458
+ * either directly on the part (`thoughtSignature` / `thought_signature`) or inside the same
459
+ * nested `extra_content.google.thought_signature` shape used on the Responses wire.
460
+ */
461
+ function googlePartThoughtSignature(part: GoogleResponsePart): string | undefined {
462
+ const direct = part.thoughtSignature ?? part.thought_signature;
463
+ if (typeof direct === "string" && direct.length > 0) return direct;
464
+ const nested = part.extra_content?.google?.thought_signature;
465
+ return typeof nested === "string" && nested.length > 0 ? nested : undefined;
466
+ }
467
+
468
+ /**
469
+ * Carry a Gemini thought signature with the exact function-call part that produced it. Google
470
+ * validates the signature against that specific part, so it must ride the individual tool call
471
+ * rather than be re-matched by name/arguments later (issue #1735).
472
+ */
473
+ function googleToolCallMetadataFromPart(
474
+ part: GoogleResponsePart,
475
+ fallbackSignature?: string,
476
+ ): { providerMetadata: OcxProviderOpaqueToolCallMetadata } | undefined {
477
+ const signature = googlePartThoughtSignature(part) ?? fallbackSignature;
478
+ if (!isLikelyRealThoughtSignature(signature)) return undefined;
479
+ return { providerMetadata: { google: { thoughtSignature: signature } } };
480
+ }
481
+
482
+ /**
483
+ * Google marks model-internal reasoning as a normal text-bearing part plus `thought: true`.
484
+ * Keep that provider visibility bit authoritative here so the streaming and buffered parsers
485
+ * cannot accidentally expose the same hidden reasoning through different event types.
486
+ */
487
+ function googlePartTextEvent(part: GoogleResponsePart): AdapterEvent | undefined {
488
+ // A malformed scalar/object is not text and must not cross the AdapterEvent boundary. Dropping
489
+ // only this optional field preserves the rest of the part without inventing assistant output by
490
+ // coercion; an empty string keeps its existing no-event behavior.
491
+ if (typeof part.text !== "string" || part.text.length === 0) return undefined;
492
+ return part.thought === true
493
+ ? { type: "reasoning_raw_delta", text: part.text }
494
+ : { type: "text_delta", text: part.text };
495
+ }
496
+
497
+ interface InvalidGoogleFunctionCallDiagnostic {
498
+ reason:
499
+ | "function_call_not_object"
500
+ | "function_call_name_invalid"
501
+ | "function_call_name_blank";
502
+ partIndex: number;
503
+ valueType: string;
504
+ }
505
+
506
+ interface InvalidGoogleShapeDiagnostic {
507
+ reason:
508
+ | "candidates_not_array"
509
+ | "candidate_not_object"
510
+ | "content_not_object"
511
+ | "parts_not_array"
512
+ | "part_not_object";
513
+ partIndex?: number;
514
+ valueType: string;
515
+ }
516
+
517
+ function isGoogleRecord(value: unknown): value is Record<string, unknown> {
518
+ return value !== null && typeof value === "object" && !Array.isArray(value);
519
+ }
520
+
521
+ function googleStructuralValueType(value: unknown): string {
522
+ if (value === null) return "null";
523
+ return Array.isArray(value) ? "array" : typeof value;
524
+ }
525
+
526
+ /**
527
+ * Gemini delivers one complete functionCall per part, so a missing name cannot be repaired by a
528
+ * later delta. Validate the whole parts array before observing signatures or emitting content: a
529
+ * malformed call must terminate the claimed response rather than enter replay state or reach the
530
+ * bridge as a nameless dispatch. Null remains an absence encoding, matching other optional fields.
531
+ */
532
+ function diagnoseGoogleFunctionCalls(
533
+ parts: readonly GoogleResponsePart[],
534
+ ): InvalidGoogleFunctionCallDiagnostic | undefined {
535
+ for (let partIndex = 0; partIndex < parts.length; partIndex++) {
536
+ const functionCall = parts[partIndex]?.functionCall;
537
+ if (functionCall === undefined || functionCall === null) continue;
538
+ if (!isGoogleRecord(functionCall)) {
539
+ return {
540
+ reason: "function_call_not_object",
541
+ partIndex,
542
+ valueType: googleStructuralValueType(functionCall),
543
+ };
544
+ }
545
+ if (typeof functionCall.name !== "string") {
546
+ return {
547
+ reason: "function_call_name_invalid",
548
+ partIndex,
549
+ valueType: googleStructuralValueType(functionCall.name),
550
+ };
551
+ }
552
+ if (functionCall.name.trim().length === 0) {
553
+ return {
554
+ reason: "function_call_name_blank",
555
+ partIndex,
556
+ valueType: "string",
557
+ };
558
+ }
559
+ }
560
+ return undefined;
561
+ }
562
+
563
+ function googleFunctionCall(part: GoogleResponsePart): GoogleFunctionCall | undefined {
564
+ const functionCall = part.functionCall;
565
+ if (!isGoogleRecord(functionCall) || typeof functionCall.name !== "string") return undefined;
566
+ return { name: functionCall.name, args: functionCall.args };
567
+ }
568
+
569
+ function invalidGoogleFunctionCallEvent(
570
+ diagnostic: InvalidGoogleFunctionCallDiagnostic,
571
+ ): Extract<AdapterEvent, { type: "error" }> {
572
+ const subject = diagnostic.reason === "function_call_not_object"
573
+ ? "invalid function call"
574
+ : diagnostic.reason === "function_call_name_blank"
575
+ ? "blank function call name"
576
+ : "invalid function call name";
577
+ return {
578
+ type: "error",
579
+ message: `google response contained ${subject} (${diagnostic.reason}; partIndex=${diagnostic.partIndex}; valueType=${diagnostic.valueType}) — cannot dispatch`,
580
+ };
581
+ }
582
+
583
+ /**
584
+ * A candidate's `content` is claimed model output inside a well-formed frame, so it is governed by
585
+ * the #1332 nested-shape rule (fail closed) rather than #1240's root-frame padding rule (skip).
586
+ *
587
+ * Absence stays legal, and so does one encoding of it: an empty array is how a JSON writer with no
588
+ * distinct empty-object form spells an empty `content`, and it already behaves as "no parts". A
589
+ * NON-empty array is the opposite case — `content?.parts` silently reads `undefined` from it, so a
590
+ * candidate shaped `content: [{ parts: [...] }]` dropped its own text and completed as an empty
591
+ * turn.
592
+ */
593
+ function diagnoseGoogleContent(content: unknown): InvalidGoogleShapeDiagnostic | undefined {
594
+ if (content === undefined || content === null || isGoogleRecord(content)) return undefined;
595
+ if (Array.isArray(content) && content.length === 0) return undefined;
596
+ return { reason: "content_not_object", valueType: googleStructuralValueType(content) };
597
+ }
598
+
599
+ /**
600
+ * `content.parts` sits one rung below the candidate guard added in #1332, and both parsers
601
+ * consumed it unchecked: `for (const part of {})` throws `{} is not iterable`, and a `[null]`
602
+ * element throws on `part.thoughtSignature`. A `parts` that is absent or `null` keeps its existing
603
+ * skip — only a present, non-null container is validated.
604
+ */
605
+ function diagnoseGoogleParts(parts: unknown): InvalidGoogleShapeDiagnostic | undefined {
606
+ if (!Array.isArray(parts)) {
607
+ return { reason: "parts_not_array", valueType: googleStructuralValueType(parts) };
608
+ }
609
+ for (let partIndex = 0; partIndex < parts.length; partIndex++) {
610
+ const part: unknown = parts[partIndex];
611
+ if (!isGoogleRecord(part)) {
612
+ return { reason: "part_not_object", partIndex, valueType: googleStructuralValueType(part) };
613
+ }
614
+ }
615
+ return undefined;
616
+ }
617
+
618
+ function invalidGoogleShapeEvent(
619
+ diagnostic: InvalidGoogleShapeDiagnostic,
620
+ ): Extract<AdapterEvent, { type: "error" }> {
621
+ const at = diagnostic.partIndex !== undefined ? `; partIndex=${diagnostic.partIndex}` : "";
622
+ // The subject names the rung that failed, so an operator reading a log can tell a broken
623
+ // candidate list from a well-formed candidate whose parts are broken. The candidate subject keeps
624
+ // the exact wording #1332 introduced as its prefix, so an existing grep still matches.
625
+ const subject = diagnostic.reason === "candidates_not_array" || diagnostic.reason === "candidate_not_object"
626
+ ? "candidates"
627
+ : diagnostic.reason === "content_not_object"
628
+ ? "content"
629
+ : "content parts";
630
+ return {
631
+ type: "error",
632
+ message: `google response contained invalid ${subject} (${diagnostic.reason}${at}; valueType=${diagnostic.valueType})`,
633
+ };
634
+ }
635
+
636
+ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapter {
637
+ // Per-request closure: resolveAdapter builds a fresh adapter per request (server.ts), so buildRequest
638
+ // can stash the CCA model/session for parseStream's reasoning-replay observation.
639
+ let antigravityModel: string | undefined;
640
+ let antigravitySession: string | undefined;
641
+ // Vertex returns the same opaque Gemini thought signatures as CCA, but its replay namespace
642
+ // must stay transport-scoped: a signature minted by one Google backend must never be sent to
643
+ // another merely because the public model id and first prompt happen to match.
644
+ let vertexReplayModel: string | undefined;
645
+ let vertexReplaySession: string | undefined;
646
+ let restoreGoogleToolName = (name: string): string => name;
647
+ return {
648
+ name: "google",
649
+
650
+ // Vertex + Antigravity get Kiro-style retry/timeout + classified, redacted errors.
651
+ // Direct AI-Studio uses the canonical server transport (fetchWithTransientRetry), which
652
+ // retries transient 5xx responses through providerFetch while preserving multi-key pool
653
+ // 429 rotation and raw error formatting.
654
+ ...(provider.googleMode === "vertex" || provider.googleMode === "cloud-code-assist"
655
+ ? {
656
+ fetchResponse: (request: AdapterRequest, ctx?: AdapterFetchContext): Promise<Response> =>
657
+ (provider.googleMode === "cloud-code-assist" ? fetchAntigravityWithRetry : fetchVertexWithRetry)(request, ctx),
658
+ formatErrorBody: (status: number, _headers: Headers, payloadText: string): string =>
659
+ (provider.googleMode === "cloud-code-assist" ? safeAntigravityHttpErrorMessage : safeVertexHttpErrorMessage)(status, payloadText),
660
+ }
661
+ : {}),
662
+
663
+ async buildRequest(parsed: OcxParsedRequest) {
664
+ const routedModelId = provider.googleMode === "cloud-code-assist"
665
+ ? resolveAntigravityEffortWireModel(
666
+ parsed.modelId,
667
+ mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning),
668
+ provider.baseUrl,
669
+ ).wireModelId
670
+ : provider.googleMode === "vertex"
671
+ ? parsed.modelId
672
+ : resolveDirectGeminiWireModelId(parsed.modelId, provider.directGeminiWireRenames !== false);
673
+ // AI Studio's `-tiered` spelling is wire-only; CCA aliases may migrate to another generation.
674
+ const identityModelId = provider.googleMode === "cloud-code-assist" ? routedModelId : parsed.modelId;
675
+ const { systemInstruction, contents } = messagesToGeminiFormat(
676
+ parsed,
677
+ identityModelId,
678
+ provider.googleMode === "cloud-code-assist",
679
+ );
680
+ const tools = toolsToGeminiFormat(parsed);
681
+
682
+ const body: Record<string, unknown> = { contents };
683
+ if (systemInstruction) body.systemInstruction = systemInstruction;
684
+ if (tools) body.tools = tools;
685
+ // Only meaningful with declarations on the wire: mode ANY with an empty
686
+ // catalog is a guaranteed upstream 400.
687
+ const toolConfig = tools ? toolChoiceToGeminiToolConfig(parsed) : undefined;
688
+ if (toolConfig) body.toolConfig = toolConfig;
689
+
690
+ const generationConfig: Record<string, unknown> = {};
691
+ if (parsed.options.maxOutputTokens) generationConfig.maxOutputTokens = parsed.options.maxOutputTokens;
692
+ if (parsed.options.temperature !== undefined) generationConfig.temperature = parsed.options.temperature;
693
+ if (parsed.options.topP !== undefined) generationConfig.topP = parsed.options.topP;
694
+ if (parsed.options.stopSequences) generationConfig.stopSequences = parsed.options.stopSequences;
695
+ // Effort → thinkingLevel follows the configured ladder: any model advertising reasoning
696
+ // efforts (registry preset or user config) sends the mapped level, so a picker-selected
697
+ // effort actually reaches the wire (gemini-3.1-pro-preview ships a ladder). The original
698
+ // gemini-3.5/3.6-flash direct-mode slice stays hardcoded so unladdered configs keep their
699
+ // current behavior; Vertex participates only through an explicitly configured ladder (the
700
+ // seed google-vertex entry ships none). Image models are excluded — thinkingConfig would
701
+ // suppress the responseModalities fallback below. CCA maps effort on its envelope path.
702
+ const thinkingEligible = provider.googleMode !== "cloud-code-assist"
703
+ && !isImageCapableModel(parsed.modelId)
704
+ && (configuredReasoningEfforts(provider, parsed.modelId) !== undefined
705
+ || (provider.googleMode !== "vertex"
706
+ && (parsed.modelId === "gemini-3.5-flash" || parsed.modelId === "gemini-3.6-flash")));
707
+ const thinkingLevel = thinkingEligible
708
+ ? mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning)
709
+ : undefined;
710
+ if (thinkingLevel) generationConfig.thinkingConfig = { thinkingLevel };
711
+ if (!generationConfig.thinkingConfig && isImageCapableModel(parsed.modelId)) {
712
+ generationConfig.responseModalities = ["TEXT", "IMAGE"];
713
+ }
714
+ if (Object.keys(generationConfig).length > 0) body.generationConfig = generationConfig;
715
+
716
+ const ccaAlwaysSse = provider.googleMode === "cloud-code-assist";
717
+ const method = ccaAlwaysSse || parsed.stream ? "streamGenerateContent" : "generateContent";
718
+ const streamParam = ccaAlwaysSse || parsed.stream ? "?alt=sse" : "";
719
+ const headers: Record<string, string> = { "Content-Type": "application/json" };
720
+ if (provider.headers) Object.assign(headers, provider.headers);
721
+
722
+ if (provider.googleMode === "cloud-code-assist") {
723
+ // Google Antigravity (Cloud Code Assist): wrap the flat Gemini body in the CCA envelope.
724
+ const token = provider.apiKey?.trim();
725
+ if (!token) throw new Error("google-antigravity oauth token missing — run ocx login google-antigravity");
726
+ const base = provider.baseUrl?.trim();
727
+ if (!base) throw new Error("google-antigravity requires a non-empty baseUrl");
728
+ const canonicalBase = canonicalAntigravityHttpsHost(base) ?? base;
729
+ if (!isAntigravityHttpsHost(canonicalBase)) {
730
+ throw new Error("google-antigravity requires an HTTPS baseUrl");
731
+ }
732
+ const url = `${canonicalBase.replace(/\/+$/, "")}/v1internal:${method}${streamParam}`;
733
+ const project = provider.project;
734
+ if (!project) throw new Error("Antigravity requires a discovered Cloud Code Assist project id (re-run `ocx login google-antigravity`).");
735
+ const sessionId = antigravitySessionId(parsed);
736
+ const mappedEffort = mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning);
737
+ const { wireModelId, thinkingLevel } = resolveAntigravityEffortWireModel(
738
+ parsed.modelId,
739
+ mappedEffort,
740
+ provider.baseUrl,
741
+ );
742
+ antigravityModel = wireModelId;
743
+ antigravitySession = sessionId;
744
+ // Effort → thinkingConfig for CCA (CLIProxyAPI proven: request.generationConfig.thinkingConfig).
745
+ // Suffix/compat IDs return thinkingLevel=undefined — the suffix IS the effort, no contradiction.
746
+ if (thinkingLevel) {
747
+ const gc = (body.generationConfig ?? {}) as Record<string, unknown>;
748
+ gc.thinkingConfig = { thinkingLevel };
749
+ body.generationConfig = gc;
750
+ }
751
+ // Reasoning continuity: Gemini models re-inject cached thoughtSignatures; Claude-on-Antigravity
752
+ // sanitizes signatures inline (no cache). Both guard against the upstream 400 on bad signatures.
753
+ // The real Antigravity client puts the session id ONLY at `request.sessionId` (camelCase,
754
+ // nested) — matching CLIProxyAPI `generateStableSessionID`. An extra top-level/snake_case
755
+ // spelling is a non-first-party key, so we send the single canonical location.
756
+ const draftRequest: Record<string, unknown> = { ...body, sessionId };
757
+ // Claude-on-Antigravity forces VALIDATED function calling (the real client always sets it).
758
+ if (/claude/i.test(wireModelId)) {
759
+ // VALIDATED would defeat a client's tool_choice "none": honor it by dropping the
760
+ // declarations instead, the wire shape of a tool-less Claude turn.
761
+ if (parsed.options.toolChoice === "none") {
762
+ delete draftRequest.tools;
763
+ delete draftRequest.toolConfig;
764
+ }
765
+ const existing = (draftRequest.toolConfig ?? {}) as Record<string, unknown>;
766
+ const fcc = (existing.functionCallingConfig ?? {}) as Record<string, unknown>;
767
+ draftRequest.toolConfig = { ...existing, functionCallingConfig: { ...fcc, mode: "VALIDATED" } };
768
+ }
769
+ const compiled = compileGoogleWireBody(draftRequest);
770
+ const request = compiled.body;
771
+ if (systemInstruction) {
772
+ request.preambleConfig = { mode: "SYSTEM_INSTRUCTION_MODE_REPLACE" };
773
+ }
774
+ restoreGoogleToolName = compiled.restoreToolName;
775
+ // Compile names before replay: signatures are keyed by the exact provider-visible name.
776
+ if (Array.isArray((request as { contents?: unknown[] }).contents)) {
777
+ const contents = (request as { contents: unknown[] }).contents;
778
+ const strippedModelTail = /claude/i.test(wireModelId) ? stripTrailingClaudePrefill(contents) : false;
779
+ if (antigravityUsesReplayCache(wireModelId)) {
780
+ applyAntigravityReplay(wireModelId, sessionId, contents);
781
+ } else {
782
+ sanitizeAntigravityClaudeSignatures(contents);
783
+ }
784
+ // Claude-on-Antigravity rejects assistant-tail (model-tail in Gemini terms) histories
785
+ // as prefill: "This model does not support assistant message prefill. The conversation
786
+ // must end with a user message." Context compaction, previous_response_id expansion,
787
+ // and interrupted-turn replay can all produce a model-tail history. Append a user
788
+ // "(continue)" nudge, mirroring the anthropic adapter's tail guard (src/adapters/anthropic.ts).
789
+ // When a trailing model turn was stripped, append even if the history now ends with user.
790
+ if (/claude/i.test(wireModelId)) {
791
+ const last = contents.length > 0 ? contents[contents.length - 1] as { role?: string } : undefined;
792
+ if (strippedModelTail || !last || last.role === "model") {
793
+ contents.push({ role: "user", parts: [{ text: "(continue)" }] });
794
+ }
795
+ }
796
+ }
797
+ const envelope = {
798
+ model: wireModelId,
799
+ // The envelope's `userAgent` field is a protocol constant ("antigravity"), distinct from
800
+ // the HTTP `User-Agent` header (the real IDE UA). CLIProxyAPI `geminiToAntigravity` hardcodes
801
+ // the body field; only the header carries the versioned client string.
802
+ userAgent: "antigravity",
803
+ requestType: "agent",
804
+ project,
805
+ requestId: `agent-${crypto.randomUUID()}`,
806
+ request,
807
+ };
808
+ headers["User-Agent"] = ANTIGRAVITY_REQUEST_UA;
809
+ headers["Authorization"] = `Bearer ${token}`;
810
+ if (/claude/i.test(wireModelId)) {
811
+ headers["anthropic-beta"] = "interleaved-thinking-2025-05-14";
812
+ }
813
+ return { url, method: "POST", headers, body: JSON.stringify(envelope) };
814
+ }
815
+
816
+ if (provider.googleMode === "vertex") {
817
+ const compiled = compileGoogleWireBody(body);
818
+ restoreGoogleToolName = compiled.restoreToolName;
819
+ const vertexProject = provider.project || process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT || "api-key";
820
+ const vertexLocation = provider.location || process.env.GOOGLE_CLOUD_LOCATION || "global";
821
+ vertexReplayModel = `vertex:${vertexProject}:${vertexLocation}:${parsed.modelId}`;
822
+ vertexReplaySession = vertexReplaySessionId(parsed);
823
+ // Compile names before replay so the cache matches the exact provider-visible
824
+ // functionCall identity. This is the same bounded TTL/LRU store used by CCA, with the
825
+ // transport prefix above preventing cross-backend signature reuse (#1254).
826
+ if (Array.isArray((compiled.body as { contents?: unknown[] }).contents)) {
827
+ applyAntigravityReplay(
828
+ vertexReplayModel,
829
+ vertexReplaySession,
830
+ (compiled.body as { contents: unknown[] }).contents,
831
+ );
832
+ }
833
+ // Vertex AI: project/location endpoint with GCP ADC, or x-goog-api-key fast path.
834
+ const apiKey = resolveVertexApiKey(provider.apiKey);
835
+ if (apiKey) {
836
+ const url = `https://aiplatform.googleapis.com/v1/publishers/google/models/${parsed.modelId}:${method}${streamParam}`;
837
+ headers["x-goog-api-key"] = apiKey;
838
+ return { url, method: "POST", headers, body: JSON.stringify(compiled.body) };
839
+ }
840
+ const project = provider.project || process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT;
841
+ if (!project) throw new Error("Vertex AI requires a project id (provider.project or GOOGLE_CLOUD_PROJECT/GCLOUD_PROJECT).");
842
+ const location = provider.location || process.env.GOOGLE_CLOUD_LOCATION;
843
+ if (!location) throw new Error("Vertex AI requires a location (provider.location or GOOGLE_CLOUD_LOCATION).");
844
+ const locationError = googleVertexLocationConfigError(location);
845
+ if (locationError) throw new Error(locationError);
846
+ const host = location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com`;
847
+ const url = `https://${host}/v1/projects/${project}/locations/${location}/publishers/google/models/${parsed.modelId}:${method}${streamParam}`;
848
+ const token = await getVertexAccessToken();
849
+ headers["Authorization"] = `Bearer ${token}`;
850
+ return { url, method: "POST", headers, body: JSON.stringify(compiled.body) };
851
+ }
852
+
853
+ // ai-studio (default): Generative Language API + x-goog-api-key.
854
+ const url = `${provider.baseUrl}/v1beta/models/${routedModelId}:${method}${streamParam}`;
855
+ const apiKey = provider.apiKey?.trim();
856
+ if (!apiKey) throw new Error("google (AI Studio) requires a non-empty API key");
857
+ headers["x-goog-api-key"] = apiKey;
858
+
859
+ const compiled = compileGoogleWireBody(body);
860
+ restoreGoogleToolName = compiled.restoreToolName;
861
+ return { url, method: "POST", headers, body: JSON.stringify(compiled.body) };
862
+ },
863
+
864
+ async *parseStream(response: Response, budget: TranslatorBudget): AsyncGenerator<AdapterEvent> {
865
+ if (!response.body) {
866
+ yield { type: "error", message: "No response body" };
867
+ return;
868
+ }
869
+ // Streaming responses are processed incrementally (SSE chunks), so the full body
870
+ // is never buffered — no Content-Length pre-check is needed here. Per-image size
871
+ // protection is enforced on each chunk via MAX_ENCODED_BYTES_PER_IMAGE before
872
+ // materializeInlineImage is called (see the inline.data check below).
873
+
874
+ const reader = response.body.getReader();
875
+ const decoder = new TextDecoder();
876
+ const budgetEncoder = new TextEncoder();
877
+ let buffer = "";
878
+ let bufferBytes = 0;
879
+ // Raw unterminated-line bytes, independent of TextDecoder's pending UTF-8
880
+ // state. `bufferBytes` is the decoded residual and can undercount by 1–3
881
+ // bytes when a chunk ends mid-character.
882
+ let incompleteLineBytes = 0;
883
+ let pendingUsage: OcxUsage | undefined;
884
+ let toolCallsStarted = 0;
885
+ let lastFinishReason: string | undefined;
886
+ let sawAnyFrame = false;
887
+ let sawTerminalSignal = false;
888
+ let pendingStreamThoughtSig: string | undefined;
889
+
890
+ const handleDataLine = async function* (line: string): AsyncGenerator<AdapterEvent, "continue" | "content" | "terminate"> {
891
+ const payload = line.slice(5).trim();
892
+ if (!payload) return "continue";
893
+ if (budgetEncoder.encode(payload).byteLength > sseFrameMaxBytes) {
894
+ yield { type: "error", message: `upstream SSE data frame exceeds ${sseFrameMaxBytes} bytes` };
895
+ return "terminate";
896
+ }
897
+ let emittedContentEvent = false;
898
+
899
+ let parsed: unknown;
900
+ try {
901
+ parsed = JSON.parse(payload);
902
+ } catch {
903
+ yield { type: "error", message: "malformed upstream SSE data frame" };
904
+ return "terminate";
905
+ }
906
+ // `JSON.parse("null")` returns null rather than throwing, so the catch above cannot cover
907
+ // it and the `chunk.error` read below crashed the stream (see openai-chat.ts). Skip such a
908
+ // frame rather than terminating, for the reason given there: it is padding between real
909
+ // frames, not a broken stream. Deliberately returns BEFORE `sawAnyFrame`, so a stream made
910
+ // only of non-record frames still fails the terminal-signal check below instead of
911
+ // completing empty. An unparseable frame stays terminal.
912
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
913
+ return "continue";
914
+ }
915
+ const chunk = parsed as Record<string, unknown>;
916
+ sawAnyFrame = true;
917
+
918
+ // Inline provider error inside a 200 stream → terminal error (see openai-chat.ts).
919
+ if (chunk.error) {
920
+ const err = chunk.error as { message?: string } | undefined;
921
+ // Clear-on-invalid: a signature rejection means our replayed thoughtSignatures are stale.
922
+ // Drop the cache entry so the next turn starts clean instead of re-injecting a bad sig.
923
+ const replayModel = provider.googleMode === "cloud-code-assist" ? antigravityModel : vertexReplayModel;
924
+ const replaySession = provider.googleMode === "cloud-code-assist" ? antigravitySession : vertexReplaySession;
925
+ if ((provider.googleMode === "cloud-code-assist" || provider.googleMode === "vertex")
926
+ && replayModel && replaySession
927
+ && /signature|invalid_argument|invalid argument/i.test(err?.message ?? "")) {
928
+ clearAntigravityReplay(replayModel, replaySession);
929
+ }
930
+ yield { type: "error", message: err?.message ?? "upstream error" };
931
+ return "terminate";
932
+ }
933
+
934
+ // Antigravity (CCA) nests the standard Gemini payload under `response`.
935
+ let root = chunk;
936
+ if (provider.googleMode === "cloud-code-assist") {
937
+ const wrapped = chunk.response;
938
+ if (!wrapped || typeof wrapped !== "object" || Array.isArray(wrapped)) {
939
+ yield { type: "error", message: "google-antigravity response missing response wrapper" };
940
+ return "terminate";
941
+ }
942
+ root = wrapped as Record<string, unknown>;
943
+ }
944
+ // usageMetadata is a top-level field independent of candidates; read it BEFORE the
945
+ // candidates guard so a usage-only final chunk is not dropped.
946
+ const usageMeta = root.usageMetadata as Record<string, number> | undefined;
947
+ if (usageMeta) {
948
+ // Accumulate usage; emit a single terminal `done` post-loop so usage is never
949
+ // dropped on EOF and the stream never yields two `done` events.
950
+ pendingUsage = usageFromGemini(usageMeta);
951
+ sawTerminalSignal = true;
952
+ }
953
+ const rawCandidates = root.candidates;
954
+ // `null` is an absence encoding, not corruption, and terminating on it is the #1219
955
+ // failure mode one rung in: a `{"candidates":null}` frame arriving between a content
956
+ // delta and the finish chunk killed a turn whose answer had already fully arrived. An
957
+ // absent key and an empty array are already skipped here; `null` joins them. A non-null
958
+ // non-array container is still claimed structure the parser cannot read, and stays
959
+ // terminal.
960
+ if (rawCandidates === undefined || rawCandidates === null) return "continue";
961
+ if (!Array.isArray(rawCandidates)) {
962
+ yield invalidGoogleShapeEvent({
963
+ reason: "candidates_not_array",
964
+ valueType: googleStructuralValueType(rawCandidates),
965
+ });
966
+ return "terminate";
967
+ }
968
+ if (rawCandidates.length === 0) return "continue";
969
+ const rawCandidate = rawCandidates[0];
970
+ if (!isGoogleRecord(rawCandidate)) {
971
+ // Unlike a root `data: null` keepalive, this is a claimed response candidate. Treat it
972
+ // as terminal protocol corruption so the turn cannot complete after silently losing
973
+ // a candidate or tool call (#1325).
974
+ yield invalidGoogleShapeEvent({
975
+ reason: "candidate_not_object",
976
+ valueType: googleStructuralValueType(rawCandidate),
977
+ });
978
+ return "terminate";
979
+ }
980
+ const candidate = rawCandidate as {
981
+ content?: unknown;
982
+ finishReason?: string;
983
+ };
984
+
985
+ if (typeof candidate.finishReason === "string" && candidate.finishReason) {
986
+ lastFinishReason = candidate.finishReason;
987
+ sawTerminalSignal = true;
988
+ }
989
+
990
+ // One rung below the candidate guard above, same rule: this is claimed content, not
991
+ // padding, so it fails closed rather than being iterated or silently dropped (#1325).
992
+ const rawContent: unknown = candidate.content;
993
+ const invalidContent = diagnoseGoogleContent(rawContent);
994
+ if (invalidContent) {
995
+ yield invalidGoogleShapeEvent(invalidContent);
996
+ return "terminate";
997
+ }
998
+ const rawParts: unknown = isGoogleRecord(rawContent) ? rawContent.parts : undefined;
999
+ let parts: GoogleResponsePart[] | undefined;
1000
+ if (rawParts !== undefined && rawParts !== null) {
1001
+ const invalidParts = diagnoseGoogleParts(rawParts);
1002
+ if (invalidParts) {
1003
+ yield invalidGoogleShapeEvent(invalidParts);
1004
+ return "terminate";
1005
+ }
1006
+ parts = rawParts as GoogleResponsePart[];
1007
+ const invalidFunctionCall = diagnoseGoogleFunctionCalls(parts);
1008
+ if (invalidFunctionCall) {
1009
+ yield invalidGoogleFunctionCallEvent(invalidFunctionCall);
1010
+ return "terminate";
1011
+ }
1012
+ }
1013
+ // Record Gemini thought signatures for the next stateless tool-result turn. Vertex and
1014
+ // Antigravity use separate model namespaces so opaque provider state cannot cross routes.
1015
+ const replayModel = provider.googleMode === "cloud-code-assist" ? antigravityModel : vertexReplayModel;
1016
+ const replaySession = provider.googleMode === "cloud-code-assist" ? antigravitySession : vertexReplaySession;
1017
+ if ((provider.googleMode === "cloud-code-assist" || provider.googleMode === "vertex")
1018
+ && parts && replayModel && replaySession) {
1019
+ pendingStreamThoughtSig = observeAntigravityReplay(
1020
+ replayModel,
1021
+ replaySession,
1022
+ parts as unknown[],
1023
+ pendingStreamThoughtSig,
1024
+ );
1025
+ }
1026
+ if (parts) {
1027
+ for (const part of parts) {
1028
+ const sig = googlePartThoughtSignature(part);
1029
+ if (part.thought === true && sig && isLikelyRealThoughtSignature(sig)) {
1030
+ pendingStreamThoughtSig = sig;
1031
+ }
1032
+ const textEvent = googlePartTextEvent(part);
1033
+ if (textEvent) {
1034
+ emittedContentEvent = true;
1035
+ yield textEvent;
1036
+ }
1037
+ const inline = (part as { inlineData?: { mimeType?: string; data?: string } }).inlineData;
1038
+ if (inline && typeof inline.data === "string") {
1039
+ if (inline.data.length > MAX_ENCODED_BYTES_PER_IMAGE) {
1040
+ yield { type: "error", message: "inline image exceeds per-image size cap" };
1041
+ } else {
1042
+ try {
1043
+ const filePath = await materializeInlineImage(inline.data, imageBudget);
1044
+ const escapedPath = artifactMarkdownUrl(filePath);
1045
+ emittedContentEvent = true;
1046
+ yield { type: "text_delta", text: `\n![image](${escapedPath})\n` };
1047
+ } catch {
1048
+ yield { type: "error", message: "failed to materialize inline image" };
1049
+ }
1050
+ }
1051
+ }
1052
+ const functionCall = googleFunctionCall(part);
1053
+ if (functionCall) {
1054
+ const id = `call_${crypto.randomUUID().slice(0, 8)}`;
1055
+ toolCallsStarted++;
1056
+ emittedContentEvent = true;
1057
+ const restoredName = restoreGoogleToolName(functionCall.name);
1058
+ yield {
1059
+ type: "tool_call_start",
1060
+ id,
1061
+ name: restoredName,
1062
+ ...googleToolCallMetadataFromPart(part, pendingStreamThoughtSig),
1063
+ };
1064
+ yield { type: "tool_call_delta", arguments: JSON.stringify(functionCall.args ?? {}) };
1065
+ yield { type: "tool_call_end" };
1066
+ }
1067
+ }
1068
+ }
1069
+ return emittedContentEvent ? "content" : "continue";
1070
+ };
1071
+ const imageBudget = createImageBudget();
1072
+
1073
+ try {
1074
+ while (true) {
1075
+ const { done, value } = await reader.read();
1076
+ if (done) break;
1077
+ const incoming = value ?? new Uint8Array();
1078
+ // Cap each incomplete line on raw bytes before decode and before waiting
1079
+ // for a newline — otherwise a single unterminated data: payload can grow
1080
+ // without bound, buffer.length is UTF-16 units, and a mid-character
1081
+ // decode residual undercounts the true line. Reset at each newline so
1082
+ // several sub-cap frames in one network chunk are not rejected as one
1083
+ // oversized frame.
1084
+ const lineScan = scanSseLineBytes(incompleteLineBytes, incoming);
1085
+ if (lineScan.maximum > sseFrameMaxBytes) {
1086
+ yield { type: "error", message: `upstream SSE data frame exceeds ${sseFrameMaxBytes} bytes` };
1087
+ try { await reader.cancel(); } catch { /* ignore */ }
1088
+ return;
1089
+ }
1090
+ incompleteLineBytes = lineScan.residual;
1091
+ const nextBuffer = buffer + decoder.decode(value, { stream: true });
1092
+ const nextBufferBytes = budgetEncoder.encode(nextBuffer).byteLength;
1093
+ const appendReservation = budget.reserveTransient(nextBufferBytes, { kind: "live_transient" });
1094
+ buffer = nextBuffer;
1095
+ appendReservation.commitRetained();
1096
+ budget.releaseRetained(bufferBytes, { kind: "live_transient" });
1097
+ bufferBytes = nextBufferBytes;
1098
+
1099
+ const lines = buffer.split("\n");
1100
+ buffer = lines.pop() ?? "";
1101
+ const residualBytes = budgetEncoder.encode(buffer).byteLength;
1102
+ const residualReservation = budget.reserveTransient(residualBytes, { kind: "live_transient" });
1103
+ residualReservation.commitRetained();
1104
+ budget.releaseRetained(bufferBytes, { kind: "live_transient" });
1105
+ bufferBytes = residualBytes;
1106
+
1107
+ let sawLiveness = false;
1108
+ let sawContentEvent = false;
1109
+ for (const line of lines) {
1110
+ if (line.startsWith("data:")) {
1111
+ const result = yield* handleDataLine(line);
1112
+ if (result === "terminate") return;
1113
+ if (result === "content") sawContentEvent = true;
1114
+ continue;
1115
+ }
1116
+ sawLiveness = true;
1117
+ if (line.startsWith(":") || !line.trim()) continue;
1118
+ debugDroppedFrame("google", line);
1119
+ }
1120
+ if (sawLiveness && !sawContentEvent) yield { type: "heartbeat" };
1121
+ }
1122
+ buffer += decoder.decode();
1123
+ if (buffer.trim().length > 0) {
1124
+ const residual = buffer.trim();
1125
+ if (residual.startsWith(":")) {
1126
+ yield { type: "heartbeat" };
1127
+ } else if (!residual.startsWith("data:")) {
1128
+ yield { type: "error", message: "upstream stream ended with an incomplete SSE frame — possible truncation" };
1129
+ return;
1130
+ } else if ((yield* handleDataLine(residual)) === "terminate") return;
1131
+ }
1132
+ // Fail-closed: a turn cut off mid tool call (MAX_TOKENS / MALFORMED_FUNCTION_CALL) surfaces
1133
+ // an error instead of a silently-incomplete done. Mirrors kiro-truncation.
1134
+ if ((provider.googleMode === "vertex" || provider.googleMode === "cloud-code-assist")
1135
+ && isVertexTruncatedTurn(lastFinishReason, toolCallsStarted)) {
1136
+ yield { type: "error", message: vertexTruncationErrorMessage(lastFinishReason) };
1137
+ return;
1138
+ }
1139
+ if (!sawAnyFrame || !sawTerminalSignal) {
1140
+ yield { type: "error", message: "upstream stream ended without a terminal signal — possible truncation" };
1141
+ return;
1142
+ }
1143
+ const stopReason = lastFinishReason === "MAX_TOKENS"
1144
+ ? "max_tokens"
1145
+ : ["SAFETY", "RECITATION", "BLOCKLIST", "PROHIBITED_CONTENT", "SPII"].includes(lastFinishReason ?? "")
1146
+ ? "content_filter"
1147
+ : undefined;
1148
+ yield {
1149
+ type: "done",
1150
+ usage: pendingUsage,
1151
+ ...(stopReason ? { stopReason } : {}),
1152
+ };
1153
+ } catch (error) {
1154
+ if (!isTranslatorBudgetExceededError(error)) throw error;
1155
+ try { await reader.cancel(error); } catch { /* already closed */ }
1156
+ yield {
1157
+ type: "error",
1158
+ status: 502,
1159
+ errorType: "upstream_error",
1160
+ code: "translation_buffer_limit",
1161
+ message: "upstream translation buffer exceeded the safe limit",
1162
+ };
1163
+ } finally {
1164
+ budget.releaseRetained(bufferBytes, { kind: "live_transient" });
1165
+ reader.releaseLock();
1166
+ }
1167
+ },
1168
+
1169
+ async parseResponse(response: Response, budget: TranslatorBudget): Promise<AdapterEvent[]> {
1170
+ // Cloud Code Assist exposes only the SSE transport. Unary callers still use this
1171
+ // buffered adapter entry point, so collect the exact same events parseStream emits
1172
+ // instead of maintaining a second CCA JSON parser.
1173
+ if (provider.googleMode === "cloud-code-assist") {
1174
+ const events: AdapterEvent[] = [];
1175
+ let previousTail: AdapterEvent | undefined;
1176
+ try {
1177
+ for await (const event of this.parseStream(response, budget)) {
1178
+ retainTranslatedEvent(event, budget, previousTail);
1179
+ events.push(event);
1180
+ previousTail = event;
1181
+ }
1182
+ return events;
1183
+ } catch (error) {
1184
+ for (const event of events) releaseTranslatedEvent(event, budget);
1185
+ if (!isTranslatorBudgetExceededError(error)) throw error;
1186
+ return [{
1187
+ type: "error",
1188
+ status: 502,
1189
+ errorType: "upstream_error",
1190
+ code: "translation_buffer_limit",
1191
+ message: "upstream translation buffer exceeded the safe limit",
1192
+ }];
1193
+ }
1194
+ }
1195
+ // Reject oversized responses before JSON parse. Prefer Content-Length when
1196
+ // present and truthful; always stream-read with a hard byte cap so a missing
1197
+ // or lying Content-Length cannot force a full in-memory buffer + parse.
1198
+ const contentLength = Number(response.headers.get("content-length"));
1199
+ if (Number.isFinite(contentLength) && contentLength > MAX_RESPONSE_BYTES) {
1200
+ try { await response.body?.cancel(); } catch { /* ignore */ }
1201
+ return [{ type: "error", message: `google response too large (content-length ${contentLength} exceeds ${MAX_RESPONSE_BYTES} bytes)` }];
1202
+ }
1203
+ let rawText: string;
1204
+ let rawTextBytes = 0;
1205
+ try {
1206
+ const reader = response.body?.getReader();
1207
+ if (!reader) return [{ type: "error", message: "google response had no body" }];
1208
+ const chunks: Uint8Array[] = [];
1209
+ let total = 0;
1210
+ try {
1211
+ for (;;) {
1212
+ const { done, value } = await reader.read();
1213
+ if (done) break;
1214
+ total += value.byteLength;
1215
+ if (total > MAX_RESPONSE_BYTES) {
1216
+ await reader.cancel().catch(() => {});
1217
+ return [{ type: "error", message: `google response too large (exceeded ${MAX_RESPONSE_BYTES} bytes)` }];
1218
+ }
1219
+ budget.chargeRetained(value.byteLength, { kind: "retained_collectors" });
1220
+ chunks.push(value);
1221
+ }
1222
+ } finally {
1223
+ try { await reader.cancel(); } catch { /* ignore */ }
1224
+ reader.releaseLock();
1225
+ }
1226
+ const bytesReservation = budget.reserveTransient(total, { kind: "retained_collectors" });
1227
+ const bytes = new Uint8Array(total);
1228
+ let offset = 0;
1229
+ for (const chunk of chunks) {
1230
+ bytes.set(chunk, offset);
1231
+ offset += chunk.byteLength;
1232
+ }
1233
+ bytesReservation.commitRetained();
1234
+ budget.releaseRetained(total, { kind: "retained_collectors" });
1235
+ rawText = new TextDecoder().decode(bytes);
1236
+ rawTextBytes = new TextEncoder().encode(rawText).byteLength;
1237
+ const textReservation = budget.reserveTransient(rawTextBytes, { kind: "retained_collectors" });
1238
+ textReservation.commitRetained();
1239
+ budget.releaseRetained(total, { kind: "retained_collectors" });
1240
+ } catch (err) {
1241
+ return [{ type: "error", message: err instanceof Error ? err.message : "failed to read google response body" }];
1242
+ }
1243
+ let raw: Record<string, unknown>;
1244
+ let rawBytes = 0;
1245
+ try {
1246
+ raw = JSON.parse(rawText) as Record<string, unknown>;
1247
+ rawBytes = new TextEncoder().encode(JSON.stringify(raw)).byteLength;
1248
+ const rawReservation = budget.reserveTransient(rawBytes, { kind: "retained_collectors" });
1249
+ rawReservation.commitRetained();
1250
+ budget.releaseRetained(rawTextBytes, { kind: "retained_collectors" });
1251
+ } catch {
1252
+ budget.releaseRetained(rawTextBytes, { kind: "retained_collectors" });
1253
+ return [{ type: "error", message: "google response was not valid JSON" }];
1254
+ }
1255
+ const finish = (events: AdapterEvent[]): AdapterEvent[] => {
1256
+ retainTranslatedEventBatch(events, budget);
1257
+ budget.releaseRetained(rawBytes, { kind: "retained_collectors" });
1258
+ rawBytes = 0;
1259
+ return events;
1260
+ };
1261
+ if (raw.error) {
1262
+ const err = raw.error as { message?: string };
1263
+ return finish([{ type: "error", message: err.message ?? "upstream error" }]);
1264
+ }
1265
+ const json = raw;
1266
+ const events: AdapterEvent[] = [];
1267
+
1268
+ const rawCandidates: unknown = json.candidates;
1269
+ // Parity with the streaming path, which has rejected a non-array `candidates` since #1332.
1270
+ // Buffered accepted `"abc"` outright (`"abc".length` is 3, so the emptiness check below
1271
+ // passed and `candidates[0]` was the character `"a"`), and reported `{}`/`5` as an absent
1272
+ // candidate list rather than a malformed one.
1273
+ if (rawCandidates !== undefined && rawCandidates !== null && !Array.isArray(rawCandidates)) {
1274
+ return finish([invalidGoogleShapeEvent({
1275
+ reason: "candidates_not_array",
1276
+ valueType: googleStructuralValueType(rawCandidates),
1277
+ })]);
1278
+ }
1279
+ const candidates = rawCandidates as { finishReason?: string }[] | undefined;
1280
+ if (!candidates?.length) {
1281
+ return finish([{ type: "error", message: "google response contained no candidates" }]);
1282
+ }
1283
+ const rawCandidate: unknown = candidates[0];
1284
+ if (!isGoogleRecord(rawCandidate)) {
1285
+ // The streaming parser already treats this as terminal protocol corruption (#1325/#1332).
1286
+ // Buffered returned a bare `done`, so a claimed-but-malformed candidate was reported to
1287
+ // the caller as a successful empty turn.
1288
+ return finish([invalidGoogleShapeEvent({
1289
+ reason: "candidate_not_object",
1290
+ valueType: googleStructuralValueType(rawCandidate),
1291
+ })]);
1292
+ }
1293
+ const candidate = rawCandidate as { content?: unknown; finishReason?: string };
1294
+ let toolCallsStarted = 0;
1295
+ const imageBudget = createImageBudget();
1296
+ const rawContent: unknown = candidate.content;
1297
+ const invalidContent = diagnoseGoogleContent(rawContent);
1298
+ if (invalidContent) return finish([invalidGoogleShapeEvent(invalidContent)]);
1299
+ const rawParts: unknown = isGoogleRecord(rawContent) ? rawContent.parts : undefined;
1300
+ if (rawParts !== undefined && rawParts !== null) {
1301
+ const invalidParts = diagnoseGoogleParts(rawParts);
1302
+ if (invalidParts) return finish([invalidGoogleShapeEvent(invalidParts)]);
1303
+ const parts = rawParts as GoogleResponsePart[];
1304
+ const invalidFunctionCall = diagnoseGoogleFunctionCalls(parts);
1305
+ if (invalidFunctionCall) return finish([invalidGoogleFunctionCallEvent(invalidFunctionCall)]);
1306
+ // Non-streaming Google-family response: observe thought signatures for the next turn,
1307
+ // using the same transport-scoped namespace as the streaming path.
1308
+ const replayModel = vertexReplayModel;
1309
+ const replaySession = vertexReplaySession;
1310
+ if (provider.googleMode === "vertex" && replayModel && replaySession) {
1311
+ observeAntigravityReplay(replayModel, replaySession, parts as unknown[]);
1312
+ }
1313
+ let pendingThoughtSig: string | undefined;
1314
+ for (const part of parts) {
1315
+ const sig = googlePartThoughtSignature(part);
1316
+ if (part.thought === true && sig && isLikelyRealThoughtSignature(sig)) {
1317
+ pendingThoughtSig = sig;
1318
+ }
1319
+ const textEvent = googlePartTextEvent(part);
1320
+ if (textEvent) events.push(textEvent);
1321
+ const inline = (part as { inlineData?: { mimeType?: string; data?: string } }).inlineData;
1322
+ if (inline && typeof inline.data === "string") {
1323
+ if (inline.data.length > MAX_ENCODED_BYTES_PER_IMAGE) {
1324
+ events.push({ type: "error", message: "inline image exceeds per-image size cap" });
1325
+ } else {
1326
+ try {
1327
+ const filePath = await materializeInlineImage(inline.data, imageBudget);
1328
+ const escapedPath = artifactMarkdownUrl(filePath);
1329
+ events.push({ type: "text_delta", text: `\n![image](${escapedPath})\n` });
1330
+ } catch {
1331
+ events.push({ type: "error", message: "failed to materialize inline image" });
1332
+ }
1333
+ }
1334
+ }
1335
+ const functionCall = googleFunctionCall(part);
1336
+ if (functionCall) {
1337
+ const id = `call_${crypto.randomUUID().slice(0, 8)}`;
1338
+ toolCallsStarted++;
1339
+ events.push({
1340
+ type: "tool_call_start",
1341
+ id,
1342
+ name: restoreGoogleToolName(functionCall.name),
1343
+ ...googleToolCallMetadataFromPart(part, pendingThoughtSig),
1344
+ });
1345
+ events.push({ type: "tool_call_delta", arguments: JSON.stringify(functionCall.args ?? {}) });
1346
+ events.push({ type: "tool_call_end" });
1347
+ }
1348
+ }
1349
+ }
1350
+
1351
+ // Fail-closed truncation, same as the stream path: a non-stream turn cut off mid tool call
1352
+ // (MAX_TOKENS / MALFORMED_FUNCTION_CALL) surfaces an error instead of a silent done.
1353
+ if (provider.googleMode === "vertex"
1354
+ && isVertexTruncatedTurn(candidate.finishReason, toolCallsStarted)) {
1355
+ return finish([{ type: "error", message: vertexTruncationErrorMessage(candidate.finishReason) }]);
1356
+ }
1357
+
1358
+ const usage = json.usageMetadata as Record<string, number> | undefined;
1359
+ // Mirror the streaming path: a buffered turn cut off by the token limit or a content filter
1360
+ // must carry its stop reason, or the bridge sees a clean `done` and reports the truncated
1361
+ // turn as completed — and, on a compaction turn, installs the half-written summary as
1362
+ // replacement history (#422).
1363
+ const finishReason = candidate.finishReason as string | undefined;
1364
+ const stopReason = finishReason === "MAX_TOKENS"
1365
+ ? "max_tokens"
1366
+ : ["SAFETY", "RECITATION", "BLOCKLIST", "PROHIBITED_CONTENT", "SPII"].includes(finishReason ?? "")
1367
+ ? "content_filter"
1368
+ : undefined;
1369
+ events.push({
1370
+ type: "done",
1371
+ usage: usageFromGemini(usage),
1372
+ ...(stopReason ? { stopReason } : {}),
1373
+ });
1374
+ return finish(events);
1375
+ },
1376
+ };
1377
+ }