@iislee/opencodex 2.11.0 → 2.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (675) hide show
  1. package/AGENTS_INSTALL.md +109 -0
  2. package/README.md +114 -19
  3. package/bin/ocx.mjs +164 -36
  4. package/bin/package-main.mjs +1 -1
  5. package/gui/dist/assets/index-BF38heuV.js +104 -0
  6. package/gui/dist/assets/index-DMiI18Kv.css +1 -0
  7. package/gui/dist/index.html +2 -2
  8. package/gui/dist/provider-icons/alibaba-color.svg +1 -1
  9. package/gui/dist/provider-icons/antigravity-color.svg +1 -1
  10. package/gui/dist/provider-icons/claude-color.svg +1 -1
  11. package/gui/dist/provider-icons/cline-color.svg +16 -0
  12. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
  13. package/gui/dist/provider-icons/commandcode-color.svg +1 -0
  14. package/gui/dist/provider-icons/copilot-color.svg +1 -1
  15. package/gui/dist/provider-icons/cursor-color.svg +1 -1
  16. package/gui/dist/provider-icons/deepseek-color.svg +1 -1
  17. package/gui/dist/provider-icons/firepass-color.svg +1 -1
  18. package/gui/dist/provider-icons/fireworks-color.svg +1 -1
  19. package/gui/dist/provider-icons/gemini-color.svg +1 -1
  20. package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
  21. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
  22. package/gui/dist/provider-icons/grok.svg +1 -1
  23. package/gui/dist/provider-icons/groq-color.svg +1 -1
  24. package/gui/dist/provider-icons/huggingface-color.svg +1 -1
  25. package/gui/dist/provider-icons/kimi-color.svg +1 -1
  26. package/gui/dist/provider-icons/kiro-color.svg +2 -2
  27. package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
  28. package/gui/dist/provider-icons/mistral-color.svg +1 -1
  29. package/gui/dist/provider-icons/moonshot-color.svg +1 -1
  30. package/gui/dist/provider-icons/nvidia-color.svg +1 -1
  31. package/gui/dist/provider-icons/ollama-color.svg +1 -1
  32. package/gui/dist/provider-icons/openai.svg +1 -1
  33. package/gui/dist/provider-icons/opencode.svg +2 -1
  34. package/gui/dist/provider-icons/openrouter-color.svg +1 -1
  35. package/gui/dist/provider-icons/pi.svg +2 -2
  36. package/gui/dist/provider-icons/qianfan-color.svg +1 -1
  37. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
  38. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
  39. package/gui/dist/provider-icons/vllm-color.svg +1 -1
  40. package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
  41. package/package.json +19 -10
  42. package/src/adapters/anthropic-output-schema.ts +137 -0
  43. package/src/adapters/anthropic.ts +376 -52
  44. package/src/adapters/base.ts +54 -7
  45. package/src/adapters/client-fingerprint.ts +18 -12
  46. package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
  47. package/src/adapters/command-code.ts +601 -0
  48. package/src/adapters/cursor/checkpoint-store.ts +303 -0
  49. package/src/adapters/cursor/cursor-errors.ts +129 -6
  50. package/src/adapters/cursor/discovery.ts +61 -4
  51. package/src/adapters/cursor/effort-map.ts +27 -3
  52. package/src/adapters/cursor/framing.ts +39 -0
  53. package/src/adapters/cursor/h2-pool.ts +123 -0
  54. package/src/adapters/cursor/http1-bidi.ts +361 -0
  55. package/src/adapters/cursor/images.ts +704 -0
  56. package/src/adapters/cursor/live-models.ts +174 -58
  57. package/src/adapters/cursor/live-transport.ts +609 -170
  58. package/src/adapters/cursor/native-exec-common.ts +23 -2
  59. package/src/adapters/cursor/native-exec-fs.ts +10 -7
  60. package/src/adapters/cursor/native-exec-network.ts +1 -1
  61. package/src/adapters/cursor/native-exec-shell.ts +0 -1
  62. package/src/adapters/cursor/native-exec.ts +101 -14
  63. package/src/adapters/cursor/protobuf-events.ts +829 -11
  64. package/src/adapters/cursor/protobuf-request.ts +383 -65
  65. package/src/adapters/cursor/request-builder.ts +237 -31
  66. package/src/adapters/cursor/tool-definitions.ts +242 -12
  67. package/src/adapters/cursor/tool-result-normalize.ts +92 -0
  68. package/src/adapters/cursor/transport.ts +22 -0
  69. package/src/adapters/cursor/types.ts +28 -1
  70. package/src/adapters/cursor.ts +133 -7
  71. package/src/adapters/google-antigravity-replay.ts +558 -34
  72. package/src/adapters/google-antigravity-wire.ts +43 -10
  73. package/src/adapters/google-http.ts +38 -10
  74. package/src/adapters/google-tool-schema.ts +80 -15
  75. package/src/adapters/google-truncation.ts +11 -0
  76. package/src/adapters/google.ts +618 -74
  77. package/src/adapters/identity.ts +39 -6
  78. package/src/adapters/kiro-errors.ts +11 -0
  79. package/src/adapters/kiro-events.ts +19 -1
  80. package/src/adapters/kiro-thinking.ts +18 -2
  81. package/src/adapters/kiro-tools.ts +10 -1
  82. package/src/adapters/kiro.ts +92 -54
  83. package/src/adapters/mimo-free.ts +17 -0
  84. package/src/adapters/openai-chat-url.ts +11 -0
  85. package/src/adapters/openai-chat.ts +1311 -336
  86. package/src/adapters/openai-responses-url.ts +16 -0
  87. package/src/adapters/openai-responses.ts +830 -56
  88. package/src/adapters/registry.ts +175 -0
  89. package/src/adapters/responses-tool-schema.ts +67 -0
  90. package/src/adapters/tool-call-id.ts +119 -0
  91. package/src/adapters/tool-catalog-nudge.ts +104 -21
  92. package/src/adapters/xai-web-search.ts +185 -0
  93. package/src/bridge.ts +428 -61
  94. package/src/chat/inbound.ts +43 -19
  95. package/src/chat/outbound.ts +82 -26
  96. package/src/claude/agents-inject.ts +32 -9
  97. package/src/claude/context-windows.ts +21 -5
  98. package/src/claude/desktop-3p.ts +243 -9
  99. package/src/claude/gateway-cache.ts +41 -4
  100. package/src/claude/inbound.ts +72 -3
  101. package/src/claude/model-info.ts +38 -15
  102. package/src/claude/outbound.ts +70 -16
  103. package/src/cli/account-api.ts +35 -1
  104. package/src/cli/account-auth.ts +33 -6
  105. package/src/cli/account-catalog-refresh.ts +14 -0
  106. package/src/cli/account-extended.ts +389 -2
  107. package/src/cli/account-main.ts +317 -0
  108. package/src/cli/account.ts +30 -6
  109. package/src/cli/agent.ts +45 -1
  110. package/src/cli/claude-agent-startup-sync.ts +73 -0
  111. package/src/cli/claude-desktop.ts +29 -4
  112. package/src/cli/claude.ts +103 -34
  113. package/src/cli/codex-log-guard-doctor.ts +103 -0
  114. package/src/cli/codex-shim-autorestore.ts +2 -0
  115. package/src/cli/codex-shim-readiness.ts +76 -0
  116. package/src/cli/combo.ts +8 -0
  117. package/src/cli/config-command.ts +74 -10
  118. package/src/cli/dispatch.ts +593 -0
  119. package/src/cli/doctor.ts +315 -43
  120. package/src/cli/ensure-desired-integrations.ts +152 -0
  121. package/src/cli/export-command.ts +46 -20
  122. package/src/cli/help.ts +20 -271
  123. package/src/cli/index.ts +378 -519
  124. package/src/cli/init.ts +4 -17
  125. package/src/cli/integrations.ts +120 -2
  126. package/src/cli/lab.ts +607 -0
  127. package/src/cli/launcher-context.ts +77 -0
  128. package/src/cli/minimax.ts +497 -0
  129. package/src/cli/models-runtime.ts +35 -2
  130. package/src/cli/models.ts +100 -14
  131. package/src/cli/observe.ts +92 -3
  132. package/src/cli/opencode.ts +4 -2
  133. package/src/cli/provider-runtime.ts +18 -1
  134. package/src/cli/provider.ts +24 -3
  135. package/src/cli/ready.ts +301 -0
  136. package/src/cli/registry.ts +437 -0
  137. package/src/cli/root.ts +86 -0
  138. package/src/cli/route-policy.ts +92 -0
  139. package/src/cli/runtime-api.ts +6 -3
  140. package/src/cli/star-prompt.ts +71 -15
  141. package/src/cli/status.ts +10 -3
  142. package/src/cli/system-restart-client.ts +146 -0
  143. package/src/cli/tray-proxy.ts +153 -6
  144. package/src/cli/v2.ts +105 -10
  145. package/src/cli.ts +1 -1
  146. package/src/clients/config-export.ts +1358 -21
  147. package/src/codex/account-label.ts +14 -1
  148. package/src/codex/account-lifecycle.ts +130 -13
  149. package/src/codex/account-namespaces.ts +49 -3
  150. package/src/codex/account-priority.ts +83 -0
  151. package/src/codex/account-store.ts +29 -2
  152. package/src/codex/account-usability.ts +25 -2
  153. package/src/codex/admission.ts +256 -0
  154. package/src/codex/affinity-debug.ts +162 -0
  155. package/src/codex/app-server-processes.ts +493 -106
  156. package/src/codex/app-server-restart-service.ts +232 -0
  157. package/src/codex/auth-api.ts +849 -242
  158. package/src/codex/auth-collision.ts +5 -3
  159. package/src/codex/auth-context.ts +345 -32
  160. package/src/codex/autostart-health.ts +8 -1
  161. package/src/codex/catalog/account-models.ts +67 -0
  162. package/src/codex/catalog/aggregation.ts +68 -10
  163. package/src/codex/catalog/bundled.ts +331 -33
  164. package/src/codex/catalog/effort.ts +121 -30
  165. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  166. package/src/codex/catalog/kinds.ts +2 -0
  167. package/src/codex/catalog/metadata.ts +529 -45
  168. package/src/codex/catalog/native-models.ts +72 -0
  169. package/src/codex/catalog/parsing.ts +224 -30
  170. package/src/codex/catalog/provider-fetch.ts +1460 -134
  171. package/src/codex/catalog/sync.ts +1449 -186
  172. package/src/codex/catalog-admission.ts +199 -0
  173. package/src/codex/catalog-refresh-status.ts +105 -0
  174. package/src/codex/catalog-write-serialization.ts +242 -0
  175. package/src/codex/catalog.ts +6 -3
  176. package/src/codex/codex-write-lock.ts +384 -0
  177. package/src/codex/convergence-types.ts +614 -0
  178. package/src/codex/convergence.ts +651 -0
  179. package/src/codex/coordinator-doctor.ts +332 -0
  180. package/src/codex/custom-model-catalog-migration.ts +176 -0
  181. package/src/codex/desired-state.ts +230 -0
  182. package/src/codex/features.ts +636 -39
  183. package/src/codex/generation.ts +202 -0
  184. package/src/codex/history-job.ts +407 -0
  185. package/src/codex/history-lock.ts +242 -0
  186. package/src/codex/history-migration-guardian.ts +26 -20
  187. package/src/codex/history-provider.ts +231 -28
  188. package/src/codex/history-transition.ts +105 -0
  189. package/src/codex/history-worker.ts +220 -0
  190. package/src/codex/inject-coordination.ts +290 -0
  191. package/src/codex/inject.ts +1073 -152
  192. package/src/codex/injected-marker.ts +37 -3
  193. package/src/codex/integration-record.ts +266 -0
  194. package/src/codex/internal/catalog-writer.ts +203 -0
  195. package/src/codex/internal/history-writer.ts +80 -0
  196. package/src/codex/journal.ts +66 -4
  197. package/src/codex/log-guard/inspect.ts +506 -0
  198. package/src/codex/log-guard/lock.ts +150 -0
  199. package/src/codex/log-guard/maintenance.ts +403 -0
  200. package/src/codex/log-guard/path-safety.ts +88 -0
  201. package/src/codex/log-guard/policy.ts +44 -0
  202. package/src/codex/log-guard/processes.ts +205 -0
  203. package/src/codex/log-guard/protection.ts +489 -0
  204. package/src/codex/log-guard/sqlite-errors.ts +9 -0
  205. package/src/codex/main-account-cache.ts +24 -0
  206. package/src/codex/main-account.ts +29 -1
  207. package/src/codex/management-convergence.ts +167 -0
  208. package/src/codex/model-cache.ts +56 -10
  209. package/src/codex/model-entitlements.ts +353 -0
  210. package/src/codex/native-main-admission.ts +47 -0
  211. package/src/codex/native-main-auth-temp.ts +187 -0
  212. package/src/codex/native-main-claim.ts +178 -0
  213. package/src/codex/native-main-lock-file.ts +162 -0
  214. package/src/codex/native-main-owner.ts +329 -0
  215. package/src/codex/native-profile-api.ts +247 -0
  216. package/src/codex/native-profile-manager.ts +1531 -0
  217. package/src/codex/native-profile-processes.ts +121 -0
  218. package/src/codex/native-profile-recovery.ts +99 -0
  219. package/src/codex/native-profile-stage-store.ts +387 -0
  220. package/src/codex/native-profile-startup.ts +492 -0
  221. package/src/codex/native-profile-store.ts +855 -0
  222. package/src/codex/native-profile-types.ts +120 -0
  223. package/src/codex/native-residue.ts +682 -0
  224. package/src/codex/paths.ts +80 -1
  225. package/src/codex/plan-from-token.ts +140 -0
  226. package/src/codex/plan.ts +40 -0
  227. package/src/codex/plugins-doctor.ts +1 -1
  228. package/src/codex/pool-rotation.ts +74 -4
  229. package/src/codex/project-config-warnings.ts +20 -6
  230. package/src/codex/prompt-journal.ts +352 -0
  231. package/src/codex/prompt-layers.ts +967 -0
  232. package/src/codex/prompt-lock.ts +143 -0
  233. package/src/codex/quota-rejection.ts +298 -0
  234. package/src/codex/quota.ts +175 -13
  235. package/src/codex/refresh.ts +11 -2
  236. package/src/codex/reset-credit-recovery.ts +1044 -0
  237. package/src/codex/routing.ts +505 -94
  238. package/src/codex/runtime.ts +159 -38
  239. package/src/codex/shim.ts +1009 -28
  240. package/src/codex/subagent-model-fallback.ts +350 -35
  241. package/src/codex/sync.ts +191 -2
  242. package/src/codex/transition-state.ts +612 -0
  243. package/src/codex/upstream-host-health.ts +368 -0
  244. package/src/codex/user-identity.ts +557 -0
  245. package/src/codex/warmup.ts +187 -81
  246. package/src/codex/write-coordination.ts +114 -0
  247. package/src/combos/failover.ts +20 -0
  248. package/src/combos/index.ts +4 -0
  249. package/src/combos/request.ts +32 -0
  250. package/src/combos/types.ts +81 -9
  251. package/src/config/provider-name.ts +24 -0
  252. package/src/config.ts +1762 -140
  253. package/src/generated/compatibility-version.json +3164 -0
  254. package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +19 -17
  255. package/src/grok/inject.ts +16 -5
  256. package/src/grok/inspect.ts +45 -0
  257. package/src/grok/sync.ts +2 -2
  258. package/src/images/loop.ts +152 -29
  259. package/src/images/plan.ts +23 -13
  260. package/src/integrations/config-io.ts +269 -0
  261. package/src/integrations/journal.ts +315 -0
  262. package/src/integrations/merge.ts +135 -0
  263. package/src/integrations/mutation-flight.ts +71 -0
  264. package/src/integrations/native/ownership-preflight.ts +202 -0
  265. package/src/integrations/omp-yaml-source.ts +358 -0
  266. package/src/integrations/owned-refresh.ts +74 -0
  267. package/src/integrations/ownership.ts +111 -0
  268. package/src/integrations/registry.ts +159 -0
  269. package/src/integrations/serialize.ts +314 -0
  270. package/src/integrations/state.ts +361 -0
  271. package/src/integrations/store.ts +103 -0
  272. package/src/integrations/writer-lock.ts +98 -0
  273. package/src/integrations/writer.ts +691 -0
  274. package/src/lab/artifacts/sanitize.ts +586 -0
  275. package/src/lab/artifacts/secure-fs.ts +475 -0
  276. package/src/lab/artifacts/store.ts +310 -0
  277. package/src/lab/automation/budgets.ts +78 -0
  278. package/src/lab/automation/config-persistence.ts +256 -0
  279. package/src/lab/automation/constants.ts +39 -0
  280. package/src/lab/automation/cooldown.ts +103 -0
  281. package/src/lab/automation/dispatch.ts +211 -0
  282. package/src/lab/automation/index.ts +13 -0
  283. package/src/lab/automation/orchestrator.ts +499 -0
  284. package/src/lab/automation/persistence.ts +512 -0
  285. package/src/lab/automation/planner.ts +371 -0
  286. package/src/lab/automation/policy.ts +136 -0
  287. package/src/lab/automation/queue.ts +191 -0
  288. package/src/lab/automation/recovery.ts +24 -0
  289. package/src/lab/automation/route-context.ts +21 -0
  290. package/src/lab/automation/run-key.ts +44 -0
  291. package/src/lab/automation/runs-query.ts +34 -0
  292. package/src/lab/automation/types.ts +160 -0
  293. package/src/lab/conformance/assertion.ts +325 -0
  294. package/src/lab/conformance/digest.ts +22 -0
  295. package/src/lab/conformance/executor.ts +741 -0
  296. package/src/lab/conformance/fixture-provider.ts +27 -0
  297. package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
  298. package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
  299. package/src/lab/conformance/harness-budget.ts +47 -0
  300. package/src/lab/conformance/index.ts +5 -0
  301. package/src/lab/conformance/jcs.ts +64 -0
  302. package/src/lab/conformance/json-pointer.ts +39 -0
  303. package/src/lab/conformance/manifest.ts +180 -0
  304. package/src/lab/conformance/mcp-stub.ts +179 -0
  305. package/src/lab/conformance/negative-controls.ts +164 -0
  306. package/src/lab/conformance/observation.ts +355 -0
  307. package/src/lab/conformance/runner.ts +68 -0
  308. package/src/lab/conformance/sse-normalize.ts +59 -0
  309. package/src/lab/conformance/suite-manifest.ts +78 -0
  310. package/src/lab/conformance/types.ts +214 -0
  311. package/src/lab/constants.ts +126 -0
  312. package/src/lab/digest.ts +64 -0
  313. package/src/lab/events/errors.ts +9 -0
  314. package/src/lab/events/limits.ts +117 -0
  315. package/src/lab/events/types.ts +229 -0
  316. package/src/lab/events/validate.ts +781 -0
  317. package/src/lab/fabric/constants.ts +40 -0
  318. package/src/lab/fabric/executor.ts +492 -0
  319. package/src/lab/fabric/index.ts +80 -0
  320. package/src/lab/fabric/manifest.ts +222 -0
  321. package/src/lab/fabric/observe.ts +489 -0
  322. package/src/lab/fabric/patch.ts +79 -0
  323. package/src/lab/fabric/producer-child.ts +139 -0
  324. package/src/lab/fabric/producer-isolate.ts +276 -0
  325. package/src/lab/fabric/producer-protocol.ts +61 -0
  326. package/src/lab/fabric/scratch.ts +439 -0
  327. package/src/lab/fabric/subject.ts +106 -0
  328. package/src/lab/fabric/types.ts +134 -0
  329. package/src/lab/fabric/verifier.ts +98 -0
  330. package/src/lab/index.ts +54 -0
  331. package/src/lab/ledger/artifact-refs.ts +127 -0
  332. package/src/lab/ledger/invalidation.ts +136 -0
  333. package/src/lab/ledger/purge.ts +310 -0
  334. package/src/lab/ledger/store.ts +532 -0
  335. package/src/lab/live/credential-lease.ts +53 -0
  336. package/src/lab/live/destination.ts +155 -0
  337. package/src/lab/live/executor.ts +336 -0
  338. package/src/lab/live/inert-tools.ts +56 -0
  339. package/src/lab/live/manifest.ts +85 -0
  340. package/src/lab/live/mcp-loopback.ts +57 -0
  341. package/src/lab/live/runner.ts +19 -0
  342. package/src/lab/live/sandbox.ts +61 -0
  343. package/src/lab/live/suite-manifest.ts +41 -0
  344. package/src/lab/live/transport.ts +118 -0
  345. package/src/lab/live/types.ts +197 -0
  346. package/src/lab/observe/from-conformance.ts +301 -0
  347. package/src/lab/observe/from-live.ts +117 -0
  348. package/src/lab/paths.ts +153 -0
  349. package/src/lab/projection/rebuild.ts +495 -0
  350. package/src/lab/projection/schema.ts +135 -0
  351. package/src/lab/projection/verdicts.ts +474 -0
  352. package/src/lab/projection/verification.ts +412 -0
  353. package/src/lab/public/bundle.ts +217 -0
  354. package/src/lab/public/community-authority.ts +175 -0
  355. package/src/lab/public/community-files.ts +29 -0
  356. package/src/lab/public/community.ts +479 -0
  357. package/src/lab/public/file-safety.ts +155 -0
  358. package/src/lab/public/ids.ts +26 -0
  359. package/src/lab/public/index.ts +16 -0
  360. package/src/lab/public/mutation-lock.ts +424 -0
  361. package/src/lab/public/operator.ts +353 -0
  362. package/src/lab/public/origin-purge.ts +79 -0
  363. package/src/lab/public/origin.ts +203 -0
  364. package/src/lab/public/privacy.ts +143 -0
  365. package/src/lab/public/private-file.ts +261 -0
  366. package/src/lab/public/project.ts +124 -0
  367. package/src/lab/public/purge-test-fault.ts +21 -0
  368. package/src/lab/public/purge.ts +223 -0
  369. package/src/lab/public/registry.ts +44 -0
  370. package/src/lab/public/revocation.ts +252 -0
  371. package/src/lab/public/signature.ts +243 -0
  372. package/src/lab/public/storage.ts +105 -0
  373. package/src/lab/public/strict-json.ts +206 -0
  374. package/src/lab/public/time.ts +26 -0
  375. package/src/lab/public/types.ts +172 -0
  376. package/src/lab/public/validate.ts +391 -0
  377. package/src/lab/query/catalog.ts +101 -0
  378. package/src/lab/query/connection.ts +107 -0
  379. package/src/lab/query/constants.ts +4 -0
  380. package/src/lab/query/cursor.ts +132 -0
  381. package/src/lab/query/dto-map.ts +277 -0
  382. package/src/lab/query/errors.ts +22 -0
  383. package/src/lab/query/freshness.ts +53 -0
  384. package/src/lab/query/index.ts +45 -0
  385. package/src/lab/query/latest-observation.ts +59 -0
  386. package/src/lab/query/passive-production.ts +159 -0
  387. package/src/lab/query/queries.ts +444 -0
  388. package/src/lab/query/types.ts +266 -0
  389. package/src/lab/subject/behavior-fingerprint.ts +77 -0
  390. package/src/lab/subject/installation-salt.ts +112 -0
  391. package/src/lab/subject/protocol-subject.ts +80 -0
  392. package/src/lab/subject/route-subject.ts +74 -0
  393. package/src/lib/app-owned-memory-stores.ts +22 -0
  394. package/src/lib/bounded-body.ts +153 -9
  395. package/src/lib/bun-runtime.ts +125 -12
  396. package/src/lib/bun-stream-caps.ts +13 -9
  397. package/src/lib/codex-restart-contract.ts +120 -0
  398. package/src/lib/config-ownership.ts +6 -2
  399. package/src/lib/destination-policy.ts +65 -1
  400. package/src/lib/errors.ts +44 -2
  401. package/src/lib/fabric-task-execution-authority.ts +7 -0
  402. package/src/lib/fabric-task-host.ts +29 -0
  403. package/src/lib/lab-activation.ts +223 -0
  404. package/src/lib/lab-live-execution-authority.ts +13 -0
  405. package/src/lib/lab-live-host.ts +30 -0
  406. package/src/lib/lab-live-pinned-sender.ts +56 -0
  407. package/src/lib/lab-live-route-production.ts +130 -0
  408. package/src/lib/lab-passive-linker-registration.ts +26 -0
  409. package/src/lib/local-management-attestation.ts +51 -0
  410. package/src/lib/local-management-capability.ts +100 -0
  411. package/src/lib/local-provider-reload-contract.ts +100 -0
  412. package/src/lib/optional-shutdown-hooks.ts +57 -0
  413. package/src/lib/pinned-http.ts +145 -26
  414. package/src/lib/process-control.ts +4 -1
  415. package/src/lib/provider-outbound.ts +49 -9
  416. package/src/lib/redact.ts +419 -3
  417. package/src/lib/self-launch-argv.ts +15 -0
  418. package/src/lib/server-resource-ownership.ts +71 -0
  419. package/src/lib/shadow-call.ts +35 -4
  420. package/src/lib/sse-decoder.ts +41 -0
  421. package/src/lib/state-store-registrations.ts +10 -2
  422. package/src/lib/system-restart-contract.ts +73 -0
  423. package/src/lib/token-estimate.ts +19 -2
  424. package/src/lib/tool-argument-integers.ts +202 -0
  425. package/src/lib/translator-budget.ts +44 -0
  426. package/src/lib/upstream-http-version.ts +57 -0
  427. package/src/lib/upstream-reachability.ts +95 -0
  428. package/src/lib/upstream-retry.ts +156 -3
  429. package/src/lib/windows-atomic-replace.ts +155 -0
  430. package/src/lib/windows-elevation.ts +70 -2
  431. package/src/lib/windows-secret-acl.ts +409 -69
  432. package/src/lib/windows-service-wrappers.ts +72 -0
  433. package/src/lib/windows-text.ts +106 -0
  434. package/src/lib/windows-user-principal.ts +341 -0
  435. package/src/lib/winsw.ts +33 -5
  436. package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
  437. package/src/oauth/account-import/index.ts +15 -0
  438. package/src/oauth/account-import/parser.ts +83 -0
  439. package/src/oauth/account-import/registry.ts +18 -0
  440. package/src/oauth/account-import/service.ts +75 -0
  441. package/src/oauth/account-import/types.ts +91 -0
  442. package/src/oauth/anthropic.ts +12 -1
  443. package/src/oauth/callback-server.ts +8 -2
  444. package/src/oauth/chatgpt.ts +12 -1
  445. package/src/oauth/command-code.ts +239 -0
  446. package/src/oauth/cursor.ts +46 -5
  447. package/src/oauth/google-antigravity.ts +35 -3
  448. package/src/oauth/health.ts +20 -12
  449. package/src/oauth/index.ts +398 -66
  450. package/src/oauth/key-providers.ts +16 -0
  451. package/src/oauth/kimi.ts +16 -2
  452. package/src/oauth/kiro.ts +50 -6
  453. package/src/oauth/local-token-detect.ts +11 -2
  454. package/src/oauth/log.ts +3 -1
  455. package/src/oauth/login-cli.ts +88 -28
  456. package/src/oauth/nous.ts +798 -0
  457. package/src/oauth/store.ts +119 -21
  458. package/src/oauth/token-guardian.ts +9 -3
  459. package/src/pi/models.ts +2 -2
  460. package/src/providers/alibaba-region-migration.ts +1 -1
  461. package/src/providers/antigravity-models.ts +521 -31
  462. package/src/providers/base-url-choices.ts +10 -0
  463. package/src/providers/codex-capacity.ts +292 -0
  464. package/src/providers/command-code-efforts.ts +144 -0
  465. package/src/providers/context-cap.ts +22 -5
  466. package/src/providers/cursor-pool.ts +72 -0
  467. package/src/providers/derive.ts +253 -6
  468. package/src/providers/fastwire.ts +501 -0
  469. package/src/providers/free-directory.ts +10 -7
  470. package/src/providers/google-vertex-location.ts +14 -0
  471. package/src/providers/key-failover.ts +71 -3
  472. package/src/providers/label.ts +1 -1
  473. package/src/providers/model-discovery-limits.ts +16 -0
  474. package/src/providers/model-discovery.ts +115 -22
  475. package/src/providers/model-rename-migration.ts +255 -0
  476. package/src/providers/model-rename-startup.ts +28 -0
  477. package/src/providers/openai-sidecar.ts +72 -4
  478. package/src/providers/openai-tier-startup.ts +31 -2
  479. package/src/providers/openai-tiers.ts +119 -4
  480. package/src/providers/openai-virtual-models.ts +1 -0
  481. package/src/providers/opencode-zen-rate-limit.ts +102 -0
  482. package/src/providers/provider-id-rewrite.ts +29 -0
  483. package/src/providers/quota.ts +1319 -38
  484. package/src/providers/registry.ts +1429 -111
  485. package/src/providers/request-pacing.ts +310 -0
  486. package/src/providers/service-tier.ts +277 -0
  487. package/src/providers/slug-codec.ts +42 -6
  488. package/src/providers/static-model-discovery.ts +86 -0
  489. package/src/providers/xai-responses-opt-in.ts +15 -0
  490. package/src/providers/xai-transport.ts +11 -4
  491. package/src/reasoning-effort.ts +49 -1
  492. package/src/responses/compaction.ts +26 -1
  493. package/src/responses/custom-tool-compat.ts +266 -0
  494. package/src/responses/hosted-tool-policy.ts +9 -0
  495. package/src/responses/namespace-tool-compat.ts +355 -0
  496. package/src/responses/parser.ts +220 -38
  497. package/src/responses/provider-continuation.ts +98 -0
  498. package/src/responses/provider-opaque-metadata.ts +73 -0
  499. package/src/responses/reasoning-envelope.ts +9 -1
  500. package/src/responses/reasoning-replay-cache.ts +426 -0
  501. package/src/responses/schema.ts +7 -1
  502. package/src/responses/spill-store.ts +75 -10
  503. package/src/responses/state.ts +565 -27
  504. package/src/responses/thought-signature-replay.ts +347 -0
  505. package/src/responses/tool-search-compat.ts +301 -0
  506. package/src/responses/truncated-stop-reason.ts +60 -0
  507. package/src/router.ts +366 -30
  508. package/src/routing/analytics.ts +378 -0
  509. package/src/routing/capability.ts +244 -0
  510. package/src/routing/compatibility/assemble.ts +73 -0
  511. package/src/routing/compatibility/behavior.ts +278 -0
  512. package/src/routing/compatibility/catalog.ts +99 -0
  513. package/src/routing/compatibility/endpoint.ts +52 -0
  514. package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
  515. package/src/routing/compatibility/policy.ts +181 -0
  516. package/src/routing/compatibility/provider-slot.ts +56 -0
  517. package/src/routing/compatibility/reader.ts +110 -0
  518. package/src/routing/compatibility/subject.ts +191 -0
  519. package/src/routing/compatibility/types.ts +64 -0
  520. package/src/routing/compatibility/version.ts +104 -0
  521. package/src/routing/cost.ts +77 -0
  522. package/src/routing/evaluator.ts +495 -0
  523. package/src/routing/health.ts +412 -0
  524. package/src/routing/history/cursor.ts +43 -0
  525. package/src/routing/history/indexer.ts +605 -0
  526. package/src/routing/history/schema.ts +72 -0
  527. package/src/routing/profile-namespace.ts +15 -0
  528. package/src/routing/profile.ts +547 -0
  529. package/src/routing/quota.ts +145 -0
  530. package/src/routing/request-evidence.ts +45 -0
  531. package/src/routing/trace.ts +776 -0
  532. package/src/server/adapter-resolve.ts +2 -29
  533. package/src/server/auth-cors.ts +267 -46
  534. package/src/server/background-lifecycle.ts +182 -0
  535. package/src/server/chat-completions.ts +130 -56
  536. package/src/server/chat-native-sse.ts +331 -0
  537. package/src/server/chat-native.ts +426 -0
  538. package/src/server/claude-messages.ts +159 -43
  539. package/src/server/direct-local-http.ts +347 -0
  540. package/src/server/effort-policy.ts +18 -0
  541. package/src/server/github-copilot-responses-repair.ts +338 -0
  542. package/src/server/gui-static.ts +39 -10
  543. package/src/server/images.ts +94 -12
  544. package/src/server/index.ts +865 -181
  545. package/src/server/lifecycle.ts +284 -13
  546. package/src/server/live.ts +136 -17
  547. package/src/server/local-management-read-client.ts +90 -0
  548. package/src/server/local-provider-reload-client.ts +137 -0
  549. package/src/server/management/agent-settings-routes.ts +398 -103
  550. package/src/server/management/api-key-usage.ts +31 -5
  551. package/src/server/management/body.ts +6 -0
  552. package/src/server/management/combo-routes.ts +62 -24
  553. package/src/server/management/config-routes.ts +464 -51
  554. package/src/server/management/context.ts +80 -2
  555. package/src/server/management/integration-routes.ts +498 -0
  556. package/src/server/management/lab-automation-routes.ts +206 -0
  557. package/src/server/management/lab-routes.ts +563 -0
  558. package/src/server/management/logs-usage-routes.ts +101 -32
  559. package/src/server/management/model-routes.ts +189 -131
  560. package/src/server/management/model-rows.ts +163 -0
  561. package/src/server/management/native-integration-routes.ts +769 -0
  562. package/src/server/management/oauth-account-routes.ts +80 -4
  563. package/src/server/management/provider-capability-config.ts +48 -0
  564. package/src/server/management/provider-routes.ts +764 -157
  565. package/src/server/management/request-history-routes.ts +191 -0
  566. package/src/server/management/routing-analytics-routes.ts +74 -0
  567. package/src/server/management/routing-profile-routes.ts +380 -0
  568. package/src/server/management/shared.ts +27 -11
  569. package/src/server/management/sidebar-routes.ts +47 -31
  570. package/src/server/management/storage-log-guard-routes.ts +186 -0
  571. package/src/server/management/sync-response.ts +69 -0
  572. package/src/server/management/system-restart.ts +288 -32
  573. package/src/server/management/system-routes.ts +77 -0
  574. package/src/server/management/usage-summary-cache.ts +9 -1
  575. package/src/server/management/vision-sidecar-options.ts +167 -0
  576. package/src/server/management/web-search-sidecar-options.ts +120 -0
  577. package/src/server/management-api.ts +115 -14
  578. package/src/server/management-auth.ts +220 -5
  579. package/src/server/passive-route-linker.ts +66 -0
  580. package/src/server/ports.ts +41 -1
  581. package/src/server/proxy-liveness.ts +132 -5
  582. package/src/server/readiness.ts +99 -0
  583. package/src/server/relay-eager.ts +82 -42
  584. package/src/server/relay.ts +236 -76
  585. package/src/server/request-decompress.ts +113 -6
  586. package/src/server/request-log.ts +235 -22
  587. package/src/server/responses/agent-task-recovery-cache.ts +143 -0
  588. package/src/server/responses/agent-task-recovery.ts +465 -0
  589. package/src/server/responses/collaboration.ts +204 -35
  590. package/src/server/responses/compact.ts +442 -55
  591. package/src/server/responses/core.ts +2872 -331
  592. package/src/server/responses/empty-completion-guard.ts +276 -0
  593. package/src/server/responses/encrypted-payload.ts +62 -39
  594. package/src/server/responses/fetch-helpers.ts +79 -4
  595. package/src/server/responses/input-admission.ts +185 -0
  596. package/src/server/responses/pacing-overload.ts +13 -0
  597. package/src/server/responses/policy-fallback.ts +178 -0
  598. package/src/server/responses/responses-field-backfill.ts +251 -0
  599. package/src/server/responses/terminal-guard.ts +26 -5
  600. package/src/server/responses/upstream-error.ts +5 -0
  601. package/src/server/responses/ws-upstream.ts +308 -0
  602. package/src/server/responses-custom-tool-repair.ts +282 -0
  603. package/src/server/responses-item-id-repair.ts +54 -6
  604. package/src/server/responses-json-events.ts +90 -0
  605. package/src/server/responses-model-rewrite.ts +29 -0
  606. package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
  607. package/src/server/responses-snapshot-repair.ts +621 -0
  608. package/src/server/responses-terminal-repair.ts +342 -0
  609. package/src/server/responses-tool-search-repair.ts +267 -0
  610. package/src/server/responses-undeclared-tool-guard.ts +153 -0
  611. package/src/server/responses.ts +18 -2
  612. package/src/server/search.ts +78 -13
  613. package/src/server/sse-frame-buffer.ts +292 -0
  614. package/src/server/sse-payload-rewrite.ts +110 -22
  615. package/src/server/startup-action-control.ts +8 -1
  616. package/src/server/startup-health-cache.ts +19 -1
  617. package/src/server/system-env.ts +80 -9
  618. package/src/server/ws-bridge.ts +39 -38
  619. package/src/service-manager-probe.ts +892 -0
  620. package/src/service.ts +1111 -90
  621. package/src/sidecar/auth.ts +92 -0
  622. package/src/sidecar/candidates.ts +83 -0
  623. package/src/storage/cleanup.ts +2 -2
  624. package/src/storage/scanner.ts +1 -1
  625. package/src/storage/worker-lifecycle.ts +14 -14
  626. package/src/tray/windows-tray.ps1 +83 -9
  627. package/src/tray/windows.ts +43 -16
  628. package/src/types/accounts.ts +37 -0
  629. package/src/types/config.ts +845 -0
  630. package/src/types/provider.ts +545 -0
  631. package/src/types/request.ts +384 -0
  632. package/src/types/tools.ts +131 -0
  633. package/src/types/wire.ts +80 -0
  634. package/src/types.ts +104 -1236
  635. package/src/update/index.ts +32 -19
  636. package/src/update/job.ts +442 -67
  637. package/src/update/notify.ts +12 -6
  638. package/src/update/npm-cache-preflight.d.mts +47 -0
  639. package/src/update/npm-cache-preflight.mjs +201 -0
  640. package/src/update/transactional-install.d.mts +22 -0
  641. package/src/update/transactional-install.mjs +259 -0
  642. package/src/usage/cost.ts +0 -0
  643. package/src/usage/expected-prices.ts +268 -16
  644. package/src/usage/log.ts +606 -41
  645. package/src/usage/summary.ts +177 -9
  646. package/src/usage/user-cost-overlay-reconciler.ts +313 -0
  647. package/src/usage/user-cost-overlays.ts +314 -0
  648. package/src/vision/anthropic-describe.ts +10 -6
  649. package/src/vision/backends.ts +97 -0
  650. package/src/vision/describe.ts +9 -3
  651. package/src/vision/eligibility.ts +250 -0
  652. package/src/vision/index.ts +238 -24
  653. package/src/vision/reasoning.ts +55 -0
  654. package/src/vision/routed-describe.ts +175 -0
  655. package/src/vision/timeout-bounds.ts +9 -0
  656. package/src/web-search/anthropic-executor.ts +13 -7
  657. package/src/web-search/backends.ts +108 -0
  658. package/src/web-search/exa-executor.ts +88 -0
  659. package/src/web-search/executor.ts +11 -3
  660. package/src/web-search/gemini-executor.ts +141 -0
  661. package/src/web-search/index.ts +150 -15
  662. package/src/web-search/loop.ts +279 -50
  663. package/src/web-search/parse.ts +125 -30
  664. package/src/web-search/sources.ts +60 -0
  665. package/src/web-search/xai-executor.ts +219 -0
  666. package/gui/dist/assets/index-DTpMHS4F.js +0 -67
  667. package/gui/dist/assets/index-ZNVDE3C7.css +0 -1
  668. package/gui/dist/provider-icons/antigravity.svg +0 -1
  669. package/gui/dist/provider-icons/claude.svg +0 -1
  670. package/gui/dist/provider-icons/copilot.svg +0 -1
  671. package/gui/dist/provider-icons/cursor.svg +0 -2
  672. package/gui/dist/provider-icons/gemini.svg +0 -1
  673. package/gui/dist/provider-icons/grok-color.svg +0 -1
  674. package/gui/dist/provider-icons/kiro.svg +0 -14
  675. package/src/cli/internal-dispatch.ts +0 -20
@@ -1,14 +1,28 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import type { IncomingMeta, ProviderAdapter } from "./base";
3
- import { namespacedToolName, type AdapterEvent, type OcxParsedRequest, type OcxProviderConfig, type OcxUsage } from "../types";
3
+ import { namespacedToolName, type AdapterEvent, type OcxParsedRequest, type OcxProviderConfig, type OcxUsage, type TierDecision } from "../types";
4
4
  import { catalogModelSupportsReasoningSummaries } from "../codex/catalog";
5
- import { COMPACT_PROMPT, decodeCompactionSummary, SUMMARY_PREFIX } from "../responses/compaction";
5
+ import { COMPACT_PROMPT, compactionItemToText, decodeCompactionSummary, isCompactionItemType } from "../responses/compaction";
6
6
  import { collectResponsesToolGroups } from "../responses/tool-groups";
7
+ import { isHostedToolUnsupportedForModel } from "../responses/hosted-tool-policy";
7
8
  import { decodeServerSentEvents } from "../lib/sse-decoder";
8
- import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers";
9
+ import {
10
+ CODEX_FORWARD_BASE_URL,
11
+ destinationDecodesNativeCompactionBlob,
12
+ isCanonicalOpenAiForwardProvider,
13
+ isOpenAiOperatedResponsesDestination,
14
+ } from "../providers/openai-tiers";
9
15
  import { OCX_REASONING_PREFIX } from "../responses/reasoning-envelope";
10
16
  import { modelRecordValue } from "../reasoning-effort";
11
17
  import type { TranslatorBudget } from "../lib/translator-budget";
18
+ import { rewriteRoutedCustomToolsForUpstream } from "../responses/custom-tool-compat";
19
+ import { rewriteRoutedToolSearchForUpstream } from "../responses/tool-search-compat";
20
+ import { rewriteRoutedNamespaceToolsForUpstream } from "../responses/namespace-tool-compat";
21
+ import { openaiResponsesUrl } from "./openai-responses-url";
22
+ import { normalizeXaiResponsesWebSearch } from "./xai-web-search";
23
+ import {
24
+ createAdapterTierMetadata,
25
+ } from "../providers/fastwire";
12
26
 
13
27
  // Headers relayed verbatim from the caller in OAuth-passthrough ("forward") mode.
14
28
  // Exported so the web-search sidecar reuses the exact same forwarded-auth set for its ChatGPT call.
@@ -32,7 +46,14 @@ export const FORWARD_HEADERS = [
32
46
  "x-responsesapi-include-timing-metrics",
33
47
  ];
34
48
 
35
- export function sanitizeReasoningInputContent(body: unknown): unknown {
49
+ export function sanitizeReasoningInputContent(
50
+ body: unknown,
51
+ opts?: {
52
+ preserveRawReasoningContent?: boolean;
53
+ dropNullContentChannel?: boolean;
54
+ stripEncryptedContent?: boolean;
55
+ },
56
+ ): unknown {
36
57
  if (!body || typeof body !== "object" || Array.isArray(body)) return body;
37
58
  const raw = body as Record<string, unknown>;
38
59
  if (!Array.isArray(raw.input)) return body;
@@ -46,13 +67,48 @@ export function sanitizeReasoningInputContent(body: unknown): unknown {
46
67
  // ocxr1 envelopes are proxy-minted (Anthropic signatures), not OpenAI encryption — the native
47
68
  // backend cannot decrypt them and would reject the request. Strip regardless of content shape.
48
69
  const hasOcxEnvelope = typeof rec.encrypted_content === "string" && rec.encrypted_content.startsWith(OCX_REASONING_PREFIX);
49
- if (!hasRawContent && !hasOcxEnvelope) return item;
70
+ const hasOutputStatus = Object.prototype.hasOwnProperty.call(rec, "status");
71
+ const hasEncryptedContent = Object.prototype.hasOwnProperty.call(rec, "encrypted_content");
72
+ const stripEncryptedContent = hasOcxEnvelope
73
+ || (opts?.stripEncryptedContent === true && hasEncryptedContent);
74
+ // Codex serializes an absent reasoning content channel as `"content": null`. The field is
75
+ // optional and null carries nothing, but a strict gateway rejects the item on its declared type
76
+ // — xAI answers `Could not decode the compaction blob`, naming the sibling `encrypted_content`
77
+ // rather than the field it actually refused, which is why this reads as a blob failure. Drop the
78
+ // key so the item matches the shape the upstream issued.
79
+ //
80
+ // Gated to routed destinations. An OpenAI-operated backend binds the blob to the item's exact
81
+ // shape, so deleting a field there invalidates it (`The encrypted content ... could not be
82
+ // verified`); the two requirements are exactly opposed, and a live regression proved it. That
83
+ // gate is also why this drop may touch an item that keeps its blob: xAI demonstrably accepts its
84
+ // own blob without the null channel, and the destinations that bind blobs to item shape never
85
+ // reach this branch. This is independent of the output-only status removal below.
86
+ const dropNullContentChannel = opts?.dropNullContentChannel === true
87
+ && "content" in rec && !Array.isArray(rec.content);
88
+ // `status` is output-only. Measured OpenAI reasoning items never contain it, and Grok accepts
89
+ // its own encrypted_content with status removed. Keeping a foreign status beside a retained
90
+ // blob makes OpenAI reject the field before blob validation, starving the provenance recovery
91
+ // of the opaque-blob error it needs. Content blanking remains the separate pre-existing rule.
92
+ const stripOutputStatus = hasOutputStatus;
93
+ const blankContent = !dropNullContentChannel
94
+ && !opts?.preserveRawReasoningContent
95
+ && (hasRawContent || hasOcxEnvelope);
96
+ if (!blankContent && !stripOutputStatus && !stripEncryptedContent && !dropNullContentChannel) {
97
+ return item;
98
+ }
50
99
  changed = true;
100
+ const next: Record<string, unknown> = { ...rec };
101
+ if (dropNullContentChannel) delete next.content;
102
+ if (stripOutputStatus) delete next.status;
103
+ if (stripEncryptedContent) delete next.encrypted_content;
51
104
  // Routed models can produce raw `reasoning_text` output items. Codex echoes those in later
52
105
  // native GPT requests, but ChatGPT's Responses backend accepts reasoning input only with empty
53
106
  // `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;
107
+ // DeepSeek's Responses API instead ACCEPTS plaintext reasoning replay (its compatibility
108
+ // guide merges reasoning items into the adjacent assistant message), so providers flagged
109
+ // `preserveResponsesReasoningContent` keep it — deleting valid replay content there breaks
110
+ // continuations after tool calls (issue #875 family).
111
+ if (blankContent) next.content = [];
56
112
  return next;
57
113
  });
58
114
 
@@ -100,6 +156,71 @@ function stripInvalidItemIds(body: unknown): unknown {
100
156
  return changed ? { ...body, input } : body;
101
157
  }
102
158
 
159
+ /**
160
+ * Codex-private tool fields that only the ChatGPT backend understands.
161
+ *
162
+ * A third-party Responses gateway validates its schema and rejects the whole request before
163
+ * inference — xAI answers `Argument not supported: external_web_access` — so these are removed at
164
+ * the noncanonical boundary while the tool and every public option stay.
165
+ *
166
+ * Keep this a table. Each private bit Codex attaches has so far arrived as its own bespoke strip
167
+ * with its own traversal, and the traversals disagreed about which containers they covered; a new
168
+ * one should be a row here instead. `toolTypes` omitted means the field is private on any tool.
169
+ */
170
+ const CANONICAL_ONLY_TOOL_FIELDS: readonly { field: string; toolTypes?: ReadonlySet<string>; capabilityGated?: boolean }[] = [
171
+ // ChatGPT's browsing policy bit. The public hosted tool is enabled by its presence alone.
172
+ // OWNERSHIP: official OpenAI API-key traffic and unclassified gateways ACCEPT this field, so
173
+ // it is only stripped when the provider capability denies it (supportsOpenAiWebSearchToolFields
174
+ // === false), matching stripOpenAiOnlyWebSearchFields; see
175
+ // tests/responses-routed-web-search-fields.test.ts.
176
+ { field: "external_web_access", toolTypes: new Set(["web_search", "web_search_preview"]), capabilityGated: true },
177
+ // Deferred-discovery marker. `activateDeferredTool` clears it only for tools a `tool_search_output`
178
+ // already loaded, so a still-deferred declaration — including one promoted out of a namespace
179
+ // group — otherwise reaches the wire carrying it.
180
+ { field: "defer_loading" },
181
+ ];
182
+
183
+ function stripCanonicalOnlyToolFields(body: unknown, includeCapabilityGated: boolean): unknown {
184
+ if (!isPlainObject(body)) return body;
185
+
186
+ const rewriteTools = (tools: unknown[]): unknown[] => {
187
+ let changed = false;
188
+ const rewritten = tools.map(tool => {
189
+ if (!isPlainObject(tool)) return tool;
190
+ let next = tool;
191
+ for (const { field, toolTypes, capabilityGated } of CANONICAL_ONLY_TOOL_FIELDS) {
192
+ if (capabilityGated && !includeCapabilityGated) continue;
193
+ if (!Object.hasOwn(next, field)) continue;
194
+ if (toolTypes && (typeof next.type !== "string" || !toolTypes.has(next.type))) continue;
195
+ const { [field]: _private, ...rest } = next;
196
+ next = rest;
197
+ }
198
+ if (next === tool) return tool;
199
+ changed = true;
200
+ return next;
201
+ });
202
+ return changed ? rewritten : tools;
203
+ };
204
+
205
+ let rewrittenBody = body;
206
+ if (Array.isArray(body.tools)) {
207
+ const tools = rewriteTools(body.tools);
208
+ if (tools !== body.tools) rewrittenBody = { ...rewrittenBody, tools };
209
+ }
210
+ if (!Array.isArray(body.input)) return rewrittenBody;
211
+
212
+ let input: unknown[] | undefined;
213
+ for (let index = 0; index < body.input.length; index += 1) {
214
+ const item = body.input[index];
215
+ if (!isPlainObject(item) || item.type !== "additional_tools" || !Array.isArray(item.tools)) continue;
216
+ const tools = rewriteTools(item.tools);
217
+ if (tools === item.tools) continue;
218
+ input ??= [...body.input];
219
+ input[index] = { ...item, tools };
220
+ }
221
+ return input ? { ...rewrittenBody, input } : rewrittenBody;
222
+ }
223
+
103
224
  /**
104
225
  * When `store` is false, the upstream API does not persist response items. Any item ID
105
226
  * forwarded in `input` is then interpreted as a reference to a stored item that does not
@@ -123,43 +244,47 @@ function stripItemIdsWhenUnstored(body: unknown): unknown {
123
244
  }
124
245
 
125
246
  /**
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.
247
+ * Normalize replayed compaction items for the destination backend.
248
+ *
249
+ * A compaction item carries an `encrypted_content` blob the client replays verbatim on every later
250
+ * turn, and only the backend that minted it can decode it. Proxy-minted `ocx1:` envelopes are
251
+ * transparent base64 rather than encryption, so no upstream can read them and they always become
252
+ * plain user messages. Native blobs have multiple possible minters, so a destination's ability to
253
+ * decode its own blobs does not make a blob from a previous serving identity portable. On a known
254
+ * identity mismatch the blob degrades to the same note the bridged parser uses, even when the
255
+ * destination normally accepts native blobs. Without a known mismatch, the destination capability
256
+ * keeps the existing behavior.
257
+ *
258
+ * A bare `context_compaction` marker carries no blob and is forwarded untouched.
130
259
  */
131
- function scrubOcxCompactionItems(body: unknown): unknown {
260
+ function scrubOcxCompactionItems(
261
+ body: unknown,
262
+ destinationDecodesNativeBlob: boolean,
263
+ threadServingIdentityChanged: boolean,
264
+ ): unknown {
132
265
  if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
133
266
 
134
267
  let changed = false;
135
268
  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;
269
+ if (!isPlainObject(item) || !isCompactionItemType(item.type)) return item;
270
+ const encrypted = typeof item.encrypted_content === "string" ? item.encrypted_content : undefined;
271
+ if (encrypted === undefined) return item;
272
+ if (
273
+ decodeCompactionSummary(encrypted) === null
274
+ && destinationDecodesNativeBlob
275
+ && !threadServingIdentityChanged
276
+ ) return item;
140
277
  changed = true;
141
278
  return {
142
279
  type: "message",
143
280
  role: "user",
144
- content: [{ type: "input_text", text: `${SUMMARY_PREFIX}\n\n${decoded}` }],
281
+ content: [{ type: "input_text", text: compactionItemToText(encrypted) }],
145
282
  };
146
283
  });
147
284
 
148
285
  return changed ? { ...body, input } : body;
149
286
  }
150
287
 
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
288
  /**
164
289
  * Strip unsupported `reasoning` sub-parameters for native slugs that reject them (e.g. Spark).
165
290
  * codex-rs injects `reasoning.context` and `reasoning.summary` based on catalog flags; Spark's
@@ -178,6 +303,28 @@ function stripUnsupportedReasoningParams(body: unknown): unknown {
178
303
  return { ...body, reasoning: Object.keys(rest).length > 0 ? rest : undefined };
179
304
  }
180
305
 
306
+ /**
307
+ * GPT-5.6 replaced the legacy 24-hour retention field with `prompt_cache_options.ttl`, and the
308
+ * ChatGPT backend 400s the whole request when the retired field is present (issue #2092).
309
+ *
310
+ * The retired field is NOT translated to the replacement: 5.6 carries a different TTL contract,
311
+ * and implicit caching still applies when the caller sent no replacement options. Inventing a
312
+ * value here would silently change a caching decision the caller never made.
313
+ *
314
+ * Deliberately narrow on both axes, because a wider strip is a behavior change rather than a fix:
315
+ * only the gpt-5.6 family (an older model may still honor the field), and only on the canonical
316
+ * ChatGPT backend, which is the deployment that rejects it. Matching is exact-or-dashed-prefix so
317
+ * a future `gpt-5.60` is not swept up by a bare `startsWith`.
318
+ */
319
+ function stripDeprecatedPromptCacheRetention(body: unknown, modelId: unknown): unknown {
320
+ if (!isPlainObject(body)) return body;
321
+ if (typeof modelId !== "string") return body;
322
+ if (modelId !== "gpt-5.6" && !modelId.startsWith("gpt-5.6-")) return body;
323
+ if (!Object.hasOwn(body, "prompt_cache_retention")) return body;
324
+ const { prompt_cache_retention: _retention, ...rest } = body;
325
+ return rest;
326
+ }
327
+
181
328
  /**
182
329
  * A false model capability prevents Codex from emitting summary fields after the catalog refresh.
183
330
  * Strip them here as well so an already-running client with a stale catalog cannot keep sending an
@@ -396,6 +543,112 @@ function normalizeToolSchemas(body: unknown): unknown {
396
543
  return normalizedBody;
397
544
  }
398
545
 
546
+ function activateDeferredTool(tool: Record<string, unknown>): Record<string, unknown> {
547
+ const { defer_loading: _, ...activeTool } = tool;
548
+ if (tool.type !== "namespace" || !Array.isArray(tool.tools)) return activeTool;
549
+ return {
550
+ ...activeTool,
551
+ tools: tool.tools.map(inner => isPlainObject(inner) ? activateDeferredTool(inner) : inner),
552
+ };
553
+ }
554
+
555
+ function mergeLoadedTools(declaredTools: unknown[], loadedTools: unknown[]): unknown[] {
556
+ const merged = [...declaredTools];
557
+ let changed = false;
558
+
559
+ for (const candidate of loadedTools) {
560
+ if (!isPlainObject(candidate) || typeof candidate.name !== "string") continue;
561
+ const loaded = activateDeferredTool(candidate);
562
+ if (loaded.type === "namespace" && Array.isArray(loaded.tools)) {
563
+ const namespaceIndex = merged.findIndex(tool =>
564
+ isPlainObject(tool) && tool.type === "namespace" && tool.name === loaded.name
565
+ );
566
+ if (namespaceIndex < 0) {
567
+ merged.push(loaded);
568
+ changed = true;
569
+ continue;
570
+ }
571
+
572
+ const namespace = merged[namespaceIndex];
573
+ if (!isPlainObject(namespace)) continue;
574
+ const namespaceTools = Array.isArray(namespace.tools) ? namespace.tools : [];
575
+ const nextNamespaceTools = [...namespaceTools];
576
+ let namespaceChanged = "defer_loading" in namespace;
577
+ for (const tool of loaded.tools) {
578
+ if (!isPlainObject(tool) || typeof tool.name !== "string") continue;
579
+ const declaredIndex = nextNamespaceTools.findIndex(declared =>
580
+ isPlainObject(declared) && declared.name === tool.name
581
+ );
582
+ if (declaredIndex < 0) {
583
+ nextNamespaceTools.push(tool);
584
+ namespaceChanged = true;
585
+ continue;
586
+ }
587
+ const declared = nextNamespaceTools[declaredIndex];
588
+ if (isPlainObject(declared) && "defer_loading" in declared) {
589
+ nextNamespaceTools[declaredIndex] = activateDeferredTool(declared);
590
+ namespaceChanged = true;
591
+ }
592
+ }
593
+ if (!namespaceChanged) continue;
594
+ const { defer_loading: _, ...activeNamespace } = namespace;
595
+ merged[namespaceIndex] = { ...activeNamespace, tools: nextNamespaceTools };
596
+ changed = true;
597
+ continue;
598
+ }
599
+
600
+ const declaredIndex = merged.findIndex(tool =>
601
+ isPlainObject(tool) && tool.type !== "namespace" && tool.name === loaded.name
602
+ );
603
+ if (declaredIndex < 0) {
604
+ merged.push(loaded);
605
+ changed = true;
606
+ } else {
607
+ const declared = merged[declaredIndex];
608
+ if (isPlainObject(declared) && "defer_loading" in declared) {
609
+ merged[declaredIndex] = activateDeferredTool(declared);
610
+ changed = true;
611
+ }
612
+ }
613
+ }
614
+
615
+ return changed ? merged : declaredTools;
616
+ }
617
+
618
+ /**
619
+ * Client-executed tool search only changes Codex's parsed tool context. Routed passthrough keeps
620
+ * serializing the raw request, so activate those returned definitions for upstreams that do not
621
+ * implement the native deferred-loading handshake themselves.
622
+ */
623
+ function promoteClientLoadedTools(body: unknown): unknown {
624
+ if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
625
+
626
+ const loadedTools = body.input.flatMap(item =>
627
+ isPlainObject(item) && item.type === "tool_search_output" && Array.isArray(item.tools)
628
+ ? item.tools
629
+ : []
630
+ );
631
+ if (loadedTools.length === 0) return body;
632
+
633
+ if (Array.isArray(body.tools)) {
634
+ const tools = mergeLoadedTools(body.tools, loadedTools);
635
+ return tools === body.tools ? body : { ...body, tools };
636
+ }
637
+
638
+ const additionalToolsIndex = body.input.findIndex(item =>
639
+ isPlainObject(item) && item.type === "additional_tools" && Array.isArray(item.tools)
640
+ );
641
+ if (additionalToolsIndex < 0) return { ...body, tools: mergeLoadedTools([], loadedTools) };
642
+
643
+ const additionalTools = body.input[additionalToolsIndex];
644
+ if (!isPlainObject(additionalTools) || !Array.isArray(additionalTools.tools)) return body;
645
+ const tools = mergeLoadedTools(additionalTools.tools, loadedTools);
646
+ if (tools === additionalTools.tools) return body;
647
+ const input = [...body.input];
648
+ input[additionalToolsIndex] = { ...additionalTools, tools };
649
+ return { ...body, input };
650
+ }
651
+
399
652
  const MAX_RESPONSES_CALL_ID_LENGTH = 64;
400
653
  const REPAIRED_CALL_ID_PREFIX = "call_ocx_";
401
654
  const REPAIRED_CALL_ID_DIGEST_LENGTH = MAX_RESPONSES_CALL_ID_LENGTH - REPAIRED_CALL_ID_PREFIX.length;
@@ -462,6 +715,15 @@ function toolOutputText(output: unknown): string {
462
715
  * expansion misses (proxy restart, unrecorded prior turn), previous_response_id is stripped
463
716
  * (the ChatGPT backend rejects it), so the delta may carry items that reference now-absent
464
717
  * prior items and 400 upstream:
718
+ * - `function_call`/`local_shell_call`/`custom_tool_call` without their paired output item
719
+ * ("No tool output found for tool call <call_id>"). A stateless upstream cannot resolve
720
+ * the pair from its own storage, so a placeholder output is synthesized to keep the
721
+ * turn continuable without pretending the result was real. Synthetic outputs are
722
+ * emitted after the complete parallel call batch, in call order alongside any real
723
+ * outputs, so the adjacency normalizer can still recognize the batch as one
724
+ * reasoning-bearing assistant turn (#1477). Gated on
725
+ * `synthesizeMissingCallOutputs` (stateless AND non-forward wires); forward replay keeps
726
+ * fail-closed behavior.
465
727
  * - `function_call_output`/`custom_tool_call_output` without their paired call item
466
728
  * ("No tool call found for function call output with call_id ..."). Converted to user
467
729
  * messages so the result text survives. `function_call_output` also pairs with
@@ -471,26 +733,65 @@ function toolOutputText(output: unknown): string {
471
733
  * reasoning chain is intact and must be preserved.
472
734
  * Runs on every forward request; with intact pairs it returns the original reference.
473
735
  */
474
- function repairOrphanedInputItems(body: unknown, dropReasoning: boolean): unknown {
736
+ /**
737
+ * Backfill `queries` on a replayed single-query `web_search_call`.
738
+ *
739
+ * `webSearchAction()` in the bridge now emits both keys, but that only helps items
740
+ * created after the fix. A conversation that already recorded
741
+ * `{type:"search", query:"..."}` replays that stored item on every subsequent turn, and
742
+ * DeepSeek's native Responses parser requires `queries` — so upgrading alone leaves
743
+ * those threads permanently 400ing with `missing field 'queries'` (#930).
744
+ *
745
+ * Runs on every Responses request, on both `input` items and the `action` nested inside
746
+ * them. Returns the original reference when nothing needs repair, so the common path
747
+ * allocates nothing.
748
+ */
749
+ function backfillWebSearchQueries(body: unknown): unknown {
750
+ if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
751
+ let changed = false;
752
+ const input = body.input.map(item => {
753
+ if (!isPlainObject(item) || item.type !== "web_search_call") return item;
754
+ const action = item.action;
755
+ if (!isPlainObject(action) || action.type !== "search") return item;
756
+ if (typeof action.query !== "string" || Array.isArray(action.queries)) return item;
757
+ changed = true;
758
+ return { ...item, action: { ...action, queries: [action.query] } };
759
+ });
760
+ return changed ? { ...body, input } : body;
761
+ }
762
+
763
+ function repairOrphanedInputItems(body: unknown, dropReasoning: boolean, synthesizeMissingCallOutputs = false): unknown {
475
764
  if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
476
765
  const input = body.input;
477
766
 
478
767
  const functionCallIds = new Set<string>();
479
768
  const customCallIds = new Set<string>();
769
+ const functionOutputIds = new Set<string>();
770
+ const customOutputIds = new Set<string>();
480
771
  for (const item of input) {
481
772
  if (!isPlainObject(item) || typeof item.call_id !== "string") continue;
482
773
  if (item.type === "function_call" || item.type === "local_shell_call") functionCallIds.add(item.call_id);
483
774
  else if (item.type === "custom_tool_call") customCallIds.add(item.call_id);
775
+ else if (item.type === "function_call_output") functionOutputIds.add(item.call_id);
776
+ else if (item.type === "custom_tool_call_output") customOutputIds.add(item.call_id);
484
777
  }
485
778
 
486
779
  let changed = false;
487
780
  const repaired: unknown[] = [];
781
+ const syntheticKeys = new Set<string>();
782
+ const pendingSyntheticOutputs: unknown[] = [];
783
+ const flushPendingSyntheticOutputs = (): void => {
784
+ if (pendingSyntheticOutputs.length === 0) return;
785
+ repaired.push(...pendingSyntheticOutputs);
786
+ pendingSyntheticOutputs.length = 0;
787
+ };
488
788
  for (const item of input) {
489
- if (!isPlainObject(item)) { repaired.push(item); continue; }
789
+ if (!isPlainObject(item)) { flushPendingSyntheticOutputs(); repaired.push(item); continue; }
490
790
  if (dropReasoning && item.type === "reasoning") { changed = true; continue; }
491
791
  const isFnOutput = item.type === "function_call_output";
492
792
  const isCustomOutput = item.type === "custom_tool_call_output";
493
793
  if (isFnOutput || isCustomOutput) {
794
+ flushPendingSyntheticOutputs();
494
795
  const callId = typeof item.call_id === "string" ? item.call_id : "";
495
796
  const paired = isFnOutput ? functionCallIds.has(callId) : customCallIds.has(callId);
496
797
  if (!paired) {
@@ -503,10 +804,198 @@ function repairOrphanedInputItems(body: unknown, dropReasoning: boolean): unknow
503
804
  continue;
504
805
  }
505
806
  }
807
+ const isFnCall = item.type === "function_call" || item.type === "local_shell_call";
808
+ const isCustomCall = item.type === "custom_tool_call";
809
+ if (isFnCall || isCustomCall) {
810
+ repaired.push(item);
811
+ if (synthesizeMissingCallOutputs) {
812
+ const callId = typeof item.call_id === "string" ? item.call_id : "";
813
+ const hasOutput = isFnCall ? functionOutputIds.has(callId) : customOutputIds.has(callId);
814
+ if (!hasOutput && callId) {
815
+ changed = true;
816
+ const name = typeof item.name === "string" && item.name.length > 0 ? item.name : callId;
817
+ const text = `[ocx] no tool result was recorded for "${name}"; execution status unknown — do not treat this as success, failure, or user-provided input.`;
818
+ syntheticKeys.add(`${isFnCall ? "function" : "custom"}:${callId}`);
819
+ pendingSyntheticOutputs.push(isFnCall
820
+ ? { type: "function_call_output", call_id: callId, output: text }
821
+ : { type: "custom_tool_call_output", call_id: callId, output: text });
822
+ }
823
+ }
824
+ continue;
825
+ }
826
+ flushPendingSyntheticOutputs();
506
827
  repaired.push(item);
507
828
  }
829
+ flushPendingSyntheticOutputs();
508
830
 
509
- return changed ? { ...body, input: repaired } : body;
831
+ const callKeyOf = (item: unknown): string | null => {
832
+ if (!isPlainObject(item) || typeof item.call_id !== "string") return null;
833
+ if (item.type === "function_call" || item.type === "local_shell_call") return `function:${item.call_id}`;
834
+ if (item.type === "custom_tool_call") return `custom:${item.call_id}`;
835
+ return null;
836
+ };
837
+ const outputKeyOf = (item: unknown): string | null => {
838
+ if (!isPlainObject(item) || typeof item.call_id !== "string") return null;
839
+ if (item.type === "function_call_output") return `function:${item.call_id}`;
840
+ if (item.type === "custom_tool_call_output") return `custom:${item.call_id}`;
841
+ return null;
842
+ };
843
+ const reorderBatchOutputs = (items: unknown[]): unknown[] => {
844
+ const ordered: unknown[] = [];
845
+ const claimedOutputIndexes = new Set<number>();
846
+ const outputIndexesByKey = new Map<string, { indexes: number[]; offset: number }>();
847
+ for (let outputIndex = 0; outputIndex < items.length; outputIndex += 1) {
848
+ const outputKey = outputKeyOf(items[outputIndex]);
849
+ if (outputKey === null) continue;
850
+ const bucket = outputIndexesByKey.get(outputKey);
851
+ if (bucket) bucket.indexes.push(outputIndex);
852
+ else outputIndexesByKey.set(outputKey, { indexes: [outputIndex], offset: 0 });
853
+ }
854
+ let index = 0;
855
+ while (index < items.length) {
856
+ if (claimedOutputIndexes.has(index)) { index += 1; continue; }
857
+ const key = callKeyOf(items[index]);
858
+ if (key === null) { ordered.push(items[index]); index += 1; continue; }
859
+ const batch: unknown[] = [];
860
+ const batchKeys: string[] = [];
861
+ let cursor = index;
862
+ while (cursor < items.length) {
863
+ const nextKey = callKeyOf(items[cursor]);
864
+ if (nextKey === null) break;
865
+ batch.push(items[cursor]);
866
+ batchKeys.push(nextKey);
867
+ cursor += 1;
868
+ }
869
+ const hasSynthetic = batchKeys.some(batchKey => syntheticKeys.has(batchKey));
870
+ if (!hasSynthetic) {
871
+ ordered.push(...batch);
872
+ index = cursor;
873
+ continue;
874
+ }
875
+ const batchOutputs: unknown[] = [];
876
+ for (const batchKey of batchKeys) {
877
+ const bucket = outputIndexesByKey.get(batchKey);
878
+ if (!bucket) continue;
879
+ while (bucket.offset < bucket.indexes.length && bucket.indexes[bucket.offset]! < cursor) {
880
+ bucket.offset += 1;
881
+ }
882
+ while (bucket.offset < bucket.indexes.length) {
883
+ const outputIndex = bucket.indexes[bucket.offset]!;
884
+ bucket.offset += 1;
885
+ if (claimedOutputIndexes.has(outputIndex)) continue;
886
+ claimedOutputIndexes.add(outputIndex);
887
+ batchOutputs.push(items[outputIndex]);
888
+ break;
889
+ }
890
+ }
891
+ ordered.push(...batch, ...batchOutputs);
892
+ index = cursor;
893
+ }
894
+ return ordered;
895
+ };
896
+
897
+ return changed ? { ...body, input: reorderBatchOutputs(repaired) } : body;
898
+ }
899
+
900
+ /**
901
+ * Make unambiguous Responses tool batches contiguous for upstream parsers that require it.
902
+ *
903
+ * [Decision Log]
904
+ * - 목적과 의도: Keep Codex hook-injected developer context without splitting a parallel tool-call turn away from its reasoning or making a strict upstream reject matching results.
905
+ * - 기존 구현 및 제약 조건: The orphan repair verifies only pair presence, while the original pair-by-pair reorder turned `reasoning, call A, call B, output A, output B` into two assistant turns and made DeepSeek reject call B for missing reasoning (#1477).
906
+ * - 검토한 주요 대안: Disable parallel calls (DeepSeek always enables them); duplicate reasoning per call; reorder each pair; or normalize the complete unambiguous call batch.
907
+ * - 선택한 방식: Treat calls emitted before the first matched result as one batch, emit all calls followed by their matched outputs, and preserve intervening non-tool items immediately after the batch.
908
+ * - 다른 대안 대신 이 방식을 선택한 이유: Batch normalization matches the Responses parallel-call shape without fabricating reasoning, while the provider gate and unique-pair requirement keep the blast radius narrow.
909
+ * - 장점, 단점 및 영향: DeepSeek keeps one reasoning-bearing assistant turn for parallel calls and still accepts hook-interleaved single calls; tolerant providers stay byte/order equivalent, and duplicate, missing, or backwards call/result pairs are not guessed.
910
+ */
911
+ function normalizeResponsesToolResultAdjacency(body: unknown): unknown {
912
+ if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
913
+ const input = body.input;
914
+ const calls = new Map<string, number[]>();
915
+ const outputs = new Map<string, number[]>();
916
+
917
+ const appendIndex = (map: Map<string, number[]>, key: string, index: number): void => {
918
+ const existing = map.get(key);
919
+ if (existing) existing.push(index);
920
+ else map.set(key, [index]);
921
+ };
922
+
923
+ for (let index = 0; index < input.length; index += 1) {
924
+ const item = input[index];
925
+ if (!isPlainObject(item) || typeof item.call_id !== "string" || item.call_id.length === 0) continue;
926
+ if (item.type === "function_call" || item.type === "local_shell_call") {
927
+ appendIndex(calls, `function:${item.call_id}`, index);
928
+ } else if (item.type === "custom_tool_call") {
929
+ appendIndex(calls, `custom:${item.call_id}`, index);
930
+ } else if (item.type === "function_call_output") {
931
+ appendIndex(outputs, `function:${item.call_id}`, index);
932
+ } else if (item.type === "custom_tool_call_output") {
933
+ appendIndex(outputs, `custom:${item.call_id}`, index);
934
+ }
935
+ }
936
+
937
+ const pairs: Array<{ callIndex: number; outputIndex: number }> = [];
938
+ for (const [key, callIndices] of calls) {
939
+ const outputIndices = outputs.get(key);
940
+ if (!outputIndices) return body;
941
+ if (callIndices.length !== 1 || outputIndices.length !== 1) return body;
942
+ const callIndex = callIndices[0]!;
943
+ const outputIndex = outputIndices[0]!;
944
+ if (outputIndex <= callIndex) return body;
945
+ pairs.push({ callIndex, outputIndex });
946
+ }
947
+ // Reject any collected output that lacks exactly one matching call. A lone or
948
+ // duplicated output is ambiguous, and normalizing on top of it could sever a
949
+ // result from the reasoning-bearing call turn it belongs to.
950
+ for (const [key, outputIndices] of outputs) {
951
+ const callIndices = calls.get(key);
952
+ if (!callIndices || callIndices.length !== 1 || outputIndices.length !== 1) return body;
953
+ }
954
+ pairs.sort((left, right) => left.callIndex - right.callIndex);
955
+
956
+ const movedIndices = new Set<number>();
957
+ const batchAt = new Map<number, unknown[]>();
958
+ for (let cursor = 0; cursor < pairs.length;) {
959
+ const group = [pairs[cursor]!];
960
+ let firstOutputIndex = pairs[cursor]!.outputIndex;
961
+ let next = cursor + 1;
962
+ while (next < pairs.length && pairs[next]!.callIndex < firstOutputIndex) {
963
+ group.push(pairs[next]!);
964
+ firstOutputIndex = Math.min(firstOutputIndex, pairs[next]!.outputIndex);
965
+ next += 1;
966
+ }
967
+
968
+ // Within one reasoning turn the outputs must appear in the same order as their
969
+ // calls. If they are reversed, normalizing would fabricate a new output order;
970
+ // leave the ambiguous history untouched instead.
971
+ for (let groupIndex = 1; groupIndex < group.length; groupIndex += 1) {
972
+ if (group[groupIndex]!.outputIndex < group[groupIndex - 1]!.outputIndex) return body;
973
+ }
974
+
975
+ const batch = [
976
+ ...group.map(pair => input[pair.callIndex]),
977
+ ...group.map(pair => input[pair.outputIndex]),
978
+ ];
979
+ const anchor = group[0]!.callIndex;
980
+ const alreadyContiguous = batch.every((item, offset) => input[anchor + offset] === item);
981
+ if (!alreadyContiguous) {
982
+ batchAt.set(anchor, batch);
983
+ for (const pair of group) {
984
+ movedIndices.add(pair.callIndex);
985
+ movedIndices.add(pair.outputIndex);
986
+ }
987
+ }
988
+ cursor = next;
989
+ }
990
+ if (batchAt.size === 0) return body;
991
+
992
+ const normalized: unknown[] = [];
993
+ for (let index = 0; index < input.length; index += 1) {
994
+ const batch = batchAt.get(index);
995
+ if (batch) normalized.push(...batch);
996
+ if (!movedIndices.has(index)) normalized.push(input[index]);
997
+ }
998
+ return { ...body, input: normalized };
510
999
  }
511
1000
 
512
1001
  /**
@@ -526,6 +1015,15 @@ function stripPreviousResponseId(body: unknown, strip: boolean): unknown {
526
1015
  return rest;
527
1016
  }
528
1017
 
1018
+ /** Apply the settled tier only to a fresh outbound object; `_rawBody` remains caller-owned. */
1019
+ function applyTierDecisionToResponsesBody(body: unknown, decision: TierDecision | undefined): unknown {
1020
+ if (!decision || decision.kind === "forward-caller" || !isPlainObject(body)) return body;
1021
+ const next: Record<string, unknown> = { ...body };
1022
+ if (decision.kind === "set") next.service_tier = decision.value;
1023
+ else delete next.service_tier;
1024
+ return next;
1025
+ }
1026
+
529
1027
  /**
530
1028
  * Drop request parameters a stateless Responses upstream cannot implement, and pin
531
1029
  * `store` false.
@@ -540,8 +1038,8 @@ function stripPreviousResponseId(body: unknown, strip: boolean): unknown {
540
1038
  * `prompt` is a reference to a server-stored prompt template — the most stateful
541
1039
  * field in the accepted schema.
542
1040
  *
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
1041
+ * `service_tier` is deliberately NOT dropped: the final TierDecision is applied to a
1042
+ * detached outbound body before this sanitizer chain, and silently deleting a configured knob is
545
1043
  * worse than forwarding a parameter the upstream ignores.
546
1044
  *
547
1045
  * MUST run before the composed sanitize chain below: `stripItemIdsWhenUnstored` keys
@@ -608,6 +1106,128 @@ function declaresImageGenClientTool(tool: unknown): boolean {
608
1106
  return isImageGenClientName(tool.name);
609
1107
  }
610
1108
 
1109
+ /** Rewrite client image-gen selectors to the hosted tool without widening caller restrictions. */
1110
+ function preferHostedImageGenToolChoice(toolChoice: unknown): unknown {
1111
+ if (!isPlainObject(toolChoice)) return toolChoice;
1112
+ if ((toolChoice.type === "function" || toolChoice.type === "custom") && typeof toolChoice.name === "string") {
1113
+ return isImageGenClientName(toolChoice.name) ? { type: HOSTED_IMAGE_GENERATION_TOOL } : toolChoice;
1114
+ }
1115
+ if (toolChoice.type !== "allowed_tools" || !Array.isArray(toolChoice.tools)) return toolChoice;
1116
+ const hasHostedImageTool = toolChoice.tools.some(tool => isPlainObject(tool) && tool.type === HOSTED_IMAGE_GENERATION_TOOL);
1117
+ let changed = false;
1118
+ let addedHostedImageTool = false;
1119
+ const tools: unknown[] = [];
1120
+ for (const tool of toolChoice.tools) {
1121
+ const isClientImageTool = isPlainObject(tool)
1122
+ && (tool.type === "function" || tool.type === "custom")
1123
+ && typeof tool.name === "string"
1124
+ && isImageGenClientName(tool.name);
1125
+ if (!isClientImageTool) {
1126
+ tools.push(tool);
1127
+ continue;
1128
+ }
1129
+ changed = true;
1130
+ if (!hasHostedImageTool && !addedHostedImageTool) {
1131
+ tools.push({ type: HOSTED_IMAGE_GENERATION_TOOL });
1132
+ addedHostedImageTool = true;
1133
+ }
1134
+ }
1135
+ return changed ? { ...toolChoice, tools } : toolChoice;
1136
+ }
1137
+
1138
+ /**
1139
+ * Some Responses-compatible gateways reserve the hosted image namespace even when the request
1140
+ * does not explicitly declare `image_generation`. For an explicitly configured model, remove only
1141
+ * colliding client declarations so the gateway's hosted tool can take precedence.
1142
+ */
1143
+ function preferConfiguredHostedTools(
1144
+ body: unknown,
1145
+ provider: OcxProviderConfig,
1146
+ modelId: string,
1147
+ selectedModelId?: string,
1148
+ ): unknown {
1149
+ // A virtual model's advertised id takes precedence over its resolved wire-model id.
1150
+ // Read own properties only: a routed model id of `constructor`/`toString` would
1151
+ // otherwise resolve to an inherited Object.prototype function and throw on the
1152
+ // membership test below, failing the request before it is dispatched.
1153
+ const preferenceMap = provider.modelPreferHostedTools;
1154
+ const ownPreference = (key: string | undefined): string[] | undefined => {
1155
+ if (!key || !preferenceMap || !Object.prototype.hasOwnProperty.call(preferenceMap, key)) return undefined;
1156
+ const entry = preferenceMap[key];
1157
+ return Array.isArray(entry) ? entry : undefined;
1158
+ };
1159
+ const preferredTools = ownPreference(selectedModelId) ?? ownPreference(modelId);
1160
+ if (!preferredTools?.includes(HOSTED_IMAGE_GENERATION_TOOL) || !isPlainObject(body)) return body;
1161
+
1162
+ const stripGroup = (tools: unknown[]): unknown[] => {
1163
+ const filtered = tools.filter(tool => !declaresImageGenClientTool(tool));
1164
+ return filtered.length === tools.length ? tools : filtered;
1165
+ };
1166
+
1167
+ let changed = false;
1168
+ let tools = body.tools;
1169
+ let strippedTopLevelImageGenTool = false;
1170
+ if (Array.isArray(body.tools)) {
1171
+ tools = stripGroup(body.tools);
1172
+ strippedTopLevelImageGenTool = tools !== body.tools;
1173
+ changed ||= strippedTopLevelImageGenTool;
1174
+ }
1175
+
1176
+ let input = body.input;
1177
+ const strippedAdditionalToolsIndices = new Set<number>();
1178
+ if (Array.isArray(body.input)) {
1179
+ let nestedChanged = false;
1180
+ const mappedInput = body.input.map((item, index) => {
1181
+ if (!isPlainObject(item) || item.type !== "additional_tools" || !Array.isArray(item.tools)) return item;
1182
+ const nestedTools = stripGroup(item.tools);
1183
+ if (nestedTools === item.tools) return item;
1184
+ strippedAdditionalToolsIndices.add(index);
1185
+ nestedChanged = true;
1186
+ return { ...item, tools: nestedTools };
1187
+ });
1188
+ if (nestedChanged) {
1189
+ input = mappedInput;
1190
+ changed = true;
1191
+ }
1192
+ }
1193
+
1194
+ const hasToolChoice = Object.hasOwn(body, "tool_choice");
1195
+ const toolChoice = hasToolChoice ? preferHostedImageGenToolChoice(body.tool_choice) : body.tool_choice;
1196
+ const toolChoiceChanged = hasToolChoice && toolChoice !== body.tool_choice;
1197
+ const hasHostedImageGenTool = (toolGroup: unknown): boolean => Array.isArray(toolGroup)
1198
+ && toolGroup.some(tool => isPlainObject(tool) && tool.type === HOSTED_IMAGE_GENERATION_TOOL);
1199
+ const hasHostedImageGenDeclaration = hasHostedImageGenTool(tools)
1200
+ || (Array.isArray(input) && input.some(item => isPlainObject(item)
1201
+ && item.type === "additional_tools"
1202
+ && hasHostedImageGenTool(item.tools)));
1203
+ if ((strippedTopLevelImageGenTool || strippedAdditionalToolsIndices.size > 0) && !hasHostedImageGenDeclaration) {
1204
+ if (strippedTopLevelImageGenTool && Array.isArray(tools)) {
1205
+ tools = [...tools, { type: HOSTED_IMAGE_GENERATION_TOOL }];
1206
+ } else if (strippedAdditionalToolsIndices.size > 0 && Array.isArray(input)) {
1207
+ // Restore into the FIRST stripped container only. Tool declarations are
1208
+ // request-scoped, not container-scoped — the containers are separate carriers for
1209
+ // one tool set, so a single hosted declaration covers the request. An earlier
1210
+ // revision restored into every stripped container and put `image_generation` on
1211
+ // the wire twice; review caught it.
1212
+ const firstStripped = Math.min(...strippedAdditionalToolsIndices);
1213
+ input = input.map((item, index) => index === firstStripped
1214
+ && isPlainObject(item)
1215
+ && Array.isArray(item.tools)
1216
+ ? { ...item, tools: [...item.tools, { type: HOSTED_IMAGE_GENERATION_TOOL }] }
1217
+ : item);
1218
+ }
1219
+ }
1220
+ changed ||= toolChoiceChanged;
1221
+ if (!changed) return body;
1222
+ const next: Record<string, unknown> = {
1223
+ ...body,
1224
+ ...(Array.isArray(body.tools) ? { tools } : {}),
1225
+ ...(Array.isArray(body.input) ? { input } : {}),
1226
+ };
1227
+ if (toolChoiceChanged) next.tool_choice = toolChoice;
1228
+ return next;
1229
+ }
1230
+
611
1231
  /**
612
1232
  * Lower one complete Codex image-gen namespace to public Responses function tools.
613
1233
  *
@@ -866,17 +1486,75 @@ function normalizeImageGenClientTools(body: unknown): unknown {
866
1486
  function stripUnsupportedHostedTools(body: unknown): unknown {
867
1487
  if (!isPlainObject(body) || !Array.isArray(body.tools)) return body;
868
1488
  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
1489
  const tools = body.tools.filter(t => {
873
1490
  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));
1491
+ return !type || !isHostedToolUnsupportedForModel(model, type);
876
1492
  });
877
1493
  return tools.length === body.tools.length ? body : { ...body, tools };
878
1494
  }
879
1495
 
1496
+ /**
1497
+ * OpenAI hosted web_search config fields that a capability-classified Responses
1498
+ * upstream may reject wholesale. xAI's /v1/responses 400s the entire request on
1499
+ * `external_web_access` and `search_context_size` ("Argument not supported"),
1500
+ * which killed every routed Grok turn whose client (Codex) attaches its
1501
+ * default web_search tool config (probe 2026-08-21: both fields 400
1502
+ * individually; `user_location` and `filters` are accepted and kept).
1503
+ * The caller decides whether to apply this compatibility transform from explicit
1504
+ * provider capability metadata; an unclassified upstream keeps the fields.
1505
+ */
1506
+ const OPENAI_ONLY_WEB_SEARCH_FIELDS = ["external_web_access", "search_context_size"] as const;
1507
+
1508
+ function stripOpenAiOnlyWebSearchFieldsFromTools(tools: unknown[]): {
1509
+ tools: unknown[];
1510
+ changed: boolean;
1511
+ } {
1512
+ let changed = false;
1513
+ const stripped = tools.map(tool => {
1514
+ if (!isPlainObject(tool) || (tool.type !== "web_search" && tool.type !== "web_search_preview")) {
1515
+ return tool;
1516
+ }
1517
+ if (!OPENAI_ONLY_WEB_SEARCH_FIELDS.some(field => Object.hasOwn(tool, field))) return tool;
1518
+ const { external_web_access: _access, search_context_size: _size, ...rest } = tool;
1519
+ changed = true;
1520
+ return rest;
1521
+ });
1522
+ return { tools: changed ? stripped : tools, changed };
1523
+ }
1524
+
1525
+ export function stripOpenAiOnlyWebSearchFields(body: unknown): unknown {
1526
+ if (!isPlainObject(body)) return body;
1527
+
1528
+ let next: Record<string, unknown> = body;
1529
+ let changed = false;
1530
+ if (Array.isArray(body.tools)) {
1531
+ const stripped = stripOpenAiOnlyWebSearchFieldsFromTools(body.tools);
1532
+ if (stripped.changed) {
1533
+ next = { ...next, tools: stripped.tools };
1534
+ changed = true;
1535
+ }
1536
+ }
1537
+
1538
+ if (Array.isArray(body.input)) {
1539
+ let inputChanged = false;
1540
+ const input = body.input.map(item => {
1541
+ if (!isPlainObject(item) || item.type !== "additional_tools" || !Array.isArray(item.tools)) {
1542
+ return item;
1543
+ }
1544
+ const stripped = stripOpenAiOnlyWebSearchFieldsFromTools(item.tools);
1545
+ if (!stripped.changed) return item;
1546
+ inputChanged = true;
1547
+ return { ...item, tools: stripped.tools };
1548
+ });
1549
+ if (inputChanged) {
1550
+ next = { ...next, input };
1551
+ changed = true;
1552
+ }
1553
+ }
1554
+
1555
+ return changed ? next : body;
1556
+ }
1557
+
880
1558
  /** Replace every `input_image` part under a routed-compaction body with a short marker. */
881
1559
  function stripInputImagesDeep(value: unknown): unknown {
882
1560
  if (Array.isArray(value)) return value.map(stripInputImagesDeep);
@@ -901,7 +1579,8 @@ function stripInputImagesDeep(value: unknown): unknown {
901
1579
  */
902
1580
  function buildRoutedCompactionBody(body: unknown): unknown {
903
1581
  if (!isPlainObject(body)) return body;
904
- const { tools: _tools, tool_choice: _toolChoice, parallel_tool_calls: _parallel, ...rest } = body;
1582
+ // `text` goes with the tool fields: the summary must be prose, not schema-constrained JSON.
1583
+ const { tools: _tools, tool_choice: _toolChoice, parallel_tool_calls: _parallel, text: _text, ...rest } = body;
905
1584
  const input = Array.isArray(body.input) ? body.input : [];
906
1585
  const kept = input.filter(item => !isPlainObject(item)
907
1586
  // `additional_tools` is how Codex Desktop's responses-lite shape carries tools;
@@ -963,29 +1642,38 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
963
1642
  let url: string;
964
1643
 
965
1644
  if (provider.authMode === "forward") {
1645
+ const mayForwardCallerCredentials = isCanonicalOpenAiForwardProvider(provider);
966
1646
  // OAuth passthrough: ChatGPT backend path is `${baseUrl}/responses` (no /v1).
967
- url = `${provider.baseUrl}/responses`;
1647
+ const baseUrl = mayForwardCallerCredentials
1648
+ ? CODEX_FORWARD_BASE_URL
1649
+ : provider.baseUrl.replace(/\/+$/, "");
1650
+ url = `${baseUrl}/responses`;
968
1651
  if (provider.headers) Object.assign(headers, provider.headers); // static headers first…
969
1652
  const runtimeProvider = provider as {
970
1653
  _codexAccountOverride?: { accessToken: string; chatgptAccountId: string };
971
1654
  _codexAccountRequired?: boolean;
972
1655
  };
973
- if (runtimeProvider._codexAccountRequired && !runtimeProvider._codexAccountOverride) {
1656
+ if (
1657
+ mayForwardCallerCredentials
1658
+ && runtimeProvider._codexAccountRequired
1659
+ && !runtimeProvider._codexAccountOverride
1660
+ ) {
974
1661
  throw new Error("Codex pool account auth is required but unavailable");
975
1662
  }
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.
1663
+ if (mayForwardCallerCredentials) {
1664
+ for (const h of FORWARD_HEADERS) {
1665
+ const v = incoming?.headers.get(h);
1666
+ if (v) headers[h] = v; // …so forwarded auth always wins.
1667
+ }
979
1668
  }
980
1669
  const override = runtimeProvider._codexAccountOverride;
981
- if (override) {
1670
+ if (override && mayForwardCallerCredentials) {
982
1671
  headers["authorization"] = `Bearer ${override.accessToken}`;
983
1672
  headers["chatgpt-account-id"] = override.chatgptAccountId;
984
1673
  }
985
1674
  } else {
986
1675
  if (provider.responsesPath === undefined) {
987
- const base = provider.baseUrl.replace(/\/v1\/?$/, "");
988
- url = `${base}/v1/responses`;
1676
+ url = openaiResponsesUrl(provider.baseUrl);
989
1677
  } else {
990
1678
  const base = provider.baseUrl.replace(/\/$/, "");
991
1679
  url = `${base}${provider.responsesPath}`;
@@ -995,11 +1683,17 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
995
1683
  }
996
1684
 
997
1685
  const forward = provider.authMode === "forward";
1686
+ let convertedRoutedCustomToolNames: Set<string> | undefined;
1687
+ let convertedRoutedToolSearchNames: Set<string> | undefined;
1688
+ let convertedRoutedNamespaceToolAliases: Map<string, { namespace: string; name: string }> | undefined;
998
1689
  const unexpandedMiss = !!parsed.previousResponseId && parsed._previousResponseInputExpanded !== true;
999
1690
  let outBody = stripPreviousResponseId(
1000
1691
  parsed._rawBody,
1001
1692
  forward || parsed._previousResponseInputExpanded === true,
1002
1693
  );
1694
+ // stripPreviousResponseId() intentionally returns its input on a no-op. Detach before the
1695
+ // tier write so a force-fast/default decision can never mutate parsed._rawBody.
1696
+ outBody = applyTierDecisionToResponsesBody(outBody, parsed.options?.tierDecision);
1003
1697
  const stateless = provider.statelessResponses === true;
1004
1698
  if (stateless) outBody = stripStatefulResponsesParams(outBody);
1005
1699
  // A replay miss can leave a function_call_output whose paired function_call sat
@@ -1008,28 +1702,104 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
1008
1702
  // backend gets — dropping previous_response_id is not much use if the body that
1009
1703
  // reaches the wire is unparseable.
1010
1704
  if (forward || stateless) {
1011
- outBody = repairOrphanedInputItems(outBody, unexpandedMiss);
1705
+ outBody = repairOrphanedInputItems(outBody, unexpandedMiss, stateless && !forward);
1706
+ }
1707
+ if (provider.requiresAdjacentResponsesToolResults === true) {
1708
+ outBody = normalizeResponsesToolResultAdjacency(outBody);
1012
1709
  }
1013
1710
  if (forward) {
1014
1711
  outBody = stripUnsupportedForwardParams(outBody);
1712
+ // Only the canonical ChatGPT backend rejects the retired field; a self-hosted or
1713
+ // third-party forward gateway may still accept it, so this must not be widened.
1714
+ if (isCanonicalOpenAiForwardProvider(provider)) {
1715
+ outBody = stripDeprecatedPromptCacheRetention(outBody, parsed.modelId);
1716
+ }
1717
+ } else {
1718
+ outBody = preferConfiguredHostedTools(
1719
+ outBody,
1720
+ provider,
1721
+ parsed.modelId,
1722
+ parsed._openAiVirtualSelectedModelId,
1723
+ );
1724
+ outBody = normalizeImageGenClientTools(outBody);
1015
1725
  }
1016
- else outBody = normalizeImageGenClientTools(outBody);
1017
1726
  if (forward || parsed._previousResponseInputExpanded === true) {
1018
1727
  outBody = repairOversizedReplayCallIds(outBody);
1019
1728
  }
1020
1729
  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).
1730
+ // Repair stored history from before the bridge emitted both keys: a conversation
1731
+ // that already recorded a single-query web_search_call replays it every turn, and
1732
+ // a strict parser rejects the whole request over it (#930).
1733
+ outBody = backfillWebSearchQueries(outBody);
1734
+ if (!isCanonicalOpenAiForwardProvider(provider)) {
1735
+ outBody = promoteClientLoadedTools(outBody);
1736
+ }
1737
+ if (!isCanonicalOpenAiForwardProvider(provider)) {
1738
+ const rewritten = rewriteRoutedCustomToolsForUpstream(
1739
+ outBody,
1740
+ provider.supportsResponsesCustomTools,
1741
+ );
1742
+ outBody = rewritten.body;
1743
+ convertedRoutedCustomToolNames = rewritten.names;
1744
+ }
1745
+ if (!isCanonicalOpenAiForwardProvider(provider)) {
1746
+ // Run after custom-tool lowering so the search compatibility layer can choose a
1747
+ // collision-free public function name against the final routed function catalog.
1748
+ const rewritten = rewriteRoutedToolSearchForUpstream(outBody);
1749
+ outBody = rewritten.body;
1750
+ convertedRoutedToolSearchNames = rewritten.names;
1751
+ }
1752
+ if (!isCanonicalOpenAiForwardProvider(provider)) {
1753
+ // Codex 0.147 emits private namespace tool groups, while public/third-party Responses
1754
+ // gateways accept only flat tool variants. Run after custom/tool-search lowering so
1755
+ // namespace children already carry their final public kind before they are promoted.
1756
+ const rewritten = rewriteRoutedNamespaceToolsForUpstream(outBody);
1757
+ outBody = rewritten.body;
1758
+ convertedRoutedNamespaceToolAliases = rewritten.aliases;
1759
+ // Preserve xAI's cached-only fail-closed semantics and image-search mapping before the
1760
+ // generic capability fallback removes the private OpenAI fields.
1761
+ outBody = normalizeXaiResponsesWebSearch(outBody, provider);
1762
+ // xAI and explicitly classified compatible gateways reject these OpenAI web_search
1763
+ // extensions. Keep them for OpenAI API-key traffic and unclassified gateways.
1764
+ if (provider.supportsOpenAiWebSearchToolFields === false) {
1765
+ outBody = stripOpenAiOnlyWebSearchFields(outBody);
1766
+ }
1767
+ // Last, so promoted namespace children are also cleared of Codex-private fields.
1768
+ outBody = stripCanonicalOnlyToolFields(outBody, provider.supportsOpenAiWebSearchToolFields === false);
1769
+ }
1770
+ // Same predicate as the routedCompaction gate in handleResponses(): an authMode check would
1771
+ // let a noncanonical custom forward provider skip this rewrite while the server still routes
1772
+ // it as a summarizer turn (#422). The compaction body build removes the tool surface and must
1773
+ // therefore be the last routed transform: anything before it may depend on the declarations;
1774
+ // anything after it cannot.
1024
1775
  if (parsed._compactionRequest === true && !isCanonicalOpenAiForwardProvider(provider)) {
1025
1776
  outBody = buildRoutedCompactionBody(outBody);
1026
1777
  }
1027
- const sanitizedBody = normalizeToolSchemas(stripSparkCompatibility(stripUnsupportedReasoningParams(stripItemIdsWhenUnstored(stripInvalidItemIds(stripUnsupportedHostedTools(sanitizeReasoningInputContent(scrubOcxCompactionItems(outBody))))))));
1028
- const body = JSON.stringify(stripDisabledReasoningSummaries(
1778
+ const threadServingIdentityChanged = parsed._stripReasoningEncryptedContent === true;
1779
+ const sanitizedBody = normalizeToolSchemas(stripSparkCompatibility(stripUnsupportedReasoningParams(stripItemIdsWhenUnstored(stripInvalidItemIds(stripUnsupportedHostedTools(sanitizeReasoningInputContent(scrubOcxCompactionItems(
1780
+ outBody,
1781
+ destinationDecodesNativeCompactionBlob(provider),
1782
+ threadServingIdentityChanged,
1783
+ ), {
1784
+ preserveRawReasoningContent: provider.preserveResponsesReasoningContent === true,
1785
+ dropNullContentChannel: !isOpenAiOperatedResponsesDestination(provider),
1786
+ stripEncryptedContent: threadServingIdentityChanged,
1787
+ })))))));
1788
+ const finalBody = stripDisabledReasoningSummaries(
1029
1789
  normalizeConfiguredReasoningSummaryDelivery(sanitizedBody, provider, parsed.modelId),
1030
1790
  provider,
1031
1791
  parsed.modelId,
1032
- ));
1792
+ );
1793
+ const actualServiceTier = isPlainObject(finalBody) && typeof finalBody.service_tier === "string"
1794
+ ? finalBody.service_tier
1795
+ : null;
1796
+ const tierLog = createAdapterTierMetadata(
1797
+ parsed.options?.tierObservation,
1798
+ parsed.options?.tierDecision,
1799
+ actualServiceTier === null ? null : "service-tier",
1800
+ actualServiceTier,
1801
+ );
1802
+ const body = JSON.stringify(finalBody);
1033
1803
  const releaseBodyObservation = translatorBudget.observeExternallyCapped(
1034
1804
  "passthrough_serialization",
1035
1805
  new TextEncoder().encode(body).byteLength,
@@ -1040,6 +1810,10 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
1040
1810
  headers,
1041
1811
  body,
1042
1812
  releaseBodyObservation,
1813
+ ...(convertedRoutedCustomToolNames ? { convertedRoutedCustomToolNames } : {}),
1814
+ ...(convertedRoutedToolSearchNames ? { convertedRoutedToolSearchNames } : {}),
1815
+ ...(convertedRoutedNamespaceToolAliases ? { convertedRoutedNamespaceToolAliases } : {}),
1816
+ ...(tierLog ? { tierLog } : {}),
1043
1817
  };
1044
1818
  },
1045
1819