@bitkyc08/opencodex 2.10.0 → 2.10.1

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 (274) hide show
  1. package/AGENTS_INSTALL.md +77 -0
  2. package/README.md +4 -10
  3. package/bin/ocx.mjs +71 -18
  4. package/gui/dist/assets/index-Cd6_PBKn.css +1 -0
  5. package/gui/dist/assets/index-ChZQsmBY.js +70 -0
  6. package/gui/dist/index.html +2 -2
  7. package/gui/dist/provider-icons/alibaba-color.svg +1 -1
  8. package/gui/dist/provider-icons/antigravity-color.svg +1 -1
  9. package/gui/dist/provider-icons/claude-color.svg +1 -1
  10. package/gui/dist/provider-icons/cline-color.svg +16 -0
  11. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
  12. package/gui/dist/provider-icons/copilot-color.svg +1 -1
  13. package/gui/dist/provider-icons/cursor-color.svg +1 -1
  14. package/gui/dist/provider-icons/deepseek-color.svg +1 -1
  15. package/gui/dist/provider-icons/firepass-color.svg +1 -1
  16. package/gui/dist/provider-icons/fireworks-color.svg +1 -1
  17. package/gui/dist/provider-icons/gemini-color.svg +1 -1
  18. package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
  19. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
  20. package/gui/dist/provider-icons/grok.svg +1 -1
  21. package/gui/dist/provider-icons/groq-color.svg +1 -1
  22. package/gui/dist/provider-icons/huggingface-color.svg +1 -1
  23. package/gui/dist/provider-icons/kimi-color.svg +1 -1
  24. package/gui/dist/provider-icons/kiro-color.svg +2 -2
  25. package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
  26. package/gui/dist/provider-icons/mistral-color.svg +1 -1
  27. package/gui/dist/provider-icons/moonshot-color.svg +1 -1
  28. package/gui/dist/provider-icons/nvidia-color.svg +1 -1
  29. package/gui/dist/provider-icons/ollama-color.svg +1 -1
  30. package/gui/dist/provider-icons/openai.svg +1 -1
  31. package/gui/dist/provider-icons/opencode.svg +2 -1
  32. package/gui/dist/provider-icons/openrouter-color.svg +1 -1
  33. package/gui/dist/provider-icons/pi.svg +2 -2
  34. package/gui/dist/provider-icons/qianfan-color.svg +1 -1
  35. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
  36. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
  37. package/gui/dist/provider-icons/vllm-color.svg +1 -1
  38. package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
  39. package/package.json +8 -4
  40. package/src/adapters/anthropic.ts +208 -14
  41. package/src/adapters/base.ts +16 -5
  42. package/src/adapters/cursor/effort-map.ts +3 -2
  43. package/src/adapters/cursor/framing.ts +39 -0
  44. package/src/adapters/cursor/live-transport.ts +105 -95
  45. package/src/adapters/cursor/native-exec.ts +32 -6
  46. package/src/adapters/cursor/protobuf-request.ts +20 -15
  47. package/src/adapters/cursor/request-builder.ts +21 -7
  48. package/src/adapters/cursor/types.ts +7 -0
  49. package/src/adapters/google-antigravity-replay.ts +237 -21
  50. package/src/adapters/google-truncation.ts +11 -0
  51. package/src/adapters/google.ts +50 -9
  52. package/src/adapters/identity.ts +39 -6
  53. package/src/adapters/kiro-errors.ts +11 -0
  54. package/src/adapters/kiro-events.ts +19 -1
  55. package/src/adapters/kiro-thinking.ts +10 -2
  56. package/src/adapters/kiro-tools.ts +10 -1
  57. package/src/adapters/kiro.ts +37 -11
  58. package/src/adapters/openai-chat.ts +284 -83
  59. package/src/adapters/openai-responses.ts +182 -24
  60. package/src/bridge.ts +177 -7
  61. package/src/chat/outbound.ts +78 -23
  62. package/src/claude/agents-inject.ts +27 -5
  63. package/src/claude/inbound.ts +11 -1
  64. package/src/claude/model-info.ts +13 -10
  65. package/src/claude/outbound.ts +17 -0
  66. package/src/cli/account-api.ts +24 -0
  67. package/src/cli/account-auth.ts +31 -6
  68. package/src/cli/account-main.ts +317 -0
  69. package/src/cli/account.ts +5 -0
  70. package/src/cli/claude.ts +2 -1
  71. package/src/cli/doctor.ts +93 -22
  72. package/src/cli/export-command.ts +26 -12
  73. package/src/cli/help.ts +8 -6
  74. package/src/cli/index.ts +56 -22
  75. package/src/cli/integrations.ts +84 -1
  76. package/src/cli/observe.ts +54 -1
  77. package/src/cli/opencode.ts +2 -1
  78. package/src/cli/provider-runtime.ts +18 -1
  79. package/src/cli/route-policy.ts +92 -0
  80. package/src/cli/runtime-api.ts +6 -3
  81. package/src/cli/star-prompt.ts +71 -15
  82. package/src/cli/status.ts +1 -1
  83. package/src/cli/v2.ts +36 -9
  84. package/src/clients/config-export.ts +687 -10
  85. package/src/codex/account-lifecycle.ts +30 -5
  86. package/src/codex/account-usability.ts +22 -2
  87. package/src/codex/admission.ts +255 -0
  88. package/src/codex/auth-api.ts +427 -140
  89. package/src/codex/auth-context.ts +155 -30
  90. package/src/codex/autostart-health.ts +8 -1
  91. package/src/codex/catalog/account-models.ts +62 -0
  92. package/src/codex/catalog/aggregation.ts +14 -1
  93. package/src/codex/catalog/bundled.ts +282 -32
  94. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  95. package/src/codex/catalog/metadata.ts +51 -6
  96. package/src/codex/catalog/parsing.ts +6 -3
  97. package/src/codex/catalog/provider-fetch.ts +576 -41
  98. package/src/codex/catalog/sync.ts +505 -66
  99. package/src/codex/catalog-admission.ts +197 -0
  100. package/src/codex/catalog-write-serialization.ts +241 -0
  101. package/src/codex/catalog.ts +2 -1
  102. package/src/codex/codex-write-lock.ts +372 -0
  103. package/src/codex/convergence-types.ts +593 -0
  104. package/src/codex/convergence.ts +441 -0
  105. package/src/codex/desired-state.ts +177 -0
  106. package/src/codex/features.ts +52 -8
  107. package/src/codex/generation.ts +202 -0
  108. package/src/codex/history-job.ts +257 -0
  109. package/src/codex/history-lock.ts +241 -0
  110. package/src/codex/history-migration-guardian.ts +18 -5
  111. package/src/codex/history-provider.ts +9 -2
  112. package/src/codex/history-transition.ts +105 -0
  113. package/src/codex/history-worker.ts +176 -0
  114. package/src/codex/inject-coordination.ts +245 -0
  115. package/src/codex/inject.ts +605 -124
  116. package/src/codex/integration-record.ts +266 -0
  117. package/src/codex/internal/catalog-writer.ts +203 -0
  118. package/src/codex/internal/history-writer.ts +80 -0
  119. package/src/codex/journal.ts +10 -1
  120. package/src/codex/main-account-cache.ts +24 -0
  121. package/src/codex/management-convergence.ts +114 -0
  122. package/src/codex/native-main-admission.ts +47 -0
  123. package/src/codex/native-main-auth-temp.ts +187 -0
  124. package/src/codex/native-main-claim.ts +167 -0
  125. package/src/codex/native-main-lock-file.ts +158 -0
  126. package/src/codex/native-main-owner.ts +315 -0
  127. package/src/codex/native-profile-api.ts +247 -0
  128. package/src/codex/native-profile-manager.ts +1512 -0
  129. package/src/codex/native-profile-processes.ts +121 -0
  130. package/src/codex/native-profile-recovery.ts +99 -0
  131. package/src/codex/native-profile-stage-store.ts +387 -0
  132. package/src/codex/native-profile-startup.ts +340 -0
  133. package/src/codex/native-profile-store.ts +855 -0
  134. package/src/codex/native-profile-types.ts +120 -0
  135. package/src/codex/native-residue.ts +557 -0
  136. package/src/codex/project-config-warnings.ts +18 -4
  137. package/src/codex/prompt-journal.ts +311 -0
  138. package/src/codex/prompt-layers.ts +967 -0
  139. package/src/codex/prompt-lock.ts +143 -0
  140. package/src/codex/quota-rejection.ts +224 -0
  141. package/src/codex/quota.ts +86 -3
  142. package/src/codex/routing.ts +299 -62
  143. package/src/codex/runtime.ts +159 -38
  144. package/src/codex/shim.ts +39 -13
  145. package/src/codex/subagent-model-fallback.ts +73 -12
  146. package/src/codex/transition-state.ts +604 -0
  147. package/src/codex/upstream-host-health.ts +70 -0
  148. package/src/codex/user-identity.ts +266 -0
  149. package/src/codex/write-coordination.ts +114 -0
  150. package/src/config.ts +562 -26
  151. package/src/generated/jawcode-model-metadata.ts +2 -2
  152. package/src/grok/inject.ts +15 -4
  153. package/src/grok/inspect.ts +45 -0
  154. package/src/images/loop.ts +113 -20
  155. package/src/integrations/config-io.ts +151 -0
  156. package/src/integrations/journal.ts +315 -0
  157. package/src/integrations/merge.ts +135 -0
  158. package/src/integrations/native/ownership-preflight.ts +165 -0
  159. package/src/integrations/ownership.ts +111 -0
  160. package/src/integrations/registry.ts +101 -0
  161. package/src/integrations/serialize.ts +235 -0
  162. package/src/integrations/state.ts +290 -0
  163. package/src/integrations/store.ts +103 -0
  164. package/src/integrations/writer.ts +492 -0
  165. package/src/lib/bounded-body.ts +46 -8
  166. package/src/lib/bun-runtime.ts +110 -1
  167. package/src/lib/bun-stream-caps.ts +2 -1
  168. package/src/lib/redact.ts +407 -2
  169. package/src/lib/shadow-call.ts +24 -0
  170. package/src/lib/translator-budget.ts +10 -0
  171. package/src/lib/upstream-reachability.ts +91 -0
  172. package/src/lib/upstream-retry.ts +154 -2
  173. package/src/lib/windows-secret-acl.ts +212 -11
  174. package/src/lib/winsw.ts +9 -3
  175. package/src/oauth/index.ts +61 -3
  176. package/src/oauth/key-providers.ts +4 -0
  177. package/src/oauth/kiro.ts +50 -6
  178. package/src/oauth/store.ts +31 -6
  179. package/src/oauth/token-guardian.ts +9 -3
  180. package/src/providers/codex-capacity.ts +288 -0
  181. package/src/providers/derive.ts +33 -1
  182. package/src/providers/free-directory.ts +3 -1
  183. package/src/providers/key-failover.ts +71 -3
  184. package/src/providers/openai-sidecar.ts +64 -4
  185. package/src/providers/openai-virtual-models.ts +1 -0
  186. package/src/providers/quota.ts +334 -26
  187. package/src/providers/registry.ts +284 -16
  188. package/src/providers/xai-transport.ts +11 -4
  189. package/src/responses/compaction.ts +8 -1
  190. package/src/responses/hosted-tool-policy.ts +9 -0
  191. package/src/responses/parser.ts +44 -2
  192. package/src/responses/reasoning-envelope.ts +9 -1
  193. package/src/responses/reasoning-replay-cache.ts +105 -0
  194. package/src/responses/spill-store.ts +45 -8
  195. package/src/responses/state.ts +161 -17
  196. package/src/router.ts +302 -16
  197. package/src/routing/analytics.ts +377 -0
  198. package/src/routing/capability.ts +204 -0
  199. package/src/routing/cost.ts +77 -0
  200. package/src/routing/evaluator.ts +444 -0
  201. package/src/routing/health.ts +401 -0
  202. package/src/routing/history/cursor.ts +43 -0
  203. package/src/routing/history/indexer.ts +590 -0
  204. package/src/routing/history/schema.ts +72 -0
  205. package/src/routing/profile.ts +423 -0
  206. package/src/routing/quota.ts +124 -0
  207. package/src/routing/request-evidence.ts +45 -0
  208. package/src/routing/trace.ts +686 -0
  209. package/src/server/auth-cors.ts +46 -6
  210. package/src/server/chat-completions.ts +28 -13
  211. package/src/server/claude-messages.ts +23 -15
  212. package/src/server/gui-static.ts +39 -10
  213. package/src/server/images.ts +10 -1
  214. package/src/server/index.ts +238 -52
  215. package/src/server/lifecycle.ts +228 -9
  216. package/src/server/live.ts +14 -3
  217. package/src/server/management/agent-settings-routes.ts +64 -14
  218. package/src/server/management/combo-routes.ts +5 -5
  219. package/src/server/management/config-routes.ts +1 -1
  220. package/src/server/management/context.ts +42 -2
  221. package/src/server/management/integration-routes.ts +538 -0
  222. package/src/server/management/logs-usage-routes.ts +1 -1
  223. package/src/server/management/model-routes.ts +32 -113
  224. package/src/server/management/model-rows.ts +117 -0
  225. package/src/server/management/native-integration-routes.ts +587 -0
  226. package/src/server/management/oauth-account-routes.ts +1 -1
  227. package/src/server/management/provider-routes.ts +218 -117
  228. package/src/server/management/request-history-routes.ts +191 -0
  229. package/src/server/management/routing-analytics-routes.ts +74 -0
  230. package/src/server/management/routing-profile-routes.ts +177 -0
  231. package/src/server/management/shared.ts +2 -2
  232. package/src/server/management/sidebar-routes.ts +47 -31
  233. package/src/server/management/sync-response.ts +69 -0
  234. package/src/server/management/system-restart.ts +276 -24
  235. package/src/server/management/system-routes.ts +4 -0
  236. package/src/server/management-api.ts +84 -9
  237. package/src/server/management-auth.ts +43 -5
  238. package/src/server/relay-eager.ts +82 -42
  239. package/src/server/relay.ts +120 -6
  240. package/src/server/request-log.ts +26 -6
  241. package/src/server/responses/collaboration.ts +63 -8
  242. package/src/server/responses/compact.ts +272 -41
  243. package/src/server/responses/core.ts +730 -132
  244. package/src/server/responses/fetch-helpers.ts +15 -1
  245. package/src/server/responses-item-id-repair.ts +32 -3
  246. package/src/server/responses-json-events.ts +52 -0
  247. package/src/server/responses-snapshot-repair.ts +621 -0
  248. package/src/server/search.ts +51 -6
  249. package/src/server/sse-payload-rewrite.ts +89 -12
  250. package/src/server/startup-health-cache.ts +7 -1
  251. package/src/server/ws-bridge.ts +11 -17
  252. package/src/service-manager-probe.ts +297 -0
  253. package/src/service.ts +222 -32
  254. package/src/tray/windows-tray.ps1 +9 -0
  255. package/src/tray/windows.ts +15 -7
  256. package/src/types.ts +194 -14
  257. package/src/update/index.ts +13 -13
  258. package/src/update/job.ts +24 -21
  259. package/src/update/notify.ts +7 -3
  260. package/src/usage/cost.ts +0 -0
  261. package/src/usage/expected-prices.ts +129 -10
  262. package/src/usage/log.ts +50 -15
  263. package/src/usage/summary.ts +4 -4
  264. package/src/vision/index.ts +6 -1
  265. package/src/web-search/loop.ts +161 -34
  266. package/gui/dist/assets/index-OY43ubAq.css +0 -1
  267. package/gui/dist/assets/index-YwNnKZcL.js +0 -67
  268. package/gui/dist/provider-icons/antigravity.svg +0 -1
  269. package/gui/dist/provider-icons/claude.svg +0 -1
  270. package/gui/dist/provider-icons/copilot.svg +0 -1
  271. package/gui/dist/provider-icons/cursor.svg +0 -2
  272. package/gui/dist/provider-icons/gemini.svg +0 -1
  273. package/gui/dist/provider-icons/grok-color.svg +0 -1
  274. package/gui/dist/provider-icons/kiro.svg +0 -14
@@ -9,7 +9,8 @@ import { parseRequest } from "../../responses/parser";
9
9
  import { buildCompactV1Output, COMPACT_PROMPT, decodeCompactionSummary, extractCompactUserMessages } from "../../responses/compaction";
10
10
  import { FORWARD_HEADERS, sanitizeReasoningInputContent } from "../../adapters/openai-responses";
11
11
  import { expandPreviousResponseInput, previousResponseProviderState, rememberResponseState } from "../../responses/state";
12
- import { routeModel } from "../../router";
12
+ import { NoEligiblePolicyCandidateError, routeModel } from "../../router";
13
+ import { evidenceFromBody } from "../../routing/request-evidence";
13
14
  import {
14
15
  advanceComboAfterFailure,
15
16
  comboDefaultEffort,
@@ -27,7 +28,7 @@ import {
27
28
  import { isInjectionDebugEnabled } from "../../lib/debug-settings";
28
29
  import { injectionDebugLog } from "../../lib/injection-debug-log";
29
30
  import { modelInList, namespacedToolName } from "../../types";
30
- import type { AdapterEvent, OcxConfig, OcxParsedRequest, OcxProviderConfig, OcxProviderContinuationState, OcxUsage } from "../../types";
31
+ import type { AdapterEvent, CodexAccountMode, OcxConfig, OcxParsedRequest, OcxProviderConfig, OcxProviderContinuationState, OcxUsage } from "../../types";
31
32
  import {
32
33
  forceRefreshOAuthAccessSnapshot,
33
34
  getOAuthCredentialApiBaseUrl,
@@ -42,9 +43,11 @@ import { createAdapterEventQueue, preflightAdapterEvents } from "../../adapters/
42
43
  import {
43
44
  applyCodexAuthContextToProvider,
44
45
  CodexAccountCooldownError,
46
+ codexMainProfileDrainingResponse,
45
47
  cooldownErrorResponse,
46
48
  CodexAuthContextError,
47
49
  CodexDirectAuthenticationError,
50
+ CodexMainProfileDrainingError,
48
51
  CodexPoolAuthenticationError,
49
52
  CodexThreadAffinityExpiredError,
50
53
  headersForCodexAuthContext,
@@ -52,6 +55,7 @@ import {
52
55
  resolveCodexAuthContext,
53
56
  codexProbeLeaseId,
54
57
  codexProbeQuotaScope,
58
+ releaseCodexAuthContextProbeLease,
55
59
  type CodexAuthContext,
56
60
  } from "../../codex/auth-context";
57
61
  import {
@@ -59,7 +63,14 @@ import {
59
63
  recordCodexUpstreamOutcome,
60
64
  type CodexUpstreamOutcome,
61
65
  } from "../../codex/routing";
62
- import { fetchWithResetRetry, fetchWithTransientRetry, applyUpstreamRecoveryInit } from "../../lib/upstream-retry";
66
+ import {
67
+ fetchWithResetRetry,
68
+ fetchWithTransientRetry,
69
+ applyUpstreamRecoveryInit,
70
+ type UpstreamSendRecovery,
71
+ } from "../../lib/upstream-retry";
72
+ import { classifyTransportFailureKind, transportErrorCode } from "../../lib/upstream-reachability";
73
+ import { recordUpstreamHostFailure, resetUpstreamHostHealth, upstreamHostHealthKey } from "../../codex/upstream-host-health";
63
74
  import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential } from "../auth-cors";
64
75
  import { listOpenAiForwardSidecarCandidates, resolveFirstUsableOpenAiSidecar, type ResolvedOpenAiForwardSidecar } from "../../providers/openai-sidecar";
65
76
  import { isCanonicalOpenAiForwardProvider, supportsNativeResponsesCompactEndpoint } from "../../providers/openai-tiers";
@@ -72,7 +83,8 @@ import { hasKeyPoolFailover, rotateProviderTransportOn429 } from "../../provider
72
83
  import { shouldAttemptImageTierRetry } from "../image-retry";
73
84
  import { resolveProviderTransport } from "../../providers/xai-transport";
74
85
  import type { WsData } from "../ws-bridge";
75
- import { registerTurn, trackStreamLifetime, unregisterTurn } from "../lifecycle";
86
+ import { codexAccountSelectionForTurn, registerTurn, trackStreamLifetime, unregisterTurn } from "../lifecycle";
87
+ import type { AdmissionLease } from "../../lib/admission";
76
88
  import { redactSecretString } from "../../lib/redact";
77
89
  import { readBoundedResponseBody } from "../../lib/bounded-body";
78
90
  import { supportedLadderFor } from "../effort-policy";
@@ -101,7 +113,7 @@ import { hasResponsesItemIdRepair, relaySseWithResponsesItemIdRepair } from "../
101
113
  import type { EffectiveSubagentRoster, SpawnAgentSurface } from "../../codex/catalog";
102
114
 
103
115
  import { decodeRequestErrorResponse, handleResponses, usesCodexForwardPoolAuth } from "./core";
104
- import { fetchWithHeaderTimeout, providerFetch, safeHostLabel } from "./fetch-helpers";
116
+ import { fetchWithHeaderTimeout, providerFetch, safeHostLabel, safeOriginLabel } from "./fetch-helpers";
105
117
 
106
118
  export const COMPACT_RESPONSE_MAX_BYTES = 32 * 1024 * 1024;
107
119
 
@@ -117,10 +129,88 @@ export function compactResponseTooLargeError(): Response {
117
129
 
118
130
 
119
131
 
132
+ /**
133
+ * Resolve one eligible pool account other than `excludeAccountId`, and build everything
134
+ * the alternate send needs. Returns null when no alternate exists or construction fails,
135
+ * in which case the caller keeps the first account's rejection intact.
136
+ *
137
+ * Mirrors the auth resolution the native compact branch already does for the first
138
+ * account, so the alternate is built the same way rather than through a second,
139
+ * divergent path.
140
+ */
141
+ async function resolveAlternateCompactContext(args: {
142
+ req: Request;
143
+ config: OcxConfig;
144
+ route: { provider: OcxProviderConfig; codexAccountMode?: CodexAccountMode };
145
+ selectedModelId: string | undefined;
146
+ excludeAccountId: string | null;
147
+ turnAdmissionLease?: AdmissionLease;
148
+ }): Promise<{ authCtx: CodexAuthContext; provider: OcxProviderConfig; headers: Headers } | null> {
149
+ const { req, config, route, selectedModelId, excludeAccountId, turnAdmissionLease } = args;
150
+ if (!route.codexAccountMode || !excludeAccountId) return null;
151
+ try {
152
+ const authCtx = await resolveCodexAuthContext(req.headers, config, route.codexAccountMode, {
153
+ ...(selectedModelId ? { modelId: selectedModelId } : {}),
154
+ excludeAccountId,
155
+ beginCodexAccountSelection: codexAccountSelectionForTurn(turnAdmissionLease),
156
+ });
157
+ if (!authCtx.accountId || authCtx.accountId === excludeAccountId) return null;
158
+ const provider = applyCodexAuthContextToProvider(route.provider, authCtx, route.codexAccountMode);
159
+ const headers = new Headers({ "content-type": "application/json" });
160
+ const selected = headersForCodexAuthContext(req.headers, authCtx);
161
+ for (const name of FORWARD_HEADERS) {
162
+ const value = selected.get(name);
163
+ if (value) headers.set(name, value);
164
+ }
165
+ const override = (provider as { _codexAccountOverride?: { accessToken: string; chatgptAccountId: string } })._codexAccountOverride;
166
+ if (override) {
167
+ headers.set("authorization", `Bearer ${override.accessToken}`);
168
+ headers.set("chatgpt-account-id", override.chatgptAccountId);
169
+ }
170
+ if (provider.apiKey) headers.set("authorization", `Bearer ${resolveEnvValue(provider.apiKey)}`);
171
+ return { authCtx, provider, headers };
172
+ } catch (err) {
173
+ if (err instanceof CodexMainProfileDrainingError) {
174
+ // The native-main fence can start after account A has already rejected the
175
+ // request. Treat the now-fenced main profile as no alternate and preserve A's
176
+ // real rejection instead of replacing it with a synthetic drain response.
177
+ return null;
178
+ }
179
+ // No eligible alternate (all cooled, affinity expired, reauth needed) — the caller
180
+ // returns the first account's rejection unchanged, which is today's behavior.
181
+ return null;
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Headers a client needs to back off correctly after a pool rejection. The buffered
187
+ * response is rebuilt from scratch, so anything not listed here is dropped — which is
188
+ * what silently discarded `Retry-After` and the reset hints from a 429 before.
189
+ * Deliberately narrow: no hop-by-hop headers, no content-length (the buffered body sets
190
+ * its own), no cookies.
191
+ */
192
+ const COMPACT_PASSTHROUGH_HEADERS = [
193
+ "retry-after",
194
+ "x-codex-primary-reset-at",
195
+ "x-codex-secondary-reset-at",
196
+ "x-codex-tertiary-reset-at",
197
+ // A relayed 3xx keeps its Location so the client can follow it (#914).
198
+ "location",
199
+ ];
200
+
201
+ function compactResponseHeaders(upstream: Response): Headers {
202
+ const headers = new Headers({ "Content-Type": upstream.headers.get("content-type") ?? "application/json" });
203
+ for (const name of COMPACT_PASSTHROUGH_HEADERS) {
204
+ const value = upstream.headers.get(name);
205
+ if (value) headers.set(name, value);
206
+ }
207
+ return headers;
208
+ }
209
+
120
210
  export async function bufferCompactResponse(upstream: Response, signal: AbortSignal): Promise<Response> {
121
211
  const reader = upstream.body?.getReader();
122
- const contentType = upstream.headers.get("content-type") ?? "application/json";
123
- if (!reader) return new Response(null, { status: upstream.status, headers: { "Content-Type": contentType } });
212
+ const headers = compactResponseHeaders(upstream);
213
+ if (!reader) return new Response(null, { status: upstream.status, statusText: upstream.statusText, headers });
124
214
  const declaredLength = Number(upstream.headers.get("content-length"));
125
215
  if (Number.isFinite(declaredLength) && declaredLength > COMPACT_RESPONSE_MAX_BYTES) {
126
216
  await reader.cancel("compact_response_too_large").catch(() => undefined);
@@ -153,7 +243,7 @@ export async function bufferCompactResponse(upstream: Response, signal: AbortSig
153
243
  body.set(chunk, offset);
154
244
  offset += chunk.byteLength;
155
245
  }
156
- return new Response(body, { status: upstream.status, headers: { "Content-Type": contentType } });
246
+ return new Response(body, { status: upstream.status, statusText: upstream.statusText, headers });
157
247
  }
158
248
 
159
249
 
@@ -162,6 +252,7 @@ export async function handleResponsesCompact(
162
252
  req: Request,
163
253
  config: OcxConfig,
164
254
  logCtx: RequestLogContext,
255
+ turnAdmissionLease?: AdmissionLease,
165
256
  ): Promise<Response> {
166
257
  let body: unknown;
167
258
  try {
@@ -179,14 +270,26 @@ export async function handleResponsesCompact(
179
270
 
180
271
  let route;
181
272
  try {
182
- route = routeModel(config, raw.model);
273
+ // Compact requests route through the same policy evaluation as normal
274
+ // turns, so body-derived evidence (tools/image) must reach the first
275
+ // evaluation too - not only the later handleResponses dispatch.
276
+ route = routeModel(config, raw.model, evidenceFromBody(raw));
183
277
  } catch (err) {
278
+ if (err instanceof NoEligiblePolicyCandidateError) {
279
+ // Persist the evaluation trace (per-candidate exclusions + the
280
+ // no-eligible reason) so a failed compact policy request stays
281
+ // auditable, matching the other request handlers.
282
+ logCtx.routeDecision = err.trace;
283
+ }
184
284
  return formatErrorResponse(404, "invalid_request_error", err instanceof Error ? err.message : String(err));
185
285
  }
186
286
  const selectedModelId = route.modelId;
187
287
  logCtx.requestedModel = raw.model;
188
288
  logCtx.model = selectedModelId;
189
- logCtx.provider = route.providerName;
289
+ logCtx.routeDecision = route.routeDecision;
290
+ logCtx.provider = route.codexAccountNamespace
291
+ ? `${route.providerName}-${route.codexAccountNamespace}`
292
+ : route.providerName;
190
293
  logCtx.providerAdapter = route.provider.adapter;
191
294
  const virtual = resolveOpenAiCompactModel(route.providerName, selectedModelId);
192
295
  if (virtual) {
@@ -218,7 +321,11 @@ export async function handleResponsesCompact(
218
321
  const headers = new Headers({ "content-type": "application/json" });
219
322
  try {
220
323
  if (route.codexAccountMode) {
221
- authCtx = await resolveCodexAuthContext(req.headers, config, route.codexAccountMode, { modelId: selectedModelId });
324
+ authCtx = await resolveCodexAuthContext(req.headers, config, route.codexAccountMode, {
325
+ accountId: route.codexAccountId,
326
+ modelId: selectedModelId,
327
+ beginCodexAccountSelection: codexAccountSelectionForTurn(turnAdmissionLease),
328
+ });
222
329
  const selected = headersForCodexAuthContext(req.headers, authCtx);
223
330
  compactProvider = applyCodexAuthContextToProvider(route.provider, authCtx, route.codexAccountMode);
224
331
  for (const name of FORWARD_HEADERS) {
@@ -233,8 +340,9 @@ export async function handleResponsesCompact(
233
340
  }
234
341
  } catch (err) {
235
342
  if (err instanceof CodexAccountCooldownError) {
236
- return cooldownErrorResponse(err);
343
+ return cooldownErrorResponse(err, Date.now(), route.codexAccountNamespace);
237
344
  }
345
+ if (err instanceof CodexMainProfileDrainingError) return codexMainProfileDrainingResponse();
238
346
  if (err instanceof CodexThreadAffinityExpiredError) {
239
347
  return formatErrorResponse(409, "invalid_request_error", "Codex thread account affinity expired; start a new session");
240
348
  }
@@ -247,7 +355,9 @@ export async function handleResponsesCompact(
247
355
  throw err;
248
356
  }
249
357
  const base = (compactProvider.baseUrl ?? "").replace(/\/$/, "");
250
- if (compactProvider.apiKey) headers.set("authorization", `Bearer ${resolveEnvValue(compactProvider.apiKey)}`);
358
+ if (compactProvider.authMode !== "forward" && compactProvider.apiKey) {
359
+ headers.set("authorization", `Bearer ${resolveEnvValue(compactProvider.apiKey)}`);
360
+ }
251
361
  const { reasoning: _reasoning, ...compactBodyRaw } = raw as typeof raw & { reasoning?: unknown };
252
362
  // The regular /v1/responses path applies sanitizeReasoningInputContent via the adapter's
253
363
  // buildRequest, but the compact endpoint forwards directly. Apply the same sanitizer here
@@ -256,50 +366,167 @@ export async function handleResponsesCompact(
256
366
  const compactUrl = `${base}/responses/compact`;
257
367
  const compactThreadId = req.headers.get("x-codex-parent-thread-id");
258
368
  const connectMs = config.connectTimeoutMs ?? 200_000;
369
+ // Takes its context explicitly: the alternate-account flow below records a rejection
370
+ // against A while promoting B, then records B's own outcome. A closure over a single
371
+ // `authCtx` cannot express either.
259
372
  const recordCompactPoolOutcome = (
373
+ ctx: CodexAuthContext,
260
374
  outcome: CodexUpstreamOutcome,
261
- meta: { retryAfter?: string | null; resetAt?: unknown | unknown[] } = {},
375
+ meta: {
376
+ retryAfter?: string | null;
377
+ resetAt?: unknown | unknown[];
378
+ promoteAccountId?: string;
379
+ } = {},
262
380
  ) => {
263
- if (!usesCodexForwardPoolAuth(authCtx, route.provider)) return;
264
- recordCodexUpstreamOutcome(config, authCtx.accountId, outcome, {
381
+ if (!usesCodexForwardPoolAuth(ctx, route.provider)) return;
382
+ recordCodexUpstreamOutcome(config, ctx.accountId, outcome, {
265
383
  ...meta,
266
384
  threadId: compactThreadId,
385
+ fixedAccount: ctx.fixedAccount,
267
386
  modelId: selectedModelId,
268
- probeLeaseId: codexProbeLeaseId(authCtx),
269
- probeQuotaScope: codexProbeQuotaScope(authCtx),
270
- writerGeneration: authCtx.kind === "pool" || authCtx.kind === "main-pool"
271
- ? authCtx.writerGeneration
387
+ probeLeaseId: codexProbeLeaseId(ctx),
388
+ probeQuotaScope: codexProbeQuotaScope(ctx),
389
+ writerGeneration: ctx.kind === "pool" || ctx.kind === "main-pool"
390
+ ? ctx.writerGeneration
272
391
  : undefined,
273
392
  });
274
393
  };
394
+ // Two recovery modes, mirroring retryCodexPoolOnAlternateAccount() on the regular
395
+ // path (core.ts:396). The first account keeps the full ladder — transient-5xx retry
396
+ // wrapping reset retry — because those retries happen before any alternate is even
397
+ // considered. The alternate is one bounded send: a second ladder would multiply the
398
+ // work an already-rejecting pool is doing.
399
+ const sendCompactAttempt = (
400
+ sendProvider: OcxProviderConfig,
401
+ sendHeaders: Headers,
402
+ recovery: "normal" | "single",
403
+ ): Promise<Response> => {
404
+ const doFetch = (upstreamRecovery?: UpstreamSendRecovery) => fetchWithHeaderTimeout(
405
+ compactUrl,
406
+ applyUpstreamRecoveryInit({
407
+ method: "POST",
408
+ headers: sendHeaders,
409
+ body: JSON.stringify({ ...compactBody, model: route.modelId }),
410
+ }, upstreamRecovery),
411
+ req.signal,
412
+ connectMs,
413
+ false,
414
+ providerFetch(sendProvider),
415
+ // Every credential-bearing forward send gets manual redirects, not only
416
+ // pool sends: direct mode carries the caller's credential too (#914).
417
+ sendProvider.authMode === "forward",
418
+ ).then(res => {
419
+ // Every real attempt response — including an intermediate 5xx the retry
420
+ // wrapper replaces — proves the host was reached (#914 review).
421
+ resetUpstreamHostHealth(upstreamHostHealthKey(route.providerName, safeOriginLabel(compactUrl)));
422
+ return res;
423
+ });
424
+ return recovery === "single"
425
+ ? doFetch()
426
+ : fetchWithTransientRetry(doFetch, { abortSignal: req.signal, label: safeHostLabel(compactUrl) });
427
+ };
428
+
429
+ // The account each outcome belongs to. Reassigned only when the alternate send below
430
+ // actually happens, so every recorder call names the context that produced it.
431
+ let outcomeCtx = authCtx;
275
432
  let upstream: Response;
276
433
  try {
277
434
  // Same connect timeout + keep-alive reset + transient-5xx recovery as /v1/responses —
278
435
  // compact hits the same ChatGPT host and must soft-avoid / clear affinity (#186).
279
- upstream = await fetchWithTransientRetry(
280
- recovery => fetchWithHeaderTimeout(
281
- compactUrl,
282
- applyUpstreamRecoveryInit({
283
- method: "POST",
284
- headers,
285
- body: JSON.stringify({ ...compactBody, model: route.modelId }),
286
- }, recovery),
287
- req.signal,
288
- connectMs,
289
- false,
290
- providerFetch(compactProvider),
291
- ),
292
- { abortSignal: req.signal, label: safeHostLabel(compactUrl) },
293
- );
436
+ upstream = await sendCompactAttempt(compactProvider, headers, "normal");
294
437
  } catch (err) {
295
438
  if (req.signal.aborted) {
296
- recordCompactPoolOutcome(499);
439
+ recordCompactPoolOutcome(outcomeCtx, 499);
297
440
  return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
298
441
  }
299
- const outcome = err instanceof Error && err.name === "TimeoutError" ? "timeout" : "connect_error";
300
- recordCompactPoolOutcome(outcome);
442
+ const outcome = classifyTransportFailureKind(err);
443
+ // Host-level evidence stands regardless of pool membership (#914 review).
444
+ if (outcome === "connect_neutral") {
445
+ recordUpstreamHostFailure(
446
+ upstreamHostHealthKey(route.providerName, safeOriginLabel(compactUrl)),
447
+ { code: transportErrorCode(err) },
448
+ );
449
+ }
450
+ recordCompactPoolOutcome(outcomeCtx, outcome);
301
451
  return formatErrorResponse(502, "upstream_error", "Failed to connect to compact upstream");
302
452
  }
453
+
454
+ // Bounded same-request alternate: the regular /v1/responses path already does this
455
+ // (core.ts:319-423) and recognizes exactly 429/402. Without it a pool rejection
456
+ // surfaces to the client, which retries the compact task OUTSIDE the logical request
457
+ // — reporting exhausted retries while another pool account sat idle (#913).
458
+ if (
459
+ (upstream.status === 429 || upstream.status === 402)
460
+ && usesCodexForwardPoolAuth(authCtx, route.provider)
461
+ && !authCtx.fixedAccount
462
+ && route.codexAccountMode
463
+ && !req.signal.aborted
464
+ ) {
465
+ const firstRetryAfter = upstream.headers.get("retry-after");
466
+ const firstResetAt = [
467
+ upstream.headers.get("x-codex-primary-reset-at"),
468
+ upstream.headers.get("x-codex-secondary-reset-at"),
469
+ upstream.headers.get("x-codex-tertiary-reset-at"),
470
+ ].filter(Boolean);
471
+ // Build the alternate COMPLETELY before cancelling the first body: if construction
472
+ // throws, the first rejection is still intact and can be returned to the client.
473
+ const alternate = await resolveAlternateCompactContext({
474
+ req,
475
+ config,
476
+ route,
477
+ selectedModelId,
478
+ excludeAccountId: authCtx.accountId,
479
+ turnAdmissionLease,
480
+ });
481
+ // Resolution can await a credential refresh, so the client may have gone away
482
+ // while we were choosing B. Re-check before spending anything: recording A,
483
+ // cancelling its body, and sending B are all observable side effects, and B's
484
+ // quota is not ours to spend on a request nobody is waiting for.
485
+ if (alternate && req.signal.aborted) {
486
+ releaseCodexAuthContextProbeLease(alternate.authCtx);
487
+ recordCompactPoolOutcome(outcomeCtx, 499);
488
+ return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
489
+ }
490
+ if (alternate) {
491
+ // Same order the regular path uses (core.ts:349-357): a 429/402 carries the
492
+ // quota snapshot that produced it, so refresh A's cache before recording its
493
+ // rejection. Skipping this leaves quota-strategy routing and the dashboard
494
+ // reading numbers from before the account ran out.
495
+ if (authCtx.kind === "pool" || authCtx.kind === "main-pool") {
496
+ const { applyAccountQuotaFromUpstreamHeaders } = await import("../../codex/auth-api");
497
+ applyAccountQuotaFromUpstreamHeaders(
498
+ authCtx.accountId,
499
+ upstream.headers,
500
+ authCtx.writerGeneration,
501
+ );
502
+ }
503
+ recordCompactPoolOutcome(authCtx, upstream.status, {
504
+ retryAfter: firstRetryAfter,
505
+ resetAt: firstResetAt,
506
+ ...(alternate.authCtx.accountId ? { promoteAccountId: alternate.authCtx.accountId } : {}),
507
+ });
508
+ await upstream.body?.cancel().catch(() => undefined);
509
+ outcomeCtx = alternate.authCtx;
510
+ try {
511
+ upstream = await sendCompactAttempt(alternate.provider, alternate.headers, "single");
512
+ } catch (err) {
513
+ if (req.signal.aborted) {
514
+ recordCompactPoolOutcome(outcomeCtx, 499);
515
+ return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
516
+ }
517
+ const outcome = classifyTransportFailureKind(err);
518
+ // Host-level evidence stands regardless of pool membership (#914 review).
519
+ if (outcome === "connect_neutral") {
520
+ recordUpstreamHostFailure(
521
+ upstreamHostHealthKey(route.providerName, safeOriginLabel(compactUrl)),
522
+ { code: transportErrorCode(err) },
523
+ );
524
+ }
525
+ recordCompactPoolOutcome(outcomeCtx, outcome);
526
+ return formatErrorResponse(502, "upstream_error", "Failed to connect to compact upstream");
527
+ }
528
+ }
529
+ }
303
530
  const retryAfter = upstream.headers.get("retry-after");
304
531
  const resetAt = [
305
532
  upstream.headers.get("x-codex-primary-reset-at"),
@@ -310,12 +537,16 @@ export async function handleResponsesCompact(
310
537
  // Record pool health only after the body is fully delivered (or definitively failed).
311
538
  // A premature 200 would clear soft-avoid while the client still sees a buffer 502.
312
539
  if (buffered.status === 499) {
313
- recordCompactPoolOutcome(499);
540
+ recordCompactPoolOutcome(outcomeCtx, 499);
314
541
  return buffered;
315
542
  }
316
543
  // Always record the real upstream status: a local buffering failure after a
317
544
  // 200 upstream response must not soft-avoid a healthy account or rotate a thread.
318
- recordCompactPoolOutcome(upstream.status, { retryAfter, resetAt });
545
+ recordCompactPoolOutcome(outcomeCtx, upstream.status, { retryAfter, resetAt });
546
+ // Lift usage and response metadata from the buffered upstream JSON into the
547
+ // request log; the routed branch gets the same through handleResponses. The
548
+ // synthetic buffer errors are not upstream bodies and stay uninspected.
549
+ if (buffered.ok) inspectResponseLogJson(logCtx, await buffered.clone().text());
319
550
  return buffered;
320
551
  }
321
552
 
@@ -337,7 +568,7 @@ export async function handleResponsesCompact(
337
568
  headers: internalHeaders,
338
569
  body: JSON.stringify(internalBody),
339
570
  });
340
- const response = await handleResponses(internalReq, config, logCtx, { abortSignal: req.signal });
571
+ const response = await handleResponses(internalReq, config, logCtx, { abortSignal: req.signal, turnAdmissionLease });
341
572
  if (!response.ok) return response;
342
573
  let json: { output?: unknown[]; status?: unknown; error?: unknown };
343
574
  try {