@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,1137 @@
1
+ import { createHash } from "node:crypto";
2
+ import type { IncomingMeta, ProviderAdapter } from "./base";
3
+ import { namespacedToolName, type AdapterEvent, type OcxParsedRequest, type OcxProviderConfig, type OcxUsage } from "../types";
4
+ import { catalogModelSupportsReasoningSummaries } from "../codex/catalog";
5
+ import { COMPACT_PROMPT, decodeCompactionSummary, SUMMARY_PREFIX } from "../responses/compaction";
6
+ import { collectResponsesToolGroups } from "../responses/tool-groups";
7
+ import { decodeServerSentEvents } from "../lib/sse-decoder";
8
+ import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers";
9
+ import { OCX_REASONING_PREFIX } from "../responses/reasoning-envelope";
10
+ import { modelRecordValue } from "../reasoning-effort";
11
+ import type { TranslatorBudget } from "../lib/translator-budget";
12
+
13
+ // Headers relayed verbatim from the caller in OAuth-passthrough ("forward") mode.
14
+ // Exported so the web-search sidecar reuses the exact same forwarded-auth set for its ChatGPT call.
15
+ export const FORWARD_HEADERS = [
16
+ "authorization",
17
+ "chatgpt-account-id",
18
+ "openai-beta",
19
+ "originator",
20
+ "session_id",
21
+ "session-id",
22
+ "thread-id",
23
+ "x-client-request-id",
24
+ "x-codex-beta-features",
25
+ "x-codex-installation-id",
26
+ "x-codex-parent-thread-id",
27
+ "x-codex-turn-metadata",
28
+ "x-codex-turn-state",
29
+ "x-codex-window-id",
30
+ "x-oai-attestation",
31
+ "x-openai-subagent",
32
+ "x-responsesapi-include-timing-metrics",
33
+ ];
34
+
35
+ export function sanitizeReasoningInputContent(body: unknown): unknown {
36
+ if (!body || typeof body !== "object" || Array.isArray(body)) return body;
37
+ const raw = body as Record<string, unknown>;
38
+ if (!Array.isArray(raw.input)) return body;
39
+
40
+ let changed = false;
41
+ const input = raw.input.map(item => {
42
+ if (!item || typeof item !== "object" || Array.isArray(item)) return item;
43
+ const rec = item as Record<string, unknown>;
44
+ if (rec.type !== "reasoning") return item;
45
+ const hasRawContent = Array.isArray(rec.content) && rec.content.length > 0;
46
+ // ocxr1 envelopes are proxy-minted (Anthropic signatures), not OpenAI encryption — the native
47
+ // backend cannot decrypt them and would reject the request. Strip regardless of content shape.
48
+ const hasOcxEnvelope = typeof rec.encrypted_content === "string" && rec.encrypted_content.startsWith(OCX_REASONING_PREFIX);
49
+ if (!hasRawContent && !hasOcxEnvelope) return item;
50
+ changed = true;
51
+ // Routed models can produce raw `reasoning_text` output items. Codex echoes those in later
52
+ // native GPT requests, but ChatGPT's Responses backend accepts reasoning input only with empty
53
+ // `content`; keep summaries/ids and drop the raw content so native passthrough does not 400.
54
+ const next: Record<string, unknown> = { ...rec, content: [] };
55
+ if (hasOcxEnvelope) delete next.encrypted_content;
56
+ return next;
57
+ });
58
+
59
+ return changed ? { ...raw, input } : body;
60
+ }
61
+
62
+ function stripUnsupportedReasoningSummaryDelivery(body: unknown, modelId: string): unknown {
63
+ if (catalogModelSupportsReasoningSummaries(modelId) !== false) return body;
64
+ if (!isPlainObject(body) || !isPlainObject(body.stream_options)) return body;
65
+ if (!("reasoning_summary_delivery" in body.stream_options)) return body;
66
+
67
+ const streamOptions = { ...body.stream_options };
68
+ delete streamOptions.reasoning_summary_delivery;
69
+ const next = { ...body };
70
+ if (Object.keys(streamOptions).length > 0) next.stream_options = streamOptions;
71
+ else delete next.stream_options;
72
+ return next;
73
+ }
74
+
75
+ function stripInvalidItemIds(body: unknown): unknown {
76
+ if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
77
+
78
+ const validPrefixes: Record<string, string> = {
79
+ message: "msg_",
80
+ agent_message: "amsg_",
81
+ reasoning: "rs_",
82
+ function_call: "fc_",
83
+ custom_tool_call: "ctc_",
84
+ tool_search_call: "tsc_",
85
+ web_search_call: "ws_",
86
+ };
87
+ let changed = false;
88
+ const input = body.input.map(item => {
89
+ if (!isPlainObject(item) || typeof item.type !== "string") return item;
90
+ const validPrefix = validPrefixes[item.type];
91
+ if (!validPrefix) return item;
92
+ if (typeof item.id === "string" && item.id.startsWith(validPrefix)) return item;
93
+ if (!("id" in item)) return item;
94
+ changed = true;
95
+ const next = { ...item };
96
+ delete next.id;
97
+ return next;
98
+ });
99
+
100
+ return changed ? { ...body, input } : body;
101
+ }
102
+
103
+ /**
104
+ * When `store` is false, the upstream API does not persist response items. Any item ID
105
+ * forwarded in `input` is then interpreted as a reference to a stored item that does not
106
+ * exist, producing a 404. Strip all item IDs in this case — `call_id` pairing is unaffected.
107
+ * Matches codex-rs behavior (core/src/client.rs:918-925).
108
+ */
109
+ function stripItemIdsWhenUnstored(body: unknown): unknown {
110
+ if (!isPlainObject(body) || body.store !== false) return body;
111
+ if (!Array.isArray(body.input)) return body;
112
+
113
+ let changed = false;
114
+ const input = body.input.map(item => {
115
+ if (!isPlainObject(item) || !("id" in item)) return item;
116
+ changed = true;
117
+ const next = { ...item };
118
+ delete next.id;
119
+ return next;
120
+ });
121
+
122
+ return changed ? { ...body, input } : body;
123
+ }
124
+
125
+ /**
126
+ * Replace proxy-minted compaction items (`encrypted_content` starting with `ocx1:`) with plain
127
+ * user messages before forwarding to the ChatGPT backend. Our envelope is transparent base64, not
128
+ * OpenAI encryption — the native backend cannot decrypt it and would reject the request. Real
129
+ * OpenAI-encrypted compaction items are forwarded untouched.
130
+ */
131
+ function scrubOcxCompactionItems(body: unknown): unknown {
132
+ if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
133
+
134
+ let changed = false;
135
+ const input = body.input.map(item => {
136
+ if (!isPlainObject(item)) return item;
137
+ if (item.type !== "compaction" && item.type !== "compaction_summary" && item.type !== "context_compaction") return item;
138
+ const decoded = typeof item.encrypted_content === "string" ? decodeCompactionSummary(item.encrypted_content) : null;
139
+ if (decoded === null) return item;
140
+ changed = true;
141
+ return {
142
+ type: "message",
143
+ role: "user",
144
+ content: [{ type: "input_text", text: `${SUMMARY_PREFIX}\n\n${decoded}` }],
145
+ };
146
+ });
147
+
148
+ return changed ? { ...body, input } : body;
149
+ }
150
+
151
+ /**
152
+ * Hosted (OpenAI-executed) tool types that specific native slugs reject at request time. Codex
153
+ * attaches these for app skills (e.g. `image_generation` for imagegen) regardless of the target
154
+ * model, and the passthrough path forwards the raw body untouched — so a slug that doesn't support
155
+ * the tool 400s (`Tool 'image_generation' is not supported with gpt-5.3-codex-spark.`). Each entry
156
+ * maps a model-slug matcher to the hosted tool types that must be stripped before forwarding.
157
+ * Extend this when another native slug rejects a hosted tool (e.g. `code_interpreter`).
158
+ */
159
+ const UNSUPPORTED_HOSTED_TOOLS: ReadonlyArray<{ match: (model: string) => boolean; tools: ReadonlySet<string> }> = [
160
+ { match: model => model.includes("codex-spark"), tools: new Set(["image_generation", "tool_search"]) },
161
+ ];
162
+
163
+ /**
164
+ * Strip unsupported `reasoning` sub-parameters for native slugs that reject them (e.g. Spark).
165
+ * codex-rs injects `reasoning.context` and `reasoning.summary` based on catalog flags; Spark's
166
+ * backend rejects both. The catalog fix prevents `use_responses_lite` from being set, but this
167
+ * is a defense-in-depth guard so stale on-disk catalogs don't break until the user runs `ocx sync`.
168
+ */
169
+ function stripUnsupportedReasoningParams(body: unknown): unknown {
170
+ if (!isPlainObject(body)) return body;
171
+ const model = typeof body.model === "string" ? body.model : "";
172
+ if (!model.includes("codex-spark")) return body;
173
+ if (!isPlainObject(body.reasoning)) return body;
174
+ const reasoning = body.reasoning as Record<string, unknown>;
175
+ // Spark supports reasoning.effort but rejects context, summary, and generate_summary.
176
+ const { context: _ctx, summary: _sum, generate_summary: _gs, ...rest } = reasoning;
177
+ if (_ctx === undefined && _sum === undefined && _gs === undefined) return body;
178
+ return { ...body, reasoning: Object.keys(rest).length > 0 ? rest : undefined };
179
+ }
180
+
181
+ /**
182
+ * A false model capability prevents Codex from emitting summary fields after the catalog refresh.
183
+ * Strip them here as well so an already-running client with a stale catalog cannot keep sending an
184
+ * upstream-rejected `reasoning_summary_delivery` value (issue #323).
185
+ */
186
+ function stripDisabledReasoningSummaries(
187
+ body: unknown,
188
+ provider: OcxProviderConfig,
189
+ modelId: string,
190
+ ): unknown {
191
+ if (modelRecordValue(provider.modelSupportsReasoningSummaries, modelId) !== false || !isPlainObject(body)) {
192
+ return body;
193
+ }
194
+
195
+ let changed = false;
196
+ let streamOptions = body.stream_options;
197
+ if (isPlainObject(streamOptions) && Object.hasOwn(streamOptions, "reasoning_summary_delivery")) {
198
+ const { reasoning_summary_delivery: _delivery, ...rest } = streamOptions;
199
+ streamOptions = rest;
200
+ changed = true;
201
+ }
202
+
203
+ let reasoning = body.reasoning;
204
+ if (isPlainObject(reasoning)) {
205
+ const { summary: _summary, generate_summary: _generateSummary, ...rest } = reasoning;
206
+ if (_summary !== undefined || _generateSummary !== undefined) {
207
+ reasoning = rest;
208
+ changed = true;
209
+ }
210
+ }
211
+
212
+ if (!changed) return body;
213
+ return {
214
+ ...body,
215
+ ...(isPlainObject(streamOptions) && Object.keys(streamOptions).length > 0
216
+ ? { stream_options: streamOptions }
217
+ : { stream_options: undefined }),
218
+ ...(isPlainObject(reasoning) && Object.keys(reasoning).length > 0
219
+ ? { reasoning }
220
+ : { reasoning: undefined }),
221
+ };
222
+ }
223
+
224
+ /**
225
+ * Normalize only the delivery enum Codex already emitted. Do not inject a field into callers that
226
+ * did not request summaries, and leave every unconfigured provider/model byte-for-byte unchanged.
227
+ */
228
+ function normalizeConfiguredReasoningSummaryDelivery(
229
+ body: unknown,
230
+ provider: OcxProviderConfig,
231
+ modelId: string,
232
+ ): unknown {
233
+ const delivery = modelRecordValue(provider.modelReasoningSummaryDelivery, modelId);
234
+ if (delivery === undefined || !isPlainObject(body) || !isPlainObject(body.stream_options)) return body;
235
+ if (!Object.hasOwn(body.stream_options, "reasoning_summary_delivery")) return body;
236
+ if (body.stream_options.reasoning_summary_delivery === delivery) return body;
237
+ return {
238
+ ...body,
239
+ stream_options: {
240
+ ...body.stream_options,
241
+ reasoning_summary_delivery: delivery,
242
+ },
243
+ };
244
+ }
245
+
246
+ /**
247
+ * Comprehensive Spark compatibility layer. codex-rs emits five tool types (function,
248
+ * namespace, tool_search, web_search, custom) plus extensions (defer_loading,
249
+ * parallel_tool_calls, tool_search_call/output items). Spark's serving path only
250
+ * supports flat function tools and hosted web_search. This function:
251
+ * - Flattens namespace tools → promotes inner functions to top level
252
+ * - Drops unsupported tool types (tool_search, custom)
253
+ * - Strips defer_loading from function tools
254
+ * - Strips namespace from input items
255
+ * - Drops tool_search_call/tool_search_output input items
256
+ * - Sets parallel_tool_calls to false
257
+ */
258
+ function stripSparkCompatibility(body: unknown): unknown {
259
+ if (!isPlainObject(body)) return body;
260
+ const model = typeof body.model === "string" ? body.model : "";
261
+ if (!model.includes("codex-spark")) return body;
262
+
263
+ let changed = false;
264
+
265
+ const SPARK_SAFE_TOOL_TYPES = new Set(["function", "web_search", "web_search_preview"]);
266
+
267
+ let tools = body.tools;
268
+ if (Array.isArray(tools)) {
269
+ const flattened: unknown[] = [];
270
+ for (const t of tools) {
271
+ if (isPlainObject(t) && t.type === "namespace") {
272
+ changed = true;
273
+ if (Array.isArray(t.tools)) {
274
+ for (const inner of t.tools) flattened.push(inner);
275
+ }
276
+ } else if (isPlainObject(t) && typeof t.type === "string" && !SPARK_SAFE_TOOL_TYPES.has(t.type)) {
277
+ changed = true;
278
+ } else {
279
+ flattened.push(t);
280
+ }
281
+ }
282
+ // Strip defer_loading from promoted/remaining function tools.
283
+ tools = flattened.map(t => {
284
+ if (isPlainObject(t) && t.type === "function" && "defer_loading" in t) {
285
+ const { defer_loading: _, ...rest } = t;
286
+ changed = true;
287
+ return rest;
288
+ }
289
+ return t;
290
+ });
291
+ }
292
+
293
+ // Clean input items: strip namespace, drop tool_search_call/tool_search_output.
294
+ const SPARK_UNSUPPORTED_INPUT_TYPES = new Set([
295
+ "tool_search_call", "tool_search_output",
296
+ "custom_tool_call", "custom_tool_call_output",
297
+ ]);
298
+ let input = body.input;
299
+ if (Array.isArray(input)) {
300
+ const cleaned: unknown[] = [];
301
+ for (const item of input) {
302
+ if (isPlainObject(item) && typeof item.type === "string" && SPARK_UNSUPPORTED_INPUT_TYPES.has(item.type)) {
303
+ changed = true;
304
+ continue;
305
+ }
306
+ // Process additional_tools items: filter their inner tools array the same way.
307
+ if (isPlainObject(item) && item.type === "additional_tools" && Array.isArray(item.tools)) {
308
+ const innerTools = item.tools as unknown[];
309
+ const filteredInner: unknown[] = [];
310
+ for (const t of innerTools) {
311
+ if (isPlainObject(t) && t.type === "namespace") {
312
+ changed = true;
313
+ if (Array.isArray(t.tools)) {
314
+ for (const fn of t.tools) filteredInner.push(fn);
315
+ }
316
+ } else if (isPlainObject(t) && typeof t.type === "string" && !SPARK_SAFE_TOOL_TYPES.has(t.type)) {
317
+ changed = true; // drop custom, tool_search, etc.
318
+ } else {
319
+ filteredInner.push(t);
320
+ }
321
+ }
322
+ // Strip defer_loading from remaining function tools.
323
+ const cleanedInner = filteredInner.map(t => {
324
+ if (isPlainObject(t) && t.type === "function" && "defer_loading" in t) {
325
+ const { defer_loading: _, ...rest } = t;
326
+ changed = true;
327
+ return rest;
328
+ }
329
+ return t;
330
+ });
331
+ cleaned.push({ ...item, tools: cleanedInner });
332
+ continue;
333
+ }
334
+ if (isPlainObject(item) && "namespace" in item) {
335
+ const { namespace: _, ...rest } = item;
336
+ changed = true;
337
+ cleaned.push(rest);
338
+ } else {
339
+ cleaned.push(item);
340
+ }
341
+ }
342
+ if (changed) input = cleaned;
343
+ }
344
+
345
+ // Force parallel_tool_calls off for Spark.
346
+ const extraOverrides: Record<string, unknown> = {};
347
+ if (body.parallel_tool_calls === true) { extraOverrides.parallel_tool_calls = false; changed = true; }
348
+
349
+ return changed
350
+ ? { ...body, ...(tools !== body.tools ? { tools } : {}), ...(input !== body.input ? { input } : {}), ...extraOverrides }
351
+ : body;
352
+ }
353
+
354
+ function isPlainObject(v: unknown): v is Record<string, unknown> {
355
+ return !!v && typeof v === "object" && !Array.isArray(v);
356
+ }
357
+
358
+ function normalizeFunctionToolSchema(tool: unknown): unknown {
359
+ if (!isPlainObject(tool) || tool.type !== "function") return tool;
360
+ if (isPlainObject(tool.parameters) && tool.parameters.type === "object") return tool;
361
+ return {
362
+ ...tool,
363
+ parameters: { ...(isPlainObject(tool.parameters) ? tool.parameters : {}), type: "object" },
364
+ };
365
+ }
366
+
367
+ function normalizeToolSchemas(body: unknown): unknown {
368
+ if (!isPlainObject(body)) return body;
369
+
370
+ const normalizeTools = (tools: unknown[]): unknown[] => {
371
+ let changed = false;
372
+ const normalized = tools.map((tool) => {
373
+ const fixed = normalizeFunctionToolSchema(tool);
374
+ if (fixed !== tool) changed = true;
375
+ return fixed;
376
+ });
377
+ return changed ? normalized : tools;
378
+ };
379
+
380
+ let normalizedBody = body;
381
+ if (Array.isArray(body.tools)) {
382
+ const tools = normalizeTools(body.tools);
383
+ if (tools !== body.tools) normalizedBody = { ...normalizedBody, tools };
384
+ }
385
+ if (Array.isArray(normalizedBody.input)) {
386
+ let inputChanged = false;
387
+ const input = normalizedBody.input.map((item) => {
388
+ if (!isPlainObject(item) || item.type !== "additional_tools" || !Array.isArray(item.tools)) return item;
389
+ const tools = normalizeTools(item.tools);
390
+ if (tools === item.tools) return item;
391
+ inputChanged = true;
392
+ return { ...item, tools };
393
+ });
394
+ if (inputChanged) normalizedBody = { ...normalizedBody, input };
395
+ }
396
+ return normalizedBody;
397
+ }
398
+
399
+ const MAX_RESPONSES_CALL_ID_LENGTH = 64;
400
+ const REPAIRED_CALL_ID_PREFIX = "call_ocx_";
401
+ const REPAIRED_CALL_ID_DIGEST_LENGTH = MAX_RESPONSES_CALL_ID_LENGTH - REPAIRED_CALL_ID_PREFIX.length;
402
+
403
+ /**
404
+ * The ChatGPT Responses backend rejects input `call_id` values longer than 64 characters. Codex
405
+ * sidechat/fork replay can namespace call ids from routed providers past that limit. Forward mode
406
+ * already sends explicit replay input without `previous_response_id`, so it is safe to replace each
407
+ * oversized id and every matching call/output occurrence with one deterministic request-local alias.
408
+ * Raw API-key continuations are intentionally excluded because an output-only continuation may
409
+ * reference a call stored upstream under the original id. Proxy-expanded API-key replays are
410
+ * explicit and stateless here, so they are safe to repair too.
411
+ */
412
+ function repairOversizedReplayCallIds(body: unknown): unknown {
413
+ if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
414
+
415
+ const occupied = new Set<string>();
416
+ for (const item of body.input) {
417
+ if (!isPlainObject(item) || typeof item.call_id !== "string") continue;
418
+ if (item.call_id.length <= MAX_RESPONSES_CALL_ID_LENGTH) occupied.add(item.call_id);
419
+ }
420
+
421
+ const aliases = new Map<string, string>();
422
+ let changed = false;
423
+ const input = body.input.map(item => {
424
+ if (!isPlainObject(item) || typeof item.call_id !== "string") return item;
425
+ const original = item.call_id;
426
+ if (original.length <= MAX_RESPONSES_CALL_ID_LENGTH) return item;
427
+
428
+ let alias = aliases.get(original);
429
+ if (!alias) {
430
+ let salt = 0;
431
+ do {
432
+ const hashInput = salt === 0 ? original : `${original}\0${salt}`;
433
+ const digest = createHash("sha256").update(hashInput).digest("hex");
434
+ alias = `${REPAIRED_CALL_ID_PREFIX}${digest.slice(0, REPAIRED_CALL_ID_DIGEST_LENGTH)}`;
435
+ salt += 1;
436
+ } while (occupied.has(alias));
437
+ aliases.set(original, alias);
438
+ occupied.add(alias);
439
+ }
440
+
441
+ changed = true;
442
+ return { ...item, call_id: alias };
443
+ });
444
+
445
+ return changed ? { ...body, input } : body;
446
+ }
447
+
448
+ /** Flatten a Responses tool-output `output` value (string or content-part array) to plain text. */
449
+ function toolOutputText(output: unknown): string {
450
+ if (typeof output === "string") return output;
451
+ if (!Array.isArray(output)) return JSON.stringify(output ?? "");
452
+ return output.map(part => {
453
+ if (!isPlainObject(part)) return "";
454
+ if (typeof part.text === "string") return part.text;
455
+ if (part.type === "refusal" && typeof part.refusal === "string") return `[refusal] ${part.refusal}`;
456
+ return "";
457
+ }).filter(Boolean).join("\n");
458
+ }
459
+
460
+ /**
461
+ * Repair a forward-mode input array whose continuation context was lost. When the replay
462
+ * expansion misses (proxy restart, unrecorded prior turn), previous_response_id is stripped
463
+ * (the ChatGPT backend rejects it), so the delta may carry items that reference now-absent
464
+ * prior items and 400 upstream:
465
+ * - `function_call_output`/`custom_tool_call_output` without their paired call item
466
+ * ("No tool call found for function call output with call_id ..."). Converted to user
467
+ * messages so the result text survives. `function_call_output` also pairs with
468
+ * `local_shell_call` (codex-rs emits shell outputs as function_call_output).
469
+ * - `reasoning` items ("Item 'rs_*' ... was provided without its required following item").
470
+ * Dropped, but only when `dropReasoning` (unexpanded miss): on a replay hit the prior
471
+ * reasoning chain is intact and must be preserved.
472
+ * Runs on every forward request; with intact pairs it returns the original reference.
473
+ */
474
+ function repairOrphanedInputItems(body: unknown, dropReasoning: boolean): unknown {
475
+ if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
476
+ const input = body.input;
477
+
478
+ const functionCallIds = new Set<string>();
479
+ const customCallIds = new Set<string>();
480
+ for (const item of input) {
481
+ if (!isPlainObject(item) || typeof item.call_id !== "string") continue;
482
+ if (item.type === "function_call" || item.type === "local_shell_call") functionCallIds.add(item.call_id);
483
+ else if (item.type === "custom_tool_call") customCallIds.add(item.call_id);
484
+ }
485
+
486
+ let changed = false;
487
+ const repaired: unknown[] = [];
488
+ for (const item of input) {
489
+ if (!isPlainObject(item)) { repaired.push(item); continue; }
490
+ if (dropReasoning && item.type === "reasoning") { changed = true; continue; }
491
+ const isFnOutput = item.type === "function_call_output";
492
+ const isCustomOutput = item.type === "custom_tool_call_output";
493
+ if (isFnOutput || isCustomOutput) {
494
+ const callId = typeof item.call_id === "string" ? item.call_id : "";
495
+ const paired = isFnOutput ? functionCallIds.has(callId) : customCallIds.has(callId);
496
+ if (!paired) {
497
+ changed = true;
498
+ repaired.push({
499
+ type: "message",
500
+ role: "user",
501
+ content: [{ type: "input_text", text: `[tool output for ${callId || "unknown call"}]\n${toolOutputText(item.output)}` }],
502
+ });
503
+ continue;
504
+ }
505
+ }
506
+ repaired.push(item);
507
+ }
508
+
509
+ return changed ? { ...body, input: repaired } : body;
510
+ }
511
+
512
+ /**
513
+ * Remove `previous_response_id` before forwarding. Two triggers:
514
+ * - the proxy expanded the request into a full input replay (the id is now redundant), or
515
+ * - the target is the ChatGPT backend (`authMode: "forward"`), whose Codex REST endpoint
516
+ * categorically rejects the parameter with `{"detail":"Unsupported parameter:
517
+ * previous_response_id"}` (strict allowlist; it also rejects `metadata` and
518
+ * `max_output_tokens`). Codex only sends the id on WS turns, and ocx converts those to
519
+ * internal HTTP requests, so forwarding it upstream is a guaranteed 400 — stripping is
520
+ * strictly better even when the local replay state missed. API-key mode keeps the field on
521
+ * unexpanded requests: the platform `/v1/responses` supports real server-side storage.
522
+ */
523
+ function stripPreviousResponseId(body: unknown, strip: boolean): unknown {
524
+ if (!strip || !isPlainObject(body) || !Object.prototype.hasOwnProperty.call(body, "previous_response_id")) return body;
525
+ const { previous_response_id: _previousResponseId, ...rest } = body;
526
+ return rest;
527
+ }
528
+
529
+ /**
530
+ * Drop request parameters a stateless Responses upstream cannot implement, and pin
531
+ * `store` false.
532
+ *
533
+ * `previous_response_id` is listed here as well as in `stripPreviousResponseId`
534
+ * because that helper's strip is conditional on replay expansion, and it keeps the
535
+ * field for API-key providers on the premise that the platform offers real
536
+ * server-side storage. DeepSeek documents the opposite: "the API is stateless:
537
+ * responses and conversations are not stored on the server", so the field can never
538
+ * be honoured regardless of expansion state.
539
+ *
540
+ * `prompt` is a reference to a server-stored prompt template — the most stateful
541
+ * field in the accepted schema.
542
+ *
543
+ * `service_tier` is deliberately NOT dropped: the server writes it for fast mode
544
+ * (`responses/core.ts`), and silently deleting a configured knob inside an adapter is
545
+ * worse than forwarding a parameter the upstream ignores.
546
+ *
547
+ * MUST run before the composed sanitize chain below: `stripItemIdsWhenUnstored` keys
548
+ * off `store === false`, and a stateless upstream cannot resolve a stored item id.
549
+ * Returns a copy, so `parsed._rawBody` keeps the client's original `store` value and
550
+ * the local replay cache still records the turn.
551
+ */
552
+ function stripStatefulResponsesParams(body: unknown): unknown {
553
+ if (!isPlainObject(body)) return body;
554
+ const drop = ["previous_response_id", "conversation", "background", "metadata", "prompt"] as const;
555
+ const present = drop.some(key => Object.prototype.hasOwnProperty.call(body, key));
556
+ if (!present && body.store === false) return body;
557
+ const next: Record<string, unknown> = { ...body };
558
+ for (const key of drop) delete next[key];
559
+ next.store = false;
560
+ return next;
561
+ }
562
+
563
+ /**
564
+ * Remove top-level parameters the ChatGPT backend (`authMode: "forward"`) rejects
565
+ * with `{"detail":"Unsupported parameter: …"}` (strict allowlist). Codex CLI never
566
+ * sends these — it controls output length via `reasoning.effort` — but third-party
567
+ * Responses API clients (GJC, SDK wrappers) include `max_output_tokens` per the
568
+ * public spec. `metadata` is likewise absent from the allowlist. No-op when the
569
+ * body carries neither field, keeping the common Codex path allocation-free.
570
+ */
571
+ function stripUnsupportedForwardParams(body: unknown): unknown {
572
+ if (!isPlainObject(body)) return body;
573
+ const hasMot = Object.prototype.hasOwnProperty.call(body, "max_output_tokens");
574
+ const hasMeta = Object.prototype.hasOwnProperty.call(body, "metadata");
575
+ if (!hasMot && !hasMeta) return body;
576
+ const { max_output_tokens: _mot, metadata: _meta, ...rest } = body;
577
+ return rest;
578
+ }
579
+
580
+ const IMAGE_GEN_NAMESPACE = "image_gen";
581
+ const HOSTED_IMAGE_GENERATION_TOOL = "image_generation";
582
+ const IMAGE_GEN_DOTTED_PREFIX = `${IMAGE_GEN_NAMESPACE}.`;
583
+ const IMAGE_GEN_WIRE_PREFIX = `${IMAGE_GEN_NAMESPACE}__`;
584
+
585
+ /** Remove a supported client prefix before constructing the canonical image-gen wire alias. */
586
+ function imageGenLocalName(name: string): string {
587
+ if (name.startsWith(IMAGE_GEN_DOTTED_PREFIX)) return name.slice(IMAGE_GEN_DOTTED_PREFIX.length);
588
+ if (name.startsWith(IMAGE_GEN_WIRE_PREFIX)) return name.slice(IMAGE_GEN_WIRE_PREFIX.length);
589
+ return name;
590
+ }
591
+
592
+ /** Build the flat public-Responses name used only on the upstream wire. */
593
+ function imageGenWireName(name: string): string {
594
+ return namespacedToolName(IMAGE_GEN_NAMESPACE, imageGenLocalName(name));
595
+ }
596
+
597
+ /** Match client image-gen declarations across namespace, legacy dotted, and canonical wire forms. */
598
+ function isImageGenClientName(name: string): boolean {
599
+ return name === IMAGE_GEN_NAMESPACE
600
+ || name.startsWith(IMAGE_GEN_DOTTED_PREFIX)
601
+ || name.startsWith(IMAGE_GEN_WIRE_PREFIX);
602
+ }
603
+
604
+ /** Identify declarations that should activate image-gen request normalization. */
605
+ function declaresImageGenClientTool(tool: unknown): boolean {
606
+ if (!isPlainObject(tool) || typeof tool.name !== "string") return false;
607
+ if (tool.type === "namespace") return tool.name === IMAGE_GEN_NAMESPACE;
608
+ return isImageGenClientName(tool.name);
609
+ }
610
+
611
+ /**
612
+ * Lower one complete Codex image-gen namespace to public Responses function tools.
613
+ *
614
+ * The public API reserves the `image_gen` namespace and restricts function names to a flat safe
615
+ * alphabet. `image_gen__<tool>` is therefore an upstream-only alias; client-facing responses are
616
+ * restored to explicit `{ namespace: "image_gen", name: "<tool>" }` calls by the server. Only a
617
+ * non-empty namespace containing named function tools is safe to lower. Malformed, empty, and
618
+ * future namespace shapes stay untouched instead of silently losing client capabilities.
619
+ */
620
+ function flattenImageGenNamespace(tool: unknown): Record<string, unknown>[] | undefined {
621
+ if (
622
+ !isPlainObject(tool)
623
+ || tool.type !== "namespace"
624
+ || tool.name !== IMAGE_GEN_NAMESPACE
625
+ || !Array.isArray(tool.tools)
626
+ || tool.tools.length === 0
627
+ ) return undefined;
628
+
629
+ for (const innerTool of tool.tools) {
630
+ if (
631
+ !isPlainObject(innerTool)
632
+ || innerTool.type !== "function"
633
+ || typeof innerTool.name !== "string"
634
+ || innerTool.name.length === 0
635
+ ) return undefined;
636
+ }
637
+
638
+ return tool.tools.map(innerTool => {
639
+ const functionTool = innerTool as Record<string, unknown> & { name: string };
640
+ return {
641
+ ...functionTool,
642
+ name: imageGenWireName(functionTool.name),
643
+ };
644
+ });
645
+ }
646
+
647
+ /** Convert a legacy dotted function declaration while preserving all other function metadata. */
648
+ function normalizeFlatImageGenFunction(tool: unknown): unknown {
649
+ if (
650
+ !isPlainObject(tool)
651
+ || tool.type !== "function"
652
+ || typeof tool.name !== "string"
653
+ || !tool.name.startsWith(IMAGE_GEN_DOTTED_PREFIX)
654
+ ) return tool;
655
+ return { ...tool, name: imageGenWireName(tool.name) };
656
+ }
657
+
658
+ /** Return the image-gen function name used for stable cross-container deduplication. */
659
+ function imageGenFunctionName(tool: unknown): string | undefined {
660
+ if (!isPlainObject(tool) || tool.type !== "function" || typeof tool.name !== "string") {
661
+ return undefined;
662
+ }
663
+ return isImageGenClientName(tool.name) ? tool.name : undefined;
664
+ }
665
+
666
+ /** True only when a declaration can yield a callable upstream-safe image-gen function alias. */
667
+ function declaresUsableImageGenAlias(tool: unknown): boolean {
668
+ if (flattenImageGenNamespace(tool)) return true;
669
+ if (!isPlainObject(tool) || tool.type !== "function" || typeof tool.name !== "string") {
670
+ return false;
671
+ }
672
+ if (tool.name.startsWith(IMAGE_GEN_DOTTED_PREFIX)) {
673
+ return tool.name.length > IMAGE_GEN_DOTTED_PREFIX.length;
674
+ }
675
+ return tool.name.startsWith(IMAGE_GEN_WIRE_PREFIX)
676
+ && tool.name.length > IMAGE_GEN_WIRE_PREFIX.length;
677
+ }
678
+
679
+ /** Collect client tool-choice names and the exact upstream aliases declared for them. */
680
+ function imageGenToolChoiceAliases(toolGroups: unknown[][]): Map<string, string> {
681
+ const aliases = new Map<string, string>();
682
+
683
+ for (const group of toolGroups) {
684
+ for (const tool of group) {
685
+ const flattened = flattenImageGenNamespace(tool);
686
+ if (flattened) {
687
+ for (const candidate of flattened) {
688
+ const wireName = candidate.name as string;
689
+ aliases.set(`${IMAGE_GEN_DOTTED_PREFIX}${imageGenLocalName(wireName)}`, wireName);
690
+ aliases.set(wireName, wireName);
691
+ }
692
+ continue;
693
+ }
694
+ if (!isPlainObject(tool) || tool.type !== "function" || typeof tool.name !== "string") {
695
+ continue;
696
+ }
697
+ if (
698
+ tool.name.startsWith(IMAGE_GEN_DOTTED_PREFIX)
699
+ && tool.name.length > IMAGE_GEN_DOTTED_PREFIX.length
700
+ ) {
701
+ aliases.set(tool.name, imageGenWireName(tool.name));
702
+ } else if (
703
+ tool.name.startsWith(IMAGE_GEN_WIRE_PREFIX)
704
+ && tool.name.length > IMAGE_GEN_WIRE_PREFIX.length
705
+ ) {
706
+ aliases.set(tool.name, tool.name);
707
+ }
708
+ }
709
+ }
710
+
711
+ return aliases;
712
+ }
713
+
714
+ /** Rewrite function selectors only when their corresponding declaration receives a wire alias. */
715
+ function normalizeImageGenToolChoice(
716
+ toolChoice: unknown,
717
+ aliases: ReadonlyMap<string, string>,
718
+ ): unknown {
719
+ if (!isPlainObject(toolChoice)) return toolChoice;
720
+
721
+ if (toolChoice.type === "function" && typeof toolChoice.name === "string") {
722
+ const alias = aliases.get(toolChoice.name);
723
+ return alias && alias !== toolChoice.name ? { ...toolChoice, name: alias } : toolChoice;
724
+ }
725
+
726
+ if (toolChoice.type !== "allowed_tools" || !Array.isArray(toolChoice.tools)) return toolChoice;
727
+ let changed = false;
728
+ const tools = toolChoice.tools.map(tool => {
729
+ if (!isPlainObject(tool) || tool.type !== "function" || typeof tool.name !== "string") {
730
+ return tool;
731
+ }
732
+ const alias = aliases.get(tool.name);
733
+ if (!alias || alias === tool.name) return tool;
734
+ changed = true;
735
+ return { ...tool, name: alias };
736
+ });
737
+ return changed ? { ...toolChoice, tools } : toolChoice;
738
+ }
739
+
740
+ /** Identify replayed image-gen calls that require upstream wire encoding. */
741
+ function declaresImageGenFunctionCall(item: unknown): boolean {
742
+ if (!isPlainObject(item) || item.type !== "function_call" || typeof item.name !== "string") {
743
+ return false;
744
+ }
745
+ return item.namespace === IMAGE_GEN_NAMESPACE || isImageGenClientName(item.name);
746
+ }
747
+
748
+ /** Encode native or legacy replay calls to the same flat name used by tool declarations. */
749
+ function normalizeImageGenFunctionCall(item: unknown): unknown {
750
+ if (!declaresImageGenFunctionCall(item) || !isPlainObject(item) || typeof item.name !== "string") {
751
+ return item;
752
+ }
753
+ if (item.namespace === IMAGE_GEN_NAMESPACE) {
754
+ const { namespace: _namespace, ...rest } = item;
755
+ return { ...rest, name: imageGenWireName(item.name) };
756
+ }
757
+ if (item.name.startsWith(IMAGE_GEN_DOTTED_PREFIX)) {
758
+ return { ...item, name: imageGenWireName(item.name) };
759
+ }
760
+ return item;
761
+ }
762
+
763
+ /**
764
+ * Normalize Codex's private image-gen tool declaration for API-key Responses providers.
765
+ *
766
+ * A complete `image_gen` namespace is flattened to safe `image_gen__<tool>` aliases even when it is
767
+ * the only image tool in the request. Replayed client calls are encoded to the same alias, including
768
+ * legacy dotted calls from older compatibility attempts. When a usable alias replaces a client
769
+ * image-gen declaration, the duplicate hosted `image_generation` entry is removed. Duplicate aliases
770
+ * are resolved in stable container order: top-level tools first, then Responses Lite
771
+ * `additional_tools` entries.
772
+ *
773
+ * This function is called only on the API-key path. ChatGPT forward mode understands the private
774
+ * namespace and must keep it. Copy-on-write preserves the original request reference when no
775
+ * namespace is flattened, hosted tool removed, or duplicate function discarded.
776
+ */
777
+ function normalizeImageGenClientTools(body: unknown): unknown {
778
+ if (!isPlainObject(body)) return body;
779
+
780
+ const toolGroups = collectResponsesToolGroups(body);
781
+ const hasImageGenClientTool = toolGroups.some(group => group.some(declaresImageGenClientTool))
782
+ || (Array.isArray(body.input) && body.input.some(declaresImageGenFunctionCall));
783
+ if (!hasImageGenClientTool) return body;
784
+ const hasUsableImageGenAlias = toolGroups.some(group => group.some(declaresUsableImageGenAlias));
785
+ const toolChoiceAliases = imageGenToolChoiceAliases(toolGroups);
786
+
787
+ const seenFunctionNames = new Set<string>();
788
+ const normalizeGroup = (tools: unknown[]): unknown[] => {
789
+ const normalized: unknown[] = [];
790
+ let groupChanged = false;
791
+
792
+ for (const tool of tools) {
793
+ if (
794
+ hasUsableImageGenAlias
795
+ && isPlainObject(tool)
796
+ && tool.type === HOSTED_IMAGE_GENERATION_TOOL
797
+ ) {
798
+ groupChanged = true;
799
+ continue;
800
+ }
801
+
802
+ const flattened = flattenImageGenNamespace(tool);
803
+ const candidates = flattened ?? [tool];
804
+ if (flattened) groupChanged = true;
805
+
806
+ for (const candidate of candidates) {
807
+ const normalizedCandidate = normalizeFlatImageGenFunction(candidate);
808
+ if (normalizedCandidate !== candidate) groupChanged = true;
809
+ const functionName = imageGenFunctionName(normalizedCandidate);
810
+ if (functionName && seenFunctionNames.has(functionName)) {
811
+ groupChanged = true;
812
+ continue;
813
+ }
814
+ if (functionName) seenFunctionNames.add(functionName);
815
+ normalized.push(normalizedCandidate);
816
+ }
817
+ }
818
+
819
+ return groupChanged ? normalized : tools;
820
+ };
821
+
822
+ let changed = false;
823
+ let tools = body.tools;
824
+ if (Array.isArray(body.tools)) {
825
+ tools = normalizeGroup(body.tools);
826
+ changed ||= tools !== body.tools;
827
+ }
828
+
829
+ let input = body.input;
830
+ if (Array.isArray(body.input)) {
831
+ let nestedChanged = false;
832
+ const mappedInput = body.input.map(item => {
833
+ if (isPlainObject(item) && item.type === "additional_tools" && Array.isArray(item.tools)) {
834
+ const nestedTools = normalizeGroup(item.tools);
835
+ if (nestedTools === item.tools) return item;
836
+ nestedChanged = true;
837
+ return { ...item, tools: nestedTools };
838
+ }
839
+ const normalizedCall = normalizeImageGenFunctionCall(item);
840
+ if (normalizedCall !== item) nestedChanged = true;
841
+ return normalizedCall;
842
+ });
843
+ if (nestedChanged) {
844
+ input = mappedInput;
845
+ changed = true;
846
+ }
847
+ }
848
+
849
+ const toolChoice = normalizeImageGenToolChoice(body.tool_choice, toolChoiceAliases);
850
+ changed ||= toolChoice !== body.tool_choice;
851
+
852
+ if (!changed) return body;
853
+ return {
854
+ ...body,
855
+ ...(Array.isArray(body.tools) ? { tools } : {}),
856
+ ...(Array.isArray(body.input) ? { input } : {}),
857
+ ...(Object.prototype.hasOwnProperty.call(body, "tool_choice") ? { tool_choice: toolChoice } : {}),
858
+ };
859
+ }
860
+
861
+ /**
862
+ * Remove hosted tool entries the target native slug rejects, so the OAuth-passthrough body never
863
+ * carries a tool the upstream model 400s on. No-op (returns the original reference) when nothing
864
+ * matches, keeping the common path allocation-free.
865
+ */
866
+ function stripUnsupportedHostedTools(body: unknown): unknown {
867
+ if (!isPlainObject(body) || !Array.isArray(body.tools)) return body;
868
+ const model = typeof body.model === "string" ? body.model : "";
869
+ const unsupported = UNSUPPORTED_HOSTED_TOOLS.filter(e => e.match(model));
870
+ if (unsupported.length === 0) return body;
871
+
872
+ const tools = body.tools.filter(t => {
873
+ const type = isPlainObject(t) && typeof t.type === "string" ? t.type : undefined;
874
+ if (!type) return true;
875
+ return !unsupported.some(e => e.tools.has(type));
876
+ });
877
+ return tools.length === body.tools.length ? body : { ...body, tools };
878
+ }
879
+
880
+ /** Replace every `input_image` part under a routed-compaction body with a short marker. */
881
+ function stripInputImagesDeep(value: unknown): unknown {
882
+ if (Array.isArray(value)) return value.map(stripInputImagesDeep);
883
+ if (!isPlainObject(value)) return value;
884
+ if (value.type === "input_image") {
885
+ return { type: "input_text", text: "[image omitted for compaction]" };
886
+ }
887
+ const out: Record<string, unknown> = {};
888
+ for (const [key, entry] of Object.entries(value)) out[key] = stripInputImagesDeep(entry);
889
+ return out;
890
+ }
891
+
892
+ /**
893
+ * Rewrite a compaction turn for an upstream that does not speak Codex's private
894
+ * `compaction_trigger` item: drop the trigger and the whole tool surface, and ask
895
+ * for the handoff summary in plain terms instead (#422).
896
+ *
897
+ * The adapter builds from `parsed._rawBody`, so the summarizer prompt that
898
+ * handleResponses() pushed onto `parsed.context` never reaches the wire — it has to
899
+ * be applied here. Images go too: a summary needs no pixels, and a text-only
900
+ * gateway would reject them.
901
+ */
902
+ function buildRoutedCompactionBody(body: unknown): unknown {
903
+ if (!isPlainObject(body)) return body;
904
+ const { tools: _tools, tool_choice: _toolChoice, parallel_tool_calls: _parallel, ...rest } = body;
905
+ const input = Array.isArray(body.input) ? body.input : [];
906
+ const kept = input.filter(item => !isPlainObject(item)
907
+ // `additional_tools` is how Codex Desktop's responses-lite shape carries tools;
908
+ // leaving it in would break the no-tools invariant even with `tools` removed.
909
+ || (item.type !== "compaction_trigger" && item.type !== "additional_tools"));
910
+ return {
911
+ ...rest,
912
+ input: [
913
+ ...(stripInputImagesDeep(kept) as unknown[]),
914
+ { type: "message", role: "user", content: [{ type: "input_text", text: COMPACT_PROMPT }] },
915
+ ],
916
+ };
917
+ }
918
+
919
+ /** Read the Responses `usage` block, if the gateway sent one. */
920
+ function usageFromResponsesPayload(payload: unknown): OcxUsage | undefined {
921
+ if (!isPlainObject(payload) || !isPlainObject(payload.usage)) return undefined;
922
+ const usage = payload.usage;
923
+ const inputTokens = typeof usage.input_tokens === "number" ? usage.input_tokens : 0;
924
+ const outputTokens = typeof usage.output_tokens === "number" ? usage.output_tokens : 0;
925
+ if (inputTokens === 0 && outputTokens === 0) return undefined;
926
+ return {
927
+ inputTokens,
928
+ outputTokens,
929
+ ...(typeof usage.total_tokens === "number" ? { totalTokens: usage.total_tokens } : {}),
930
+ };
931
+ }
932
+
933
+ function responsesPayloadText(response: unknown): string {
934
+ if (!isPlainObject(response) || !Array.isArray(response.output)) return "";
935
+ return response.output
936
+ .filter(item => isPlainObject(item) && item.type === "message")
937
+ .flatMap(item => (Array.isArray((item as Record<string, unknown>).content)
938
+ ? (item as { content: unknown[] }).content
939
+ : []))
940
+ .filter(part => isPlainObject(part) && part.type === "output_text")
941
+ .map(part => String((part as { text?: unknown }).text ?? ""))
942
+ .join("");
943
+ }
944
+
945
+ function responsesErrorMessage(payload: unknown): string {
946
+ if (!isPlainObject(payload)) return "upstream compaction failed";
947
+ const err = payload.error;
948
+ if (typeof err === "string") return err;
949
+ if (isPlainObject(err) && typeof err.message === "string") return err.message;
950
+ const incomplete = payload.incomplete_details;
951
+ if (isPlainObject(incomplete) && typeof incomplete.reason === "string") return incomplete.reason;
952
+ return "upstream compaction failed";
953
+ }
954
+
955
+ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig): ProviderAdapter & { passthrough: true } {
956
+ return {
957
+ name: "openai-responses",
958
+ passthrough: true as const,
959
+
960
+ buildRequest(parsed: OcxParsedRequest, incoming: IncomingMeta) {
961
+ const translatorBudget = incoming.translatorBudget;
962
+ const headers: Record<string, string> = { "Content-Type": "application/json" };
963
+ let url: string;
964
+
965
+ if (provider.authMode === "forward") {
966
+ // OAuth passthrough: ChatGPT backend path is `${baseUrl}/responses` (no /v1).
967
+ url = `${provider.baseUrl}/responses`;
968
+ if (provider.headers) Object.assign(headers, provider.headers); // static headers first…
969
+ const runtimeProvider = provider as {
970
+ _codexAccountOverride?: { accessToken: string; chatgptAccountId: string };
971
+ _codexAccountRequired?: boolean;
972
+ };
973
+ if (runtimeProvider._codexAccountRequired && !runtimeProvider._codexAccountOverride) {
974
+ throw new Error("Codex pool account auth is required but unavailable");
975
+ }
976
+ for (const h of FORWARD_HEADERS) {
977
+ const v = incoming?.headers.get(h);
978
+ if (v) headers[h] = v; // …so forwarded auth always wins.
979
+ }
980
+ const override = runtimeProvider._codexAccountOverride;
981
+ if (override) {
982
+ headers["authorization"] = `Bearer ${override.accessToken}`;
983
+ headers["chatgpt-account-id"] = override.chatgptAccountId;
984
+ }
985
+ } else {
986
+ if (provider.responsesPath === undefined) {
987
+ const base = provider.baseUrl.replace(/\/v1\/?$/, "");
988
+ url = `${base}/v1/responses`;
989
+ } else {
990
+ const base = provider.baseUrl.replace(/\/$/, "");
991
+ url = `${base}${provider.responsesPath}`;
992
+ }
993
+ if (provider.apiKey) headers["Authorization"] = `Bearer ${provider.apiKey}`;
994
+ if (provider.headers) Object.assign(headers, provider.headers);
995
+ }
996
+
997
+ const forward = provider.authMode === "forward";
998
+ const unexpandedMiss = !!parsed.previousResponseId && parsed._previousResponseInputExpanded !== true;
999
+ let outBody = stripPreviousResponseId(
1000
+ parsed._rawBody,
1001
+ forward || parsed._previousResponseInputExpanded === true,
1002
+ );
1003
+ const stateless = provider.statelessResponses === true;
1004
+ if (stateless) outBody = stripStatefulResponsesParams(outBody);
1005
+ // A replay miss can leave a function_call_output whose paired function_call sat
1006
+ // in the prefix that was never expanded. A stateless upstream cannot resolve the
1007
+ // pair from its own storage either, so it needs the same repair the forward
1008
+ // backend gets — dropping previous_response_id is not much use if the body that
1009
+ // reaches the wire is unparseable.
1010
+ if (forward || stateless) {
1011
+ outBody = repairOrphanedInputItems(outBody, unexpandedMiss);
1012
+ }
1013
+ if (forward) {
1014
+ outBody = stripUnsupportedForwardParams(outBody);
1015
+ }
1016
+ else outBody = normalizeImageGenClientTools(outBody);
1017
+ if (forward || parsed._previousResponseInputExpanded === true) {
1018
+ outBody = repairOversizedReplayCallIds(outBody);
1019
+ }
1020
+ outBody = stripUnsupportedReasoningSummaryDelivery(outBody, parsed.modelId);
1021
+ // Same predicate as the routedCompaction gate in handleResponses(): an
1022
+ // authMode check would let a noncanonical custom forward provider skip this
1023
+ // rewrite while the server still routes it as a summarizer turn (#422).
1024
+ if (parsed._compactionRequest === true && !isCanonicalOpenAiForwardProvider(provider)) {
1025
+ outBody = buildRoutedCompactionBody(outBody);
1026
+ }
1027
+ const sanitizedBody = normalizeToolSchemas(stripSparkCompatibility(stripUnsupportedReasoningParams(stripItemIdsWhenUnstored(stripInvalidItemIds(stripUnsupportedHostedTools(sanitizeReasoningInputContent(scrubOcxCompactionItems(outBody))))))));
1028
+ const body = JSON.stringify(stripDisabledReasoningSummaries(
1029
+ normalizeConfiguredReasoningSummaryDelivery(sanitizedBody, provider, parsed.modelId),
1030
+ provider,
1031
+ parsed.modelId,
1032
+ ));
1033
+ const releaseBodyObservation = translatorBudget.observeExternallyCapped(
1034
+ "passthrough_serialization",
1035
+ new TextEncoder().encode(body).byteLength,
1036
+ );
1037
+ return {
1038
+ url,
1039
+ method: "POST",
1040
+ headers,
1041
+ body,
1042
+ releaseBodyObservation,
1043
+ };
1044
+ },
1045
+
1046
+ // The passthrough normally relays the upstream stream verbatim and never parses.
1047
+ // The exception is a routed compaction turn: the server drives this adapter like
1048
+ // an ordinary one so the bridge can build the single compaction item (#422).
1049
+ async *parseStream(response: Response, budget: TranslatorBudget): AsyncGenerator<AdapterEvent> {
1050
+ if (!response.body) {
1051
+ yield { type: "error", message: "passthrough adapter received no response body" };
1052
+ return;
1053
+ }
1054
+ const budgetEncoder = new TextEncoder();
1055
+ let deltas = "";
1056
+ let doneText = "";
1057
+ let snapshot = "";
1058
+ let usage: OcxUsage | undefined;
1059
+ for await (const event of decodeServerSentEvents(response.body, { translatorBudget: budget })) {
1060
+ let payload: unknown;
1061
+ try { payload = JSON.parse(event.data); } catch { continue; }
1062
+ if (!isPlainObject(payload)) continue;
1063
+ switch (payload.type) {
1064
+ case "response.output_text.delta":
1065
+ if (typeof payload.delta === "string") {
1066
+ const next = deltas + payload.delta;
1067
+ const previousBytes = budgetEncoder.encode(deltas).byteLength;
1068
+ const reservation = budget.reserveTransient(budgetEncoder.encode(next).byteLength, { kind: "retained_collectors" });
1069
+ deltas = next;
1070
+ reservation.commitRetained();
1071
+ budget.releaseRetained(previousBytes, { kind: "retained_collectors" });
1072
+ }
1073
+ break;
1074
+ case "response.output_text.done":
1075
+ if (typeof payload.text === "string") {
1076
+ const next = doneText + payload.text;
1077
+ const previousBytes = budgetEncoder.encode(doneText).byteLength;
1078
+ const reservation = budget.reserveTransient(budgetEncoder.encode(next).byteLength, { kind: "retained_collectors" });
1079
+ doneText = next;
1080
+ reservation.commitRetained();
1081
+ budget.releaseRetained(previousBytes, { kind: "retained_collectors" });
1082
+ }
1083
+ break;
1084
+ case "response.failed":
1085
+ case "error":
1086
+ yield { type: "error", message: responsesErrorMessage(payload.response ?? payload) };
1087
+ return;
1088
+ case "response.incomplete":
1089
+ yield { type: "incomplete", reason: responsesErrorMessage(payload.response ?? payload) };
1090
+ return;
1091
+ case "response.completed":
1092
+ {
1093
+ const next = responsesPayloadText(payload.response);
1094
+ const previousBytes = budgetEncoder.encode(snapshot).byteLength;
1095
+ const reservation = budget.reserveTransient(budgetEncoder.encode(next).byteLength, { kind: "retained_collectors" });
1096
+ snapshot = next;
1097
+ reservation.commitRetained();
1098
+ budget.releaseRetained(previousBytes, { kind: "retained_collectors" });
1099
+ }
1100
+ usage = usageFromResponsesPayload(payload.response);
1101
+ break;
1102
+ }
1103
+ }
1104
+ // Gateways differ in which of these they emit; prefer the authoritative
1105
+ // completed snapshot so text is never double-counted.
1106
+ const text = snapshot || doneText || deltas;
1107
+ if (text) yield { type: "text_delta", text };
1108
+ budget.releaseRetained(budgetEncoder.encode(deltas).byteLength + budgetEncoder.encode(doneText).byteLength + budgetEncoder.encode(snapshot).byteLength, { kind: "retained_collectors" });
1109
+ yield { type: "done", ...(usage ? { usage } : {}) };
1110
+ },
1111
+
1112
+ async parseResponse(response: Response, budget: TranslatorBudget): Promise<AdapterEvent[]> {
1113
+ let payload: unknown;
1114
+ try { payload = await response.json(); } catch {
1115
+ return [{ type: "error", message: "malformed upstream compaction response" }];
1116
+ }
1117
+ budget.chargeRetained(new TextEncoder().encode(JSON.stringify(payload)).byteLength, { kind: "retained_collectors" });
1118
+ if (!isPlainObject(payload)) {
1119
+ return [{ type: "error", message: "malformed upstream compaction response" }];
1120
+ }
1121
+ if (payload.error || payload.status === "failed") {
1122
+ return [{ type: "error", message: responsesErrorMessage(payload) }];
1123
+ }
1124
+ if (payload.status === "incomplete") {
1125
+ return [{ type: "incomplete", reason: responsesErrorMessage(payload) }];
1126
+ }
1127
+ const text = responsesPayloadText(payload);
1128
+ if (!text) {
1129
+ // A completed turn with no usable text cannot become a summary; saying so is
1130
+ // better than installing an empty compaction as replacement history.
1131
+ return [{ type: "error", message: "upstream compaction returned no summary text" }];
1132
+ }
1133
+ const usage = usageFromResponsesPayload(payload);
1134
+ return [{ type: "text_delta", text }, { type: "done", ...(usage ? { usage } : {}) }];
1135
+ },
1136
+ };
1137
+ }