@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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Default-on periodic catalog refresh so newly released models appear without a
|
|
3
3
|
* manual `ocx sync` (issue #3630).
|
|
4
4
|
*
|
|
5
5
|
* This is load-bearing, not a convenience. The served model set is otherwise
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
const DEFAULT_INTERVAL_MS = 60 * 60_000;
|
|
26
26
|
const MIN_INTERVAL_MS = 15 * 60_000;
|
|
27
|
+
const INITIAL_DELAY_MS = 3 * 60_000;
|
|
27
28
|
/**
|
|
28
29
|
* Commit-lock wait only. Gather already has per-provider timeouts, and automatic
|
|
29
30
|
* callers fail fast and defer (ConvergeRequest.mode) rather than holding the
|
|
@@ -37,6 +38,7 @@ const MAX_JOURNAL_BYTES = 1024 * 1024;
|
|
|
37
38
|
const MAX_CATALOG_BYTES = 64 * 1024 * 1024;
|
|
38
39
|
|
|
39
40
|
let timer: ReturnType<typeof setInterval> | null = null;
|
|
41
|
+
let initialTimer: ReturnType<typeof setTimeout> | null = null;
|
|
40
42
|
let detachShutdownHook: (() => void) | null = null;
|
|
41
43
|
/** The bounded cadence the live timer was created with, so a tick can notice config drift. */
|
|
42
44
|
let liveIntervalMs: number | null = null;
|
|
@@ -170,8 +172,10 @@ function boundedInterval(value: number): number {
|
|
|
170
172
|
/** Re-arm the timer when the operator changed the cadence since it was created. */
|
|
171
173
|
function restartIfCadenceChanged(configured: number): void {
|
|
172
174
|
if (timer === null || boundedInterval(configured) === liveIntervalMs) return;
|
|
173
|
-
|
|
174
|
-
|
|
175
|
+
clearInterval(timer);
|
|
176
|
+
liveIntervalMs = boundedInterval(configured);
|
|
177
|
+
timer = setInterval(() => void tick(), liveIntervalMs);
|
|
178
|
+
timer.unref?.();
|
|
175
179
|
}
|
|
176
180
|
|
|
177
181
|
async function tick(): Promise<void> {
|
|
@@ -199,6 +203,13 @@ async function tick(): Promise<void> {
|
|
|
199
203
|
// and the unref'd timer is left running so flipping the minutes back on is
|
|
200
204
|
// picked up without a process restart.
|
|
201
205
|
if (configured === 0) return;
|
|
206
|
+
// The default-on case belongs to installs whose local Codex client this proxy manages. With
|
|
207
|
+
// the integration off (or on a hub or sibling), an absent section keeps the old opt-in
|
|
208
|
+
// meaning: no background converge may write the native Codex home unasked. An explicit
|
|
209
|
+
// `enabled: true` still refreshes, as it did before the default flipped.
|
|
210
|
+
const { shouldSyncCodexOnStart } = await import("./desired-state");
|
|
211
|
+
const codexManaged = shouldSyncCodexOnStart(config);
|
|
212
|
+
if (!codexManaged && config.catalogAutoRefresh?.enabled !== true) return;
|
|
202
213
|
// A stop or restart landed while the config resolved: this tick no longer owns the timer,
|
|
203
214
|
// so it must neither count as a refresh nor adopt a cadence for a generation that is gone.
|
|
204
215
|
if (entryGeneration !== generation) return;
|
|
@@ -218,6 +229,12 @@ async function tick(): Promise<void> {
|
|
|
218
229
|
} else if (heal === "not-healed") {
|
|
219
230
|
console.info("[catalog-auto-refresh] injected Codex config keys were rewritten externally; not re-injected this tick");
|
|
220
231
|
}
|
|
232
|
+
const { refreshCatalogAutoRefreshSources, catalogAutoRefreshReloadRequired } =
|
|
233
|
+
await import("./catalog-auto-refresh-sources");
|
|
234
|
+
if (entryGeneration !== generation) return;
|
|
235
|
+
// Codex sources read Codex credentials and probe its binary; only a managed client needs them.
|
|
236
|
+
if (codexManaged) await refreshCatalogAutoRefreshSources(config, () => entryGeneration === generation);
|
|
237
|
+
if (entryGeneration !== generation) return;
|
|
221
238
|
const [{ createManagementConvergeCodex }, { createCatalogConvergeRequest }] = await Promise.all([
|
|
222
239
|
import("./management-convergence"),
|
|
223
240
|
import("./catalog-admission"),
|
|
@@ -231,11 +248,16 @@ async function tick(): Promise<void> {
|
|
|
231
248
|
// createManagementConvergeCodex always projects catalog-only. Any other kind is a
|
|
232
249
|
// funnel contract break, not something this scheduler should re-classify.
|
|
233
250
|
if (outcome.kind !== "catalog-only") return;
|
|
234
|
-
const { recordCatalogAutoRefreshOutcome } = await import("./catalog-refresh-status");
|
|
235
|
-
|
|
251
|
+
const { recordCatalogAutoRefreshOutcome, lastCatalogAutoRefreshOutcome } = await import("./catalog-refresh-status");
|
|
252
|
+
const reloadRequired = (outcome.changed || lastCatalogAutoRefreshOutcome()?.reloadRequired === true)
|
|
253
|
+
? await catalogAutoRefreshReloadRequired(outcome.changed) : false;
|
|
254
|
+
if (entryGeneration !== generation) return;
|
|
255
|
+
recordCatalogAutoRefreshOutcome(outcome.catalogRefresh, outcome.changed, reloadRequired);
|
|
236
256
|
if (outcome.changed) {
|
|
237
257
|
// Privacy scan: no provider names, model ids, paths, or account identifiers.
|
|
238
|
-
console.info(
|
|
258
|
+
console.info(reloadRequired
|
|
259
|
+
? "[catalog-auto-refresh] served model set changed; running Codex sessions keep the old list until restarted (ocx sync --restart-codex)"
|
|
260
|
+
: "[catalog-auto-refresh] served model set changed");
|
|
239
261
|
}
|
|
240
262
|
} catch {
|
|
241
263
|
// A failed refresh is not an error worth surfacing: the next tick tries again.
|
|
@@ -253,8 +275,16 @@ export function startCatalogAutoRefresh(intervalMs = DEFAULT_INTERVAL_MS): void
|
|
|
253
275
|
timer = setInterval(() => void tick(), bounded);
|
|
254
276
|
// Never keep the process alive for a catalog refresh.
|
|
255
277
|
timer.unref?.();
|
|
278
|
+
const startedGeneration = generation;
|
|
279
|
+
initialTimer = setTimeout(() => {
|
|
280
|
+
if (startedGeneration !== generation) return;
|
|
281
|
+
initialTimer = null;
|
|
282
|
+
return tick();
|
|
283
|
+
}, INITIAL_DELAY_MS);
|
|
284
|
+
initialTimer.unref?.();
|
|
256
285
|
void import("../lib/optional-shutdown-hooks")
|
|
257
286
|
.then(hooks => {
|
|
287
|
+
if (startedGeneration !== generation) return;
|
|
258
288
|
detachShutdownHook = hooks.registerOptionalShutdownHook(
|
|
259
289
|
"catalog-auto-refresh",
|
|
260
290
|
stopCatalogAutoRefresh,
|
|
@@ -266,6 +296,10 @@ export function startCatalogAutoRefresh(intervalMs = DEFAULT_INTERVAL_MS): void
|
|
|
266
296
|
}
|
|
267
297
|
|
|
268
298
|
export function stopCatalogAutoRefresh(): void {
|
|
299
|
+
if (initialTimer) {
|
|
300
|
+
clearTimeout(initialTimer);
|
|
301
|
+
initialTimer = null;
|
|
302
|
+
}
|
|
269
303
|
if (timer) {
|
|
270
304
|
clearInterval(timer);
|
|
271
305
|
timer = null;
|
|
@@ -290,7 +324,9 @@ export function isCatalogAutoRefreshRunning(): boolean {
|
|
|
290
324
|
* microtasks before this settles.
|
|
291
325
|
*/
|
|
292
326
|
export async function syncCatalogAutoRefreshCadence(): Promise<void> {
|
|
327
|
+
const entryGeneration = generation;
|
|
293
328
|
const { loadConfig, resolveCatalogAutoRefreshIntervalMs } = await import("../config");
|
|
329
|
+
if (entryGeneration !== generation) return;
|
|
294
330
|
const configured = resolveCatalogAutoRefreshIntervalMs(loadConfig());
|
|
295
331
|
// 0 is dormant: tick() already returns before converging, and the timer stays unref'd.
|
|
296
332
|
if (configured === 0) return;
|
|
@@ -108,6 +108,7 @@ export interface CatalogAutoRefreshOutcome {
|
|
|
108
108
|
readonly at: number;
|
|
109
109
|
readonly disposition: CatalogDisposition;
|
|
110
110
|
readonly changed: boolean;
|
|
111
|
+
readonly reloadRequired: boolean;
|
|
111
112
|
/**
|
|
112
113
|
* A refresh that has failed repeatedly is the signal an operator needs, and the
|
|
113
114
|
* boolean disposition alone cannot express it: skipped and failed look the same
|
|
@@ -158,6 +159,7 @@ function freezeCatalogAutoRefreshOutcome(
|
|
|
158
159
|
at: outcome.at,
|
|
159
160
|
disposition: freezeCatalogDisposition(outcome.disposition),
|
|
160
161
|
changed: outcome.changed,
|
|
162
|
+
reloadRequired: outcome.reloadRequired,
|
|
161
163
|
consecutiveFailures: outcome.consecutiveFailures,
|
|
162
164
|
});
|
|
163
165
|
}
|
|
@@ -171,6 +173,7 @@ function freezeCatalogAutoRefreshOutcome(
|
|
|
171
173
|
export function recordCatalogAutoRefreshOutcome(
|
|
172
174
|
disposition: CatalogDisposition,
|
|
173
175
|
changed: boolean,
|
|
176
|
+
reloadRequired = false,
|
|
174
177
|
): CatalogAutoRefreshOutcome | null {
|
|
175
178
|
const normalized = normalizeCatalogDisposition(disposition);
|
|
176
179
|
if (normalized === null) return null;
|
|
@@ -181,6 +184,7 @@ export function recordCatalogAutoRefreshOutcome(
|
|
|
181
184
|
at: Date.now(),
|
|
182
185
|
disposition: normalized,
|
|
183
186
|
changed: changed === true,
|
|
187
|
+
reloadRequired: reloadRequired === true,
|
|
184
188
|
consecutiveFailures,
|
|
185
189
|
});
|
|
186
190
|
lastAutoRefreshOutcome = outcome;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { OcxConfig } from "../types";
|
|
2
|
+
|
|
3
|
+
/** Display-only WHAM observation, never persisted or used for routing. */
|
|
4
|
+
export interface CodexCredits {
|
|
5
|
+
hasCredits?: boolean;
|
|
6
|
+
unlimited?: boolean;
|
|
7
|
+
overageLimitReached?: boolean;
|
|
8
|
+
balance?: string;
|
|
9
|
+
approxLocalMessages?: [number, number];
|
|
10
|
+
approxCloudMessages?: [number, number];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* An entry without `credits` means "this identity was observed and reported nothing to show".
|
|
15
|
+
* Keeping that apart from "never observed" lets the account listing bypass the persisted quota
|
|
16
|
+
* cache exactly once per identity after a restart, instead of on every dashboard poll.
|
|
17
|
+
*/
|
|
18
|
+
const observations = new Map<string, { identity: string; credits?: CodexCredits }>();
|
|
19
|
+
|
|
20
|
+
function messageRange(raw: unknown): [number, number] | undefined {
|
|
21
|
+
return Array.isArray(raw) && raw.length === 2
|
|
22
|
+
&& raw.every(value => typeof value === "number" && Number.isFinite(value) && value >= 0)
|
|
23
|
+
? [raw[0], raw[1]] : undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Undefined keeps the prior observation; null or unusable input clears it. */
|
|
27
|
+
export function parseCodexCredits(raw: unknown): CodexCredits | null | undefined {
|
|
28
|
+
if (raw === undefined) return undefined;
|
|
29
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
30
|
+
const value = raw as Record<string, unknown>;
|
|
31
|
+
const credits: CodexCredits = {};
|
|
32
|
+
if (typeof value.has_credits === "boolean") credits.hasCredits = value.has_credits;
|
|
33
|
+
if (typeof value.unlimited === "boolean") credits.unlimited = value.unlimited;
|
|
34
|
+
if (typeof value.overage_limit_reached === "boolean") credits.overageLimitReached = value.overage_limit_reached;
|
|
35
|
+
if (typeof value.balance === "string" && /^\d+(\.\d+)?$/.test(value.balance)) credits.balance = value.balance;
|
|
36
|
+
else if (typeof value.balance === "number" && Number.isFinite(value.balance) && value.balance >= 0) {
|
|
37
|
+
// String(1e-7) is "1e-7"; the DTO promises a plain decimal string, which the GUI validates.
|
|
38
|
+
credits.balance = value.balance.toLocaleString("en-US", { useGrouping: false, maximumFractionDigits: 20 });
|
|
39
|
+
}
|
|
40
|
+
const local = messageRange(value.approx_local_messages);
|
|
41
|
+
const cloud = messageRange(value.approx_cloud_messages);
|
|
42
|
+
if (local) credits.approxLocalMessages = local;
|
|
43
|
+
if (cloud) credits.approxCloudMessages = cloud;
|
|
44
|
+
return Object.keys(credits).length > 0 ? credits : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function rememberCodexCredits(accountId: string, identity: string, parsed: CodexCredits | null | undefined): void {
|
|
48
|
+
const previous = observations.get(accountId);
|
|
49
|
+
if (parsed === undefined) {
|
|
50
|
+
// Omission keeps a same-identity observation; otherwise it still records that this identity
|
|
51
|
+
// answered, so the listing stops forcing fresh reads for it.
|
|
52
|
+
if (previous?.identity !== identity) observations.set(accountId, { identity });
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
observations.set(accountId, parsed === null ? { identity } : { identity, credits: structuredClone(parsed) });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function codexCreditsFor(accountId: string, identity: string | null): CodexCredits | undefined {
|
|
59
|
+
const observation = observations.get(accountId);
|
|
60
|
+
if (!observation) return undefined;
|
|
61
|
+
if (identity !== observation.identity) {
|
|
62
|
+
observations.delete(accountId);
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
return observation.credits ? structuredClone(observation.credits) : undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Whether this identity has answered at least one usage read since the process started. */
|
|
69
|
+
export function hasCodexCreditsObservation(accountId: string, identity: string | null): boolean {
|
|
70
|
+
const observation = observations.get(accountId);
|
|
71
|
+
return observation !== undefined && identity !== null && observation.identity === identity;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function codexCreditsDtoField(config: Pick<OcxConfig, "showCodexCredits">, accountId: string, identity: string | null): { credits?: CodexCredits } {
|
|
75
|
+
const credits = codexCreditsFor(accountId, identity);
|
|
76
|
+
return config.showCodexCredits === true && credits ? { credits } : {};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function pruneCodexCredits(liveAccountIds: Iterable<string>): void {
|
|
80
|
+
const live = new Set(liveAccountIds);
|
|
81
|
+
for (const id of observations.keys()) if (!live.has(id)) observations.delete(id);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function resetCodexCreditsForTests(): void {
|
|
85
|
+
observations.clear();
|
|
86
|
+
}
|
|
@@ -66,9 +66,9 @@ export function getMainAccountHardLockStatus(
|
|
|
66
66
|
const windows = governingWindows(quota);
|
|
67
67
|
const blocking = windows.filter(w => validPercent(w.percent) && w.percent >= MAIN_ACCOUNT_HARD_LOCK_PERCENT);
|
|
68
68
|
if (blocking.length > 0) {
|
|
69
|
-
// The lock holds until every blocking window reads lower, so the earliest possible unlock is
|
|
69
|
+
// The lock holds until every blocking window reads lower or is authoritatively absent, so the earliest possible unlock is
|
|
70
70
|
// the latest blocking reset. One blocking window without a future reset makes it unknowable.
|
|
71
|
-
// A predicted reset is not evidence of recovery
|
|
71
|
+
// A predicted reset is not evidence of recovery; fresh lower usage or validated WHAM absence releases.
|
|
72
72
|
const resets = blocking.map(w => resetTimestamp(w.resetAt));
|
|
73
73
|
const resetAt = resets.every(r => r !== undefined && r > now) ? Math.max(...(resets as number[])) : undefined;
|
|
74
74
|
return { enabled: true, state: "blocked", ...(resetAt !== undefined ? { resetAt } : {}) };
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
} from "./catalog/native-models";
|
|
21
21
|
import { loadPersistedCodexRuntime } from "./runtime";
|
|
22
22
|
import { codexRuntimeStateEpoch } from "./runtime";
|
|
23
|
+
import { recordDiscoveredNativeModels, validateDiscoveredNativeRows } from "./catalog/discovered-natives";
|
|
23
24
|
import { pinnedNativeModelRows } from "./catalog/pinned-models";
|
|
24
25
|
import { codexCredentialMutationEpoch } from "./credential-mutation-epoch";
|
|
25
26
|
import {
|
|
@@ -650,6 +651,7 @@ function boundedAvailabilityMessage(message: string): string {
|
|
|
650
651
|
|
|
651
652
|
/** Keep valid roster slugs while dropping malformed program and availability metadata. */
|
|
652
653
|
function parseAccountModels(text: string): {
|
|
654
|
+
discoveredRows: Record<string, unknown>[];
|
|
653
655
|
models: ReadonlySet<string>;
|
|
654
656
|
accessProgramsByModel: ReadonlyMap<string, CodexAvailableAccessPrograms>;
|
|
655
657
|
availabilityNuxByModel: ReadonlyMap<string, { message: string }>;
|
|
@@ -681,7 +683,8 @@ function parseAccountModels(text: string): {
|
|
|
681
683
|
}
|
|
682
684
|
return [row.slug];
|
|
683
685
|
});
|
|
684
|
-
return { models: new Set(models), accessProgramsByModel, availabilityNuxByModel
|
|
686
|
+
return { models: new Set(models), accessProgramsByModel, availabilityNuxByModel,
|
|
687
|
+
discoveredRows: validateDiscoveredNativeRows(payload.models) };
|
|
685
688
|
} catch {
|
|
686
689
|
return null;
|
|
687
690
|
}
|
|
@@ -757,6 +760,7 @@ async function fetchAccountModels(
|
|
|
757
760
|
if (!usable) {
|
|
758
761
|
return unconfirmedAccountModels(credential, clientVersion, now, { kind: "parsed-empty" });
|
|
759
762
|
}
|
|
763
|
+
recordDiscoveredNativeModels(parsed.discoveredRows, clientVersion, now);
|
|
760
764
|
const hasUnknownGatedAbsence = [...ACCOUNT_GATED_NATIVE_MODEL_MINIMUM_CLIENT_VERSIONS]
|
|
761
765
|
// Reachable only through tier 1, an inbound client_version below the floor. Every other
|
|
762
766
|
// resolution is now structurally >= every recorded minimum, because the floor is the max
|
|
@@ -789,6 +793,123 @@ async function fetchAccountModels(
|
|
|
789
793
|
}
|
|
790
794
|
}
|
|
791
795
|
|
|
796
|
+
/**
|
|
797
|
+
* Client version the discovery-only roster request claims.
|
|
798
|
+
*
|
|
799
|
+
* Upstream filters `/models` by client version AND by a rollout gate the row's own
|
|
800
|
+
* `minimal_client_version` does not describe: on 2026-09-30 `gpt-6.1-sol` carried
|
|
801
|
+
* `minimal_client_version: "0.153.0"` yet was served only to `client_version >= 0.159.0`, while the
|
|
802
|
+
* installed Codex was 0.158.0-alpha. Asking under the installed version (what the entitlement path
|
|
803
|
+
* must do, #2886) therefore could not see the model at all. Discovery asks as a client newer than
|
|
804
|
+
* any release so it sees the whole roster; this proxy already serves pinned rows to older
|
|
805
|
+
* clients, so the version only widens what we learn, never what an account may call.
|
|
806
|
+
*/
|
|
807
|
+
export const CODEX_ROSTER_DISCOVERY_CLIENT_VERSION = "99.0.0";
|
|
808
|
+
|
|
809
|
+
export type CodexNativeRosterDiscoveryOutcome = "recorded" | "not-modified" | "unavailable";
|
|
810
|
+
|
|
811
|
+
export interface CodexNativeRosterDiscoveryOptions extends Pick<
|
|
812
|
+
CodexModelEntitlementResolveOptions,
|
|
813
|
+
"credentials" | "fetcher" | "signal" | "now" | "nativeMainRefreshDependencies"
|
|
814
|
+
> {
|
|
815
|
+
/** Checked right before publication; false means the caller's scheduler generation is gone. */
|
|
816
|
+
readonly isCurrent?: () => boolean;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* Last ETag per credential identity, so an unchanged roster costs a 304 when upstream honours it.
|
|
821
|
+
* A 304 carries no rows and so cannot renew a discovery's last-seen time; a model visible only
|
|
822
|
+
* under the discovery version would then expire after its retention while still being served.
|
|
823
|
+
* The ETag is therefore used only within a day of the full fetch that earned it.
|
|
824
|
+
*/
|
|
825
|
+
const discoveryEtags = new Map<string, { etag: string; fetchedAt: number }>();
|
|
826
|
+
const DISCOVERY_ETAG_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Background discovery of native rows this build does not pin, independent of entitlement.
|
|
830
|
+
*
|
|
831
|
+
* It never writes the entitlement cache: a roster fetched under the discovery version answers a
|
|
832
|
+
* different question than "may this client call this model", and letting it satisfy entitlement
|
|
833
|
+
* reads would reintroduce the cross-version leak #2548/#2886 closed. The first account that
|
|
834
|
+
* answers is enough to learn a row; whether a given account may call it stays with the
|
|
835
|
+
* entitlement path and, for ungated rows, with the upstream status the request receives.
|
|
836
|
+
*/
|
|
837
|
+
export async function discoverCodexNativeRoster(
|
|
838
|
+
config: Pick<OcxConfig, "codexAccounts">,
|
|
839
|
+
options: CodexNativeRosterDiscoveryOptions = {},
|
|
840
|
+
): Promise<CodexNativeRosterDiscoveryOutcome> {
|
|
841
|
+
const fetcher = options.fetcher ?? fetch;
|
|
842
|
+
const run = async (excluded: ReadonlySet<string>, releaseMainLease?: () => void) => {
|
|
843
|
+
const credentials: CodexModelEntitlementCredentialSnapshot[] = [...(options.credentials ?? [])];
|
|
844
|
+
for (const accountId of options.credentials ? [] : normalizedCandidateAccountIds(config)) {
|
|
845
|
+
if (excluded.has(accountId)) continue;
|
|
846
|
+
const credential = await accountCredentialSnapshot(accountId, {
|
|
847
|
+
nativeMainRefreshDependencies: options.nativeMainRefreshDependencies,
|
|
848
|
+
signal: options.signal,
|
|
849
|
+
}).catch(() => null);
|
|
850
|
+
if (credential) credentials.push(credential);
|
|
851
|
+
}
|
|
852
|
+
releaseMainLease?.();
|
|
853
|
+
for (const credential of credentials) {
|
|
854
|
+
const outcome = await fetchDiscoveryRoster(credential, fetcher, options).catch(() => "unavailable" as const);
|
|
855
|
+
if (outcome !== "unavailable") return outcome;
|
|
856
|
+
}
|
|
857
|
+
return "unavailable" as const;
|
|
858
|
+
};
|
|
859
|
+
try {
|
|
860
|
+
return await withNativeMainCredentialAdmission(run);
|
|
861
|
+
} catch {
|
|
862
|
+
return "unavailable";
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
async function fetchDiscoveryRoster(
|
|
867
|
+
credential: CodexModelEntitlementCredentialSnapshot,
|
|
868
|
+
fetcher: typeof fetch,
|
|
869
|
+
options: Pick<CodexNativeRosterDiscoveryOptions, "signal" | "now" | "isCurrent">,
|
|
870
|
+
): Promise<CodexNativeRosterDiscoveryOutcome> {
|
|
871
|
+
const controller = new AbortController();
|
|
872
|
+
const abort = () => controller.abort(options.signal?.reason);
|
|
873
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
874
|
+
const timer = setTimeout(() => controller.abort(new DOMException("Codex model discovery timed out", "TimeoutError")), MODEL_ROSTER_TIMEOUT_MS);
|
|
875
|
+
try {
|
|
876
|
+
const headers = new Headers({ Authorization: `Bearer ${credential.accessToken}`, Accept: "application/json" });
|
|
877
|
+
if (credential.chatgptAccountId) headers.set("ChatGPT-Account-Id", credential.chatgptAccountId);
|
|
878
|
+
const now = options.now ?? Date.now();
|
|
879
|
+
const cached = discoveryEtags.get(credential.credentialIdentity);
|
|
880
|
+
if (cached && now - cached.fetchedAt < DISCOVERY_ETAG_MAX_AGE_MS) headers.set("If-None-Match", cached.etag);
|
|
881
|
+
const response = await fetcher(codexModelsUrl(CODEX_ROSTER_DISCOVERY_CLIENT_VERSION), {
|
|
882
|
+
headers,
|
|
883
|
+
redirect: "error",
|
|
884
|
+
signal: controller.signal,
|
|
885
|
+
});
|
|
886
|
+
if (response.status === 304) return "not-modified";
|
|
887
|
+
if (!response.ok) return "unavailable";
|
|
888
|
+
const body = await readBoundedResponseBody(response, {
|
|
889
|
+
signal: controller.signal,
|
|
890
|
+
maxBytes: MODEL_ROSTER_MAX_BYTES,
|
|
891
|
+
fatalUtf8: true,
|
|
892
|
+
});
|
|
893
|
+
if (!body.displaySafe || body.truncated) return "unavailable";
|
|
894
|
+
const parsed = parseAccountModels(body.text);
|
|
895
|
+
if (parsed === null || parsed.models.size === 0) return "unavailable";
|
|
896
|
+
if (controller.signal.aborted || options.isCurrent?.() === false) return "unavailable";
|
|
897
|
+
recordDiscoveredNativeModels(parsed.discoveredRows, CODEX_ROSTER_DISCOVERY_CLIENT_VERSION, now);
|
|
898
|
+
const nextEtag = response.headers.get("etag");
|
|
899
|
+
if (nextEtag && nextEtag.length <= 256) discoveryEtags.set(credential.credentialIdentity, { etag: nextEtag, fetchedAt: now });
|
|
900
|
+
else discoveryEtags.delete(credential.credentialIdentity);
|
|
901
|
+
if (discoveryEtags.size > 64) discoveryEtags.delete(discoveryEtags.keys().next().value!);
|
|
902
|
+
return "recorded";
|
|
903
|
+
} finally {
|
|
904
|
+
clearTimeout(timer);
|
|
905
|
+
options.signal?.removeEventListener("abort", abort);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
export function resetCodexNativeRosterDiscoveryForTests(): void {
|
|
910
|
+
discoveryEtags.clear();
|
|
911
|
+
}
|
|
912
|
+
|
|
792
913
|
function directCallerCredential(headers: Headers): CodexModelEntitlementCredentialSnapshot | null {
|
|
793
914
|
const match = /^Bearer\s+(\S+)$/i.exec(headers.get("authorization")?.trim() ?? "");
|
|
794
915
|
if (!match) return null;
|
package/src/codex/quota-types.ts
CHANGED
|
@@ -118,6 +118,7 @@ export type WhamUsageResponse = {
|
|
|
118
118
|
secondary_window?: WhamUsageWindow | null;
|
|
119
119
|
tertiary_window?: WhamUsageWindow | null;
|
|
120
120
|
};
|
|
121
|
+
credits?: unknown;
|
|
121
122
|
rate_limit_reset_credits?: { available_count: number } | null;
|
|
122
123
|
additional_rate_limits?: WhamAdditionalRateLimit[] | null;
|
|
123
124
|
};
|
package/src/codex/quota.ts
CHANGED
|
@@ -827,8 +827,8 @@ function filterMainPolicyMonthlyQuota(
|
|
|
827
827
|
|
|
828
828
|
/**
|
|
829
829
|
* Parse ordinary main-policy usage, rejecting messages with invalid numeric window percentages.
|
|
830
|
-
*
|
|
831
|
-
*
|
|
830
|
+
* A measured long primary, or explicitly absent primary with measured weekly secondary,
|
|
831
|
+
* proves replacement only with complete long/null topology. Null supplies no usable evidence.
|
|
832
832
|
*/
|
|
833
833
|
export function parseMainPolicyUsageQuota(data: WhamUsageResponse): MainPolicyQuotaObservation | null {
|
|
834
834
|
const windows = [data.rate_limit?.primary_window, data.rate_limit?.secondary_window, data.rate_limit?.tertiary_window];
|
|
@@ -840,7 +840,8 @@ export function parseMainPolicyUsageQuota(data: WhamUsageResponse): MainPolicyQu
|
|
|
840
840
|
// carries a valid usage reading: a long window without used_percent leaves that
|
|
841
841
|
// window's usage unknown, and unknown usage must never release a block.
|
|
842
842
|
// Headers never supply this proof, and reset time alone still cannot release a block.
|
|
843
|
-
if (quota &&
|
|
843
|
+
if (quota && (isMeasuredLongWindow(primary)
|
|
844
|
+
|| (primary === null && isMeasuredLongWindow(secondary) && quota.weeklyPercent !== undefined))
|
|
844
845
|
&& (secondary === null || isMeasuredLongWindow(secondary))
|
|
845
846
|
&& (tertiary === null || isMeasuredLongWindow(tertiary))) {
|
|
846
847
|
return { ...quota, shortWindowAbsent: true };
|
package/src/codex/shim-probe.ts
CHANGED
|
@@ -146,9 +146,12 @@ function finishAfterStderr(status) {
|
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
try {
|
|
149
|
+
// BUN_BE_BUN selects the supervisor's interpreter mode, not the saved launcher.
|
|
150
|
+
const launcherEnv = { ...process.env };
|
|
151
|
+
delete launcherEnv.BUN_BE_BUN;
|
|
149
152
|
launcher = spawn(launcherShellPath, [wrapperPath, "--version"], {
|
|
150
153
|
detached: true,
|
|
151
|
-
env:
|
|
154
|
+
env: launcherEnv,
|
|
152
155
|
stdio: ["ignore", "ignore", "pipe", "pipe"],
|
|
153
156
|
});
|
|
154
157
|
if (!launcher.pid) throw new Error("Codex shim probe launcher has no pid");
|
|
@@ -252,6 +255,7 @@ function probeUnixShimInstall(wrapperPath: string): UnixShimProbeResult {
|
|
|
252
255
|
const stderrPath = join(probeDir, "stderr");
|
|
253
256
|
const env: NodeJS.ProcessEnv = {
|
|
254
257
|
...process.env,
|
|
258
|
+
BUN_BE_BUN: "1",
|
|
255
259
|
OCX_SHIM_BYPASS: "1",
|
|
256
260
|
OCX_SHIM_PROBE: "1",
|
|
257
261
|
OCX_SHIM_PROBE_REENTRY_PATH: reentryPath,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { BUN_RUNTIME_PATH_ENV, BUN_RUNTIME_SOURCE_ENV } from "../lib/bun-runtime";
|
|
2
2
|
import type { BunRuntimeSource } from "../lib/bun-runtime";
|
|
3
3
|
import { serviceApiTokenFilePath } from "../lib/service-secrets";
|
|
4
|
+
import { selfLaunchArgv } from "../lib/self-launch-argv";
|
|
4
5
|
import { windowsEnvIndirectBatchValue } from "../lib/win-paths";
|
|
5
6
|
|
|
6
7
|
const SHIM_MARKER = "opencodex codex autostart shim";
|
|
@@ -83,6 +84,8 @@ function shQuote(value: string): string {
|
|
|
83
84
|
export function buildUnixCodexShim(realCodexPath: string, bunPath: string, cliPath: string, bunRuntimeSource: BunRuntimeSource, tokenFile = serviceApiTokenFilePath()): string {
|
|
84
85
|
const internalCommands = CODEX_INTERNAL_COMMANDS.join("|");
|
|
85
86
|
const valueOptions = CODEX_GLOBAL_OPTIONS_WITH_VALUE.join("|");
|
|
87
|
+
const cliArgs = selfLaunchArgv([], { sourceEntrypoint: cliPath, isStandaloneExecutable: bunRuntimeSource === "standalone" })
|
|
88
|
+
.map(arg => `${shQuote(arg)} `).join("");
|
|
86
89
|
return `#!/usr/bin/env sh
|
|
87
90
|
# ${SHIM_MARKER}
|
|
88
91
|
# ${UNIX_SHIM_REVISION_MARKER}
|
|
@@ -154,7 +157,7 @@ case "$ocx_subcommand" in
|
|
|
154
157
|
;;
|
|
155
158
|
*)
|
|
156
159
|
if [ -z "$OCX_SHIM_BYPASS" ]; then
|
|
157
|
-
if ! ${BUN_RUNTIME_SOURCE_ENV}=${shQuote(bunRuntimeSource)} ${BUN_RUNTIME_PATH_ENV}=${shQuote(bunPath)} ${shQuote(bunPath)} ${
|
|
160
|
+
if ! ${BUN_RUNTIME_SOURCE_ENV}=${shQuote(bunRuntimeSource)} ${BUN_RUNTIME_PATH_ENV}=${shQuote(bunPath)} ${shQuote(bunPath)} ${cliArgs}ensure >/dev/null 2>&1; then
|
|
158
161
|
printf '%s\\n' ${shQuote(CODEX_SHIM_ENSURE_FAILED_DIAGNOSTIC)} >&2
|
|
159
162
|
fi
|
|
160
163
|
fi
|
|
@@ -184,12 +187,13 @@ function windowsBatchSet(name: string, value: string): string {
|
|
|
184
187
|
export function buildWindowsCodexShim(realCodexPath: string, bunPath: string, cliPath: string, bunRuntimeSource: BunRuntimeSource): string {
|
|
185
188
|
const internalCommandChecks = CODEX_INTERNAL_COMMANDS.map(command => `if /I "%~1"=="${command}" goto run_codex`).join("\r\n");
|
|
186
189
|
const valueOptionChecks = CODEX_GLOBAL_OPTIONS_WITH_VALUE.map(option => `if /I "%~1"=="${option}" goto skip_option_value`).join("\r\n");
|
|
190
|
+
const cliArgs = selfLaunchArgv([], { sourceEntrypoint: cliPath, isStandaloneExecutable: bunRuntimeSource === "standalone" });
|
|
187
191
|
return `@echo off\r
|
|
188
192
|
rem ${SHIM_MARKER}\r
|
|
189
193
|
setlocal\r
|
|
190
194
|
${windowsBatchSet("OCX_REAL_CODEX", realCodexPath)}\r
|
|
191
195
|
${windowsBatchSet("OCX_BUN", bunPath)}\r
|
|
192
|
-
${windowsBatchSet("OCX_CLI",
|
|
196
|
+
${cliArgs.length ? windowsBatchSet("OCX_CLI", cliArgs[0]!) : ""}\r
|
|
193
197
|
${windowsBatchSet("OCX_API_TOKEN_FILE", serviceApiTokenFilePath())}\r
|
|
194
198
|
if "%OPENCODEX_API_AUTH_TOKEN%"=="" if exist "%OCX_API_TOKEN_FILE%" set /p OPENCODEX_API_AUTH_TOKEN=<"%OCX_API_TOKEN_FILE%"\r
|
|
195
199
|
if not "%OCX_SHIM_BYPASS%"=="" goto run_codex\r
|
|
@@ -216,7 +220,7 @@ goto scan_codex_args\r
|
|
|
216
220
|
setlocal\r
|
|
217
221
|
${windowsBatchSet(BUN_RUNTIME_SOURCE_ENV, bunRuntimeSource)}\r
|
|
218
222
|
${windowsBatchSet(BUN_RUNTIME_PATH_ENV, bunPath)}\r
|
|
219
|
-
"%OCX_BUN%" "%OCX_CLI%" ensure >nul 2>nul\r
|
|
223
|
+
"%OCX_BUN%" ${cliArgs.length ? '"%OCX_CLI%" ' : ""}ensure >nul 2>nul\r
|
|
220
224
|
if errorlevel 1 echo ${CODEX_SHIM_ENSURE_FAILED_DIAGNOSTIC} 1>&2\r
|
|
221
225
|
endlocal\r
|
|
222
226
|
:run_codex\r
|
|
@@ -232,6 +236,8 @@ export function buildWindowsPowerShellCodexShim(realCodexPath: string, bunPath:
|
|
|
232
236
|
const internalCommands = CODEX_INTERNAL_COMMANDS.map(command => psString(command)).join(", ");
|
|
233
237
|
const valueOptions = CODEX_GLOBAL_OPTIONS_WITH_VALUE.map(option => psString(option)).join(", ");
|
|
234
238
|
const tokenFile = serviceApiTokenFilePath();
|
|
239
|
+
const cliArgs = selfLaunchArgv([], { sourceEntrypoint: cliPath, isStandaloneExecutable: bunRuntimeSource === "standalone" })
|
|
240
|
+
.map(arg => `${psString(arg)} `).join("");
|
|
235
241
|
return `#!/usr/bin/env pwsh
|
|
236
242
|
# ${SHIM_MARKER}
|
|
237
243
|
$hadApiAuthToken = Test-Path Env:\\OPENCODEX_API_AUTH_TOKEN
|
|
@@ -263,7 +269,7 @@ if (-not $skipEnsure) {
|
|
|
263
269
|
$ocxEnsureFailed = $false
|
|
264
270
|
# Caught, not propagated: a throwing ensure used to escape this wrapper and Codex never
|
|
265
271
|
# launched at all, which is a lockout produced by the autostart helper itself (#5261).
|
|
266
|
-
try { & ${psString(bunPath)} ${
|
|
272
|
+
try { & ${psString(bunPath)} ${cliArgs}ensure *> $null; if ($LASTEXITCODE -ne 0) { $ocxEnsureFailed = $true } }
|
|
267
273
|
catch { $ocxEnsureFailed = $true }
|
|
268
274
|
finally {
|
|
269
275
|
if ($null -eq $priorRuntimeSource) { Remove-Item Env:\\${BUN_RUNTIME_SOURCE_ENV} -ErrorAction SilentlyContinue }
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { loadDiscoveredNativeModels } from "../codex/catalog/discovered-natives";
|
|
1
2
|
import { setConfiguredNativeOpenAiModels } from "../codex/catalog/native-models";
|
|
2
3
|
import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers-destination";
|
|
3
4
|
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
@@ -20,10 +21,11 @@ export function configuredNativeOpenAiModelIds(config: Pick<OcxConfig, "provider
|
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* Rebuild every process-local registry derived from a committed config: user price overlays and
|
|
23
|
-
* configured native GPT models. Called wherever
|
|
24
|
-
* current, so
|
|
24
|
+
* configured/discovered native GPT models. Called wherever loaded, persisted or reconciled
|
|
25
|
+
* config becomes current, so catalog registration follows the active config and OpenCodex home.
|
|
25
26
|
*/
|
|
26
27
|
export function refreshConfigDerivedRegistries(config: OcxConfig): void {
|
|
27
28
|
refreshUserCostOverlays(config);
|
|
28
29
|
setConfiguredNativeOpenAiModels(configuredNativeOpenAiModelIds(config));
|
|
30
|
+
loadDiscoveredNativeModels();
|
|
29
31
|
}
|
|
@@ -455,6 +455,14 @@ function dropCodexSafetyBufferingError(value: unknown): string | null {
|
|
|
455
455
|
return "schema_invalid: dropCodexSafetyBuffering: must be a boolean or omitted";
|
|
456
456
|
}
|
|
457
457
|
|
|
458
|
+
function showCodexCreditsError(value: unknown): string | null {
|
|
459
|
+
const raw = rawConfigRecord(value);
|
|
460
|
+
if (!raw || !Object.hasOwn(raw, "showCodexCredits")) return null;
|
|
461
|
+
const enabled = raw.showCodexCredits;
|
|
462
|
+
if (enabled === undefined || typeof enabled === "boolean") return null;
|
|
463
|
+
return "schema_invalid: showCodexCredits: must be a boolean or omitted";
|
|
464
|
+
}
|
|
465
|
+
|
|
458
466
|
function oauthOpenBrowserError(value: unknown): string | null {
|
|
459
467
|
const raw = rawConfigRecord(value);
|
|
460
468
|
if (!raw || !Object.hasOwn(raw, "oauthOpenBrowser")) return null;
|
|
@@ -645,6 +653,7 @@ export function validateConfigCandidate(value: unknown): { ok: true; config: Ocx
|
|
|
645
653
|
?? emptyCompletionRetryError(value)
|
|
646
654
|
?? dropCodexSafetyBufferingError(value)
|
|
647
655
|
?? oauthOpenBrowserError(value)
|
|
656
|
+
?? showCodexCreditsError(value)
|
|
648
657
|
?? runtimeRoleError(value)
|
|
649
658
|
?? remoteGuiConfigError(value)
|
|
650
659
|
?? clientConnectionConfigError(value)
|
|
@@ -18,7 +18,7 @@ export function metricsExportEnabled(config: Pick<OcxConfig, "metricsExport">):
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
* Default cadence for
|
|
21
|
+
* Default cadence for catalog auto-refresh (issue #3630): one converge pass
|
|
22
22
|
* per hour. Each pass spends a live /models call against every enabled provider, and
|
|
23
23
|
* provider catalogs are themselves cached upstream for minutes, so an hour is fresh
|
|
24
24
|
* enough for newly released models to appear without an `ocx sync`.
|
|
@@ -33,15 +33,17 @@ export const CATALOG_AUTO_REFRESH_DEFAULT_INTERVAL_MS: number = 60 * 60_000;
|
|
|
33
33
|
export const CATALOG_AUTO_REFRESH_MIN_INTERVAL_MS: number = 15 * 60_000;
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
36
|
+
* Enabled by default; explicit false or a zero cadence disables background work.
|
|
37
|
+
* Malformed enabled values still mean off. Pure on purpose: the scheduler calls this from a
|
|
38
38
|
* dynamically imported context, so it takes an explicit config slice and reads nothing
|
|
39
39
|
* global.
|
|
40
40
|
*/
|
|
41
41
|
export function isCatalogAutoRefreshEnabled(
|
|
42
42
|
config: Pick<OcxConfig, "catalogAutoRefresh">,
|
|
43
43
|
): boolean {
|
|
44
|
-
|
|
44
|
+
const section = config.catalogAutoRefresh;
|
|
45
|
+
return (section?.enabled === undefined || section.enabled === true)
|
|
46
|
+
&& section?.intervalMinutes !== 0;
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
/**
|
|
@@ -196,6 +196,7 @@ export const configSchema = z.object({
|
|
|
196
196
|
// A malformed hand edit must not silently stop opening the browser: fall back
|
|
197
197
|
// to undefined, which resolves to the historical auto-open behavior.
|
|
198
198
|
oauthOpenBrowser: z.boolean().optional().catch(undefined),
|
|
199
|
+
showCodexCredits: z.boolean().optional().catch(false),
|
|
199
200
|
openaiProviderTierVersion: z.union([z.literal(1), z.literal(2)]).optional(),
|
|
200
201
|
// Invalid hand edits must not discard an otherwise usable config.
|
|
201
202
|
googleAntigravityStaticCatalogVersion: z.union([z.literal(1), z.literal(2)]).optional().catch(undefined),
|
|
@@ -1058,6 +1058,7 @@ export const quotaResetNotifySchema = z.object({
|
|
|
1058
1058
|
|
|
1059
1059
|
/**
|
|
1060
1060
|
* Catalog auto-refresh section (issue #3630).
|
|
1061
|
+
* Missing section or enabled flag uses the hourly default-on scheduler.
|
|
1061
1062
|
*
|
|
1062
1063
|
* `.strict()` like its neighbour: a typo in an optional feature section should surface as a
|
|
1063
1064
|
* rejected write rather than a silently ignored key that leaves the operator believing they
|