@bitkyc08/opencodex 2.41.0 → 2.43.0-preview.20260906

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 (260) 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-VGeQEZ_v.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 +54 -24
  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/identity.ts +8 -2
  26. package/src/adapters/kiro-calibration.ts +181 -0
  27. package/src/adapters/kiro.ts +135 -3
  28. package/src/adapters/openai-responses.ts +259 -29
  29. package/src/adapters/responses-code-mode.ts +59 -0
  30. package/src/adapters/tool-catalog-nudge.ts +1 -1
  31. package/src/adapters/xai-schema-analysis.ts +86 -0
  32. package/src/adapters/xai-tool-schema.ts +2 -87
  33. package/src/adapters/xai-web-search.ts +1 -1
  34. package/src/bridge.ts +47 -13
  35. package/src/chat/inbound.ts +11 -3
  36. package/src/claude/inbound-content-options.ts +60 -0
  37. package/src/claude/inbound-model-options.ts +142 -0
  38. package/src/claude/inbound-records.ts +7 -0
  39. package/src/claude/inbound.ts +10 -202
  40. package/src/claude/model-info.ts +45 -0
  41. package/src/cli/account-auth.ts +49 -9
  42. package/src/cli/account-extended.ts +7 -1
  43. package/src/cli/capabilities.ts +15 -4
  44. package/src/cli/claude.ts +232 -39
  45. package/src/cli/config-command.ts +9 -1
  46. package/src/cli/dispatch.ts +5 -1
  47. package/src/cli/doctor.ts +10 -0
  48. package/src/cli/effort.ts +372 -0
  49. package/src/cli/export-command.ts +3 -9
  50. package/src/cli/help.ts +1 -0
  51. package/src/cli/index.ts +13 -0
  52. package/src/cli/init.ts +4 -0
  53. package/src/cli/model-selection-guidance.ts +30 -0
  54. package/src/cli/models-runtime.ts +3 -2
  55. package/src/cli/models.ts +8 -3
  56. package/src/cli/observe.ts +3 -1
  57. package/src/cli/opencode.ts +4 -1
  58. package/src/cli/provider-runtime.ts +65 -0
  59. package/src/cli/provider.ts +8 -0
  60. package/src/cli/registry.ts +16 -2
  61. package/src/cli/runtime-api.ts +3 -1
  62. package/src/cli/star-prompt.ts +22 -6
  63. package/src/cli/status-probes.ts +168 -0
  64. package/src/cli/status.ts +5 -168
  65. package/src/clients/config-export/constants.ts +69 -0
  66. package/src/clients/config-export/contracts.ts +154 -0
  67. package/src/clients/config-export/dsh.ts +132 -0
  68. package/src/clients/config-export/fast-models.ts +29 -0
  69. package/src/clients/config-export/mcode.ts +83 -0
  70. package/src/clients/config-export/model-metadata.ts +108 -0
  71. package/src/clients/config-export/omp.ts +104 -0
  72. package/src/clients/config-export/zcode.ts +92 -0
  73. package/src/clients/config-export.ts +18 -710
  74. package/src/codex/account-lifecycle.ts +20 -3
  75. package/src/codex/account-usability.ts +2 -0
  76. package/src/codex/auth-api.ts +272 -32
  77. package/src/codex/auth-context.ts +328 -24
  78. package/src/codex/catalog/effort.ts +44 -5
  79. package/src/codex/catalog/metadata.ts +149 -14
  80. package/src/codex/catalog/native-models.ts +116 -4
  81. package/src/codex/catalog/parsing.ts +122 -8
  82. package/src/codex/catalog/provider-fetch.ts +154 -23
  83. package/src/codex/catalog/reserve.ts +52 -0
  84. package/src/codex/catalog/sync.ts +89 -16
  85. package/src/codex/catalog.ts +1 -1
  86. package/src/codex/convergence-types.ts +1 -0
  87. package/src/codex/convergence.ts +2 -0
  88. package/src/codex/data/upstream-models.json +169 -0
  89. package/src/codex/forward-transport-headers.ts +25 -0
  90. package/src/codex/inject.ts +99 -34
  91. package/src/codex/injected-marker.ts +30 -4
  92. package/src/codex/journal.ts +14 -0
  93. package/src/codex/legacy-config-keys.ts +68 -0
  94. package/src/codex/log-guard/inspect-schema.ts +137 -0
  95. package/src/codex/log-guard/inspect.ts +2 -134
  96. package/src/codex/loopback-target.ts +54 -0
  97. package/src/codex/main-account-cache.ts +63 -1
  98. package/src/codex/main-account-hard-lock.ts +52 -0
  99. package/src/codex/main-account.ts +3 -1
  100. package/src/codex/management-convergence.ts +3 -0
  101. package/src/codex/model-entitlements.ts +54 -4
  102. package/src/codex/project-config-warnings.ts +92 -2
  103. package/src/codex/prompt-layers/encoding.ts +80 -0
  104. package/src/codex/prompt-layers/paths.ts +54 -0
  105. package/src/codex/prompt-layers/revision.ts +55 -0
  106. package/src/codex/prompt-layers/toml-edit.ts +163 -0
  107. package/src/codex/prompt-layers/toml-read.ts +181 -0
  108. package/src/codex/prompt-layers.ts +14 -520
  109. package/src/codex/quota-auto-refresh-state.ts +16 -0
  110. package/src/codex/quota-auto-refresh.ts +219 -0
  111. package/src/codex/quota-types.ts +51 -0
  112. package/src/codex/quota.ts +252 -93
  113. package/src/codex/reserve-availability.ts +177 -0
  114. package/src/codex/routing.ts +28 -9
  115. package/src/codex/shim.ts +53 -11
  116. package/src/codex/subagent-model-fallback.ts +23 -3
  117. package/src/combos/failover.ts +125 -7
  118. package/src/combos/identifiers.ts +89 -0
  119. package/src/combos/index.ts +4 -0
  120. package/src/combos/resolve.ts +80 -9
  121. package/src/combos/types.ts +20 -93
  122. package/src/config/subagent-models.ts +24 -0
  123. package/src/config.ts +156 -13
  124. package/src/generated/compatibility-version.json +474 -178
  125. package/src/generated/model-metadata.ts +1 -1
  126. package/src/integrations/journal.ts +65 -4
  127. package/src/integrations/store.ts +5 -0
  128. package/src/lab/events/limits.ts +4 -0
  129. package/src/lib/destination-policy.ts +31 -2
  130. package/src/lib/errors.ts +39 -0
  131. package/src/lib/provider-outbound.ts +69 -3
  132. package/src/lib/proxy-env.ts +22 -0
  133. package/src/lib/redact-folding.ts +176 -0
  134. package/src/lib/redact.ts +2 -175
  135. package/src/lib/state-store-sweeper.ts +20 -6
  136. package/src/lib/token-estimate.ts +94 -27
  137. package/src/lib/windows-user-principal.ts +53 -5
  138. package/src/oauth/account-quota-rank.ts +40 -1
  139. package/src/oauth/anthropic-routing.ts +99 -3
  140. package/src/oauth/chatgpt-device.ts +187 -0
  141. package/src/oauth/chatgpt.ts +31 -4
  142. package/src/oauth/generic-account-failover.ts +36 -13
  143. package/src/oauth/index.ts +140 -29
  144. package/src/oauth/log.ts +3 -0
  145. package/src/oauth/login-cli.ts +5 -0
  146. package/src/oauth/meta-muse.ts +117 -15
  147. package/src/oauth/pool-settings-capability.ts +15 -4
  148. package/src/providers/api-keys.ts +8 -10
  149. package/src/providers/default-aliases.ts +39 -0
  150. package/src/providers/derive.ts +10 -2
  151. package/src/providers/fastwire.ts +36 -7
  152. package/src/providers/initial-model-selection-runtime.ts +90 -0
  153. package/src/providers/initial-model-selection.ts +120 -0
  154. package/src/providers/key-failover.ts +134 -54
  155. package/src/providers/key-store.ts +11 -1
  156. package/src/providers/label.ts +1 -1
  157. package/src/providers/model-discovery.ts +76 -0
  158. package/src/providers/model-rename-startup.ts +72 -8
  159. package/src/providers/muse-subscription-usage.ts +95 -0
  160. package/src/providers/openai-sidecar.ts +17 -5
  161. package/src/providers/openai-tiers-destination.ts +102 -0
  162. package/src/providers/openai-tiers.ts +2 -99
  163. package/src/providers/opencode-go-transport.ts +41 -0
  164. package/src/providers/quota-key-accounts.ts +141 -0
  165. package/src/providers/quota-types.ts +9 -0
  166. package/src/providers/quota.ts +625 -98
  167. package/src/providers/registry.ts +60 -17
  168. package/src/providers/xai-responses-opt-in.ts +31 -5
  169. package/src/quota/reset-activation.ts +81 -0
  170. package/src/quota/reset-detector.ts +305 -0
  171. package/src/quota/reset-notify-config.ts +162 -0
  172. package/src/quota/reset-observer.ts +125 -0
  173. package/src/quota/reset-poller.ts +160 -0
  174. package/src/quota/reset-seen-store.ts +385 -0
  175. package/src/quota/reset-sinks.ts +199 -0
  176. package/src/quota/window-mapping.ts +106 -0
  177. package/src/responses/apply-patch-envelope.ts +46 -0
  178. package/src/responses/code-mode-helper-compat.ts +39 -1
  179. package/src/responses/custom-tool-compat.ts +10 -4
  180. package/src/responses/hosted-tool-policy.ts +12 -4
  181. package/src/responses/parser-content.ts +133 -0
  182. package/src/responses/parser-text-format.ts +24 -0
  183. package/src/responses/parser-tools.ts +188 -0
  184. package/src/responses/parser.ts +3 -326
  185. package/src/responses/state.ts +124 -28
  186. package/src/router.ts +48 -13
  187. package/src/routing/analytics.ts +1 -0
  188. package/src/routing/capability.ts +17 -4
  189. package/src/server/auth-cors.ts +7 -1
  190. package/src/server/background-lifecycle.ts +23 -1
  191. package/src/server/chat-completions.ts +25 -3
  192. package/src/server/claude-messages.ts +62 -5
  193. package/src/server/effort-row.ts +1 -1
  194. package/src/server/fast-row.ts +295 -0
  195. package/src/server/gui-static.ts +30 -4
  196. package/src/server/index.ts +122 -28
  197. package/src/server/live.ts +18 -4
  198. package/src/server/management/agent-settings-routes.ts +2 -2
  199. package/src/server/management/combo-routes.ts +37 -9
  200. package/src/server/management/config-routes.ts +93 -2
  201. package/src/server/management/integration-routes.ts +108 -0
  202. package/src/server/management/model-routes.ts +13 -3
  203. package/src/server/management/model-rows.ts +20 -1
  204. package/src/server/management/native-integration-routes.ts +4 -1
  205. package/src/server/management/oauth-account-routes.ts +45 -10
  206. package/src/server/management/provider-routes.ts +34 -3
  207. package/src/server/management/quota-reset-routes.ts +57 -0
  208. package/src/server/management/route-registry.ts +7 -4
  209. package/src/server/management/shared.ts +19 -5
  210. package/src/server/management/system-routes.ts +3 -2
  211. package/src/server/management-api.ts +14 -2
  212. package/src/server/ports.ts +12 -2
  213. package/src/server/relay-eager.ts +38 -23
  214. package/src/server/relay.ts +4 -0
  215. package/src/server/request-log.ts +6 -0
  216. package/src/server/responses/codex-ws-correlation.ts +65 -0
  217. package/src/server/responses/codex-ws-exchange.ts +261 -0
  218. package/src/server/responses/codex-ws-metadata.ts +134 -0
  219. package/src/server/responses/codex-ws-pool.ts +162 -0
  220. package/src/server/responses/codex-ws-request.ts +87 -0
  221. package/src/server/responses/codex-ws-session.ts +93 -0
  222. package/src/server/responses/codex-ws-wire.ts +144 -0
  223. package/src/server/responses/collaboration.ts +41 -1
  224. package/src/server/responses/compact.ts +105 -12
  225. package/src/server/responses/core.ts +510 -57
  226. package/src/server/responses/empty-completion-guard.ts +4 -0
  227. package/src/server/responses/fetch-helpers.ts +10 -3
  228. package/src/server/responses/input-admission.ts +16 -9
  229. package/src/server/responses/responses-field-backfill.ts +1 -1
  230. package/src/server/responses/ws-upstream.ts +34 -318
  231. package/src/server/responses-custom-tool-repair.ts +20 -4
  232. package/src/server/responses-undeclared-tool-guard.ts +100 -8
  233. package/src/server/safe-response-headers.ts +23 -0
  234. package/src/server/search.ts +9 -0
  235. package/src/server/subagent-models-startup.ts +27 -0
  236. package/src/server/system-env-shell.ts +238 -0
  237. package/src/server/system-env.ts +7 -234
  238. package/src/server/ws-bridge.ts +3 -25
  239. package/src/server/xai-responses-startup.ts +21 -0
  240. package/src/service-manager-probe.ts +1 -1
  241. package/src/service.ts +55 -16
  242. package/src/types/config.ts +108 -12
  243. package/src/types/provider.ts +36 -7
  244. package/src/types/request.ts +8 -0
  245. package/src/types/tools.ts +26 -1
  246. package/src/types.ts +2 -0
  247. package/src/update/notify.ts +8 -2
  248. package/src/usage/cost.ts +38 -28
  249. package/src/usage/expected-prices.ts +34 -15
  250. package/src/usage/log.ts +2 -0
  251. package/src/usage/model-identity.ts +26 -0
  252. package/src/usage/summary.ts +15 -1
  253. package/src/vision/describe.ts +6 -0
  254. package/src/vision/image-rewrite.ts +108 -0
  255. package/src/vision/index.ts +19 -306
  256. package/src/vision/plan.ts +205 -0
  257. package/src/web-search/executor.ts +6 -0
  258. package/src/web-search/index.ts +8 -1
  259. package/gui/dist/assets/index-B2YjLA-i.css +0 -1
  260. package/gui/dist/assets/index-aPup8CKb.js +0 -112
@@ -2,7 +2,7 @@ import { execFileSync } from "node:child_process";
2
2
  import { createHash } from "node:crypto";
3
3
  import { copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
4
4
  import { delimiter, dirname, join, resolve } from "node:path";
5
- import { atomicWriteFile, expandUserPath, getConfigDir, websocketsEnabled } from "../../config";
5
+ import { atomicWriteFile, expandUserPath, getConfigDir, loadConfig, ultraFastTierEnabled, websocketsEnabled } from "../../config";
6
6
  import { CODEX_CONFIG_PATH, CODEX_MODELS_CACHE_PATH, DEFAULT_CATALOG_PATH, readRootTomlString, resolveCodexConfigPath } from "../paths";
7
7
  import { clearModelCache, DEFAULT_MODEL_CACHE_TTL_MS, getFreshCached, getStaleCached, isModelsFetchCoolingDown, markModelsFetchFailure, setCached } from "../model-cache";
8
8
  import { buildModelsRequest, resolveModelsAuthToken } from "../../oauth";
@@ -31,10 +31,11 @@ import { redactSecretString } from "../../lib/redact";
31
31
  import upstreamModelsSnapshot from "../data/upstream-models.json";
32
32
 
33
33
 
34
- import { NATIVE_OPENAI_CONTEXT_OVERRIDES, SUPPORTED_NATIVE_OPENAI_SLUGS, UPSTREAM_NATIVE_ENTRIES, isNativeOpenAiCapabilityAliasModel, nativeMultiAgentVersion, nativeOpenAiAutoCompactTokenLimit, nativeOpenAiContextWindow, nativeOpenAiMaxInputTokens, type NativeContextLimitsInput } from "./metadata";
34
+ import { NATIVE_OPENAI_CONTEXT_OVERRIDES, SUPPORTED_NATIVE_OPENAI_SLUGS, UPSTREAM_NATIVE_ENTRIES, hasNativeOpenAiCapabilityMetadata, nativeMultiAgentVersion, nativeOpenAiAutoCompactTokenLimit, nativeOpenAiContextWindow, nativeOpenAiMaxInputTokens, type NativeContextLimitsInput } from "./metadata";
35
35
  import { clampAutoCompactTokenLimit } from "../../providers/auto-compact-budget";
36
36
  import { trustedAccountBoundNativeCatalogSlug } from "./account-models";
37
37
  import { CODEX_NATIVE_ALIAS_CATALOG_KIND } from "./kinds";
38
+ import { NATIVE_GPT6_ASTRA_MODEL } from "./native-models";
38
39
 
39
40
  export function legacyCatalogBackupPath(): string {
40
41
  return join(getConfigDir(), "catalog-backup.json");
@@ -95,6 +96,8 @@ export const CODEX_PROVIDER_MODEL_CATALOG_KIND = "provider-model-v1";
95
96
  export interface CatalogModel {
96
97
  id: string;
97
98
  provider: string;
99
+ /** Canonical or configured short alias for the provider segment. */
100
+ providerAlias?: string | null;
98
101
  /** Public Codex-facing slug override (used by combo aliases). */
99
102
  alias?: string;
100
103
  /** Explicit combo takeover of a bare OpenAI-native catalog id. */
@@ -303,7 +306,98 @@ const NO_FAST_TIER_NATIVE_SLUGS = new Set([
303
306
  "gpt-5.3-codex-spark",
304
307
  ]);
305
308
 
309
+ /** Does this row already carry an `ultrafast` tier the operator put there themselves? */
310
+ /**
311
+ * Read the opt-in from the live config, ONCE per catalog build.
312
+ *
313
+ * `deriveEntry` and its call sites are pure `RawEntry -> RawEntry` transforms with no
314
+ * config parameter, so the flag is resolved here rather than threaded through all of them.
315
+ * It is memoized because `normalizeRoutedCatalogEntry` runs per entry in a sync loop, and
316
+ * `loadConfig()` chmods the config dir, hardens three secrets, reads the file and runs a
317
+ * full Zod parse — doing that once per catalog row would be a real cost for one boolean.
318
+ * Callers holding a config still pass `opts.ultraFastTier` explicitly, which bypasses this
319
+ * entirely and is what the tests do. A read failure means OFF, matching `.catch(false)`.
320
+ */
321
+ let ultraFastOptInCache: { value: boolean; at: number } | null = null;
322
+ const ULTRA_FAST_OPT_IN_TTL_MS = 5_000;
323
+
324
+ function ultraFastTierOptIn(): boolean {
325
+ const now = Date.now();
326
+ if (ultraFastOptInCache && now - ultraFastOptInCache.at < ULTRA_FAST_OPT_IN_TTL_MS) {
327
+ return ultraFastOptInCache.value;
328
+ }
329
+ let value = false;
330
+ try {
331
+ value = ultraFastTierEnabled(loadConfig());
332
+ } catch {
333
+ value = false;
334
+ }
335
+ ultraFastOptInCache = { value, at: now };
336
+ return value;
337
+ }
338
+
339
+ /** Test seam: drop the memoized opt-in so a config change is observed immediately. */
340
+ export function resetUltraFastTierOptInCache(): void {
341
+ ultraFastOptInCache = null;
342
+ }
343
+
344
+ function entryDeclaresUltraFast(entry: RawEntry): boolean {
345
+ const tiers = entry.service_tiers;
346
+ const declaredInTiers = Array.isArray(tiers) && tiers.some(tier => (
347
+ !!tier && typeof tier === "object" && "id" in tier
348
+ && String((tier as { id?: unknown }).id).trim().toLowerCase() === "ultrafast"
349
+ ));
350
+ const speeds = entry.additional_speed_tiers;
351
+ const declaredInSpeeds = Array.isArray(speeds) && speeds.some(speed => (
352
+ typeof speed === "string" && speed.trim().toLowerCase() === "ultrafast"
353
+ ));
354
+ return declaredInTiers || declaredInSpeeds;
355
+ }
356
+
357
+ /**
358
+ * Keep the operator's `ultrafast` and drop everything else.
359
+ *
360
+ * A routed row must not inherit the native template's `priority` tier, which is the whole
361
+ * reason this strip exists. Preserving the supplied tier without this narrowing would
362
+ * smuggle Fast onto third-party providers under an unrelated flag.
363
+ */
364
+ function retainOnlyUltraFastTier(entry: RawEntry): void {
365
+ const tiers = entry.service_tiers;
366
+ const keptTiers = Array.isArray(tiers)
367
+ ? tiers.filter(tier => (
368
+ !!tier && typeof tier === "object" && "id" in tier
369
+ && String((tier as { id?: unknown }).id).trim().toLowerCase() === "ultrafast"
370
+ ))
371
+ : [];
372
+ if (keptTiers.length > 0) entry.service_tiers = keptTiers;
373
+ else delete entry.service_tiers;
374
+
375
+ const speeds = entry.additional_speed_tiers;
376
+ const keptSpeeds = Array.isArray(speeds)
377
+ ? speeds.filter(speed => typeof speed === "string" && speed.trim().toLowerCase() === "ultrafast")
378
+ : [];
379
+ if (keptSpeeds.length > 0) entry.additional_speed_tiers = keptSpeeds;
380
+ else delete entry.additional_speed_tiers;
381
+
382
+ // A default of `priority` on a row that now only offers ultrafast would name a tier the
383
+ // row no longer carries.
384
+ if (String(entry.service_tier ?? "").trim().toLowerCase() !== "ultrafast") delete entry.service_tier;
385
+ if (String(entry.default_service_tier ?? "").trim().toLowerCase() !== "ultrafast") {
386
+ delete entry.default_service_tier;
387
+ }
388
+ }
389
+
306
390
  export function normalizeServiceTiers(entry: RawEntry): RawEntry {
391
+ // Repair only the old built-in Astra speed copy on persisted native/account rows.
392
+ // A custom description (and every other field) remains user-owned.
393
+ const nativeSlug = trustedAccountBoundNativeCatalogSlug(entry) ?? entry.slug;
394
+ if (nativeSlug === NATIVE_GPT6_ASTRA_MODEL && Array.isArray(entry.service_tiers)) {
395
+ entry.service_tiers = entry.service_tiers.map(tier =>
396
+ tier && typeof tier === "object" && tier.id === "priority"
397
+ && tier.description === "1.5x speed, increased usage"
398
+ ? { ...tier, description: "2x speed, increased usage" } : tier,
399
+ );
400
+ }
307
401
  // Strip service tiers for models that do not actually support the Fast tier.
308
402
  if (typeof entry.slug === "string" && NO_FAST_TIER_NATIVE_SLUGS.has(entry.slug)) {
309
403
  delete entry.service_tier;
@@ -502,6 +596,8 @@ export function ensureStrictCatalogFields(
502
596
  export type MultiAgentMode = "v1" | "default" | "v2";
503
597
 
504
598
  export interface MultiAgentModeOptions {
599
+ /** Caller-owned source metadata already defines the default for these projected rows. */
600
+ preserveDefaultMultiAgentVersion?: (entry: RawEntry) => boolean;
505
601
  /**
506
602
  * When the catalog is in v2 mode, stamp ChatGPT-native rows as v1 instead.
507
603
  * Routed parents get v2 (plaintext child tasks). Native Sol/Terra stay on v1
@@ -526,7 +622,7 @@ export function catalogEntryIsNativeChatGpt(entry: RawEntry): boolean {
526
622
  if (
527
623
  entry.opencodex_catalog_kind === CODEX_CUSTOM_MODEL_CATALOG_KIND
528
624
  && entry.use_responses_lite === true
529
- && isNativeOpenAiCapabilityAliasModel(routedNativeSlug)
625
+ && hasNativeOpenAiCapabilityMetadata(routedNativeSlug)
530
626
  ) return true;
531
627
  if (UPSTREAM_NATIVE_ENTRIES.has(slug) || SUPPORTED_NATIVE_OPENAI_SLUGS.has(slug)) return true;
532
628
  return false;
@@ -577,6 +673,7 @@ export function applyMultiAgentMode(
577
673
  // Restore upstream defaults: clear any stale forced multi_agent_version and
578
674
  // re-apply upstream pins from the snapshot for native entries that have one.
579
675
  for (const entry of entries) {
676
+ if (options.preserveDefaultMultiAgentVersion?.(entry)) continue;
580
677
  const slug = typeof entry.slug === "string" ? entry.slug : "";
581
678
  const nativeAlias = entry.opencodex_catalog_kind === CODEX_NATIVE_ALIAS_CATALOG_KIND;
582
679
  const routedNativeSlug = slug.startsWith(`${OPENAI_CODEX_PROVIDER_ID}/`)
@@ -584,7 +681,7 @@ export function applyMultiAgentMode(
584
681
  : "";
585
682
  const codexForwardCapabilityAlias = entry.opencodex_catalog_kind === CODEX_CUSTOM_MODEL_CATALOG_KIND
586
683
  && entry.use_responses_lite === true
587
- && isNativeOpenAiCapabilityAliasModel(routedNativeSlug)
684
+ && hasNativeOpenAiCapabilityMetadata(routedNativeSlug)
588
685
  ? routedNativeSlug
589
686
  : undefined;
590
687
  const upstreamPin = nativeAlias
@@ -612,17 +709,34 @@ export function normalizeRoutedCatalogEntry(
612
709
  entry: RawEntry,
613
710
  parallelToolCalls = false,
614
711
  toolMode?: "code_mode_only" | "shell" | string,
712
+ opts?: { ultraFastTier?: boolean },
615
713
  ): RawEntry {
616
714
  delete entry.model_messages;
617
715
  delete entry.tool_mode;
618
716
  applyRoutedCodexToolMode(entry, toolMode);
619
717
  delete entry.multi_agent_version;
718
+ delete entry.multi_agent_reasoning_effort;
620
719
  delete entry.use_responses_lite;
621
720
  delete entry.supports_websockets;
622
- delete entry.additional_speed_tiers;
623
- delete entry.service_tier;
624
- delete entry.service_tiers;
625
- delete entry.default_service_tier;
721
+ /*
722
+ * Tier metadata is stripped from routed rows because a row cloned from a native template
723
+ * would otherwise hand a third-party provider OpenAI's tiers.
724
+ *
725
+ * The opt-in carves out exactly one case: an `ultrafast` the OPERATOR put in their own
726
+ * catalog. #3429's reporter added it by hand and watched a regeneration delete it every
727
+ * time. Preserving what they wrote is not the same as advertising a tier — upstream
728
+ * publishes only `priority`, and synthesizing an ultrafast row is what PR #2994 was
729
+ * closed for. So this keeps a supplied tier and still never invents one.
730
+ */
731
+ const keepUltraFast = (opts?.ultraFastTier ?? ultraFastTierOptIn()) && entryDeclaresUltraFast(entry);
732
+ if (!keepUltraFast) {
733
+ delete entry.additional_speed_tiers;
734
+ delete entry.service_tier;
735
+ delete entry.service_tiers;
736
+ delete entry.default_service_tier;
737
+ } else {
738
+ retainOnlyUltraFastTier(entry);
739
+ }
626
740
  // Routed rows cloned from native templates must not inherit OpenAI-only summary delivery.
627
741
  // Explicit provider/model metadata is re-applied after this normalization step.
628
742
  delete entry.supports_reasoning_summaries;
@@ -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,
@@ -81,7 +84,7 @@ import { createAdmissionGate, ResourceAdmissionError, type AdmissionMetrics } fr
81
84
 
82
85
  import { CODEX_CUSTOM_MODEL_CATALOG_KIND, JAWCODE_CATALOG_AUGMENT_PROVIDERS, catalogModelSlug, shouldExposeRoutedModel } from "./parsing";
83
86
  import type { CatalogModel } from "./parsing";
84
- import { disabledNativeSlugs, hasComboTargets, isNativeOpenAiCapabilityAliasModel, NATIVE_GPT56_MAX_INPUT_TOKENS, nativeContextLimits, nativeDefaultReasoningEffort, nativeInputModalities, nativeOpenAiAutoCompactTokenLimit, nativeOpenAiContextWindow, nativeOpenAiMaxInputTokens, nativeOpenAiMaxOutputTokens, nativeOpenAiSlugs, nativeParallelToolCalls, nativeReasoningEfforts } from "./metadata";
87
+ import { disabledNativeSlugs, hasComboTargets, hasNativeOpenAiCapabilityMetadata, NATIVE_GPT56_MAX_INPUT_TOKENS, nativeContextLimits, nativeOpenAiCapabilityDisplayName, nativeDefaultReasoningEffort, nativeInputModalities, nativeOpenAiAutoCompactTokenLimit, nativeOpenAiContextWindow, nativeOpenAiMaxInputTokens, nativeOpenAiMaxOutputTokens, nativeOpenAiSlugs, nativeParallelToolCalls, nativeReasoningEfforts } from "./metadata";
85
88
  import { deriveComboCatalogModel, normalizedOpenAiApiSignature, openAiApiCollisionWarnings, replaceLastComboCatalogOmissions, warnUncataloguedComboOnce } from "./aggregation";
86
89
  import type { ComboCatalogOmission } from "./aggregation";
87
90
  import type { CatalogGatherProviderAuthEvidence } from "./filesystem-evidence";
@@ -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)
@@ -2190,7 +2318,7 @@ async function gatherRoutedModelsUncached(
2190
2318
  const codexForwardNativeCapabilityAlias = cm.provider === OPENAI_CODEX_PROVIDER_ID
2191
2319
  && providerForCanonicalCheck !== undefined
2192
2320
  && isCanonicalOpenAiForwardProvider(providerForCanonicalCheck)
2193
- && isNativeOpenAiCapabilityAliasModel(cm.modelId);
2321
+ && hasNativeOpenAiCapabilityMetadata(cm.modelId);
2194
2322
  const customNativeLimits = {
2195
2323
  ...nativeContextLimits(config),
2196
2324
  ...(typeof cm.contextWindow === "number" && cm.contextWindow > 0
@@ -2252,7 +2380,8 @@ async function gatherRoutedModelsUncached(
2252
2380
  // Display-only label: never feeds routing (customModels are keyed by routedSlug below).
2253
2381
  ...(cm.displayName
2254
2382
  ? { displayName: cm.displayName }
2255
- : codexForwardNativeCapabilityAlias ? { displayName: "Daybreak Blue" } : {}),
2383
+ : codexForwardNativeCapabilityAlias
2384
+ ? { displayName: nativeOpenAiCapabilityDisplayName(cm.modelId) ?? cm.modelId } : {}),
2256
2385
  ...(customContextWindow !== undefined ? { contextWindow: customContextWindow } : {}),
2257
2386
  ...(customMaxInputTokens !== undefined ? { maxInputTokens: customMaxInputTokens } : {}),
2258
2387
  ...(customMaxOutputTokens !== undefined ? { maxOutputTokens: customMaxOutputTokens } : {}),
@@ -2454,7 +2583,9 @@ function augmentRoutedModelsWithCapturedOpenAiApiRows(
2454
2583
  const maxOutputTokens = routedMaxOutputTokens(
2455
2584
  OPENAI_API_PROVIDER_ID,
2456
2585
  configured,
2457
- 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 },
2458
2589
  policy.virtualModels?.[id]?.wireModelId ?? id,
2459
2590
  );
2460
2591
  return {