@bitkyc08/opencodex 2.10.2 → 2.11.0

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 (121) hide show
  1. package/README.md +31 -0
  2. package/bin/ocx.mjs +10 -0
  3. package/gui/dist/assets/index-Bk-PN-70.css +1 -0
  4. package/gui/dist/assets/index-BynIEIV-.js +70 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +4 -2
  7. package/src/adapters/cursor/effort-map.ts +11 -0
  8. package/src/adapters/cursor/live-transport.ts +11 -0
  9. package/src/adapters/cursor/native-exec-fs.ts +9 -6
  10. package/src/adapters/cursor/native-exec.ts +4 -2
  11. package/src/adapters/cursor/protobuf-events.ts +176 -4
  12. package/src/adapters/cursor/request-builder.ts +15 -4
  13. package/src/adapters/cursor/tool-definitions.ts +118 -2
  14. package/src/adapters/google.ts +15 -5
  15. package/src/adapters/openai-chat.ts +24 -2
  16. package/src/adapters/openai-responses.ts +2 -1
  17. package/src/bridge.ts +9 -5
  18. package/src/chat/outbound.ts +4 -3
  19. package/src/claude/desktop-3p.ts +222 -2
  20. package/src/claude/outbound.ts +15 -6
  21. package/src/cli/account-api.ts +4 -0
  22. package/src/cli/account-extended.ts +112 -0
  23. package/src/cli/account.ts +23 -6
  24. package/src/cli/claude-desktop.ts +26 -3
  25. package/src/cli/config-command.ts +9 -0
  26. package/src/cli/help.ts +18 -2
  27. package/src/cli/index.ts +277 -55
  28. package/src/cli/models.ts +5 -1
  29. package/src/cli/provider.ts +8 -2
  30. package/src/cli/ready.ts +301 -0
  31. package/src/cli/system-restart-client.ts +146 -0
  32. package/src/cli/tray-proxy.ts +153 -6
  33. package/src/clients/config-export.ts +12 -19
  34. package/src/codex/account-lifecycle.ts +3 -0
  35. package/src/codex/account-namespaces.ts +49 -3
  36. package/src/codex/account-priority.ts +83 -0
  37. package/src/codex/auth-api.ts +83 -0
  38. package/src/codex/auth-context.ts +5 -2
  39. package/src/codex/catalog/provider-fetch.ts +11 -0
  40. package/src/codex/catalog/sync.ts +23 -1
  41. package/src/codex/codex-write-lock.ts +16 -4
  42. package/src/codex/desired-state.ts +37 -4
  43. package/src/codex/history-job.ts +15 -5
  44. package/src/codex/history-provider.ts +31 -14
  45. package/src/codex/history-worker.ts +28 -4
  46. package/src/codex/inject-coordination.ts +13 -1
  47. package/src/codex/inject.ts +360 -66
  48. package/src/codex/internal/history-writer.ts +1 -1
  49. package/src/codex/native-main-lock-file.ts +5 -1
  50. package/src/codex/native-main-owner.ts +17 -3
  51. package/src/codex/native-profile-manager.ts +19 -0
  52. package/src/codex/native-profile-startup.ts +8 -0
  53. package/src/codex/native-residue.ts +140 -27
  54. package/src/codex/pool-rotation.ts +74 -4
  55. package/src/codex/refresh.ts +7 -0
  56. package/src/codex/routing.ts +177 -36
  57. package/src/codex/subagent-model-fallback.ts +34 -4
  58. package/src/codex/sync.ts +61 -0
  59. package/src/codex/upstream-host-health.ts +329 -31
  60. package/src/combos/request.ts +2 -0
  61. package/src/config.ts +221 -2
  62. package/src/images/loop.ts +1 -1
  63. package/src/integrations/native/ownership-preflight.ts +39 -2
  64. package/src/lib/bun-stream-caps.ts +3 -3
  65. package/src/lib/sse-decoder.ts +41 -0
  66. package/src/lib/system-restart-contract.ts +73 -0
  67. package/src/lib/windows-secret-acl.ts +141 -39
  68. package/src/lib/windows-user-principal.ts +283 -0
  69. package/src/lib/winsw.ts +18 -2
  70. package/src/oauth/key-providers.ts +12 -0
  71. package/src/providers/derive.ts +54 -2
  72. package/src/providers/free-directory.ts +6 -5
  73. package/src/providers/model-discovery.ts +9 -3
  74. package/src/providers/quota.ts +592 -0
  75. package/src/providers/registry.ts +316 -13
  76. package/src/responses/parser.ts +26 -10
  77. package/src/responses/reasoning-replay-cache.ts +1 -0
  78. package/src/routing/profile-namespace.ts +15 -0
  79. package/src/routing/profile.ts +2 -1
  80. package/src/server/auth-cors.ts +44 -13
  81. package/src/server/chat-completions.ts +0 -4
  82. package/src/server/claude-messages.ts +73 -15
  83. package/src/server/github-copilot-responses-repair.ts +338 -0
  84. package/src/server/index.ts +328 -111
  85. package/src/server/lifecycle.ts +36 -0
  86. package/src/server/management/agent-settings-routes.ts +147 -56
  87. package/src/server/management/config-routes.ts +7 -2
  88. package/src/server/management/context.ts +4 -0
  89. package/src/server/management/native-integration-routes.ts +199 -20
  90. package/src/server/management/provider-routes.ts +41 -0
  91. package/src/server/management/routing-profile-routes.ts +234 -5
  92. package/src/server/management/system-restart.ts +12 -10
  93. package/src/server/management/system-routes.ts +20 -0
  94. package/src/server/management-auth.ts +51 -3
  95. package/src/server/ports.ts +41 -1
  96. package/src/server/proxy-liveness.ts +129 -4
  97. package/src/server/readiness.ts +99 -0
  98. package/src/server/relay.ts +113 -97
  99. package/src/server/request-log.ts +10 -4
  100. package/src/server/responses/compact.ts +107 -12
  101. package/src/server/responses/core.ts +220 -39
  102. package/src/server/responses-item-id-repair.ts +22 -3
  103. package/src/server/responses-model-rewrite.ts +29 -0
  104. package/src/server/sse-frame-buffer.ts +292 -0
  105. package/src/server/sse-payload-rewrite.ts +25 -14
  106. package/src/server/ws-bridge.ts +27 -22
  107. package/src/service-manager-probe.ts +520 -10
  108. package/src/service.ts +134 -2
  109. package/src/storage/worker-lifecycle.ts +14 -14
  110. package/src/tray/windows-tray.ps1 +74 -9
  111. package/src/types.ts +68 -2
  112. package/src/update/index.ts +12 -0
  113. package/src/update/job.ts +392 -18
  114. package/src/update/npm-cache-preflight.d.mts +47 -0
  115. package/src/update/npm-cache-preflight.mjs +201 -0
  116. package/src/usage/log.ts +1 -1
  117. package/src/vision/index.ts +77 -2
  118. package/src/web-search/loop.ts +1 -1
  119. package/src/web-search/parse.ts +4 -1
  120. package/gui/dist/assets/index-BKVqyYqT.js +0 -70
  121. package/gui/dist/assets/index-Ca_3269W.css +0 -1
@@ -196,6 +196,8 @@ export interface ProviderRegistryEntry {
196
196
  supportsServiceTier?: boolean;
197
197
  /** Registry default for plaintext reasoning replay; see `OcxProviderConfig.preserveResponsesReasoningContent`. Registry-only like `supportsServiceTier`. */
198
198
  preserveResponsesReasoningContent?: boolean;
199
+ /** Registry defaults for per-model Codex reasoning propagation; explicit user keys win during enrichment. */
200
+ modelSupportsReasoningSummaries?: Record<string, boolean>;
199
201
  modelDiscovery?: ProviderModelDiscoverySpec;
200
202
  contextWindow?: number;
201
203
  modelContextWindows?: Record<string, number>;
@@ -719,6 +721,62 @@ const BASETEN_MODEL_INPUT_MODALITIES: Record<string, string[]> = {
719
721
  "moonshotai/Kimi-K2.7-Code": ["text", "image"],
720
722
  "moonshotai/Kimi-K3": ["text", "image"],
721
723
  };
724
+
725
+ // 260801 DigitalOcean and Scaleway expose OpenAI-shaped `/v1/models` rows with only
726
+ // id/object/created/owned_by, while their shared serverless catalogs also contain
727
+ // non-chat and endpoint-specific models. Fail closed by intersecting live discovery
728
+ // with ids that the providers' current first-party model tables establish for Chat
729
+ // Completions. A newly listed id therefore needs a docs-backed registry refresh before
730
+ // it can enter the Codex catalog.
731
+ // Evidence: https://docs.digitalocean.com/products/inference/details/models/
732
+ // https://docs.digitalocean.com/reference/api/reference/serverless-inference/
733
+ // https://www.scaleway.com/en/docs/generative-apis/reference-content/supported-models/
734
+ const DIGITALOCEAN_CHAT_COMPLETION_MODELS = [
735
+ "arcee-trinity-large-thinking",
736
+ "openai-gpt-5.6-sol",
737
+ "openai-gpt-5.6-terra",
738
+ "openai-gpt-5.6-luna",
739
+ "qwen3-coder-flash",
740
+ "qwen3.5-397b-a17b",
741
+ "deepseek-v4-pro",
742
+ "deepseek-4-flash",
743
+ "deepseek-3.2",
744
+ "gemma-4-31B-it",
745
+ "minimax-m2.5",
746
+ "kimi-k3",
747
+ "kimi-k2.6",
748
+ "kimi-k2.5",
749
+ "llama3.3-70b-instruct",
750
+ "llama-4-maverick",
751
+ "mistral-3-14B",
752
+ "nemotron-3-ultra-550b",
753
+ "nvidia-nemotron-3-super-120b",
754
+ "nemotron-3-nano-omni",
755
+ "nemotron-nano-12b-v2-vl",
756
+ "mimo-v2.5-pro",
757
+ "glm-5.2",
758
+ "glm-5.1",
759
+ "glm-5",
760
+ // The API reference uses this native slash id in its Chat Completions example.
761
+ "meta-llama/Meta-Llama-3.1-8B-Instruct",
762
+ ] as const;
763
+ const SCALEWAY_SERVERLESS_CHAT_MODELS = [
764
+ "glm-5.2",
765
+ // gpt-oss-120b is intentionally omitted: Scaleway requires Responses API for tool calling,
766
+ // while this preset routes Codex agent tools through Chat Completions.
767
+ "qwen3.6-35b-a3b",
768
+ "qwen3.5-397b-a17b",
769
+ "qwen3-235b-a22b-instruct-2507",
770
+ "qwen3-coder-30b-a3b-instruct",
771
+ "gemma-4-26b-a4b-it",
772
+ "llama-3.3-70b-instruct",
773
+ "mistral-medium-3.5-128b",
774
+ "mistral-small-3.2-24b-instruct-2506",
775
+ "pixtral-12b-2409",
776
+ ] as const;
777
+ const SCALEWAY_MODEL_INPUT_MODALITIES: Record<string, string[]> = {
778
+ "pixtral-12b-2409": ["text", "image"],
779
+ };
722
780
  const UMANS_MODELS = [
723
781
  "umans-coder",
724
782
  "umans-kimi-k2.7",
@@ -1048,6 +1106,12 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1048
1106
  ...Object.fromEntries(OPENCODE_GO_THINKING_TOGGLE_MODELS.map(id => [id, THINKING_TOGGLE_MAP])),
1049
1107
  ...Object.fromEntries(DEEPSEEK_THINKING_MODELS.map(id => [id, deepseekReasoningMapFor(id)])),
1050
1108
  },
1109
+ modelSupportsReasoningSummaries: {
1110
+ "glm-5.2": true,
1111
+ "glm-5.1": true,
1112
+ "glm-5": true,
1113
+ ...Object.fromEntries(DEEPSEEK_THINKING_MODELS.map(id => [id, true])),
1114
+ },
1051
1115
  thinkingToggleModels: OPENCODE_GO_THINKING_TOGGLE_MODELS,
1052
1116
  thinkingBudgetModels: THINKING_BUDGET_MODELS,
1053
1117
  noReasoningModels: ["kimi-k2.7-code", "kimi-k2.7-code-highspeed"],
@@ -1251,10 +1315,17 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1251
1315
  // for no gain.
1252
1316
  "deepseek-v4-flash": { wire: "openai-responses", inbound: ["responses"] },
1253
1317
  },
1254
- // DeepSeek's Codex Responses stream can deliver output without closing on the
1255
- // terminal event. Keep Codex on WebSocket, but use the provider's bounded JSON
1256
- // response upstream so the bridge can synthesize a complete WS event sequence.
1257
- modelResponsesUpstreamStreaming: { "deepseek-v4-flash": false },
1318
+ // The #875-era bounded-JSON force (`modelResponsesUpstreamStreaming`) is retired
1319
+ // for this entry: the official guide documents a `response.completed` /
1320
+ // `response.incomplete` / `response.failed` terminal with NO `data: [DONE]`
1321
+ // sentinel, and live probes (2026-08-07, including the tool-result replay shape
1322
+ // that originally stalled) close on the terminal. The relay's terminal boundary
1323
+ // (src/server/relay.ts) already cuts the stream at that event and synthesizes
1324
+ // `[DONE]`, so forcing stream:false only delayed every byte until generation
1325
+ // finished (28-46 s of silence on long turns). The registry knob itself remains
1326
+ // for providers that need it — re-adding one line here restores the old policy.
1327
+ // Evidence: https://api-docs.deepseek.com/guides/responses_api/ +
1328
+ // devlog/_plan/260807_deepseek_responses_streaming/000_plan.md.
1258
1329
  // DeepSeek's Responses route emits bare UUID item ids, which leave Codex
1259
1330
  // clients stuck on an uncommitted turn (#938). Client-facing only — raw
1260
1331
  // continuation snapshots keep the upstream ids.
@@ -1284,6 +1355,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1284
1355
  */
1285
1356
  modelReasoningEfforts: Object.fromEntries(DEEPSEEK_THINKING_MODELS.map(id => [id, deepseekThinkingEffortsFor(id)])),
1286
1357
  modelReasoningEffortMap: Object.fromEntries(DEEPSEEK_THINKING_MODELS.map(id => [id, deepseekReasoningMapFor(id)])),
1358
+ modelSupportsReasoningSummaries: Object.fromEntries(DEEPSEEK_THINKING_MODELS.map(id => [id, true])),
1287
1359
  preserveReasoningContentModels: DEEPSEEK_THINKING_MODELS,
1288
1360
  // Issue #88: every DeepSeek API model is text-only input (no image support upstream) — the
1289
1361
  // vision sidecar describes attached images for them, and the catalog advertises image input
@@ -1329,6 +1401,69 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1329
1401
  },
1330
1402
  note: "Serverless text and vision-language chat models only; Hyperbolic's separate image, audio, and GPU endpoints are out of scope.",
1331
1403
  },
1404
+ {
1405
+ // Primary sources checked 2026-08-03:
1406
+ // - docs.nscale.com documents the production OpenAI-compatible endpoint, bearer service
1407
+ // tokens, /v1/models, and a tool-calling request using this exact Llama model id.
1408
+ // - nscale.com/policies/terms-conditions identifies Nscale AS as the service operator and
1409
+ // covers customers using its public-cloud inference offering. Maintainer: @olddonkey;
1410
+ // no affiliation with Nscale.
1411
+ id: "nscale",
1412
+ label: "Nscale Serverless Inference",
1413
+ baseUrl: "https://inference.api.nscale.com/v1",
1414
+ adapter: "openai-chat",
1415
+ authKind: "key",
1416
+ dashboardUrl: "https://console.nscale.com",
1417
+ defaultModel: "meta-llama/Llama-3.1-8B-Instruct",
1418
+ models: ["meta-llama/Llama-3.1-8B-Instruct"],
1419
+ liveModels: true,
1420
+ preserveCustomDestination: true,
1421
+ // Nscale documents tools but not parallel tool calls. Keep requests serialized.
1422
+ parallelToolCalls: false,
1423
+ // The API schema accepts reasoning_effort, but does not publish per-model tiers.
1424
+ reasoningEfforts: [],
1425
+ modelDiscovery: {
1426
+ path: "models",
1427
+ maxResponseBytes: 256 * 1024,
1428
+ maxModels: 256,
1429
+ filter: {
1430
+ // Nscale's catalog mixes chat, image, and embedding rows without a modality field.
1431
+ // Admit only the exact model used in its official tool-calling API example.
1432
+ allOf: [{ path: ["id"], equalsAny: ["meta-llama/Llama-3.1-8B-Instruct"] }],
1433
+ },
1434
+ },
1435
+ note: "Serverless OpenAI-compatible inference. Live discovery admits only the tool-capable model established by Nscale's official API example; other mixed-catalog rows remain hidden pending equivalent evidence.",
1436
+ },
1437
+ {
1438
+ // Primary sources checked 2026-08-03:
1439
+ // - docs.vultr.com documents the fixed OpenAI-compatible base URL, per-subscription bearer
1440
+ // key, /v1/models, and states that tool calling is currently limited to kimi-k2-instruct.
1441
+ // - Vultr's official properties identify VULTR as a The Constant Company, LLC trademark and
1442
+ // document customer API integrations. Maintainer: @olddonkey; no affiliation with Vultr.
1443
+ id: "vultr",
1444
+ label: "Vultr Serverless Inference",
1445
+ baseUrl: "https://api.vultrinference.com/v1",
1446
+ adapter: "openai-chat",
1447
+ authKind: "key",
1448
+ dashboardUrl: "https://my.vultr.com",
1449
+ defaultModel: "kimi-k2-instruct",
1450
+ models: ["kimi-k2-instruct"],
1451
+ liveModels: true,
1452
+ preserveCustomDestination: true,
1453
+ parallelToolCalls: false,
1454
+ reasoningEfforts: [],
1455
+ modelDiscovery: {
1456
+ path: "models",
1457
+ maxResponseBytes: 256 * 1024,
1458
+ maxModels: 256,
1459
+ filter: {
1460
+ // Vultr explicitly limits tool calling to this model. A coding agent must not select
1461
+ // another chat model that cannot complete its tool loop.
1462
+ allOf: [{ path: ["id"], equalsAny: ["kimi-k2-instruct"] }],
1463
+ },
1464
+ },
1465
+ note: "Serverless Inference subscription API. Live discovery exposes only kimi-k2-instruct because Vultr documents it as the sole tool-calling model.",
1466
+ },
1332
1467
  {
1333
1468
  id: "baseten",
1334
1469
  label: "Baseten Model APIs",
@@ -1380,6 +1515,101 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1380
1515
  // 401 UNAUTHORIZED without a Bearer key. Primary source: https://commandcode.ai/docs/provider.
1381
1516
  note: "Command Code Provider API (OpenAI-compatible); API access requires the Provider plan. Use `ocx login command-code` for OAuth account login (imports an existing local Command Code CLI credential when present). Docs: https://commandcode.ai/docs/provider.",
1382
1517
  },
1518
+ {
1519
+ id: "sambanova",
1520
+ label: "SambaNova Cloud",
1521
+ baseUrl: "https://api.sambanova.ai/v1",
1522
+ adapter: "openai-chat",
1523
+ authKind: "key",
1524
+ dashboardUrl: "https://cloud.sambanova.ai/apis",
1525
+ liveModels: true,
1526
+ preserveCustomDestination: true,
1527
+ apiKeyValidation: "unknown",
1528
+ // SambaNova documents this request field but does not yet support parallel function calls.
1529
+ parallelToolCalls: false,
1530
+ // The public catalog does not report a trustworthy per-model reasoning contract.
1531
+ reasoningEfforts: [],
1532
+ modelDiscovery: {
1533
+ path: "models",
1534
+ maxResponseBytes: 128 * 1024,
1535
+ maxModels: 128,
1536
+ },
1537
+ note: "SambaNova Cloud text-generation models only; private SambaStudio deployment endpoints are outside this preset.",
1538
+ },
1539
+ {
1540
+ id: "nebius",
1541
+ label: "Nebius Token Factory",
1542
+ baseUrl: "https://api.tokenfactory.nebius.com/v1",
1543
+ adapter: "openai-chat",
1544
+ authKind: "key",
1545
+ dashboardUrl: "https://tokenfactory.nebius.com",
1546
+ liveModels: true,
1547
+ preserveCustomDestination: true,
1548
+ // The public tools guide documents single function selection, not parallel tool calls.
1549
+ parallelToolCalls: false,
1550
+ // Missing reasoning metadata must not promote a model to Codex's full fallback ladder.
1551
+ reasoningEfforts: [],
1552
+ modelDiscovery: {
1553
+ path: "models",
1554
+ query: { verbose: "true" },
1555
+ maxResponseBytes: 512 * 1024,
1556
+ maxModels: 512,
1557
+ filter: {
1558
+ // Keep rows whose reported architecture output includes text (for example,
1559
+ // text->text or text+image->text); embedding and image-generation rows are excluded.
1560
+ allOf: [{ path: ["architecture", "modality"], containsAny: ["->text"] }],
1561
+ },
1562
+ },
1563
+ note: "Shared Token Factory text-output inference only; live discovery excludes embedding and image-generation rows.",
1564
+ },
1565
+ {
1566
+ id: "digitalocean",
1567
+ label: "DigitalOcean Serverless Inference",
1568
+ baseUrl: "https://inference.do-ai.run/v1",
1569
+ adapter: "openai-chat",
1570
+ authKind: "key",
1571
+ dashboardUrl: "https://cloud.digitalocean.com/model-studio/manage-keys",
1572
+ liveModels: true,
1573
+ preserveCustomDestination: true,
1574
+ // The Chat Completions contract documents function calls but not universal parallel support.
1575
+ parallelToolCalls: false,
1576
+ // Unknown catalog rows must not inherit Codex's full fallback reasoning ladder.
1577
+ reasoningEfforts: [],
1578
+ modelDiscovery: {
1579
+ path: "models",
1580
+ maxResponseBytes: 256 * 1024,
1581
+ maxModels: 256,
1582
+ filter: {
1583
+ allOf: [{ path: ["id"], equalsAny: DIGITALOCEAN_CHAT_COMPLETION_MODELS }],
1584
+ },
1585
+ },
1586
+ note: "Shared Serverless Inference Chat Completions only; agent-specific, dedicated, Responses-only, embedding, and media-generation models are outside this preset.",
1587
+ },
1588
+ {
1589
+ id: "scaleway",
1590
+ label: "Scaleway Generative APIs",
1591
+ baseUrl: "https://api.scaleway.ai/v1",
1592
+ adapter: "openai-chat",
1593
+ authKind: "key",
1594
+ dashboardUrl: "https://console.scaleway.com/generative-api",
1595
+ liveModels: true,
1596
+ freeTier: true,
1597
+ preserveCustomDestination: true,
1598
+ // Parallel support varies by model; avoid advertising it as a provider-wide capability.
1599
+ parallelToolCalls: false,
1600
+ // The generic `/models` rows carry no trustworthy reasoning metadata.
1601
+ reasoningEfforts: [],
1602
+ modelInputModalities: SCALEWAY_MODEL_INPUT_MODALITIES,
1603
+ modelDiscovery: {
1604
+ path: "models",
1605
+ maxResponseBytes: 128 * 1024,
1606
+ maxModels: 128,
1607
+ filter: {
1608
+ allOf: [{ path: ["id"], equalsAny: SCALEWAY_SERVERLESS_CHAT_MODELS }],
1609
+ },
1610
+ },
1611
+ note: "Shared Generative APIs Serverless Chat Completions only; project-qualified and dedicated deployment hosts require a custom provider.",
1612
+ },
1383
1613
  // FREEZE 2026-07-10: exact serverless ids remain auth-gated/unverified. Evidence: devlog/_plan/260710_provider_hardening/003_research_aggregators.md.
1384
1614
  { id: "together", label: "Together", baseUrl: "https://api.together.xyz/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://api.together.xyz/settings/api-keys" },
1385
1615
  { id: "fireworks", label: "Fireworks", baseUrl: "https://api.fireworks.ai/inference/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://fireworks.ai/account/api-keys" },
@@ -1439,6 +1669,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1439
1669
  modelSuffixBracketStrip: true,
1440
1670
  noVisionModels: ZAI_GLM_52_MODELS,
1441
1671
  modelReasoningEfforts: Object.fromEntries(ZAI_GLM_52_MODELS.map(id => [id, ZAI_GLM_52_REASONING_EFFORTS])),
1672
+ modelSupportsReasoningSummaries: Object.fromEntries(ZAI_GLM_52_MODELS.map(id => [id, true])),
1442
1673
  preserveReasoningContentModels: ZAI_GLM_52_MODELS,
1443
1674
  },
1444
1675
  // Zhipu's domestic BigModel platform: OpenAI-compatible pay-as-you-go on open.bigmodel.cn — a
@@ -1475,11 +1706,50 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1475
1706
  modelReasoningEffortMap: Object.fromEntries(
1476
1707
  ZHIPU_BIGMODEL_THINKING_TOGGLE_MODELS.map(id => [id, THINKING_TOGGLE_MAP]),
1477
1708
  ),
1709
+ modelSupportsReasoningSummaries: Object.fromEntries(
1710
+ ZHIPU_BIGMODEL_THINKING_TOGGLE_MODELS.map(id => [id, true]),
1711
+ ),
1478
1712
  preserveReasoningContentModels: ZHIPU_BIGMODEL_THINKING_TOGGLE_MODELS,
1479
1713
  // No liveModels: GET /api/paas/v4/models has not been observed to answer on this host, and a
1480
1714
  // false live claim yields an empty picker at runtime. Flip it on once someone verifies it.
1481
1715
  note: "Domestic BigModel pay-as-you-go endpoint (open.bigmodel.cn)",
1482
1716
  },
1717
+ // BigModel's Coding Plan is a SEPARATE endpoint from the pay-as-you-go row above, and that is
1718
+ // the whole reason this one exists. #1100 was reported against
1719
+ // `https://open.bigmodel.cn/api/coding/paas/v4`; the row above covers only `/api/paas/v4`, so
1720
+ // destination enrichment matched nothing, `modelSupportsReasoningSummaries` stayed unset, and
1721
+ // Codex kept dropping the inbound reasoning object — effort displayed as `-`.
1722
+ //
1723
+ // A prefix or fuzzy endpoint match would have been the shortcut. It is also how a config
1724
+ // pointed at one vendor route silently inherits another route's metadata, so endpoints stay
1725
+ // exact and each one gets its own row.
1726
+ //
1727
+ // The id is NOT `glm-cn`, which the free-provider directory already binds to this same coding
1728
+ // path: registering it here would let routedProviderConfig() canonicalize a saved `glm-cn`
1729
+ // config onto this baseUrl. Same reasoning as `zhipu-bigmodel` above.
1730
+ //
1731
+ // Models follow Z.AI's coding-plan list rather than the pay-as-you-go one. This endpoint is
1732
+ // the subscription product, and the reporter's `glm-5.2` is only on that side.
1733
+ {
1734
+ id: "zhipu-bigmodel-coding",
1735
+ label: "Zhipu AI — BigModel Coding Plan",
1736
+ baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4",
1737
+ adapter: "openai-chat",
1738
+ authKind: "key",
1739
+ dashboardUrl: "https://bigmodel.cn/console/usercenter/apikeys",
1740
+ defaultModel: "glm-5.2",
1741
+ models: ["glm-5.2", "glm-5.2[1m]", "glm-5.1", "glm-5", "glm-4.6"],
1742
+ jawcodeBundle: "zai",
1743
+ modelContextWindows: { "glm-5.2": 1_000_000, "glm-5.2[1m]": 1_000_000 },
1744
+ modelSuffixBracketStrip: true,
1745
+ noVisionModels: ZAI_GLM_52_MODELS,
1746
+ modelReasoningEfforts: Object.fromEntries(ZAI_GLM_52_MODELS.map(id => [id, ZAI_GLM_52_REASONING_EFFORTS])),
1747
+ modelSupportsReasoningSummaries: Object.fromEntries(ZAI_GLM_52_MODELS.map(id => [id, true])),
1748
+ preserveReasoningContentModels: ZAI_GLM_52_MODELS,
1749
+ // No liveModels: the same reasoning as the pay-as-you-go row — an unverified live claim
1750
+ // yields an empty picker at runtime.
1751
+ note: "Domestic BigModel Coding Plan endpoint (open.bigmodel.cn)",
1752
+ },
1483
1753
  { id: "nanogpt", label: "NanoGPT", baseUrl: "https://nano-gpt.com/api/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://nano-gpt.com/api" },
1484
1754
  { id: "synthetic", label: "Synthetic", baseUrl: "https://api.synthetic.new/openai/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://synthetic.new" },
1485
1755
  // SiliconFlow publishes an OpenAI-compatible chat endpoint and a dynamic model catalog. Do not
@@ -1750,15 +2020,20 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1750
2020
  preserveReasoningContentModels: KIMI_THINKING_MODELS,
1751
2021
  },
1752
2022
  {
1753
- id: "opencode-zen",
1754
- label: "opencode zen",
1755
- baseUrl: "https://opencode.ai/zen/v1",
1756
- adapter: "openai-chat",
1757
- authKind: "key",
1758
- dashboardUrl: "https://opencode.ai/auth",
1759
- // #1043: without this the proxy forwards image parts to text-only Zen models and
1760
- // the upstream rejects the whole request with a 400.
1761
- noVisionModels: OPENCODE_ZEN_TEXT_ONLY_MODELS,
2023
+ id: "opencode-zen", label: "opencode zen", baseUrl: "https://opencode.ai/zen/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://opencode.ai/auth",
2024
+ // Same opencode.ai/zen/v1 gateway as `opencode-free` (keyed tier): DeepSeek thinking mode
2025
+ // requires the assistant's original reasoning_content to be replayed on tool-call
2026
+ // continuations, or the gateway answers HTTP 400 (issues #950/#994). Mirror the DeepSeek
2027
+ // reasoning + thinking metadata so `opencode-zen/deepseek-v4-flash-free` — and the other
2028
+ // Zen DeepSeek thinking models — never serialize a bare tool-call turn.
2029
+ modelReasoningEfforts: Object.fromEntries(
2030
+ [...DEEPSEEK_THINKING_MODELS, ...OPENCODE_FREE_DEEPSEEK_MODELS].map(id => [id, deepseekThinkingEffortsFor(id)]),
2031
+ ),
2032
+ modelReasoningEffortMap: Object.fromEntries(
2033
+ [...DEEPSEEK_THINKING_MODELS, ...OPENCODE_FREE_DEEPSEEK_MODELS].map(id => [id, deepseekReasoningMapFor(id)]),
2034
+ ),
2035
+ preserveReasoningContentModels: [...DEEPSEEK_THINKING_MODELS, ...OPENCODE_FREE_DEEPSEEK_MODELS],
2036
+ noVisionModels: [...OPENCODE_ZEN_TEXT_ONLY_MODELS, ...DEEPSEEK_THINKING_MODELS],
1762
2037
  },
1763
2038
  { id: "vercel-ai-gateway", label: "Vercel AI Gateway", baseUrl: "https://ai-gateway.vercel.sh/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://vercel.com/dashboard" },
1764
2039
  {
@@ -1798,6 +2073,34 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1798
2073
  models: ["mimo-auto"],
1799
2074
  note: "No key needed — uses Xiaomi MiMo's free public tier (limited-time offer). A JWT is bootstrapped automatically with an anonymous random client id stored locally. The endpoint contract mirrors the official MiMoCode client and is not publicly documented — Xiaomi may change or restrict it at any time. Prompts may be processed/retained by Xiaomi; do not send confidential material.",
1800
2075
  },
2076
+ // Xiaomi MiMo paid token plan. Separate host and wire from both `xiaomi` (Anthropic) and
2077
+ // `mimo-free` (free tier, bespoke adapter), so it needs its own entry rather than a variant.
2078
+ //
2079
+ // Pinned to openai-chat deliberately (#1158). The endpoint answers the Responses wire for
2080
+ // plain turns, which is why users configuring it by hand pick `openai-responses` — MiMo
2081
+ // documents Responses support. But its gateway rejects `type: "custom"` tools with
2082
+ // `400 responses_feature_not_supported`, and `apply_patch` is a custom tool, so every agentic
2083
+ // turn fails while chat turns succeed. The Chat path lowers custom tools to `{input: string}`
2084
+ // functions and restores them as `custom_tool_call`, so the capability survives intact.
2085
+ // Stripping the tools instead would stop the 400 and disable the agent loop.
2086
+ {
2087
+ id: "mimo",
2088
+ label: "Xiaomi MiMo (token plan)",
2089
+ baseUrl: "https://token-plan-cn.xiaomimimo.com/v1",
2090
+ adapter: "openai-chat",
2091
+ authKind: "key",
2092
+ dashboardUrl: "https://xiaomimimo.com",
2093
+ defaultModel: "mimo-v2.5-pro",
2094
+ models: ["mimo-v2.5-pro", "mimo-v2.5"],
2095
+ // The gateway validates the ladder strictly and rejects anything above `high`.
2096
+ reasoningEfforts: ["low", "medium", "high"],
2097
+ reasoningEffortMap: { xhigh: "high", max: "high", ultra: "high" },
2098
+ // A user may already have hand-rolled a provider under this id against a different host;
2099
+ // without this, routedProviderConfig() would canonicalize their base URL onto ours and send
2100
+ // their key somewhere they did not choose.
2101
+ preserveCustomDestination: true,
2102
+ note: "Xiaomi MiMo paid token plan. Pinned to the Chat wire: the Responses endpoint rejects freeform (custom) tools such as apply_patch with 400 responses_feature_not_supported, so agentic turns fail there while plain turns succeed. Reasoning tiers above high are clamped.",
2103
+ },
1801
2104
  { id: "cloudflare-ai-gateway", label: "Cloudflare AI Gateway", baseUrl: "https://gateway.ai.cloudflare.com/v1/{account-id}/{gateway}/anthropic", adapter: "anthropic", authKind: "key", dashboardUrl: "https://dash.cloudflare.com/?to=/:account/ai/ai-gateway" },
1802
2105
  {
1803
2106
  // Cloudflare Workers AI: OpenAI-compatible endpoint. The base URL contains {account_id}
@@ -668,9 +668,12 @@ export function parseRequest(body: unknown): OcxParsedRequest {
668
668
  ...(data.tools as unknown[] ?? []),
669
669
  ...loadedToolSpecs,
670
670
  ]);
671
- // Detect structured-output mode (Responses `text.format`) so the web-search sidecar can render its
672
- // tool_result as JSON rather than prose that could corrupt the model's schema-constrained answer.
673
- const structuredOutput = detectStructuredOutput(data.text);
671
+ // Capture structured-output mode (Responses `text.format`): the format object rides
672
+ // options.textFormat for adapters whose wire has an equivalent (openai-chat response_format),
673
+ // while the `_structuredOutput` flag keeps the web-search sidecar rendering its tool_result
674
+ // as JSON rather than prose that could corrupt the model's schema-constrained answer.
675
+ const textFormat = parseTextFormat(data.text);
676
+ if (textFormat) options.textFormat = textFormat;
674
677
 
675
678
  return {
676
679
  modelId: data.model,
@@ -682,17 +685,30 @@ export function parseRequest(body: unknown): OcxParsedRequest {
682
685
  ...(replayedInputPrefixLength > 0 ? { _replayPrefixLen: replayedInputPrefixLength } : {}),
683
686
  ...(webSearch ? { _webSearch: webSearch } : {}),
684
687
  ...(imageGen ? { _imageGeneration: imageGen } : {}),
685
- ...(structuredOutput ? { _structuredOutput: true } : {}),
688
+ ...(textFormat ? { _structuredOutput: true } : {}),
686
689
  ...(compactionRequest ? { _compactionRequest: true } : {}),
687
690
  ...(contextCompactionBoundary ? { _contextCompactionBoundary: true } : {}),
688
691
  };
689
692
  }
690
693
 
691
- /** True when the Responses `text.format` requests structured output (json_schema or json_object). */
692
- function detectStructuredOutput(text: unknown): boolean {
693
- if (!isObj(text)) return false;
694
+ /**
695
+ * The Responses `text.format` object when it requests structured output (json_schema or
696
+ * json_object), undefined otherwise. Acceptance is identical to the boolean detector this
697
+ * replaces; unknown or malformed formats are ignored, never rejected, so the native
698
+ * passthrough keeps forwarding whatever the caller sent via `_rawBody`.
699
+ */
700
+ function parseTextFormat(text: unknown): OcxRequestOptions["textFormat"] {
701
+ if (!isObj(text)) return undefined;
694
702
  const format = (text as { format?: unknown }).format;
695
- if (!isObj(format)) return false;
696
- const t = (format as { type?: unknown }).type;
697
- return t === "json_schema" || t === "json_object";
703
+ if (!isObj(format)) return undefined;
704
+ const f = format as { type?: unknown; name?: unknown; description?: unknown; schema?: unknown; strict?: unknown };
705
+ if (f.type === "json_object") return { type: "json_object" };
706
+ if (f.type !== "json_schema") return undefined;
707
+ return {
708
+ type: "json_schema",
709
+ ...(typeof f.name === "string" ? { name: f.name } : {}),
710
+ ...(typeof f.description === "string" ? { description: f.description } : {}),
711
+ ...(isObj(f.schema) ? { schema: f.schema as Record<string, unknown> } : {}),
712
+ ...(typeof f.strict === "boolean" ? { strict: f.strict } : {}),
713
+ };
698
714
  }
@@ -44,6 +44,7 @@ const keyFor = (callId: string, scope: string | undefined): string =>
44
44
  * id is never read again.
45
45
  */
46
46
  export function rememberReasoningForCall(callId: string, text: string, scope?: string): void {
47
+ // Empty provider deltas are absence of new reasoning, not a request to erase a candidate.
47
48
  if (!callId || typeof text !== "string" || text.length === 0) return;
48
49
  const bytes = Buffer.byteLength(text, "utf8");
49
50
  // A single entry larger than the whole budget would immediately evict itself.
@@ -0,0 +1,15 @@
1
+ import type { OcxRoutingProfileConfig } from "../types";
2
+
3
+ /** Canonical public namespace reserved for routing-policy model ids. */
4
+ export const POLICY_NAMESPACE = "policy";
5
+
6
+ /** Public namespace prefixes claimed by slash-qualified routing-profile aliases. */
7
+ export function routingProfileAliasNamespacePrefixes(
8
+ config: { routingProfiles?: Record<string, OcxRoutingProfileConfig> },
9
+ ): string[] {
10
+ return Object.values(config.routingProfiles ?? {}).flatMap((profile) => {
11
+ const alias = typeof profile?.alias === "string" ? profile.alias.trim() : "";
12
+ const slash = alias.indexOf("/");
13
+ return slash > 0 ? [alias.slice(0, slash)] : [];
14
+ });
15
+ }
@@ -13,8 +13,9 @@ import type {
13
13
  import { codexAccountNamespaceEntries } from "../codex/account-namespaces";
14
14
  import { listComboIds, resolveComboId } from "../combos";
15
15
  import { hasOwnProvider } from "../config";
16
+ import { POLICY_NAMESPACE } from "./profile-namespace";
16
17
 
17
- export const POLICY_NAMESPACE = "policy";
18
+ export { POLICY_NAMESPACE };
18
19
 
19
20
  export const POLICY_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
20
21
  export const POLICY_ALIAS_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}(?:\/[A-Za-z0-9][A-Za-z0-9._-]{0,63})?$/;
@@ -73,7 +73,7 @@ export function isSameOriginAsRequest(req: Request, origin: string): boolean {
73
73
  }
74
74
  }
75
75
 
76
- export function isAllowedRequestOrigin(req: Request, config: OcxConfig): boolean {
76
+ export function isAllowedRequestOrigin(req: Request, config: RequestPolicyView): boolean {
77
77
  const origin = req.headers.get("Origin");
78
78
  if (!isApiAuthRequired(config)) {
79
79
  if (!isLoopbackRequestHost(req.headers.get("Host"))) return false;
@@ -82,7 +82,7 @@ export function isAllowedRequestOrigin(req: Request, config: OcxConfig): boolean
82
82
  return !origin || isLoopbackOriginValue(origin) || isSameOriginAsRequest(req, origin) || isExtraAllowedOrigin(origin, config);
83
83
  }
84
84
 
85
- function isExtraAllowedOrigin(origin: string, cfg: OcxConfig): boolean {
85
+ function isExtraAllowedOrigin(origin: string, cfg: RequestPolicyView): boolean {
86
86
  if (!cfg.corsAllowOrigins?.length) return false;
87
87
  const parsedOrigin = comparableOrigin(origin);
88
88
  return cfg.corsAllowOrigins.some(allowed => {
@@ -136,7 +136,7 @@ export function browserSecurityHeaders(): Record<string, string> {
136
136
  };
137
137
  }
138
138
 
139
- export function corsHeaders(req?: Request, config?: OcxConfig): Record<string, string> {
139
+ export function corsHeaders(req?: Request, config?: RequestPolicyView): Record<string, string> {
140
140
  const origin = req?.headers.get("Origin");
141
141
  const allowOrigin = origin && req && config && isAllowedRequestOrigin(req, config) ? origin : _corsOrigin;
142
142
  return {
@@ -160,7 +160,7 @@ export function managementCorsHeaders(req?: Request, config?: OcxConfig): Record
160
160
  return headers;
161
161
  }
162
162
 
163
- export function withCors(response: Response, req: Request, config: OcxConfig): Response {
163
+ export function withCors(response: Response, req: Request, config: RequestPolicyView): Response {
164
164
  const headers = new Headers(response.headers);
165
165
  for (const [name, value] of Object.entries(corsHeaders(req, config))) {
166
166
  headers.set(name, value);
@@ -184,14 +184,18 @@ export function withManagementCors(response: Response, req: Request, config: Ocx
184
184
  });
185
185
  }
186
186
 
187
- export function jsonResponse(data: unknown, status = 200, req?: Request, config?: OcxConfig): Response {
187
+ export function jsonResponse(data: unknown, status = 200, req?: Request, config?: RequestPolicyView): Response {
188
188
  return new Response(JSON.stringify(data), {
189
189
  status,
190
190
  headers: { "Content-Type": "application/json", ...corsHeaders(req, config) },
191
191
  });
192
192
  }
193
193
 
194
- export function configuredApiAuthToken(_config: OcxConfig): string | undefined {
194
+ // The parameter is vestigial the token has always come from the environment — but callers
195
+ // pass a config, so keep accepting one. Typed as `unknown` rather than `OcxConfig` so a narrow
196
+ // policy view can reach it too (#1102); widening to OcxConfig here would force every caller in
197
+ // the admission path back to the full config.
198
+ export function configuredApiAuthToken(_config?: unknown): string | undefined {
195
199
  const token = process.env.OPENCODEX_API_AUTH_TOKEN?.trim();
196
200
  return token || undefined;
197
201
  }
@@ -208,10 +212,37 @@ export function isLoopbackHostname(hostname: string | undefined): boolean {
208
212
  return normalized === "" || normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1" || normalized === "[::1]";
209
213
  }
210
214
 
211
- export function isApiAuthRequired(config: OcxConfig): boolean {
215
+ export function isApiAuthRequired(config: Pick<OcxConfig, "hostname">): boolean {
212
216
  return !isLoopbackHostname(config.hostname);
213
217
  }
214
218
 
219
+ /**
220
+ * The slice of config that decides admission and CORS, and nothing else (#1102).
221
+ *
222
+ * The unauthenticated loopback listener shares this process with the public one: same routing,
223
+ * same account pool, same drain. The only thing it must see differently is its own bind
224
+ * address, because `isApiAuthRequired` reads `hostname` and the shared config says "0.0.0.0".
225
+ *
226
+ * Two ways to express that were rejected. Passing the whole config with `hostname` rewritten
227
+ * and holding it for the listener's lifetime would go stale the moment the management API
228
+ * changes a setting. Adding an `allowUnauthenticated` parameter to the resolvers would create a
229
+ * callable admission bypass that the PUBLIC listener could also reach — the switch would exist
230
+ * on the wrong side of the boundary.
231
+ *
232
+ * So this type is deliberately narrow: it cannot masquerade as a business config, and a policy
233
+ * view that leaks into a routing path fails to typecheck rather than silently taking effect.
234
+ */
235
+ export type RequestPolicyView = Pick<OcxConfig, "hostname" | "corsAllowOrigins" | "apiKeys">;
236
+
237
+ /** Derive the per-request policy view for a listener. Cheap enough to build per request. */
238
+ export function requestPolicyView(config: OcxConfig, bindHostname: string): RequestPolicyView {
239
+ return {
240
+ hostname: bindHostname,
241
+ ...(config.corsAllowOrigins ? { corsAllowOrigins: config.corsAllowOrigins } : {}),
242
+ ...(config.apiKeys ? { apiKeys: config.apiKeys } : {}),
243
+ };
244
+ }
245
+
215
246
  export function assertServerAuthConfig(config: OcxConfig): void {
216
247
  const hasConfiguredDataCredential = !!configuredApiAuthToken(config)
217
248
  || (config.apiKeys ?? []).some(entry => !!entry.key.trim());
@@ -253,7 +284,7 @@ export type DataPlaneAdmission =
253
284
  * discarded, which is what makes per-key attribution possible without touching
254
285
  * the admission decision itself.
255
286
  */
256
- export function resolveDataPlaneAdmissionSecret(token: string, config: OcxConfig): DataPlaneAdmission | null {
287
+ export function resolveDataPlaneAdmissionSecret(token: string, config: Pick<OcxConfig, "apiKeys">): DataPlaneAdmission | null {
257
288
  const actual = token.trim();
258
289
  if (!actual) return null;
259
290
  if (secretEquals(actual, configuredApiAuthToken(config))) return { kind: "environment" };
@@ -341,7 +372,7 @@ export function validateForwardAdmissionCredential(headers: Headers, config: Ocx
341
372
  * Resolving form of `hasValidApiAuth`: identical header precedence, identical
342
373
  * decision, but it names the admission instead of collapsing it to a boolean.
343
374
  */
344
- export function resolveApiAuth(req: Request, config: OcxConfig): DataPlaneAdmission | null {
375
+ export function resolveApiAuth(req: Request, config: RequestPolicyView): DataPlaneAdmission | null {
345
376
  // A loopback bind never reads a token at all, so there is no key to name.
346
377
  if (!isApiAuthRequired(config)) return { kind: "loopback" };
347
378
  const actual = req.headers.get("x-opencodex-api-key")?.trim()
@@ -352,11 +383,11 @@ export function resolveApiAuth(req: Request, config: OcxConfig): DataPlaneAdmiss
352
383
  return resolveDataPlaneAdmissionSecret(actual, config);
353
384
  }
354
385
 
355
- export function hasValidApiAuth(req: Request, config: OcxConfig): boolean {
386
+ export function hasValidApiAuth(req: Request, config: RequestPolicyView): boolean {
356
387
  return resolveApiAuth(req, config) !== null;
357
388
  }
358
389
 
359
- export function requireApiAuth(req: Request, config: OcxConfig, _kind: "data-plane"): Response | null {
390
+ export function requireApiAuth(req: Request, config: RequestPolicyView, _kind: "data-plane"): Response | null {
360
391
  if (hasValidApiAuth(req, config)) return null;
361
392
  return formatErrorResponse(401, "authentication_error", "opencodex API key required");
362
393
  }
@@ -366,7 +397,7 @@ export function requireApiAuth(req: Request, config: OcxConfig, _kind: "data-pla
366
397
  * Codex Direct. Remote binds must use the dedicated proxy header so the two bearer
367
398
  * domains can never be confused.
368
399
  */
369
- export function resolveResponsesApiAuth(req: Request, config: OcxConfig): DataPlaneAdmission | null {
400
+ export function resolveResponsesApiAuth(req: Request, config: RequestPolicyView): DataPlaneAdmission | null {
370
401
  if (!isApiAuthRequired(config)) return { kind: "loopback" };
371
402
  // Dedicated header ONLY. `Authorization` on these transports may belong to
372
403
  // Codex Direct passthrough, and the two bearer domains must stay unconfusable.
@@ -375,7 +406,7 @@ export function resolveResponsesApiAuth(req: Request, config: OcxConfig): DataPl
375
406
  return resolveDataPlaneAdmissionSecret(actual, config);
376
407
  }
377
408
 
378
- export function requireResponsesApiAuth(req: Request, config: OcxConfig): Response | null {
409
+ export function requireResponsesApiAuth(req: Request, config: RequestPolicyView): Response | null {
379
410
  if (resolveResponsesApiAuth(req, config)) return null;
380
411
  return formatErrorResponse(401, "authentication_error", "opencodex API key required");
381
412
  }