@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.
- package/gui/dist/assets/{App-mHGa7dE1.js → App-B00Yq11u.js} +15 -15
- package/gui/dist/assets/App-KLz1TAXi.css +1 -0
- package/gui/dist/assets/{Tray-BxY_RYZg.js → Tray-D3Ij1Cvf.js} +1 -1
- package/gui/dist/assets/{index-ur_BPgK1.js → index-BYQQfBGK.js} +12 -12
- package/gui/dist/assets/index-DBT3Q31Y.css +1 -0
- package/gui/dist/assets/{quota-summary-D1Vq7N9H.js → quota-summary-yPtcEhGI.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/kiro/stream.ts +49 -28
- package/src/adapters/openai-chat.ts +3 -5
- package/src/adapters/openai-responses/passthrough.ts +2 -5
- package/src/codex/auth-api/account-list.ts +14 -2
- package/src/codex/auth-api/main-account-probe.ts +2 -0
- package/src/codex/auth-api/pool-quota-probe.ts +4 -0
- package/src/codex/autostart-health.ts +15 -3
- package/src/codex/catalog/discovered-natives.ts +166 -0
- package/src/codex/catalog/metadata.ts +25 -6
- package/src/codex/catalog/model-hints.ts +8 -0
- package/src/codex/catalog/model-visibility.ts +4 -0
- package/src/codex/catalog/native-models.ts +46 -19
- package/src/codex/catalog-auto-refresh-sources.ts +71 -0
- package/src/codex/catalog-auto-refresh.ts +42 -6
- package/src/codex/catalog-refresh-status.ts +4 -0
- package/src/codex/credits.ts +86 -0
- package/src/codex/main-account-hard-lock.ts +2 -2
- package/src/codex/model-entitlements.ts +122 -1
- package/src/codex/quota-types.ts +1 -0
- package/src/codex/quota.ts +4 -3
- package/src/codex/shim-probe.ts +5 -1
- package/src/codex/shim-templates.ts +10 -4
- package/src/config/derived-registries.ts +4 -2
- package/src/config/diagnostics.ts +9 -0
- package/src/config/feature-flags.ts +6 -4
- package/src/config/schema/config-schema.ts +1 -0
- package/src/config/schema/leaf-validators.ts +1 -0
- package/src/generated/compatibility-version.json +67 -43
- package/src/generated/model-metadata.ts +1 -1
- package/src/lib/test-home-guard.ts +47 -3
- package/src/providers/fastwire.ts +22 -0
- package/src/providers/model-rename-startup.ts +7 -5
- package/src/providers/registry/entries-core.ts +13 -6
- package/src/providers/registry/entries-extended.ts +14 -8
- package/src/providers/registry/model-seeds.ts +14 -1
- package/src/providers/stale-model-roster-migration.ts +101 -0
- package/src/providers/xai-fast-model.ts +85 -0
- package/src/server/auth-cors.ts +1 -0
- package/src/server/background-lifecycle.ts +2 -2
- package/src/server/management/config-routes.ts +13 -1
- package/src/server/management/oauth-account-routes.ts +8 -2
- package/src/server/responses/core-normalize.ts +3 -0
- package/src/server/startup-health-cache.ts +7 -2
- package/src/service/desktop-startup.ts +93 -0
- package/src/types/config.ts +12 -11
- package/src/types/provider.ts +3 -1
- package/src/types/request.ts +2 -0
- package/src/usage/log.ts +3 -1
- package/gui/dist/assets/App-DCBismRi.css +0 -1
- 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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
290
|
-
for (const id of
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
|
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(
|
|
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
|
+
}
|