@bitkyc08/opencodex 2.76.0 → 2.78.0-preview.20261005

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 (353) hide show
  1. package/bin/ocx.mjs +11 -10
  2. package/gui/dist/assets/App-CppMz-6w.js +54 -0
  3. package/gui/dist/assets/App-lCIslZ1L.css +1 -0
  4. package/gui/dist/assets/{Tray-DjNwwrWS.js → Tray-CJWQqtVv.js} +1 -1
  5. package/gui/dist/assets/index-DQt9srNi.js +94 -0
  6. package/gui/dist/assets/index-DmQB8D_R.css +1 -0
  7. package/gui/dist/assets/quota-summary-BMsyoZH2.js +1 -0
  8. package/gui/dist/index.html +4 -3
  9. package/package.json +1 -1
  10. package/src/adapters/anthropic/beta-allowlist.ts +19 -2
  11. package/src/adapters/anthropic/client-identity.ts +13 -4
  12. package/src/adapters/anthropic/native-client-preamble.ts +31 -0
  13. package/src/adapters/anthropic/passthrough.ts +103 -25
  14. package/src/adapters/antigravity-refusal.ts +28 -0
  15. package/src/adapters/cursor/protobuf-request.ts +6 -0
  16. package/src/adapters/cursor/request-builder.ts +3 -0
  17. package/src/adapters/cursor/structured-output.ts +13 -0
  18. package/src/adapters/cursor/types.ts +2 -0
  19. package/src/adapters/devin/cloud-direct/chat.ts +7 -2
  20. package/src/adapters/devin/trajectory.ts +49 -0
  21. package/src/adapters/devin.ts +13 -0
  22. package/src/adapters/ollama-native.ts +62 -19
  23. package/src/adapters/openai-responses/passthrough.ts +133 -103
  24. package/src/adapters/openai-responses/request-strips.ts +10 -4
  25. package/src/adapters/zed.ts +26 -4
  26. package/src/bridge/response-json.ts +14 -14
  27. package/src/bridge/sse.ts +15 -5
  28. package/src/chatgpt/app-server-shim/bundle-trust.ts +40 -10
  29. package/src/chatgpt/app-server-shim/gate-rewrite.ts +3 -0
  30. package/src/claude/agents-inject.ts +4 -43
  31. package/src/claude/gateway-cache.ts +16 -13
  32. package/src/claude/inbound-model-options.ts +9 -2
  33. package/src/claude/intercept/cli-catalog.ts +27 -16
  34. package/src/claude/intercept/client-hello.ts +103 -0
  35. package/src/claude/intercept/model-bindings.ts +3 -2
  36. package/src/claude/intercept/picker-bootstrap.ts +37 -16
  37. package/src/claude/intercept/picker-budget.ts +41 -0
  38. package/src/claude/intercept/picker-listener.ts +279 -66
  39. package/src/claude/outbound.ts +2 -0
  40. package/src/claude/subagent-force-status.ts +53 -0
  41. package/src/claude/subagent-model.ts +93 -0
  42. package/src/cli/access-audio-input.ts +84 -0
  43. package/src/cli/access-audio-live.ts +99 -0
  44. package/src/cli/access-audio.ts +123 -0
  45. package/src/cli/access-data-client.ts +127 -0
  46. package/src/cli/access-data-plane.ts +104 -0
  47. package/src/cli/access-data-response.ts +76 -0
  48. package/src/cli/access.ts +65 -2
  49. package/src/cli/account-api.ts +33 -6
  50. package/src/cli/account-auth.ts +111 -42
  51. package/src/cli/account-key-quota.ts +118 -0
  52. package/src/cli/account-main.ts +11 -4
  53. package/src/cli/account-next-actions.ts +63 -0
  54. package/src/cli/account-policy-dto.ts +68 -0
  55. package/src/cli/account-policy.ts +167 -0
  56. package/src/cli/account-target.ts +8 -0
  57. package/src/cli/account.ts +37 -9
  58. package/src/cli/agent-runtime-settings.ts +130 -0
  59. package/src/cli/agent-settings.ts +101 -0
  60. package/src/cli/agent.ts +29 -2
  61. package/src/cli/alias.ts +7 -2
  62. package/src/cli/aside-profiles.ts +2 -2
  63. package/src/cli/capabilities-access-remote.ts +147 -0
  64. package/src/cli/capabilities-accounts.ts +195 -0
  65. package/src/cli/capabilities-agents-routing.ts +414 -0
  66. package/src/cli/capabilities-base.ts +783 -0
  67. package/src/cli/capabilities-command.ts +1 -0
  68. package/src/cli/capabilities-integrations.ts +264 -0
  69. package/src/cli/capabilities-lab.ts +214 -0
  70. package/src/cli/capabilities-observe-system.ts +662 -0
  71. package/src/cli/capabilities-provider-models.ts +302 -0
  72. package/src/cli/capabilities.ts +30 -1190
  73. package/src/cli/capability-types.ts +54 -0
  74. package/src/cli/catalog-command-result.ts +57 -0
  75. package/src/cli/chatgpt-command.ts +35 -9
  76. package/src/cli/claude-agent-startup-sync.ts +17 -0
  77. package/src/cli/claude-desktop-profile.ts +100 -0
  78. package/src/cli/claude-desktop.ts +39 -6
  79. package/src/cli/claude.ts +22 -6
  80. package/src/cli/codex-shim-autorestore.ts +2 -1
  81. package/src/cli/codex-shim-readiness.ts +109 -0
  82. package/src/cli/combo-input.ts +88 -0
  83. package/src/cli/combo-result.ts +52 -0
  84. package/src/cli/combo-stats.ts +123 -0
  85. package/src/cli/combo.ts +37 -14
  86. package/src/cli/companion-timeline.ts +142 -0
  87. package/src/cli/companion-usage.ts +146 -0
  88. package/src/cli/companion.ts +12 -3
  89. package/src/cli/config-command.ts +24 -8
  90. package/src/cli/connect.ts +19 -0
  91. package/src/cli/dispatch.ts +55 -82
  92. package/src/cli/doctor.ts +22 -4
  93. package/src/cli/export-command.ts +20 -9
  94. package/src/cli/gui.ts +8 -3
  95. package/src/cli/help-catalog.ts +101 -0
  96. package/src/cli/help-models-context.ts +17 -0
  97. package/src/cli/help-navigation.ts +50 -0
  98. package/src/cli/help-recovery.ts +66 -0
  99. package/src/cli/help.ts +69 -11
  100. package/src/cli/index.ts +38 -27
  101. package/src/cli/injection-follow.ts +35 -0
  102. package/src/cli/inspect.ts +5 -0
  103. package/src/cli/integration-aside-sync.ts +69 -0
  104. package/src/cli/integration-cursor.ts +106 -0
  105. package/src/cli/integration-input.ts +60 -0
  106. package/src/cli/integration-journal.ts +49 -0
  107. package/src/cli/integration-plan-dto.ts +156 -0
  108. package/src/cli/integration-preview.ts +215 -0
  109. package/src/cli/integrations.ts +24 -18
  110. package/src/cli/json-input.ts +134 -0
  111. package/src/cli/link.ts +32 -4
  112. package/src/cli/local-sync-result.ts +41 -0
  113. package/src/cli/log-follow.ts +84 -0
  114. package/src/cli/log-view-filter.ts +146 -0
  115. package/src/cli/logout-command.ts +44 -0
  116. package/src/cli/model-picker-ordering.ts +137 -0
  117. package/src/cli/models-custom-input.ts +107 -0
  118. package/src/cli/models-custom-runtime.ts +112 -0
  119. package/src/cli/models-order.ts +194 -0
  120. package/src/cli/models-runtime-subcommands.ts +2 -0
  121. package/src/cli/models-runtime.ts +9 -1
  122. package/src/cli/models.ts +66 -81
  123. package/src/cli/observe-stream.ts +142 -0
  124. package/src/cli/observe.ts +75 -34
  125. package/src/cli/opencode.ts +54 -10
  126. package/src/cli/provider-batch.ts +77 -0
  127. package/src/cli/provider-lifecycle-runtime.ts +170 -0
  128. package/src/cli/provider-result.ts +124 -0
  129. package/src/cli/provider-runtime.ts +15 -12
  130. package/src/cli/provider-settings.ts +143 -0
  131. package/src/cli/provider.ts +116 -64
  132. package/src/cli/registry.ts +90 -24
  133. package/src/cli/remote-workspace-hub.ts +83 -0
  134. package/src/cli/remote-workspace.ts +6 -1
  135. package/src/cli/restart-failure.ts +27 -0
  136. package/src/cli/root.ts +42 -9
  137. package/src/cli/route-policy-write.ts +137 -0
  138. package/src/cli/route-policy.ts +8 -0
  139. package/src/cli/runtime-api.ts +57 -80
  140. package/src/cli/secret-args.ts +149 -0
  141. package/src/cli/settings-result.ts +30 -0
  142. package/src/cli/storage.ts +77 -7
  143. package/src/cli/system-command.ts +60 -2
  144. package/src/cli/system-restart-client.ts +20 -3
  145. package/src/cli/system-settings-parity.ts +83 -0
  146. package/src/cli/tray-proxy.ts +3 -2
  147. package/src/cli/update-restart-candidate.ts +21 -0
  148. package/src/cli/update-restart-child.ts +81 -0
  149. package/src/cli/update-restart-home.ts +38 -0
  150. package/src/cli/update-restart-transport.ts +193 -0
  151. package/src/cli/update-restart.ts +260 -0
  152. package/src/cli/usage-model-search.ts +58 -0
  153. package/src/cli/usage-report.ts +13 -5
  154. package/src/cli/v2-input.ts +66 -0
  155. package/src/cli/v2-local-output.ts +37 -0
  156. package/src/cli/v2-runtime.ts +90 -0
  157. package/src/cli/v2.ts +102 -144
  158. package/src/clients/config-export/contracts.ts +100 -0
  159. package/src/clients/config-export/droid.ts +54 -2
  160. package/src/clients/config-export/dsh.ts +21 -0
  161. package/src/clients/config-export/kilo.ts +12 -2
  162. package/src/clients/config-export/model-metadata.ts +10 -4
  163. package/src/clients/config-export/reasoning-metadata.ts +47 -0
  164. package/src/clients/config-export.ts +53 -67
  165. package/src/clients/omo-role-models.ts +17 -3
  166. package/src/codex/account-credit-use.ts +4 -2
  167. package/src/codex/account-runtime-state.ts +21 -0
  168. package/src/codex/account-store.ts +11 -47
  169. package/src/codex/account-usability.ts +11 -8
  170. package/src/codex/auth-api/account-list.ts +73 -63
  171. package/src/codex/auth-api/main-account-probe.ts +25 -12
  172. package/src/codex/auth-api/pool-mode-gate.ts +6 -9
  173. package/src/codex/auth-api/pool-quota-probe.ts +2 -4
  174. package/src/codex/auth-api/reset-credit-service.ts +3 -2
  175. package/src/codex/auth-context.ts +30 -11
  176. package/src/codex/catalog/aggregation.ts +3 -0
  177. package/src/codex/catalog/build-entries.ts +1 -1
  178. package/src/codex/catalog/bundled.ts +9 -2
  179. package/src/codex/catalog/heal-observation.ts +66 -0
  180. package/src/codex/catalog/model-hints.ts +2 -1
  181. package/src/codex/catalog/model-visibility.ts +6 -3
  182. package/src/codex/catalog/parsing.ts +2 -0
  183. package/src/codex/catalog/provider-models.ts +13 -4
  184. package/src/codex/catalog/publication-observer.ts +19 -0
  185. package/src/codex/catalog/remote.ts +13 -15
  186. package/src/codex/catalog/restore.ts +19 -6
  187. package/src/codex/catalog/retained-sync.ts +60 -11
  188. package/src/codex/catalog/routed-removal.ts +116 -0
  189. package/src/codex/catalog/write-audit-contract.ts +21 -0
  190. package/src/codex/catalog/write-audit.ts +171 -0
  191. package/src/codex/catalog-self-heal.ts +284 -0
  192. package/src/codex/catalog-write-serialization.ts +71 -7
  193. package/src/codex/chatgpt-refresh-failure.ts +56 -0
  194. package/src/codex/codex-home-owner.ts +124 -0
  195. package/src/codex/convergence.ts +57 -16
  196. package/src/codex/features.ts +8 -1
  197. package/src/codex/inject/restore.ts +46 -4
  198. package/src/codex/inject.ts +11 -1
  199. package/src/codex/internal/catalog-writer.ts +120 -13
  200. package/src/codex/journal.ts +49 -13
  201. package/src/codex/main-account.ts +144 -11
  202. package/src/codex/management-convergence.ts +3 -0
  203. package/src/codex/quota-refresh-outcome.ts +17 -2
  204. package/src/codex/quota-types.ts +36 -1
  205. package/src/codex/quota.ts +67 -16
  206. package/src/codex/refresh.ts +20 -9
  207. package/src/codex/routing/cooldown-math.ts +10 -4
  208. package/src/codex/routing/selection.ts +11 -4
  209. package/src/codex/routing.ts +9 -9
  210. package/src/codex/runtime.ts +7 -1
  211. package/src/codex/shim-diagnostics.ts +80 -0
  212. package/src/codex/shim-fingerprint.ts +7 -2
  213. package/src/codex/shim-migration.ts +153 -0
  214. package/src/codex/shim-overlay.ts +509 -0
  215. package/src/codex/shim-path-resolution.ts +145 -0
  216. package/src/codex/shim-probe.ts +4 -3
  217. package/src/codex/shim-state-file.ts +93 -7
  218. package/src/codex/shim.ts +54 -107
  219. package/src/codex/subagent-model-fallback.ts +2 -1
  220. package/src/codex/sync.ts +24 -4
  221. package/src/combos/failover.ts +101 -1
  222. package/src/combos/index.ts +1 -0
  223. package/src/combos/jev.ts +13 -3
  224. package/src/combos/resolve.ts +12 -0
  225. package/src/config/atomic-write.ts +13 -8
  226. package/src/config/diagnostics.ts +23 -1
  227. package/src/config/load-degrade.ts +7 -0
  228. package/src/config/provider-validation.ts +3 -0
  229. package/src/config/schema/chatgpt-desktop.ts +22 -0
  230. package/src/config/schema/compaction-triggers.ts +8 -0
  231. package/src/config/schema/config-schema.ts +43 -14
  232. package/src/config/schema/leaf-validators.ts +8 -3
  233. package/src/config/subagent-models.ts +12 -0
  234. package/src/generated/compatibility-version.json +645 -237
  235. package/src/grok/session-identity.ts +33 -0
  236. package/src/integrations/catalog-refresh.ts +1 -1
  237. package/src/integrations/merge.ts +13 -5
  238. package/src/integrations/mutation-plan.ts +50 -5
  239. package/src/integrations/omp-yaml-source.ts +181 -1
  240. package/src/integrations/ownership-policy.ts +66 -0
  241. package/src/integrations/registry.ts +50 -1
  242. package/src/integrations/state.ts +106 -19
  243. package/src/integrations/target.ts +25 -2
  244. package/src/integrations/writer.ts +107 -35
  245. package/src/lib/gui-pair-capability.ts +17 -0
  246. package/src/lib/package-tree-integrity.ts +42 -1
  247. package/src/lib/provider-client-headers.ts +62 -0
  248. package/src/lib/proxy-env.ts +3 -0
  249. package/src/lib/test-home-guard.ts +2 -1
  250. package/src/oauth/anthropic-account-refusal.ts +19 -8
  251. package/src/oauth/anthropic-continuity.ts +65 -0
  252. package/src/oauth/anthropic-identity.ts +75 -0
  253. package/src/oauth/anthropic-routing.ts +20 -15
  254. package/src/oauth/anthropic.ts +34 -3
  255. package/src/oauth/generic-account-failover.ts +21 -0
  256. package/src/oauth/health.ts +32 -5
  257. package/src/oauth/index.ts +42 -35
  258. package/src/oauth/local-token-detect.ts +7 -0
  259. package/src/oauth/pool-settings-capability.ts +9 -2
  260. package/src/oauth/refresh-policy.ts +2 -0
  261. package/src/oauth/store.ts +55 -6
  262. package/src/oauth/types.ts +9 -0
  263. package/src/protocols/encoders/messages.ts +4 -3
  264. package/src/protocols/plan-snapshot.ts +1 -1
  265. package/src/protocols/settings.ts +28 -4
  266. package/src/providers/antigravity-effort-families.ts +75 -0
  267. package/src/providers/antigravity-models.ts +54 -16
  268. package/src/providers/antigravity-wire-snapshot.ts +66 -0
  269. package/src/providers/derive.ts +2 -0
  270. package/src/providers/model-rename-fields.ts +3 -0
  271. package/src/providers/new-model-policy.ts +48 -10
  272. package/src/providers/registry/entries-extended.ts +3 -0
  273. package/src/providers/registry/model-ids.ts +2 -0
  274. package/src/providers/registry/model-seeds.ts +5 -0
  275. package/src/providers/registry/types.ts +4 -0
  276. package/src/responses/compaction.ts +19 -0
  277. package/src/router.ts +6 -0
  278. package/src/server/auth-cors.ts +9 -0
  279. package/src/server/caller-session-identity.ts +32 -0
  280. package/src/server/chat-completions.ts +10 -0
  281. package/src/server/claude-messages.ts +29 -15
  282. package/src/server/droid-reasoning-default.ts +50 -0
  283. package/src/server/github-copilot-responses-repair.ts +3 -6
  284. package/src/server/grok-responses-control-frame.ts +2 -2
  285. package/src/server/grok-responses-snapshot-repair.ts +4 -5
  286. package/src/server/gui-session.ts +33 -11
  287. package/src/server/index/claude-intercept-lifecycle.ts +4 -0
  288. package/src/server/index/serve-options.ts +13 -7
  289. package/src/server/index.ts +1 -1
  290. package/src/server/management/agent-settings-routes.ts +12 -134
  291. package/src/server/management/config-routes.ts +1 -1
  292. package/src/server/management/context.ts +8 -1
  293. package/src/server/management/cursor-integration-routes.ts +3 -3
  294. package/src/server/management/integration-routes.ts +69 -11
  295. package/src/server/management/link-routes.ts +20 -13
  296. package/src/server/management/model-routes.ts +3 -3
  297. package/src/server/management/model-row-export-metadata.ts +150 -0
  298. package/src/server/management/model-rows.ts +148 -9
  299. package/src/server/management/native-integration-routes.ts +2 -2
  300. package/src/server/management/oauth-account-routes.ts +96 -30
  301. package/src/server/management/route-registry.ts +29 -27
  302. package/src/server/management/shared.ts +19 -5
  303. package/src/server/management/subagent-model-routes.ts +214 -0
  304. package/src/server/messages-native-eligibility.ts +3 -6
  305. package/src/server/messages-native-oauth.ts +82 -35
  306. package/src/server/messages-native.ts +78 -16
  307. package/src/server/relay.ts +14 -44
  308. package/src/server/request-log-generation-window.ts +38 -0
  309. package/src/server/request-log.ts +13 -0
  310. package/src/server/responses/adapter-dispatch.ts +152 -28
  311. package/src/server/responses/codex-auth-error.ts +10 -2
  312. package/src/server/responses/combo-stream-preflight.ts +29 -3
  313. package/src/server/responses/compaction-routing.ts +10 -1
  314. package/src/server/responses/core-auth.ts +67 -30
  315. package/src/server/responses/core-codex-account.ts +6 -3
  316. package/src/server/responses/core-combo-failure.ts +5 -1
  317. package/src/server/responses/core-combo-native.ts +7 -1
  318. package/src/server/responses/core-combo.ts +64 -7
  319. package/src/server/responses/core-normalize.ts +7 -0
  320. package/src/server/responses/core-options.ts +5 -0
  321. package/src/server/responses/fetch-helpers.ts +9 -0
  322. package/src/server/responses/passthrough-delivery.ts +1 -1
  323. package/src/server/responses/policy-fallback.ts +1 -1
  324. package/src/server/responses/request-prepare.ts +5 -0
  325. package/src/server/responses/request-spend.ts +30 -30
  326. package/src/server/responses-custom-tool-repair.ts +2 -2
  327. package/src/server/responses-hosted-image-display.ts +70 -9
  328. package/src/server/responses-terminal-repair.ts +15 -2
  329. package/src/server/sse-frame-buffer.ts +38 -36
  330. package/src/server/sse-payload-rewrite.ts +78 -49
  331. package/src/server/ws-bridge.ts +1 -11
  332. package/src/service/cli.ts +6 -1
  333. package/src/service/ownership-mutation-lease.d.mts +26 -0
  334. package/src/service/ownership-mutation-lease.mjs +76 -1
  335. package/src/service/windows-taskxml.ts +1 -2
  336. package/src/service-manager-probe.ts +36 -2
  337. package/src/types/config.ts +6 -0
  338. package/src/types/provider.ts +28 -10
  339. package/src/update/index.ts +2 -1
  340. package/src/update/npm-invocation.mjs +35 -3
  341. package/src/update/proxy-liveness-probe.mjs +38 -8
  342. package/src/update/update-failure-guidance.d.mts +6 -0
  343. package/src/update/update-failure-guidance.mjs +30 -5
  344. package/src/usage/expected-prices.ts +9 -0
  345. package/src/usage/log.ts +7 -0
  346. package/src/usage/summary.ts +82 -1
  347. package/src/web-search/loop.ts +11 -8
  348. package/src/web-search/run-turn-loop.ts +10 -7
  349. package/gui/dist/assets/App-CddorHdA.js +0 -51
  350. package/gui/dist/assets/App-DMudtISW.css +0 -1
  351. package/gui/dist/assets/index-Btze1I3T.css +0 -1
  352. package/gui/dist/assets/index-CvnY7E6i.js +0 -86
  353. package/gui/dist/assets/quota-summary-B_AWpHfP.js +0 -1
@@ -1,5 +1,8 @@
1
1
  import { classifyAnthropic429 } from "../../oauth/anthropic-rate-limit-policy";
2
2
  import { rotateAnthropicAccountOnResponse } from "../../oauth/anthropic-account-refusal";
3
+ import { authorizeResendForRecovery } from "../../lib/request-resend-gate";
4
+ import { ambiguousResendAllowanceFor, selfContainedResponsesBody } from "./reset-replay";
5
+ import { transientSendCapFor } from "./request-send-budget";
3
6
  import { isNonReplayableResponse } from "../../lib/upstream-retry";
4
7
  import { isLocalUpstream } from "../../lib/local-upstream";
5
8
  import type { ResponsesRequestContext, ResponsesAdmissionState } from "./core-options";
@@ -25,6 +28,7 @@ import { withProviderRequestSlot } from "../../providers/request-pacing";
25
28
  import { providerFetch, fetchWithHeaderTimeout, safeHostLabel } from "./fetch-helpers";
26
29
  import {
27
30
  transientRetryPolicyFor,
31
+ resetReplayPolicyFor,
28
32
  rateLimitRetryPolicyFor,
29
33
  hasKeyPoolFailover,
30
34
  rotateProviderTransportOn401,
@@ -46,9 +50,10 @@ import type { OpaqueBlobRecoveryGuard } from "./core-opaque-recovery";
46
50
  import type { AttemptRecoveryKind } from "../../usage/log";
47
51
  import type { OAuthAccessSnapshot } from "../../oauth";
48
52
  import { OAuthAccountPausedError, OAuthLoginRequiredError, publicOAuthAuthenticationErrorMessage } from "../../oauth";
49
- import { getAccountSet } from "../../oauth/store";
53
+ import { getAccountSet, markAccountNeedsReauthIfGeneration } from "../../oauth/store";
50
54
  import { tryKiroAlternateAfterTerminalRefresh } from "../../oauth/kiro-terminal-failover";
51
55
  import { classifyKiroRefusal } from "../../adapters/kiro-refusal";
56
+ import { classifyAntigravityRefusal } from "../../adapters/antigravity-refusal";
52
57
  import { normalizeFinalKiroHttpError } from "../../adapters/kiro-retry";
53
58
  import { noteKiroMonthlyRefusal } from "../../providers/kiro-usage";
54
59
  import { persistKiroAccountState } from "../../providers/kiro-account-state-disk";
@@ -155,6 +160,8 @@ export async function prepareAdapterExchange(
155
160
  | "reserveCredentialHop"
156
161
  | "pendingHopPermit"
157
162
  | "workflowRootId"
163
+ | "claimAmbiguousResend"
164
+ | "sendsUsed"
158
165
  >,
159
166
  ) {
160
167
  const { options, config, logCtx, req } = requestContext;
@@ -188,9 +195,18 @@ export async function prepareAdapterExchange(
188
195
  recoveryClassFor,
189
196
  sendBudgetExhausted,
190
197
  reserveCredentialHop,
198
+ claimAmbiguousResend,
191
199
  } = sendBudgetState;
192
200
 
193
201
 
202
+ let selfContainedJudgment: boolean | undefined;
203
+ const requestIsSelfContained = (): boolean =>
204
+ selfContainedJudgment ??= selfContainedResponsesBody(parsed._rawBody);
205
+ const claimPreHeaderResend = (): boolean => authorizeResendForRecovery(
206
+ "pre-header", "connection-reset",
207
+ ambiguousResendAllowanceFor(route.provider, requestIsSelfContained, claimAmbiguousResend),
208
+ ).allowed;
209
+
194
210
  const upstream = new AbortController();
195
211
  const cleanupUpstreamAbort = linkAbortSignal(upstream, options.abortSignal);
196
212
  const connectMs = config.connectTimeoutMs ?? 200_000;
@@ -342,6 +358,13 @@ export async function prepareAdapterExchange(
342
358
  const compactPrepaid = options.compactionRecoveryAttempted ? sendBudgetState.pendingHopPermit : undefined;
343
359
  if (compactPrepaid) sendBudgetState.pendingHopPermit = undefined;
344
360
  let compactPrepaidUsed = false;
361
+ // Combo and emergency compaction admission already book this target's first send.
362
+ // Its configured initial ceiling is target-local; the shared remainder below still
363
+ // accounts for earlier targets without deducting their sends from this target twice.
364
+ const initialSendCap = transientPolicy || resetReplayPolicyFor(route.provider)
365
+ ? transientSendCapFor(transientPolicy?.attempts,
366
+ (options.comboAttempt || compactPrepaid) ? 0 : sendBudgetState.sendsUsed)
367
+ : 1;
345
368
  const fetchWithRetryPolicy = (route.provider.adapter === "google" || transientPolicy)
346
369
  ? fetchWithTransientRetry
347
370
  : fetchWithResetRetry;
@@ -366,15 +389,12 @@ export async function prepareAdapterExchange(
366
389
  {
367
390
  abortSignal: upstream.signal,
368
391
  label: safeHostLabel(builtInitialRequest.url),
369
- ...(transientPolicy || compactPrepaid
370
- // Draws the remainder, not the raw policy. A combo child inherits the parent's
371
- // holder but used to take a fresh full allowance on its own first send, so the
372
- // shared counter was inherited without ever being read as a limit.
392
+ claimAmbiguousResend: claimPreHeaderResend,
393
+ ...(transientPolicy || resetReplayPolicyFor(route.provider) || compactPrepaid
373
394
  ? {
374
- // The first emergency send is already paid for. Only retries consume the
375
- // remaining allowance; treating the booking as unavailable blocks a cap of two.
376
- attempts: Math.min(transientPolicy?.attempts ?? 1,
377
- remainingTransientSendBudget(transientPolicy?.attempts ?? 1) + (compactPrepaid ? 1 : 0)),
395
+ // A pending compaction permit already booked this leg's first physical send.
396
+ attempts: Math.min(initialSendCap,
397
+ remainingTransientSendBudget(initialSendCap) + (compactPrepaid ? 1 : 0)),
378
398
  onSendsConsumed: noteTransientSends,
379
399
  }
380
400
  : {}),
@@ -544,14 +564,21 @@ export async function prepareAdapterExchange(
544
564
  const refetchWithPolicy = (route.provider.adapter === "google" || refetchTransientPolicy)
545
565
  ? fetchWithTransientRetry
546
566
  : fetchWithResetRetry;
547
- // Same rule as the passthrough rebuild: spend the base allowance first, then the one
548
- // shared final-recovery reserve, so a recovery that follows a spent streak still gets
549
- // its single send instead of dying at three.
550
- const refetchAllowance = refetchTransientPolicy
551
- ? recoverySendAllowance(
552
- refetchTransientPolicy.attempts,
553
- recoveryClassFor(recovery),
567
+ const helperCountsSends = refetchTransientPolicy !== null || resetReplayPolicyFor(route.provider) !== null;
568
+ const prepaid = sendBudgetState.pendingHopPermit;
569
+ const configuredTotal = refetchTransientPolicy?.attempts;
570
+ const refetchCap = transientSendCapFor(configuredTotal,
571
+ sendBudgetState.sendsUsed - (prepaid ? 1 : 0));
572
+ // An exact total includes a booked hop, even when it consumed the last base slot.
573
+ // Keep that funded send while forbidding the final reserve from widening the total.
574
+ const prepaidLastSlot = helperCountsSends && configuredTotal !== undefined && prepaid
575
+ && refetchCap > 0 && remainingTransientSendBudget(refetchCap) === 0;
576
+ if (prepaidLastSlot) sendBudgetState.pendingHopPermit = undefined;
577
+ const refetchAllowance = helperCountsSends
578
+ ? prepaidLastSlot ? { attempts: 1, permit: prepaid } : recoverySendAllowance(
579
+ refetchCap, recoveryClassFor(recovery),
554
580
  `${route.providerName}|${route.modelId}|${recovery}`,
581
+ { allowFinalRecoveryReserve: configuredTotal === undefined },
555
582
  )
556
583
  : undefined;
557
584
  try {
@@ -565,7 +592,7 @@ export async function prepareAdapterExchange(
565
592
  // can be refused above before it does. use() past the first attempt is a no-op.
566
593
  onDispatch?.();
567
594
  if (preserveFailureResponse) replacementAdmitted = true;
568
- if (!refetchTransientPolicy) chargeFastDowngradeWorkflowSend();
595
+ if (!refetchAllowance) chargeFastDowngradeWorkflowSend();
569
596
  return fetchWithHeaderTimeout(retryRequest.url,
570
597
  applyUpstreamRecoveryInit({
571
598
  method: retryRequest.method, headers: retryRequest.headers, body: retryRequest.body,
@@ -579,6 +606,7 @@ export async function prepareAdapterExchange(
579
606
  {
580
607
  abortSignal: upstream.signal,
581
608
  label: safeHostLabel(retryRequest.url),
609
+ claimAmbiguousResend: claimPreHeaderResend,
582
610
  ...(refetchAllowance
583
611
  ? {
584
612
  attempts: refetchAllowance.attempts,
@@ -637,7 +665,7 @@ export async function prepareAdapterExchange(
637
665
  const adapterOwnsDispatch = transportState.activeAdapter.fetchResponse !== undefined;
638
666
  const hop = reserveCredentialHop("auth-recovery",
639
667
  `${route.providerName}|${route.modelId}|antigravity-auth`,
640
- !adapterOwnsDispatch && transientRetryPolicyFor(route.provider) !== null);
668
+ !adapterOwnsDispatch && (transientRetryPolicyFor(route.provider) !== null || resetReplayPolicyFor(route.provider) !== null));
641
669
  if (!hop.allowed) return null;
642
670
  try {
643
671
  const snapshot = await failoverAccountSnapshot(route.providerName, nextId);
@@ -707,7 +735,7 @@ export async function prepareAdapterExchange(
707
735
  const adapterOwnsDispatch = transportState.activeAdapter.fetchResponse !== undefined;
708
736
  const hop = reserveCredentialHop("auth-recovery",
709
737
  `${route.providerName}|${route.modelId}|terminal-refresh-account`,
710
- !adapterOwnsDispatch && transientRetryPolicyFor(route.provider) !== null);
738
+ !adapterOwnsDispatch && (transientRetryPolicyFor(route.provider) !== null || resetReplayPolicyFor(route.provider) !== null));
711
739
  if (hop.allowed) {
712
740
  try {
713
741
  const admitted = await applyFailoverSnapshot(alternate);
@@ -860,7 +888,7 @@ export async function prepareAdapterExchange(
860
888
  const hop = reserveCredentialHop(
861
889
  "repair",
862
890
  `${route.providerName}|${route.modelId}|anthropic-fast-downgrade`,
863
- !adapterOwnsDispatch && transientRetryPolicyFor(route.provider) !== null,
891
+ !adapterOwnsDispatch && (transientRetryPolicyFor(route.provider) !== null || resetReplayPolicyFor(route.provider) !== null),
864
892
  );
865
893
  if (hop.allowed) {
866
894
  anthropicFastDowngradeGuard.attempted = true;
@@ -1058,10 +1086,9 @@ export async function prepareAdapterExchange(
1058
1086
  const hop = reserveCredentialHop(
1059
1087
  "auth-recovery",
1060
1088
  `${route.providerName}|${route.modelId}|adapter-recovery-oauth-429`,
1061
- // Only a helper-routed replay reports this send back. A reset-only refetch reports
1062
- // nothing and an adapter ladder settles the booking itself, so promising an external
1063
- // report on either would leave a booking pending until it swallowed a later charge.
1064
- !adapterOwnsDispatch && transientRetryPolicyFor(route.provider) !== null,
1089
+ // Transient and opted-in reset helpers report physical sends; an adapter-owned
1090
+ // ladder settles its own booking.
1091
+ !adapterOwnsDispatch && (transientRetryPolicyFor(route.provider) !== null || resetReplayPolicyFor(route.provider) !== null),
1065
1092
  );
1066
1093
  if (!hop.allowed) break;
1067
1094
  const nextAccountId = rotateGenericOAuthAccountOnRefusal(
@@ -1163,10 +1190,9 @@ export async function prepareAdapterExchange(
1163
1190
  const hop = reserveCredentialHop(
1164
1191
  "auth-recovery",
1165
1192
  `${route.providerName}|${route.modelId}|adapter-recovery-oauth-429`,
1166
- // Only a helper-routed replay reports this send back. A reset-only refetch reports
1167
- // nothing and an adapter ladder settles the booking itself, so promising an external
1168
- // report on either would leave a booking pending until it swallowed a later charge.
1169
- !adapterOwnsDispatch && transientRetryPolicyFor(route.provider) !== null,
1193
+ // Transient and opted-in reset helpers report physical sends; an adapter-owned
1194
+ // ladder settles its own booking.
1195
+ !adapterOwnsDispatch && (transientRetryPolicyFor(route.provider) !== null || resetReplayPolicyFor(route.provider) !== null),
1170
1196
  );
1171
1197
  if (!hop.allowed) break;
1172
1198
  const nextAccountId = rotateGenericOAuthAccountOn429(
@@ -1243,6 +1269,104 @@ export async function prepareAdapterExchange(
1243
1269
  break;
1244
1270
  }
1245
1271
  }
1272
+ // Antigravity verify-account quarantine: a 403 demanding account verification
1273
+ // is terminal for THAT account — its grant still refreshes, so only a manual
1274
+ // verification followed by a re-login clears it. Mark it needsReauth (durable,
1275
+ // shown as `needs-reauth(verify)` in `ocx account list` with a `verify_account`
1276
+ // health reason in the management API, excluded from the pool until re-login)
1277
+ // and replay the same request on the next eligible account so the pool keeps
1278
+ // serving.
1279
+ while (
1280
+ upstreamResponse.status === 403
1281
+ && route.providerName === "google-antigravity"
1282
+ && transportState.genericFailoverAccountId
1283
+ ) {
1284
+ const refusal = classifyAntigravityRefusal(upstreamResponse.status,
1285
+ await readDisplaySafeErrorText(upstreamResponse.clone(), upstream.signal, ""));
1286
+ if (refusal.kind === "other") break;
1287
+ const sent = transportState.sentOAuthSnapshot;
1288
+ const failedAccountId = transportState.genericFailoverAccountId;
1289
+ // Fenced write: a 403 arriving after the credential rotated (refresh or
1290
+ // re-login) must not quarantine the new login. Without the matching sent
1291
+ // snapshot there is no owner for this write, so skip marking.
1292
+ if (!sent || sent.accountId !== failedAccountId) break;
1293
+ try {
1294
+ await markAccountNeedsReauthIfGeneration(route.providerName, failedAccountId, sent.generation, undefined, "verify_account");
1295
+ } catch {
1296
+ // A failed quarantine write must preserve the refusal, not authorize another send.
1297
+ break recovery;
1298
+ }
1299
+ if (!antigravityPoolActivated || transportState.genericFailovers >= transportState.genericFailoverLimit) break;
1300
+ const adapterOwnsDispatch = transportState.activeAdapter.fetchResponse !== undefined;
1301
+ const hop = reserveCredentialHop(
1302
+ "auth-recovery",
1303
+ `${route.providerName}|${route.modelId}|adapter-recovery-oauth-verify`,
1304
+ !adapterOwnsDispatch && (transientRetryPolicyFor(route.provider) !== null || resetReplayPolicyFor(route.provider) !== null),
1305
+ );
1306
+ if (!hop.allowed) break;
1307
+ // Auth-refusal rotation, not the rate-limit one: a verification refusal
1308
+ // must not record rate-limit cooldown semantics against the account.
1309
+ const nextAccountId = rotateAntigravityAccountOnAuthRefusal(
1310
+ antigravityPoolActivated,
1311
+ failedAccountId,
1312
+ sent.generation,
1313
+ route.modelId,
1314
+ );
1315
+ if (!nextAccountId) {
1316
+ hop.permit?.release();
1317
+ break;
1318
+ }
1319
+ // The refusal body stays alive until the replacement owns the outcome: it
1320
+ // is also the preserveFailureResponse below, so cancelling it early would
1321
+ // hand back a 403 with a destroyed body when the rebuild is refused.
1322
+ const failedResponse = upstreamResponse;
1323
+ try {
1324
+ // The FULL snapshot, not just the bearer: Antigravity pairs an
1325
+ // account-matched projectId with its token, so a token-only swap would mix
1326
+ // one account's credential with another's routing data.
1327
+ const snapshot = await failoverAccountSnapshot(route.providerName, nextAccountId);
1328
+ if (!await applyFailoverSnapshot(snapshot)) {
1329
+ hop.permit?.release();
1330
+ break;
1331
+ }
1332
+ invalidateSameTargetRequest();
1333
+ transportState.activeAdapter = resolveSelectionAdapter(
1334
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
1335
+ config.cacheRetention,
1336
+ );
1337
+ bindRouteReasoningReplayScope({
1338
+ parsed,
1339
+ providerName: route.providerName,
1340
+ provider: route.provider,
1341
+ adapterName: transportState.activeAdapter.name,
1342
+ oauthCredentialSnapshot: transportState.replayOAuthCredentialSnapshot,
1343
+ });
1344
+ sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, transportState.activeAdapter.name, logCtx.accountLogLabel);
1345
+ recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, transportState.activeAdapter.name);
1346
+ sendBudgetState.pendingHopPermit = hop.permit;
1347
+ let result: Response | { failed: Response };
1348
+ try {
1349
+ result = await rebuildAndRefetch("oauth-account-403", () => {
1350
+ if (!adapterOwnsDispatch) hop.permit?.use();
1351
+ }, failedResponse);
1352
+ } finally {
1353
+ sendBudgetState.pendingHopPermit = undefined;
1354
+ }
1355
+ if ("failed" in result) {
1356
+ hop.permit?.release();
1357
+ if (result.failed !== failedResponse) return result.failed;
1358
+ // Preserve common redaction and combo consumption without another recovery send.
1359
+ break recovery;
1360
+ }
1361
+ transportState.genericFailovers += 1;
1362
+ try { void failedResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
1363
+ upstreamResponse = result;
1364
+ if (isNonReplayableResponse(upstreamResponse)) continue recovery;
1365
+ } catch {
1366
+ hop.permit?.release();
1367
+ break;
1368
+ }
1369
+ }
1246
1370
  }
1247
1371
  // Unknown provenance is deliberately fail-soft in pre-flight: after a restart, TTL expiry,
1248
1372
  // or LRU eviction, a valid same-backend blob must survive. A decoder's own 4xx identity is
@@ -30,9 +30,15 @@ export function codexModelAvailabilityErrorResponse(error: CodexModelAvailabilit
30
30
  return formatErrorResponse(400, "invalid_request_error", error.message);
31
31
  }
32
32
 
33
+ /** Actionable sign-in guidance without guessing which upstream revocation cause occurred. */
34
+ export const CODEX_MAIN_SIGN_IN_REQUIRED_MESSAGE =
35
+ "Codex main account needs sign-in. Run `codex login` to sign in again.";
36
+
33
37
  export function nativeMainRefreshFailureResponse(error: unknown): Response {
34
38
  if (error instanceof MainAccountTokenRefreshError && error.reason === "reauth") {
35
- return formatErrorResponse(401, "authentication_error", "Codex main account needs reauthentication");
39
+ // Same sentence the quarantine produces on every later request, so the first refusal and the
40
+ // ones after it do not describe one dead credential two different ways.
41
+ return formatErrorResponse(401, "authentication_error", CODEX_MAIN_SIGN_IN_REQUIRED_MESSAGE);
36
42
  }
37
43
  if (error instanceof MainAccountTokenRefreshError
38
44
  || error instanceof MainAuthJsonChangedDuringRefreshError
@@ -98,7 +104,9 @@ export function mapCodexAuthContextErrorToResponse(
98
104
  return codexModelAvailabilityErrorResponse(error);
99
105
  }
100
106
  if (error instanceof CodexPoolAuthenticationError || error instanceof CodexDirectAuthenticationError) {
101
- return formatErrorResponse(401, "authentication_error", error.message);
107
+ return formatErrorResponse(401, "authentication_error",
108
+ error instanceof CodexPoolAuthenticationError && error.quarantinedMain
109
+ ? CODEX_MAIN_SIGN_IN_REQUIRED_MESSAGE : error.message);
102
110
  }
103
111
  if (error instanceof CodexMainSubstitutionUnavailableError) {
104
112
  return formatErrorResponse(
@@ -1,3 +1,4 @@
1
+ import { codexAccountModelRefusal, codexAccountModelRefusalPayload, hasConflictingCodexModelRefusalEnvelopes, type CodexAccountModelRefusal } from "../../combos/failover";
1
2
  import type { ResponsesTerminalStatus } from "../../bridge";
2
3
  import { comboFailureDecision } from "../../combos";
3
4
  import { httpStatusFromTerminalError } from "../../lib/errors";
@@ -68,6 +69,26 @@ function bareErrorStatus(payload: unknown): number | undefined {
68
69
  });
69
70
  }
70
71
 
72
+ /** Match the selected SSE error envelope before projection drops competing fields. */
73
+ function terminalModelRefusal(status: number, event: Record<string, unknown>): CodexAccountModelRefusal {
74
+ if (hasConflictingCodexModelRefusalEnvelopes(status, event)) return "ambiguous";
75
+ const outer = codexAccountModelRefusalPayload(status, event);
76
+ if (outer === "ambiguous") return outer;
77
+ const nested = event.response;
78
+ if (event.type !== "error" && nested && typeof nested === "object" && !Array.isArray(nested)) {
79
+ const nestedKind = codexAccountModelRefusalPayload(status, nested);
80
+ if (nestedKind === "ambiguous") return nestedKind;
81
+ const nestedError = (nested as Record<string, unknown>).error;
82
+ if (nestedError && typeof nestedError === "object" && !Array.isArray(nestedError)) {
83
+ return nestedKind;
84
+ }
85
+ }
86
+ if (event.type === "error" && !Object.hasOwn(event, "error") && typeof event.message === "string") {
87
+ return codexAccountModelRefusal(status, event.message, { allowNestedResponse: false });
88
+ }
89
+ return outer;
90
+ }
91
+
71
92
  function bareErrorIsRetryable(payload: unknown): boolean {
72
93
  const status = bareErrorStatus(payload);
73
94
  if (status === undefined || !payload || typeof payload !== "object" || Array.isArray(payload)) {
@@ -84,7 +105,9 @@ function bareErrorIsRetryable(payload: unknown): boolean {
84
105
  const message = typeof error.message === "string"
85
106
  ? error.message
86
107
  : typeof event.message === "string" ? event.message : "";
87
- return comboFailureDecision(status, message, { code }) === "hop";
108
+ return comboFailureDecision(status, message, {
109
+ code, codexModelRefusal: terminalModelRefusal(status, event),
110
+ }) === "hop";
88
111
  }
89
112
 
90
113
  function retryableZeroOutputTerminal(payload: unknown): boolean {
@@ -226,7 +249,7 @@ function failedTerminalResponse(
226
249
 
227
250
  export type ComboStreamPreflightResult =
228
251
  | { kind: "accepted"; response: Response }
229
- | { kind: "failed"; response: Response }
252
+ | { kind: "failed"; response: Response; codexModelRefusal?: CodexAccountModelRefusal }
230
253
  /**
231
254
  * The body errored mid-stream and `replayReadErrors` asked for the prefix back rather than
232
255
  * a rethrow. `stage` is how far the inspection actually got; whether that permits a
@@ -322,7 +345,10 @@ export async function preflightComboStreamResponse(
322
345
  || retryableTerminalPayload?.type === "error")
323
346
  && !outputCommitted && retryableTerminalPayload) {
324
347
  await reader.cancel("retrying zero-output combo stream terminal").catch(() => undefined);
325
- return { kind: "failed", response: failedTerminalResponse(response, retryableTerminalPayload, logCtx) };
348
+ const failed = failedTerminalResponse(response, retryableTerminalPayload, logCtx);
349
+ return { kind: "failed", response: failed,
350
+ ...(failed.status === 400
351
+ ? { codexModelRefusal: terminalModelRefusal(failed.status, retryableTerminalPayload) } : {}) };
326
352
  }
327
353
  if (next.done || terminalStatus !== undefined || outputCommitted
328
354
  || bufferedBytes >= COMBO_STREAM_PREFLIGHT_MAX_BYTES
@@ -1,6 +1,6 @@
1
1
  import type { OcxConfig } from "../../types";
2
2
  import { isDeclaredReasoningEffort } from "../../reasoning-effort";
3
- import { COMPACTION_TRIGGERS } from "../../config/schema/compaction-triggers";
3
+ import { COMPACTION_TRIGGERS, validCompactionSourceModels } from "../../config/schema/compaction-triggers";
4
4
  import { routeConcreteModel, type RouteResult } from "../../router";
5
5
  import { resolveComboId } from "../../combos/identifiers";
6
6
  import { resolvePolicyProfileId } from "../../routing/profile";
@@ -57,6 +57,15 @@ export function applyCompactionRoutingOverride(
57
57
  && (typeof override.reasoningEffort !== "string" || !isDeclaredReasoningEffort(override.reasoningEffort))) return null;
58
58
  const triggers = configuredTriggers(override.triggers);
59
59
  if (!triggers) return null;
60
+ if (override.sourceModels !== undefined) {
61
+ if (!validCompactionSourceModels(override.sourceModels)) return null;
62
+ // Match the selector the conversation actually routed on: a synthetic `--fast` or
63
+ // effort suffix is ingress decoration, so `cheap--fast` is still the allowlisted `cheap`.
64
+ const source = sourceSelectorOf(config, raw.model);
65
+ if (!override.sourceModels.some(selector => selector.endsWith("/*")
66
+ ? source.startsWith(selector.slice(0, -1)) && source.length > selector.length - 1
67
+ : source === selector)) return null;
68
+ }
60
69
  if (options.endpoint !== "compact"
61
70
  && !(Array.isArray(raw.input) && raw.input.some(item => record(item)?.type === "compaction_trigger"))) return null;
62
71
 
@@ -41,13 +41,19 @@ import { markLocalRequestLogRefusal } from "../request-log";
41
41
  import { CODEX_POOL_REFRESH_INCOMPLETE_LOG_REASON } from "../../codex/pool-refresh-backoff";
42
42
  import { codexAuthContextLogLabel } from "../../codex/account-label";
43
43
  import { forceRefreshMainAccountToken } from "../../codex/main-account";
44
+ import { safeCallerSessionId } from "../caller-session-identity";
44
45
 
45
- /** Keep synthesized Claude identity out of request headers reused by policy/combo fallback. */
46
+ /** Normalize caller aliases only on native egress; preserve ingress and fallback identity. */
46
47
  export function withClaudeNativeSession(headers: Headers, provider: OcxProviderConfig, sessionId?: string): Headers {
47
- if (!sessionId || !isCanonicalOpenAiForwardProvider(provider)
48
- || headers.has("session_id") || headers.has("session-id") || headers.has("thread-id")) return headers;
48
+ // Empty values count as absent, matching materializeCodexUpstreamAuth, so first attempts and
49
+ // retries rebuilt from caller headers pick the same identity.
50
+ if (!isCanonicalOpenAiForwardProvider(provider) || headers.get("session_id")) return headers;
51
+ const alias = ["session-id", "thread-id"].find(name => headers.get(name));
52
+ const nativeSession = alias ? safeCallerSessionId(headers.get(alias)) : sessionId;
53
+ // A non-empty invalid alias suppresses weaker metadata identity.
54
+ if (!nativeSession) return headers;
49
55
  const forwarded = new Headers(headers);
50
- forwarded.set("session_id", sessionId);
56
+ forwarded.set("session_id", nativeSession);
51
57
  return forwarded;
52
58
  }
53
59
 
@@ -57,6 +63,9 @@ export type ResponsesAuthResolution =
57
63
  | { ok: false; response: Response };
58
64
 
59
65
 
66
+ export type CodexCredentialOwnershipOptions = Pick<HandleResponsesOptions,
67
+ "admission" | "callerDirectAuth" | "stripClaudeMainAuthForNoncanonicalForward" | "trustedClaudeMainAuth">;
68
+
60
69
  /**
61
70
  * The caller credential the final Codex auth resolution will be given, as far as the ROUTE
62
71
  * decides it: a route change that may cross a credential domain drops the raw caller credential,
@@ -67,15 +76,53 @@ export type ResponsesAuthResolution =
67
76
  * an HMAC of exactly this Authorization header. Two copies of this rule would put preview and
68
77
  * final auth in different scopes the first time one of them changed.
69
78
  */
79
+ /**
80
+ * The stored native-main credential this process injected itself, when it is the bearer the
81
+ * resolution will see.
82
+ *
83
+ * `handleClaudeMessages` reads `~/.codex/auth.json` and attaches it so OpenAI-backed sidecars stay
84
+ * reachable on a translated turn. It is OUR credential, not the caller's -- the ingress says so
85
+ * where it sets `nativeCallerAuth` and `callerDirectAuth` to null for exactly this reason -- and
86
+ * every reader of the resolution's input headers has to agree about that, or one of them conclud
87
+ * the request owns a credential that in fact belongs to the Pool.
88
+ */
89
+ export function injectedClaudeMainBearer(
90
+ route: Pick<RouteResult, "provider">,
91
+ options: CodexCredentialOwnershipOptions,
92
+ ): { authorization: string; chatgptAccountId?: string } | undefined {
93
+ return options.stripClaudeMainAuthForNoncanonicalForward === true
94
+ && isCanonicalOpenAiForwardProvider(route.provider)
95
+ ? options.trustedClaudeMainAuth : undefined;
96
+ }
97
+
98
+ function validatedCallerDirectHeaders(
99
+ config: OcxConfig,
100
+ route: Pick<RouteResult, "provider">,
101
+ options: CodexCredentialOwnershipOptions,
102
+ ): Headers | undefined {
103
+ if (!options.callerDirectAuth || !isCanonicalOpenAiForwardProvider(route.provider)) return undefined;
104
+ const headers = new Headers({ authorization: options.callerDirectAuth.authorization });
105
+ if (options.callerDirectAuth.chatgptAccountId) headers.set("chatgpt-account-id", options.callerDirectAuth.chatgptAccountId);
106
+ return captureCallerDirectAuth(headers, config) ? headers : undefined;
107
+ }
108
+
70
109
  export function codexRouteCredentialDomainHeaders(
71
110
  req: Request,
72
111
  route: RouteResult,
73
112
  options: HandleResponsesOptions,
74
113
  credentialDomainWasRewritten: boolean,
114
+ config: OcxConfig,
75
115
  ): Headers {
76
- const trustedClaudeMainForFinalRoute = options.stripClaudeMainAuthForNoncanonicalForward === true
77
- && isCanonicalOpenAiForwardProvider(route.provider)
78
- ? options.trustedClaudeMainAuth : undefined;
116
+ const callerDirect = validatedCallerDirectHeaders(config, route, options);
117
+ if (callerDirect) {
118
+ const headers = new Headers(req.headers);
119
+ headers.set("authorization", callerDirect.get("authorization")!);
120
+ const accountId = callerDirect.get("chatgpt-account-id");
121
+ if (accountId) headers.set("chatgpt-account-id", accountId);
122
+ else headers.delete("chatgpt-account-id");
123
+ return headers;
124
+ }
125
+ const trustedClaudeMainForFinalRoute = injectedClaudeMainBearer(route, options);
79
126
  if (trustedClaudeMainForFinalRoute) {
80
127
  const claudeMainHeaders = new Headers(req.headers);
81
128
  claudeMainHeaders.set("authorization", trustedClaudeMainForFinalRoute.authorization);
@@ -117,15 +164,25 @@ export function codexRouteCredentialDomainHeaders(
117
164
  export function codexRouteCredentialOwnership(
118
165
  authInputHeaders: Headers,
119
166
  config: OcxConfig,
120
- route: RouteResult,
121
- options: HandleResponsesOptions,
167
+ route: Pick<RouteResult, "provider" | "codexAccountMode">,
168
+ options: CodexCredentialOwnershipOptions,
122
169
  ): { substituteMainCredential: boolean; requestScopedMainCredential: boolean } {
123
170
  const substituteMainCredential = options.admission?.source === "bearer"
124
171
  && (route.codexAccountMode !== undefined || isCanonicalOpenAiForwardProvider(route.provider));
125
172
  return {
126
173
  substituteMainCredential,
174
+ // A request-scoped main credential is one the CALLER owns: it exists for this request only,
175
+ // owns no Pool state, and must never be quarantined or refreshed on the Pool's behalf. The
176
+ // stored credential this process injected is the opposite of that in every respect, and
177
+ // `hasForwardableCodexBearer` cannot tell them apart -- it sees a Codex JWT with an account
178
+ // id either way. Counting ours as the caller's made the native main slot caller-owned on
179
+ // every translated Claude turn: `main` instead of `main-pool`, so an upstream 401 on a
180
+ // revoked session recorded no outcome, refreshed nothing, and retired nothing, and the next
181
+ // request sent the same dead token again.
127
182
  requestScopedMainCredential: route.codexAccountMode !== undefined
128
183
  && !substituteMainCredential
184
+ && (validatedCallerDirectHeaders(config, route, options) !== undefined
185
+ || injectedClaudeMainBearer(route, options) === undefined)
129
186
  && hasForwardableCodexBearer(authInputHeaders, config),
130
187
  };
131
188
  }
@@ -149,6 +206,7 @@ export async function resolveResponsesCodexAuth(
149
206
  route,
150
207
  options,
151
208
  credentialDomainWasRewritten,
209
+ config,
152
210
  );
153
211
  // A caller-auth transport that is not canonical OpenAI (keyless Cursor) consumes the
154
212
  // caller's Authorization as its own upstream token. Keep that contract only for a clean
@@ -171,27 +229,6 @@ export async function resolveResponsesCodexAuth(
171
229
  authInputHeaders = scoped;
172
230
  }
173
231
  }
174
- // The caller's own Direct credential may cross an internal route change only to the
175
- // canonical OpenAI transport, under a predicate deliberately STRICTER than plain
176
- // unchanged-route Direct forwarding: a clean non-proxy bearer whose ChatGPT-domain
177
- // marker is valid, with any explicit account header matching that marker. Unchanged
178
- // routes keep their legacy rules; sidecar enrichment grants no primary authority.
179
- if (options.callerDirectAuth && isCanonicalOpenAiForwardProvider(route.provider)) {
180
- const directHeaders = new Headers({
181
- authorization: options.callerDirectAuth.authorization,
182
- ...(options.callerDirectAuth.chatgptAccountId
183
- ? { "chatgpt-account-id": options.callerDirectAuth.chatgptAccountId } : {}),
184
- });
185
- if (captureCallerDirectAuth(directHeaders, config)) {
186
- authInputHeaders = new Headers(authInputHeaders);
187
- authInputHeaders.set("authorization", options.callerDirectAuth.authorization);
188
- if (options.callerDirectAuth.chatgptAccountId) {
189
- authInputHeaders.set("chatgpt-account-id", options.callerDirectAuth.chatgptAccountId);
190
- } else {
191
- authInputHeaders.delete("chatgpt-account-id");
192
- }
193
- }
194
- }
195
232
  // #1686: a caller that proved admission with a BEARER presented one of our own secrets.
196
233
  // Refusing it here is what made the codex-cli `env_key` contract unusable against Direct.
197
234
  // Admitting it is only safe because the stored main credential is substituted below, so
@@ -59,7 +59,7 @@ import {
59
59
  import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "../../codex/catalog/native-models";
60
60
  import { isRequestExecutionBudget } from "../../lib/request-execution-budget";
61
61
  import type { SingleUseDispatchPermit } from "../../lib/request-execution-budget";
62
- import { hasForwardableCodexBearer } from "../auth-cors";
62
+ import { codexRouteCredentialOwnership, type CodexCredentialOwnershipOptions } from "./core-auth";
63
63
  import { bindRouteReasoningReplayScope } from "./core-replay";
64
64
  import {
65
65
  conversationStateBindingFromAuth,
@@ -360,7 +360,7 @@ export interface CodexPoolAccountRetryArgs {
360
360
  route: Pick<RouteResult, "providerName" | "modelId" | "provider" | "staticPolicy">;
361
361
  parsed: OcxParsedRequest;
362
362
  logCtx: RequestLogContext;
363
- options: {
363
+ options: CodexCredentialOwnershipOptions & {
364
364
  admission?: DataPlaneAdmission;
365
365
  codexAuthPolicy?: CodexAuthPolicyConfig;
366
366
  visionDescribeTerminal?: boolean;
@@ -641,10 +641,13 @@ export async function retryCodexPoolOnAlternateAccount(
641
641
  "pool",
642
642
  {
643
643
  excludeAccountId: firstAuthCtx.accountId,
644
+ signal: options.abortSignal,
644
645
  admission: options.admission,
645
646
  codexAuthPolicy: options.codexAuthPolicy,
646
647
  modelId: route.modelId,
647
- requestScopedMainCredential: hasForwardableCodexBearer(callerAuthHeaders, config),
648
+ requestScopedMainCredential: codexRouteCredentialOwnership(callerAuthHeaders, config, {
649
+ provider: route.provider, codexAccountMode: "pool",
650
+ }, options).requestScopedMainCredential,
648
651
  beginCodexAccountSelection: codexAccountSelectionForTurn(options.turnAdmissionLease),
649
652
  resolveCodexModelEntitlements: entitlementResolver,
650
653
  },
@@ -1,3 +1,4 @@
1
+ import { codexAccountModelRefusal, codexAccountModelRefusalHardStopCode, type CodexAccountModelRefusal } from "../../combos/failover";
1
2
  import { parseRetryAfterMs } from "../../combos";
2
3
  import type { ConsumedComboFailure, HandleResponsesOptions } from "./core-options";
3
4
  import type { OcxUsage } from "../../types";
@@ -36,6 +37,7 @@ export async function consumeComboFailure(
36
37
  const nonReplayable = isNonReplayableResponse(response);
37
38
  const fallback = `Provider error ${response.status}`;
38
39
  let classificationText = fallback;
40
+ let codexModelRefusal: CodexAccountModelRefusal | undefined;
39
41
  let usage: OcxUsage | undefined;
40
42
  let upstreamCode: string | undefined;
41
43
  let upstreamMessage: string | undefined;
@@ -60,10 +62,11 @@ export async function consumeComboFailure(
60
62
  && isRateLimitOrQuotaFailureMessage(quotaMessage);
61
63
  }
62
64
  if (body.displaySafe && !body.truncated) {
65
+ if (response.status === 400) codexModelRefusal = codexAccountModelRefusal(response.status, body.text);
63
66
  const normalized = normalizeUpstreamErrorText(body.text, fallback);
64
67
  if (utf8Trusted || isCyberPolicyCode(normalized.code) || isCyberPolicyMessage(normalized.safeText)) {
65
68
  classificationText = normalized.safeText;
66
- upstreamCode = normalized.code;
69
+ upstreamCode = codexAccountModelRefusalHardStopCode(response.status, body.text) ?? normalized.code;
67
70
  upstreamMessage = normalized.message;
68
71
  upstreamType = normalized.type;
69
72
  }
@@ -116,6 +119,7 @@ export async function consumeComboFailure(
116
119
  response: failureResponse,
117
120
  ...(nonReplayable ? { nonReplayable: true } : {}),
118
121
  classificationText,
122
+ ...(codexModelRefusal !== undefined ? { codexModelRefusal } : {}),
119
123
  ...(normalizedUpstreamCode !== undefined ? { upstreamCode: normalizedUpstreamCode } : {}),
120
124
  ...(upstreamType !== undefined ? { upstreamType } : {}),
121
125
  ...(!cyberFailure && cooldownRetryAfter !== undefined ? { retryAfter: cooldownRetryAfter } : {}),