@bitkyc08/opencodex 2.27.0 → 2.28.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/{index-7jlKgmJd.js → index-D2sP-biU.js} +11 -11
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +59 -0
- package/src/adapters/base.ts +2 -0
- package/src/adapters/google-antigravity-replay.ts +16 -8
- package/src/adapters/google.ts +21 -4
- package/src/adapters/openai-chat.ts +151 -54
- package/src/adapters/openai-responses.ts +37 -0
- package/src/cli/index.ts +19 -6
- package/src/codex/account-usability.ts +3 -0
- package/src/codex/auth-api.ts +22 -5
- package/src/codex/auth-context.ts +55 -2
- package/src/codex/catalog/metadata.ts +17 -3
- package/src/codex/catalog/native-models.ts +22 -14
- package/src/codex/catalog/sync.ts +57 -11
- package/src/codex/convergence.ts +61 -13
- package/src/codex/model-entitlements.ts +353 -0
- package/src/codex/quota.ts +28 -3
- package/src/codex/routing.ts +14 -8
- package/src/generated/compatibility-version.json +51 -39
- package/src/lib/destination-policy.ts +47 -0
- package/src/lib/shadow-call.ts +15 -0
- package/src/oauth/index.ts +33 -5
- package/src/oauth/store.ts +11 -5
- package/src/providers/fastwire.ts +39 -8
- package/src/providers/quota.ts +9 -2
- package/src/providers/registry.ts +74 -5
- package/src/providers/service-tier.ts +16 -8
- package/src/responses/parser.ts +3 -9
- package/src/responses/tool-search-compat.ts +301 -0
- package/src/router.ts +7 -0
- package/src/routing/capability.ts +26 -9
- package/src/routing/compatibility/behavior.ts +41 -3
- package/src/server/chat-native.ts +11 -2
- package/src/server/index.ts +54 -8
- package/src/server/management/agent-settings-routes.ts +16 -2
- package/src/server/request-log.ts +31 -0
- package/src/server/responses/compact.ts +54 -7
- package/src/server/responses/core.ts +246 -39
- package/src/server/responses/responses-field-backfill.ts +88 -6
- package/src/server/responses/terminal-guard.ts +10 -0
- package/src/server/responses-tool-search-repair.ts +217 -0
- package/src/server/system-env.ts +74 -5
- package/src/usage/log.ts +4 -0
package/src/cli/index.ts
CHANGED
|
@@ -46,9 +46,8 @@ import { loadServiceTokenFromFile } from "../lib/service-secrets";
|
|
|
46
46
|
import { diagnoseService, isServiceOwnershipError, serviceCommand, serviceEnvironmentOwnedHere, serviceStartableFromTray, serviceStatusSummary, stopServiceIfInstalled, uninstallServiceIfInstalled } from "../service";
|
|
47
47
|
import { startupHealthSummary } from "../codex/autostart-health";
|
|
48
48
|
import { drainAndShutdown, isRecyclingForExit, startServer } from "../server";
|
|
49
|
-
import { injectSystemEnv, revertSystemEnv } from "../server/system-env";
|
|
49
|
+
import { injectSystemEnv, reconcileShellHook, revertSystemEnv, uninstallShellHook } from "../server/system-env";
|
|
50
50
|
import { buildDesktop3pRegistry } from "../claude/desktop-3p";
|
|
51
|
-
import { installShellHook, uninstallShellHook } from "../server/system-env";
|
|
52
51
|
import { startTokenGuardian } from "../oauth/token-guardian";
|
|
53
52
|
import { startHistoryMigrationGuardian } from "../codex/history-migration-guardian";
|
|
54
53
|
import { maybeShowStarPrompt } from "./star-prompt";
|
|
@@ -57,6 +56,18 @@ import { maybeShowUpdatePrompt } from "../update/notify";
|
|
|
57
56
|
import { syncModelsToCodex } from "../codex/sync";
|
|
58
57
|
import { setIntegrationEnabled, shouldSyncCodexOnStart, shouldSyncGrokOnStart, syncCodexOnStartIfEnabled } from "../codex/desired-state";
|
|
59
58
|
|
|
59
|
+
/**
|
|
60
|
+
* A failed shell-hook reconcile is not cosmetic: a stale hook keeps sourcing
|
|
61
|
+
* `claude-env.sh` from every new interactive shell, pointing at a proxy or a CLI that may no
|
|
62
|
+
* longer exist. `reconcileShellHook` already reports `state: "failed"`, but both call sites
|
|
63
|
+
* discarded it, so the one outcome the user has to act on was the one they never saw.
|
|
64
|
+
*/
|
|
65
|
+
function reportShellHookFailure(result: { state: "installed" | "absent" | "failed"; reason?: string }): void {
|
|
66
|
+
if (result.state !== "failed") return;
|
|
67
|
+
console.warn(` Claude shell hook not reconciled${result.reason ? `: ${result.reason}` : ""}`);
|
|
68
|
+
console.warn(" Check ~/.zshrc for the '# opencodex claude-env hook' block.");
|
|
69
|
+
}
|
|
70
|
+
|
|
60
71
|
|
|
61
72
|
import { removeOwnedConfigState } from "../lib/config-ownership";
|
|
62
73
|
import { withProcessRuntimeProvenance } from "../lib/bun-runtime";
|
|
@@ -366,9 +377,10 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
366
377
|
|
|
367
378
|
// System-wide env injection AFTER signal handlers are registered (crash safety:
|
|
368
379
|
// syncCleanup reverts even if injection itself or subsequent startup steps fail).
|
|
369
|
-
await injectSystemEnv(port, config).catch(() => {});
|
|
370
|
-
//
|
|
371
|
-
|
|
380
|
+
const systemEnv = await injectSystemEnv(port, config).catch(() => ({ injected: false }));
|
|
381
|
+
// The hook is useful only for an installed Claude Code CLI. Reconcile instead of
|
|
382
|
+
// appending unconditionally so stale OpenCodex-owned hooks are removed as well.
|
|
383
|
+
reportShellHookFailure(reconcileShellHook(systemEnv.injected));
|
|
372
384
|
|
|
373
385
|
await maybeShowStarPrompt(); // once-only Yes/No GitHub-star prompt on first interactive start
|
|
374
386
|
// Post-startup sync drives the readiness gate AND the #1046 stale app-server
|
|
@@ -455,7 +467,8 @@ async function handleEnsure(options: { existingIsSuccess?: boolean } = {}): Prom
|
|
|
455
467
|
});
|
|
456
468
|
if (synced?.status === "skipped") console.log(" Codex integration OFF; startup left Codex native.");
|
|
457
469
|
// Ensure env file exists for already-running proxy (may have been deleted or pre-dates this feature).
|
|
458
|
-
await injectSystemEnv(live.port, config).catch(() => {});
|
|
470
|
+
const systemEnv = await injectSystemEnv(live.port, config).catch(() => ({ injected: false }));
|
|
471
|
+
reportShellHookFailure(reconcileShellHook(systemEnv.injected));
|
|
459
472
|
// Refresh the Grok Build fence too (same contract as start). live.hostname is the
|
|
460
473
|
// hostname the running proxy actually bound — config.hostname may have drifted.
|
|
461
474
|
try {
|
|
@@ -10,6 +10,8 @@ export interface CodexAccountUsabilityOptions {
|
|
|
10
10
|
nativeMainSelectionOnly?: boolean;
|
|
11
11
|
/** Test seam for proving whether routing attempted a physical native-token read. */
|
|
12
12
|
isMainAccountTokenLive?: typeof isMainAccountTokenLive;
|
|
13
|
+
/** Confirmed account ids for an account-gated model; omitted for ordinary native models. */
|
|
14
|
+
modelEligibleAccountIds?: ReadonlySet<string>;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
export function isCodexAccountUsable(
|
|
@@ -17,6 +19,7 @@ export function isCodexAccountUsable(
|
|
|
17
19
|
accountId: string,
|
|
18
20
|
options: CodexAccountUsabilityOptions = {},
|
|
19
21
|
): boolean {
|
|
22
|
+
if (options.modelEligibleAccountIds && !options.modelEligibleAccountIds.has(accountId)) return false;
|
|
20
23
|
if (accountId === MAIN_CODEX_ACCOUNT_ID) {
|
|
21
24
|
// Startup recovery owns the physical auth/vault boundary. Never parse or select
|
|
22
25
|
// native __main__ while an encrypted switch journal is pending or inconclusive.
|
package/src/codex/auth-api.ts
CHANGED
|
@@ -644,12 +644,13 @@ async function retryMainAccountInfoIfIdentityChanged(
|
|
|
644
644
|
requestAccountId: string | null,
|
|
645
645
|
retriesRemaining: number,
|
|
646
646
|
nativeMainLease: AdmissionLease,
|
|
647
|
+
explicitRefresh: boolean,
|
|
647
648
|
): Promise<MainAccountInfoFetchResult | null> {
|
|
648
649
|
const currentAccountId = getMainChatgptAccountId();
|
|
649
650
|
if (currentAccountId === null || currentAccountId === requestAccountId) return null;
|
|
650
651
|
reconcileMainCodexAccountRuntimeState();
|
|
651
652
|
return retriesRemaining > 0
|
|
652
|
-
? fetchMainAccountInfoWhileOwned(true, retriesRemaining - 1, nativeMainLease)
|
|
653
|
+
? fetchMainAccountInfoWhileOwned(true, retriesRemaining - 1, nativeMainLease, explicitRefresh)
|
|
653
654
|
: { info: EMPTY_MAIN_ACCOUNT_INFO, credentialChecked: true, hasCredential: true };
|
|
654
655
|
}
|
|
655
656
|
|
|
@@ -696,6 +697,13 @@ async function fetchMainAccountInfoWhileOwned(
|
|
|
696
697
|
forceRefresh: boolean,
|
|
697
698
|
retriesRemaining: number,
|
|
698
699
|
nativeMainLease: AdmissionLease,
|
|
700
|
+
/**
|
|
701
|
+
* Whether the *caller* asked for this refresh. `forceRefresh` also means "bypass the
|
|
702
|
+
* cache", and `retryMainAccountInfoIfIdentityChanged` re-enters with it set purely to
|
|
703
|
+
* re-read after the identity changed. Keeping the two apart stops that retry from
|
|
704
|
+
* promoting a background poll into operator intent below.
|
|
705
|
+
*/
|
|
706
|
+
explicitRefresh: boolean = forceRefresh,
|
|
699
707
|
): Promise<MainAccountInfoFetchResult> {
|
|
700
708
|
const writerGeneration = captureConfigGeneration();
|
|
701
709
|
reconcileMainCodexAccountRuntimeState();
|
|
@@ -724,7 +732,7 @@ async function fetchMainAccountInfoWhileOwned(
|
|
|
724
732
|
});
|
|
725
733
|
if (!resp.ok) {
|
|
726
734
|
const terminalAuthFailure = await isTerminalMainAuthResponse(resp, isMainAccountTokenVerifiablyLive());
|
|
727
|
-
const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease);
|
|
735
|
+
const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh);
|
|
728
736
|
if (retried) return retried;
|
|
729
737
|
if (terminalAuthFailure) {
|
|
730
738
|
clearMainAccountInfoCache();
|
|
@@ -733,7 +741,7 @@ async function fetchMainAccountInfoWhileOwned(
|
|
|
733
741
|
return { info: EMPTY_MAIN_ACCOUNT_INFO, credentialChecked: true, hasCredential: true };
|
|
734
742
|
}
|
|
735
743
|
const data = (await resp.json()) as WhamUsageResponse;
|
|
736
|
-
const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease);
|
|
744
|
+
const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh);
|
|
737
745
|
if (retried) return retried;
|
|
738
746
|
const plan = nonEmptyPlan(data.plan_type) ?? nonEmptyPlan(cached?.plan) ?? nonEmptyPlan(getMainAccountPlan());
|
|
739
747
|
const quota = parseUsageQuota({ ...data, ...(plan ? { plan_type: plan } : {}) });
|
|
@@ -745,7 +753,16 @@ async function fetchMainAccountInfoWhileOwned(
|
|
|
745
753
|
ts: Date.now(),
|
|
746
754
|
};
|
|
747
755
|
setMainAccountInfoCache(result);
|
|
748
|
-
|
|
756
|
+
// Only an explicit refresh may retract a reauth quarantine. A 200 from
|
|
757
|
+
// /wham/usage proves the token authenticates to the usage endpoint; it does not
|
|
758
|
+
// prove the account can serve Responses traffic, which is a different backend path
|
|
759
|
+
// and still answers 403 for a workspace the token may no longer select (#327).
|
|
760
|
+
// Letting the background poll clear the flag put such an account straight back into
|
|
761
|
+
// rotation: the next request failed the same way and re-marked it, so needsReauth
|
|
762
|
+
// never settled and the dashboard kept showing nothing — the symptom #327 reported.
|
|
763
|
+
// An explicit refresh is an operator asking to re-evaluate, normally right after
|
|
764
|
+
// signing in again, so it stays authoritative.
|
|
765
|
+
if (explicitRefresh) clearAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
|
|
749
766
|
// Mirror main quota + plan into the shared stores so the rotation engine can
|
|
750
767
|
// score and auto-switch the main account exactly like a pool account (Option A).
|
|
751
768
|
setMainAccountPlan(result.plan);
|
|
@@ -760,7 +777,7 @@ async function fetchMainAccountInfoWhileOwned(
|
|
|
760
777
|
...(freshResetCredits !== undefined ? { freshResetCredits } : {}),
|
|
761
778
|
};
|
|
762
779
|
} catch {
|
|
763
|
-
const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease);
|
|
780
|
+
const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh);
|
|
764
781
|
return retried ?? { info: EMPTY_MAIN_ACCOUNT_INFO, credentialChecked: true, hasCredential: true };
|
|
765
782
|
}
|
|
766
783
|
}
|
|
@@ -24,6 +24,13 @@ import {
|
|
|
24
24
|
pickAlternateCodexAccount,
|
|
25
25
|
resolveCodexAccountForThreadDetailed,
|
|
26
26
|
} from "./routing";
|
|
27
|
+
import {
|
|
28
|
+
entitledCodexAccountIdsForModel,
|
|
29
|
+
isDirectCallerEntitledToCodexModel,
|
|
30
|
+
resolveCodexModelEntitlements,
|
|
31
|
+
type CodexModelEntitlementSnapshot,
|
|
32
|
+
} from "./model-entitlements";
|
|
33
|
+
import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "./catalog/native-models";
|
|
27
34
|
import type { CodexCooldownSource, CodexQuotaScope } from "./routing";
|
|
28
35
|
import { maskAccountId } from "../lib/privacy";
|
|
29
36
|
import { formatErrorResponse } from "../bridge";
|
|
@@ -289,6 +296,14 @@ export interface ResolveCodexAuthContextOptions {
|
|
|
289
296
|
isMainAccountTokenLive?: () => boolean;
|
|
290
297
|
getMainAccountToken?: typeof getMainAccountToken;
|
|
291
298
|
primeCodexPoolQuotas?: (config: OcxConfig, reason: string) => Promise<void>;
|
|
299
|
+
/** Test seam for account-gated native model discovery. */
|
|
300
|
+
resolveCodexModelEntitlements?: (
|
|
301
|
+
config: Pick<OcxConfig, "codexAccounts">,
|
|
302
|
+
) => Promise<CodexModelEntitlementSnapshot>;
|
|
303
|
+
/** Direct requests admitted with a proxy bearer substitute the stored native-main credential. */
|
|
304
|
+
substituteMainCredentialForDirect?: boolean;
|
|
305
|
+
/** Test seam for a Direct request's own forwarded ChatGPT credential. */
|
|
306
|
+
isDirectCallerEntitledToCodexModel?: (headers: Headers, modelId: string) => Promise<boolean>;
|
|
292
307
|
}
|
|
293
308
|
|
|
294
309
|
export interface CodexAccountSelectionAdmission {
|
|
@@ -312,8 +327,28 @@ export async function resolveCodexAuthContext(
|
|
|
312
327
|
// selected stored credential even while the canonical OpenAI provider is globally Direct.
|
|
313
328
|
if (mode === "direct" && fixedAccountId === undefined) {
|
|
314
329
|
if (!hasCallerCodexBearer(headers)) throw new CodexDirectAuthenticationError();
|
|
330
|
+
if (options.modelId && ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(options.modelId)) {
|
|
331
|
+
const entitled = options.substituteMainCredentialForDirect
|
|
332
|
+
? entitledCodexAccountIdsForModel(
|
|
333
|
+
await (options.resolveCodexModelEntitlements ?? resolveCodexModelEntitlements)(config),
|
|
334
|
+
options.modelId,
|
|
335
|
+
)?.has(MAIN_CODEX_ACCOUNT_ID) === true
|
|
336
|
+
: await (options.isDirectCallerEntitledToCodexModel ?? isDirectCallerEntitledToCodexModel)(
|
|
337
|
+
headers,
|
|
338
|
+
options.modelId,
|
|
339
|
+
);
|
|
340
|
+
if (!entitled) {
|
|
341
|
+
throw new CodexPoolAuthenticationError("The selected ChatGPT account does not support this model");
|
|
342
|
+
}
|
|
343
|
+
}
|
|
315
344
|
return { kind: "main", accountId: null };
|
|
316
345
|
}
|
|
346
|
+
const entitlementSnapshot = options.modelId && ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(options.modelId)
|
|
347
|
+
? await (options.resolveCodexModelEntitlements ?? resolveCodexModelEntitlements)(config)
|
|
348
|
+
: undefined;
|
|
349
|
+
const modelEligibleAccountIds = entitlementSnapshot
|
|
350
|
+
? entitledCodexAccountIdsForModel(entitlementSnapshot, options.modelId)
|
|
351
|
+
: undefined;
|
|
317
352
|
// Retained startup recovery makes the physical main identity ineligible. Routing
|
|
318
353
|
// can still preserve service by selecting a healthy configured pool account.
|
|
319
354
|
const nativeMainTrafficBlocked = isNativeMainTrafficBlocked();
|
|
@@ -325,6 +360,7 @@ export async function resolveCodexAuthContext(
|
|
|
325
360
|
nativeMainSelectionOnly: !nativeMainTrafficBlocked
|
|
326
361
|
&& selectionAdmission?.mainProfileDraining === true,
|
|
327
362
|
isMainAccountTokenLive: options.isMainAccountTokenLive,
|
|
363
|
+
modelEligibleAccountIds,
|
|
328
364
|
};
|
|
329
365
|
let accountId: string;
|
|
330
366
|
const quotaScope = codexQuotaScopeForModel(options.modelId);
|
|
@@ -354,7 +390,11 @@ export async function resolveCodexAuthContext(
|
|
|
354
390
|
const selected = resolution.status === "selected" ? resolution.accountId : null;
|
|
355
391
|
if (!selected) {
|
|
356
392
|
if (fixedAccountId !== undefined) {
|
|
357
|
-
throw new CodexPoolAuthenticationError(
|
|
393
|
+
throw new CodexPoolAuthenticationError(
|
|
394
|
+
modelEligibleAccountIds && !modelEligibleAccountIds.has(fixedAccountId)
|
|
395
|
+
? "Selected Codex account does not support this model"
|
|
396
|
+
: "Selected Codex account is unavailable",
|
|
397
|
+
);
|
|
358
398
|
}
|
|
359
399
|
// Recovery deliberately makes physical main ineligible. If no healthy
|
|
360
400
|
// pool route is configured and main is the intended route, report the
|
|
@@ -364,7 +404,9 @@ export async function resolveCodexAuthContext(
|
|
|
364
404
|
if (nativeMainTrafficBlocked && !options.excludeAccountId) {
|
|
365
405
|
throw new CodexMainProfileDrainingError();
|
|
366
406
|
}
|
|
367
|
-
throw new CodexPoolAuthenticationError(
|
|
407
|
+
throw new CodexPoolAuthenticationError(
|
|
408
|
+
modelEligibleAccountIds ? "No eligible Codex account supports this model" : undefined,
|
|
409
|
+
);
|
|
368
410
|
}
|
|
369
411
|
accountId = selected;
|
|
370
412
|
if (accountId === MAIN_CODEX_ACCOUNT_ID && nativeMainTrafficBlocked) {
|
|
@@ -377,6 +419,17 @@ export async function resolveCodexAuthContext(
|
|
|
377
419
|
) {
|
|
378
420
|
throw new CodexMainProfileDrainingError();
|
|
379
421
|
}
|
|
422
|
+
// Some legacy Pool fallbacks preserve a configured active account even when it is not
|
|
423
|
+
// currently selectable, so token/cooldown code can produce the historical actionable error.
|
|
424
|
+
// Model entitlement is different: sending the request would spend a turn on an account whose
|
|
425
|
+
// authenticated roster already denied the model. Reassert this boundary after every selector.
|
|
426
|
+
if (modelEligibleAccountIds && !modelEligibleAccountIds.has(accountId)) {
|
|
427
|
+
throw new CodexPoolAuthenticationError(
|
|
428
|
+
fixedAccountId !== undefined
|
|
429
|
+
? "Selected Codex account does not support this model"
|
|
430
|
+
: "No eligible Codex account supports this model",
|
|
431
|
+
);
|
|
432
|
+
}
|
|
380
433
|
if (fixedAccountId !== undefined) {
|
|
381
434
|
if (isCodexAccountPaused(config, accountId)) {
|
|
382
435
|
throw new CodexPoolAuthenticationError("Selected Codex account is unavailable");
|
|
@@ -12,7 +12,7 @@ import { modelInList } from "../../types";
|
|
|
12
12
|
import { CODEX_REASONING_LEVELS, codexEffortRank, configuredReasoningEfforts, modelRecordValue, sanitizeCodexReasoningEfforts } from "../../reasoning-effort";
|
|
13
13
|
import { getModelMetadata, getModelMetadataCaseInsensitive, listModelMetadata, resolveMetadataProvider } from "../../generated/model-metadata";
|
|
14
14
|
import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../../providers/derive";
|
|
15
|
-
import { getProviderRegistryEntry } from "../../providers/registry";
|
|
15
|
+
import { getProviderRegistryEntry, providerCodexAccountMode } from "../../providers/registry";
|
|
16
16
|
import { applyProviderContextCap, providerContextCap } from "../../providers/context-cap";
|
|
17
17
|
import { routedSlug, slugEquals, slugsEquivalent } from "../../providers/slug-codec";
|
|
18
18
|
import { identifyRoutedModel } from "../../adapters/identity";
|
|
@@ -38,6 +38,7 @@ import { readCurrentCatalogOrCache, readCurrentCodexCatalog, readCurrentCodexMod
|
|
|
38
38
|
import { trustedAccountBoundNativeCatalogSlug, visibleCodexAccountSelectors } from "./account-models";
|
|
39
39
|
import { CODEX_NATIVE_ALIAS_CATALOG_KIND } from "./kinds";
|
|
40
40
|
import {
|
|
41
|
+
ACCOUNT_GATED_NATIVE_OPENAI_MODELS,
|
|
41
42
|
NATIVE_DAYBREAK_BLUE_MODEL,
|
|
42
43
|
NATIVE_OPENAI_CAPABILITY_ALIAS_MODELS,
|
|
43
44
|
NATIVE_OPENAI_MODELS,
|
|
@@ -45,6 +46,8 @@ import {
|
|
|
45
46
|
isNativeOpenAiCapabilityAliasModel,
|
|
46
47
|
nativeOpenAiCapabilitySourceSlug,
|
|
47
48
|
} from "./native-models";
|
|
49
|
+
import { cachedAvailableAccountGatedNativeModels } from "../model-entitlements";
|
|
50
|
+
import { MAIN_CODEX_ACCOUNT_ID } from "../main-account";
|
|
48
51
|
export { CODEX_NATIVE_ALIAS_CATALOG_KIND } from "./kinds";
|
|
49
52
|
export {
|
|
50
53
|
NATIVE_DAYBREAK_BLUE_MODEL,
|
|
@@ -390,7 +393,14 @@ export function nativeModelRows(config: Pick<OcxConfig, "disabledModels" | "comb
|
|
|
390
393
|
// Both user levers, not just the cap: a per-model window set from the dashboard has to show
|
|
391
394
|
// up on the row the dashboard itself renders.
|
|
392
395
|
const limits = nativeContextLimits(config);
|
|
393
|
-
|
|
396
|
+
const bareEligibleAccountIds = providerCodexAccountMode(
|
|
397
|
+
OPENAI_CODEX_PROVIDER_ID,
|
|
398
|
+
config.providers?.[OPENAI_CODEX_PROVIDER_ID],
|
|
399
|
+
) === "direct" ? new Set([MAIN_CODEX_ACCOUNT_ID]) : undefined;
|
|
400
|
+
const availableGated = cachedAvailableAccountGatedNativeModels(Date.now(), bareEligibleAccountIds);
|
|
401
|
+
return NATIVE_OPENAI_MODELS
|
|
402
|
+
.filter(slug => !ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug) || availableGated.has(slug))
|
|
403
|
+
.filter(slug => !shadowed.has(slug)).map(slug => {
|
|
394
404
|
const contextWindow = nativeOpenAiContextWindow(slug, limits);
|
|
395
405
|
const maxInputTokens = nativeOpenAiMaxInputTokens(slug, limits);
|
|
396
406
|
return {
|
|
@@ -475,7 +485,11 @@ export function shouldUpgradeToUpstreamEntry(entry: RawEntry): boolean {
|
|
|
475
485
|
|
|
476
486
|
export function nativeOpenAiSlugs(): string[] {
|
|
477
487
|
const live = catalogNativeSlugs();
|
|
478
|
-
|
|
488
|
+
const availableGated = cachedAvailableAccountGatedNativeModels();
|
|
489
|
+
const candidates = live.length > 0 ? unique([...live, ...DOCUMENTED_NATIVE_OPENAI_ADDITIONS]) : NATIVE_OPENAI_MODELS;
|
|
490
|
+
return candidates.filter(slug => (
|
|
491
|
+
!ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug) || availableGated.has(slug)
|
|
492
|
+
));
|
|
479
493
|
}
|
|
480
494
|
|
|
481
495
|
const ACCOUNT_BOUND_OPENAI_NATIVE_PREFIX = /^(?:gpt-|o1-|o3-|o4-)/;
|
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
/** ChatGPT/Codex wire id observed for the account-native Daybreak Blue surface. */
|
|
2
2
|
export const NATIVE_DAYBREAK_BLUE_MODEL = "gpt-daybreak-blue-latest";
|
|
3
3
|
|
|
4
|
+
/** Native ChatGPT/Codex ids whose availability is proven per authenticated account. */
|
|
5
|
+
export const ACCOUNT_GATED_NATIVE_OPENAI_MODELS: ReadonlySet<string> = new Set([
|
|
6
|
+
NATIVE_DAYBREAK_BLUE_MODEL,
|
|
7
|
+
]);
|
|
8
|
+
|
|
4
9
|
/**
|
|
5
10
|
* Account-native aliases whose Codex capabilities track another pinned native row.
|
|
6
11
|
*
|
|
7
12
|
* This is catalog metadata inheritance only. Routing always preserves the requested
|
|
8
|
-
* wire id
|
|
9
|
-
*
|
|
13
|
+
* wire id for the separately billed API-key `daybreak-*-latest` surface, so the two never
|
|
14
|
+
* collapse into each other.
|
|
15
|
+
*
|
|
16
|
+
* The ChatGPT/Codex surface is different: an account-gated request IS rewritten to its
|
|
17
|
+
* canonical wire model before it leaves the process (`applyCodexAccountGatedWireNormalization`
|
|
18
|
+
* in src/server/responses/core.ts), because the authenticated backend rejects the gated slug
|
|
19
|
+
* on shards that do not carry it. The catalog keeps the product identity; only the wire moves.
|
|
10
20
|
*/
|
|
11
21
|
const NATIVE_OPENAI_CAPABILITY_SOURCES: Readonly<Record<string, string>> = Object.freeze({
|
|
12
22
|
[NATIVE_DAYBREAK_BLUE_MODEL]: "gpt-5.6-sol",
|
|
@@ -16,14 +26,14 @@ const NATIVE_OPENAI_CAPABILITY_SOURCES: Readonly<Record<string, string>> = Objec
|
|
|
16
26
|
* Native ids whose capability metadata is inherited from another pinned native row.
|
|
17
27
|
*
|
|
18
28
|
* Membership here is about METADATA INHERITANCE only, and is independent of whether the
|
|
19
|
-
* slug is also
|
|
20
|
-
*
|
|
21
|
-
*
|
|
29
|
+
* slug is also present in `NATIVE_OPENAI_MODELS`. `gpt-daybreak-blue-latest` is now in BOTH:
|
|
30
|
+
* it inherits Sol's capability shape AND is a supported account-gated native id (owner decision,
|
|
31
|
+
* devlog 260816_codexrs_multiagent_v2_and_history_perf/011).
|
|
22
32
|
*
|
|
23
33
|
* The maps that consume the union of these two lists (`PINNED_NATIVE_CAPABILITY_ENTRIES`,
|
|
24
34
|
* `UPSTREAM_NATIVE_ENTRIES`) are keyed by slug, so an overlapping id collapses to one
|
|
25
|
-
* entry. Catalog row generation iterates `NATIVE_OPENAI_MODELS
|
|
26
|
-
*
|
|
35
|
+
* entry. Catalog row generation iterates `NATIVE_OPENAI_MODELS`, then entitlement evidence limits
|
|
36
|
+
* it to at most one bare row and one row per entitled account selector.
|
|
27
37
|
*/
|
|
28
38
|
export const NATIVE_OPENAI_CAPABILITY_ALIAS_MODELS = Object.freeze(
|
|
29
39
|
Object.keys(NATIVE_OPENAI_CAPABILITY_SOURCES),
|
|
@@ -42,16 +52,14 @@ export function nativeOpenAiCapabilitySourceSlug(slug: string): string {
|
|
|
42
52
|
*
|
|
43
53
|
* `gpt-daybreak-blue-latest` is entitlement-gated upstream: it is absent from codex-rs's
|
|
44
54
|
* bundled catalog and reaches a client only through an authenticated `/models` response.
|
|
45
|
-
* It is listed here by explicit owner decision so the
|
|
46
|
-
* observation, because opencodex injects `model_catalog_json` and codex-rs therefore builds
|
|
55
|
+
* It is listed here by explicit owner decision so the capability template exists without waiting
|
|
56
|
+
* for an observation, because opencodex injects `model_catalog_json` and codex-rs therefore builds
|
|
47
57
|
* a `StaticModelsManager` whose refresh is a no-op — an entitled account had no way to
|
|
48
58
|
* discover it on a clean install.
|
|
49
59
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* retry one alternate account on that exact body; a selector-qualified route is fixed and
|
|
54
|
-
* relays immediately). `disabledModels` hides the row but is NOT a runtime routing denial.
|
|
60
|
+
* Availability is not static: catalog sync and Pool routing require the account's authenticated
|
|
61
|
+
* `/models` roster to contain the slug. An unconfirmed or unentitled account never receives the
|
|
62
|
+
* request. `disabledModels` remains the independent user visibility control.
|
|
55
63
|
*
|
|
56
64
|
* Devlog: 260816_codexrs_multiagent_v2_and_history_perf/011 §4-bis.
|
|
57
65
|
*/
|
|
@@ -30,7 +30,15 @@ import { providerDestinationResolvedError } from "../../lib/destination-policy";
|
|
|
30
30
|
import { redactSecretString } from "../../lib/redact";
|
|
31
31
|
import upstreamModelsSnapshot from "../data/upstream-models.json";
|
|
32
32
|
import { OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
|
|
33
|
+
import { providerCodexAccountMode } from "../../providers/registry";
|
|
33
34
|
import { codexAccountNamespaceEntries, isMainCodexAccountTarget } from "../account-namespaces";
|
|
35
|
+
import { MAIN_CODEX_ACCOUNT_ID } from "../main-account";
|
|
36
|
+
import {
|
|
37
|
+
availableAccountGatedNativeModels,
|
|
38
|
+
isCodexModelEntitlementSnapshotCurrent,
|
|
39
|
+
resolveCodexModelEntitlements,
|
|
40
|
+
type CodexModelEntitlementSnapshot,
|
|
41
|
+
} from "../model-entitlements";
|
|
34
42
|
|
|
35
43
|
|
|
36
44
|
import { CODEX_CUSTOM_MODEL_CATALOG_KIND, CODEX_PROVIDER_MODEL_CATALOG_KIND, activeCodexModelsCachePath, applyCatalogMetadata, applyMultiAgentMode, applyNativeOpenAiContextOverride, applyRoutedCodexToolMode, catalogBackupPathFor, catalogHasRoutedEntries, catalogModelSlug, ensureStrictCatalogFields, findNativeTemplate, isDefaultCatalogPath, isRoutedModelCompatibilityExcluded, legacyCatalogBackupPath, normalizeRoutedCatalogEntry, normalizeServiceTiers, readCatalog, readCatalogBackup, readCodexCatalogPath, readNativeBaseline } from "./parsing";
|
|
@@ -64,6 +72,7 @@ import {
|
|
|
64
72
|
} from "../internal/catalog-writer";
|
|
65
73
|
import { codexRuntimeStatePath } from "../runtime";
|
|
66
74
|
import { accountBoundNativeDisplayName, CODEX_ACCOUNT_BOUND_CATALOG_KIND, trustedAccountBoundNativeCatalogSlug, visibleCodexAccountSelectors } from "./account-models";
|
|
75
|
+
import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "./native-models";
|
|
67
76
|
|
|
68
77
|
export const MAX_SPAWN_AGENT_MODEL_OVERRIDES = 5;
|
|
69
78
|
|
|
@@ -1238,6 +1247,7 @@ interface RetainedCatalogSyncWrite {
|
|
|
1238
1247
|
readonly read: RetainedCatalogSyncRead;
|
|
1239
1248
|
readonly permit: CatalogWritePermit;
|
|
1240
1249
|
readonly owningCodexHome: string;
|
|
1250
|
+
readonly modelEntitlements: CodexModelEntitlementSnapshot;
|
|
1241
1251
|
}
|
|
1242
1252
|
|
|
1243
1253
|
function optionalFileBytes(path: string): string | null {
|
|
@@ -1401,6 +1411,7 @@ function writeRetainedCatalogSync({
|
|
|
1401
1411
|
read,
|
|
1402
1412
|
permit,
|
|
1403
1413
|
owningCodexHome,
|
|
1414
|
+
modelEntitlements,
|
|
1404
1415
|
}: RetainedCatalogSyncWrite): RetainedCatalogSyncResult {
|
|
1405
1416
|
const { catalogPath, catalog, onDiskCatalog } = read;
|
|
1406
1417
|
const catalogModelsForMerge = catalogModelsForMergeWithNativeRecovery(
|
|
@@ -1436,7 +1447,28 @@ function writeRetainedCatalogSync({
|
|
|
1436
1447
|
const modelPickerOrder = config.modelPickerOrder ?? [];
|
|
1437
1448
|
const multiAgentMode: MultiAgentMode = config.multiAgentMode === "v1" || config.multiAgentMode === "v2" ? config.multiAgentMode : "default";
|
|
1438
1449
|
const exactComboSlugs = exactComboCatalogSlugs(config);
|
|
1439
|
-
const
|
|
1450
|
+
const bareEligibleAccountIds = providerCodexAccountMode(
|
|
1451
|
+
OPENAI_CODEX_PROVIDER_ID,
|
|
1452
|
+
config.providers[OPENAI_CODEX_PROVIDER_ID],
|
|
1453
|
+
) === "direct" ? new Set([MAIN_CODEX_ACCOUNT_ID]) : undefined;
|
|
1454
|
+
const availableBareGatedNativeSlugs = availableAccountGatedNativeModels(
|
|
1455
|
+
modelEntitlements,
|
|
1456
|
+
bareEligibleAccountIds,
|
|
1457
|
+
);
|
|
1458
|
+
const availableAccountGatedNativeSlugs = availableAccountGatedNativeModels(modelEntitlements);
|
|
1459
|
+
const availableBareNativeSlugs = NATIVE_OPENAI_MODELS.filter(slug => (
|
|
1460
|
+
!ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug) || availableBareGatedNativeSlugs.has(slug)
|
|
1461
|
+
));
|
|
1462
|
+
const availableAccountNativeSlugs = NATIVE_OPENAI_MODELS.filter(slug => (
|
|
1463
|
+
!ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug) || availableAccountGatedNativeSlugs.has(slug)
|
|
1464
|
+
));
|
|
1465
|
+
const unavailableGatedNativeSlugs = new Set([...ACCOUNT_GATED_NATIVE_OPENAI_MODELS].filter(slug => (
|
|
1466
|
+
!availableBareGatedNativeSlugs.has(slug)
|
|
1467
|
+
)));
|
|
1468
|
+
const suppressedBareNativeSlugs = new Set([
|
|
1469
|
+
...desktopAllowlistSuppressedNativeSlugs(config),
|
|
1470
|
+
...unavailableGatedNativeSlugs,
|
|
1471
|
+
]);
|
|
1440
1472
|
const hasPhysicalComboProvider = Object.hasOwn(config.providers, COMBO_NAMESPACE);
|
|
1441
1473
|
const includeNativeOpenAi = shouldIncludeNativeOpenAi(config);
|
|
1442
1474
|
const includeAccountBoundNativeOpenAi = shouldIncludeAccountBoundNativeOpenAi(config);
|
|
@@ -1451,12 +1483,21 @@ function writeRetainedCatalogSync({
|
|
|
1451
1483
|
...(onDiskCatalog?.models ?? []).filter(entry =>
|
|
1452
1484
|
trustedAccountBoundNativeCatalogSlug(entry) !== undefined),
|
|
1453
1485
|
];
|
|
1454
|
-
const
|
|
1455
|
-
? accountBoundNativeOpenAiSlugs(observedAccountNativeEntries)
|
|
1456
|
-
: [];
|
|
1486
|
+
const accountTargets = new Map(codexAccountNamespaceEntries(config));
|
|
1457
1487
|
const accountNativeSlugsBySelector = accountSelectors.length > 0
|
|
1458
|
-
? accountBoundNativeOpenAiSlugsBySelector(config, observedAccountNativeEntries)
|
|
1488
|
+
? new Map([...accountBoundNativeOpenAiSlugsBySelector(config, observedAccountNativeEntries)].map(([selector, slugs]) => {
|
|
1489
|
+
const target = accountTargets.get(selector);
|
|
1490
|
+
const accountId = target && isMainCodexAccountTarget(target) ? MAIN_CODEX_ACCOUNT_ID : target;
|
|
1491
|
+
const entitled = accountId ? modelEntitlements.modelsByAccount.get(accountId) : undefined;
|
|
1492
|
+
const confirmed = accountId ? modelEntitlements.confirmedAccountIds.has(accountId) : false;
|
|
1493
|
+
return [selector, slugs.filter(slug => (
|
|
1494
|
+
!ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug) || (confirmed && entitled?.has(slug) === true)
|
|
1495
|
+
))] as const;
|
|
1496
|
+
}))
|
|
1459
1497
|
: new Map<string, readonly string[]>();
|
|
1498
|
+
const accountNativeSlugs = accountSelectors.length > 0
|
|
1499
|
+
? [...new Set([...accountNativeSlugsBySelector.values()].flatMap(slugs => [...slugs]))]
|
|
1500
|
+
: [];
|
|
1460
1501
|
// Unknown account-native ids have no safe bare/global identity. They are only projected through
|
|
1461
1502
|
// the selector map above; the no-selector catalog remains the static native/API-key surface.
|
|
1462
1503
|
const observedNativeSlugs: string[] = [];
|
|
@@ -1510,7 +1551,7 @@ function writeRetainedCatalogSync({
|
|
|
1510
1551
|
const accountBoundEntries = includeAccountBoundNativeOpenAi && accountSelectors.length > 0
|
|
1511
1552
|
? buildCatalogEntriesFromObservedState({
|
|
1512
1553
|
template: template ? JSON.parse(JSON.stringify(template)) : null,
|
|
1513
|
-
gptSlugs:
|
|
1554
|
+
gptSlugs: availableAccountNativeSlugs,
|
|
1514
1555
|
goModels: [],
|
|
1515
1556
|
featured,
|
|
1516
1557
|
wsEnabled,
|
|
@@ -1550,7 +1591,7 @@ function writeRetainedCatalogSync({
|
|
|
1550
1591
|
openaiContextCap,
|
|
1551
1592
|
policy: {
|
|
1552
1593
|
...CANONICAL_NATIVE_CATALOG_CONTENT_POLICY,
|
|
1553
|
-
nativeBackfillSlugs: [...
|
|
1594
|
+
nativeBackfillSlugs: [...availableBareNativeSlugs, ...observedNativeSlugs],
|
|
1554
1595
|
warningPolicy: "emit",
|
|
1555
1596
|
},
|
|
1556
1597
|
});
|
|
@@ -1664,10 +1705,13 @@ export async function syncCatalogModels(
|
|
|
1664
1705
|
evidence: retainedCatalogSyncEvidence(config, preflightRead.catalogPath, preflightRead.catalog),
|
|
1665
1706
|
processEvidence: retainedCatalogProcessEvidence(),
|
|
1666
1707
|
};
|
|
1667
|
-
const goModels = await
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1708
|
+
const [goModels, modelEntitlements] = await Promise.all([
|
|
1709
|
+
gatherRoutedModels(config, {
|
|
1710
|
+
comboOmissions,
|
|
1711
|
+
providerModelOutcomes,
|
|
1712
|
+
}),
|
|
1713
|
+
resolveCodexModelEntitlements(config),
|
|
1714
|
+
]);
|
|
1671
1715
|
const committed = withCatalogWriteSerialization(owningCodexHome, permit => {
|
|
1672
1716
|
// Desired state can flip OFF during the provider await above. The catalog
|
|
1673
1717
|
// evidence revalidation below cannot see that — intent lives in our config,
|
|
@@ -1687,6 +1731,7 @@ export async function syncCatalogModels(
|
|
|
1687
1731
|
}
|
|
1688
1732
|
const current = revalidateRetainedCatalogSync(config, prepared);
|
|
1689
1733
|
if (current === null) return null;
|
|
1734
|
+
if (!isCodexModelEntitlementSnapshotCurrent(modelEntitlements)) return null;
|
|
1690
1735
|
return writeRetainedCatalogSync({
|
|
1691
1736
|
config,
|
|
1692
1737
|
goModels,
|
|
@@ -1695,6 +1740,7 @@ export async function syncCatalogModels(
|
|
|
1695
1740
|
read: current,
|
|
1696
1741
|
permit,
|
|
1697
1742
|
owningCodexHome,
|
|
1743
|
+
modelEntitlements,
|
|
1698
1744
|
});
|
|
1699
1745
|
});
|
|
1700
1746
|
if (committed.kind === "completed" && committed.value !== null) return committed.value;
|