@bitkyc08/opencodex 2.59.0 → 2.61.0-preview.20260922

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 (374) hide show
  1. package/AGENTS_INSTALL.md +64 -0
  2. package/README.md +28 -1
  3. package/bin/ocx.mjs +382 -209
  4. package/gui/dist/assets/App-E64Rzjap.js +50 -0
  5. package/gui/dist/assets/Tray-_nfzD8k4.js +1 -0
  6. package/gui/dist/assets/index-DpdfZWMK.js +86 -0
  7. package/gui/dist/assets/index-_bpvxJu0.css +1 -0
  8. package/gui/dist/assets/usage-companion-chart-DtoK7T6h.js +1 -0
  9. package/gui/dist/favicon.png +0 -0
  10. package/gui/dist/index.html +2 -2
  11. package/gui/dist/provider-icons/stepfun-color.svg +1 -0
  12. package/package.json +8 -1
  13. package/src/adapters/anthropic-image-codec.ts +16 -2
  14. package/src/adapters/anthropic-image-normalize.ts +49 -2
  15. package/src/adapters/anthropic.ts +20 -1
  16. package/src/adapters/base.ts +23 -0
  17. package/src/adapters/coding-agent/protocol.ts +36 -6
  18. package/src/adapters/coding-agent/turn.ts +32 -4
  19. package/src/adapters/command-code.ts +52 -4
  20. package/src/adapters/cursor/catalog.ts +51 -7
  21. package/src/adapters/cursor/checkpoint-store.ts +3 -0
  22. package/src/adapters/cursor/discovery.ts +11 -8
  23. package/src/adapters/cursor/live-transport.ts +26 -9
  24. package/src/adapters/cursor/protobuf-request.ts +6 -3
  25. package/src/adapters/cursor/request-builder.ts +20 -4
  26. package/src/adapters/cursor/transport.ts +19 -0
  27. package/src/adapters/cursor.ts +14 -3
  28. package/src/adapters/declaration-carrier.ts +45 -0
  29. package/src/adapters/devin/cloud-direct/chat.ts +3 -1
  30. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +42 -5
  31. package/src/adapters/devin.ts +125 -36
  32. package/src/adapters/google-antigravity-replay.ts +1 -1
  33. package/src/adapters/google-antigravity-wire.ts +34 -7
  34. package/src/adapters/google-errors.ts +7 -1
  35. package/src/adapters/google-http.ts +49 -10
  36. package/src/adapters/google-tool-schema.ts +595 -31
  37. package/src/adapters/google-wire-compiler.ts +93 -10
  38. package/src/adapters/google-wire-shape.ts +461 -0
  39. package/src/adapters/google.ts +66 -11
  40. package/src/adapters/image.ts +4 -1
  41. package/src/adapters/input-media-guard.ts +21 -9
  42. package/src/adapters/kiro/usage.ts +3 -2
  43. package/src/adapters/kiro-tool-fallback.ts +1 -1
  44. package/src/adapters/ollama-native.ts +6 -0
  45. package/src/adapters/openai-chat/developer-role.ts +61 -0
  46. package/src/adapters/openai-chat/messages.ts +46 -27
  47. package/src/adapters/openai-chat/parallel-tool-calls.ts +32 -0
  48. package/src/adapters/openai-chat/passthrough.ts +33 -9
  49. package/src/adapters/openai-chat/reasoning-wire.ts +89 -0
  50. package/src/adapters/openai-chat-images.ts +3 -1
  51. package/src/adapters/openai-chat.ts +23 -58
  52. package/src/adapters/openai-responses/image-gen.ts +8 -6
  53. package/src/adapters/openai-responses/passthrough.ts +17 -3
  54. package/src/adapters/openai-responses/reasoning.ts +7 -0
  55. package/src/adapters/opencode-go-additional-tools.ts +12 -2
  56. package/src/adapters/registry.ts +3 -2
  57. package/src/adapters/run-turn-queue.ts +178 -29
  58. package/src/adapters/xai-web-search.ts +16 -1
  59. package/src/bridge/errors.ts +8 -2
  60. package/src/bridge/response-json.ts +9 -1
  61. package/src/bridge/sse.ts +13 -151
  62. package/src/chat/inbound.ts +141 -5
  63. package/src/claude/desktop-3p.ts +7 -1
  64. package/src/claude/desktop-first-party.ts +183 -0
  65. package/src/claude/desktop-gateway-state.ts +41 -0
  66. package/src/claude/inbound-content-options.ts +6 -0
  67. package/src/claude/inbound.ts +32 -6
  68. package/src/claude/intercept/connect-proxy.ts +179 -0
  69. package/src/claude/intercept/listener.ts +122 -0
  70. package/src/claude/intercept/local-ca.ts +298 -0
  71. package/src/claude/intercept/runtime.ts +98 -0
  72. package/src/claude/intercept/settings.ts +189 -0
  73. package/src/cli/access.ts +87 -0
  74. package/src/cli/account-auth.ts +19 -0
  75. package/src/cli/account-extended.ts +4 -4
  76. package/src/cli/capabilities.ts +31 -0
  77. package/src/cli/claude-desktop.ts +206 -16
  78. package/src/cli/codex-shim-autorestore.ts +3 -0
  79. package/src/cli/companion.ts +56 -0
  80. package/src/cli/dispatch.ts +46 -7
  81. package/src/cli/doctor.ts +28 -9
  82. package/src/cli/ensure-desired-integrations.ts +43 -5
  83. package/src/cli/help.ts +7 -9
  84. package/src/cli/hub.ts +3 -2
  85. package/src/cli/index.ts +203 -59
  86. package/src/cli/init.ts +8 -0
  87. package/src/cli/integrations.ts +7 -1
  88. package/src/cli/opencode.ts +2 -2
  89. package/src/cli/provider.ts +13 -1
  90. package/src/cli/registry.ts +41 -2
  91. package/src/cli/resolve.ts +230 -0
  92. package/src/cli/root.ts +24 -1
  93. package/src/cli/start-ownership-publication.ts +56 -0
  94. package/src/cli/status-probes.ts +2 -18
  95. package/src/cli/status.ts +62 -0
  96. package/src/cli/stop-report.ts +143 -0
  97. package/src/cli/uninstall-plan.ts +9 -0
  98. package/src/client/machine-api.ts +2 -2
  99. package/src/client/machine-listener.ts +4 -7
  100. package/src/client/runtime.ts +26 -2
  101. package/src/clients/aside-profiles.ts +4 -0
  102. package/src/clients/config-export/zcode-store.ts +157 -0
  103. package/src/clients/config-export.ts +36 -0
  104. package/src/codex/account-store.ts +65 -0
  105. package/src/codex/app-server-processes.ts +72 -40
  106. package/src/codex/auth-api/account-list.ts +19 -11
  107. package/src/codex/auth-api/login-flow.ts +6 -1
  108. package/src/codex/auth-api/pool-quota-probe.ts +30 -7
  109. package/src/codex/autostart-health.ts +28 -0
  110. package/src/codex/catalog/build-entries.ts +2 -2
  111. package/src/codex/catalog/effort.ts +3 -3
  112. package/src/codex/catalog/gather-capture.ts +21 -2
  113. package/src/codex/catalog/model-hints.ts +29 -28
  114. package/src/codex/catalog/parsing.ts +7 -0
  115. package/src/codex/catalog/provider-models.ts +43 -17
  116. package/src/codex/catalog/retained-sync.ts +24 -28
  117. package/src/codex/catalog/routed-gather.ts +19 -0
  118. package/src/codex/context-compat.ts +5 -2
  119. package/src/codex/convergence.ts +2 -2
  120. package/src/codex/desired-state.ts +4 -1
  121. package/src/codex/history-job.ts +6 -6
  122. package/src/codex/history-provider.ts +31 -166
  123. package/src/codex/history-rollout-read.ts +174 -0
  124. package/src/codex/inject/config-toml.ts +41 -6
  125. package/src/codex/inject/paginated-openai-compat.ts +90 -0
  126. package/src/codex/inject.ts +18 -15
  127. package/src/codex/injected-marker.ts +18 -0
  128. package/src/codex/internal/catalog-writer.ts +33 -1
  129. package/src/codex/main-account.ts +6 -0
  130. package/src/codex/model-cache.ts +99 -6
  131. package/src/codex/model-entitlement-admission.ts +59 -0
  132. package/src/codex/model-entitlements.ts +116 -54
  133. package/src/codex/native-main-admission.ts +83 -0
  134. package/src/codex/observed-model-denials.ts +101 -8
  135. package/src/codex/prompt-text-probe.ts +9 -6
  136. package/src/codex/routing/health-store.ts +39 -0
  137. package/src/codex/routing/selection.ts +37 -1
  138. package/src/codex/routing.ts +12 -42
  139. package/src/codex/shim-templates.ts +29 -3
  140. package/src/codex/shim.ts +1 -1
  141. package/src/codex/subagent-model-fallback.ts +22 -4
  142. package/src/combos/failover.ts +3 -0
  143. package/src/companion/settings.ts +132 -0
  144. package/src/config/admitted-identity.ts +222 -0
  145. package/src/config/atomic-write.ts +117 -5
  146. package/src/config/diagnostics.ts +22 -1
  147. package/src/config/feature-flags.ts +5 -0
  148. package/src/config/load-degrade.ts +52 -7
  149. package/src/config/process-state.ts +1 -1
  150. package/src/config/proxy-env.ts +8 -2
  151. package/src/config/schema/compaction-triggers.ts +11 -0
  152. package/src/config/schema/config-schema.ts +31 -1
  153. package/src/config/schema/leaf-validators.ts +59 -0
  154. package/src/config.ts +3 -3
  155. package/src/generated/compatibility-version.json +604 -280
  156. package/src/grok/reset-coupons.ts +38 -19
  157. package/src/images/loop.ts +6 -1
  158. package/src/integrations/aside-profile-context.ts +37 -3
  159. package/src/integrations/aside-profile-journal.ts +68 -3
  160. package/src/integrations/aside-profiles.ts +128 -3
  161. package/src/integrations/config-io.ts +44 -10
  162. package/src/integrations/merge.ts +120 -13
  163. package/src/integrations/mutation-plan.ts +921 -0
  164. package/src/integrations/registry.ts +38 -0
  165. package/src/integrations/state.ts +78 -45
  166. package/src/integrations/target.ts +208 -0
  167. package/src/integrations/writer.ts +134 -110
  168. package/src/lab/conformance/fixture-provider.ts +5 -0
  169. package/src/lab/live/transport.ts +4 -0
  170. package/src/lab/live/types.ts +5 -0
  171. package/src/lab/subject/behavior-fingerprint.ts +1 -1
  172. package/src/lib/admin-secrets.ts +9 -1
  173. package/src/lib/browser-launch-notice.ts +59 -0
  174. package/src/lib/bun-runtime.ts +6 -2
  175. package/src/lib/debug-log-buffer.ts +6 -1
  176. package/src/lib/debug.ts +63 -0
  177. package/src/lib/errors.ts +79 -0
  178. package/src/lib/http-response-semantics.ts +57 -0
  179. package/src/lib/lab-live-pinned-sender.ts +26 -12
  180. package/src/lib/open-url.ts +51 -7
  181. package/src/lib/package-tree-integrity.ts +2 -1
  182. package/src/lib/package-version.ts +8 -0
  183. package/src/lib/pinned-http.ts +142 -2
  184. package/src/lib/plain-data.ts +103 -0
  185. package/src/lib/process-control.ts +13 -5
  186. package/src/lib/provider-egress.ts +310 -0
  187. package/src/lib/provider-outbound.ts +109 -16
  188. package/src/lib/proxy-env.ts +82 -7
  189. package/src/lib/request-execution-budget.ts +72 -0
  190. package/src/lib/request-failure-attribution.ts +183 -0
  191. package/src/lib/request-failure-model.ts +236 -0
  192. package/src/lib/request-resend-gate.ts +138 -0
  193. package/src/lib/socks5-fetch.ts +136 -26
  194. package/src/lib/spend-ledger-owner.ts +364 -0
  195. package/src/lib/spend-reservation-ledger.ts +218 -27
  196. package/src/lib/standalone.ts +16 -0
  197. package/src/lib/upstream-retry.ts +167 -16
  198. package/src/lib/windows-system-proxy.ts +16 -11
  199. package/src/lib/winsw.ts +2 -2
  200. package/src/oauth/callback-server.ts +4 -3
  201. package/src/oauth/generic-account-failover.ts +1 -0
  202. package/src/oauth/health.ts +12 -1
  203. package/src/oauth/index.ts +27 -108
  204. package/src/oauth/login-cli.ts +80 -29
  205. package/src/oauth/login-flow-state.ts +127 -0
  206. package/src/providers/api-key-resolve.ts +133 -0
  207. package/src/providers/api-key-selection.ts +5 -1
  208. package/src/providers/derive.ts +34 -17
  209. package/src/providers/devin-cli-authmode-migration.ts +14 -10
  210. package/src/providers/key-failover.ts +97 -19
  211. package/src/providers/key-store.ts +34 -110
  212. package/src/providers/model-rename-fields.ts +147 -0
  213. package/src/providers/model-rename-migration.ts +179 -38
  214. package/src/providers/model-rename-startup.ts +7 -5
  215. package/src/providers/openai-virtual-models.ts +42 -2
  216. package/src/providers/quota/antigravity.ts +22 -2
  217. package/src/providers/quota/vendor-probes-key.ts +38 -23
  218. package/src/providers/reasoning-metadata.ts +43 -18
  219. package/src/providers/registry/entries-core.ts +47 -20
  220. package/src/providers/registry/entries-extended.ts +63 -4
  221. package/src/providers/registry/model-ids.ts +168 -0
  222. package/src/providers/registry/model-seeds.ts +56 -10
  223. package/src/providers/registry/types.ts +2 -0
  224. package/src/providers/resolved-model-policy-merge.ts +167 -0
  225. package/src/providers/resolved-model-policy.ts +406 -0
  226. package/src/providers/stale-vision-classification-migration.ts +137 -0
  227. package/src/providers/xai-transport.ts +12 -1
  228. package/src/reasoning-effort.ts +8 -0
  229. package/src/responses/apply-patch-envelope.ts +0 -12
  230. package/src/responses/freeform-wrapper-scan.ts +279 -0
  231. package/src/responses/function-call-compat.ts +38 -1
  232. package/src/responses/inline-document.ts +65 -0
  233. package/src/responses/input-media.ts +42 -8
  234. package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
  235. package/src/responses/muse-tool-name-alias.ts +19 -0
  236. package/src/responses/parser-content.ts +8 -2
  237. package/src/responses/parser-tools.ts +3 -0
  238. package/src/responses/parser.ts +3 -1
  239. package/src/responses/progressive-freeform-input.ts +130 -0
  240. package/src/responses/reasoning-envelope.ts +30 -0
  241. package/src/responses/schema.ts +3 -0
  242. package/src/responses/state.ts +5 -12
  243. package/src/responses/tool-name-aliases.ts +15 -1
  244. package/src/router.ts +108 -117
  245. package/src/routing/compatibility/behavior.ts +9 -0
  246. package/src/routing/compatibility/subject.ts +16 -1
  247. package/src/server/adapter-resolve.ts +9 -0
  248. package/src/server/admission-model-scope.ts +219 -0
  249. package/src/server/audio-live.ts +9 -3
  250. package/src/server/audio-upstream.ts +18 -0
  251. package/src/server/auth-cors.ts +29 -0
  252. package/src/server/chat-completions.ts +60 -4
  253. package/src/server/chat-native.ts +19 -4
  254. package/src/server/claude-messages.ts +61 -20
  255. package/src/server/effort-row.ts +11 -3
  256. package/src/server/grok-responses-control-frame.ts +160 -1
  257. package/src/server/grok-responses-snapshot-repair.ts +113 -11
  258. package/src/server/gui-freshness.ts +103 -0
  259. package/src/server/gui-static.ts +7 -9
  260. package/src/server/images.ts +59 -6
  261. package/src/server/index/claude-intercept-lifecycle.ts +49 -0
  262. package/src/server/index/serve-options.ts +142 -39
  263. package/src/server/index/spend-ledger-lifecycle.ts +92 -0
  264. package/src/server/index/startup-warnings.ts +24 -0
  265. package/src/server/index/websocket-handler.ts +6 -1
  266. package/src/server/index.ts +34 -38
  267. package/src/server/lifecycle.ts +4 -4
  268. package/src/server/live-call-bindings.ts +6 -0
  269. package/src/server/live.ts +88 -3
  270. package/src/server/management/agent-settings-routes.ts +121 -36
  271. package/src/server/management/aside-profile-routes.ts +266 -7
  272. package/src/server/management/companion-routes.ts +77 -0
  273. package/src/server/management/config-routes.ts +18 -2
  274. package/src/server/management/context.ts +3 -0
  275. package/src/server/management/integration-routes.ts +287 -5
  276. package/src/server/management/logs-usage-routes.ts +19 -0
  277. package/src/server/management/metrics-routes.ts +20 -0
  278. package/src/server/management/model-rows.ts +224 -12
  279. package/src/server/management/native-integration-routes.ts +103 -6
  280. package/src/server/management/oauth-account-routes.ts +45 -7
  281. package/src/server/management/route-registry.ts +20 -0
  282. package/src/server/management/shared.ts +28 -4
  283. package/src/server/management/system-restart.ts +7 -2
  284. package/src/server/management/system-routes.ts +2 -0
  285. package/src/server/management/usage-aggregate-cache.ts +4 -0
  286. package/src/server/management/usage-timeline-routes.ts +44 -0
  287. package/src/server/management-api.ts +10 -9
  288. package/src/server/management-auth.ts +15 -1
  289. package/src/server/proxy-liveness.ts +75 -0
  290. package/src/server/readiness.ts +29 -10
  291. package/src/server/relay-eager.ts +24 -2
  292. package/src/server/relay.ts +138 -12
  293. package/src/server/request-log-failure-attribution.ts +99 -0
  294. package/src/server/request-log.ts +169 -2
  295. package/src/server/request-metrics.ts +298 -0
  296. package/src/server/responses/adapter-continuation.ts +3 -3
  297. package/src/server/responses/adapter-dispatch.ts +11 -6
  298. package/src/server/responses/codex-ws-wire.ts +34 -8
  299. package/src/server/responses/combo-stream-preflight.ts +168 -6
  300. package/src/server/responses/compact.ts +43 -10
  301. package/src/server/responses/compaction-routing.ts +111 -0
  302. package/src/server/responses/core-codex-account.ts +8 -3
  303. package/src/server/responses/core-combo.ts +7 -7
  304. package/src/server/responses/core-normalize.ts +6 -12
  305. package/src/server/responses/core-opaque-recovery.ts +91 -0
  306. package/src/server/responses/core-options.ts +4 -0
  307. package/src/server/responses/encrypted-payload.ts +20 -2
  308. package/src/server/responses/fetch-helpers.ts +124 -8
  309. package/src/server/responses/input-admission.ts +10 -0
  310. package/src/server/responses/passthrough-delivery.ts +45 -15
  311. package/src/server/responses/passthrough-dispatch.ts +215 -44
  312. package/src/server/responses/passthrough-error.ts +27 -8
  313. package/src/server/responses/policy-fallback.ts +5 -13
  314. package/src/server/responses/request-prepare.ts +109 -18
  315. package/src/server/responses/request-send-budget.ts +17 -1
  316. package/src/server/responses/request-sidecar-auth.ts +1 -1
  317. package/src/server/responses/request-transport.ts +26 -6
  318. package/src/server/responses/reset-replay.ts +108 -0
  319. package/src/server/responses/run-turn-execution.ts +25 -3
  320. package/src/server/responses/sidecar-execution.ts +17 -2
  321. package/src/server/responses/ws-upstream.ts +14 -27
  322. package/src/server/responses-custom-tool-repair.ts +27 -54
  323. package/src/server/responses-request-tool-scope.ts +214 -0
  324. package/src/server/responses-undeclared-tool-guard.ts +35 -2
  325. package/src/server/search.ts +25 -1
  326. package/src/server/sse-payload-rewrite.ts +1 -1
  327. package/src/server/usage-ledger-retention.ts +73 -0
  328. package/src/service/cli.ts +48 -2
  329. package/src/service/health.ts +3 -2
  330. package/src/service/install-state-contract.d.mts +27 -0
  331. package/src/service/install-state-contract.mjs +34 -0
  332. package/src/service/launchd.ts +1 -1
  333. package/src/service/orchestration.ts +2 -4
  334. package/src/service/ownership-compatibility.ts +164 -0
  335. package/src/service/ownership-mutation-lease.d.mts +32 -0
  336. package/src/service/ownership-mutation-lease.mjs +211 -0
  337. package/src/service/repair.ts +45 -1
  338. package/src/service/state-lock.ts +269 -0
  339. package/src/service/state-record.d.mts +36 -0
  340. package/src/service/state-record.mjs +138 -0
  341. package/src/service/state.ts +582 -68
  342. package/src/service/windows-taskxml.ts +11 -10
  343. package/src/service.ts +7 -3
  344. package/src/tray/windows-tray.ps1 +156 -4
  345. package/src/types/config.ts +48 -3
  346. package/src/types/provider.ts +91 -0
  347. package/src/types/request.ts +30 -2
  348. package/src/types/tools.ts +33 -0
  349. package/src/types.ts +4 -0
  350. package/src/update/index.ts +207 -63
  351. package/src/update/job.ts +9 -5
  352. package/src/update/ownership-transaction.ts +47 -0
  353. package/src/update/restart-ownership.ts +54 -0
  354. package/src/update/runtime-ownership.d.mts +40 -0
  355. package/src/update/runtime-ownership.mjs +122 -0
  356. package/src/usage/attempt-delivery.ts +198 -0
  357. package/src/usage/cache-diagnostic.ts +305 -0
  358. package/src/usage/failure-fingerprint.ts +118 -0
  359. package/src/usage/failure-projection-cache.ts +174 -0
  360. package/src/usage/failure-projection.ts +174 -0
  361. package/src/usage/ledger-retention.ts +165 -0
  362. package/src/usage/log.ts +126 -79
  363. package/src/usage/request-outcome.ts +150 -0
  364. package/src/usage/retention-contract.ts +28 -0
  365. package/src/usage/summary.ts +2 -2
  366. package/src/usage/telemetry-contract.ts +237 -0
  367. package/src/usage/timeline.ts +236 -0
  368. package/src/vision/eligibility.ts +88 -9
  369. package/src/vision/plan.ts +34 -10
  370. package/src/web-search/alpha-search.ts +21 -1
  371. package/src/web-search/executor.ts +41 -2
  372. package/src/web-search/loop.ts +6 -1
  373. package/gui/dist/assets/index-C5IebErG.js +0 -136
  374. package/gui/dist/assets/index-OESInAjC.css +0 -1
@@ -1,4 +1,10 @@
1
1
  import { capturePoolQuotaWriter } from "../../codex/account-store";
2
+ import {
3
+ admissionModelDeniedResponse,
4
+ AdmissionModelDeniedError,
5
+ assertRouteAllowedByScope,
6
+ resolveAdmissionModelScope,
7
+ } from "../admission-model-scope";
2
8
  import type { Server } from "bun";
3
9
  import { bridgeToResponsesSSE, buildResponseJSON, formatErrorResponse, type ResponsesTerminalStatus } from "../../bridge";
4
10
  import {
@@ -10,6 +16,7 @@ import { parseRequest } from "../../responses/parser";
10
16
  import { buildCompactV1Output, COMPACT_PROMPT, decodeCompactionSummary, extractCompactUserMessages } from "../../responses/compaction";
11
17
  import { FORWARD_HEADERS, sanitizeReasoningInputContent } from "../../adapters/openai-responses";
12
18
  import { expandPreviousResponseInput, previousResponseProviderState, rememberResponseState } from "../../responses/state";
19
+ import { repairLegacyDottedToolCallNames } from "../../responses/legacy-dotted-tool-name-repair";
13
20
  import { NoEligiblePolicyCandidateError, routeCompactionModel } from "../../router";
14
21
  import { evidenceFromBody } from "../../routing/request-evidence";
15
22
  import {
@@ -124,7 +131,12 @@ import { slugsEquivalent } from "../../providers/slug-codec";
124
131
  import { decideTier, tierValueAfterDecision } from "../../providers/fastwire";
125
132
  import { fastPolicyForModel } from "../../providers/service-tier";
126
133
  import { parseFastOnlyRowId } from "../fast-row";
127
- import { applyOpenAiVirtualModel, resolveOpenAiCompactModel } from "../../providers/openai-virtual-models";
134
+ import { captureOpenAiVirtualWirePolicy, resolveOpenAiCompactModel } from "../../providers/openai-virtual-models";
135
+ import {
136
+ applyCompactionRoutingOverride,
137
+ compactionRoutingKeepsProviderIdentity,
138
+ type CompactionRoutingOverride,
139
+ } from "./compaction-routing";
128
140
  import { isUsageDebugEnabled } from "../../usage/debug";
129
141
  import {
130
142
  readJsonRequestBody,
@@ -271,6 +283,7 @@ function compactHandoffRoute(
271
283
  }
272
284
 
273
285
  export interface HandleResponsesCompactOptions {
286
+ compactionRoutingOverride?: CompactionRoutingOverride | null;
274
287
  nativeMainRefreshDependencies?: NativeMainRefreshDependencies;
275
288
  /** Release the listener's idle guard only after the complete request body is accepted. */
276
289
  onRequestBodyRead?: () => void;
@@ -603,6 +616,9 @@ export async function handleResponsesCompact(
603
616
  if (!body || typeof body !== "object" || Array.isArray(body)) {
604
617
  return formatErrorResponse(400, "invalid_request_error", "Invalid compaction request body");
605
618
  }
619
+ if (!options.compactionRoutingOverride) {
620
+ options = { ...options, compactionRoutingOverride: applyCompactionRoutingOverride(body, req.headers, config, { endpoint: "compact" }) };
621
+ }
606
622
  const raw = body as { model?: unknown; input?: unknown };
607
623
  if (typeof raw.model !== "string" || raw.model.length === 0) {
608
624
  return formatErrorResponse(400, "invalid_request_error", "compaction request requires a model");
@@ -617,11 +633,12 @@ export async function handleResponsesCompact(
617
633
  // The client's own selector, kept for the request log: `raw.model` is rewritten to the
618
634
  // base id above, and logCtx.requestedModel is assigned from it further down, so without
619
635
  // this the log would lose which id the client actually asked for.
620
- const compactRequestedModel = compactFastRow ? compactFastRow.baseId + "--fast" : raw.model;
636
+ const compactRequestedModel = options.compactionRoutingOverride?.sourceModel
637
+ ?? (compactFastRow ? compactFastRow.baseId + "--fast" : raw.model);
621
638
 
622
639
  // Recall the last completed client-visible bare model after a combo switch (#3891).
623
640
  // Configured selectors take precedence over this implicit session hint.
624
- if (typeof compactModel === "string" && !compactModel.includes("/") && !compactFastRow
641
+ if (!options.compactionRoutingOverride && typeof compactModel === "string" && !compactModel.includes("/") && !compactFastRow
625
642
  && !resolveComboId(config, compactModel)) {
626
643
  const recalledComboId = recallComboForLane(config, sessionLaneIdFromRequest(req.headers), compactModel);
627
644
  if (recalledComboId) {
@@ -644,7 +661,12 @@ export async function handleResponsesCompact(
644
661
  // routes ordinary turns elsewhere (#2901); the compaction-scoped router
645
662
  // may land that on the configured default provider instead of 404.
646
663
  route = routeCompactionModel(config, compactModel, evidenceFromBody(raw));
664
+ // A compaction override picks the model, not the caller, so the key's scope
665
+ // is applied to what the override resolved to rather than to the selector
666
+ // the client sent.
667
+ assertRouteAllowedByScope(resolveAdmissionModelScope(config, admission), compactRequestedModel, route);
647
668
  } catch (err) {
669
+ if (err instanceof AdmissionModelDeniedError) return admissionModelDeniedResponse(err);
648
670
  if (err instanceof NoEligiblePolicyCandidateError) {
649
671
  // Persist the evaluation trace (per-candidate exclusions + the
650
672
  // no-eligible reason) so a failed compact policy request stays
@@ -684,6 +706,7 @@ export async function handleResponsesCompact(
684
706
  const virtual = resolveOpenAiCompactModel(route.providerName, selectedModelId);
685
707
  if (virtual) {
686
708
  route.modelId = virtual.wireModelId;
709
+ captureOpenAiVirtualWirePolicy(route, virtual);
687
710
  logCtx.model = virtual.selectedModelId;
688
711
  logCtx.resolvedModel = virtual.wireModelId;
689
712
  } else {
@@ -747,7 +770,12 @@ export async function handleResponsesCompact(
747
770
  // no budget at all, so `handleResponsesInner` minted a fresh four after the native attempt
748
771
  // had already spent some of the first one.
749
772
  const sendBudget: RequestExecutionBudget = options.sendBudget ?? createRequestExecutionBudget();
750
- if (supportsNativeResponsesCompactEndpoint(route.providerName, route.provider) && !accountGatedCompactWireModel && !route.combo) {
773
+ // A manual override onto another backend must not mint ciphertext the conversation model cannot replay.
774
+ const manualOverrideCrossesProvider = options.compactionRoutingOverride
775
+ ? !compactionRoutingKeepsProviderIdentity(config, options.compactionRoutingOverride, route)
776
+ : false;
777
+ if (supportsNativeResponsesCompactEndpoint(route.providerName, route.provider) && !accountGatedCompactWireModel && !route.combo
778
+ && !manualOverrideCrossesProvider) {
751
779
  if (req.signal.aborted) {
752
780
  return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
753
781
  }
@@ -845,7 +873,12 @@ export async function handleResponsesCompact(
845
873
  // The regular /v1/responses path applies sanitizeReasoningInputContent via the adapter's
846
874
  // buildRequest, but the compact endpoint forwards directly. Apply the same sanitizer here
847
875
  // so routed-model reasoning items (reasoning_text content) don't 400 the ChatGPT backend.
848
- const compactBody = sanitizeReasoningInputContent(compactBodyRaw) as typeof compactBodyRaw;
876
+ // #5095: the compact endpoint forwards `raw` directly, so it needs the same legacy dotted
877
+ // call-name repair the adapter applies. A damaged item here refuses the compaction itself,
878
+ // which is the request a long task depends on to keep going.
879
+ const compactBody = repairLegacyDottedToolCallNames(
880
+ sanitizeReasoningInputContent(compactBodyRaw),
881
+ ) as typeof compactBodyRaw;
849
882
  {
850
883
  const binding = conversationStateBindingFromAuth(authCtx, codexPoolAffinityKey(req.headers));
851
884
  if (binding) {
@@ -1303,9 +1336,9 @@ export async function handleResponsesCompact(
1303
1336
  // synthetic buffer errors are not upstream bodies and stay uninspected.
1304
1337
  if (buffered.ok) {
1305
1338
  inspectResponseLogJson(logCtx, await buffered.clone().text());
1306
- forgetCompactHandoffRoute(req, admission);
1339
+ if (!options.compactionRoutingOverride) forgetCompactHandoffRoute(req, admission);
1307
1340
  rememberServingConversationStateIssuer(outcomeCtx, codexPoolAffinityKey(req.headers));
1308
- } else if (quotaFailure && !storedPool401ReplayAttempted) {
1341
+ } else if (!options.compactionRoutingOverride && quotaFailure && !storedPool401ReplayAttempted) {
1309
1342
  const fallbackModel = compactHandoffRoute(req, admission, raw.model);
1310
1343
  if (fallbackModel && !req.signal.aborted) {
1311
1344
  const fallbackReq = new Request(req.url, {
@@ -1368,7 +1401,7 @@ export async function handleResponsesCompact(
1368
1401
  // The routed compaction turn is a handoff inside the same logical request, so it draws the
1369
1402
  // REMAINDER. Minting here is what let a native attempt spend three sends and the routed
1370
1403
  // fallback spend four more.
1371
- const response = await handleResponses(internalReq, config, logCtx, { abortSignal: req.signal, turnAdmissionLease, sendBudget, ...(admission ? { admission } : {}) });
1404
+ const response = await handleResponses(internalReq, config, logCtx, { abortSignal: req.signal, turnAdmissionLease, sendBudget, compactionRoutingOverride: options.compactionRoutingOverride, ...(admission ? { admission } : {}) });
1372
1405
  if (!response.ok) return response;
1373
1406
  let json: { output?: unknown[]; status?: unknown; error?: unknown };
1374
1407
  if (response.headers.get("content-type")?.includes("text/event-stream")) {
@@ -1438,7 +1471,7 @@ export async function handleResponsesCompact(
1438
1471
  const result = new Response(JSON.stringify({ output: compactionItems }), {
1439
1472
  headers: { "Content-Type": "application/json" },
1440
1473
  });
1441
- rememberCompactHandoffRoute(req, admission, raw.model);
1474
+ if (!options.compactionRoutingOverride) rememberCompactHandoffRoute(req, admission, raw.model);
1442
1475
  return result;
1443
1476
  }
1444
1477
  const encrypted = compactionItems[0]!.encrypted_content;
@@ -1449,6 +1482,6 @@ export async function handleResponsesCompact(
1449
1482
  }
1450
1483
  const summary = decoded;
1451
1484
  const output = buildCompactV1Output(extractCompactUserMessages(inputItems), summary);
1452
- rememberCompactHandoffRoute(req, admission, raw.model);
1485
+ if (!options.compactionRoutingOverride) rememberCompactHandoffRoute(req, admission, raw.model);
1453
1486
  return new Response(JSON.stringify({ output }), { headers: { "Content-Type": "application/json" } });
1454
1487
  }
@@ -0,0 +1,111 @@
1
+ import type { OcxConfig } from "../../types";
2
+ import { isDeclaredReasoningEffort } from "../../reasoning-effort";
3
+ import { COMPACTION_TRIGGERS } from "../../config/schema/compaction-triggers";
4
+ import { routeConcreteModel, type RouteResult } from "../../router";
5
+ import { resolveComboId } from "../../combos/identifiers";
6
+ import { recallComboForLane } from "./combo-session-recall";
7
+ import { sessionLaneIdFromRequest } from "../request-log-conversation";
8
+
9
+ /** `sourceModel` is the conversation's own selector before the rewrite. */
10
+ export interface CompactionRoutingOverride {
11
+ sourceModel: string;
12
+ /** Combo the lane remembers for a bare `sourceModel` (#3891); the conversation resumes there, not on the bare route. */
13
+ sourceCombo?: string;
14
+ /** Combo the configured override resolves to; its children route concretely but stay portable. */
15
+ targetCombo?: string;
16
+ }
17
+
18
+ function record(value: unknown): Record<string, unknown> | undefined {
19
+ return value !== null && typeof value === "object" && !Array.isArray(value)
20
+ ? value as Record<string, unknown>
21
+ : undefined;
22
+ }
23
+
24
+ /**
25
+ * Triggers this override covers. Absent means manual only, which is the narrowest
26
+ * reading of the setting and leaves automatic compaction exactly as it routes today.
27
+ * A hand-edited value the schema would have rejected disables the override rather
28
+ * than widening it, so a malformed edit can never route more than it names.
29
+ */
30
+ function configuredTriggers(value: unknown): readonly string[] | null {
31
+ if (value === undefined) return ["manual"];
32
+ if (!Array.isArray(value) || value.length === 0) return null;
33
+ if (!value.every(entry => typeof entry === "string" && (COMPACTION_TRIGGERS as readonly string[]).includes(entry))) return null;
34
+ if (new Set(value).size !== value.length) return null;
35
+ return value as readonly string[];
36
+ }
37
+
38
+ export interface CompactionRoutingOverrideOptions {
39
+ /** `responses` requires a `compaction_trigger` input item; the native compact endpoint carries none. */
40
+ endpoint?: "responses" | "compact";
41
+ transport?: "websocket";
42
+ }
43
+
44
+ export function applyCompactionRoutingOverride(
45
+ body: unknown,
46
+ headers: Headers,
47
+ config: OcxConfig,
48
+ options: CompactionRoutingOverrideOptions = {},
49
+ ): CompactionRoutingOverride | null {
50
+ const override = config.compactionRouting;
51
+ const raw = record(body);
52
+ if (!raw || typeof raw.model !== "string" || !raw.model.trim()
53
+ || typeof override?.model !== "string" || !override.model.trim()) return null;
54
+ if (override.reasoningEffort !== undefined
55
+ && (typeof override.reasoningEffort !== "string" || !isDeclaredReasoningEffort(override.reasoningEffort))) return null;
56
+ const triggers = configuredTriggers(override.triggers);
57
+ if (!triggers) return null;
58
+ if (options.endpoint !== "compact"
59
+ && !(Array.isArray(raw.input) && raw.input.some(item => record(item)?.type === "compaction_trigger"))) return null;
60
+
61
+ const metadata: unknown[] = [];
62
+ const header = headers.get("x-codex-turn-metadata");
63
+ if (options.transport !== "websocket" && header !== null) metadata.push(header);
64
+ const client = record(raw.client_metadata);
65
+ if (client && Object.hasOwn(client, "x-codex-turn-metadata")) metadata.push(client["x-codex-turn-metadata"]);
66
+ if (metadata.length === 0) return null;
67
+ let trigger: string | undefined;
68
+ for (const value of metadata) {
69
+ if (typeof value !== "string") return null;
70
+ try {
71
+ const parsed = record(JSON.parse(value));
72
+ if (parsed?.request_kind !== "compaction") return null;
73
+ const carried = record(parsed.compaction)?.trigger;
74
+ if (typeof carried !== "string" || !triggers.includes(carried)) return null;
75
+ // Copies that name different triggers are not agreement, and picking either one would
76
+ // let a caller widen an override by disagreeing with itself.
77
+ if (trigger !== undefined && trigger !== carried) return null;
78
+ trigger = carried;
79
+ } catch {
80
+ return null;
81
+ }
82
+ }
83
+ if (trigger === undefined) return null;
84
+
85
+ const sourceModel = raw.model;
86
+ const sourceCombo = recallComboForLane(config, sessionLaneIdFromRequest(headers), sourceModel);
87
+ const targetCombo = resolveComboId(config, override.model.trim()) ?? undefined;
88
+ raw.model = override.model.trim();
89
+ if (override.reasoningEffort !== undefined) {
90
+ raw.reasoning = { ...record(raw.reasoning), effort: override.reasoningEffort };
91
+ }
92
+ return { sourceModel, ...(sourceCombo ? { sourceCombo } : {}), ...(targetCombo ? { targetCombo } : {}) };
93
+ }
94
+
95
+ /** Same provider identity keeps caller auth and may use native compact; its ciphertext replays only there. */
96
+ export function compactionRoutingKeepsProviderIdentity(
97
+ config: OcxConfig,
98
+ override: CompactionRoutingOverride,
99
+ route: RouteResult,
100
+ ): boolean {
101
+ if (route.combo || override.sourceCombo || override.targetCombo || resolveComboId(config, override.sourceModel)) return false;
102
+ let source: RouteResult;
103
+ try {
104
+ source = routeConcreteModel(config, override.sourceModel);
105
+ } catch {
106
+ return false;
107
+ }
108
+ return source.providerName === route.providerName
109
+ && source.codexAccountMode === route.codexAccountMode
110
+ && source.codexAccountNamespace === route.codexAccountNamespace;
111
+ }
@@ -328,7 +328,8 @@ export interface CodexPoolAccountRetryArgs {
328
328
  /** Sanitized caller input, before any selected Pool credential was materialized. */
329
329
  callerAuthHeaders: Headers;
330
330
  config: OcxConfig;
331
- route: { providerName: string; modelId: string; provider: OcxProviderConfig };
331
+ /** Actual routed result narrowed to the fields this retry consumes. */
332
+ route: Pick<RouteResult, "providerName" | "modelId" | "provider" | "staticPolicy">;
332
333
  parsed: OcxParsedRequest;
333
334
  logCtx: RequestLogContext;
334
335
  options: {
@@ -681,7 +682,7 @@ export async function retryCodexPoolOnAlternateAccount(
681
682
  "pool",
682
683
  );
683
684
  const retryAdapter = resolveAdapter(
684
- resolveWireProtocolOverride(route.providerName, route.modelId, retryProvider, inboundWire),
685
+ resolveWireProtocolOverride(route.providerName, route.modelId, retryProvider, inboundWire, route.staticPolicy),
685
686
  config.cacheRetention,
686
687
  route.providerName,
687
688
  );
@@ -841,7 +842,11 @@ export async function retryCodexPoolOnAlternateAccount(
841
842
  parsed.modelId,
842
843
  );
843
844
  if (retryModelDenial !== undefined) {
844
- recordCodexModelDenialEvidence(retryAuthCtx.accountId, retryModelDenial);
845
+ recordCodexModelDenialEvidence(
846
+ retryAuthCtx.accountId,
847
+ retryModelDenial,
848
+ retryAuthCtx.kind === "pool" ? retryAuthCtx.generation : undefined,
849
+ );
845
850
  }
846
851
  if (!retrySameConfirmedAccount || retrySendCount >= maxRetrySends) break;
847
852
  // Caller-owned main is an alternate-account replay and can never enter the bounded
@@ -29,7 +29,6 @@ import {
29
29
  import { formatErrorResponse } from "../../bridge";
30
30
  import {
31
31
  expandPreviousResponseInput,
32
- previousResponseScopeMismatch,
33
32
  previousResponseReplayFailure,
34
33
  previousResponseProviderState,
35
34
  } from "../../responses/state";
@@ -201,11 +200,12 @@ export async function executeComboResponses(
201
200
  // imageInput is disabled (and so targets see the full replayed input).
202
201
  const inboundClientThreadId = req.headers.get("x-codex-parent-thread-id")?.trim() || undefined;
203
202
  const body = expandPreviousResponseInput(rawBody, inboundClientThreadId);
204
- const scopeMismatch = previousResponseScopeMismatch(body);
205
- if (scopeMismatch) {
206
- console.warn("[opencodex] dropped a previous_response_id with a mismatched client task scope; continuing fresh");
203
+ const replayFailure = previousResponseReplayFailure(body);
204
+ if (replayFailure?.reason === "scope_mismatch") {
205
+ console.warn("[opencodex] refusing continuation because the client task scope does not match replay state");
207
206
  }
208
- if (previousResponseReplayFailure(body)) {
207
+ // Local replay failures require full client replay.
208
+ if (replayFailure) {
209
209
  return formatErrorResponse(
210
210
  400,
211
211
  "previous_response_not_found",
@@ -235,7 +235,7 @@ export async function executeComboResponses(
235
235
  sourceBody: body,
236
236
  previousResponseInputExpanded: body !== rawBody
237
237
  && typeof (body as { previous_response_id?: unknown }).previous_response_id === "string",
238
- providerContinuation: !scopeMismatch && body !== rawBody && requestedPreviousId
238
+ providerContinuation: body !== rawBody && requestedPreviousId
239
239
  ? previousResponseProviderState(requestedPreviousId)
240
240
  : undefined,
241
241
  recoveredPlaintext: false,
@@ -521,7 +521,7 @@ export async function executeComboResponses(
521
521
  // The live config can change while the child is streaming. Never retain credentials.
522
522
  const currentCombo = getCombo(config, comboId);
523
523
  const provider = config.providers[completedTarget.provider];
524
- if (Object.hasOwn(config.providers, completedTarget.provider)
524
+ if (!options.compactionRoutingOverride && Object.hasOwn(config.providers, completedTarget.provider)
525
525
  && provider && provider.disabled !== true
526
526
  && currentCombo?.targets.some(target => targetKey(target) === targetKey(completedTarget))) {
527
527
  rememberComboForLane(sessionLaneIdFromRequest(req.headers), comboId, completedTarget, model, writerGeneration);
@@ -13,7 +13,6 @@ import { MAIN_CODEX_ACCOUNT_ID } from "../../codex/main-account";
13
13
  import type { RequestLogContext } from "../request-log";
14
14
  import type { HandleResponsesOptions } from "./core-options";
15
15
  import { prepareEffortNormalization } from "../effort-policy";
16
- import { providerModelResponsesUpstreamStreaming } from "../../providers/registry";
17
16
  import { resolveOpenCodeGoTransport } from "../../providers/opencode-go-transport";
18
17
  import { getOrAllocateRequestSessionLane } from "../request-log-conversation";
19
18
  import { shouldPreparePlaintextV2AgentMessages } from "../../responses/plaintext-v2-agent-messages";
@@ -80,6 +79,7 @@ export function canPassThroughEncryptedV2AgentTask(
80
79
  route.modelId,
81
80
  provider,
82
81
  inboundWire,
82
+ route.staticPolicy,
83
83
  ).adapter === "openai-responses";
84
84
  }
85
85
 
@@ -126,6 +126,8 @@ export async function applyFinalRouteRequestNormalization(args: {
126
126
  const responseModelId = parsed.modelId;
127
127
  const preserveAnthropicResponseModel = route.providerName === "anthropic"
128
128
  || route.provider.adapter === "anthropic";
129
+ const finalSelectedModelId = route.modelId;
130
+ const virtualModel = applyOpenAiVirtualModel(parsed, route, logCtx, inboundWire);
129
131
 
130
132
  // Apply the routed model id upstream: routing may strip a "<provider>/" namespace.
131
133
  if (route.modelId !== parsed.modelId) {
@@ -136,11 +138,7 @@ export async function applyFinalRouteRequestNormalization(args: {
136
138
  }
137
139
  // Transport-neutral reliability policy (#875): applies to any Responses
138
140
  // upstream whose final adapter is openai-responses, not only WS turns.
139
- const responsesUpstreamStreaming = providerModelResponsesUpstreamStreaming(
140
- route.providerName,
141
- route.provider,
142
- route.modelId,
143
- );
141
+ const responsesUpstreamStreaming = route.staticPolicy.model.responsesUpstreamStreaming;
144
142
 
145
143
  // Preserve the routed destination for Go recognition, then settle the wire before
146
144
  // deriving protocol-scoped affinity. Recognition must not inspect the flipped adapter.
@@ -150,6 +148,7 @@ export async function applyFinalRouteRequestNormalization(args: {
150
148
  route.modelId,
151
149
  routedProvider,
152
150
  inboundWire,
151
+ route.staticPolicy,
153
152
  );
154
153
  route.provider = resolveOpenCodeGoTransport(wireProvider,
155
154
  args.claudeGoAffinity ? args.claudeGoAffinity.sessionLane : getOrAllocateRequestSessionLane(req),
@@ -168,7 +167,7 @@ export async function applyFinalRouteRequestNormalization(args: {
168
167
  || (!summary && route.provider.showThinkingSummary !== true);
169
168
  }
170
169
  if (preserveAnthropicResponseModel) parsed._responseModelId = responseModelId;
171
- logCtx.model = route.modelId;
170
+ logCtx.model = virtualModel?.selectedModelId ?? route.modelId;
172
171
  logCtx.provider = route.providerName;
173
172
  logCtx.providerAdapter = route.provider.adapter;
174
173
  logCtx.routeDecision = route.routeDecision;
@@ -194,11 +193,6 @@ export async function applyFinalRouteRequestNormalization(args: {
194
193
  (parsed._rawBody as Record<string, unknown>).store = false;
195
194
  }
196
195
 
197
- // Final selected model before virtual wire-model rewriting (Pro aliases).
198
- const finalSelectedModelId = route.modelId;
199
-
200
- // Virtual model rewriting: Pro aliases → base model + reasoning.mode="pro".
201
- applyOpenAiVirtualModel(parsed, route, logCtx);
202
196
  if (parsed._responseModelId !== undefined && parsed._responseModelId !== parsed.modelId) {
203
197
  logCtx.resolvedModel = route.modelId;
204
198
  logCtx.preserveResolvedModelFromRoute = true;
@@ -88,6 +88,7 @@ export function isEncryptedFunctionOutputRejection(bodyText: string): boolean {
88
88
  try {
89
89
  const payload = JSON.parse(bodyText) as unknown;
90
90
  if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
91
+ if (upstreamErrorMessageFromPayload(payload) === ENCRYPTED_FUNCTION_OUTPUT_REJECTION) return true;
91
92
  const record = payload as { detail?: unknown; message?: unknown; error?: unknown };
92
93
  if (record.detail === ENCRYPTED_FUNCTION_OUTPUT_REJECTION) return true;
93
94
  if (record.message === ENCRYPTED_FUNCTION_OUTPUT_REJECTION) return true;
@@ -115,6 +116,88 @@ export function isReasoningBlobCallerMismatchMessage(message: string): boolean {
115
116
  }
116
117
 
117
118
 
119
+ /**
120
+ * Longest embedded payload this will parse. The wrapper is a short error envelope; anything
121
+ * larger is not the shape being matched, and refusing to walk it keeps an upstream-controlled
122
+ * string from deciding how much work the classifier does.
123
+ */
124
+ const LITELLM_EMBEDDED_PAYLOAD_LIMIT = 16_384;
125
+ const LITELLM_WRAPPER_PREFIX = "litellm.BadRequestError:";
126
+ const LITELLM_WRAPPER_MARKER = "OpenAIException - ";
127
+
128
+ /**
129
+ * The JSON an OpenAI-compatible gateway embeds in its own error message, or undefined.
130
+ *
131
+ * Brace-aware rather than a regex because the embedded object legitimately contains braces and
132
+ * escaped quotes inside its message, and the gateway appends its own prose after the closing
133
+ * brace. Counting depth outside string literals is the only way to find the real end.
134
+ */
135
+ function liteLlmEmbeddedErrorPayload(message: string): unknown {
136
+ if (!message.startsWith(LITELLM_WRAPPER_PREFIX)) return undefined;
137
+ const markerIndex = message.indexOf(LITELLM_WRAPPER_MARKER);
138
+ if (markerIndex < 0) return undefined;
139
+ const start = message.indexOf("{", markerIndex + LITELLM_WRAPPER_MARKER.length);
140
+ if (start < 0) return undefined;
141
+ const end = Math.min(message.length, start + LITELLM_EMBEDDED_PAYLOAD_LIMIT);
142
+
143
+ let depth = 0;
144
+ let inString = false;
145
+ let escaped = false;
146
+ for (let index = start; index < end; index += 1) {
147
+ const character = message[index]!;
148
+ if (inString) {
149
+ if (escaped) escaped = false;
150
+ else if (character === "\\") escaped = true;
151
+ else if (character === '"') inString = false;
152
+ continue;
153
+ }
154
+ if (character === '"') { inString = true; continue; }
155
+ if (character === "{") depth += 1;
156
+ else if (character === "}") {
157
+ depth -= 1;
158
+ if (depth === 0) {
159
+ try { return JSON.parse(message.slice(start, index + 1)) as unknown; } catch { return undefined; }
160
+ }
161
+ }
162
+ }
163
+ return undefined;
164
+ }
165
+
166
+
167
+ /** True for an error message that is a gateway envelope rather than an upstream's own wording. */
168
+ function isLiteLlmEnvelopeMessage(message: string): boolean {
169
+ return message.startsWith(LITELLM_WRAPPER_PREFIX) && message.includes(LITELLM_WRAPPER_MARKER);
170
+ }
171
+
172
+
173
+ /**
174
+ * An OpenAI-compatible gateway relaying the one authoritative ciphertext rejection inside its
175
+ * own error string.
176
+ *
177
+ * Deliberately narrower than {@link isSelfIdentifiedOpaqueBlobRejection}. The embedded payload is
178
+ * matched against exactly one identity -- `invalid_request_error` carrying
179
+ * `invalid_encrypted_content` -- and the generic classifier is NOT re-run against it. Re-running
180
+ * it would let every other opaque identity arrive through the wrapper as well: the code-less
181
+ * unverifiable-ciphertext wording, the #4469 caller mismatch, and the two xAI decoder strings.
182
+ * Each of those was admitted on evidence from a specific upstream about how that upstream words
183
+ * its own rejection, and a gateway in between is not that evidence. Only the coded identity is
184
+ * unambiguous enough to survive relaying.
185
+ */
186
+ export function isLiteLlmWrappedCiphertextRejection(payload: unknown): boolean {
187
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
188
+ const outer = (payload as { error?: unknown }).error;
189
+ if (!outer || typeof outer !== "object" || Array.isArray(outer)) return false;
190
+ const message = (outer as { message?: unknown }).message;
191
+ if (typeof message !== "string") return false;
192
+ const embedded = liteLlmEmbeddedErrorPayload(message);
193
+ if (!embedded || typeof embedded !== "object" || Array.isArray(embedded)) return false;
194
+ const inner = (embedded as { error?: unknown }).error;
195
+ if (!inner || typeof inner !== "object" || Array.isArray(inner)) return false;
196
+ const { type, code } = inner as { type?: unknown; code?: unknown };
197
+ return type === "invalid_request_error" && code === "invalid_encrypted_content";
198
+ }
199
+
200
+
118
201
  export function isSelfIdentifiedOpaqueBlobRejection(bodyText: string): boolean {
119
202
  if (isEncryptedFunctionOutputRejection(bodyText)) return true;
120
203
  try {
@@ -131,6 +214,14 @@ export function isSelfIdentifiedOpaqueBlobRejection(bodyText: string): boolean {
131
214
 
132
215
  if (record.error && typeof record.error === "object" && !Array.isArray(record.error)) {
133
216
  const error = record.error as { type?: unknown; code?: unknown; message?: unknown };
217
+ // A gateway envelope is decided ONLY by its embedded payload, before any wording check
218
+ // below runs. Those checks match anchored phrases anywhere in the message, and a gateway
219
+ // quotes the upstream's message inside its own -- so without this the relayed text would
220
+ // satisfy the caller-mismatch identity and gain a resend the strict wrapper check exists to
221
+ // withhold. Returning here rather than falling through is the point.
222
+ if (typeof error.message === "string" && isLiteLlmEnvelopeMessage(error.message)) {
223
+ return isLiteLlmWrappedCiphertextRejection(payload);
224
+ }
134
225
  if (error.type === "invalid_request_error") {
135
226
  if (error.code === "invalid_encrypted_content") return true;
136
227
  if (
@@ -11,6 +11,7 @@ import type { NativeMainRefreshDependencies } from "../../codex/main-account";
11
11
  import type { InboundWire } from "../../providers/registry";
12
12
  import type { ExplicitOpenAiCallerAuth } from "../../providers/openai-sidecar";
13
13
  import type { CallerDirectAuth } from "../../providers/caller-authorization";
14
+ import type { CompactionRoutingOverride } from "./compaction-routing";
14
15
  import type { TranslatorBudget } from "../../lib/translator-budget";
15
16
  import type { TransientSendBudget } from "../../lib/upstream-retry";
16
17
  import type { RequestLogContext } from "../request-log";
@@ -51,6 +52,8 @@ export interface HandleResponsesOptions {
51
52
  admission?: DataPlaneAdmission;
52
53
  /** Called at most once after the complete client body is read and accepted for dispatch. */
53
54
  onRequestBodyRead?: () => void;
55
+ /** Internal handoff for retry wrappers that must reuse the already-accounted request body. */
56
+ onRequestBodyParsed?: (body: unknown) => void;
54
57
  forceEmptyResponseId?: boolean;
55
58
  /** Internal, connection-owned control channel; never reconstructed from headers. */
56
59
  nativeControl?: NativeResponseControl;
@@ -105,6 +108,7 @@ export interface HandleResponsesOptions {
105
108
  callerDirectAuth?: CallerDirectAuth | null;
106
109
  /** Internal recursion guard; callers outside this module must not set it. */
107
110
  comboAttempt?: boolean;
111
+ compactionRoutingOverride?: CompactionRoutingOverride | null;
108
112
  /** Internal combo handoff for one parent-validated continuation snapshot. */
109
113
  comboReplaySnapshot?: {
110
114
  sourceBody: unknown;
@@ -98,6 +98,19 @@ import type { EffectiveSubagentRoster, SpawnAgentSurface } from "../../codex/cat
98
98
 
99
99
 
100
100
  export function looksLikeBackendCiphertext(payload: string): boolean {
101
+ // Unknown replay history has no authenticity proof. Require the key-independent Fernet wire
102
+ // structure instead of granting ciphertext authority to any long base64-like model output.
103
+ // Proven backend bytes still remain opaque and byte-identical; malformed/plaintext slots are
104
+ // lowered by the compatibility path below rather than poisoning every later native replay.
105
+ return isStructurallyValidFernetToken(payload);
106
+ }
107
+
108
+ /**
109
+ * Pre-route compatibility keeps an encoded-looking unknown slot opaque until the destination is
110
+ * known. A routed destination strips that slot instead of exposing possible truncated ciphertext;
111
+ * the canonical backend later applies the stricter structural classifier.
112
+ */
113
+ function looksLikeUnknownOpaqueSlot(payload: string): boolean {
101
114
  return payload.length >= 64 && /^[A-Za-z0-9+/=_-]+$/.test(payload);
102
115
  }
103
116
 
@@ -480,7 +493,10 @@ function contentWithoutCiphertext(content: unknown[]): unknown[] {
480
493
 
481
494
 
482
495
 
483
- export function sanitizeEncryptedContentInPlace(input: unknown): number {
496
+ export function sanitizeEncryptedContentInPlace(
497
+ input: unknown,
498
+ options: { preserveUnknownOpaqueSlots?: boolean } = {},
499
+ ): number {
484
500
  if (!Array.isArray(input)) return 0;
485
501
  let rewritten = 0;
486
502
  const protectedFragments = new WeakSet<object>();
@@ -512,7 +528,9 @@ export function sanitizeEncryptedContentInPlace(input: unknown): number {
512
528
  && typeof (child as { encrypted_content?: unknown }).encrypted_content === "string"
513
529
  ) {
514
530
  const payload = (child as { encrypted_content: string }).encrypted_content;
515
- if (!protectedFragments.has(child) && !looksLikeBackendCiphertext(payload)) {
531
+ const preserveUnknown = options.preserveUnknownOpaqueSlots === true
532
+ && looksLikeUnknownOpaqueSlot(payload);
533
+ if (!protectedFragments.has(child) && !looksLikeBackendCiphertext(payload) && !preserveUnknown) {
516
534
  const parts = encryptedSlotParts(payload);
517
535
  frame.node.splice(frame.index, 1, ...parts);
518
536
  rewritten += 1;