@bitkyc08/opencodex 2.42.0 → 2.43.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 (246) hide show
  1. package/AGENTS_INSTALL.md +2 -2
  2. package/README.md +31 -0
  3. package/bin/ocx.mjs +10 -1
  4. package/gui/dist/assets/index-DS1NE4Jn.css +1 -0
  5. package/gui/dist/assets/index-Djowl68T.js +112 -0
  6. package/gui/dist/index.html +2 -2
  7. package/package.json +1 -1
  8. package/src/adapters/anthropic-image-codec.ts +304 -0
  9. package/src/adapters/anthropic-image-normalize.ts +8 -298
  10. package/src/adapters/anthropic.ts +30 -7
  11. package/src/adapters/command-code.ts +7 -5
  12. package/src/adapters/cursor/desktop-executor-contract.ts +15 -0
  13. package/src/adapters/cursor/images.ts +36 -6
  14. package/src/adapters/cursor/live-transport.ts +7 -2
  15. package/src/adapters/cursor/native-exec-desktop.ts +2 -15
  16. package/src/adapters/cursor/protobuf-request.ts +13 -3
  17. package/src/adapters/cursor/tool-definitions.ts +5 -670
  18. package/src/adapters/cursor/tool-guidance.ts +236 -0
  19. package/src/adapters/cursor/tool-naming.ts +252 -0
  20. package/src/adapters/cursor/tool-schemas.ts +195 -0
  21. package/src/adapters/cursor/types.ts +6 -3
  22. package/src/adapters/exec-tool-result-normalize.ts +1 -1
  23. package/src/adapters/google-errors.ts +9 -1
  24. package/src/adapters/google.ts +1 -0
  25. package/src/adapters/kiro-calibration.ts +181 -0
  26. package/src/adapters/kiro.ts +135 -3
  27. package/src/adapters/openai-responses.ts +216 -26
  28. package/src/adapters/responses-code-mode.ts +59 -0
  29. package/src/adapters/tool-catalog-nudge.ts +1 -1
  30. package/src/adapters/xai-schema-analysis.ts +86 -0
  31. package/src/adapters/xai-tool-schema.ts +2 -87
  32. package/src/adapters/xai-web-search.ts +1 -1
  33. package/src/bridge.ts +22 -10
  34. package/src/chat/inbound.ts +11 -3
  35. package/src/claude/inbound-content-options.ts +60 -0
  36. package/src/claude/inbound-model-options.ts +142 -0
  37. package/src/claude/inbound-records.ts +7 -0
  38. package/src/claude/inbound.ts +10 -202
  39. package/src/claude/model-info.ts +45 -0
  40. package/src/cli/account-auth.ts +21 -6
  41. package/src/cli/capabilities.ts +13 -2
  42. package/src/cli/claude.ts +232 -39
  43. package/src/cli/config-command.ts +9 -1
  44. package/src/cli/dispatch.ts +5 -1
  45. package/src/cli/doctor.ts +10 -0
  46. package/src/cli/effort.ts +372 -0
  47. package/src/cli/export-command.ts +3 -9
  48. package/src/cli/help.ts +1 -0
  49. package/src/cli/index.ts +13 -0
  50. package/src/cli/init.ts +4 -0
  51. package/src/cli/model-selection-guidance.ts +30 -0
  52. package/src/cli/models-runtime.ts +3 -2
  53. package/src/cli/models.ts +8 -3
  54. package/src/cli/opencode.ts +4 -1
  55. package/src/cli/provider-runtime.ts +65 -0
  56. package/src/cli/provider.ts +8 -0
  57. package/src/cli/registry.ts +16 -2
  58. package/src/cli/runtime-api.ts +3 -1
  59. package/src/cli/star-prompt.ts +22 -6
  60. package/src/cli/status-probes.ts +168 -0
  61. package/src/cli/status.ts +5 -168
  62. package/src/clients/config-export/constants.ts +69 -0
  63. package/src/clients/config-export/contracts.ts +154 -0
  64. package/src/clients/config-export/dsh.ts +132 -0
  65. package/src/clients/config-export/fast-models.ts +29 -0
  66. package/src/clients/config-export/mcode.ts +83 -0
  67. package/src/clients/config-export/model-metadata.ts +108 -0
  68. package/src/clients/config-export/omp.ts +104 -0
  69. package/src/clients/config-export/zcode.ts +92 -0
  70. package/src/clients/config-export.ts +18 -710
  71. package/src/codex/account-lifecycle.ts +20 -3
  72. package/src/codex/account-usability.ts +2 -0
  73. package/src/codex/auth-api.ts +170 -23
  74. package/src/codex/auth-context.ts +328 -24
  75. package/src/codex/catalog/effort.ts +30 -4
  76. package/src/codex/catalog/metadata.ts +39 -9
  77. package/src/codex/catalog/native-models.ts +53 -12
  78. package/src/codex/catalog/parsing.ts +119 -5
  79. package/src/codex/catalog/provider-fetch.ts +150 -20
  80. package/src/codex/catalog/reserve.ts +52 -0
  81. package/src/codex/catalog/sync.ts +89 -16
  82. package/src/codex/convergence-types.ts +1 -0
  83. package/src/codex/convergence.ts +2 -0
  84. package/src/codex/data/upstream-models.json +1 -1
  85. package/src/codex/forward-transport-headers.ts +25 -0
  86. package/src/codex/inject.ts +3 -28
  87. package/src/codex/legacy-config-keys.ts +68 -0
  88. package/src/codex/log-guard/inspect-schema.ts +137 -0
  89. package/src/codex/log-guard/inspect.ts +2 -134
  90. package/src/codex/loopback-target.ts +54 -0
  91. package/src/codex/main-account-cache.ts +63 -1
  92. package/src/codex/main-account-hard-lock.ts +52 -0
  93. package/src/codex/main-account.ts +3 -1
  94. package/src/codex/management-convergence.ts +3 -0
  95. package/src/codex/model-entitlements.ts +54 -4
  96. package/src/codex/project-config-warnings.ts +92 -2
  97. package/src/codex/prompt-layers/encoding.ts +80 -0
  98. package/src/codex/prompt-layers/paths.ts +54 -0
  99. package/src/codex/prompt-layers/revision.ts +55 -0
  100. package/src/codex/prompt-layers/toml-edit.ts +163 -0
  101. package/src/codex/prompt-layers/toml-read.ts +181 -0
  102. package/src/codex/prompt-layers.ts +14 -520
  103. package/src/codex/quota-auto-refresh-state.ts +16 -0
  104. package/src/codex/quota-auto-refresh.ts +219 -0
  105. package/src/codex/quota-types.ts +51 -0
  106. package/src/codex/quota.ts +252 -93
  107. package/src/codex/reserve-availability.ts +177 -0
  108. package/src/codex/routing.ts +28 -9
  109. package/src/codex/shim.ts +53 -11
  110. package/src/codex/subagent-model-fallback.ts +23 -3
  111. package/src/combos/failover.ts +125 -7
  112. package/src/combos/identifiers.ts +89 -0
  113. package/src/combos/index.ts +4 -0
  114. package/src/combos/resolve.ts +80 -9
  115. package/src/combos/types.ts +20 -93
  116. package/src/config/subagent-models.ts +24 -0
  117. package/src/config.ts +156 -13
  118. package/src/generated/compatibility-version.json +454 -166
  119. package/src/generated/model-metadata.ts +1 -1
  120. package/src/integrations/journal.ts +65 -4
  121. package/src/integrations/store.ts +5 -0
  122. package/src/lab/events/limits.ts +4 -0
  123. package/src/lib/destination-policy.ts +31 -2
  124. package/src/lib/errors.ts +39 -0
  125. package/src/lib/provider-outbound.ts +69 -3
  126. package/src/lib/proxy-env.ts +22 -0
  127. package/src/lib/redact-folding.ts +176 -0
  128. package/src/lib/redact.ts +2 -175
  129. package/src/lib/state-store-sweeper.ts +20 -6
  130. package/src/lib/token-estimate.ts +94 -27
  131. package/src/lib/windows-user-principal.ts +53 -5
  132. package/src/oauth/anthropic-routing.ts +99 -3
  133. package/src/oauth/generic-account-failover.ts +36 -13
  134. package/src/oauth/index.ts +127 -26
  135. package/src/oauth/login-cli.ts +5 -0
  136. package/src/oauth/meta-muse.ts +117 -15
  137. package/src/oauth/pool-settings-capability.ts +15 -4
  138. package/src/providers/api-keys.ts +8 -10
  139. package/src/providers/default-aliases.ts +39 -0
  140. package/src/providers/derive.ts +10 -2
  141. package/src/providers/fastwire.ts +36 -7
  142. package/src/providers/initial-model-selection-runtime.ts +90 -0
  143. package/src/providers/initial-model-selection.ts +120 -0
  144. package/src/providers/key-failover.ts +134 -54
  145. package/src/providers/key-store.ts +11 -1
  146. package/src/providers/label.ts +1 -1
  147. package/src/providers/model-discovery.ts +76 -0
  148. package/src/providers/model-rename-startup.ts +72 -8
  149. package/src/providers/openai-sidecar.ts +17 -5
  150. package/src/providers/openai-tiers-destination.ts +102 -0
  151. package/src/providers/openai-tiers.ts +2 -99
  152. package/src/providers/opencode-go-transport.ts +41 -0
  153. package/src/providers/quota-key-accounts.ts +141 -0
  154. package/src/providers/quota-types.ts +9 -0
  155. package/src/providers/quota.ts +535 -104
  156. package/src/providers/registry.ts +60 -17
  157. package/src/providers/xai-responses-opt-in.ts +31 -5
  158. package/src/quota/reset-activation.ts +81 -0
  159. package/src/quota/reset-detector.ts +305 -0
  160. package/src/quota/reset-notify-config.ts +162 -0
  161. package/src/quota/reset-observer.ts +125 -0
  162. package/src/quota/reset-poller.ts +160 -0
  163. package/src/quota/reset-seen-store.ts +385 -0
  164. package/src/quota/reset-sinks.ts +199 -0
  165. package/src/quota/window-mapping.ts +106 -0
  166. package/src/responses/apply-patch-envelope.ts +46 -0
  167. package/src/responses/code-mode-helper-compat.ts +39 -1
  168. package/src/responses/custom-tool-compat.ts +10 -4
  169. package/src/responses/hosted-tool-policy.ts +12 -4
  170. package/src/responses/parser-content.ts +133 -0
  171. package/src/responses/parser-text-format.ts +24 -0
  172. package/src/responses/parser-tools.ts +188 -0
  173. package/src/responses/parser.ts +3 -326
  174. package/src/responses/state.ts +124 -28
  175. package/src/router.ts +48 -13
  176. package/src/routing/analytics.ts +1 -0
  177. package/src/routing/capability.ts +17 -4
  178. package/src/server/auth-cors.ts +7 -1
  179. package/src/server/background-lifecycle.ts +23 -1
  180. package/src/server/chat-completions.ts +25 -3
  181. package/src/server/claude-messages.ts +62 -5
  182. package/src/server/effort-row.ts +1 -1
  183. package/src/server/fast-row.ts +295 -0
  184. package/src/server/gui-static.ts +30 -4
  185. package/src/server/index.ts +107 -21
  186. package/src/server/management/agent-settings-routes.ts +2 -2
  187. package/src/server/management/combo-routes.ts +37 -9
  188. package/src/server/management/config-routes.ts +93 -2
  189. package/src/server/management/integration-routes.ts +108 -0
  190. package/src/server/management/model-routes.ts +13 -3
  191. package/src/server/management/model-rows.ts +20 -1
  192. package/src/server/management/native-integration-routes.ts +4 -1
  193. package/src/server/management/oauth-account-routes.ts +38 -10
  194. package/src/server/management/provider-routes.ts +34 -3
  195. package/src/server/management/quota-reset-routes.ts +57 -0
  196. package/src/server/management/route-registry.ts +7 -4
  197. package/src/server/management/shared.ts +19 -5
  198. package/src/server/management/system-routes.ts +3 -2
  199. package/src/server/management-api.ts +14 -2
  200. package/src/server/ports.ts +12 -2
  201. package/src/server/relay-eager.ts +38 -23
  202. package/src/server/relay.ts +4 -0
  203. package/src/server/request-log.ts +6 -0
  204. package/src/server/responses/codex-ws-correlation.ts +65 -0
  205. package/src/server/responses/codex-ws-exchange.ts +261 -0
  206. package/src/server/responses/codex-ws-metadata.ts +134 -0
  207. package/src/server/responses/codex-ws-pool.ts +162 -0
  208. package/src/server/responses/codex-ws-request.ts +87 -0
  209. package/src/server/responses/codex-ws-session.ts +93 -0
  210. package/src/server/responses/codex-ws-wire.ts +144 -0
  211. package/src/server/responses/collaboration.ts +41 -1
  212. package/src/server/responses/compact.ts +105 -12
  213. package/src/server/responses/core.ts +476 -57
  214. package/src/server/responses/fetch-helpers.ts +10 -3
  215. package/src/server/responses/input-admission.ts +16 -9
  216. package/src/server/responses/responses-field-backfill.ts +1 -1
  217. package/src/server/responses/ws-upstream.ts +34 -318
  218. package/src/server/responses-custom-tool-repair.ts +20 -4
  219. package/src/server/responses-undeclared-tool-guard.ts +100 -8
  220. package/src/server/safe-response-headers.ts +23 -0
  221. package/src/server/search.ts +9 -0
  222. package/src/server/subagent-models-startup.ts +27 -0
  223. package/src/server/system-env-shell.ts +238 -0
  224. package/src/server/system-env.ts +7 -234
  225. package/src/server/ws-bridge.ts +3 -25
  226. package/src/server/xai-responses-startup.ts +21 -0
  227. package/src/service-manager-probe.ts +1 -1
  228. package/src/service.ts +55 -16
  229. package/src/types/config.ts +108 -12
  230. package/src/types/provider.ts +36 -7
  231. package/src/types/tools.ts +26 -1
  232. package/src/types.ts +2 -0
  233. package/src/update/notify.ts +8 -2
  234. package/src/usage/cost.ts +38 -28
  235. package/src/usage/expected-prices.ts +34 -15
  236. package/src/usage/log.ts +2 -0
  237. package/src/usage/model-identity.ts +26 -0
  238. package/src/usage/summary.ts +15 -1
  239. package/src/vision/describe.ts +6 -0
  240. package/src/vision/image-rewrite.ts +108 -0
  241. package/src/vision/index.ts +19 -306
  242. package/src/vision/plan.ts +205 -0
  243. package/src/web-search/executor.ts +6 -0
  244. package/src/web-search/index.ts +8 -1
  245. package/gui/dist/assets/index-BU1tE0sr.js +0 -112
  246. package/gui/dist/assets/index-DL9-iS6J.css +0 -1
@@ -1,3 +1,5 @@
1
+ import { effectiveProviderAlias, effectiveProviderAliasDecision } from "../../providers/default-aliases";
2
+ import { initialModelSelectionPending } from "../../providers/initial-model-selection";
1
3
  import { execFileSync } from "node:child_process";
2
4
  import { createHash, createHmac, randomBytes } from "node:crypto";
3
5
  import { copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
@@ -32,7 +34,7 @@ import type { OcxConfig, OcxProviderConfig } from "../../types";
32
34
  import { modelInList } from "../../types";
33
35
  import { CODEX_REASONING_LEVELS, codexEffortRank, configuredReasoningEfforts, modelRecordValue, sanitizeCodexReasoningEfforts } from "../../reasoning-effort";
34
36
  import { isModelVisionSidecarConsumer } from "../../vision/eligibility";
35
- import { getModelMetadata, getModelMetadataCaseInsensitive, listModelMetadata, resolveMetadataProvider } from "../../generated/model-metadata";
37
+ import { getModelMetadata, getModelMetadataCaseInsensitive, listModelMetadata, resolveMetadataProvider, type ModelMetadata } from "../../generated/model-metadata";
36
38
  import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../../providers/derive";
37
39
  import {
38
40
  captureFastPolicyAuthority,
@@ -68,6 +70,7 @@ import {
68
70
  import { redactSecretString } from "../../lib/redact";
69
71
  import {
70
72
  extractProviderModelItems,
73
+ isRegistryModelDiscoveryUrl,
71
74
  readBoundedDiscoveryJson,
72
75
  resolveProviderModelDiscovery,
73
76
  type ModelDiscoveryResponseFailure,
@@ -164,6 +167,7 @@ interface CapturedProviderGather {
164
167
  readonly request: CapturedModelsRequest;
165
168
  readonly fastPolicyAuthority: FastPolicyAuthority;
166
169
  readonly metadataModelIdCaseFold: boolean;
170
+ readonly effectiveAlias?: string | null;
167
171
  readonly observedAuth?: ModelsAuthResolution;
168
172
  /**
169
173
  * Configured model ids this provider must keep even when live discovery omits
@@ -380,6 +384,7 @@ function captureTrustedOpenAiApiPolicy(
380
384
  models: entry.models,
381
385
  ...(entry.modelContextWindows ? { modelContextWindows: entry.modelContextWindows } : {}),
382
386
  ...(entry.modelMaxInputTokens ? { modelMaxInputTokens: entry.modelMaxInputTokens } : {}),
387
+ ...(entry.modelMaxOutputTokens ? { modelMaxOutputTokens: entry.modelMaxOutputTokens } : {}),
383
388
  ...(entry.virtualModels ? { virtualModels: entry.virtualModels } : {}),
384
389
  ...(entry.modelInputModalities ? { modelInputModalities: entry.modelInputModalities } : {}),
385
390
  ...(entry.modelReasoningEfforts ? { modelReasoningEfforts: entry.modelReasoningEfforts } : {}),
@@ -413,6 +418,7 @@ function captureProviderGather(
413
418
  configured: OcxProviderConfig,
414
419
  authResolver: ModelsAuthResolver,
415
420
  retainConfiguredModelIds?: ReadonlySet<string>,
421
+ config?: Pick<OcxConfig, "providers">,
416
422
  ): CapturedProviderGather {
417
423
  const enriched = detachedClone(withCanonicalOpenAiForwardAuthDefault(name, configured));
418
424
  enrichProviderFromRegistry(name, enriched);
@@ -454,6 +460,7 @@ function captureProviderGather(
454
460
  maxModels: discovery.maxModels,
455
461
  trustedOpenAiApi,
456
462
  });
463
+ const effectiveAlias = effectiveProviderAliasDecision(name, configured, config);
457
464
  return Object.freeze({
458
465
  name,
459
466
  provider,
@@ -462,6 +469,7 @@ function captureProviderGather(
462
469
  request,
463
470
  fastPolicyAuthority,
464
471
  metadataModelIdCaseFold,
472
+ effectiveAlias,
465
473
  ...(observedAuth ? { observedAuth: Object.freeze({ ...observedAuth }) } : {}),
466
474
  ...(retainConfiguredModelIds && retainConfiguredModelIds.size > 0
467
475
  ? { retainConfiguredModelIds }
@@ -503,6 +511,7 @@ function captureGatherFlight(
503
511
  provider,
504
512
  authResolver,
505
513
  comboTargetsByProvider.get(name),
514
+ config,
506
515
  ));
507
516
  const discoveryPolicySnapshots = Object.freeze(providers.map(provider => provider.policy));
508
517
  return Object.freeze({
@@ -625,8 +634,36 @@ export function clearGatherRoutedModelsInflight(): void {
625
634
  gatherInflight.clear();
626
635
  }
627
636
 
637
+ const NUMERIC_MODEL_ID_SEGMENT = /^\d+$/;
638
+
639
+ /**
640
+ * Resolve an unknown Claude point release or date pin from the nearest configured
641
+ * family row. Only numeric tail segments are removed so unrelated model families
642
+ * cannot inherit one another's limits.
643
+ */
644
+ function anthropicFamilyContextWindow(
645
+ record: Record<string, number> | undefined,
646
+ id: string,
647
+ ): number | undefined {
648
+ if (!record || !id.toLowerCase().startsWith("claude-")) return undefined;
649
+ let candidate = id;
650
+ while (true) {
651
+ const cut = candidate.lastIndexOf("-");
652
+ if (cut <= 0 || !NUMERIC_MODEL_ID_SEGMENT.test(candidate.slice(cut + 1))) return undefined;
653
+ candidate = candidate.slice(0, cut);
654
+ const value = modelRecordValue(record, candidate);
655
+ if (typeof value === "number" && value > 0) return value;
656
+ }
657
+ }
658
+
659
+ /**
660
+ * Resolve the configured context window in exact-model, Anthropic numeric-family,
661
+ * then provider-wide order. Return undefined when the selected value is not positive.
662
+ */
628
663
  export function configuredContextWindow(prov: OcxProviderConfig, id: string): number | undefined {
629
- const configured = modelRecordValue(prov.modelContextWindows, id) ?? prov.contextWindow;
664
+ const configured = modelRecordValue(prov.modelContextWindows, id)
665
+ ?? (prov.adapter === "anthropic" ? anthropicFamilyContextWindow(prov.modelContextWindows, id) : undefined)
666
+ ?? prov.contextWindow;
630
667
  return typeof configured === "number" && configured > 0 ? configured : undefined;
631
668
  }
632
669
 
@@ -717,7 +754,7 @@ function configuredVerbositySupport(name: string, prov: OcxProviderConfig | unde
717
754
  // Read from the PROVIDER CONFIG, never from PROVIDER_REGISTRY. A gather flight captures its
718
755
  // registry authority up front and forbids any later registry read, so consulting the registry
719
756
  // here made a custom-destination flight fall back to "configured" instead of serving its own
720
- // discovery result (tests/codex-gather-authority.test.ts). `applyVerbosityDefaults` in
757
+ // discovery result (tests/codex-integration/codex-gather-authority.test.ts). `applyVerbosityDefaults` in
721
758
  // providers/derive.ts materializes the registry default into the config at seed/enrich time.
722
759
  return prov.supportsVerbosity;
723
760
  }
@@ -728,8 +765,17 @@ export function applyProviderConfigHints(
728
765
  model: CatalogModel,
729
766
  providerCap?: number,
730
767
  metadataModelIdCaseFold?: boolean,
768
+ effectiveAlias?: string | null,
731
769
  ): CatalogModel {
732
770
  const displayName = configuredModelDisplayName(prov, model.id);
771
+ // The alias decision is resolved once at flight admission (captureProviderGather) and threaded
772
+ // through as `effectiveAlias`. Re-deriving it here would read PROVIDER_REGISTRY after admission,
773
+ // which is exactly the authority leak tests/codex-integration/codex-gather-authority.test.ts
774
+ // forbids: a flight must not consult the live registry once its transport has been captured.
775
+ // When no decision was threaded in, carry whatever the row already resolved to instead.
776
+ const providerAlias = typeof effectiveAlias === "string" || effectiveAlias === null
777
+ ? effectiveAlias
778
+ : model.providerAlias;
733
779
  const configuredCap = configuredContextWindow(prov, model.id);
734
780
  const configuredMaxInput = configuredMaxInputTokens(prov, model.id);
735
781
  const maxOutputTokens = routedMaxOutputTokens(name, prov, model, model.id, metadataModelIdCaseFold);
@@ -755,6 +801,7 @@ export function applyProviderConfigHints(
755
801
  const {
756
802
  supportsServiceTier: _staleServiceTier,
757
803
  fastTierDescription: _staleFastTierDescription,
804
+ providerAlias: _staleProviderAlias,
758
805
  ...modelWithoutServiceTier
759
806
  } = model;
760
807
  // 已发现窗口只允许被配置值压低;缺窗口时,已开的 Context cap 就是实际窗口。
@@ -767,6 +814,7 @@ export function applyProviderConfigHints(
767
814
  const hinted = {
768
815
  ...modelWithoutServiceTier,
769
816
  ...(displayName !== undefined ? { displayName } : {}),
817
+ ...(providerAlias !== undefined ? { providerAlias } : {}),
770
818
  ...(hintedWindow !== undefined ? { contextWindow: hintedWindow } : {}),
771
819
  ...(inputModalities ? { inputModalities } : {}),
772
820
  ...(reasoningEfforts !== undefined ? { reasoningEfforts } : {}),
@@ -826,8 +874,9 @@ export function catalogHintsFromProviderConfig(
826
874
  id: string,
827
875
  contextCap?: number,
828
876
  metadataModelIdCaseFold?: boolean,
877
+ effectiveAlias?: string | null,
829
878
  ): Partial<CatalogModel> {
830
- const hinted = applyProviderConfigHints(name, prov, { id, provider: name }, contextCap, metadataModelIdCaseFold);
879
+ const hinted = applyProviderConfigHints(name, prov, { id, provider: name }, contextCap, metadataModelIdCaseFold, effectiveAlias);
831
880
  const { provider: _provider, id: _id, ...hints } = hinted;
832
881
  return hints;
833
882
  }
@@ -838,8 +887,9 @@ export function applyConfigHintsToCachedModels(
838
887
  models: CatalogModel[],
839
888
  contextCap?: number,
840
889
  metadataModelIdCaseFold?: boolean,
890
+ effectiveAlias?: string | null,
841
891
  ): CatalogModel[] {
842
- return models.map(model => applyProviderConfigHints(name, prov, model, contextCap, metadataModelIdCaseFold));
892
+ return models.map(model => applyProviderConfigHints(name, prov, model, contextCap, metadataModelIdCaseFold, effectiveAlias));
843
893
  }
844
894
 
845
895
 
@@ -862,6 +912,62 @@ interface ComboCatalogMemberFallback {
862
912
  readonly reasoningEfforts?: readonly string[];
863
913
  }
864
914
 
915
+ /**
916
+ * Ladder advertised for a combo member whose vendor metadata says it reasons but
917
+ * carries no explicit ladder (Claude, Grok). Codex needs a non-empty ladder to show
918
+ * the effort control; the routed adapters clamp to the real upstream top rung.
919
+ */
920
+ const ROUTED_COMBO_MEMBER_REASONING_EFFORTS: readonly string[] = ["low", "medium", "high", "xhigh", "max"];
921
+
922
+ /**
923
+ * Vendor-table lookup tolerant of point releases and date pins. Configured combo
924
+ * targets often name a variant the table does not carry (`claude-fable-5-1`,
925
+ * `claude-opus-4-5-20251101`); the base family row still describes its modality
926
+ * and reasoning capability, so fall back to it before giving up.
927
+ */
928
+ function comboMemberVendorMetadata(provider: string, modelId: string): ModelMetadata | undefined {
929
+ const exact = getModelMetadataCaseInsensitive(provider, modelId);
930
+ if (exact) return exact;
931
+ let candidate = modelId.replace(/\[[^\]]*\]$/, "");
932
+ while (true) {
933
+ const trimmed = candidate.replace(/-\d+$/, "");
934
+ if (trimmed === candidate || !trimmed.includes("-")) return undefined;
935
+ const hit = getModelMetadataCaseInsensitive(provider, trimmed);
936
+ if (hit) return hit;
937
+ candidate = trimmed;
938
+ }
939
+ }
940
+
941
+ /**
942
+ * Combo members are usually thin discovery rows (id + context window). Without a
943
+ * capability source the combo intersection collapses to text-only / no effort ladder,
944
+ * and the Codex app then refuses image attachments and hides the effort picker for
945
+ * every Claude combo. The generated vendor table knows both, so use it as the
946
+ * last-resort fallback when the caller supplied none.
947
+ *
948
+ * `ModelMetadata.maxTokens` is the OUTPUT ceiling, so it fills `maxOutputTokens`.
949
+ * Mapping it onto `maxInputTokens` would be read by the combo intersection
950
+ * (`aggregation.ts` `Math.min` over member input ceilings) as a 128k input limit and
951
+ * shrink a 1M Claude combo window to 128k, taking autoCompactTokenLimit down with it.
952
+ */
953
+ function vendorMetadataComboFallback(target: { provider: string; model: string }): ComboCatalogMemberFallback | undefined {
954
+ const metadataProvider = resolveMetadataProvider(target.provider);
955
+ const metadata = metadataProvider ? comboMemberVendorMetadata(metadataProvider, target.model) : undefined;
956
+ if (!metadata) return undefined;
957
+ return {
958
+ ...(typeof metadata.contextWindow === "number" && metadata.contextWindow > 0
959
+ ? { contextWindow: metadata.contextWindow }
960
+ : {}),
961
+ ...(typeof metadata.maxTokens === "number" && metadata.maxTokens > 0
962
+ ? { maxOutputTokens: metadata.maxTokens }
963
+ : {}),
964
+ ...(Array.isArray(metadata.input) && metadata.input.length > 0
965
+ ? { inputModalities: [...metadata.input] }
966
+ : {}),
967
+ ...(metadata.reasoning === true ? { reasoningEfforts: [...ROUTED_COMBO_MEMBER_REASONING_EFFORTS] } : {}),
968
+ };
969
+ }
970
+
865
971
  /**
866
972
  * Resolve a combo target to a catalog member for derivation.
867
973
  * Prefer discovery metadata; when the target is missing from the gather map or
@@ -877,11 +983,12 @@ export function resolveComboCatalogMember(
877
983
  memberByKey: ReadonlyMap<string, CatalogModel>,
878
984
  providers: ReadonlyMap<string, OcxProviderConfig>,
879
985
  contextCap?: number,
880
- fallback?: ComboCatalogMemberFallback,
986
+ callerFallback?: ComboCatalogMemberFallback,
881
987
  metadataModelIdCaseFold?: boolean,
882
988
  ): CatalogModel | undefined {
883
989
  const existing = memberByKey.get(targetKey(target));
884
990
  const prov = providers.get(target.provider);
991
+ const fallback = callerFallback ?? vendorMetadataComboFallback(target);
885
992
  // Disabled providers never contribute members — even a complete discovery row
886
993
  // is unusable for catalog derivation while the provider is off.
887
994
  if (prov?.disabled === true) return undefined;
@@ -1410,7 +1517,7 @@ async function fetchProviderModelsWithAuth(
1410
1517
  const configured: CatalogModel[] = configuredIds.map(id => ({
1411
1518
  id,
1412
1519
  provider: name,
1413
- ...catalogHintsFromProviderConfig(name, prov, id, contextCap, metadataModelIdCaseFold),
1520
+ ...catalogHintsFromProviderConfig(name, prov, id, contextCap, metadataModelIdCaseFold, captured.effectiveAlias),
1414
1521
  }));
1415
1522
  const withConfiguredRetention = (
1416
1523
  models: CatalogModel[],
@@ -1464,7 +1571,7 @@ async function fetchProviderModelsWithAuth(
1464
1571
  : [{
1465
1572
  id: prov.defaultModel,
1466
1573
  provider: name,
1467
- ...catalogHintsFromProviderConfig(name, prov, prov.defaultModel, contextCap, metadataModelIdCaseFold),
1574
+ ...catalogHintsFromProviderConfig(name, prov, prov.defaultModel, contextCap, metadataModelIdCaseFold, captured.effectiveAlias),
1468
1575
  }];
1469
1576
  const vertexDefaultSeed = seedVertexDefault ? configured[0] : undefined;
1470
1577
  const withVertexDefaultSeed = (models: CatalogModel[]): CatalogModel[] => (
@@ -1481,7 +1588,7 @@ async function fetchProviderModelsWithAuth(
1481
1588
  const cachedCursor = getFreshCached(name, ttlMs);
1482
1589
  if (cachedCursor) {
1483
1590
  return observed(
1484
- withConfiguredRetention(applyConfigHintsToCachedModels(name, prov, cachedCursor, undefined, metadataModelIdCaseFold)),
1591
+ withConfiguredRetention(applyConfigHintsToCachedModels(name, prov, cachedCursor, undefined, metadataModelIdCaseFold, captured.effectiveAlias)),
1485
1592
  "authoritative",
1486
1593
  );
1487
1594
  }
@@ -1489,7 +1596,7 @@ async function fetchProviderModelsWithAuth(
1489
1596
  const cooling = getStaleCached(name);
1490
1597
  return observed(
1491
1598
  withConfiguredRetention(
1492
- cooling ? applyConfigHintsToCachedModels(name, prov, cooling, undefined, metadataModelIdCaseFold) : configured,
1599
+ cooling ? applyConfigHintsToCachedModels(name, prov, cooling, undefined, metadataModelIdCaseFold, captured.effectiveAlias) : configured,
1493
1600
  ),
1494
1601
  "degraded",
1495
1602
  );
@@ -1530,7 +1637,7 @@ async function fetchProviderModelsWithAuth(
1530
1637
  const staleCursor = getStaleCached(name);
1531
1638
  return observed(
1532
1639
  withConfiguredRetention(
1533
- staleCursor ? applyConfigHintsToCachedModels(name, prov, staleCursor, undefined, metadataModelIdCaseFold) : configured,
1640
+ staleCursor ? applyConfigHintsToCachedModels(name, prov, staleCursor, undefined, metadataModelIdCaseFold, captured.effectiveAlias) : configured,
1534
1641
  ),
1535
1642
  "degraded",
1536
1643
  );
@@ -1548,7 +1655,7 @@ async function fetchProviderModelsWithAuth(
1548
1655
  if (fresh) {
1549
1656
  return observed(
1550
1657
  withConfiguredRetention(
1551
- withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, fresh, contextCap, metadataModelIdCaseFold)),
1658
+ withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, fresh, contextCap, metadataModelIdCaseFold, captured.effectiveAlias)),
1552
1659
  ),
1553
1660
  "authoritative",
1554
1661
  ); // dedups Codex's frequent /v1/models polling within the TTL
@@ -1560,7 +1667,7 @@ async function fetchProviderModelsWithAuth(
1560
1667
  return observed(
1561
1668
  withConfiguredRetention(
1562
1669
  stale
1563
- ? withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, stale, contextCap, metadataModelIdCaseFold))
1670
+ ? withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, stale, contextCap, metadataModelIdCaseFold, captured.effectiveAlias))
1564
1671
  : failedDiscoveryConfigured,
1565
1672
  ),
1566
1673
  "degraded",
@@ -1600,7 +1707,7 @@ async function fetchProviderModelsWithAuth(
1600
1707
  return {
1601
1708
  models: withConfiguredRetention(
1602
1709
  stale
1603
- ? withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, stale, contextCap, metadataModelIdCaseFold))
1710
+ ? withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, stale, contextCap, metadataModelIdCaseFold, captured.effectiveAlias))
1604
1711
  : failedDiscoveryConfigured,
1605
1712
  ),
1606
1713
  fallback: stale ? "stale" : "configured",
@@ -1608,16 +1715,24 @@ async function fetchProviderModelsWithAuth(
1608
1715
  };
1609
1716
  };
1610
1717
  try {
1718
+ // Canonical-URL TUN transparency for Clash/Surge/Mihomo fake-IP DNS:
1719
+ // `isRegistryModelDiscoveryUrl` proves the FINAL request URL is the
1720
+ // registry's own fixed discovery URL, so a purely-benchmark DNS answer may
1721
+ // be pin-connected through the intercepting TUN without proxy env. The
1722
+ // proof is on the URL — not the provider name — because an OAuth/forward
1723
+ // name matches any baseUrl by design. Retargeted or renamed custom rows
1724
+ // fetch a different URL and keep the rejection.
1725
+ const outboundDependencies = { isCanonicalUrl: isRegistryModelDiscoveryUrl };
1611
1726
  const res = request.method === "POST"
1612
1727
  ? await providerOutboundPost(name, prov, url, {
1613
1728
  headers,
1614
1729
  body: JSON.stringify({ project }),
1615
1730
  signal: AbortSignal.timeout(8000),
1616
- })
1731
+ }, outboundDependencies)
1617
1732
  : await providerOutboundGet(name, prov, url, {
1618
1733
  headers,
1619
1734
  signal: AbortSignal.timeout(8000),
1620
- });
1735
+ }, outboundDependencies);
1621
1736
  const redirectError = await providerRedirectError(res, url);
1622
1737
  if (redirectError) {
1623
1738
  const { models, fallback, shouldLog } = failedDiscoveryFallback({ reason: "http", httpStatus: res.status });
@@ -1677,7 +1792,7 @@ async function fetchProviderModelsWithAuth(
1677
1792
  reasoningEfforts: [],
1678
1793
  ...(model.contextWindow ? { contextWindow: model.contextWindow } : {}),
1679
1794
  ...(model.inputModalities ? { inputModalities: model.inputModalities } : {}),
1680
- }, contextCap, metadataModelIdCaseFold));
1795
+ }, contextCap, metadataModelIdCaseFold, captured.effectiveAlias));
1681
1796
  const forCache = withConfiguredRetention(live, { retainComboTargets: false });
1682
1797
  if (!setCached(name, forCache, Date.now(), cacheGeneration)) {
1683
1798
  return observed(withConfiguredRetention(configured), "degraded");
@@ -1740,7 +1855,7 @@ async function fetchProviderModelsWithAuth(
1740
1855
  provider: name,
1741
1856
  ...(ownedBy ? { owned_by: ownedBy } : {}),
1742
1857
  ...discoveredHints,
1743
- }, contextCap, metadataModelIdCaseFold);
1858
+ }, contextCap, metadataModelIdCaseFold, captured.effectiveAlias);
1744
1859
  })
1745
1860
  .filter(m => shouldExposeProviderModel(name, m.id));
1746
1861
  // Capture the count BEFORE the alias/configured augmentation below pushes extra rows into
@@ -1902,6 +2017,7 @@ export function filterCatalogVisibleModels(
1902
2017
  }
1903
2018
  }
1904
2019
  return models.filter(m => {
2020
+ if (initialModelSelectionPending(config.providers[m.provider])) return false;
1905
2021
  const nativeAlias = m.provider === COMBO_NAMESPACE && m.nativeAlias === true;
1906
2022
  // disabledModels may be stored raw (canonical) or encoded (legacy UI writes).
1907
2023
  for (const stored of disabled) {
@@ -2042,6 +2158,18 @@ async function gatherRoutedModelsUncached(
2042
2158
  config,
2043
2159
  capture.openAiApiPolicy,
2044
2160
  );
2161
+ const apiProvider = activeProviders.find(provider => provider.name === OPENAI_API_PROVIDER_ID);
2162
+ // Trusted reconstruction replaces whole rows, including the earlier Fast hints.
2163
+ // Restore only that capability from the same captured authority used by discovery.
2164
+ if (apiProvider) {
2165
+ for (const model of apiAugmented) {
2166
+ if (model.provider !== OPENAI_API_PROVIDER_ID) continue;
2167
+ const policy = fastPolicyForModel(apiProvider.provider, model.id, apiProvider.name);
2168
+ const supported = serviceTierSupportFromPolicy(policy);
2169
+ if (supported !== undefined) model.supportsServiceTier = supported;
2170
+ if (supported === true && policy.fastTierDescription !== undefined) model.fastTierDescription = policy.fastTierDescription;
2171
+ }
2172
+ }
2045
2173
  const metadataModelIdCaseFoldByProvider = new Map(
2046
2174
  activeProviders.map(provider => [provider.name, provider.metadataModelIdCaseFold]),
2047
2175
  );
@@ -2132,7 +2260,7 @@ async function gatherRoutedModelsUncached(
2132
2260
  const nativeAliasMaxInput = combo.nativeAlias && combo.alias
2133
2261
  ? (combo.alias.startsWith("gpt-5.6-") || combo.alias.includes("daybreak")
2134
2262
  ? NATIVE_GPT56_MAX_INPUT_TOKENS
2135
- : nativeOpenAiMaxInputTokens(combo.alias) ?? nativeOpenAiContextWindow(combo.alias))
2263
+ : nativeOpenAiMaxInputTokens(combo.alias, comboNativeLimits) ?? nativeOpenAiContextWindow(combo.alias, comboNativeLimits))
2136
2264
  : undefined;
2137
2265
  const nativeAliasAutoCompact = combo.nativeAlias && combo.alias
2138
2266
  ? nativeOpenAiAutoCompactTokenLimit(combo.alias, comboNativeLimits)
@@ -2455,7 +2583,9 @@ function augmentRoutedModelsWithCapturedOpenAiApiRows(
2455
2583
  const maxOutputTokens = routedMaxOutputTokens(
2456
2584
  OPENAI_API_PROVIDER_ID,
2457
2585
  configured,
2458
- existingById.get(id) ?? { provider: OPENAI_API_PROVIDER_ID, id },
2586
+ policy.modelMaxOutputTokens?.[id] !== undefined
2587
+ ? { provider: OPENAI_API_PROVIDER_ID, id, maxOutputTokens: policy.modelMaxOutputTokens[id] }
2588
+ : existingById.get(id) ?? { provider: OPENAI_API_PROVIDER_ID, id },
2459
2589
  policy.virtualModels?.[id]?.wireModelId ?? id,
2460
2590
  );
2461
2591
  return {
@@ -0,0 +1,52 @@
1
+ import type { OcxConfig } from "../../types";
2
+ import { isEffectiveCodexDesktopAuthless } from "../loopback-target";
3
+ import { CODEX_ACCOUNT_BOUND_CATALOG_KIND } from "./account-models";
4
+ import { NATIVE_RESERVE_MODEL } from "./native-models";
5
+ import type { RawEntry } from "./parsing";
6
+
7
+ export const RESERVE_METADATA_SOURCE_FIELD = "opencodex_reserve_metadata_source";
8
+ /** Validated genuine source metadata on the existing catalog, never an authorization. */
9
+ export const RESERVE_SOURCE_CATALOG_FIELD = "opencodex_reserve_source";
10
+ export const RESERVE_LUNA_METADATA_SOURCE = "gpt-5.6-luna";
11
+
12
+ /** Metadata only: no process-local availability or credential state belongs in a catalog. */
13
+ export interface ReserveCatalogProjection {
14
+ readonly source: RawEntry;
15
+ readonly mainSelectors: readonly string[];
16
+ }
17
+
18
+ /** The caller supplies a validated actual observation and an already context-capped Luna pin. */
19
+ export function createReserveCatalogProjection(
20
+ config: Pick<OcxConfig, "runtimeRole" | "hostname" | "unauthenticatedLoopbackListener" | "codexDesktopAuthless">,
21
+ mainSelectors: readonly string[],
22
+ observedSource: RawEntry | null,
23
+ lunaSource: RawEntry | null,
24
+ ): ReserveCatalogProjection | undefined {
25
+ if (!isEffectiveCodexDesktopAuthless(config) || mainSelectors.length === 0) return undefined;
26
+ const original = observedSource ?? lunaSource;
27
+ if (!original) return undefined;
28
+ const source = structuredClone(original);
29
+ source.slug = NATIVE_RESERVE_MODEL;
30
+ source.display_name = observedSource?.display_name ?? "Luna Reserve";
31
+ source.description = "Manual main-account Reserve through OpenCodex; recent upstream permission is required for every request.";
32
+ // This qualified OCX endpoint accepts the selector, not an OpenAI API-key model grant.
33
+ source.supported_in_api = true;
34
+ source[RESERVE_METADATA_SOURCE_FIELD] = observedSource ? NATIVE_RESERVE_MODEL : RESERVE_LUNA_METADATA_SOURCE;
35
+ delete source.available_in_plans;
36
+ delete source.availability_nux;
37
+ delete source.upgrade;
38
+ delete source.opencodex_account_observed_native;
39
+ delete source.opencodex_account_observed_selectors;
40
+ return { source, mainSelectors: [...mainSelectors] };
41
+ }
42
+
43
+ /** Exact OCX account projection, never another provider's similarly named model. */
44
+ export function isReserveCatalogProjection(entry: RawEntry): boolean {
45
+ return entry.opencodex_catalog_kind === CODEX_ACCOUNT_BOUND_CATALOG_KIND
46
+ && typeof entry.slug === "string"
47
+ && entry.slug.indexOf("/") > 0
48
+ && entry.slug.indexOf("/") === entry.slug.lastIndexOf("/")
49
+ && entry.slug.endsWith(`/${NATIVE_RESERVE_MODEL}`)
50
+ && (entry[RESERVE_METADATA_SOURCE_FIELD] === NATIVE_RESERVE_MODEL
51
+ || entry[RESERVE_METADATA_SOURCE_FIELD] === RESERVE_LUNA_METADATA_SOURCE);
52
+ }