@iislee/opencodex 2.11.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 (476) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +225 -0
  3. package/assets/architecture.png +0 -0
  4. package/assets/banner.png +0 -0
  5. package/assets/claude-code-models.gif +0 -0
  6. package/assets/codex-app-picker.png +0 -0
  7. package/bin/ocx.mjs +451 -0
  8. package/bin/package-main.mjs +9 -0
  9. package/gui/dist/assets/index-DTpMHS4F.js +67 -0
  10. package/gui/dist/assets/index-ZNVDE3C7.css +1 -0
  11. package/gui/dist/favicon.png +0 -0
  12. package/gui/dist/icons.svg +24 -0
  13. package/gui/dist/index.html +25 -0
  14. package/gui/dist/logo.png +0 -0
  15. package/gui/dist/provider-icons/alibaba-color.svg +1 -0
  16. package/gui/dist/provider-icons/antigravity-color.svg +1 -0
  17. package/gui/dist/provider-icons/antigravity.svg +1 -0
  18. package/gui/dist/provider-icons/claude-color.svg +1 -0
  19. package/gui/dist/provider-icons/claude.svg +1 -0
  20. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
  21. package/gui/dist/provider-icons/copilot-color.svg +1 -0
  22. package/gui/dist/provider-icons/copilot.svg +1 -0
  23. package/gui/dist/provider-icons/cursor-color.svg +2 -0
  24. package/gui/dist/provider-icons/cursor.svg +2 -0
  25. package/gui/dist/provider-icons/deepseek-color.svg +1 -0
  26. package/gui/dist/provider-icons/discord.svg +1 -0
  27. package/gui/dist/provider-icons/firepass-color.svg +1 -0
  28. package/gui/dist/provider-icons/fireworks-color.svg +1 -0
  29. package/gui/dist/provider-icons/gemini-color.svg +1 -0
  30. package/gui/dist/provider-icons/gemini.svg +1 -0
  31. package/gui/dist/provider-icons/github-copilot-color.svg +1 -0
  32. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -0
  33. package/gui/dist/provider-icons/grok-color.svg +1 -0
  34. package/gui/dist/provider-icons/grok.svg +1 -0
  35. package/gui/dist/provider-icons/groq-color.svg +1 -0
  36. package/gui/dist/provider-icons/huggingface-color.svg +1 -0
  37. package/gui/dist/provider-icons/kimi-color.svg +1 -0
  38. package/gui/dist/provider-icons/kiro-color.svg +15 -0
  39. package/gui/dist/provider-icons/kiro.svg +14 -0
  40. package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
  41. package/gui/dist/provider-icons/mistral-color.svg +1 -0
  42. package/gui/dist/provider-icons/moonshot-color.svg +1 -0
  43. package/gui/dist/provider-icons/nvidia-color.svg +1 -0
  44. package/gui/dist/provider-icons/ollama-color.svg +1 -0
  45. package/gui/dist/provider-icons/openai.svg +1 -0
  46. package/gui/dist/provider-icons/opencode.svg +1 -0
  47. package/gui/dist/provider-icons/openrouter-color.svg +1 -0
  48. package/gui/dist/provider-icons/pi.svg +21 -0
  49. package/gui/dist/provider-icons/qianfan-color.svg +1 -0
  50. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
  51. package/gui/dist/provider-icons/telegram.svg +1 -0
  52. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
  53. package/gui/dist/provider-icons/vllm-color.svg +1 -0
  54. package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
  55. package/package.json +102 -0
  56. package/src/AGENTS.md +28 -0
  57. package/src/adapters/anthropic-image-guard.ts +251 -0
  58. package/src/adapters/anthropic-image-normalize.ts +518 -0
  59. package/src/adapters/anthropic.ts +1003 -0
  60. package/src/adapters/azure.ts +36 -0
  61. package/src/adapters/base.ts +72 -0
  62. package/src/adapters/client-fingerprint.ts +59 -0
  63. package/src/adapters/cursor/arg-codec.ts +38 -0
  64. package/src/adapters/cursor/arg-normalize.ts +104 -0
  65. package/src/adapters/cursor/cursor-errors.ts +165 -0
  66. package/src/adapters/cursor/discovery.ts +276 -0
  67. package/src/adapters/cursor/effort-map.ts +127 -0
  68. package/src/adapters/cursor/exec-policy.ts +88 -0
  69. package/src/adapters/cursor/framing.ts +211 -0
  70. package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
  71. package/src/adapters/cursor/kv-store.ts +52 -0
  72. package/src/adapters/cursor/live-models.ts +153 -0
  73. package/src/adapters/cursor/live-smoke-gate.ts +41 -0
  74. package/src/adapters/cursor/live-transport.ts +1214 -0
  75. package/src/adapters/cursor/mcp-config.ts +42 -0
  76. package/src/adapters/cursor/mcp-manager.ts +333 -0
  77. package/src/adapters/cursor/message-mapper.ts +49 -0
  78. package/src/adapters/cursor/native-exec-common.ts +55 -0
  79. package/src/adapters/cursor/native-exec-desktop.ts +184 -0
  80. package/src/adapters/cursor/native-exec-fs.ts +329 -0
  81. package/src/adapters/cursor/native-exec-mcp.ts +153 -0
  82. package/src/adapters/cursor/native-exec-network.ts +43 -0
  83. package/src/adapters/cursor/native-exec-shell.ts +548 -0
  84. package/src/adapters/cursor/native-exec-tools.ts +118 -0
  85. package/src/adapters/cursor/native-exec.ts +576 -0
  86. package/src/adapters/cursor/protobuf-events.ts +563 -0
  87. package/src/adapters/cursor/protobuf-request.ts +714 -0
  88. package/src/adapters/cursor/request-builder.ts +255 -0
  89. package/src/adapters/cursor/thread-continuity.ts +67 -0
  90. package/src/adapters/cursor/tool-definitions.ts +505 -0
  91. package/src/adapters/cursor/transport-retry.ts +132 -0
  92. package/src/adapters/cursor/transport.ts +57 -0
  93. package/src/adapters/cursor/types.ts +52 -0
  94. package/src/adapters/cursor.ts +196 -0
  95. package/src/adapters/google-antigravity-replay.ts +303 -0
  96. package/src/adapters/google-antigravity-wire.ts +108 -0
  97. package/src/adapters/google-errors.ts +85 -0
  98. package/src/adapters/google-http.ts +100 -0
  99. package/src/adapters/google-tool-schema.ts +173 -0
  100. package/src/adapters/google-truncation.ts +13 -0
  101. package/src/adapters/google-wire-compiler.ts +232 -0
  102. package/src/adapters/google.ts +758 -0
  103. package/src/adapters/identity.ts +44 -0
  104. package/src/adapters/image.ts +23 -0
  105. package/src/adapters/kiro-constants.ts +16 -0
  106. package/src/adapters/kiro-errors.ts +197 -0
  107. package/src/adapters/kiro-events.ts +179 -0
  108. package/src/adapters/kiro-images.ts +129 -0
  109. package/src/adapters/kiro-retry.ts +312 -0
  110. package/src/adapters/kiro-thinking.ts +96 -0
  111. package/src/adapters/kiro-tool-fallback.ts +36 -0
  112. package/src/adapters/kiro-tools.ts +215 -0
  113. package/src/adapters/kiro-truncation.ts +33 -0
  114. package/src/adapters/kiro-wire.ts +129 -0
  115. package/src/adapters/kiro.ts +1898 -0
  116. package/src/adapters/mimo-free.ts +263 -0
  117. package/src/adapters/openai-chat.ts +1005 -0
  118. package/src/adapters/openai-responses.ts +1137 -0
  119. package/src/adapters/run-turn-queue.ts +114 -0
  120. package/src/adapters/tool-catalog-nudge.ts +71 -0
  121. package/src/adapters/upstream-http-error.ts +48 -0
  122. package/src/bridge.ts +1619 -0
  123. package/src/chat/inbound.ts +295 -0
  124. package/src/chat/outbound.ts +765 -0
  125. package/src/claude/agents-inject.ts +243 -0
  126. package/src/claude/alias.ts +149 -0
  127. package/src/claude/auth-detect.ts +229 -0
  128. package/src/claude/auth-mode-migration.ts +32 -0
  129. package/src/claude/auth-mode.ts +62 -0
  130. package/src/claude/context-windows.ts +189 -0
  131. package/src/claude/desktop-3p-guard.ts +35 -0
  132. package/src/claude/desktop-3p-paths.ts +84 -0
  133. package/src/claude/desktop-3p.ts +381 -0
  134. package/src/claude/desktop-health.ts +26 -0
  135. package/src/claude/desktop-profile.ts +263 -0
  136. package/src/claude/gateway-cache.ts +70 -0
  137. package/src/claude/inbound-debug.ts +163 -0
  138. package/src/claude/inbound.ts +509 -0
  139. package/src/claude/model-info.ts +151 -0
  140. package/src/claude/outbound.ts +872 -0
  141. package/src/cli/access.ts +108 -0
  142. package/src/cli/account-api.ts +268 -0
  143. package/src/cli/account-auth.ts +223 -0
  144. package/src/cli/account-extended.ts +350 -0
  145. package/src/cli/account.ts +275 -0
  146. package/src/cli/agent-driven.ts +70 -0
  147. package/src/cli/agent.ts +184 -0
  148. package/src/cli/catalog-prewarm.ts +27 -0
  149. package/src/cli/claude-desktop.ts +188 -0
  150. package/src/cli/claude.ts +286 -0
  151. package/src/cli/codex-shim-autorestore.ts +45 -0
  152. package/src/cli/combo.ts +119 -0
  153. package/src/cli/config-command.ts +145 -0
  154. package/src/cli/debug.ts +228 -0
  155. package/src/cli/doctor.ts +930 -0
  156. package/src/cli/export-command.ts +187 -0
  157. package/src/cli/help.ts +354 -0
  158. package/src/cli/index.ts +1113 -0
  159. package/src/cli/init.ts +224 -0
  160. package/src/cli/integrations.ts +142 -0
  161. package/src/cli/interactive-confirm.ts +133 -0
  162. package/src/cli/internal-dispatch.ts +20 -0
  163. package/src/cli/models-runtime.ts +212 -0
  164. package/src/cli/models.ts +336 -0
  165. package/src/cli/observe.ts +117 -0
  166. package/src/cli/opencode.ts +586 -0
  167. package/src/cli/pi.ts +188 -0
  168. package/src/cli/provider-runtime.ts +162 -0
  169. package/src/cli/provider.ts +463 -0
  170. package/src/cli/runtime-api.ts +325 -0
  171. package/src/cli/star-prompt.ts +155 -0
  172. package/src/cli/status-oauth.ts +78 -0
  173. package/src/cli/status.ts +321 -0
  174. package/src/cli/sync-cloud.ts +283 -0
  175. package/src/cli/system-command.ts +112 -0
  176. package/src/cli/tray-proxy.ts +52 -0
  177. package/src/cli/v2.ts +173 -0
  178. package/src/cli.ts +10 -0
  179. package/src/clients/config-export.ts +377 -0
  180. package/src/clients/effective-status.ts +385 -0
  181. package/src/clients/probes/agy.ts +55 -0
  182. package/src/clients/probes/cc-switch.ts +110 -0
  183. package/src/clients/probes/claude.ts +90 -0
  184. package/src/clients/probes/codex.ts +125 -0
  185. package/src/clients/probes/grok.ts +29 -0
  186. package/src/clients/probes/opencode.ts +109 -0
  187. package/src/clients/probes/paseo.ts +55 -0
  188. package/src/clients/probes/pi.ts +55 -0
  189. package/src/cloud/onedrive-auth.ts +666 -0
  190. package/src/cloud/onedrive-graph.ts +108 -0
  191. package/src/cloud/settings.ts +75 -0
  192. package/src/cloud/sync.ts +212 -0
  193. package/src/cloud/types.ts +56 -0
  194. package/src/cloud/vault.ts +89 -0
  195. package/src/codex/account-id.ts +34 -0
  196. package/src/codex/account-label.ts +34 -0
  197. package/src/codex/account-lifecycle.ts +55 -0
  198. package/src/codex/account-namespace-match.ts +63 -0
  199. package/src/codex/account-namespaces.ts +149 -0
  200. package/src/codex/account-pause.ts +20 -0
  201. package/src/codex/account-runtime-state.ts +31 -0
  202. package/src/codex/account-store.ts +517 -0
  203. package/src/codex/account-usability.ts +20 -0
  204. package/src/codex/app-server-processes.ts +756 -0
  205. package/src/codex/auth-api.ts +1540 -0
  206. package/src/codex/auth-collision.ts +107 -0
  207. package/src/codex/auth-context.ts +352 -0
  208. package/src/codex/autostart-health.ts +149 -0
  209. package/src/codex/catalog/aggregation.ts +378 -0
  210. package/src/codex/catalog/bundled.ts +251 -0
  211. package/src/codex/catalog/effort.ts +355 -0
  212. package/src/codex/catalog/metadata.ts +180 -0
  213. package/src/codex/catalog/parsing.ts +456 -0
  214. package/src/codex/catalog/provider-fetch.ts +902 -0
  215. package/src/codex/catalog/sync.ts +620 -0
  216. package/src/codex/catalog.ts +12 -0
  217. package/src/codex/data/upstream-models.json +830 -0
  218. package/src/codex/exec-invocation.ts +22 -0
  219. package/src/codex/features.ts +969 -0
  220. package/src/codex/history-migration-guardian.ts +102 -0
  221. package/src/codex/history-provider.ts +776 -0
  222. package/src/codex/home.ts +206 -0
  223. package/src/codex/inject.ts +799 -0
  224. package/src/codex/injected-marker.ts +72 -0
  225. package/src/codex/journal.ts +163 -0
  226. package/src/codex/main-account-cache.ts +32 -0
  227. package/src/codex/main-account.ts +40 -0
  228. package/src/codex/model-cache.ts +227 -0
  229. package/src/codex/paths.ts +65 -0
  230. package/src/codex/plugins-doctor.ts +242 -0
  231. package/src/codex/pool-rotation.ts +225 -0
  232. package/src/codex/project-config-warnings.ts +411 -0
  233. package/src/codex/quota.ts +411 -0
  234. package/src/codex/refresh.ts +53 -0
  235. package/src/codex/routing.ts +1477 -0
  236. package/src/codex/runtime.ts +538 -0
  237. package/src/codex/shim.ts +1189 -0
  238. package/src/codex/subagent-defaults.ts +550 -0
  239. package/src/codex/subagent-model-fallback.ts +469 -0
  240. package/src/codex/sync.ts +130 -0
  241. package/src/codex/warmup.ts +192 -0
  242. package/src/codex/websocket-registry.ts +100 -0
  243. package/src/combos/failover.ts +140 -0
  244. package/src/combos/index.ts +41 -0
  245. package/src/combos/request.ts +62 -0
  246. package/src/combos/resolve.ts +232 -0
  247. package/src/combos/types.ts +326 -0
  248. package/src/config.ts +2356 -0
  249. package/src/generated/jawcode-model-metadata.ts +104 -0
  250. package/src/github/star-state.ts +203 -0
  251. package/src/grok/inject.ts +545 -0
  252. package/src/grok/status.ts +121 -0
  253. package/src/grok/sync.ts +103 -0
  254. package/src/grok/usage-hook/report.mjs +348 -0
  255. package/src/grok/usage-hook.ts +278 -0
  256. package/src/images/artifacts.ts +516 -0
  257. package/src/images/fulfill-video.ts +163 -0
  258. package/src/images/fulfill.ts +149 -0
  259. package/src/images/index.ts +4 -0
  260. package/src/images/loop.ts +829 -0
  261. package/src/images/plan.ts +133 -0
  262. package/src/images/synthetic-tool.ts +133 -0
  263. package/src/images/types.ts +41 -0
  264. package/src/images/xai-client.ts +141 -0
  265. package/src/images/xai-video-client.ts +163 -0
  266. package/src/index.ts +22 -0
  267. package/src/lib/abort.ts +146 -0
  268. package/src/lib/admin-secrets.ts +25 -0
  269. package/src/lib/admission.ts +83 -0
  270. package/src/lib/app-owned-memory-stores.ts +173 -0
  271. package/src/lib/app-owned-memory.ts +265 -0
  272. package/src/lib/bounded-body.ts +202 -0
  273. package/src/lib/bun-binary-validator.d.mts +3 -0
  274. package/src/lib/bun-binary-validator.mjs +18 -0
  275. package/src/lib/bun-runtime.ts +71 -0
  276. package/src/lib/bun-stream-caps.ts +126 -0
  277. package/src/lib/config-ownership.ts +360 -0
  278. package/src/lib/crash-guard.ts +344 -0
  279. package/src/lib/debug-log-buffer.ts +83 -0
  280. package/src/lib/debug-settings.ts +108 -0
  281. package/src/lib/debug.ts +31 -0
  282. package/src/lib/destination-policy.ts +316 -0
  283. package/src/lib/errors.ts +364 -0
  284. package/src/lib/eventstream-decoder.ts +253 -0
  285. package/src/lib/gcp-adc.ts +341 -0
  286. package/src/lib/injection-debug-log.ts +58 -0
  287. package/src/lib/open-url.ts +25 -0
  288. package/src/lib/pinned-http.ts +151 -0
  289. package/src/lib/privacy.ts +20 -0
  290. package/src/lib/process-control.ts +165 -0
  291. package/src/lib/provider-outbound.ts +170 -0
  292. package/src/lib/provider-url.ts +14 -0
  293. package/src/lib/proxy-env.ts +18 -0
  294. package/src/lib/redact.ts +105 -0
  295. package/src/lib/retry-after.ts +55 -0
  296. package/src/lib/service-secrets.ts +25 -0
  297. package/src/lib/shadow-call.ts +30 -0
  298. package/src/lib/sidecar-tracker.ts +52 -0
  299. package/src/lib/sse-decoder.ts +323 -0
  300. package/src/lib/state-store-registrations.ts +109 -0
  301. package/src/lib/state-store-sweeper.ts +184 -0
  302. package/src/lib/test-home-guard.ts +90 -0
  303. package/src/lib/token-estimate.ts +69 -0
  304. package/src/lib/translator-budget.ts +356 -0
  305. package/src/lib/upstream-retry.ts +239 -0
  306. package/src/lib/win-exec.ts +115 -0
  307. package/src/lib/win-paths.ts +68 -0
  308. package/src/lib/windows-elevation.ts +705 -0
  309. package/src/lib/windows-secret-acl.ts +514 -0
  310. package/src/lib/winsw.ts +375 -0
  311. package/src/oauth/anthropic-routing.ts +594 -0
  312. package/src/oauth/anthropic.ts +177 -0
  313. package/src/oauth/callback-server.ts +294 -0
  314. package/src/oauth/chatgpt.ts +150 -0
  315. package/src/oauth/cursor.ts +211 -0
  316. package/src/oauth/github-copilot.ts +428 -0
  317. package/src/oauth/google-antigravity.ts +230 -0
  318. package/src/oauth/health.ts +399 -0
  319. package/src/oauth/index.ts +1174 -0
  320. package/src/oauth/key-providers.ts +108 -0
  321. package/src/oauth/kimi.ts +213 -0
  322. package/src/oauth/kiro-credentials.ts +726 -0
  323. package/src/oauth/kiro.ts +577 -0
  324. package/src/oauth/local-token-detect.ts +121 -0
  325. package/src/oauth/log.ts +48 -0
  326. package/src/oauth/login-cli.ts +163 -0
  327. package/src/oauth/pkce.ts +15 -0
  328. package/src/oauth/store.ts +630 -0
  329. package/src/oauth/token-guardian.ts +303 -0
  330. package/src/oauth/types.ts +62 -0
  331. package/src/oauth/xai.ts +241 -0
  332. package/src/pi/extensions.ts +72 -0
  333. package/src/pi/home.ts +42 -0
  334. package/src/pi/index.ts +40 -0
  335. package/src/pi/models.ts +278 -0
  336. package/src/pi/packages.ts +219 -0
  337. package/src/pi/settings.ts +365 -0
  338. package/src/pi/status.ts +68 -0
  339. package/src/pi/sync.ts +75 -0
  340. package/src/providers/alibaba-region-backup.ts +75 -0
  341. package/src/providers/alibaba-region-migration.ts +156 -0
  342. package/src/providers/alibaba-region-startup.ts +36 -0
  343. package/src/providers/antigravity-models.ts +205 -0
  344. package/src/providers/api-keys.ts +140 -0
  345. package/src/providers/base-url-choices.ts +64 -0
  346. package/src/providers/context-cap.ts +65 -0
  347. package/src/providers/derive.ts +339 -0
  348. package/src/providers/free-directory.ts +184 -0
  349. package/src/providers/github-copilot-transport.ts +56 -0
  350. package/src/providers/key-failover.ts +203 -0
  351. package/src/providers/kiro-models.ts +67 -0
  352. package/src/providers/label.ts +19 -0
  353. package/src/providers/model-discovery.ts +356 -0
  354. package/src/providers/openai-sidecar.ts +175 -0
  355. package/src/providers/openai-tier-startup.ts +27 -0
  356. package/src/providers/openai-tiers.ts +301 -0
  357. package/src/providers/openai-virtual-models.ts +82 -0
  358. package/src/providers/openrouter-routing.ts +102 -0
  359. package/src/providers/provider-id-rewrite.ts +150 -0
  360. package/src/providers/quota.ts +1260 -0
  361. package/src/providers/registry.ts +1600 -0
  362. package/src/providers/slug-codec.ts +67 -0
  363. package/src/providers/xai-transport.ts +141 -0
  364. package/src/reasoning-effort.ts +135 -0
  365. package/src/responses/compaction.ts +117 -0
  366. package/src/responses/parser.ts +656 -0
  367. package/src/responses/reasoning-envelope.ts +52 -0
  368. package/src/responses/schema.ts +159 -0
  369. package/src/responses/spill-store.ts +394 -0
  370. package/src/responses/state.ts +895 -0
  371. package/src/responses/tool-groups.ts +19 -0
  372. package/src/router.ts +425 -0
  373. package/src/server/adapter-resolve.ts +80 -0
  374. package/src/server/auth-cors.ts +530 -0
  375. package/src/server/chat-completions.ts +368 -0
  376. package/src/server/claude-messages.ts +914 -0
  377. package/src/server/effort-policy.ts +172 -0
  378. package/src/server/gui-static.ts +123 -0
  379. package/src/server/image-retry.ts +42 -0
  380. package/src/server/images.ts +476 -0
  381. package/src/server/index.ts +1126 -0
  382. package/src/server/lifecycle.ts +227 -0
  383. package/src/server/live.ts +598 -0
  384. package/src/server/management/agent-settings-routes.ts +1169 -0
  385. package/src/server/management/api-access.ts +141 -0
  386. package/src/server/management/api-key-usage.ts +167 -0
  387. package/src/server/management/body.ts +35 -0
  388. package/src/server/management/clients-routes.ts +63 -0
  389. package/src/server/management/cloud-sync-routes.ts +266 -0
  390. package/src/server/management/combo-routes.ts +220 -0
  391. package/src/server/management/config-routes.ts +422 -0
  392. package/src/server/management/context.ts +31 -0
  393. package/src/server/management/logs-usage-routes.ts +707 -0
  394. package/src/server/management/model-routes.ts +525 -0
  395. package/src/server/management/oauth-account-routes.ts +563 -0
  396. package/src/server/management/provider-routes.ts +556 -0
  397. package/src/server/management/shared.ts +277 -0
  398. package/src/server/management/sidebar-routes.ts +90 -0
  399. package/src/server/management/system-restart.ts +179 -0
  400. package/src/server/management/system-routes.ts +117 -0
  401. package/src/server/management/usage-summary-cache.ts +86 -0
  402. package/src/server/management-api.ts +215 -0
  403. package/src/server/management-auth.ts +267 -0
  404. package/src/server/memory-watchdog.ts +156 -0
  405. package/src/server/port-reclaim.ts +307 -0
  406. package/src/server/ports.ts +116 -0
  407. package/src/server/proxy-liveness.ts +201 -0
  408. package/src/server/relay-eager.ts +313 -0
  409. package/src/server/relay.ts +1049 -0
  410. package/src/server/request-decompress.ts +132 -0
  411. package/src/server/request-log-conversation.ts +168 -0
  412. package/src/server/request-log.ts +1046 -0
  413. package/src/server/responses/collaboration.ts +354 -0
  414. package/src/server/responses/compact.ts +384 -0
  415. package/src/server/responses/core.ts +2758 -0
  416. package/src/server/responses/encrypted-payload.ts +308 -0
  417. package/src/server/responses/fetch-helpers.ts +157 -0
  418. package/src/server/responses/passthrough-error.ts +78 -0
  419. package/src/server/responses/terminal-guard.ts +230 -0
  420. package/src/server/responses/upstream-error.ts +48 -0
  421. package/src/server/responses-image-gen-repair.ts +132 -0
  422. package/src/server/responses-item-id-repair.ts +224 -0
  423. package/src/server/responses.ts +9 -0
  424. package/src/server/search.ts +136 -0
  425. package/src/server/sse-payload-rewrite.ts +175 -0
  426. package/src/server/startup-action-control.ts +308 -0
  427. package/src/server/startup-health-cache.ts +113 -0
  428. package/src/server/system-env.ts +413 -0
  429. package/src/server/windows-tcp-drop.ts +184 -0
  430. package/src/server/windows-tray-control.ts +41 -0
  431. package/src/server/ws-bridge.ts +471 -0
  432. package/src/service.ts +2554 -0
  433. package/src/stall-timeout.ts +20 -0
  434. package/src/storage/cleanup-job.ts +57 -0
  435. package/src/storage/cleanup.ts +3085 -0
  436. package/src/storage/policy-job.ts +457 -0
  437. package/src/storage/policy-scheduler.ts +40 -0
  438. package/src/storage/policy-worker.ts +59 -0
  439. package/src/storage/policy.ts +527 -0
  440. package/src/storage/restore-job.ts +299 -0
  441. package/src/storage/restore-worker.ts +58 -0
  442. package/src/storage/scanner.ts +238 -0
  443. package/src/storage/storage-mutation-coordinator.ts +139 -0
  444. package/src/storage/worker-lifecycle.ts +215 -0
  445. package/src/tray/assets/opencodex-tray-offline.ico +0 -0
  446. package/src/tray/assets/opencodex-tray-online.ico +0 -0
  447. package/src/tray/assets/opencodex-tray-warning.ico +0 -0
  448. package/src/tray/assets/opencodex-tray.png +0 -0
  449. package/src/tray/windows-tray.ps1 +290 -0
  450. package/src/tray/windows.ts +730 -0
  451. package/src/types.ts +1237 -0
  452. package/src/update/badge.ts +72 -0
  453. package/src/update/index.ts +407 -0
  454. package/src/update/job.ts +1520 -0
  455. package/src/update/notify.ts +257 -0
  456. package/src/update/npm-invocation.d.mts +23 -0
  457. package/src/update/npm-invocation.mjs +94 -0
  458. package/src/update/tray-update-plan.d.mts +18 -0
  459. package/src/update/tray-update-plan.mjs +38 -0
  460. package/src/usage/cost.ts +0 -0
  461. package/src/usage/debug.ts +97 -0
  462. package/src/usage/expected-prices.ts +164 -0
  463. package/src/usage/log.ts +658 -0
  464. package/src/usage/summary.ts +585 -0
  465. package/src/usage/totals.ts +14 -0
  466. package/src/vision/anthropic-describe.ts +185 -0
  467. package/src/vision/describe.ts +125 -0
  468. package/src/vision/index.ts +467 -0
  469. package/src/web-search/anthropic-executor.ts +189 -0
  470. package/src/web-search/executor.ts +105 -0
  471. package/src/web-search/format-result.ts +89 -0
  472. package/src/web-search/index.ts +196 -0
  473. package/src/web-search/loop.ts +664 -0
  474. package/src/web-search/parse.ts +220 -0
  475. package/src/web-search/progress-stream.ts +342 -0
  476. package/src/web-search/synthetic-tool.ts +47 -0
@@ -0,0 +1,758 @@
1
+ import type { AdapterFetchContext, AdapterRequest, ProviderAdapter } from "./base";
2
+ import { debugDroppedFrame } from "../lib/debug";
3
+ import { createHash } from "node:crypto";
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
+ OcxTextContent,
12
+ OcxToolCall,
13
+ OcxUsage,
14
+ } from "../types";
15
+ import { isAllowedToolChoice, namespacedToolName, toolAllowedByChoice } from "../types";
16
+ import { contentPartsToText, parseDataUrl } from "./image";
17
+ import { getVertexAccessToken } from "../lib/gcp-adc";
18
+ import { fetchAntigravityWithRetry, fetchVertexWithRetry } from "./google-http";
19
+ import { safeAntigravityHttpErrorMessage, safeVertexHttpErrorMessage } from "./google-errors";
20
+ import { isVertexTruncationReason, vertexTruncationErrorMessage } from "./google-truncation";
21
+ import { ANTIGRAVITY_REQUEST_UA, antigravitySessionId, isLikelyRealThoughtSignature, sanitizeAntigravityClaudeSignatures } from "./google-antigravity-wire";
22
+ import { compileGoogleWireBody } from "./google-wire-compiler";
23
+ import { neutralizeIdentity } from "./identity";
24
+ import { antigravityUsesReplayCache, applyAntigravityReplay, clearAntigravityReplay, observeAntigravityReplay } from "./google-antigravity-replay";
25
+ import { resolveAntigravityEffortWireModel } from "../providers/antigravity-models";
26
+ import {
27
+ isTranslatorBudgetExceededError,
28
+ retainTranslatedEventBatch,
29
+ type TranslatorBudget,
30
+ } from "../lib/translator-budget";
31
+ import { buildNonOpenAIToolCatalogNudgeForTools } from "./tool-catalog-nudge";
32
+ import { mapReasoningEffort } from "../reasoning-effort";
33
+
34
+ // Google-family models (Gemini/Vertex/Antigravity) tend to emit long running commentary between
35
+ // tool calls. This steers them to keep the BETWEEN-STEP text to one line and reason internally
36
+ // while still driving tools to completion. The FINAL answer is explicitly exempt so task output is
37
+ // not truncated. Appended to systemInstruction for the `google` adapter only, so non-Google
38
+ // providers are unaffected.
39
+ const GOOGLE_BREVITY_INSTRUCTION = [
40
+ "Output style for this session:",
41
+ "- While you are still working (between tool calls), keep any text you emit to a single short line; do not narrate at length.",
42
+ "- Do detailed reasoning internally, not as visible intermediate output.",
43
+ "- Prefer taking the next tool action over explaining; keep calling tools until the task is complete.",
44
+ "- This applies only to intermediate progress text. Your final answer after the work is done is exempt: write it in full and at whatever length the task requires.",
45
+ ].join("\n");
46
+
47
+ /** Vertex API key: provider.apiKey if it looks real (not a sentinel), else GOOGLE_CLOUD_API_KEY env. */
48
+ function resolveVertexApiKey(optKey?: string): string | undefined {
49
+ const realKey = optKey && !optKey.startsWith("<") && optKey !== "N/A" ? optKey : undefined;
50
+ return realKey || process.env.GOOGLE_CLOUD_API_KEY;
51
+ }
52
+
53
+ /**
54
+ * Stable tool-call id for the Gemini wire `functionCall.id` / `functionResponse.id` fields.
55
+ *
56
+ * Gemini treats these ids as optional and pairs a call with its response by id when present, so
57
+ * emitting them is harmless for Gemini models. They are REQUIRED, however, for Claude-on-Antigravity:
58
+ * the backend converts the Gemini-shaped request into Anthropic `messages`, mapping
59
+ * `functionCall.id -> tool_use.id` and `functionResponse.id -> tool_result.tool_use_id`. With no id
60
+ * the conversion fails upstream with `messages.N.content.M.tool_use.id: Field required` (HTTP 400).
61
+ *
62
+ * Anthropic's `tool_use.id` only accepts `[a-zA-Z0-9_-]`, so non-conforming characters are mapped to
63
+ * `_`. To keep the mapping injective (so two distinct raw ids like `call:a` and `call/a` cannot
64
+ * collide into one `tool_use.id` within a request), a short hash of the original raw id is appended
65
+ * whenever any character had to be rewritten. The transform is deterministic, so a call id and its
66
+ * matching result id — equal at the source, since Codex pairs them — still normalize identically and
67
+ * the call/response pairing is preserved. Returns `undefined` for an empty id so the caller omits the
68
+ * field entirely rather than inventing a non-matching one.
69
+ */
70
+ function geminiToolCallId(rawId: string | undefined): string | undefined {
71
+ const raw = rawId ?? "";
72
+ if (raw.length === 0) return undefined;
73
+ const cleaned = raw.replace(/[^a-zA-Z0-9_-]/g, "_");
74
+ if (cleaned === raw) return cleaned;
75
+ // Lossy rewrite happened: disambiguate with a deterministic suffix derived from the raw id.
76
+ const suffix = createHash("sha256").update(raw).digest("hex").slice(0, 8);
77
+ return `${cleaned}_${suffix}`;
78
+ }
79
+
80
+ /**
81
+ * Inline image parts (Gemini `inline_data`) extracted from tool-result content. Only base64 data URLs
82
+ * can be inlined; a remote URL has no mime type we can supply, so it is skipped here (the textual
83
+ * result already carries an "[image]" marker via contentPartsToText).
84
+ */
85
+ function toolResultImageParts(content: string | OcxContentPart[]): unknown[] {
86
+ if (typeof content === "string") return [];
87
+ const parts: unknown[] = [];
88
+ for (const p of content) {
89
+ if (p.type !== "image") continue;
90
+ const data = parseDataUrl(p.imageUrl);
91
+ if (data) parts.push({ inline_data: { mime_type: data.mediaType, data: data.base64 } });
92
+ }
93
+ return parts;
94
+ }
95
+
96
+ /**
97
+ * Antigravity translates these Gemini `contents` into Anthropic `messages` for Claude models, and
98
+ * Anthropic rejects a text block whose `text` is empty or absent. An empty Gemini text part reaches
99
+ * that upstream as `{"type":"text"}` — a proto3 empty string is omitted from the translated JSON —
100
+ * and 400s with `messages.N.content.M.text.text: Field required` (issue #420). An empty `parts: []`
101
+ * model turn fails the same way. Gemini itself accepts both shapes, which is why this only ever
102
+ * surfaced on Claude-on-Antigravity; the guard lives here because this is where the parts are
103
+ * built. Mirrors the Anthropic adapter's own empty-block guard (src/adapters/anthropic.ts).
104
+ */
105
+ const GEMINI_EMPTY_PLACEHOLDER = "(empty)";
106
+ const GEMINI_EMPTY_TOOL_OUTPUT_PLACEHOLDER = "(empty tool output)";
107
+
108
+ /** A Gemini text part, or undefined when the value cannot form a valid non-empty text block. */
109
+ function geminiTextPart(text: unknown): { text: string } | undefined {
110
+ return typeof text === "string" && text.length > 0 ? { text } : undefined;
111
+ }
112
+
113
+ /**
114
+ * Text for `functionResponse.response.result`. `contentPartsToText` collapses an empty array — or one
115
+ * holding only empty text — to its "[image]" marker, which would claim an image the turn does not
116
+ * actually carry (`toolResultImageParts` adds none). Fall back to the placeholder unless the content
117
+ * has something representable.
118
+ */
119
+ function geminiToolResultText(content: string | OcxContentPart[]): string {
120
+ if (typeof content === "string") return content || GEMINI_EMPTY_TOOL_OUTPUT_PLACEHOLDER;
121
+ const hasContent = content.some(p => p.type === "image" || (typeof p.text === "string" && p.text.length > 0));
122
+ return hasContent ? contentPartsToText(content) : GEMINI_EMPTY_TOOL_OUTPUT_PLACEHOLDER;
123
+ }
124
+
125
+ function messagesToGeminiFormat(parsed: OcxParsedRequest): { systemInstruction?: unknown; contents: unknown[] } {
126
+ // Neutralize Codex's GPT-5 identity line (Gemini/Antigravity share this path) so a routed model
127
+ // never misreports as GPT-5/OpenAI, and never leaks the proxy identity upstream.
128
+ const toolCatalogNudge = buildNonOpenAIToolCatalogNudgeForTools(parsed.context.tools, parsed.options.toolChoice);
129
+ const systemText = neutralizeIdentity([
130
+ ...(parsed.context.systemPrompt ?? []),
131
+ ...(toolCatalogNudge ? [toolCatalogNudge] : []),
132
+ GOOGLE_BREVITY_INSTRUCTION,
133
+ ].join("\n\n"));
134
+ const systemInstruction = { parts: [{ text: systemText }] };
135
+
136
+ const contents: unknown[] = [];
137
+
138
+ for (const msg of parsed.context.messages) {
139
+ switch (msg.role) {
140
+ case "user":
141
+ case "developer": {
142
+ if (typeof msg.content === "string") {
143
+ contents.push({ role: "user", parts: [{ text: msg.content || GEMINI_EMPTY_PLACEHOLDER }] });
144
+ } else {
145
+ const parts: unknown[] = [];
146
+ for (const p of msg.content as OcxContentPart[]) {
147
+ if (p.type === "image") {
148
+ const data = parseDataUrl(p.imageUrl);
149
+ // Gemini takes base64 via inline_data; a remote URL needs a mime type we don't have, so
150
+ // fall back to a short marker rather than inlining the URL as a huge text blob.
151
+ parts.push(data ? { inline_data: { mime_type: data.mediaType, data: data.base64 } } : { text: `[image: ${p.imageUrl}]` });
152
+ continue;
153
+ }
154
+ // Drop empty/malformed text instead of emitting `{ text: "" }` or a bare `{}` part.
155
+ const textPart = geminiTextPart(p.text);
156
+ if (textPart) parts.push(textPart);
157
+ }
158
+ contents.push({ role: "user", parts: parts.length > 0 ? parts : [{ text: GEMINI_EMPTY_PLACEHOLDER }] });
159
+ }
160
+ break;
161
+ }
162
+ case "assistant": {
163
+ const aMsg = msg as OcxAssistantMessage;
164
+ const parts: unknown[] = [];
165
+ for (const p of aMsg.content) {
166
+ if (p.type === "text") {
167
+ const textPart = geminiTextPart((p as OcxTextContent).text);
168
+ if (textPart) parts.push(textPart);
169
+ } else if (p.type === "toolCall") {
170
+ const tc = p as OcxToolCall;
171
+ // Preserve the thought signature on the function-call part so Antigravity/Gemini-3
172
+ // reasoning continuity survives history-driven (stateless) turns, not just same-process
173
+ // streaming covered by the replay cache. Only forward a REAL upstream signature — the
174
+ // Responses parser also stashes synthetic item ids (`fc_...`) on this field, and sending
175
+ // those as a thoughtSignature breaks continuity (the replay cache supplies the real one).
176
+ const callId = geminiToolCallId(tc.id);
177
+ const functionCall: Record<string, unknown> = { name: namespacedToolName(tc.namespace, tc.name), args: tc.arguments };
178
+ // Claude-on-Antigravity maps this id to Anthropic `tool_use.id`; without it the upstream
179
+ // conversion 400s. Gemini accepts the optional id and pairs call/response by it.
180
+ if (callId !== undefined) functionCall.id = callId;
181
+ const part: Record<string, unknown> = { functionCall };
182
+ if (isLikelyRealThoughtSignature(tc.thoughtSignature)) part.thoughtSignature = tc.thoughtSignature;
183
+ parts.push(part);
184
+ }
185
+ }
186
+ // A turn with nothing Gemini can represent (e.g. thinking-only) would serialize as
187
+ // `parts: []`, which the Anthropic translation rejects. Skip it, as the Anthropic
188
+ // adapter does for its own empty assistant content.
189
+ if (parts.length === 0) break;
190
+ contents.push({ role: "model", parts });
191
+ break;
192
+ }
193
+ case "toolResult": {
194
+ // The functionResponse part carries the textual result. Gemini cannot embed images inside a
195
+ // functionResponse, but it does accept sibling inline_data parts in the same user turn, so
196
+ // tool-result screenshots (e.g. Computer Use) ride along as inline_data instead of being
197
+ // flattened to a "[image]" marker the model can't actually see.
198
+ const responseId = geminiToolCallId(msg.toolCallId);
199
+ const functionResponse: Record<string, unknown> = { name: namespacedToolName(msg.toolNamespace, msg.toolName), response: { result: geminiToolResultText(msg.content) } };
200
+ // Mirror the matching functionCall id so Claude-on-Antigravity can pair this result with its
201
+ // `tool_use` block (-> Anthropic `tool_result.tool_use_id`).
202
+ if (responseId !== undefined) functionResponse.id = responseId;
203
+ const parts: unknown[] = [{ functionResponse }];
204
+ for (const part of toolResultImageParts(msg.content)) parts.push(part);
205
+ contents.push({ role: "user", parts });
206
+ break;
207
+ }
208
+ }
209
+ }
210
+
211
+ return { systemInstruction, contents };
212
+ }
213
+
214
+ function toolsToGeminiFormat(parsed: OcxParsedRequest): unknown[] | undefined {
215
+ if (!parsed.context.tools?.length) return undefined;
216
+ const allowed = isAllowedToolChoice(parsed.options.toolChoice)
217
+ ? new Set(parsed.options.toolChoice.allowedTools)
218
+ : undefined;
219
+ const tools = allowed
220
+ ? parsed.context.tools.filter(t => toolAllowedByChoice(t, allowed))
221
+ : parsed.context.tools;
222
+ if (tools.length === 0) return undefined;
223
+ return [{
224
+ functionDeclarations: tools.map(t => ({
225
+ name: namespacedToolName(t.namespace, t.name),
226
+ description: t.description,
227
+ parameters: t.parameters,
228
+ })),
229
+ }];
230
+ }
231
+
232
+ function usageFromGemini(usage: Record<string, number> | undefined): OcxUsage | undefined {
233
+ if (!usage) return undefined;
234
+ return {
235
+ inputTokens: usage.promptTokenCount ?? 0,
236
+ outputTokens: usage.candidatesTokenCount ?? 0,
237
+ ...(usage.cachedContentTokenCount !== undefined ? { cachedInputTokens: usage.cachedContentTokenCount } : {}),
238
+ ...(usage.thoughtsTokenCount !== undefined ? { reasoningOutputTokens: usage.thoughtsTokenCount } : {}),
239
+ };
240
+ }
241
+
242
+ /**
243
+ * Cap on the buffered non-streaming response body (100 MiB), matching
244
+ * IMAGES_RESPONSE_MAX_BYTES in src/server/images.ts. Enforced by streaming the
245
+ * body with a hard byte cap before JSON.parse — Content-Length alone is not
246
+ * trusted (missing/lying headers must still reject oversized payloads).
247
+ * Streaming SSE responses also cap each data frame before JSON.parse.
248
+ */
249
+ const MAX_RESPONSE_BYTES = 100 * 1024 * 1024;
250
+ const MAX_SSE_FRAME_BYTES = MAX_RESPONSE_BYTES;
251
+
252
+ // Note: imagen-* models use a different API surface (prediction/image-generation
253
+ // schema) and must NOT be treated as responseModalities-capable Gemini models.
254
+ // Explicit allowlist only — never `/gemini/ && /image/` (resurrects media-gen IDs).
255
+ const IMAGE_CAPABLE_MODELS = new Set([
256
+ "gemini-3.1-flash-image",
257
+ "gemini-2.0-flash-preview-image-generation",
258
+ "gemini-3-pro-image-preview",
259
+ ]);
260
+
261
+ function isImageCapableModel(modelId: string): boolean {
262
+ return IMAGE_CAPABLE_MODELS.has(modelId);
263
+ }
264
+
265
+ /**
266
+ * Model-visible markdown link for a materialized artifact. Uses the authenticated
267
+ * opaque HTTP route so remote/container clients can fetch the image without host
268
+ * filesystem paths leaking into the transcript.
269
+ */
270
+ function artifactMarkdownUrl(filePath: string): string {
271
+ return artifactHttpUrl(filePath).replace(/([()])/g, "\\$1");
272
+ }
273
+
274
+ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapter {
275
+ // Per-request closure: resolveAdapter builds a fresh adapter per request (server.ts), so buildRequest
276
+ // can stash the CCA model/session for parseStream's reasoning-replay observation.
277
+ let antigravityModel: string | undefined;
278
+ let antigravitySession: string | undefined;
279
+ let restoreGoogleToolName = (name: string): string => name;
280
+ return {
281
+ name: "google",
282
+
283
+ // Vertex + Antigravity get Kiro-style retry/timeout + classified, redacted errors. AI-Studio
284
+ // Gemini keeps the default server fetch path (fetchResponse stays undefined so server.ts falls back).
285
+ ...(provider.googleMode === "vertex" || provider.googleMode === "cloud-code-assist"
286
+ ? {
287
+ fetchResponse: (request: AdapterRequest, ctx?: AdapterFetchContext): Promise<Response> =>
288
+ (provider.googleMode === "cloud-code-assist" ? fetchAntigravityWithRetry : fetchVertexWithRetry)(request, ctx),
289
+ formatErrorBody: (status: number, _headers: Headers, payloadText: string): string =>
290
+ (provider.googleMode === "cloud-code-assist" ? safeAntigravityHttpErrorMessage : safeVertexHttpErrorMessage)(status, payloadText),
291
+ }
292
+ : {}),
293
+
294
+ async buildRequest(parsed: OcxParsedRequest) {
295
+ const { systemInstruction, contents } = messagesToGeminiFormat(parsed);
296
+ const tools = toolsToGeminiFormat(parsed);
297
+
298
+ const body: Record<string, unknown> = { contents };
299
+ if (systemInstruction) body.systemInstruction = systemInstruction;
300
+ if (tools) body.tools = tools;
301
+
302
+ const generationConfig: Record<string, unknown> = {};
303
+ if (parsed.options.maxOutputTokens) generationConfig.maxOutputTokens = parsed.options.maxOutputTokens;
304
+ if (parsed.options.temperature !== undefined) generationConfig.temperature = parsed.options.temperature;
305
+ if (parsed.options.topP !== undefined) generationConfig.topP = parsed.options.topP;
306
+ if (parsed.options.stopSequences) generationConfig.stopSequences = parsed.options.stopSequences;
307
+ const directFlashThinking = provider.googleMode !== "vertex"
308
+ && provider.googleMode !== "cloud-code-assist"
309
+ && (parsed.modelId === "gemini-3.5-flash" || parsed.modelId === "gemini-3.6-flash")
310
+ ? mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning)
311
+ : undefined;
312
+ if (directFlashThinking) generationConfig.thinkingConfig = { thinkingLevel: directFlashThinking };
313
+ if (!generationConfig.thinkingConfig && isImageCapableModel(parsed.modelId)) {
314
+ generationConfig.responseModalities = ["TEXT", "IMAGE"];
315
+ }
316
+ if (Object.keys(generationConfig).length > 0) body.generationConfig = generationConfig;
317
+
318
+ const method = parsed.stream ? "streamGenerateContent" : "generateContent";
319
+ const streamParam = parsed.stream ? "?alt=sse" : "";
320
+ const headers: Record<string, string> = { "Content-Type": "application/json" };
321
+ if (provider.headers) Object.assign(headers, provider.headers);
322
+
323
+ if (provider.googleMode === "cloud-code-assist") {
324
+ // Google Antigravity (Cloud Code Assist): wrap the flat Gemini body in the CCA envelope.
325
+ const token = provider.apiKey?.trim();
326
+ if (!token) throw new Error("google-antigravity oauth token missing — run ocx login google-antigravity");
327
+ const base = provider.baseUrl?.trim();
328
+ if (!base) throw new Error("google-antigravity requires a non-empty baseUrl");
329
+ const url = `${base}/v1internal:${method}${streamParam}`;
330
+ const project = provider.project;
331
+ if (!project) throw new Error("Antigravity requires a discovered Cloud Code Assist project id (re-run `ocx login google-antigravity`).");
332
+ const sessionId = antigravitySessionId(parsed);
333
+ const mappedEffort = mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning);
334
+ const { wireModelId, thinkingLevel } = resolveAntigravityEffortWireModel(parsed.modelId, mappedEffort);
335
+ antigravityModel = wireModelId;
336
+ antigravitySession = sessionId;
337
+ // Effort → thinkingConfig for CCA (CLIProxyAPI proven: request.generationConfig.thinkingConfig).
338
+ // Suffix/compat IDs return thinkingLevel=undefined — the suffix IS the effort, no contradiction.
339
+ if (thinkingLevel) {
340
+ const gc = (body.generationConfig ?? {}) as Record<string, unknown>;
341
+ gc.thinkingConfig = { thinkingLevel };
342
+ body.generationConfig = gc;
343
+ }
344
+ // Reasoning continuity: Gemini models re-inject cached thoughtSignatures; Claude-on-Antigravity
345
+ // sanitizes signatures inline (no cache). Both guard against the upstream 400 on bad signatures.
346
+ // The real Antigravity client puts the session id ONLY at `request.sessionId` (camelCase,
347
+ // nested) — matching CLIProxyAPI `generateStableSessionID`. An extra top-level/snake_case
348
+ // spelling is a non-first-party key, so we send the single canonical location.
349
+ const draftRequest: Record<string, unknown> = { ...body, sessionId };
350
+ // Claude-on-Antigravity forces VALIDATED function calling (the real client always sets it).
351
+ if (/claude/i.test(wireModelId)) {
352
+ const existing = (draftRequest.toolConfig ?? {}) as Record<string, unknown>;
353
+ const fcc = (existing.functionCallingConfig ?? {}) as Record<string, unknown>;
354
+ draftRequest.toolConfig = { ...existing, functionCallingConfig: { ...fcc, mode: "VALIDATED" } };
355
+ }
356
+ const compiled = compileGoogleWireBody(draftRequest);
357
+ const request = compiled.body;
358
+ restoreGoogleToolName = compiled.restoreToolName;
359
+ // Compile names before replay: signatures are keyed by the exact provider-visible name.
360
+ if (Array.isArray((request as { contents?: unknown[] }).contents)) {
361
+ const contents = (request as { contents: unknown[] }).contents;
362
+ if (antigravityUsesReplayCache(wireModelId)) {
363
+ applyAntigravityReplay(wireModelId, sessionId, contents);
364
+ } else {
365
+ sanitizeAntigravityClaudeSignatures(contents);
366
+ }
367
+ }
368
+ const envelope = {
369
+ model: wireModelId,
370
+ // The envelope's `userAgent` field is a protocol constant ("antigravity"), distinct from
371
+ // the HTTP `User-Agent` header (the real CLI UA). CLIProxyAPI `geminiToAntigravity` hardcodes
372
+ // the body field; only the header carries the versioned client string.
373
+ userAgent: "antigravity",
374
+ requestType: "agent",
375
+ project,
376
+ requestId: `agent-${crypto.randomUUID()}`,
377
+ request,
378
+ };
379
+ headers["User-Agent"] = ANTIGRAVITY_REQUEST_UA;
380
+ headers["Authorization"] = `Bearer ${token}`;
381
+ return { url, method: "POST", headers, body: JSON.stringify(envelope) };
382
+ }
383
+
384
+ if (provider.googleMode === "vertex") {
385
+ const compiled = compileGoogleWireBody(body);
386
+ restoreGoogleToolName = compiled.restoreToolName;
387
+ // Vertex AI: project/location endpoint with GCP ADC, or x-goog-api-key fast path.
388
+ const apiKey = resolveVertexApiKey(provider.apiKey);
389
+ if (apiKey) {
390
+ const url = `https://aiplatform.googleapis.com/v1/publishers/google/models/${parsed.modelId}:${method}${streamParam}`;
391
+ headers["x-goog-api-key"] = apiKey;
392
+ return { url, method: "POST", headers, body: JSON.stringify(compiled.body) };
393
+ }
394
+ const project = provider.project || process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT;
395
+ if (!project) throw new Error("Vertex AI requires a project id (provider.project or GOOGLE_CLOUD_PROJECT/GCLOUD_PROJECT).");
396
+ const location = provider.location || process.env.GOOGLE_CLOUD_LOCATION;
397
+ if (!location) throw new Error("Vertex AI requires a location (provider.location or GOOGLE_CLOUD_LOCATION).");
398
+ const host = location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com`;
399
+ const url = `https://${host}/v1/projects/${project}/locations/${location}/publishers/google/models/${parsed.modelId}:${method}${streamParam}`;
400
+ const token = await getVertexAccessToken();
401
+ headers["Authorization"] = `Bearer ${token}`;
402
+ return { url, method: "POST", headers, body: JSON.stringify(compiled.body) };
403
+ }
404
+
405
+ // ai-studio (default): Generative Language API + x-goog-api-key.
406
+ const url = `${provider.baseUrl}/v1beta/models/${parsed.modelId}:${method}${streamParam}`;
407
+ const apiKey = provider.apiKey?.trim();
408
+ if (!apiKey) throw new Error("google (AI Studio) requires a non-empty API key");
409
+ headers["x-goog-api-key"] = apiKey;
410
+
411
+ const compiled = compileGoogleWireBody(body);
412
+ restoreGoogleToolName = compiled.restoreToolName;
413
+ return { url, method: "POST", headers, body: JSON.stringify(compiled.body) };
414
+ },
415
+
416
+ async *parseStream(response: Response, budget: TranslatorBudget): AsyncGenerator<AdapterEvent> {
417
+ if (!response.body) {
418
+ yield { type: "error", message: "No response body" };
419
+ return;
420
+ }
421
+ // Streaming responses are processed incrementally (SSE chunks), so the full body
422
+ // is never buffered — no Content-Length pre-check is needed here. Per-image size
423
+ // protection is enforced on each chunk via MAX_ENCODED_BYTES_PER_IMAGE before
424
+ // materializeInlineImage is called (see the inline.data check below).
425
+
426
+ const reader = response.body.getReader();
427
+ const decoder = new TextDecoder();
428
+ const budgetEncoder = new TextEncoder();
429
+ let buffer = "";
430
+ let bufferBytes = 0;
431
+ let pendingUsage: OcxUsage | undefined;
432
+ let toolCallsStarted = 0;
433
+ let lastFinishReason: string | undefined;
434
+ let sawAnyFrame = false;
435
+ let sawTerminalSignal = false;
436
+
437
+ const handleDataLine = async function* (line: string): AsyncGenerator<AdapterEvent, "continue" | "content" | "terminate"> {
438
+ const payload = line.slice(5).trim();
439
+ if (!payload) return "continue";
440
+ if (payload.length > MAX_SSE_FRAME_BYTES) {
441
+ yield { type: "error", message: `upstream SSE data frame exceeds ${MAX_SSE_FRAME_BYTES} bytes` };
442
+ return "terminate";
443
+ }
444
+ let emittedContentEvent = false;
445
+
446
+ let chunk: Record<string, unknown>;
447
+ try {
448
+ chunk = JSON.parse(payload);
449
+ } catch {
450
+ yield { type: "error", message: "malformed upstream SSE data frame" };
451
+ return "terminate";
452
+ }
453
+ sawAnyFrame = true;
454
+
455
+ // Inline provider error inside a 200 stream → terminal error (see openai-chat.ts).
456
+ if (chunk.error) {
457
+ const err = chunk.error as { message?: string } | undefined;
458
+ // Clear-on-invalid: a signature rejection means our replayed thoughtSignatures are stale.
459
+ // Drop the cache entry so the next turn starts clean instead of re-injecting a bad sig.
460
+ if (provider.googleMode === "cloud-code-assist" && antigravityModel && antigravitySession
461
+ && /signature|invalid_argument|invalid argument/i.test(err?.message ?? "")) {
462
+ clearAntigravityReplay(antigravityModel, antigravitySession);
463
+ }
464
+ yield { type: "error", message: err?.message ?? "upstream error" };
465
+ return "terminate";
466
+ }
467
+
468
+ // Antigravity (CCA) nests the standard Gemini payload under `response`.
469
+ let root = chunk;
470
+ if (provider.googleMode === "cloud-code-assist") {
471
+ const wrapped = chunk.response;
472
+ if (!wrapped || typeof wrapped !== "object" || Array.isArray(wrapped)) {
473
+ yield { type: "error", message: "google-antigravity response missing response wrapper" };
474
+ return "terminate";
475
+ }
476
+ root = wrapped as Record<string, unknown>;
477
+ }
478
+ // usageMetadata is a top-level field independent of candidates; read it BEFORE the
479
+ // candidates guard so a usage-only final chunk is not dropped.
480
+ const usageMeta = root.usageMetadata as Record<string, number> | undefined;
481
+ if (usageMeta) {
482
+ // Accumulate usage; emit a single terminal `done` post-loop so usage is never
483
+ // dropped on EOF and the stream never yields two `done` events.
484
+ pendingUsage = usageFromGemini(usageMeta);
485
+ sawTerminalSignal = true;
486
+ }
487
+ const candidates = root.candidates as { content?: { parts?: unknown[] }; finishReason?: string }[] | undefined;
488
+ if (!candidates?.length) return "continue";
489
+
490
+ if (typeof candidates[0].finishReason === "string" && candidates[0].finishReason) {
491
+ lastFinishReason = candidates[0].finishReason;
492
+ sawTerminalSignal = true;
493
+ }
494
+
495
+ const parts = candidates[0].content?.parts as { text?: string; functionCall?: { name: string; args: unknown } }[] | undefined;
496
+ // Antigravity reasoning-replay: record thoughtSignatures from the model parts for the next turn.
497
+ if (provider.googleMode === "cloud-code-assist" && parts && antigravityModel && antigravitySession) {
498
+ observeAntigravityReplay(antigravityModel, antigravitySession, parts as unknown[]);
499
+ }
500
+ if (parts) {
501
+ for (const part of parts) {
502
+ if (part.text) {
503
+ emittedContentEvent = true;
504
+ yield { type: "text_delta", text: part.text };
505
+ }
506
+ const inline = (part as { inlineData?: { mimeType?: string; data?: string } }).inlineData;
507
+ if (inline && typeof inline.data === "string") {
508
+ if (inline.data.length > MAX_ENCODED_BYTES_PER_IMAGE) {
509
+ yield { type: "error", message: "inline image exceeds per-image size cap" };
510
+ } else {
511
+ try {
512
+ const filePath = await materializeInlineImage(inline.data, imageBudget);
513
+ const escapedPath = artifactMarkdownUrl(filePath);
514
+ emittedContentEvent = true;
515
+ yield { type: "text_delta", text: `\n![image](${escapedPath})\n` };
516
+ } catch {
517
+ yield { type: "error", message: "failed to materialize inline image" };
518
+ }
519
+ }
520
+ }
521
+ if (part.functionCall) {
522
+ const id = `call_${crypto.randomUUID().slice(0, 8)}`;
523
+ toolCallsStarted++;
524
+ emittedContentEvent = true;
525
+ yield { type: "tool_call_start", id, name: restoreGoogleToolName(part.functionCall.name) };
526
+ yield { type: "tool_call_delta", arguments: JSON.stringify(part.functionCall.args ?? {}) };
527
+ yield { type: "tool_call_end" };
528
+ }
529
+ }
530
+ }
531
+ return emittedContentEvent ? "content" : "continue";
532
+ };
533
+ const imageBudget = createImageBudget();
534
+
535
+ try {
536
+ while (true) {
537
+ const { done, value } = await reader.read();
538
+ if (done) break;
539
+ const nextBuffer = buffer + decoder.decode(value, { stream: true });
540
+ const nextBufferBytes = budgetEncoder.encode(nextBuffer).byteLength;
541
+ const appendReservation = budget.reserveTransient(nextBufferBytes, { kind: "live_transient" });
542
+ buffer = nextBuffer;
543
+ appendReservation.commitRetained();
544
+ budget.releaseRetained(bufferBytes, { kind: "live_transient" });
545
+ bufferBytes = nextBufferBytes;
546
+ // Cap incomplete frames before waiting for a newline — otherwise a single
547
+ // unterminated data: payload can grow without bound.
548
+ if (buffer.length > MAX_SSE_FRAME_BYTES) {
549
+ yield { type: "error", message: `upstream SSE data frame exceeds ${MAX_SSE_FRAME_BYTES} bytes` };
550
+ try { await reader.cancel(); } catch { /* ignore */ }
551
+ return;
552
+ }
553
+
554
+ const lines = buffer.split("\n");
555
+ buffer = lines.pop() ?? "";
556
+ const residualBytes = budgetEncoder.encode(buffer).byteLength;
557
+ const residualReservation = budget.reserveTransient(residualBytes, { kind: "live_transient" });
558
+ residualReservation.commitRetained();
559
+ budget.releaseRetained(bufferBytes, { kind: "live_transient" });
560
+ bufferBytes = residualBytes;
561
+
562
+ let sawLiveness = false;
563
+ let sawContentEvent = false;
564
+ for (const line of lines) {
565
+ if (line.startsWith("data:")) {
566
+ const result = yield* handleDataLine(line);
567
+ if (result === "terminate") return;
568
+ if (result === "content") sawContentEvent = true;
569
+ continue;
570
+ }
571
+ sawLiveness = true;
572
+ if (line.startsWith(":") || !line.trim()) continue;
573
+ debugDroppedFrame("google", line);
574
+ }
575
+ if (sawLiveness && !sawContentEvent) yield { type: "heartbeat" };
576
+ }
577
+ buffer += decoder.decode();
578
+ if (buffer.trim().length > 0) {
579
+ const residual = buffer.trim();
580
+ if (residual.startsWith(":")) {
581
+ yield { type: "heartbeat" };
582
+ } else if (!residual.startsWith("data:")) {
583
+ yield { type: "error", message: "upstream stream ended with an incomplete SSE frame — possible truncation" };
584
+ return;
585
+ } else if ((yield* handleDataLine(residual)) === "terminate") return;
586
+ }
587
+ // Fail-closed: a turn cut off mid tool call (MAX_TOKENS / MALFORMED_FUNCTION_CALL) surfaces
588
+ // an error instead of a silently-incomplete done. Mirrors kiro-truncation.
589
+ if ((provider.googleMode === "vertex" || provider.googleMode === "cloud-code-assist")
590
+ && toolCallsStarted > 0 && isVertexTruncationReason(lastFinishReason)) {
591
+ yield { type: "error", message: vertexTruncationErrorMessage(lastFinishReason) };
592
+ return;
593
+ }
594
+ if (!sawAnyFrame || !sawTerminalSignal) {
595
+ yield { type: "error", message: "upstream stream ended without a terminal signal — possible truncation" };
596
+ return;
597
+ }
598
+ const stopReason = lastFinishReason === "MAX_TOKENS"
599
+ ? "max_tokens"
600
+ : ["SAFETY", "RECITATION", "BLOCKLIST", "PROHIBITED_CONTENT", "SPII"].includes(lastFinishReason ?? "")
601
+ ? "content_filter"
602
+ : undefined;
603
+ yield {
604
+ type: "done",
605
+ usage: pendingUsage,
606
+ ...(stopReason ? { stopReason } : {}),
607
+ };
608
+ } catch (error) {
609
+ if (!isTranslatorBudgetExceededError(error)) throw error;
610
+ try { await reader.cancel(error); } catch { /* already closed */ }
611
+ yield {
612
+ type: "error",
613
+ status: 502,
614
+ errorType: "upstream_error",
615
+ code: "translation_buffer_limit",
616
+ message: "upstream translation buffer exceeded the safe limit",
617
+ };
618
+ } finally {
619
+ budget.releaseRetained(bufferBytes, { kind: "live_transient" });
620
+ reader.releaseLock();
621
+ }
622
+ },
623
+
624
+ async parseResponse(response: Response, budget: TranslatorBudget): Promise<AdapterEvent[]> {
625
+ // Reject oversized responses before JSON parse. Prefer Content-Length when
626
+ // present and truthful; always stream-read with a hard byte cap so a missing
627
+ // or lying Content-Length cannot force a full in-memory buffer + parse.
628
+ const contentLength = Number(response.headers.get("content-length"));
629
+ if (Number.isFinite(contentLength) && contentLength > MAX_RESPONSE_BYTES) {
630
+ try { await response.body?.cancel(); } catch { /* ignore */ }
631
+ return [{ type: "error", message: `google response too large (content-length ${contentLength} exceeds ${MAX_RESPONSE_BYTES} bytes)` }];
632
+ }
633
+ let rawText: string;
634
+ let rawTextBytes = 0;
635
+ try {
636
+ const reader = response.body?.getReader();
637
+ if (!reader) return [{ type: "error", message: "google response had no body" }];
638
+ const chunks: Uint8Array[] = [];
639
+ let total = 0;
640
+ try {
641
+ for (;;) {
642
+ const { done, value } = await reader.read();
643
+ if (done) break;
644
+ total += value.byteLength;
645
+ if (total > MAX_RESPONSE_BYTES) {
646
+ await reader.cancel().catch(() => {});
647
+ return [{ type: "error", message: `google response too large (exceeded ${MAX_RESPONSE_BYTES} bytes)` }];
648
+ }
649
+ budget.chargeRetained(value.byteLength, { kind: "retained_collectors" });
650
+ chunks.push(value);
651
+ }
652
+ } finally {
653
+ try { await reader.cancel(); } catch { /* ignore */ }
654
+ reader.releaseLock();
655
+ }
656
+ const bytesReservation = budget.reserveTransient(total, { kind: "retained_collectors" });
657
+ const bytes = new Uint8Array(total);
658
+ let offset = 0;
659
+ for (const chunk of chunks) {
660
+ bytes.set(chunk, offset);
661
+ offset += chunk.byteLength;
662
+ }
663
+ bytesReservation.commitRetained();
664
+ budget.releaseRetained(total, { kind: "retained_collectors" });
665
+ rawText = new TextDecoder().decode(bytes);
666
+ rawTextBytes = new TextEncoder().encode(rawText).byteLength;
667
+ const textReservation = budget.reserveTransient(rawTextBytes, { kind: "retained_collectors" });
668
+ textReservation.commitRetained();
669
+ budget.releaseRetained(total, { kind: "retained_collectors" });
670
+ } catch (err) {
671
+ return [{ type: "error", message: err instanceof Error ? err.message : "failed to read google response body" }];
672
+ }
673
+ let raw: Record<string, unknown>;
674
+ let rawBytes = 0;
675
+ try {
676
+ raw = JSON.parse(rawText) as Record<string, unknown>;
677
+ rawBytes = new TextEncoder().encode(JSON.stringify(raw)).byteLength;
678
+ const rawReservation = budget.reserveTransient(rawBytes, { kind: "retained_collectors" });
679
+ rawReservation.commitRetained();
680
+ budget.releaseRetained(rawTextBytes, { kind: "retained_collectors" });
681
+ } catch {
682
+ budget.releaseRetained(rawTextBytes, { kind: "retained_collectors" });
683
+ return [{ type: "error", message: "google response was not valid JSON" }];
684
+ }
685
+ const finish = (events: AdapterEvent[]): AdapterEvent[] => {
686
+ retainTranslatedEventBatch(events, budget);
687
+ budget.releaseRetained(rawBytes, { kind: "retained_collectors" });
688
+ rawBytes = 0;
689
+ return events;
690
+ };
691
+ if (raw.error) {
692
+ const err = raw.error as { message?: string };
693
+ return finish([{ type: "error", message: err.message ?? "upstream error" }]);
694
+ }
695
+ // Antigravity (CCA) nests the standard Gemini payload under `response`; unwrap it.
696
+ let json = raw;
697
+ if (provider.googleMode === "cloud-code-assist") {
698
+ const wrapped = raw.response;
699
+ if (!wrapped || typeof wrapped !== "object" || Array.isArray(wrapped)) {
700
+ return finish([{ type: "error", message: "google-antigravity response missing response wrapper" }]);
701
+ }
702
+ json = wrapped as Record<string, unknown>;
703
+ }
704
+ const events: AdapterEvent[] = [];
705
+
706
+ const candidates = json.candidates as { content?: { parts?: { text?: string; functionCall?: { name: string; args: unknown } }[] }; finishReason?: string }[] | undefined;
707
+ if (!candidates?.length) {
708
+ return finish([{ type: "error", message: "google response contained no candidates" }]);
709
+ }
710
+ let toolCallsStarted = 0;
711
+ const imageBudget = createImageBudget();
712
+ if (candidates?.[0]?.content?.parts) {
713
+ // Non-streaming CCA: observe thoughtSignatures for the next turn, same as the stream path.
714
+ if (provider.googleMode === "cloud-code-assist" && antigravityModel && antigravitySession) {
715
+ observeAntigravityReplay(antigravityModel, antigravitySession, candidates[0].content.parts as unknown[]);
716
+ }
717
+ for (const part of candidates[0].content.parts) {
718
+ if (part.text) events.push({ type: "text_delta", text: part.text });
719
+ const inline = (part as { inlineData?: { mimeType?: string; data?: string } }).inlineData;
720
+ if (inline && typeof inline.data === "string") {
721
+ if (inline.data.length > MAX_ENCODED_BYTES_PER_IMAGE) {
722
+ events.push({ type: "error", message: "inline image exceeds per-image size cap" });
723
+ } else {
724
+ try {
725
+ const filePath = await materializeInlineImage(inline.data, imageBudget);
726
+ const escapedPath = artifactMarkdownUrl(filePath);
727
+ events.push({ type: "text_delta", text: `\n![image](${escapedPath})\n` });
728
+ } catch {
729
+ events.push({ type: "error", message: "failed to materialize inline image" });
730
+ }
731
+ }
732
+ }
733
+ if (part.functionCall) {
734
+ const id = `call_${crypto.randomUUID().slice(0, 8)}`;
735
+ toolCallsStarted++;
736
+ events.push({ type: "tool_call_start", id, name: restoreGoogleToolName(part.functionCall.name) });
737
+ events.push({ type: "tool_call_delta", arguments: JSON.stringify(part.functionCall.args ?? {}) });
738
+ events.push({ type: "tool_call_end" });
739
+ }
740
+ }
741
+ }
742
+
743
+ // Fail-closed truncation, same as the stream path: a non-stream turn cut off mid tool call
744
+ // (MAX_TOKENS / MALFORMED_FUNCTION_CALL) surfaces an error instead of a silent done.
745
+ if ((provider.googleMode === "vertex" || provider.googleMode === "cloud-code-assist")
746
+ && toolCallsStarted > 0 && isVertexTruncationReason(candidates?.[0]?.finishReason)) {
747
+ return finish([{ type: "error", message: vertexTruncationErrorMessage(candidates?.[0]?.finishReason) }]);
748
+ }
749
+
750
+ const usage = json.usageMetadata as Record<string, number> | undefined;
751
+ events.push({
752
+ type: "done",
753
+ usage: usageFromGemini(usage),
754
+ });
755
+ return finish(events);
756
+ },
757
+ };
758
+ }