@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
@@ -45,6 +45,8 @@ export interface FreeDirectoryProvider {
45
45
  keyOptional?: boolean;
46
46
  models?: string[];
47
47
  liveModels: boolean;
48
+ /** Anthropic-compatible gateways that may close streams before terminal frames. */
49
+ anthropicEofTolerance?: boolean;
48
50
  note?: string;
49
51
  googleMode?: "ai-studio" | "vertex";
50
52
  }
@@ -116,7 +118,7 @@ const CONNECTABLE: Record<string, ConnectableOverride> = {
116
118
  // `unverified` and drops the shared verification date rather than borrowing it.
117
119
  bytez: openAi("https://api.bytez.com/models/v2/openai/v1", "https://bytez.com", { verification: "unverified", lastVerified: undefined, documentationUrl: "https://docs.bytez.com/", discovery: "static", liveModels: false, models: ["meta-llama/Llama-3.3-70B-Instruct", "mistralai/Mistral-7B-Instruct-v0.3", "Qwen/Qwen2.5-72B-Instruct"], note: "The recurring-credit classification is retained from the requested catalog, but the current reset terms could not be independently verified." }),
118
120
  "nous-research": openAi("https://inference-api.nousresearch.com/v1", "https://portal.nousresearch.com", { discovery: "static", liveModels: false, models: ["Hermes-4-405B", "Hermes-4-70B"] }),
119
- agentrouter: { baseUrl: "https://agentrouter.org", dashboardUrl: "https://agentrouter.org", adapter: "anthropic", authKind: "key", supportLevel: "experimental", verification: "primary", modelsUrl: "https://agentrouter.org/v1/models", lastVerified: LAST_VERIFIED, discovery: "live", liveModels: true },
121
+ agentrouter: { baseUrl: "https://agentrouter.org", dashboardUrl: "https://agentrouter.org", adapter: "anthropic", authKind: "key", supportLevel: "experimental", verification: "primary", modelsUrl: "https://agentrouter.org/v1/models", lastVerified: LAST_VERIFIED, discovery: "live", liveModels: true, anthropicEofTolerance: true },
120
122
  ai21: openAi("https://api.ai21.com/studio/v1", "https://studio.ai21.com/account/api-key", { supportLevel: "supported", verification: "official", documentationUrl: "https://docs.ai21.com/reference/models" }),
121
123
  baichuan: openAi("https://api.baichuan-ai.com/v1", "https://platform.baichuan-ai.com/console/apikey", { verification: "official" }),
122
124
  // Verified end-to-end 2026-07-30: /v1/models returns the OpenAI-shaped live catalog (13 models),
@@ -9,7 +9,7 @@
9
9
  * Modelled after src/codex/routing.ts cooldown logic but scoped to plain API-key pools.
10
10
  */
11
11
  import { saveConfigPreservingClaudeCode } from "../config";
12
- import type { OcxConfig, OcxProviderConfig } from "../types";
12
+ import type { OcxConfig, OcxProviderConfig, RateLimitRetryPolicy } from "../types";
13
13
  import { resolveProviderTransport, type OcxProviderTransport } from "./xai-transport";
14
14
  import { sweepExpiredOnWrite } from "../lib/state-store-sweeper";
15
15
 
@@ -22,6 +22,18 @@ interface KeyCooldown {
22
22
  const DEFAULT_COOLDOWN_MS = 60_000;
23
23
  const MAX_COOLDOWN_MS = 10 * 60_000; // cap at 10 min for api-key rotation
24
24
 
25
+ /**
26
+ * Default same-target 429 retry policy used when a provider opts in via a bare
27
+ * `retryOn429: {}` (presence = opt-in with these defaults).
28
+ */
29
+ const DEFAULT_RATE_LIMIT_RETRY = {
30
+ enabled: true,
31
+ attempts: 3,
32
+ intervalMs: 5_000,
33
+ maxIntervalMs: 60_000,
34
+ respectRetryAfter: true,
35
+ } as const satisfies Required<RateLimitRetryPolicy>;
36
+
25
37
  /** Map<`${providerName}\0${keyId}`, KeyCooldown> */
26
38
  const keyCooldowns = new Map<string, KeyCooldown>();
27
39
 
@@ -29,21 +41,32 @@ function cooldownKey(providerName: string, keyId: string): string {
29
41
  return `${providerName}\0${keyId}`;
30
42
  }
31
43
 
44
+ /**
45
+ * Parse an upstream `Retry-After` header: numeric seconds (including `0`) or an HTTP-date.
46
+ * Returns a bounded delay in ms (1..MAX_COOLDOWN_MS), or undefined when the value is
47
+ * malformed. An HTTP-date already in the past yields an immediate (1 ms) retry.
48
+ */
32
49
  function parseRetryAfterMs(value: string | null | undefined, now = Date.now()): number | undefined {
33
50
  const text = value?.trim();
34
51
  if (!text) return undefined;
35
52
  if (/^\d+(?:\.\d+)?$/.test(text)) {
36
53
  const seconds = Number(text);
37
- if (Number.isFinite(seconds) && seconds > 0) {
54
+ if (Number.isFinite(seconds) && seconds >= 0) {
38
55
  return Math.min(Math.max(Math.ceil(seconds * 1000), 1), MAX_COOLDOWN_MS);
39
56
  }
40
57
  }
41
58
  const timestamp = Date.parse(text);
42
59
  if (!Number.isFinite(timestamp)) return undefined;
43
60
  const delay = timestamp - now;
44
- return delay > 0 ? Math.min(delay, MAX_COOLDOWN_MS) : undefined;
61
+ // A valid HTTP-date whose retry time has already passed is an immediate retry, exactly like
62
+ // numeric `Retry-After: 0` — never a malformed-header fallback to the fixed interval.
63
+ return Math.min(Math.max(delay, 1), MAX_COOLDOWN_MS);
45
64
  }
46
65
 
66
+ /**
67
+ * True while the given key is inside its 429 cooldown window (lazily evicting the entry once the
68
+ * window expires). Used to skip keys that the upstream just rate-limited during failover.
69
+ */
47
70
  function isKeyInCooldown(providerName: string, keyId: string, now = Date.now()): boolean {
48
71
  const entry = keyCooldowns.get(cooldownKey(providerName, keyId));
49
72
  if (!entry) return false;
@@ -65,6 +88,51 @@ export function hasKeyPoolFailover(provider: OcxProviderConfig): boolean {
65
88
  return (provider.apiKeyPool?.length ?? 0) >= 2;
66
89
  }
67
90
 
91
+ /**
92
+ * Normalize a provider's `retryOn429` policy, or return null when the knob is absent,
93
+ * explicitly disabled, or the provider is not key-auth (OAuth/forward credentials must not be
94
+ * replayed on the same token, forward passthrough never reaches the recovery loop anyway, and
95
+ * local runtimes have no remote key to preserve). The returned policy is fully defaulted so
96
+ * callers never re-check fields.
97
+ */
98
+ export function rateLimitRetryPolicyFor(
99
+ provider: Pick<OcxProviderConfig, "retryOn429" | "authMode">,
100
+ ): Required<RateLimitRetryPolicy> | null {
101
+ const policy = provider.retryOn429;
102
+ if (!policy || policy.enabled === false) return null;
103
+ // Fail closed: only explicit key auth or the documented omitted-default (undefined == key for
104
+ // custom API-key providers) may use same-key replays. OAuth/forward are never replayed on the
105
+ // same token, local runtimes have no remote key to preserve, and unknown/custom values are
106
+ // rejected rather than guessed at.
107
+ if (provider.authMode !== undefined && provider.authMode !== "key") return null;
108
+ return {
109
+ enabled: policy.enabled ?? DEFAULT_RATE_LIMIT_RETRY.enabled,
110
+ attempts: policy.attempts ?? DEFAULT_RATE_LIMIT_RETRY.attempts,
111
+ intervalMs: policy.intervalMs ?? DEFAULT_RATE_LIMIT_RETRY.intervalMs,
112
+ maxIntervalMs: policy.maxIntervalMs ?? DEFAULT_RATE_LIMIT_RETRY.maxIntervalMs,
113
+ respectRetryAfter: policy.respectRetryAfter ?? DEFAULT_RATE_LIMIT_RETRY.respectRetryAfter,
114
+ };
115
+ }
116
+
117
+ /**
118
+ * Wait before the next same-target replay: upstream Retry-After (seconds or HTTP-date) when
119
+ * `respectRetryAfter` is on and the header parses, capped at `maxIntervalMs`; otherwise the
120
+ * fixed `intervalMs`, also capped at `maxIntervalMs` (a single wait never exceeds the cap).
121
+ * Malformed headers fall back to the fixed interval.
122
+ */
123
+ export function rateLimitRetryDelayMs(
124
+ policy: Required<RateLimitRetryPolicy>,
125
+ retryAfterHeader: string | null | undefined,
126
+ now = Date.now(),
127
+ ): number {
128
+ const raw = retryAfterHeader?.trim();
129
+ if (policy.respectRetryAfter && raw) {
130
+ const parsed = parseRetryAfterMs(raw, now);
131
+ if (parsed !== undefined) return Math.min(parsed, policy.maxIntervalMs);
132
+ }
133
+ return Math.min(policy.intervalMs, policy.maxIntervalMs);
134
+ }
135
+
68
136
  /**
69
137
  * Record a 429 for the current key and attempt to switch to the next available one.
70
138
  *
@@ -1,9 +1,11 @@
1
1
  import { resolveEnvValue } from "../config";
2
2
  import {
3
+ CodexPoolAuthenticationError,
3
4
  headersForCodexAuthContext,
4
5
  hasCallerCodexBearer,
5
6
  isCodexAuthContextUsable,
6
7
  resolveCodexAuthContext,
8
+ type CodexAccountSelectionAdmission,
7
9
  type CodexAuthContext,
8
10
  } from "../codex/auth-context";
9
11
  import { recordCodexUpstreamOutcome, type CodexUpstreamOutcome } from "../codex/routing";
@@ -29,6 +31,17 @@ export interface ResolvedOpenAiForwardSidecar extends OpenAiForwardSidecarCandid
29
31
  recordOutcome?: (outcome: CodexUpstreamOutcome) => void;
30
32
  }
31
33
 
34
+ /**
35
+ * Server-resolved exact account selection for a ChatGPT sidecar call.
36
+ * `accountId` must come from a validated account-qualified route; request headers
37
+ * are never trusted as account ids. `modelId` is the sidecar's actual upstream
38
+ * model so cooldown admission and outcome recording use the correct quota scope.
39
+ */
40
+ export interface ExactOpenAiSidecarAccount {
41
+ accountId: string;
42
+ modelId: string;
43
+ }
44
+
32
45
  export interface OpenAiImagesProviderSelection {
33
46
  forwardCandidates: OpenAiForwardSidecarCandidate[];
34
47
  keyed?: {
@@ -41,11 +54,18 @@ export interface OpenAiImagesProviderSelection {
41
54
 
42
55
  export function listOpenAiForwardSidecarCandidates(config: OcxConfig): OpenAiForwardSidecarCandidate[] {
43
56
  const provider = config.providers[OPENAI_CODEX_PROVIDER_ID];
44
- if (!provider || provider.disabled === true || !isCanonicalOpenAiForwardProvider(provider)) return [];
57
+ if (!provider || provider.disabled === true) return [];
58
+ // The built-in registry defaults an omitted authMode to forward. Normalize only that
59
+ // missing field before the strict adapter/destination check; explicit key mode and
60
+ // noncanonical destinations remain ineligible for ChatGPT credential injection.
61
+ const canonicalProvider = provider.authMode === undefined
62
+ ? { ...provider, authMode: "forward" as const }
63
+ : provider;
64
+ if (!isCanonicalOpenAiForwardProvider(canonicalProvider)) return [];
45
65
  return [{
46
66
  providerName: OPENAI_CODEX_PROVIDER_ID,
47
- provider,
48
- accountMode: providerCodexAccountMode(OPENAI_CODEX_PROVIDER_ID, provider) ?? "pool",
67
+ provider: canonicalProvider,
68
+ accountMode: providerCodexAccountMode(OPENAI_CODEX_PROVIDER_ID, canonicalProvider) ?? "pool",
49
69
  }];
50
70
  }
51
71
 
@@ -70,7 +90,12 @@ export async function resolveFirstUsableOpenAiSidecar(
70
90
  candidates: readonly OpenAiForwardSidecarCandidate[],
71
91
  incomingHeaders: Headers,
72
92
  config: OcxConfig,
93
+ options: {
94
+ exactAccount?: ExactOpenAiSidecarAccount;
95
+ beginCodexAccountSelection?: () => CodexAccountSelectionAdmission | undefined;
96
+ } = {},
73
97
  ): Promise<ResolvedOpenAiForwardSidecar | undefined> {
98
+ const { exactAccount } = options;
74
99
  let callerBearerMayBeForwarded = true;
75
100
  try {
76
101
  validateForwardAdmissionCredential(incomingHeaders, config);
@@ -79,6 +104,39 @@ export async function resolveFirstUsableOpenAiSidecar(
79
104
  callerBearerMayBeForwarded = false;
80
105
  }
81
106
  for (const candidate of candidates) {
107
+ if (exactAccount) {
108
+ // An account-qualified model is an explicit user choice. Resolve the stored
109
+ // credential directly even when the provider is globally Direct, and never
110
+ // consult Pool active state, affinity, probes, or alternates.
111
+ const authContext = await resolveCodexAuthContext(incomingHeaders, config, "pool", {
112
+ accountId: exactAccount.accountId,
113
+ modelId: exactAccount.modelId,
114
+ beginCodexAccountSelection: options.beginCodexAccountSelection,
115
+ });
116
+ if ((authContext.kind !== "pool" && authContext.kind !== "main-pool")
117
+ || !isCodexAuthContextUsable(authContext, config)) {
118
+ // Exact selection is fail-closed. A generation/runtime-state race must not fall through
119
+ // to the caller-bearer error or let a later candidate select another account.
120
+ throw new CodexPoolAuthenticationError("Selected Codex account is unavailable");
121
+ }
122
+ return {
123
+ ...candidate,
124
+ authContext,
125
+ headers: headersForCodexAuthContext(incomingHeaders, authContext),
126
+ recordOutcome: (outcome: CodexUpstreamOutcome) => recordCodexUpstreamOutcome(
127
+ config,
128
+ authContext.accountId,
129
+ outcome,
130
+ {
131
+ modelId: exactAccount.modelId,
132
+ fixedAccount: true,
133
+ probeLeaseId: authContext.probeLeaseId,
134
+ probeQuotaScope: authContext.probeQuotaScope,
135
+ writerGeneration: authContext.writerGeneration,
136
+ },
137
+ ),
138
+ };
139
+ }
82
140
  if (candidate.accountMode === "direct") {
83
141
  if (!callerBearerMayBeForwarded || !hasCallerCodexBearer(incomingHeaders)) continue;
84
142
  const headers = directSidecarHeaders(incomingHeaders);
@@ -89,7 +147,9 @@ export async function resolveFirstUsableOpenAiSidecar(
89
147
  headers,
90
148
  };
91
149
  }
92
- const authContext = await resolveCodexAuthContext(incomingHeaders, config, candidate.accountMode);
150
+ const authContext = await resolveCodexAuthContext(incomingHeaders, config, candidate.accountMode, {
151
+ beginCodexAccountSelection: options.beginCodexAccountSelection,
152
+ });
93
153
  if (!isCodexAuthContextUsable(authContext, config)) continue;
94
154
  return {
95
155
  ...candidate,
@@ -62,6 +62,7 @@ export function applyOpenAiVirtualModel(
62
62
  logCtx.resolvedModel = resolution.wireModelId;
63
63
  route.modelId = resolution.wireModelId;
64
64
  parsed.modelId = resolution.wireModelId;
65
+ parsed._openAiVirtualSelectedModelId = resolution.selectedModelId;
65
66
 
66
67
  if (parsed._rawBody && typeof parsed._rawBody === "object" && !Array.isArray(parsed._rawBody)) {
67
68
  const raw = parsed._rawBody as Record<string, unknown>;