@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
@@ -4,14 +4,17 @@ import {
4
4
  apiKeyTransportConfigError,
5
5
  booleanRecordConfigError,
6
6
  modelAdapterRecordConfigError,
7
+ modelPreferHostedToolsConfigError,
7
8
  codexAutoStartEnabled,
8
9
  positiveIntegerConfigError,
9
10
  positiveIntegerRecordConfigError,
10
11
  providerBaseUrlConfigError,
11
12
  providerHeadersConfigError,
12
13
  reasoningSummaryDeliveryRecordConfigError,
14
+ retryOn429PolicyConfigError,
13
15
  } from "../config";
14
16
  import { providerDestinationConfigError } from "../lib/destination-policy";
17
+ import { redactSecretString } from "../lib/redact";
15
18
  import { getProviderRegistryEntry, providerCodexAccountMode, providerMatchesRegistryTransport, registryEntryForProviderDestination } from "../providers/registry";
16
19
  import { providerConfigSeed } from "../providers/derive";
17
20
  import type { OcxConfig, OcxProviderConfig } from "../types";
@@ -80,15 +83,28 @@ export function isAllowedRequestOrigin(req: Request, config: OcxConfig): boolean
80
83
 
81
84
  function isExtraAllowedOrigin(origin: string, cfg: OcxConfig): boolean {
82
85
  if (!cfg.corsAllowOrigins?.length) return false;
86
+ const parsedOrigin = comparableOrigin(origin);
83
87
  return cfg.corsAllowOrigins.some(allowed => {
84
- try {
85
- return new URL(allowed).origin === new URL(origin).origin;
86
- } catch {
87
- return allowed === origin;
88
- }
88
+ const parsedAllowed = comparableOrigin(allowed);
89
+ return parsedOrigin !== null && parsedAllowed !== null
90
+ ? parsedAllowed === parsedOrigin
91
+ : allowed === origin;
89
92
  });
90
93
  }
91
94
 
95
+ function comparableOrigin(value: string): string | null {
96
+ try {
97
+ const parsed = new URL(value);
98
+ if (parsed.origin !== "null") return parsed.origin;
99
+ // WHATWG URL exposes authority-based custom schemes (for example browser
100
+ // extensions) as opaque `null` origins. Compare their scheme + authority so
101
+ // one allowlisted extension cannot admit every other opaque origin.
102
+ return parsed.host ? `${parsed.protocol}//${parsed.host}` : null;
103
+ } catch {
104
+ return null;
105
+ }
106
+ }
107
+
92
108
  export function managementRequestOrigin(req: Request, config: OcxConfig): string | null {
93
109
  const host = req.headers.get("Host");
94
110
  const parsedHost = parseHttpHost(host);
@@ -375,6 +391,11 @@ function sameCanonicalProviderSeed(actual: Record<string, unknown>, expected: Oc
375
391
  return actualKeys.every(key => JSON.stringify(actual[key]) === JSON.stringify((expected as unknown as Record<string, unknown>)[key]));
376
392
  }
377
393
 
394
+ /**
395
+ * Validate a provider object arriving at the management write boundary. Returns an error
396
+ * string, or null when the provider may be persisted. Caller-controlled names/fields are
397
+ * redacted and JSON-escaped so secrets never reach the response.
398
+ */
378
399
  export function providerManagementConfigError(name: unknown, provider: unknown): string | null {
379
400
  if (typeof name !== "string" || !provider || typeof provider !== "object" || Array.isArray(provider)) {
380
401
  return "provider must be a plain object";
@@ -392,7 +413,9 @@ export function providerManagementConfigError(name: unknown, provider: unknown):
392
413
  return "provider openai codexAccountMode must be pool or direct";
393
414
  }
394
415
  if (seed) seed.codexAccountMode = raw.codexAccountMode;
395
- const canonical = seed && sameCanonicalProviderSeed(raw, seed);
416
+ const canonicalCandidate = { ...raw };
417
+ delete canonicalCandidate.responsesSnapshotRepair;
418
+ const canonical = seed && sameCanonicalProviderSeed(canonicalCandidate, seed);
396
419
  if (!canonical) {
397
420
  return `provider ${name} must equal the canonical built-in provider seed`;
398
421
  }
@@ -406,6 +429,12 @@ export function providerManagementConfigError(name: unknown, provider: unknown):
406
429
  if (destinationError) return `provider ${name} ${destinationError}`;
407
430
  const headersError = providerHeadersConfigError(typed.headers);
408
431
  if (headersError) return `provider ${name} ${headersError}`;
432
+ const retryOn429Error = retryOn429PolicyConfigError(raw.retryOn429);
433
+ if (retryOn429Error) {
434
+ // The provider name is caller-controlled and can be token-shaped; redact and JSON-escape
435
+ // it before it reaches the management API response.
436
+ return `provider ${JSON.stringify(redactSecretString(name))} ${retryOn429Error}`;
437
+ }
409
438
  const apiKeyTransportError = apiKeyTransportConfigError(typed);
410
439
  if (apiKeyTransportError) return `provider ${name} ${apiKeyTransportError}`;
411
440
  const maxInputError = positiveIntegerRecordConfigError(raw.modelMaxInputTokens, "modelMaxInputTokens");
@@ -419,6 +448,16 @@ export function providerManagementConfigError(name: unknown, provider: unknown):
419
448
  if (reasoningSummaryDeliveryError) return `provider ${name} ${reasoningSummaryDeliveryError}`;
420
449
  const modelAdaptersError = modelAdapterRecordConfigError(raw.modelAdapters, "modelAdapters", name, typed);
421
450
  if (modelAdaptersError) return `provider ${name} ${modelAdaptersError}`;
451
+ const preferHostedToolsError = modelPreferHostedToolsConfigError(
452
+ raw.modelPreferHostedTools,
453
+ "modelPreferHostedTools",
454
+ name,
455
+ typed,
456
+ );
457
+ if (preferHostedToolsError) return `provider ${name} ${preferHostedToolsError}`;
458
+ if (raw.responsesSnapshotRepair !== undefined && typeof raw.responsesSnapshotRepair !== "boolean") {
459
+ return `provider ${name} responsesSnapshotRepair must be a boolean`;
460
+ }
422
461
  const defaultMaxOutputError = positiveIntegerConfigError(raw.defaultMaxOutputTokens, "defaultMaxOutputTokens");
423
462
  if (defaultMaxOutputError) return `provider ${name} ${defaultMaxOutputError}`;
424
463
  const maxOutputError = positiveIntegerRecordConfigError(raw.modelMaxOutputTokens, "modelMaxOutputTokens");
@@ -496,6 +535,7 @@ export function safeConfigDTO(config: OcxConfig): unknown {
496
535
  "modelOpenRouterRouting",
497
536
  "reasoningEfforts",
498
537
  "modelReasoningEfforts",
538
+ "reasoningWireFormat",
499
539
  "noVisionModels",
500
540
  "noReasoningModels",
501
541
  "noTemperatureModels",
@@ -18,7 +18,8 @@ import { classifyError, CYBER_POLICY_ERROR_CODE, isCyberPolicyCode } from "../li
18
18
  import { redactSecretString } from "../lib/redact";
19
19
  import { resolveClientRetryAfter } from "../lib/retry-after";
20
20
  import { estimateTokens } from "../lib/token-estimate";
21
- import { routeModel } from "../router";
21
+ import { NoEligiblePolicyCandidateError, routeModel } from "../router";
22
+ import { evidenceFromBody } from "../routing/request-evidence";
22
23
  import { resolveWireProtocolOverride } from "./adapter-resolve";
23
24
  import type { OcxConfig } from "../types";
24
25
  import { readJsonRequestBody } from "./request-decompress";
@@ -32,6 +33,7 @@ import {
32
33
  import { responseWithDeferredRequestLog } from "./relay";
33
34
  import { handleResponses } from "./responses";
34
35
  import type { AdmissionLease } from "../lib/admission";
36
+ import { tryClaimNativeMainProfileForTurn } from "../codex/native-main-admission";
35
37
  import {
36
38
  createTranslatorBudget,
37
39
  finalizeTranslatorBudgetResponse,
@@ -109,7 +111,7 @@ async function handleChatCompletionsWithBudget(
109
111
  let nativeRoute = false;
110
112
  let directRoute = false;
111
113
  try {
112
- const route = routeModel(config, internalBody.model as string);
114
+ const route = routeModel(config, internalBody.model as string, evidenceFromBody(internalBody));
113
115
  // Settle the wire once so every branch below reads the adapter this model will
114
116
  // actually use, not the provider-wide default (#404).
115
117
  route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, "chat");
@@ -117,6 +119,7 @@ async function handleChatCompletionsWithBudget(
117
119
  logCtx.providerAdapter = route.provider.adapter;
118
120
  logCtx.requestedModel = requestedModel;
119
121
  logCtx.provider = route.providerName;
122
+ logCtx.routeDecision = route.routeDecision;
120
123
  if (route.provider.adapter === "openai-responses") {
121
124
  nativeRoute = true;
122
125
  directRoute = route.codexAccountMode === "direct";
@@ -146,7 +149,12 @@ async function handleChatCompletionsWithBudget(
146
149
  const ladder = supportedLadderFor({ provider: route.provider, modelId: route.modelId });
147
150
  if (ladder !== undefined && ladder.length === 0) delete internalBody.reasoning;
148
151
  }
149
- } catch {
152
+ } catch (err) {
153
+ if (err instanceof NoEligiblePolicyCandidateError) {
154
+ logCtx.routeDecision = err.trace;
155
+ if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, 404, { closeReason: "non_stream" });
156
+ return chatCompletionsErrorResponse(404, err.message, "invalid_request_error");
157
+ }
150
158
  /* unknown model: let handleResponses shape the 404 */
151
159
  }
152
160
  void nativeRoute;
@@ -158,15 +166,22 @@ async function handleChatCompletionsWithBudget(
158
166
  if (value) headers.set(name, value);
159
167
  }
160
168
  // Prefer main ChatGPT auth so OpenAI-backed sidecars remain reachable on routed turns.
161
- if (!directRoute) try {
162
- const { getMainAccountToken } = await import("../codex/main-account");
163
- const token = getMainAccountToken();
164
- if (token) {
165
- headers.set("authorization", `Bearer ${token.accessToken}`);
166
- headers.set("chatgpt-account-id", token.chatgptAccountId);
169
+ if (!directRoute) {
170
+ // This enrichment is optional for routed/non-main providers. If native main
171
+ // is fenced, omit it and let auth-context reject only a final physical-main
172
+ // selection while healthy pool/provider routes continue.
173
+ if (tryClaimNativeMainProfileForTurn(logIds?.turnAdmissionLease)) {
174
+ try {
175
+ const { getMainAccountToken } = await import("../codex/main-account");
176
+ const token = getMainAccountToken();
177
+ if (token) {
178
+ headers.set("authorization", `Bearer ${token.accessToken}`);
179
+ headers.set("chatgpt-account-id", token.chatgptAccountId);
180
+ }
181
+ } catch {
182
+ /* optional */
183
+ }
167
184
  }
168
- } catch {
169
- /* optional */
170
185
  }
171
186
 
172
187
  const internalBodyJson = JSON.stringify(internalBody);
@@ -316,7 +331,7 @@ async function handleChatCompletionsWithBudget(
316
331
  const classified = classifyError(502, error?.type ?? "server_error", message);
317
332
  if (error?.code === "translation_buffer_limit") {
318
333
  classified.code = "translation_buffer_limit";
319
- classified.type = "invalid_request_error";
334
+ classified.type = "upstream_error";
320
335
  } else if (isCyberPolicyCode(error?.code)) {
321
336
  classified.code = CYBER_POLICY_ERROR_CODE;
322
337
  classified.type = "invalid_request_error";
@@ -327,7 +342,7 @@ async function handleChatCompletionsWithBudget(
327
342
  }
328
343
  return chatCompletionsErrorResponse(
329
344
  classified.code === "translation_buffer_limit"
330
- ? 413
345
+ ? 502
331
346
  : isCyberPolicyCode(classified.code) ? 400 : 502,
332
347
  message,
333
348
  classified.type,
@@ -25,7 +25,8 @@ import {
25
25
  } from "../claude/outbound";
26
26
  import { clearableDeadline, idleDeadline } from "../lib/abort";
27
27
  import { estimateTokens } from "../lib/token-estimate";
28
- import { routeModel } from "../router";
28
+ import { NoEligiblePolicyCandidateError, routeModel } from "../router";
29
+ import { evidenceFromBody } from "../routing/request-evidence";
29
30
  import { resolveWireProtocolOverride } from "./adapter-resolve";
30
31
  import type { OcxConfig } from "../types";
31
32
  import { readJsonRequestBody } from "./request-decompress";
@@ -34,6 +35,7 @@ import { conversationIdFromClaudeMetadata } from "./request-log-conversation";
34
35
  import { responseWithDeferredRequestLog } from "./relay";
35
36
  import { handleResponses } from "./responses";
36
37
  import type { AdmissionLease } from "../lib/admission";
38
+ import { tryClaimNativeMainProfileForTurn } from "../codex/native-main-admission";
37
39
  import {
38
40
  createTranslatorBudget,
39
41
  finalizeTranslatorBudgetResponse,
@@ -627,10 +629,11 @@ async function handleClaudeMessagesWithBudget(
627
629
  // verified live 2026-07-11). Strip them for that route; routed providers keep them.
628
630
  let nativeRoute = false;
629
631
  try {
630
- const route = routeModel(config, internalBody.model as string);
632
+ const route = routeModel(config, internalBody.model as string, evidenceFromBody(internalBody));
631
633
  // Settle the wire once so the sampling decision below reads the effective
632
634
  // adapter rather than the provider-wide default (#404).
633
635
  route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, "anthropic");
636
+ logCtx.routeDecision = route.routeDecision;
634
637
  if (route.provider.adapter === "openai-responses") {
635
638
  nativeRoute = true;
636
639
  delete internalBody.max_output_tokens;
@@ -661,7 +664,14 @@ async function handleClaudeMessagesWithBudget(
661
664
  const ladder = supportedLadderFor({ provider: route.provider, modelId: route.modelId });
662
665
  if (ladder !== undefined && ladder.length === 0) delete internalBody.reasoning;
663
666
  }
664
- } catch { /* unknown model: let handleResponses shape the 404 */ }
667
+ } catch (err) {
668
+ if (err instanceof NoEligiblePolicyCandidateError) {
669
+ logCtx.routeDecision = err.trace;
670
+ if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, 404, { closeReason: "non_stream" });
671
+ return anthropicErrorResponse(404, err.message, "invalid_request_error");
672
+ }
673
+ /* unknown model: let handleResponses shape the 404 */
674
+ }
665
675
 
666
676
  const headers = new Headers({ "content-type": "application/json" });
667
677
  for (const name of FORWARD_HEADERS) {
@@ -671,8 +681,11 @@ async function handleClaudeMessagesWithBudget(
671
681
  const value = req.headers.get(name);
672
682
  if (value) headers.set(name, value);
673
683
  }
674
- if (!nativeRoute) {
675
- // Routed replays need main ChatGPT auth so OpenAI-backed sidecars remain reachable.
684
+ // Routed replays need main ChatGPT auth so OpenAI-backed sidecars remain reachable;
685
+ // native replays have no caller ChatGPT credential. This enrichment is optional:
686
+ // auth-context later rejects a real physical-main selection, while routed/pool
687
+ // traffic continues without reading native credentials during a fence/recovery.
688
+ if (tryClaimNativeMainProfileForTurn(logIds?.turnAdmissionLease)) {
676
689
  const { getMainAccountToken } = await import("../codex/main-account");
677
690
  const token = getMainAccountToken();
678
691
  if (token) {
@@ -681,14 +694,6 @@ async function handleClaudeMessagesWithBudget(
681
694
  }
682
695
  }
683
696
  if (nativeRoute) {
684
- // No forwarded ChatGPT auth exists on this surface. Attach the main codex login
685
- // (read-only auth.json token); account-pool rotation still overrides downstream.
686
- const { getMainAccountToken } = await import("../codex/main-account");
687
- const token = getMainAccountToken();
688
- if (token) {
689
- headers.set("authorization", `Bearer ${token.accessToken}`);
690
- headers.set("chatgpt-account-id", token.chatgptAccountId);
691
- }
692
697
  // ChatGPT-backend prompt-cache affinity rides the session_id HEADER (codex
693
698
  // clients always send their session uuid; devlog 090 follow-up: body-level
694
699
  // prompt_cache_key alone still yielded cached_tokens:0). Claude Code never sends
@@ -763,8 +768,11 @@ async function handleClaudeMessagesWithBudget(
763
768
  // rewrite): log = upstream truth, client = retry signal.
764
769
  // Retryable 429s also get Retry-After (#507) so Codex-shaped clients and Claude Code
765
770
  // share a backoff hint when the upstream omitted the header.
766
- const transient = isTransientUpstreamStatus(response.status);
767
- const outStatus = transient ? 529 : response.status;
771
+ const nativeMainFence = response.status === 503
772
+ && upstreamRetryAfter?.trim() === "1"
773
+ && message === "Native Codex main profile is switching; retry this request";
774
+ const transient = !nativeMainFence && isTransientUpstreamStatus(response.status);
775
+ const outStatus = nativeMainFence ? 503 : transient ? 529 : response.status;
768
776
  const out = new Response(JSON.stringify(anthropicErrorBody(outStatus, message)), {
769
777
  status: outStatus,
770
778
  headers: {
@@ -56,16 +56,25 @@ function isFile(path: string): boolean {
56
56
  }
57
57
  }
58
58
 
59
- function htmlResponse(path: string, session?: GuiSessionBootstrap): Response {
60
- let html = readFileSync(path, "utf8");
61
- if (session) {
62
- const bootstrap = [
63
- `<meta name="opencodex-session-token" content="${session.token}">`,
64
- `<meta name="opencodex-session-csrf" content="${session.csrfToken}">`,
65
- `<meta name="opencodex-session-origin" content="${session.origin}">`,
66
- ].join("");
67
- html = html.includes("</head>") ? html.replace("</head>", `${bootstrap}</head>`) : `${bootstrap}${html}`;
68
- }
59
+ /** HTML-attribute escape for values interpolated into meta tags. */
60
+ function escapeHtmlAttribute(value: string): string {
61
+ return value
62
+ .replaceAll("&", "&amp;")
63
+ .replaceAll('"', "&quot;")
64
+ .replaceAll("<", "&lt;")
65
+ .replaceAll(">", "&gt;");
66
+ }
67
+
68
+ /** Shared session meta-tag block, escaped for quoted attribute interpolation. */
69
+ function sessionBootstrapMeta(session: GuiSessionBootstrap): string {
70
+ return [
71
+ `<meta name="opencodex-session-token" content="${escapeHtmlAttribute(session.token)}">`,
72
+ `<meta name="opencodex-session-csrf" content="${escapeHtmlAttribute(session.csrfToken)}">`,
73
+ `<meta name="opencodex-session-origin" content="${escapeHtmlAttribute(session.origin)}">`,
74
+ ].join("");
75
+ }
76
+
77
+ function htmlDocumentResponse(html: string): Response {
69
78
  return new Response(html, {
70
79
  headers: {
71
80
  "Content-Type": "text/html",
@@ -76,6 +85,26 @@ function htmlResponse(path: string, session?: GuiSessionBootstrap): Response {
76
85
  });
77
86
  }
78
87
 
88
+ function htmlResponse(path: string, session?: GuiSessionBootstrap): Response {
89
+ let html = readFileSync(path, "utf8");
90
+ if (session) {
91
+ const bootstrap = sessionBootstrapMeta(session);
92
+ html = html.includes("</head>") ? html.replace("</head>", `${bootstrap}</head>`) : `${bootstrap}${html}`;
93
+ }
94
+ return htmlDocumentResponse(html);
95
+ }
96
+
97
+ /**
98
+ * Minimal session-bootstrap document, independent of any packaged GUI build. The dev
99
+ * GUI (Vite) proxies /opencodex-session to the backend with the original host so the
100
+ * backend can mint an origin-bound loopback session even when gui/dist does not exist.
101
+ */
102
+ export function serveSessionBootstrap(session: GuiSessionBootstrap): Response {
103
+ const bootstrap = sessionBootstrapMeta(session);
104
+ const html = `<!doctype html><html><head><meta charset="utf-8">${bootstrap}</head><body></body></html>`;
105
+ return htmlDocumentResponse(html);
106
+ }
107
+
79
108
  export function serveGuiFile(
80
109
  pathname: string,
81
110
  guiDist = findGuiDist(),
@@ -15,8 +15,10 @@
15
15
  import { formatErrorResponse } from "../bridge";
16
16
  import {
17
17
  CodexAccountCooldownError,
18
+ codexMainProfileDrainingResponse,
18
19
  cooldownErrorResponse,
19
20
  CodexAuthContextError,
21
+ CodexMainProfileDrainingError,
20
22
  CodexPoolAuthenticationError,
21
23
  CodexThreadAffinityExpiredError,
22
24
  } from "../codex/auth-context";
@@ -35,6 +37,8 @@ import { safeAntigravityHttpErrorMessage } from "../adapters/google-errors";
35
37
  import { sanitizeUpstreamErrorText } from "../adapters/upstream-http-error";
36
38
  import { ANTIGRAVITY_REQUEST_UA } from "../adapters/google-antigravity-wire";
37
39
  import { decodeValidatedImageBase64, MAX_ENCODED_BYTES_PER_IMAGE } from "../images/artifacts";
40
+ import type { AdmissionLease } from "../lib/admission";
41
+ import { codexAccountSelectionForTurn } from "./lifecycle";
38
42
 
39
43
  export type ImagesEndpoint = "generations" | "edits";
40
44
 
@@ -323,6 +327,7 @@ export async function handleImages(
323
327
  config: OcxConfig,
324
328
  endpoint: ImagesEndpoint,
325
329
  logCtx: RequestLogContext,
330
+ turnAdmissionLease?: AdmissionLease,
326
331
  ): Promise<Response> {
327
332
  const candidates = selectImagesProvider(config);
328
333
  if (candidates.error) {
@@ -375,11 +380,15 @@ export async function handleImages(
375
380
  let forwardAuthError: Response | undefined;
376
381
  if (canUseOpenAiForward) {
377
382
  try {
378
- forward = await resolveFirstUsableOpenAiSidecar(candidates.forwardCandidates, req.headers, config);
383
+ forward = await resolveFirstUsableOpenAiSidecar(candidates.forwardCandidates, req.headers, config, {
384
+ beginCodexAccountSelection: codexAccountSelectionForTurn(turnAdmissionLease),
385
+ });
379
386
  if (forward) logCtx.provider = formatCodexProviderForLog(forward.providerName, codexLogAccountId(forward.authContext), config);
380
387
  } catch (err) {
381
388
  if (err instanceof CodexAccountCooldownError) {
382
389
  forwardAuthError = cooldownErrorResponse(err);
390
+ } else if (err instanceof CodexMainProfileDrainingError) {
391
+ forwardAuthError = codexMainProfileDrainingResponse();
383
392
  } else if (err instanceof CodexThreadAffinityExpiredError) {
384
393
  forwardAuthError = formatErrorResponse(409, "invalid_request_error", "Codex thread account affinity expired; start a new session");
385
394
  } else if (err instanceof CodexAuthContextError) {