@bitkyc08/opencodex 2.11.0 → 2.11.1
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-Bk-PN-70.css → index-BA1dgo4q.css} +1 -1
- package/gui/dist/assets/index-DDZpgzKk.js +70 -0
- package/gui/dist/index.html +2 -2
- package/package.json +6 -4
- package/src/adapters/anthropic.ts +10 -2
- package/src/adapters/cursor/native-exec-common.ts +6 -2
- package/src/adapters/google-antigravity-replay.ts +3 -2
- package/src/adapters/google-antigravity-wire.ts +38 -6
- package/src/adapters/google.ts +57 -10
- package/src/adapters/openai-chat.ts +19 -2
- package/src/claude/model-info.ts +1 -1
- package/src/cli/account-auth.ts +3 -1
- package/src/cli/account-catalog-refresh.ts +14 -0
- package/src/cli/account-extended.ts +15 -1
- package/src/cli/codex-shim-readiness.ts +69 -0
- package/src/cli/combo.ts +5 -0
- package/src/cli/config-command.ts +33 -4
- package/src/cli/doctor.ts +21 -0
- package/src/cli/export-command.ts +6 -6
- package/src/cli/help.ts +6 -6
- package/src/cli/index.ts +22 -5
- package/src/cli/models-runtime.ts +13 -1
- package/src/cli/provider.ts +7 -0
- package/src/clients/config-export.ts +67 -5
- package/src/codex/account-lifecycle.ts +99 -10
- package/src/codex/auth-api.ts +222 -34
- package/src/codex/catalog/account-models.ts +9 -4
- package/src/codex/catalog/aggregation.ts +41 -8
- package/src/codex/catalog/bundled.ts +54 -22
- package/src/codex/catalog/effort.ts +47 -20
- package/src/codex/catalog/kinds.ts +2 -0
- package/src/codex/catalog/metadata.ts +70 -18
- package/src/codex/catalog/native-models.ts +7 -0
- package/src/codex/catalog/parsing.ts +29 -11
- package/src/codex/catalog/provider-fetch.ts +335 -50
- package/src/codex/catalog/sync.ts +588 -126
- package/src/codex/catalog-refresh-status.ts +87 -0
- package/src/codex/catalog-write-serialization.ts +2 -1
- package/src/codex/catalog.ts +4 -3
- package/src/codex/convergence-types.ts +1 -1
- package/src/codex/convergence.ts +190 -52
- package/src/codex/custom-model-catalog-migration.ts +176 -0
- package/src/codex/features.ts +79 -1
- package/src/codex/history-job.ts +81 -1
- package/src/codex/history-lock.ts +2 -1
- package/src/codex/history-provider.ts +4 -3
- package/src/codex/inject.ts +56 -12
- package/src/codex/model-cache.ts +50 -10
- package/src/codex/transition-state.ts +10 -2
- package/src/codex/user-identity.ts +110 -2
- package/src/combos/index.ts +3 -0
- package/src/combos/types.ts +75 -9
- package/src/config.ts +26 -12
- package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +12 -10
- package/src/integrations/registry.ts +7 -0
- package/src/integrations/writer.ts +1 -1
- package/src/lib/pinned-http.ts +40 -9
- package/src/lib/process-control.ts +4 -1
- package/src/lib/provider-outbound.ts +42 -9
- package/src/oauth/cursor.ts +25 -5
- package/src/oauth/index.ts +59 -45
- package/src/providers/antigravity-models.ts +115 -3
- package/src/providers/context-cap.ts +13 -5
- package/src/providers/model-discovery-limits.ts +16 -0
- package/src/providers/model-discovery.ts +14 -15
- package/src/providers/provider-id-rewrite.ts +29 -0
- package/src/providers/quota.ts +93 -19
- package/src/providers/registry.ts +16 -5
- package/src/providers/slug-codec.ts +13 -6
- package/src/reasoning-effort.ts +22 -0
- package/src/router.ts +0 -20
- package/src/routing/history/indexer.ts +54 -39
- package/src/routing/quota.ts +77 -56
- package/src/server/index.ts +32 -7
- package/src/server/management/combo-routes.ts +43 -19
- package/src/server/management/config-routes.ts +115 -20
- package/src/server/management/model-routes.ts +10 -7
- package/src/server/management/model-rows.ts +6 -2
- package/src/server/management/oauth-account-routes.ts +12 -0
- package/src/server/management/provider-routes.ts +114 -40
- package/src/server/management/routing-profile-routes.ts +0 -22
- package/src/server/management-api.ts +7 -25
- package/src/server/responses/core.ts +3 -3
- package/src/server/responses/policy-fallback.ts +152 -0
- package/src/server/responses.ts +3 -2
- package/src/types.ts +15 -2
- package/src/usage/cost.ts +0 -0
- package/src/vision/describe.ts +3 -1
- package/src/vision/index.ts +17 -6
- package/src/vision/reasoning.ts +55 -0
- package/src/web-search/parse.ts +15 -3
- package/gui/dist/assets/index-BynIEIV-.js +0 -70
package/src/codex/auth-api.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
ConfigMutationLockError,
|
|
3
|
+
loadConfig,
|
|
4
|
+
mutatePersistedConfig,
|
|
5
|
+
saveConfigPreservingClaudeCode,
|
|
6
|
+
withConfigMutationLockSync,
|
|
7
|
+
} from "../config";
|
|
2
8
|
import { withCodexAccountLogLabel } from "./account-label";
|
|
3
9
|
import {
|
|
4
10
|
getCodexAccountCredential,
|
|
@@ -14,6 +20,14 @@ import {
|
|
|
14
20
|
TokenRefreshError,
|
|
15
21
|
} from "./account-store";
|
|
16
22
|
import { deleteCodexAccount, reconcileMainCodexAccountRuntimeState } from "./account-lifecycle";
|
|
23
|
+
import {
|
|
24
|
+
appendDefaultCodexAccountNamespace,
|
|
25
|
+
codexAccountPickerEnabled,
|
|
26
|
+
} from "./account-namespaces";
|
|
27
|
+
import {
|
|
28
|
+
catalogRefreshIsPending,
|
|
29
|
+
normalizeCatalogDisposition,
|
|
30
|
+
} from "./catalog-refresh-status";
|
|
17
31
|
import { isCodexAccountPaused, setCodexAccountPaused } from "./account-pause";
|
|
18
32
|
import {
|
|
19
33
|
clearCodexAccountPin,
|
|
@@ -85,7 +99,8 @@ export { clearMainAccountInfoCache } from "./main-account-cache";
|
|
|
85
99
|
import { maskEmail } from "../lib/privacy";
|
|
86
100
|
import { CodexWarmupError, codexWarmupFailureReason, warmCodexAccount } from "./warmup";
|
|
87
101
|
export { maskEmail } from "../lib/privacy";
|
|
88
|
-
import type { CodexAccount, OcxConfig } from "../types";
|
|
102
|
+
import type { CodexAccount, CodexAccountCredentials, OcxConfig } from "../types";
|
|
103
|
+
import type { CatalogDisposition } from "./convergence-types";
|
|
89
104
|
import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
|
|
90
105
|
import { providerCodexAccountMode } from "../providers/registry";
|
|
91
106
|
import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
@@ -131,10 +146,22 @@ function nativeMainProfileBusyResponse(): Response {
|
|
|
131
146
|
}
|
|
132
147
|
|
|
133
148
|
const MANUAL_IMPORT_ENV = "OPENCODEX_ENABLE_UNVERIFIED_CODEX_IMPORT";
|
|
149
|
+
const CODEX_CREDENTIAL_PERSISTENCE_ERROR = "Account was saved, but credential setup did not complete. Reauthenticate or remove the account.";
|
|
150
|
+
const CODEX_CREDENTIAL_PERSISTENCE_CODE = "codex_credential_persistence_failed";
|
|
134
151
|
|
|
135
152
|
const MAX_CODEX_LOGIN_STATE_ROWS = 32;
|
|
136
153
|
const CODEX_LOGIN_TERMINAL_TTL_MS = 300_000;
|
|
137
|
-
interface CodexLoginStateRow {
|
|
154
|
+
interface CodexLoginStateRow {
|
|
155
|
+
status: string;
|
|
156
|
+
startedAt: number;
|
|
157
|
+
accountId?: string;
|
|
158
|
+
email?: string;
|
|
159
|
+
error?: string;
|
|
160
|
+
code?: string;
|
|
161
|
+
needsReauth?: boolean;
|
|
162
|
+
catalogRefreshPending?: boolean;
|
|
163
|
+
doneAt?: number;
|
|
164
|
+
}
|
|
138
165
|
const codexAuthLoginState = new Map<string, CodexLoginStateRow>();
|
|
139
166
|
export class CodexLoginStateBusyError extends ResourceAdmissionError {
|
|
140
167
|
constructor() { super("codex_login_state_rows", MAX_CODEX_LOGIN_STATE_ROWS); this.name = "CodexLoginStateBusyError"; }
|
|
@@ -391,6 +418,100 @@ function saveRuntimeConfig(sourceConfig: OcxConfig, nextConfig: OcxConfig): void
|
|
|
391
418
|
Object.assign(sourceConfig, nextConfig);
|
|
392
419
|
}
|
|
393
420
|
|
|
421
|
+
interface StagedNewCodexAccountState {
|
|
422
|
+
credential: CodexAccountCredentials;
|
|
423
|
+
validatedAt: number;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
type PersistNewCodexAccountOutcome =
|
|
427
|
+
| { status: "committed"; pickerVisibilityChanged: boolean }
|
|
428
|
+
| { status: "publication-failed"; pickerVisibilityChanged: boolean };
|
|
429
|
+
|
|
430
|
+
function codexCredentialPersistenceFailure(accountId: string, catalogRefreshPending: boolean) {
|
|
431
|
+
return {
|
|
432
|
+
error: CODEX_CREDENTIAL_PERSISTENCE_ERROR,
|
|
433
|
+
code: CODEX_CREDENTIAL_PERSISTENCE_CODE,
|
|
434
|
+
accountId,
|
|
435
|
+
needsReauth: true as const,
|
|
436
|
+
...(catalogRefreshPending ? { catalogRefreshPending: true as const } : {}),
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** Persist config before publishing secret or runtime state under the shared mutation coordinator. */
|
|
441
|
+
function persistNewCodexAccount(
|
|
442
|
+
sourceConfig: OcxConfig,
|
|
443
|
+
runtimeConfig: OcxConfig,
|
|
444
|
+
addedAccount: CodexAccount,
|
|
445
|
+
staged: StagedNewCodexAccountState,
|
|
446
|
+
): PersistNewCodexAccountOutcome {
|
|
447
|
+
return withConfigMutationLockSync(() => {
|
|
448
|
+
const previousConfig = { ...runtimeConfig };
|
|
449
|
+
let pickerVisibilityChanged: boolean;
|
|
450
|
+
try {
|
|
451
|
+
const accounts = [...(runtimeConfig.codexAccounts ?? [])];
|
|
452
|
+
const retainedPickerBindingRestored = codexAccountPickerEnabled(runtimeConfig)
|
|
453
|
+
&& Object.values(runtimeConfig.codexAccountNamespaces ?? {}).includes(addedAccount.id);
|
|
454
|
+
accounts.push(addedAccount);
|
|
455
|
+
runtimeConfig.codexAccounts = accounts;
|
|
456
|
+
|
|
457
|
+
// Presence of the explicit flag distinguishes a dashboard-managed map from
|
|
458
|
+
// a hand-authored legacy map. Preserve manual maps exactly.
|
|
459
|
+
const tracksPickerNamespaces = runtimeConfig.codexAccountPickerEnabled !== undefined;
|
|
460
|
+
if (tracksPickerNamespaces && runtimeConfig.codexAccountNamespaces) {
|
|
461
|
+
runtimeConfig.codexAccountNamespaces = { ...runtimeConfig.codexAccountNamespaces };
|
|
462
|
+
}
|
|
463
|
+
const namespaceAdded = tracksPickerNamespaces
|
|
464
|
+
&& appendDefaultCodexAccountNamespace(runtimeConfig, addedAccount);
|
|
465
|
+
pickerVisibilityChanged = namespaceAdded || retainedPickerBindingRestored;
|
|
466
|
+
saveRuntimeConfig(sourceConfig, runtimeConfig);
|
|
467
|
+
} catch (error) {
|
|
468
|
+
for (const key of Object.keys(runtimeConfig) as Array<keyof OcxConfig>) {
|
|
469
|
+
delete runtimeConfig[key];
|
|
470
|
+
}
|
|
471
|
+
Object.assign(runtimeConfig, previousConfig);
|
|
472
|
+
throw error;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
try {
|
|
476
|
+
saveCodexAccountCredential(addedAccount.id, staged.credential);
|
|
477
|
+
markCodexAccountValidated(addedAccount.id, staged.validatedAt);
|
|
478
|
+
clearAccountNeedsReauth(addedAccount.id);
|
|
479
|
+
} catch {
|
|
480
|
+
// Config is already durable. Return the failure outcome through the coordinator so its
|
|
481
|
+
// generation commit is not rolled back while config.json remains changed.
|
|
482
|
+
return { status: "publication-failed" as const, pickerVisibilityChanged };
|
|
483
|
+
}
|
|
484
|
+
return { status: "committed" as const, pickerVisibilityChanged };
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** Bounded catalog-convergence callback supplied by the management dispatcher. */
|
|
489
|
+
export type CodexAuthCatalogConvergence = () => Promise<CatalogDisposition>;
|
|
490
|
+
|
|
491
|
+
interface AccountNamespaceCatalogRefresh {
|
|
492
|
+
catalogRefreshPending: boolean;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/** Collapse post-persistence convergence into the one public recovery bit. */
|
|
496
|
+
async function convergeAccountNamespaceCatalog(
|
|
497
|
+
config: OcxConfig,
|
|
498
|
+
changed: boolean,
|
|
499
|
+
convergeCodexCatalog?: CodexAuthCatalogConvergence,
|
|
500
|
+
): Promise<AccountNamespaceCatalogRefresh> {
|
|
501
|
+
if (!changed || !codexAccountPickerEnabled(config)) {
|
|
502
|
+
return { catalogRefreshPending: false };
|
|
503
|
+
}
|
|
504
|
+
if (!convergeCodexCatalog) return { catalogRefreshPending: true };
|
|
505
|
+
|
|
506
|
+
try {
|
|
507
|
+
const catalogRefresh = normalizeCatalogDisposition(await convergeCodexCatalog());
|
|
508
|
+
if (!catalogRefresh) return { catalogRefreshPending: true };
|
|
509
|
+
return { catalogRefreshPending: catalogRefreshIsPending(catalogRefresh) };
|
|
510
|
+
} catch {
|
|
511
|
+
return { catalogRefreshPending: true };
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
394
515
|
async function mapWithConcurrency<T, R>(
|
|
395
516
|
items: T[],
|
|
396
517
|
concurrency: number,
|
|
@@ -1202,6 +1323,7 @@ export async function handleCodexAuthAPI(
|
|
|
1202
1323
|
req: Request,
|
|
1203
1324
|
url: URL,
|
|
1204
1325
|
config: OcxConfig,
|
|
1326
|
+
convergeCodexCatalog?: CodexAuthCatalogConvergence,
|
|
1205
1327
|
): Promise<Response | null> {
|
|
1206
1328
|
|
|
1207
1329
|
if (url.pathname === "/api/codex-auth/accounts" && req.method === "GET") {
|
|
@@ -1240,20 +1362,38 @@ export async function handleCodexAuthAPI(
|
|
|
1240
1362
|
const latestConfig = getRuntimeConfig(config);
|
|
1241
1363
|
const commitConflict = codexAccountPersistenceConflict(latestConfig, body.id, "create");
|
|
1242
1364
|
if (commitConflict) return jsonResponse({ error: commitConflict }, 400);
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1365
|
+
const addedAccount = withCodexAccountLogLabel(
|
|
1366
|
+
{ id: body.id, email: body.email, plan: body.plan, isMain: false },
|
|
1367
|
+
latestConfig.codexAccounts ?? [],
|
|
1368
|
+
);
|
|
1369
|
+
const persistence = persistNewCodexAccount(
|
|
1370
|
+
config,
|
|
1371
|
+
latestConfig,
|
|
1372
|
+
addedAccount,
|
|
1373
|
+
{
|
|
1374
|
+
credential: {
|
|
1375
|
+
accessToken: body.accessToken,
|
|
1376
|
+
refreshToken: body.refreshToken,
|
|
1377
|
+
expiresAt: exp,
|
|
1378
|
+
chatgptAccountId: derivedAccountId,
|
|
1379
|
+
},
|
|
1380
|
+
validatedAt: warmup.validatedAt,
|
|
1381
|
+
},
|
|
1382
|
+
);
|
|
1255
1383
|
reconcileLiveStateStores();
|
|
1256
|
-
|
|
1384
|
+
if (persistence.status === "publication-failed") markAccountNeedsReauth(body.id);
|
|
1385
|
+
const catalogRefresh = await convergeAccountNamespaceCatalog(
|
|
1386
|
+
latestConfig,
|
|
1387
|
+
persistence.pickerVisibilityChanged,
|
|
1388
|
+
convergeCodexCatalog,
|
|
1389
|
+
);
|
|
1390
|
+
if (persistence.status === "publication-failed") {
|
|
1391
|
+
return jsonResponse({
|
|
1392
|
+
ok: false,
|
|
1393
|
+
...codexCredentialPersistenceFailure(body.id, catalogRefresh.catalogRefreshPending === true),
|
|
1394
|
+
}, 500);
|
|
1395
|
+
}
|
|
1396
|
+
return jsonResponse({ ok: true, ...catalogRefresh });
|
|
1257
1397
|
}
|
|
1258
1398
|
|
|
1259
1399
|
if (url.pathname === "/api/codex-auth/accounts" && req.method === "DELETE") {
|
|
@@ -1265,10 +1405,15 @@ export async function handleCodexAuthAPI(
|
|
|
1265
1405
|
if (!isValidCodexAccountId(id) && !isLegacyPoolAccount) {
|
|
1266
1406
|
return jsonResponse({ error: "Invalid account id format" }, 400);
|
|
1267
1407
|
}
|
|
1268
|
-
deleteCodexAccount(runtimeConfig, id);
|
|
1408
|
+
const pickerVisibilityChanged = deleteCodexAccount(runtimeConfig, id);
|
|
1269
1409
|
saveRuntimeConfig(config, runtimeConfig);
|
|
1270
1410
|
reconcileLiveStateStores();
|
|
1271
|
-
|
|
1411
|
+
const catalogRefresh = await convergeAccountNamespaceCatalog(
|
|
1412
|
+
runtimeConfig,
|
|
1413
|
+
pickerVisibilityChanged,
|
|
1414
|
+
convergeCodexCatalog,
|
|
1415
|
+
);
|
|
1416
|
+
return jsonResponse({ ok: true, ...catalogRefresh });
|
|
1272
1417
|
}
|
|
1273
1418
|
|
|
1274
1419
|
if (url.pathname === "/api/codex-auth/accounts/alias" && req.method === "PUT") {
|
|
@@ -1761,6 +1906,8 @@ export async function handleCodexAuthAPI(
|
|
|
1761
1906
|
const latestConfig = getRuntimeConfig(config);
|
|
1762
1907
|
const accounts = latestConfig.codexAccounts ?? [];
|
|
1763
1908
|
const existingIdx = accounts.findIndex(account => account.id === accountId);
|
|
1909
|
+
let pickerVisibilityChanged = false;
|
|
1910
|
+
let newAccountPersistence: PersistNewCodexAccountOutcome | null = null;
|
|
1764
1911
|
const commitConflict = codexAccountPersistenceConflict(
|
|
1765
1912
|
latestConfig,
|
|
1766
1913
|
accountId,
|
|
@@ -1776,22 +1923,21 @@ export async function handleCodexAuthAPI(
|
|
|
1776
1923
|
break;
|
|
1777
1924
|
}
|
|
1778
1925
|
|
|
1779
|
-
|
|
1926
|
+
const credential: CodexAccountCredentials = {
|
|
1780
1927
|
accessToken: cred.access,
|
|
1781
1928
|
refreshToken: cred.refresh,
|
|
1782
1929
|
expiresAt: cred.expires,
|
|
1783
1930
|
chatgptAccountId: oauthAccountId,
|
|
1784
|
-
}
|
|
1785
|
-
// A successful reauthentication replaces the credential generation. Do not let a
|
|
1786
|
-
// failed optional WHAM probe make the replacement inherit quota from the old record.
|
|
1787
|
-
if (reauth) clearAccountQuota(accountId);
|
|
1788
|
-
markCodexAccountValidated(accountId, warmup.validatedAt);
|
|
1789
|
-
clearAccountNeedsReauth(accountId);
|
|
1790
|
-
if (quota) {
|
|
1791
|
-
setAccountQuotaFromParsed(accountId, quota);
|
|
1792
|
-
}
|
|
1931
|
+
};
|
|
1793
1932
|
|
|
1794
1933
|
if (existingIdx >= 0) {
|
|
1934
|
+
saveCodexAccountCredential(accountId, credential);
|
|
1935
|
+
// A successful reauthentication replaces the credential generation. Do not let a
|
|
1936
|
+
// failed optional WHAM probe make the replacement inherit quota from the old record.
|
|
1937
|
+
if (reauth) clearAccountQuota(accountId);
|
|
1938
|
+
markCodexAccountValidated(accountId, warmup.validatedAt);
|
|
1939
|
+
clearAccountNeedsReauth(accountId);
|
|
1940
|
+
if (quota) setAccountQuotaFromParsed(accountId, quota);
|
|
1795
1941
|
// Keep the pool id stable; refresh display metadata after a successful login/reauth.
|
|
1796
1942
|
accounts[existingIdx] = withCodexAccountLogLabel({
|
|
1797
1943
|
...accounts[existingIdx],
|
|
@@ -1802,13 +1948,49 @@ export async function handleCodexAuthAPI(
|
|
|
1802
1948
|
latestConfig.codexAccounts = accounts;
|
|
1803
1949
|
saveRuntimeConfig(config, latestConfig);
|
|
1804
1950
|
} else {
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1951
|
+
const addedAccount = withCodexAccountLogLabel({ id: accountId, email, plan, isMain: false }, accounts);
|
|
1952
|
+
newAccountPersistence = persistNewCodexAccount(
|
|
1953
|
+
config,
|
|
1954
|
+
latestConfig,
|
|
1955
|
+
addedAccount,
|
|
1956
|
+
{
|
|
1957
|
+
credential,
|
|
1958
|
+
validatedAt: warmup.validatedAt,
|
|
1959
|
+
},
|
|
1960
|
+
);
|
|
1961
|
+
pickerVisibilityChanged = newAccountPersistence.pickerVisibilityChanged;
|
|
1808
1962
|
}
|
|
1809
1963
|
reconcileLiveStateStores();
|
|
1810
|
-
|
|
1811
|
-
|
|
1964
|
+
if (newAccountPersistence?.status === "publication-failed") {
|
|
1965
|
+
markAccountNeedsReauth(accountId);
|
|
1966
|
+
}
|
|
1967
|
+
// A new quota row is generation-gated by live account ownership. Reconcile the
|
|
1968
|
+
// durable config owner first so a partial prior sweep cannot reject this write.
|
|
1969
|
+
if (newAccountPersistence?.status === "committed" && quota) {
|
|
1970
|
+
setAccountQuotaFromParsed(accountId, quota);
|
|
1971
|
+
}
|
|
1972
|
+
const { catalogRefreshPending } = await convergeAccountNamespaceCatalog(
|
|
1973
|
+
latestConfig,
|
|
1974
|
+
pickerVisibilityChanged,
|
|
1975
|
+
convergeCodexCatalog,
|
|
1976
|
+
);
|
|
1977
|
+
if (newAccountPersistence?.status === "publication-failed") {
|
|
1978
|
+
setCodexLoginState(flowId, {
|
|
1979
|
+
status: "error",
|
|
1980
|
+
...codexCredentialPersistenceFailure(accountId, catalogRefreshPending),
|
|
1981
|
+
doneAt: Date.now(),
|
|
1982
|
+
});
|
|
1983
|
+
completed = true;
|
|
1984
|
+
} else {
|
|
1985
|
+
setCodexLoginState(flowId, {
|
|
1986
|
+
status: "done",
|
|
1987
|
+
accountId,
|
|
1988
|
+
email,
|
|
1989
|
+
...(catalogRefreshPending ? { catalogRefreshPending: true } : {}),
|
|
1990
|
+
doneAt: Date.now(),
|
|
1991
|
+
});
|
|
1992
|
+
completed = true;
|
|
1993
|
+
}
|
|
1812
1994
|
}
|
|
1813
1995
|
break;
|
|
1814
1996
|
}
|
|
@@ -1894,7 +2076,13 @@ export async function handleCodexAuthAPI(
|
|
|
1894
2076
|
const reauthStatus = url.searchParams.get("reauth") === "1";
|
|
1895
2077
|
if (flowId) {
|
|
1896
2078
|
const st = codexAuthLoginState.get(flowId);
|
|
1897
|
-
if (
|
|
2079
|
+
if (
|
|
2080
|
+
!st
|
|
2081
|
+
&& accountId
|
|
2082
|
+
&& !reauthStatus
|
|
2083
|
+
&& !isAccountNeedsReauth(accountId)
|
|
2084
|
+
&& getCodexAccountCredential(accountId)
|
|
2085
|
+
) {
|
|
1898
2086
|
return jsonResponse({ status: "done", accountId });
|
|
1899
2087
|
}
|
|
1900
2088
|
return jsonResponse(st ? { ...st, email: maskEmail(st.email) ?? undefined } : { status: "expired" });
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { OcxConfig } from "../../types";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
codexAccountNamespaceEntries,
|
|
4
|
+
codexAccountPickerEnabled,
|
|
5
|
+
isMainCodexAccountTarget,
|
|
6
|
+
} from "../account-namespaces";
|
|
3
7
|
import type { RawEntry } from "./parsing";
|
|
4
8
|
|
|
5
9
|
/** Stable nonsemantic marker used to distinguish generated rows from provider-owned rows. */
|
|
@@ -14,14 +18,15 @@ export const CODEX_ACCOUNT_BOUND_CATALOG_KIND = "account-selector-v1";
|
|
|
14
18
|
* namespace validation. Only those public keys leave this boundary; private account ids do not.
|
|
15
19
|
*/
|
|
16
20
|
export function visibleCodexAccountSelectors(
|
|
17
|
-
config: Pick<OcxConfig, "codexAccounts" | "codexAccountNamespaces">,
|
|
21
|
+
config: Pick<OcxConfig, "codexAccounts" | "codexAccountNamespaces" | "codexAccountPickerEnabled">,
|
|
18
22
|
): string[] {
|
|
23
|
+
if (!codexAccountPickerEnabled(config)) return [];
|
|
19
24
|
const storedPoolAccounts = new Set(
|
|
20
25
|
(config.codexAccounts ?? [])
|
|
21
26
|
.filter(account => !account.isMain)
|
|
22
27
|
.map(account => account.id),
|
|
23
28
|
);
|
|
24
|
-
return
|
|
29
|
+
return codexAccountNamespaceEntries(config)
|
|
25
30
|
.filter(([, accountId]) =>
|
|
26
31
|
isMainCodexAccountTarget(accountId) || storedPoolAccounts.has(accountId)
|
|
27
32
|
)
|
|
@@ -53,7 +58,7 @@ export function trustedAccountBoundNativeCatalogSlug(entry: RawEntry): string |
|
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
export function accountBoundNativeModelSlugs(
|
|
56
|
-
config: Pick<OcxConfig, "codexAccounts" | "codexAccountNamespaces">,
|
|
61
|
+
config: Pick<OcxConfig, "codexAccounts" | "codexAccountNamespaces" | "codexAccountPickerEnabled">,
|
|
57
62
|
nativeSlugs: Iterable<string>,
|
|
58
63
|
): string[] {
|
|
59
64
|
const natives = [...nativeSlugs];
|
|
@@ -9,17 +9,18 @@ import { buildModelsRequest, resolveModelsAuthToken } from "../../oauth";
|
|
|
9
9
|
import type { OcxConfig, OcxProviderConfig } from "../../types";
|
|
10
10
|
import { modelInList } from "../../types";
|
|
11
11
|
import { CODEX_REASONING_LEVELS, codexEffortRank, configuredReasoningEfforts, modelRecordValue, sanitizeCodexReasoningEfforts } from "../../reasoning-effort";
|
|
12
|
-
import {
|
|
12
|
+
import { getModelMetadata, getModelMetadataCaseInsensitive, listModelMetadata, resolveMetadataProvider } from "../../generated/model-metadata";
|
|
13
13
|
import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../../providers/derive";
|
|
14
14
|
import { getProviderRegistryEntry } from "../../providers/registry";
|
|
15
15
|
import { applyProviderContextCap, providerContextCap } from "../../providers/context-cap";
|
|
16
|
-
import { routedSlug, slugEquals, slugsEquivalent } from "../../providers/slug-codec";
|
|
16
|
+
import { routedSlug, slugEquals, slugEquivalenceKey, slugsEquivalent } from "../../providers/slug-codec";
|
|
17
17
|
import { CODEX_GPT5_IDENTITY_LINE } from "../../adapters/identity";
|
|
18
18
|
import { filterCursorConfiguredModelsByLiveDiscovery } from "../../adapters/cursor/discovery";
|
|
19
19
|
import { fetchCursorUsableModels } from "../../adapters/cursor/live-models";
|
|
20
20
|
import { isCanonicalOpenAiForwardProvider, OPENAI_API_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
|
|
21
21
|
import {
|
|
22
22
|
COMBO_NAMESPACE,
|
|
23
|
+
comboDisabledModelSelectors,
|
|
23
24
|
comboModelId,
|
|
24
25
|
getCombo,
|
|
25
26
|
listComboIds,
|
|
@@ -131,10 +132,23 @@ export function deriveComboCatalogModel(
|
|
|
131
132
|
const inputModalities = intersectStrings(
|
|
132
133
|
members.map(member => member.inputModalities ?? ["text"]),
|
|
133
134
|
);
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
135
|
+
// Unknown ladders (`undefined`) are wildcards for catalog derivation — same
|
|
136
|
+
// boundary as the GUI picker. An explicit empty ladder still constrains.
|
|
137
|
+
const advertisedLadders = members
|
|
138
|
+
.map(member => member.reasoningEfforts)
|
|
139
|
+
.filter((ladder): ladder is string[] => ladder !== undefined);
|
|
140
|
+
const reasoningEfforts = advertisedLadders.length === 0
|
|
141
|
+
? []
|
|
142
|
+
: intersectStrings(advertisedLadders);
|
|
137
143
|
const contextWindow = Math.min(...members.map(member => member.contextWindow!));
|
|
144
|
+
const limitingMembers = members.filter(member => member.contextWindow === contextWindow);
|
|
145
|
+
const hasLimitingContextCapMetadata = limitingMembers.some(
|
|
146
|
+
member => typeof member.contextCapped === "boolean",
|
|
147
|
+
);
|
|
148
|
+
// A combo is cap-limited only when every member defining its effective minimum was
|
|
149
|
+
// itself reduced by a provider cap. An uncapped member at the same minimum means the
|
|
150
|
+
// combo would have the same window even without the cap.
|
|
151
|
+
const contextCapped = limitingMembers.every(member => member.contextCapped === true);
|
|
138
152
|
const maxInputTokens = Math.min(
|
|
139
153
|
...members.map(member => member.maxInputTokens ?? member.contextWindow!),
|
|
140
154
|
);
|
|
@@ -149,9 +163,12 @@ export function deriveComboCatalogModel(
|
|
|
149
163
|
owned_by: COMBO_NAMESPACE,
|
|
150
164
|
contextWindow,
|
|
151
165
|
maxInputTokens,
|
|
166
|
+
...(hasLimitingContextCapMetadata ? { contextCapped } : {}),
|
|
152
167
|
inputModalities,
|
|
153
168
|
reasoningEfforts,
|
|
154
169
|
...(combo.alias ? { alias: combo.alias } : {}),
|
|
170
|
+
...(combo.nativeAlias ? { nativeAlias: true } : {}),
|
|
171
|
+
...(combo.displayName ? { displayName: combo.displayName } : {}),
|
|
155
172
|
...(defaultReasoningEffort ? { defaultReasoningEffort } : {}),
|
|
156
173
|
...(members.every(member => member.parallelToolCalls === true)
|
|
157
174
|
? { parallelToolCalls: true }
|
|
@@ -252,12 +269,15 @@ export function exactComboCatalogSlugs(
|
|
|
252
269
|
): Set<string> {
|
|
253
270
|
const disabled = new Set(config.disabledModels ?? []);
|
|
254
271
|
return new Set(listComboIds(config).flatMap(id => {
|
|
255
|
-
const
|
|
256
|
-
|
|
272
|
+
const raw = config.combos?.[id];
|
|
273
|
+
const alias = typeof raw?.alias === "string"
|
|
274
|
+
? raw.alias.trim()
|
|
257
275
|
: "";
|
|
258
276
|
const canonical = comboModelId(id);
|
|
259
277
|
const publicSlug = alias || canonical;
|
|
260
|
-
|
|
278
|
+
const comboDisabled = comboDisabledModelSelectors(id, raw ?? {})
|
|
279
|
+
.some(selector => disabled.has(selector));
|
|
280
|
+
return comboDisabled ? [] : [publicSlug];
|
|
261
281
|
}));
|
|
262
282
|
}
|
|
263
283
|
|
|
@@ -282,6 +302,8 @@ export const slugAliasCollisionWarnings = new Set<string>();
|
|
|
282
302
|
|
|
283
303
|
export const comboMasqueradeCollisionWarnings = new Set<string>();
|
|
284
304
|
|
|
305
|
+
export const comboUnrestorableShadowWarnings = new Set<string>();
|
|
306
|
+
|
|
285
307
|
export const accountSelectorShadowCollisionWarnings = new Set<string>();
|
|
286
308
|
let lastWarningReconciledGeneration = 0;
|
|
287
309
|
|
|
@@ -291,11 +313,13 @@ export function reconcileCatalogWarningMemos(generation: number): number {
|
|
|
291
313
|
+ comboCatalogWarningSignatures.size
|
|
292
314
|
+ slugAliasCollisionWarnings.size
|
|
293
315
|
+ comboMasqueradeCollisionWarnings.size
|
|
316
|
+
+ comboUnrestorableShadowWarnings.size
|
|
294
317
|
+ accountSelectorShadowCollisionWarnings.size;
|
|
295
318
|
openAiApiCollisionWarnings.clear();
|
|
296
319
|
comboCatalogWarningSignatures.clear();
|
|
297
320
|
slugAliasCollisionWarnings.clear();
|
|
298
321
|
comboMasqueradeCollisionWarnings.clear();
|
|
322
|
+
comboUnrestorableShadowWarnings.clear();
|
|
299
323
|
accountSelectorShadowCollisionWarnings.clear();
|
|
300
324
|
lastWarningReconciledGeneration = generation;
|
|
301
325
|
return removed;
|
|
@@ -309,6 +333,15 @@ export function warnComboMasqueradeCollisionOnce(slug: string): void {
|
|
|
309
333
|
);
|
|
310
334
|
}
|
|
311
335
|
|
|
336
|
+
export function warnComboUnrestorableShadowOnce(slug: string): void {
|
|
337
|
+
const key = slugEquivalenceKey(slug);
|
|
338
|
+
if (comboUnrestorableShadowWarnings.has(key)) return;
|
|
339
|
+
comboUnrestorableShadowWarnings.add(key);
|
|
340
|
+
console.warn(
|
|
341
|
+
`[opencodex] combo alias collision on "${safeCatalogWarningLabel(slug)}": the existing user-managed or foreign catalog row is retained because no pristine backup can restore it. Rename the combo alias to expose both models.`,
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
312
345
|
/** Warn once when a live provider row loses its reserved slug to an account selector. */
|
|
313
346
|
export function warnAccountSelectorShadowedProviderOnce(slug: string): void {
|
|
314
347
|
if (accountSelectorShadowCollisionWarnings.has(slug)) return;
|
|
@@ -9,7 +9,7 @@ import { buildModelsRequest, resolveModelsAuthToken } from "../../oauth";
|
|
|
9
9
|
import type { OcxConfig, OcxProviderConfig } from "../../types";
|
|
10
10
|
import { modelInList } from "../../types";
|
|
11
11
|
import { CODEX_REASONING_LEVELS, codexEffortRank, configuredReasoningEfforts, modelRecordValue, sanitizeCodexReasoningEfforts } from "../../reasoning-effort";
|
|
12
|
-
import {
|
|
12
|
+
import { getModelMetadata, getModelMetadataCaseInsensitive, listModelMetadata, resolveMetadataProvider } from "../../generated/model-metadata";
|
|
13
13
|
import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../../providers/derive";
|
|
14
14
|
import { getProviderRegistryEntry } from "../../providers/registry";
|
|
15
15
|
import { applyProviderContextCap, providerContextCap } from "../../providers/context-cap";
|
|
@@ -328,6 +328,9 @@ export type CatalogSourceForGather =
|
|
|
328
328
|
| "legacy-backup-fallback"
|
|
329
329
|
| "models-cache-fallback";
|
|
330
330
|
catalog: ReadonlyRawCatalog;
|
|
331
|
+
runtimeSupport:
|
|
332
|
+
| Readonly<{ kind: "available"; catalog: ReadonlyRawCatalog }>
|
|
333
|
+
| Readonly<{ kind: "unavailable" }>;
|
|
331
334
|
processLocal: Readonly<{
|
|
332
335
|
runtime: CatalogGatherProcessLocalObservation;
|
|
333
336
|
bundledCatalog: CatalogGatherProcessLocalObservation;
|
|
@@ -341,6 +344,8 @@ export type CatalogSourceForGather =
|
|
|
341
344
|
}>;
|
|
342
345
|
}>;
|
|
343
346
|
|
|
347
|
+
export type CatalogGatherPathKind = "default" | "custom";
|
|
348
|
+
|
|
344
349
|
const UNUSED_PROCESS_LOCAL = Object.freeze({ state: "unused" as const });
|
|
345
350
|
|
|
346
351
|
function sameRuntimeIdentity(
|
|
@@ -407,46 +412,73 @@ export function peekCodexRuntimeForCatalogGather(
|
|
|
407
412
|
*/
|
|
408
413
|
export function resolveCatalogSourceForGather(
|
|
409
414
|
evidenceSession: CatalogGatherEvidenceSession,
|
|
415
|
+
pathKind: CatalogGatherPathKind,
|
|
410
416
|
): CatalogSourceForGather {
|
|
411
417
|
const bundledMemo = bundledCatalogCache;
|
|
418
|
+
let runtimeSupport:
|
|
419
|
+
| Readonly<{ kind: "available"; catalog: ReadonlyRawCatalog }>
|
|
420
|
+
| Readonly<{ kind: "unavailable" }> = Object.freeze({ kind: "unavailable" });
|
|
421
|
+
let processLocal: Readonly<{
|
|
422
|
+
runtime: CatalogGatherProcessLocalObservation;
|
|
423
|
+
bundledCatalog: CatalogGatherProcessLocalObservation;
|
|
424
|
+
}> = {
|
|
425
|
+
runtime: UNUSED_PROCESS_LOCAL,
|
|
426
|
+
bundledCatalog: UNUSED_PROCESS_LOCAL,
|
|
427
|
+
};
|
|
412
428
|
if (bundledMemo?.value && bundledMemo.expiresAt > Date.now()) {
|
|
413
429
|
const runtime = peekCodexRuntimeForCatalogGather(evidenceSession);
|
|
414
430
|
if (runtime.kind === "available" && bundledMemo.key === bundledRuntimeKey(runtime.runtime)) {
|
|
415
|
-
|
|
431
|
+
runtimeSupport = Object.freeze({
|
|
416
432
|
kind: "available" as const,
|
|
417
|
-
source: "bundled-catalog-template" as const,
|
|
418
433
|
catalog: bundledMemo.value,
|
|
419
|
-
processLocal: {
|
|
420
|
-
runtime: runtime.processLocal,
|
|
421
|
-
bundledCatalog: {
|
|
422
|
-
state: "used" as const,
|
|
423
|
-
epoch: bundledMemo.epoch,
|
|
424
|
-
valueIdentity: bundledMemo.valueIdentity,
|
|
425
|
-
},
|
|
426
|
-
},
|
|
427
434
|
});
|
|
435
|
+
processLocal = {
|
|
436
|
+
runtime: runtime.processLocal,
|
|
437
|
+
bundledCatalog: {
|
|
438
|
+
state: "used" as const,
|
|
439
|
+
epoch: bundledMemo.epoch,
|
|
440
|
+
valueIdentity: bundledMemo.valueIdentity,
|
|
441
|
+
},
|
|
442
|
+
};
|
|
443
|
+
if (pathKind === "default") {
|
|
444
|
+
return cloneAndDeepFreeze({
|
|
445
|
+
kind: "available" as const,
|
|
446
|
+
source: "bundled-catalog-template" as const,
|
|
447
|
+
catalog: bundledMemo.value,
|
|
448
|
+
runtimeSupport,
|
|
449
|
+
processLocal,
|
|
450
|
+
});
|
|
451
|
+
}
|
|
428
452
|
}
|
|
429
453
|
}
|
|
430
454
|
|
|
431
|
-
const roles =
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
455
|
+
const roles = pathKind === "default"
|
|
456
|
+
? [
|
|
457
|
+
"active-catalog-merge",
|
|
458
|
+
"hashed-backup-fallback",
|
|
459
|
+
"legacy-backup-fallback",
|
|
460
|
+
"models-cache-fallback",
|
|
461
|
+
] as const
|
|
462
|
+
: [
|
|
463
|
+
"active-catalog-merge",
|
|
464
|
+
"hashed-backup-fallback",
|
|
465
|
+
"models-cache-fallback",
|
|
466
|
+
] as const;
|
|
437
467
|
for (const role of roles) {
|
|
438
468
|
const bytes = evidenceSession.readSource(role);
|
|
439
469
|
if (bytes === null) continue;
|
|
440
470
|
const catalog = parseCatalogJson(Buffer.from(bytes).toString("utf8"));
|
|
441
|
-
if (!catalog
|
|
471
|
+
if (!catalog) continue;
|
|
472
|
+
// Custom catalogs may intentionally contain only routed rows. Keep their existing source
|
|
473
|
+
// priority (active, then backup/cache) without imposing the default catalog's native-template
|
|
474
|
+
// requirement; a valid active custom file therefore remains authoritative over stale fallbacks.
|
|
475
|
+
if (pathKind === "default" && !findNativeTemplate(catalog)) continue;
|
|
442
476
|
return cloneAndDeepFreeze({
|
|
443
477
|
kind: "available" as const,
|
|
444
478
|
source: role,
|
|
445
479
|
catalog,
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
bundledCatalog: UNUSED_PROCESS_LOCAL,
|
|
449
|
-
},
|
|
480
|
+
runtimeSupport,
|
|
481
|
+
processLocal,
|
|
450
482
|
});
|
|
451
483
|
}
|
|
452
484
|
|