@bitkyc08/opencodex 2.73.0 → 2.74.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 (58) hide show
  1. package/gui/dist/assets/{App-mHGa7dE1.js → App-B00Yq11u.js} +15 -15
  2. package/gui/dist/assets/App-KLz1TAXi.css +1 -0
  3. package/gui/dist/assets/{Tray-BxY_RYZg.js → Tray-D3Ij1Cvf.js} +1 -1
  4. package/gui/dist/assets/{index-ur_BPgK1.js → index-BYQQfBGK.js} +12 -12
  5. package/gui/dist/assets/index-DBT3Q31Y.css +1 -0
  6. package/gui/dist/assets/{quota-summary-D1Vq7N9H.js → quota-summary-yPtcEhGI.js} +1 -1
  7. package/gui/dist/index.html +2 -2
  8. package/package.json +1 -1
  9. package/src/adapters/kiro/stream.ts +49 -28
  10. package/src/adapters/openai-chat.ts +3 -5
  11. package/src/adapters/openai-responses/passthrough.ts +2 -5
  12. package/src/codex/auth-api/account-list.ts +14 -2
  13. package/src/codex/auth-api/main-account-probe.ts +2 -0
  14. package/src/codex/auth-api/pool-quota-probe.ts +4 -0
  15. package/src/codex/autostart-health.ts +15 -3
  16. package/src/codex/catalog/discovered-natives.ts +166 -0
  17. package/src/codex/catalog/metadata.ts +25 -6
  18. package/src/codex/catalog/model-hints.ts +8 -0
  19. package/src/codex/catalog/model-visibility.ts +4 -0
  20. package/src/codex/catalog/native-models.ts +46 -19
  21. package/src/codex/catalog-auto-refresh-sources.ts +71 -0
  22. package/src/codex/catalog-auto-refresh.ts +42 -6
  23. package/src/codex/catalog-refresh-status.ts +4 -0
  24. package/src/codex/credits.ts +86 -0
  25. package/src/codex/main-account-hard-lock.ts +2 -2
  26. package/src/codex/model-entitlements.ts +122 -1
  27. package/src/codex/quota-types.ts +1 -0
  28. package/src/codex/quota.ts +4 -3
  29. package/src/codex/shim-probe.ts +5 -1
  30. package/src/codex/shim-templates.ts +10 -4
  31. package/src/config/derived-registries.ts +4 -2
  32. package/src/config/diagnostics.ts +9 -0
  33. package/src/config/feature-flags.ts +6 -4
  34. package/src/config/schema/config-schema.ts +1 -0
  35. package/src/config/schema/leaf-validators.ts +1 -0
  36. package/src/generated/compatibility-version.json +67 -43
  37. package/src/generated/model-metadata.ts +1 -1
  38. package/src/lib/test-home-guard.ts +47 -3
  39. package/src/providers/fastwire.ts +22 -0
  40. package/src/providers/model-rename-startup.ts +7 -5
  41. package/src/providers/registry/entries-core.ts +13 -6
  42. package/src/providers/registry/entries-extended.ts +14 -8
  43. package/src/providers/registry/model-seeds.ts +14 -1
  44. package/src/providers/stale-model-roster-migration.ts +101 -0
  45. package/src/providers/xai-fast-model.ts +85 -0
  46. package/src/server/auth-cors.ts +1 -0
  47. package/src/server/background-lifecycle.ts +2 -2
  48. package/src/server/management/config-routes.ts +13 -1
  49. package/src/server/management/oauth-account-routes.ts +8 -2
  50. package/src/server/responses/core-normalize.ts +3 -0
  51. package/src/server/startup-health-cache.ts +7 -2
  52. package/src/service/desktop-startup.ts +93 -0
  53. package/src/types/config.ts +12 -11
  54. package/src/types/provider.ts +3 -1
  55. package/src/types/request.ts +2 -0
  56. package/src/usage/log.ts +3 -1
  57. package/gui/dist/assets/App-DCBismRi.css +0 -1
  58. package/gui/dist/assets/index-pWYJt_vL.css +0 -1
@@ -4,12 +4,14 @@ import type { OcxConfig } from "../types";
4
4
  import { getCodexRoutingKind, type CodexRoutingKind } from "./inject";
5
5
  import { collectRoutingAdoption, type RoutingAdoptionEvidence } from "./routing-adoption";
6
6
  import { diagnoseCodexShim, type CodexShimDiagnostic } from "./shim";
7
+ import { diagnoseMacDesktopStartup, type DesktopStartupDiagnostic } from "../service/desktop-startup";
7
8
 
8
- export type StartupProtection = "service" | "shim" | "none";
9
+ export type StartupProtection = "service" | "desktop" | "shim" | "none";
9
10
  export type StartupHealthStatus = "native" | "protected" | "at-risk";
10
11
  export type ShimCoverage = "full" | "cli-only" | "none";
11
12
 
12
13
  export interface StartupHealthInputs {
14
+ desktop?: DesktopStartupDiagnostic;
13
15
  routingKind: CodexRoutingKind;
14
16
  autostartEnabled: boolean;
15
17
  serviceInstalled: boolean;
@@ -27,6 +29,7 @@ export interface StartupHealthInputs {
27
29
  }
28
30
 
29
31
  export interface StartupHealth {
32
+ desktop?: DesktopStartupDiagnostic;
30
33
  status: StartupHealthStatus;
31
34
  routingKind: CodexRoutingKind;
32
35
  routingInjected: boolean;
@@ -77,18 +80,22 @@ export function deriveStartupHealth(inputs: StartupHealthInputs): StartupHealth
77
80
  // We can only credit an opencodex service/shim for routing that opencodex owns.
78
81
  // An arbitrary localhost gateway has an independent lifecycle that OCX cannot repair.
79
82
  const ownsLocalRouting = inputs.routingKind === "opencodex-local";
83
+ const desktopEffective = inputs.platform === "darwin" && !inputs.diagnosticStale
84
+ && inputs.desktop?.owned === true && inputs.desktop.loginEnabled
85
+ && inputs.desktop.running && inputs.desktop.viable;
80
86
  const protection: StartupProtection = ownsLocalRouting && inputs.serviceViable
81
87
  ? "service"
88
+ : ownsLocalRouting && desktopEffective ? "desktop"
82
89
  : ownsLocalRouting && shimEffective
83
90
  ? "shim"
84
91
  : "none";
85
- const rebootSafe = !localRoutingDependency || (ownsLocalRouting && inputs.serviceViable);
92
+ const rebootSafe = !localRoutingDependency || (ownsLocalRouting && (inputs.serviceViable || desktopEffective));
86
93
  const status: StartupHealthStatus = !localRoutingDependency
87
94
  ? "native"
88
95
  : rebootSafe
89
96
  ? "protected"
90
97
  : "at-risk";
91
- const recommendedCommand = status !== "at-risk"
98
+ const recommendedCommand = status !== "at-risk" || (ownsLocalRouting && inputs.desktop?.owned)
92
99
  ? null
93
100
  : inputs.routingKind === "custom-local" || inputs.routingKind === "unknown"
94
101
  ? COMMANDS.restoreNative
@@ -115,6 +122,7 @@ export function deriveStartupHealth(inputs: StartupHealthInputs): StartupHealth
115
122
  }
116
123
 
117
124
  export interface StartupHealthDiagnostics {
125
+ desktop?: DesktopStartupDiagnostic;
118
126
  routingKind?: CodexRoutingKind;
119
127
  service?: ServiceDiagnostic;
120
128
  shim?: CodexShimDiagnostic;
@@ -129,9 +137,11 @@ export function collectStartupHealth(
129
137
  const shim = diagnostics.shim ?? diagnoseCodexShim();
130
138
  const service = diagnostics.service ?? diagnoseService();
131
139
  const routingKind = diagnostics.routingKind ?? getCodexRoutingKind();
140
+ const desktop = diagnostics.desktop ?? diagnoseMacDesktopStartup();
132
141
  const routingAdoption = diagnostics.routingAdoption
133
142
  ?? (routingKind === "opencodex-local" ? collectRoutingAdoption({ routingKind }) : undefined);
134
143
  return deriveStartupHealth({
144
+ ...(desktop ? { desktop } : {}),
135
145
  routingKind,
136
146
  autostartEnabled: codexAutoStartEnabled(config),
137
147
  serviceInstalled: service.installed,
@@ -187,9 +197,11 @@ function classifyStartupHealthSummary(health: StartupHealth): string {
187
197
  ? "custom remote Codex routing (no local restart dependency)"
188
198
  : "native Codex routing (no opencodex restart dependency)";
189
199
  if (health.protection === "service") return "protected by background service";
200
+ if (health.protection === "desktop") return "protected by desktop app at login and its proxy supervisor";
190
201
  const command = health.recommendedCommand ?? health.commands.restoreNative;
191
202
  if (health.routingKind === "unknown") return `AT RISK after restart (Codex routing could not be verified; run '${command}')`;
192
203
  if (health.routingKind === "custom-local") return `AT RISK after restart (custom local gateway lifecycle is not managed by opencodex; run '${command}')`;
204
+ if (health.desktop?.owned) return "AT RISK after restart (desktop startup could not be verified; reopen OpenCodex and check Start at Login)";
193
205
  if (health.shimCoverage === "cli-only") return `AT RISK for Codex Desktop after restart (launcher shim covers CLI scripts only; run '${command}')`;
194
206
  if (health.serviceConflict) return `AT RISK after restart (background service managers conflict; run '${command}')`;
195
207
  if (health.serviceStale) return `AT RISK after restart (background service files are stale; run '${command}')`;
@@ -0,0 +1,166 @@
1
+ import { existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { atomicWriteFile } from "../../config/atomic-write";
4
+ import { getConfigDir } from "../../config/paths";
5
+ import { assertNotRealHomeUnderTest } from "../../lib/test-home-guard";
6
+ import { isEligibleConfiguredNativeOpenAiModel, setDiscoveredNativeOpenAiModels } from "./native-models";
7
+
8
+ export const DISCOVERED_NATIVE_MAX_ROWS = 32;
9
+ /**
10
+ * A real row carries its full base instructions twice (`base_instructions` and the
11
+ * `model_messages` template): GPT-6.1 Sol's live row was 87,183 bytes on 2026-09-30, so a
12
+ * 64 KiB bound silently rejected exactly the model this store exists to discover.
13
+ */
14
+ export const DISCOVERED_NATIVE_MAX_ROW_BYTES = 256 * 1024;
15
+ export const DISCOVERED_NATIVE_MAX_FILE_BYTES = DISCOVERED_NATIVE_MAX_ROWS * (DISCOVERED_NATIVE_MAX_ROW_BYTES + 1024);
16
+ export const DISCOVERED_NATIVE_RETENTION_MS = 14 * 24 * 60 * 60 * 1000;
17
+ /**
18
+ * An unchanged row renews its last-seen time on disk at most this often. The entitlement path
19
+ * records on every successful roster fetch, including request-time ones, so writing each renewal
20
+ * would put a synchronous file replace (and on Windows an ACL pass) on ordinary traffic.
21
+ */
22
+ export const DISCOVERED_NATIVE_RENEW_INTERVAL_MS = 60 * 60 * 1000;
23
+ const FILE_NAME = "discovered-native-models.json";
24
+
25
+ export interface DiscoveredNativeModel {
26
+ slug: string;
27
+ row: Record<string, unknown>;
28
+ firstSeenAt: number;
29
+ lastSeenAt: number;
30
+ clientVersion: string;
31
+ }
32
+
33
+ let loadedPath: string | undefined;
34
+ let models: DiscoveredNativeModel[] = [];
35
+ let fingerprint = "[]";
36
+ let generation = 0;
37
+
38
+ /** Validate untrusted roster rows without filesystem effects; keep upstream capability metadata. */
39
+ export function validateDiscoveredNativeRows(rows: unknown): Record<string, unknown>[] {
40
+ if (!Array.isArray(rows)) return [];
41
+ const accepted = new Map<string, Record<string, unknown>>();
42
+ for (const candidate of rows) {
43
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) continue;
44
+ const row = candidate as Record<string, unknown>;
45
+ if (typeof row.slug !== "string" || row.slug.length > 128
46
+ || !isEligibleConfiguredNativeOpenAiModel(row.slug)
47
+ || row.supported_in_api !== true || row.visibility === "hide"
48
+ || typeof row.display_name !== "string" || !row.display_name.trim()
49
+ || !Array.isArray(row.supported_reasoning_levels)
50
+ || row.supported_reasoning_levels.length > 16
51
+ || !row.supported_reasoning_levels.every(level => level && typeof level === "object"
52
+ && !Array.isArray(level) && typeof level.effort === "string" && level.effort.length > 0
53
+ && level.effort.length <= 32 && typeof level.description === "string")) continue;
54
+ if ([row.context_window, row.max_context_window].some(value => value !== undefined && value !== null
55
+ && (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0))) continue;
56
+ try {
57
+ const serialized = JSON.stringify(row);
58
+ if (Buffer.byteLength(serialized, "utf8") > DISCOVERED_NATIVE_MAX_ROW_BYTES) continue;
59
+ if (accepted.size >= DISCOVERED_NATIVE_MAX_ROWS && !accepted.has(row.slug)) continue;
60
+ accepted.set(row.slug, JSON.parse(serialized) as Record<string, unknown>);
61
+ } catch { /* Cyclic/non-JSON rows cannot be persisted or sent to Codex. */ }
62
+ }
63
+ return [...accepted.values()];
64
+ }
65
+
66
+ function boundedModels(entries: DiscoveredNativeModel[], now: number): DiscoveredNativeModel[] {
67
+ return entries.filter(entry => entry.lastSeenAt >= now - DISCOVERED_NATIVE_RETENTION_MS)
68
+ .sort((a, b) => b.lastSeenAt - a.lastSeenAt || a.slug.localeCompare(b.slug))
69
+ .slice(0, DISCOVERED_NATIVE_MAX_ROWS);
70
+ }
71
+
72
+ function readModels(path: string, now: number): DiscoveredNativeModel[] {
73
+ return readModelsWithCount(path, now).models;
74
+ }
75
+
76
+ /** `storedCount` is the raw entry count, so a caller can tell pruning from an unchanged store. */
77
+ function readModelsWithCount(path: string, now: number): { models: DiscoveredNativeModel[]; storedCount: number } {
78
+ const empty = { models: [], storedCount: 0 };
79
+ try {
80
+ if (statSync(path).size > DISCOVERED_NATIVE_MAX_FILE_BYTES) return empty;
81
+ const bytes = readFileSync(path, "utf8");
82
+ if (Buffer.byteLength(bytes, "utf8") > DISCOVERED_NATIVE_MAX_FILE_BYTES) return empty;
83
+ const data = JSON.parse(bytes) as { version?: unknown; models?: unknown };
84
+ if (data.version !== 1 || !Array.isArray(data.models) || data.models.length > DISCOVERED_NATIVE_MAX_ROWS) return empty;
85
+ const valid = new Map<string, DiscoveredNativeModel>();
86
+ for (const entry of data.models) {
87
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
88
+ const row = validateDiscoveredNativeRows([entry.row])[0];
89
+ if (!row || entry.slug !== row.slug
90
+ || !Number.isSafeInteger(entry.firstSeenAt) || entry.firstSeenAt < 0
91
+ || !Number.isSafeInteger(entry.lastSeenAt) || entry.lastSeenAt < entry.firstSeenAt
92
+ || entry.lastSeenAt > now
93
+ || typeof entry.clientVersion !== "string" || entry.clientVersion.length > 64) continue;
94
+ valid.set(entry.slug, { slug: entry.slug, row, firstSeenAt: entry.firstSeenAt,
95
+ lastSeenAt: entry.lastSeenAt, clientVersion: entry.clientVersion });
96
+ }
97
+ return { models: boundedModels([...valid.values()], now), storedCount: data.models.length };
98
+ } catch { return empty; }
99
+ }
100
+
101
+ function publish(entries: DiscoveredNativeModel[]): void {
102
+ models = entries;
103
+ // Seen-at timestamps extend retention but do not invalidate an otherwise identical catalog.
104
+ const next = JSON.stringify(entries.map(({ slug, row }) => ({ slug, row })).sort((a, b) => a.slug.localeCompare(b.slug)));
105
+ if (next === fingerprint) return;
106
+ fingerprint = next;
107
+ generation += 1;
108
+ setDiscoveredNativeOpenAiModels(entries.map(entry => entry.row));
109
+ }
110
+
111
+ /** Load on config activation; switching OpenCodex homes never carries the previous home's rows. */
112
+ export function loadDiscoveredNativeModels(now = Date.now()): void {
113
+ const path = join(getConfigDir(), FILE_NAME);
114
+ loadedPath = path;
115
+ publish(readModels(path, now));
116
+ }
117
+
118
+ /** Monotonic process-local catalog evidence generation, independent of account identity. */
119
+ export function discoveredNativeModelsGeneration(): number {
120
+ return generation;
121
+ }
122
+
123
+ /** A confirmed nonempty roster owns discovery. Optional persistence must never fail entitlement. */
124
+ export function recordDiscoveredNativeModels(rows: unknown, clientVersion: string, now = Date.now()): void {
125
+ try {
126
+ if (!Number.isSafeInteger(now) || now < 0 || clientVersion.length > 64) return;
127
+ const dir = getConfigDir();
128
+ const path = join(dir, FILE_NAME);
129
+ // Re-read the store rather than trusting this process's snapshot: another OpenCodex process
130
+ // sharing the home may have recorded a model since, and merging onto a stale copy would drop
131
+ // it at the replace below. The write is atomic, not locked, so only a same-instant race remains.
132
+ const { models: onDisk, storedCount } = readModelsWithCount(path, now);
133
+ const merged = new Map(loadedPath === path ? models.map(entry => [entry.slug, entry]) : []);
134
+ for (const entry of onDisk) {
135
+ const known = merged.get(entry.slug);
136
+ if (!known || entry.lastSeenAt >= known.lastSeenAt) merged.set(entry.slug, entry);
137
+ }
138
+ loadedPath = path;
139
+ for (const row of validateDiscoveredNativeRows(rows)) {
140
+ const slug = row.slug as string;
141
+ const previous = merged.get(slug);
142
+ if (previous && previous.lastSeenAt > now) continue;
143
+ if (previous && JSON.stringify(previous.row) === JSON.stringify(row)
144
+ && now - previous.lastSeenAt < DISCOVERED_NATIVE_RENEW_INTERVAL_MS) continue;
145
+ merged.set(slug, { slug, row, firstSeenAt: previous?.firstSeenAt ?? now, lastSeenAt: now, clientVersion });
146
+ }
147
+ const next = boundedModels([...merged.values()], now);
148
+ const serialized = JSON.stringify({ version: 1, models: next });
149
+ if (Buffer.byteLength(serialized, "utf8") > DISCOVERED_NATIVE_MAX_FILE_BYTES) return;
150
+ publish(next);
151
+ // Nothing new to say: skip the write (and, for empty rosters, never create the file).
152
+ if (storedCount === onDisk.length && serialized === JSON.stringify({ version: 1, models: onDisk })) return;
153
+ if (next.length === 0 && !existsSync(path)) return;
154
+ assertNotRealHomeUnderTest(dir);
155
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
156
+ atomicWriteFile(path, serialized + "\n");
157
+ } catch { /* Discovery is optional; current in-memory metadata survives a disk write failure. */ }
158
+ }
159
+
160
+ export function resetDiscoveredNativeModelsForTests(): void {
161
+ loadedPath = undefined;
162
+ models = [];
163
+ fingerprint = "[]";
164
+ generation += 1;
165
+ setDiscoveredNativeOpenAiModels([]);
166
+ }
@@ -55,6 +55,8 @@ import {
55
55
  SUPPORTED_NATIVE_OPENAI_SLUGS,
56
56
  RETIRED_NATIVE_OPENAI_MODELS,
57
57
  configuredNativeOpenAiModels,
58
+ discoveredNativeOpenAiModels,
59
+ discoveredNativeOpenAiRow,
58
60
  hasNativeOpenAiCapabilityMetadata,
59
61
  isConfiguredNativeOpenAiModel,
60
62
  isNativeOpenAiCapabilityAliasModel,
@@ -219,7 +221,9 @@ const PINNED_UPSTREAM_MODELS: Map<string, RawEntry> = new Map(
219
221
  );
220
222
 
221
223
  function pinnedNativeCapabilityEntry(slug: string): RawEntry | undefined {
222
- return PINNED_UPSTREAM_MODELS.get(nativeOpenAiCapabilitySourceSlug(slug));
224
+ const discovered = discoveredNativeOpenAiRow(slug);
225
+ return discovered ? discoveredNativeCapabilityEntry(discovered)
226
+ : PINNED_UPSTREAM_MODELS.get(nativeOpenAiCapabilitySourceSlug(slug));
223
227
  }
224
228
 
225
229
  /**
@@ -419,7 +423,7 @@ export function nativeReasoningEfforts(slug: string): string[] {
419
423
  const levels = Array.isArray(upstream?.supported_reasoning_levels)
420
424
  ? upstream!.supported_reasoning_levels as Array<{ effort?: string }>
421
425
  : [];
422
- if (levels.length > 0) {
426
+ if (levels.length > 0 || discoveredNativeOpenAiRow(slug)) {
423
427
  // Preserve the exact pinned per-model ladder. In particular, GPT-5.6 Sol and Terra
424
428
  // include ultra while Luna intentionally ends at max.
425
429
  return levels.flatMap(l => typeof l.effort === "string" ? [l.effort] : []);
@@ -565,6 +569,8 @@ export function applyNativeVisibility(
565
569
  }
566
570
 
567
571
  function upstreamNativeEntryForSlug(slug: string): RawEntry | undefined {
572
+ const discovered = discoveredNativeOpenAiRow(slug);
573
+ if (discovered) return discoveredNativeCapabilityEntry(discovered);
568
574
  const sourceSlug = nativeOpenAiCapabilitySourceSlug(slug);
569
575
  // A self-described native returns its OWN pinned row; the alias-cloning branch below stays
570
576
  // reserved for slugs that genuinely borrow another model's identity. The allowlist is explicit
@@ -605,6 +611,14 @@ function upstreamNativeEntryForSlug(slug: string): RawEntry | undefined {
605
611
  return alias;
606
612
  }
607
613
 
614
+ /** Account-specific grants and prompts are projected from entitlement evidence, never a shared row. */
615
+ function discoveredNativeCapabilityEntry(row: RawEntry): RawEntry {
616
+ const entry = withDerivedBaseInstructions(structuredClone(row));
617
+ delete entry.available_access_programs;
618
+ delete entry.availability_nux;
619
+ return entry;
620
+ }
621
+
608
622
  /**
609
623
  * Backfill `base_instructions` from `model_messages.instructions_template` when upstream ships
610
624
  * only the latter.
@@ -633,7 +647,7 @@ export const UPSTREAM_NATIVE_ENTRIES: Map<string, RawEntry> = new Map(
633
647
  }),
634
648
  );
635
649
 
636
- // Configured natives join the three per-slug tables in place: other modules hold these exact
650
+ // Configured and discovered natives join the per-slug tables in place: modules hold these exact
637
651
  // objects, so a replacement would go unseen. Built-in ids are never configured, so a removal
638
652
  // cannot delete a built-in row.
639
653
  subscribeConfiguredNativeOpenAiModels((current, removed) => {
@@ -647,7 +661,11 @@ subscribeConfiguredNativeOpenAiModels((current, removed) => {
647
661
  if (pinned) PINNED_NATIVE_CAPABILITY_ENTRIES.set(slug, pinned);
648
662
  const upstream = upstreamNativeEntryForSlug(slug);
649
663
  if (upstream) UPSTREAM_NATIVE_ENTRIES.set(slug, upstream);
650
- NATIVE_OPENAI_CONTEXT_OVERRIDES[slug] = { ...NATIVE_GPT6_CONTEXT };
664
+ const discovered = discoveredNativeOpenAiRow(slug);
665
+ const contextWindow = positiveInt(discovered?.context_window) ?? NATIVE_GPT6_CONTEXT.contextWindow;
666
+ const maxContextWindow = Math.max(contextWindow,
667
+ positiveInt(discovered?.max_context_window) ?? NATIVE_GPT6_CONTEXT.maxContextWindow);
668
+ NATIVE_OPENAI_CONTEXT_OVERRIDES[slug] = { contextWindow, maxContextWindow, maxInputTokens: maxContextWindow };
651
669
  }
652
670
  });
653
671
 
@@ -692,6 +710,7 @@ const SELF_AUTHORED_NATIVE_ROWS: ReadonlySet<string> = new Set([NATIVE_GPT6_ASTR
692
710
 
693
711
  export function shouldUpgradeToUpstreamEntry(entry: RawEntry): boolean {
694
712
  if (typeof entry.slug !== "string" || !UPSTREAM_NATIVE_ENTRIES.has(entry.slug)) return false;
713
+ if (discoveredNativeOpenAiRow(entry.slug)) return true;
695
714
  if (entry.display_name === entry.slug) return true;
696
715
  // A row this project authored from a guess is not evidence of upstream truth, however genuine
697
716
  // its display name looks. Replace it once, from the pin.
@@ -703,7 +722,7 @@ export function nativeOpenAiSlugs(): string[] {
703
722
  const live = catalogNativeSlugs();
704
723
  const availableGated = cachedAvailableAccountGatedNativeModels();
705
724
  const candidates = live.length > 0
706
- ? unique([...live, ...DOCUMENTED_NATIVE_OPENAI_ADDITIONS, ...configuredNativeOpenAiModels()])
725
+ ? unique([...live, ...DOCUMENTED_NATIVE_OPENAI_ADDITIONS, ...configuredNativeOpenAiModels(), ...discoveredNativeOpenAiModels()])
707
726
  : NATIVE_OPENAI_MODELS;
708
727
  return candidates.filter(slug => (
709
728
  !ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug) || availableGated.has(slug)
@@ -913,5 +932,5 @@ function catalogNativeSlugs(): string[] {
913
932
  export function listCatalogNativeSlugs(): string[] {
914
933
  // Ensure documented additions (e.g. gpt-6-astra) appear even when the bundled catalog
915
934
  // predates the slug — mirrors nativeOpenAiSlugs() which already merges them for /v1/models.
916
- return unique([...catalogNativeSlugs(), ...DOCUMENTED_NATIVE_OPENAI_ADDITIONS, ...configuredNativeOpenAiModels()]);
935
+ return unique([...catalogNativeSlugs(), ...DOCUMENTED_NATIVE_OPENAI_ADDITIONS, ...configuredNativeOpenAiModels(), ...discoveredNativeOpenAiModels()]);
917
936
  }
@@ -437,6 +437,14 @@ export const CALLABLE_CONFIGURED_COMPATIBILITY_MODELS: Readonly<Record<string, R
437
437
  "codebuddy-cn": new Set([
438
438
  "default",
439
439
  ]),
440
+ // MiniMax serves MiniMax-M3.1-Flash-Preview to Token Plan keys, but its /v1/models roster
441
+ // does not list the preview (probed 2026-09-30), so a live roster would drop it.
442
+ minimax: new Set([
443
+ "MiniMax-M3.1-Flash-Preview",
444
+ ]),
445
+ "minimax-cn": new Set([
446
+ "MiniMax-M3.1-Flash-Preview",
447
+ ]),
440
448
  kimi: new Set([
441
449
  "k3[1m]",
442
450
  "kimi-k2.7-code",
@@ -1,4 +1,5 @@
1
1
  import { effectiveProviderAlias, effectiveProviderAliasDecision } from "../../providers/default-aliases";
2
+ import { XAI_OAUTH_FAST_VARIANT_IDS } from "../../providers/xai-fast-model";
2
3
  import { initialModelSelectionPending } from "../../providers/initial-model-selection";
3
4
  import { execFileSync } from "node:child_process";
4
5
  import { createHash, createHmac, randomBytes } from "node:crypto";
@@ -191,6 +192,9 @@ export function shouldExposeProviderModel(providerName: string, modelId: string)
191
192
  // xAI /models advertises both the dated deployment and this floating alias.
192
193
  // Keep only grok-4.20-multi-agent-0309; the alias is the same server-side id.
193
194
  if (providerName === "xai" && modelId === "grok-4.20-multi-agent-beta-latest") return false;
195
+ // grok-4.7-build-fast is grok-4.7 on a faster serving lane; it is reached through grok-4.7's Fast
196
+ // selection on OAuth (xai-fast-model.ts), so the lane id is not published as a second model.
197
+ if (providerName === "xai" && XAI_OAUTH_FAST_VARIANT_IDS.has(modelId)) return false;
194
198
  return true;
195
199
  }
196
200
 
@@ -129,7 +129,7 @@ const NATIVE_OPENAI_CAPABILITY_SOURCES: Readonly<Record<string, string>> = Objec
129
129
  * keeps rows this runtime does not expose, which is exactly why presence in the pin cannot be
130
130
  * the predicate: `gpt-5.4` is still pinned (hidden, with an upgrade to Terra) after its retirement.
131
131
  */
132
- export const SELF_DESCRIBED_NATIVE_OPENAI_MODELS: ReadonlySet<string> = new Set([
132
+ const selfDescribedNativeModels = new Set<string>([
133
133
  NATIVE_GPT6_ASTRA_MODEL,
134
134
  // Rows come from roster-pinned-models.json via pinnedNativeModelRows(), not the codex-rs pin.
135
135
  NATIVE_GPT6_SOL_MODEL,
@@ -137,6 +137,8 @@ export const SELF_DESCRIBED_NATIVE_OPENAI_MODELS: ReadonlySet<string> = new Set(
137
137
  NATIVE_GPT61_SOL_MODEL,
138
138
  ]);
139
139
 
140
+ export const SELF_DESCRIBED_NATIVE_OPENAI_MODELS: ReadonlySet<string> = selfDescribedNativeModels;
141
+
140
142
  /**
141
143
  * Native ids whose capability metadata is inherited from another pinned native row.
142
144
  *
@@ -155,7 +157,7 @@ export const NATIVE_OPENAI_CAPABILITY_ALIAS_MODELS = Object.freeze(
155
157
  );
156
158
 
157
159
  export function isNativeOpenAiCapabilityAliasModel(slug: string): boolean {
158
- return Object.hasOwn(NATIVE_OPENAI_CAPABILITY_SOURCES, slug) || configuredNativeSlugs.has(slug);
160
+ return Object.hasOwn(NATIVE_OPENAI_CAPABILITY_SOURCES, slug) || (configuredNativeSlugs.has(slug) && !discoveredNativeRows.has(slug));
159
161
  }
160
162
 
161
163
  /**
@@ -175,7 +177,7 @@ export function hasNativeOpenAiCapabilityMetadata(slug: string): boolean {
175
177
 
176
178
  export function nativeOpenAiCapabilitySourceSlug(slug: string): string {
177
179
  return NATIVE_OPENAI_CAPABILITY_SOURCES[slug]
178
- ?? (configuredNativeSlugs.has(slug) ? CONFIGURED_NATIVE_OPENAI_TEMPLATE_MODEL : slug);
180
+ ?? (configuredNativeSlugs.has(slug) && !discoveredNativeRows.has(slug) ? CONFIGURED_NATIVE_OPENAI_TEMPLATE_MODEL : slug);
179
181
  }
180
182
 
181
183
  /**
@@ -196,7 +198,7 @@ export const NATIVE_OPENAI_ALIAS_PRESENTATION: Readonly<Record<string, { display
196
198
 
197
199
  export function nativeOpenAiAliasPresentation(slug: string): { displayName: string; description: string } | undefined {
198
200
  return NATIVE_OPENAI_ALIAS_PRESENTATION[slug]
199
- ?? (configuredNativeSlugs.has(slug) ? configuredNativePresentation(slug) : undefined);
201
+ ?? (configuredNativeSlugs.has(slug) && !discoveredNativeRows.has(slug) ? configuredNativePresentation(slug) : undefined);
200
202
  }
201
203
 
202
204
  /** `gpt-6-nova` -> `GPT-6-Nova`, the same casing upstream uses for its own GPT-6 rows. */
@@ -240,8 +242,8 @@ const BUILT_IN_NATIVE_OPENAI_MODELS: readonly string[] = Object.freeze([
240
242
  ]);
241
243
 
242
244
  /**
243
- * The built-in list plus every configured native, appended in config order. The array and the Set
244
- * below are shared by reference across the catalog, `/v1/models` and the dashboard, so
245
+ * The built-in list plus configured and discovered natives in registration order. The array and
246
+ * Set below are shared by reference across the catalog, `/v1/models` and the dashboard, so
245
247
  * registration edits them in place rather than replacing them.
246
248
  */
247
249
  export const NATIVE_OPENAI_MODELS: string[] = [...BUILT_IN_NATIVE_OPENAI_MODELS];
@@ -260,6 +262,7 @@ export const SUPPORTED_NATIVE_OPENAI_SLUGS = new Set(NATIVE_OPENAI_MODELS);
260
262
  * persist/reconcile path, so any process that loads config — `ocx ensure` included — sees it.
261
263
  */
262
264
  const configuredNativeSlugs = new Set<string>();
265
+ const discoveredNativeRows = new Map<string, Record<string, unknown>>();
263
266
  type ConfiguredNativeListener = (current: readonly string[], removed: readonly string[]) => void;
264
267
  const configuredNativeListeners: ConfiguredNativeListener[] = [];
265
268
 
@@ -286,25 +289,49 @@ export function setConfiguredNativeOpenAiModels(ids: readonly string[]): void {
286
289
  const next = [...new Set(ids.filter(isEligibleConfiguredNativeOpenAiModel))];
287
290
  const previous = [...configuredNativeSlugs];
288
291
  if (next.length === previous.length && next.every((id, index) => id === previous[index])) return;
289
- const removed = previous.filter(id => !next.includes(id));
290
- for (const id of previous) {
291
- configuredNativeSlugs.delete(id);
292
- SUPPORTED_NATIVE_OPENAI_SLUGS.delete(id);
293
- const index = NATIVE_OPENAI_MODELS.indexOf(id);
294
- if (index >= 0) NATIVE_OPENAI_MODELS.splice(index, 1);
295
- }
296
- for (const id of next) {
297
- configuredNativeSlugs.add(id);
298
- SUPPORTED_NATIVE_OPENAI_SLUGS.add(id);
299
- NATIVE_OPENAI_MODELS.push(id);
292
+ configuredNativeSlugs.clear();
293
+ for (const id of next) configuredNativeSlugs.add(id);
294
+ refreshDynamicNativeOpenAiModels();
295
+ }
296
+
297
+ export function discoveredNativeOpenAiModels(): readonly string[] {
298
+ return [...discoveredNativeRows.keys()];
299
+ }
300
+
301
+ export function discoveredNativeOpenAiRow(slug: string): Record<string, unknown> | undefined {
302
+ const row = discoveredNativeRows.get(slug);
303
+ return row ? structuredClone(row) : undefined;
304
+ }
305
+
306
+ /** Validated roster rows stay self-described; a later built-in registration wins automatically. */
307
+ export function setDiscoveredNativeOpenAiModels(rows: readonly Record<string, unknown>[]): void {
308
+ for (const slug of discoveredNativeRows.keys()) selfDescribedNativeModels.delete(slug);
309
+ discoveredNativeRows.clear();
310
+ for (const row of rows) {
311
+ if (typeof row.slug !== "string" || !isEligibleConfiguredNativeOpenAiModel(row.slug)) continue;
312
+ discoveredNativeRows.set(row.slug, structuredClone(row));
313
+ selfDescribedNativeModels.add(row.slug);
300
314
  }
315
+ refreshDynamicNativeOpenAiModels();
316
+ }
317
+
318
+ function dynamicNativeOpenAiModels(): string[] {
319
+ return [...new Set([...configuredNativeSlugs, ...discoveredNativeRows.keys()])];
320
+ }
321
+
322
+ function refreshDynamicNativeOpenAiModels(): void {
323
+ const next = dynamicNativeOpenAiModels();
324
+ const removed = NATIVE_OPENAI_MODELS.filter(id => !BUILT_IN_NATIVE_OPENAI_MODELS.includes(id) && !next.includes(id));
325
+ NATIVE_OPENAI_MODELS.splice(0, NATIVE_OPENAI_MODELS.length, ...BUILT_IN_NATIVE_OPENAI_MODELS, ...next);
326
+ SUPPORTED_NATIVE_OPENAI_SLUGS.clear();
327
+ for (const id of NATIVE_OPENAI_MODELS) SUPPORTED_NATIVE_OPENAI_SLUGS.add(id);
301
328
  for (const listener of configuredNativeListeners) listener(next, removed);
302
329
  }
303
330
 
304
- /** Keep derived tables in step; the listener runs immediately with the current set. */
331
+ /** Keep derived tables in step with the configured/discovered union, including metadata updates. */
305
332
  export function subscribeConfiguredNativeOpenAiModels(listener: ConfiguredNativeListener): void {
306
333
  configuredNativeListeners.push(listener);
307
- listener(configuredNativeOpenAiModels(), []);
334
+ listener(dynamicNativeOpenAiModels(), []);
308
335
  }
309
336
 
310
337
  export function resetConfiguredNativeOpenAiModelsForTests(): void {
@@ -0,0 +1,71 @@
1
+ import type { OcxConfig } from "../types";
2
+
3
+ const SOURCE_WAIT_MS = 15_000;
4
+ const PROCESS_WAIT_MS = 1_000;
5
+
6
+ /**
7
+ * A source failure or slow roster must not prevent publication from existing evidence. The signal
8
+ * aborts when the wait bound passes or the step ends, so a late source cannot publish afterwards.
9
+ */
10
+ async function bestEffortSource(step: (active: () => boolean, signal: AbortSignal) => Promise<unknown>): Promise<void> {
11
+ let timer: ReturnType<typeof setTimeout> | undefined;
12
+ let active = true;
13
+ const controller = new AbortController();
14
+ try {
15
+ await Promise.race([
16
+ Promise.resolve().then(() => step(() => active, controller.signal)),
17
+ new Promise<void>(resolve => {
18
+ timer = setTimeout(resolve, SOURCE_WAIT_MS);
19
+ timer.unref?.();
20
+ }),
21
+ ]);
22
+ } catch {
23
+ // Convergence can still use the last confirmed source snapshots.
24
+ } finally {
25
+ active = false;
26
+ controller.abort();
27
+ clearTimeout(timer);
28
+ }
29
+ }
30
+
31
+ /** Settle source observations before admission captures them; gather itself never probes. */
32
+ export async function refreshCatalogAutoRefreshSources(
33
+ config: OcxConfig,
34
+ current: () => boolean,
35
+ ): Promise<void> {
36
+ // Runtime selection also supplies the roster's trusted client version after an upgrade.
37
+ await bestEffortSource(async active => {
38
+ const { loadBundledCodexCatalog } = await import("./catalog/bundled");
39
+ if (active() && current()) loadBundledCodexCatalog();
40
+ // The synchronous loader bounds its selected-runtime subprocess probes itself.
41
+ });
42
+ if (!current()) return;
43
+ await bestEffortSource(async active => {
44
+ const { ensureCodexEntitlementFreshness } = await import("./model-entitlements");
45
+ if (active() && current()) await ensureCodexEntitlementFreshness(config, { waitMs: SOURCE_WAIT_MS });
46
+ });
47
+ if (!current()) return;
48
+ // The entitlement roster above is asked under the installed client version, which upstream's
49
+ // rollout gate can hide a new model from (GPT-6.1 Sol was invisible to 0.158 on release day).
50
+ // Discovery asks as a newer client so an unpinned native reaches the converge below.
51
+ await bestEffortSource(async (active, signal) => {
52
+ const { discoverCodexNativeRoster } = await import("./model-entitlements");
53
+ // A stopped scheduler generation aborts too, so a stale tick cannot record discoveries.
54
+ if (active() && current()) await discoverCodexNativeRoster(config, { signal, isCurrent: () => active() && current() });
55
+ });
56
+ }
57
+
58
+ /** Observation only: running sessions own static copies and require an operator restart. */
59
+ export async function catalogAutoRefreshReloadRequired(changed = true): Promise<boolean> {
60
+ try {
61
+ const { listCodexAppServerProcesses, collectCodexAppServerCatalogStateWithin } = await import("./app-server-processes");
62
+ // A cold Windows start-time walk can miss a short deadline. The changed-set hint
63
+ // needs only the bounded process listing, so it does not depend on that walk.
64
+ if (changed) return listCodexAppServerProcesses().length > 0;
65
+ const status = await collectCodexAppServerCatalogStateWithin(PROCESS_WAIT_MS);
66
+ return status.state === "stale" || status.state === "unknown";
67
+ } catch {
68
+ // Failure to observe a restart cannot clear a previously recorded requirement.
69
+ return !changed;
70
+ }
71
+ }