@bitkyc08/opencodex 2.49.0 → 2.50.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/AGENTS_INSTALL.md +9 -1
- package/README.md +3 -0
- package/gui/dist/assets/index-C39tnjXO.js +115 -0
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/claude/inbound.ts +17 -5
- package/src/cli/account-api.ts +18 -3
- package/src/cli/account-auth.ts +8 -1
- package/src/cli/account-extended.ts +2 -1
- package/src/cli/account.ts +1 -0
- package/src/cli/capabilities.ts +15 -1
- package/src/cli/index.ts +5 -1
- package/src/cli/models-runtime.ts +8 -3
- package/src/cli/observe.ts +13 -3
- package/src/clients/config-export/zcode.ts +24 -0
- package/src/codex/account-runtime-state.ts +6 -1
- package/src/codex/account-store.ts +72 -9
- package/src/codex/account-usability.ts +3 -2
- package/src/codex/auth-api.ts +107 -23
- package/src/codex/auth-context.ts +21 -0
- package/src/codex/catalog/parsing.ts +23 -0
- package/src/codex/catalog/provider-fetch.ts +71 -2
- package/src/codex/catalog/sync.ts +14 -0
- package/src/codex/inject.ts +3 -2
- package/src/codex/quota-auto-refresh.ts +6 -1
- package/src/codex/quota.ts +54 -8
- package/src/combos/index.ts +2 -0
- package/src/combos/resolve.ts +52 -0
- package/src/config.ts +58 -0
- package/src/generated/compatibility-version.json +72 -60
- package/src/lib/errors.ts +8 -0
- package/src/lib/privacy.ts +25 -0
- package/src/oauth/health.ts +47 -12
- package/src/oauth/index.ts +46 -8
- package/src/oauth/token-guardian.ts +32 -6
- package/src/providers/google-ai-studio-model-discovery.ts +74 -0
- package/src/providers/opencode-zen-rate-limit.ts +75 -0
- package/src/providers/quota.ts +15 -0
- package/src/providers/registry.ts +1 -1
- package/src/server/auth-cors.ts +6 -0
- package/src/server/chat-completions.ts +4 -4
- package/src/server/chat-native.ts +10 -1
- package/src/server/claude-messages.ts +5 -5
- package/src/server/images.ts +2 -2
- package/src/server/index.ts +25 -2
- package/src/server/management/logs-usage-routes.ts +4 -1
- package/src/server/management/model-rows.ts +16 -1
- package/src/server/management/oauth-account-routes.ts +6 -2
- package/src/server/management/provider-routes.ts +9 -2
- package/src/server/management/request-history-routes.ts +4 -2
- package/src/server/management/route-registry.ts +5 -4
- package/src/server/management/shared.ts +66 -3
- package/src/server/management-api.ts +1 -1
- package/src/server/request-decompress.ts +91 -3
- package/src/server/request-log.ts +10 -0
- package/src/server/responses/codex-ws-wire.ts +1 -1
- package/src/server/responses/compact.ts +8 -2
- package/src/server/responses/context-overflow.ts +11 -0
- package/src/server/responses/core.ts +144 -38
- package/src/server/responses/policy-fallback.ts +6 -2
- package/src/server/search.ts +2 -2
- package/src/service.ts +92 -7
- package/src/types/accounts.ts +18 -0
- package/src/types/config.ts +36 -0
- package/src/types/provider.ts +56 -0
- package/src/types.ts +4 -0
- package/src/web-search/ollama-executor.ts +127 -0
- package/src/web-search/passthrough-bridge.ts +761 -0
- package/gui/dist/assets/index-BtyONQrZ.js +0 -115
package/src/codex/auth-api.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
isCodexAccountGenerationLive,
|
|
13
13
|
forceRefreshCodexPoolToken,
|
|
14
14
|
markCodexAccountValidated,
|
|
15
|
+
markCodexAccountValidationFailed,
|
|
15
16
|
readCodexAccountRecord,
|
|
16
17
|
saveCodexAccountCredential,
|
|
17
18
|
CodexCredentialGenerationConflictError,
|
|
@@ -114,7 +115,7 @@ export { clearMainAccountInfoCache } from "./main-account-cache";
|
|
|
114
115
|
import type { CodexQuotaRefreshOutcome } from "./quota-refresh-outcome";
|
|
115
116
|
import { getMainAccountHardLockStatus, type MainAccountHardLockStatus } from "./main-account-hard-lock";
|
|
116
117
|
import { observeMainReserveRevocation } from "./reserve-availability";
|
|
117
|
-
import {
|
|
118
|
+
import { emailMaskingEnabled, projectEmail } from "../lib/privacy";
|
|
118
119
|
import { codexWarmupFailureReason, warmCodexAccount } from "./warmup";
|
|
119
120
|
export { maskEmail } from "../lib/privacy";
|
|
120
121
|
import type { CodexAccount, CodexAccountCredentials, OcxConfig } from "../types";
|
|
@@ -192,6 +193,7 @@ interface CodexLoginStateRow {
|
|
|
192
193
|
code?: string;
|
|
193
194
|
needsReauth?: boolean;
|
|
194
195
|
catalogRefreshPending?: boolean;
|
|
196
|
+
validationPending?: boolean;
|
|
195
197
|
doneAt?: number;
|
|
196
198
|
}
|
|
197
199
|
const codexAuthLoginState = new Map<string, CodexLoginStateRow>();
|
|
@@ -368,6 +370,7 @@ function poolAccountDto(
|
|
|
368
370
|
hasCredential: boolean,
|
|
369
371
|
paused: boolean,
|
|
370
372
|
priority: number,
|
|
373
|
+
maskEmails: boolean,
|
|
371
374
|
): CodexAuthAccountDto {
|
|
372
375
|
const plan = codexPlanValue(account.plan);
|
|
373
376
|
const quota = quotaForPlan(quotaResult.quota, plan);
|
|
@@ -375,7 +378,7 @@ function poolAccountDto(
|
|
|
375
378
|
const health = projectCodexAccountHealth({ accountId: account.id, needsReauth });
|
|
376
379
|
return {
|
|
377
380
|
id: account.id,
|
|
378
|
-
email:
|
|
381
|
+
email: projectEmail(account.email, maskEmails) ?? account.email,
|
|
379
382
|
...(account.alias !== undefined ? { alias: account.alias } : {}),
|
|
380
383
|
...(plan !== undefined ? { plan } : {}),
|
|
381
384
|
logLabel: codexAccountLogLabel(account),
|
|
@@ -383,7 +386,7 @@ function poolAccountDto(
|
|
|
383
386
|
paused,
|
|
384
387
|
priority,
|
|
385
388
|
quota: quota ? { ...quota } : null,
|
|
386
|
-
needsReauth,
|
|
389
|
+
needsReauth: needsReauth || health.status === "reauth_required",
|
|
387
390
|
hasCredential,
|
|
388
391
|
...(quotaResult.quotaProbeSkipped ? { quotaProbeSkipped: true as const } : {}),
|
|
389
392
|
...oauthAccountHealthFields("codex", account.id, health),
|
|
@@ -638,7 +641,7 @@ function saveRuntimeConfig(sourceConfig: OcxConfig, nextConfig: OcxConfig): void
|
|
|
638
641
|
|
|
639
642
|
interface StagedNewCodexAccountState {
|
|
640
643
|
credential: CodexAccountCredentials;
|
|
641
|
-
validatedAt
|
|
644
|
+
validatedAt?: number;
|
|
642
645
|
}
|
|
643
646
|
|
|
644
647
|
type PersistNewCodexAccountOutcome =
|
|
@@ -691,8 +694,10 @@ function persistNewCodexAccount(
|
|
|
691
694
|
}
|
|
692
695
|
|
|
693
696
|
try {
|
|
694
|
-
saveCodexAccountCredential(addedAccount.id, staged.credential
|
|
695
|
-
|
|
697
|
+
const generation = saveCodexAccountCredential(addedAccount.id, staged.credential, {
|
|
698
|
+
validationPending: staged.validatedAt === undefined,
|
|
699
|
+
});
|
|
700
|
+
if (staged.validatedAt !== undefined) markCodexAccountValidated(addedAccount.id, staged.validatedAt, generation);
|
|
696
701
|
clearAccountNeedsReauth(addedAccount.id);
|
|
697
702
|
} catch {
|
|
698
703
|
// Config is already durable. Return the failure outcome through the coordinator so its
|
|
@@ -1101,6 +1106,7 @@ interface PoolQuotaRefreshFlight {
|
|
|
1101
1106
|
superseded?: boolean;
|
|
1102
1107
|
startCredentialGeneration?: number;
|
|
1103
1108
|
resolvedCredentialGeneration?: number;
|
|
1109
|
+
validatePending?: boolean;
|
|
1104
1110
|
};
|
|
1105
1111
|
promise: Promise<PoolQuotaResult>;
|
|
1106
1112
|
}
|
|
@@ -1480,11 +1486,12 @@ async function fetchFreshPoolAccountQuota(
|
|
|
1480
1486
|
}
|
|
1481
1487
|
}
|
|
1482
1488
|
|
|
1483
|
-
async function fetchPoolAccountQuota(
|
|
1489
|
+
export async function fetchPoolAccountQuota(
|
|
1484
1490
|
accountId: string,
|
|
1485
1491
|
forceRefresh = false,
|
|
1486
1492
|
configuredPlan?: string,
|
|
1487
1493
|
getValidToken: typeof getValidCodexToken = getValidCodexToken,
|
|
1494
|
+
validatePending = false,
|
|
1488
1495
|
afterDispatchSequence?: number,
|
|
1489
1496
|
): Promise<PoolQuotaResult> {
|
|
1490
1497
|
const existing = getAccountQuota(accountId);
|
|
@@ -1507,7 +1514,11 @@ async function fetchPoolAccountQuota(
|
|
|
1507
1514
|
&& (afterDispatchSequence === undefined || (flight.state.dispatchSequence ?? 0) > afterDispatchSequence)
|
|
1508
1515
|
&& generation !== undefined && isCodexAccountGenerationLive(accountId, generation);
|
|
1509
1516
|
});
|
|
1510
|
-
if (current)
|
|
1517
|
+
if (current) {
|
|
1518
|
+
// A manual refresh joining a passive read must not lose its validation intent.
|
|
1519
|
+
current.state.validatePending ||= validatePending;
|
|
1520
|
+
return current.promise;
|
|
1521
|
+
}
|
|
1511
1522
|
if (poolQuotaFlightCount() >= MAX_POOL_QUOTA_FLIGHTS) throw new PoolQuotaProbeBusyError();
|
|
1512
1523
|
|
|
1513
1524
|
// A post-reset request must not let an older same-account response overwrite its evidence.
|
|
@@ -1517,6 +1528,7 @@ async function fetchPoolAccountQuota(
|
|
|
1517
1528
|
}
|
|
1518
1529
|
const state: PoolQuotaRefreshFlight["state"] = {
|
|
1519
1530
|
startCredentialGeneration: record?.generation,
|
|
1531
|
+
validatePending,
|
|
1520
1532
|
};
|
|
1521
1533
|
const refresh = fetchFreshPoolAccountQuota(
|
|
1522
1534
|
accountId,
|
|
@@ -1528,18 +1540,54 @@ async function fetchPoolAccountQuota(
|
|
|
1528
1540
|
onDispatch: sequence => { state.dispatchSequence = sequence; },
|
|
1529
1541
|
mayPublish: () => state.superseded !== true,
|
|
1530
1542
|
},
|
|
1531
|
-
)
|
|
1543
|
+
).then(async result => {
|
|
1544
|
+
// A passive flight has consumed its validation decision. Remove it before
|
|
1545
|
+
// promise settlement queues other continuations, so a late explicit caller
|
|
1546
|
+
// starts fresh work instead of setting an intent nobody will read again.
|
|
1547
|
+
if (!state.validatePending) {
|
|
1548
|
+
releaseFlight();
|
|
1549
|
+
return result;
|
|
1550
|
+
}
|
|
1551
|
+
// Only an explicit account-list refresh finishes deferred registration. Passive quota
|
|
1552
|
+
// polls and startup priming remain read-only with respect to inference spending.
|
|
1553
|
+
const generation = result.freshCredentialGeneration;
|
|
1554
|
+
const record = state.validatePending ? readCodexAccountRecord(accountId) : null;
|
|
1555
|
+
if (record?.codexValidationPending && record.credential && record.deletedAt == null
|
|
1556
|
+
&& generation !== undefined && record.generation === generation
|
|
1557
|
+
&& isCompleteCodexQuotaRecoverySnapshot(result.freshQuota ?? null, result.freshPlan ?? configuredPlan)) {
|
|
1558
|
+
try {
|
|
1559
|
+
await warmCodexAccount({
|
|
1560
|
+
accessToken: record.credential.accessToken,
|
|
1561
|
+
chatgptAccountId: record.credential.chatgptAccountId,
|
|
1562
|
+
});
|
|
1563
|
+
markCodexAccountValidated(accountId, Date.now(), generation);
|
|
1564
|
+
clearAccountNeedsReauth(accountId, generation);
|
|
1565
|
+
} catch (error) {
|
|
1566
|
+
// Keep the durable restriction on any failed/partial inference response, even
|
|
1567
|
+
// when WHAM just reported headroom. No raw upstream text enters diagnostics.
|
|
1568
|
+
const reason = codexWarmupFailureReason(error);
|
|
1569
|
+
if (reason === "http_status:401" || reason === "http_status:403") {
|
|
1570
|
+
markCodexAccountValidationFailed(accountId, reason, { expectedGeneration: generation });
|
|
1571
|
+
markAccountNeedsReauth(accountId, captureConfigGeneration(), generation);
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
return result;
|
|
1576
|
+
});
|
|
1532
1577
|
const flight: PoolQuotaRefreshFlight = { state, promise: refresh };
|
|
1533
1578
|
const activeFlights = flights ?? new Set<PoolQuotaRefreshFlight>();
|
|
1534
1579
|
activeFlights.add(flight);
|
|
1535
1580
|
if (!flights) poolQuotaRefreshInFlight.set(accountId, activeFlights);
|
|
1536
|
-
|
|
1537
|
-
return await refresh;
|
|
1538
|
-
} finally {
|
|
1581
|
+
const releaseFlight = () => {
|
|
1539
1582
|
activeFlights.delete(flight);
|
|
1540
1583
|
if (activeFlights.size === 0 && poolQuotaRefreshInFlight.get(accountId) === activeFlights) {
|
|
1541
1584
|
poolQuotaRefreshInFlight.delete(accountId);
|
|
1542
1585
|
}
|
|
1586
|
+
};
|
|
1587
|
+
try {
|
|
1588
|
+
return await refresh;
|
|
1589
|
+
} finally {
|
|
1590
|
+
releaseFlight();
|
|
1543
1591
|
}
|
|
1544
1592
|
}
|
|
1545
1593
|
|
|
@@ -1594,8 +1642,10 @@ async function refreshAfterManualReset(
|
|
|
1594
1642
|
}
|
|
1595
1643
|
return { accessToken: auth.accessToken, chatgptAccountId: auth.chatgptAccountId, generation: auth.poolGeneration };
|
|
1596
1644
|
};
|
|
1645
|
+
// `validatePending` is false here: a manual reset settles cooldown, and finishing deferred
|
|
1646
|
+
// registration stays reserved for an explicit dashboard account-list refresh.
|
|
1597
1647
|
const result = await fetchPoolAccountQuota(accountId, true, account.plan, didReset ? resetToken : getValidCodexToken,
|
|
1598
|
-
didReset ? afterDispatchSequence : undefined);
|
|
1648
|
+
false, didReset ? afterDispatchSequence : undefined);
|
|
1599
1649
|
const record = readCodexAccountRecord(accountId);
|
|
1600
1650
|
const recovered = didReset && record?.credential?.chatgptAccountId === auth.chatgptAccountId
|
|
1601
1651
|
&& (result.quotaProbeAttempted?.dispatchSequence ?? 0) > afterDispatchSequence
|
|
@@ -1876,9 +1926,12 @@ export interface CodexAuthAccountsSnapshot {
|
|
|
1876
1926
|
export async function listCodexAuthAccountsSnapshot(
|
|
1877
1927
|
config: OcxConfig,
|
|
1878
1928
|
forceRefresh = false,
|
|
1929
|
+
options: { validatePending?: boolean } = {},
|
|
1879
1930
|
): Promise<CodexAuthAccountsSnapshot> {
|
|
1880
1931
|
const runtimeConfig = getRuntimeConfig(config);
|
|
1881
1932
|
const poolAccounts = (runtimeConfig.codexAccounts ?? []).filter(isSelectableCodexPoolAccount);
|
|
1933
|
+
// One redaction decision for the whole snapshot, read once from the operator's config (#3859).
|
|
1934
|
+
const maskEmails = emailMaskingEnabled(runtimeConfig);
|
|
1882
1935
|
const mainResult = await fetchMainAccountInfoAttempt(forceRefresh, 1);
|
|
1883
1936
|
const refreshedPool = await mapWithConcurrency(poolAccounts, POOL_QUOTA_REFRESH_CONCURRENCY, async account => {
|
|
1884
1937
|
const cred = getCodexAccountCredential(account.id);
|
|
@@ -1887,7 +1940,7 @@ export async function listCodexAuthAccountsSnapshot(
|
|
|
1887
1940
|
quotaResult = { quota: null, needsReauth: true };
|
|
1888
1941
|
} else {
|
|
1889
1942
|
try {
|
|
1890
|
-
quotaResult = await fetchPoolAccountQuota(account.id, forceRefresh, account.plan);
|
|
1943
|
+
quotaResult = await fetchPoolAccountQuota(account.id, forceRefresh, account.plan, getValidCodexToken, options.validatePending === true);
|
|
1891
1944
|
} catch (error) {
|
|
1892
1945
|
if (!(error instanceof PoolQuotaProbeBusyError)) throw error;
|
|
1893
1946
|
quotaResult = {
|
|
@@ -1923,6 +1976,7 @@ export async function listCodexAuthAccountsSnapshot(
|
|
|
1923
1976
|
false,
|
|
1924
1977
|
isCodexAccountPaused(runtimeConfig, accountId),
|
|
1925
1978
|
getCodexAccountPriority(runtimeConfig, accountId),
|
|
1979
|
+
maskEmails,
|
|
1926
1980
|
)];
|
|
1927
1981
|
}
|
|
1928
1982
|
const resultGeneration = quotaResult.credentialGeneration ?? quotaResult.freshCredentialGeneration;
|
|
@@ -1942,6 +1996,7 @@ export async function listCodexAuthAccountsSnapshot(
|
|
|
1942
1996
|
true,
|
|
1943
1997
|
isCodexAccountPaused(runtimeConfig, accountId),
|
|
1944
1998
|
getCodexAccountPriority(runtimeConfig, accountId),
|
|
1999
|
+
maskEmails,
|
|
1945
2000
|
)];
|
|
1946
2001
|
});
|
|
1947
2002
|
const fetchedMainGeneration = mainResult.identityGeneration ?? captureMainAccountIdentityGeneration();
|
|
@@ -1958,7 +2013,7 @@ export async function listCodexAuthAccountsSnapshot(
|
|
|
1958
2013
|
});
|
|
1959
2014
|
const main: CodexAuthAccountDto = {
|
|
1960
2015
|
id: MAIN_CODEX_ACCOUNT_ID,
|
|
1961
|
-
email:
|
|
2016
|
+
email: projectEmail(mainInfo.email, maskEmails) ?? "Codex App login",
|
|
1962
2017
|
plan: mainInfo.plan,
|
|
1963
2018
|
...(mainSnapshotLive && mainResult.quotaRefresh && mainResult.quotaRefreshGeneration !== undefined
|
|
1964
2019
|
&& isMainAccountIdentityGenerationLive(mainResult.quotaRefreshGeneration)
|
|
@@ -2019,8 +2074,12 @@ export async function refreshCodexQuotaForActivation(config: OcxConfig, accountI
|
|
|
2019
2074
|
}
|
|
2020
2075
|
}
|
|
2021
2076
|
|
|
2022
|
-
export async function listCodexAuthAccounts(
|
|
2023
|
-
|
|
2077
|
+
export async function listCodexAuthAccounts(
|
|
2078
|
+
config: OcxConfig,
|
|
2079
|
+
forceRefresh = false,
|
|
2080
|
+
options: { validatePending?: boolean } = {},
|
|
2081
|
+
): Promise<CodexAuthAccountDto[]> {
|
|
2082
|
+
return (await listCodexAuthAccountsSnapshot(config, forceRefresh, options)).accounts;
|
|
2024
2083
|
}
|
|
2025
2084
|
|
|
2026
2085
|
interface PauseExhaustedResult {
|
|
@@ -2129,6 +2188,7 @@ export async function handleCodexAuthAPI(
|
|
|
2129
2188
|
url: URL,
|
|
2130
2189
|
config: OcxConfig,
|
|
2131
2190
|
convergeCodexCatalog?: CodexAuthCatalogConvergence,
|
|
2191
|
+
principal?: import("../server/management-auth").ManagementPrincipal,
|
|
2132
2192
|
): Promise<Response | null> {
|
|
2133
2193
|
|
|
2134
2194
|
if (url.pathname === "/api/codex-auth/accounts" && req.method === "GET") {
|
|
@@ -2136,6 +2196,14 @@ export async function handleCodexAuthAPI(
|
|
|
2136
2196
|
return jsonResponse({ accounts: await listCodexAuthAccounts(config, forceRefresh) });
|
|
2137
2197
|
}
|
|
2138
2198
|
|
|
2199
|
+
if (url.pathname === "/api/codex-auth/accounts/refresh" && req.method === "POST") {
|
|
2200
|
+
// Inference spends quota: only a dashboard session carries the consent
|
|
2201
|
+
// required by AGENTS_INSTALL.md. Raw-admin/CLI refreshes remain observational.
|
|
2202
|
+
return jsonResponse({ accounts: await listCodexAuthAccounts(config, true, {
|
|
2203
|
+
validatePending: principal === "gui-session",
|
|
2204
|
+
}) });
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2139
2207
|
if (url.pathname === "/api/codex-auth/accounts" && req.method === "POST") {
|
|
2140
2208
|
return manualImportDisabledResponse();
|
|
2141
2209
|
}
|
|
@@ -2313,6 +2381,9 @@ export async function handleCodexAuthAPI(
|
|
|
2313
2381
|
const exists = (runtimeConfig.codexAccounts ?? [])
|
|
2314
2382
|
.some(account => isSelectableCodexPoolAccount(account) && account.id === body.accountId);
|
|
2315
2383
|
if (!exists) return jsonResponse({ error: "Account not found" }, 400);
|
|
2384
|
+
if (readCodexAccountRecord(body.accountId)?.codexValidationPending) {
|
|
2385
|
+
return jsonResponse({ error: "Account validation is pending. Refresh quota after recovery to validate it." }, 409);
|
|
2386
|
+
}
|
|
2316
2387
|
}
|
|
2317
2388
|
runtimeConfig.activeCodexAccountId = body.accountId ?? undefined;
|
|
2318
2389
|
// "Use this account now" outranks selection order until the account is spent:
|
|
@@ -2757,7 +2828,12 @@ export async function handleCodexAuthAPI(
|
|
|
2757
2828
|
break;
|
|
2758
2829
|
}
|
|
2759
2830
|
|
|
2760
|
-
|
|
2831
|
+
// A successful authenticated WHAM read can prove quota is exhausted without
|
|
2832
|
+
// spending an inference request. Store the account, but defer inference validation
|
|
2833
|
+
// and keep it unavailable to routing. Unknown/failed usage reads retain the gate.
|
|
2834
|
+
const warmup = isCodexQuotaExhausted(quota, plan)
|
|
2835
|
+
? { ok: true as const, validatedAt: undefined }
|
|
2836
|
+
: await verifyCodexAccountWarmup(accountId, cred.access, oauthAccountId);
|
|
2761
2837
|
if (!warmup.ok) {
|
|
2762
2838
|
const body = await warmup.response.json().catch(() => ({})) as { error?: string; reason?: string };
|
|
2763
2839
|
setCodexLoginState(flowId, {
|
|
@@ -2797,11 +2873,13 @@ export async function handleCodexAuthAPI(
|
|
|
2797
2873
|
};
|
|
2798
2874
|
|
|
2799
2875
|
if (existingIdx >= 0) {
|
|
2800
|
-
saveCodexAccountCredential(accountId, credential
|
|
2876
|
+
const generation = saveCodexAccountCredential(accountId, credential, {
|
|
2877
|
+
validationPending: warmup.validatedAt === undefined,
|
|
2878
|
+
});
|
|
2801
2879
|
// A successful reauthentication replaces the credential generation. Do not let a
|
|
2802
2880
|
// failed optional WHAM probe make the replacement inherit quota from the old record.
|
|
2803
2881
|
if (reauth) clearAccountQuota(accountId);
|
|
2804
|
-
markCodexAccountValidated(accountId, warmup.validatedAt);
|
|
2882
|
+
if (warmup.validatedAt !== undefined) markCodexAccountValidated(accountId, warmup.validatedAt, generation);
|
|
2805
2883
|
clearAccountNeedsReauth(accountId);
|
|
2806
2884
|
if (quota) setAccountQuotaFromParsed(accountId, quota);
|
|
2807
2885
|
// Keep the pool id stable; refresh display metadata after a successful login/reauth.
|
|
@@ -2852,6 +2930,7 @@ export async function handleCodexAuthAPI(
|
|
|
2852
2930
|
status: "done",
|
|
2853
2931
|
accountId,
|
|
2854
2932
|
email,
|
|
2933
|
+
...(warmup.validatedAt === undefined ? { validationPending: true } : {}),
|
|
2855
2934
|
...(catalogRefreshPending ? { catalogRefreshPending: true } : {}),
|
|
2856
2935
|
doneAt: Date.now(),
|
|
2857
2936
|
});
|
|
@@ -2952,6 +3031,9 @@ export async function handleCodexAuthAPI(
|
|
|
2952
3031
|
if (url.pathname === "/api/codex-auth/login-status" && req.method === "GET") {
|
|
2953
3032
|
const flowId = url.searchParams.get("flowId");
|
|
2954
3033
|
const accountId = url.searchParams.get("accountId")?.trim();
|
|
3034
|
+
// Transient flow state carries the address of the account being added, so it follows the
|
|
3035
|
+
// same operator policy as the stored accounts it is about to become.
|
|
3036
|
+
const maskFlowEmails = emailMaskingEnabled(config);
|
|
2955
3037
|
// Reauth always has a pre-existing credential; never treat "credential exists" as success
|
|
2956
3038
|
// when the flow map entry is gone (would false-complete on lost/expired flow state).
|
|
2957
3039
|
const reauthStatus = url.searchParams.get("reauth") === "1";
|
|
@@ -2964,13 +3046,15 @@ export async function handleCodexAuthAPI(
|
|
|
2964
3046
|
&& !isAccountNeedsReauth(accountId)
|
|
2965
3047
|
&& getCodexAccountCredential(accountId)
|
|
2966
3048
|
) {
|
|
2967
|
-
return jsonResponse({ status: "done", accountId
|
|
3049
|
+
return jsonResponse({ status: "done", accountId,
|
|
3050
|
+
...(readCodexAccountRecord(accountId)?.codexValidationPending ? { validationPending: true } : {}),
|
|
3051
|
+
});
|
|
2968
3052
|
}
|
|
2969
|
-
return jsonResponse(st ? { ...st, email:
|
|
3053
|
+
return jsonResponse(st ? { ...st, email: projectEmail(st.email, maskFlowEmails) ?? undefined } : { status: "expired" });
|
|
2970
3054
|
}
|
|
2971
3055
|
// Legacy fallback: return latest pending flow
|
|
2972
3056
|
for (const [, st] of codexAuthLoginState) {
|
|
2973
|
-
if (st.status === "pending") return jsonResponse({ ...st, email:
|
|
3057
|
+
if (st.status === "pending") return jsonResponse({ ...st, email: projectEmail(st.email, maskFlowEmails) ?? undefined });
|
|
2974
3058
|
}
|
|
2975
3059
|
return jsonResponse({ status: "idle" });
|
|
2976
3060
|
}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
getCodexAccountCredential,
|
|
8
8
|
getValidCodexToken,
|
|
9
9
|
isCodexAccountGenerationLive,
|
|
10
|
+
readCodexAccountRecord,
|
|
10
11
|
} from "./account-store";
|
|
11
12
|
import { isAccountNeedsReauth, markAccountNeedsReauth } from "./account-runtime-state";
|
|
12
13
|
import { ConfigMutationLockError } from "../config";
|
|
@@ -200,6 +201,19 @@ export class CodexPoolAuthenticationError extends Error {
|
|
|
200
201
|
}
|
|
201
202
|
}
|
|
202
203
|
|
|
204
|
+
class CodexAccountValidationPendingError extends CodexPoolAuthenticationError {
|
|
205
|
+
constructor() {
|
|
206
|
+
super("Codex account validation is pending; refresh quota after recovery to validate it");
|
|
207
|
+
this.name = "CodexAccountValidationPendingError";
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function assertCodexAccountValidationReady(accountId: string): void {
|
|
212
|
+
if (accountId !== MAIN_CODEX_ACCOUNT_ID && readCodexAccountRecord(accountId)?.codexValidationPending) {
|
|
213
|
+
throw new CodexAccountValidationPendingError();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
203
217
|
export const CODEX_MAIN_PROFILE_MAINTENANCE_MESSAGE =
|
|
204
218
|
"OpenCodex local native-main profile maintenance is active; retry this request";
|
|
205
219
|
|
|
@@ -585,6 +599,7 @@ export class CodexThreadAffinityExpiredError extends Error {
|
|
|
585
599
|
|
|
586
600
|
export function shouldMarkAccountNeedsReauthForCodexAuthFailure(cause: unknown): boolean {
|
|
587
601
|
return !(cause instanceof CodexMainAccountHardLockError)
|
|
602
|
+
&& !(cause instanceof CodexAccountValidationPendingError)
|
|
588
603
|
&& !(cause instanceof CodexReserveUnavailableError)
|
|
589
604
|
&& !(cause instanceof CodexCredentialGenerationConflictError)
|
|
590
605
|
&& !(cause instanceof CodexCredentialRefreshLockTimeoutError)
|
|
@@ -911,6 +926,9 @@ export async function resolveCodexAuthContext(
|
|
|
911
926
|
} finally {
|
|
912
927
|
selectionAdmission?.release();
|
|
913
928
|
}
|
|
929
|
+
// Legacy selectors may retain an unusable account for actionable errors. A
|
|
930
|
+
// deferred credential must never become request auth through that fallback.
|
|
931
|
+
assertCodexAccountValidationReady(accountId);
|
|
914
932
|
// Lazy prime: if the selected account has no quota yet, the pool is likely
|
|
915
933
|
// unprimed (dashboard never opened, or startup prime was blocked). Kick a
|
|
916
934
|
// best-effort prime so the NEXT routing decision has real scores. This never
|
|
@@ -1006,6 +1024,7 @@ export async function resolveCodexAuthContext(
|
|
|
1006
1024
|
|
|
1007
1025
|
try {
|
|
1008
1026
|
const token = await getValidCodexToken(accountId);
|
|
1027
|
+
assertCodexAccountValidationReady(accountId);
|
|
1009
1028
|
return {
|
|
1010
1029
|
kind: "pool",
|
|
1011
1030
|
accountId,
|
|
@@ -1045,6 +1064,7 @@ export function applyCodexAuthContextToProvider(
|
|
|
1045
1064
|
mode: CodexAccountMode | undefined,
|
|
1046
1065
|
): OcxRuntimeProviderConfig {
|
|
1047
1066
|
if (mode !== "pool" || (ctx.kind !== "pool" && ctx.kind !== "main-pool") || provider.authMode !== "forward") return provider;
|
|
1067
|
+
assertCodexAccountValidationReady(ctx.accountId);
|
|
1048
1068
|
return {
|
|
1049
1069
|
...provider,
|
|
1050
1070
|
_codexAccountOverride: {
|
|
@@ -1087,6 +1107,7 @@ export function materializeCodexUpstreamAuth(
|
|
|
1087
1107
|
if (value) selected.set(name, value);
|
|
1088
1108
|
}
|
|
1089
1109
|
if (ctx.kind === "pool" || ctx.kind === "main-pool") {
|
|
1110
|
+
assertCodexAccountValidationReady(ctx.accountId);
|
|
1090
1111
|
selected.set("authorization", `Bearer ${ctx.accessToken}`);
|
|
1091
1112
|
selected.set("chatgpt-account-id", ctx.chatgptAccountId);
|
|
1092
1113
|
if (ctx.kind === "main-pool") {
|
|
@@ -144,6 +144,29 @@ export interface CatalogModel {
|
|
|
144
144
|
codexToolMode?: "code_mode_only" | "shell";
|
|
145
145
|
/** Normalized upstream capability names retained for management/API consumers (#485 follow-up). */
|
|
146
146
|
capabilities?: string[];
|
|
147
|
+
/**
|
|
148
|
+
* This row is listed but cannot currently serve a request (#1711). Today the only value is
|
|
149
|
+
* "no_credit", set when every usable target has positive quota-exhaustion evidence.
|
|
150
|
+
*
|
|
151
|
+
* It is NOT visibility. The row stays `visibility: "list"` on purpose: the issue explicitly
|
|
152
|
+
* rejects hiding, and Codex Desktop only understands "list" and "hide" anyway, so hiding would
|
|
153
|
+
* be the one outcome the reporter asked not to have. An OpenCodex-aware consumer greys the
|
|
154
|
+
* entry; the native picker ignores the field, which is the honest limit of what a custom
|
|
155
|
+
* catalog field can do.
|
|
156
|
+
*/
|
|
157
|
+
quotaInactiveReason?: "no_credit";
|
|
158
|
+
/**
|
|
159
|
+
* Discovered per-token cost class for this routed model (#3666). "free" means the provider's
|
|
160
|
+
* own /models row reported a numeric zero for BOTH the prompt and the completion rate;
|
|
161
|
+
* "paid" means at least one rate is above zero. ABSENT means unknown — the provider published
|
|
162
|
+
* no usable pair, or this row never came from a models API at all.
|
|
163
|
+
*
|
|
164
|
+
* Fail closed: a partial, non-numeric, or negative rate leaves the field absent, never "free",
|
|
165
|
+
* because showing a paid model under a Free filter costs the user money while hiding a free
|
|
166
|
+
* one costs a click. This is a management/Dashboard projection only — deriveEntry never
|
|
167
|
+
* serializes it into the Codex catalog, and it never affects routing or visibility.
|
|
168
|
+
*/
|
|
169
|
+
pricingStatus?: "free" | "paid";
|
|
147
170
|
/** OpenCodex-only catalog ownership marker; Codex ignores the serialized extension field. */
|
|
148
171
|
catalogKind?: typeof CODEX_CUSTOM_MODEL_CATALOG_KIND | typeof CODEX_PROVIDER_MODEL_CATALOG_KIND;
|
|
149
172
|
}
|
|
@@ -60,6 +60,7 @@ import {
|
|
|
60
60
|
comboModelId,
|
|
61
61
|
getCombo,
|
|
62
62
|
listComboIds,
|
|
63
|
+
quotaInactiveReason,
|
|
63
64
|
targetKey,
|
|
64
65
|
} from "../../combos";
|
|
65
66
|
import type { NormalizedComboConfig } from "../../combos/types";
|
|
@@ -79,6 +80,7 @@ import {
|
|
|
79
80
|
type ProviderModelsApiItem,
|
|
80
81
|
type ResolvedProviderModelDiscovery,
|
|
81
82
|
} from "../../providers/model-discovery";
|
|
83
|
+
import { extractGoogleAiStudioModelItems } from "../../providers/google-ai-studio-model-discovery";
|
|
82
84
|
import { applyConfiguredHeadersLast, fetchOllamaShowEnrichment, ollamaShowEnrichable } from "../../providers/ollama-show";
|
|
83
85
|
import upstreamModelsSnapshot from "../data/upstream-models.json";
|
|
84
86
|
import { createAdmissionGate, ResourceAdmissionError, type AdmissionMetrics } from "../../lib/admission";
|
|
@@ -1400,6 +1402,51 @@ function modelInputModalities(
|
|
|
1400
1402
|
return undefined;
|
|
1401
1403
|
}
|
|
1402
1404
|
|
|
1405
|
+
/**
|
|
1406
|
+
* A per-token rate exactly as a /models row publishes it, or undefined when the value is not a
|
|
1407
|
+
* usable non-negative number. Providers ship these both as JSON numbers and as decimal strings —
|
|
1408
|
+
* OpenRouter encodes free as the string `"0.00000000"` — so both shapes are accepted and nothing
|
|
1409
|
+
* else is. The explicit numeric-shape test has to run BEFORE any coercion: `Number("")` and
|
|
1410
|
+
* `Number(" ")` are both 0 and `Number(true)` is 1, so a bare `Number(value)` would classify a
|
|
1411
|
+
* row with an empty price string as free.
|
|
1412
|
+
*/
|
|
1413
|
+
const DISCOVERED_PRICING_RATE_PATTERN = /^-?\d+(?:\.\d+)?(?:[eE][-+]?\d+)?$/;
|
|
1414
|
+
|
|
1415
|
+
function discoveredPricingRate(value: unknown): number | undefined {
|
|
1416
|
+
const numeric = typeof value === "number"
|
|
1417
|
+
? value
|
|
1418
|
+
: typeof value === "string" && DISCOVERED_PRICING_RATE_PATTERN.test(value.trim())
|
|
1419
|
+
? Number(value.trim())
|
|
1420
|
+
: undefined;
|
|
1421
|
+
if (numeric === undefined || !Number.isFinite(numeric) || numeric < 0) return undefined;
|
|
1422
|
+
return numeric;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
/**
|
|
1426
|
+
* Cost class for one discovered row, read from the provider's own `pricing` object (#3666).
|
|
1427
|
+
*
|
|
1428
|
+
* Fail closed. Only a complete pair of non-negative numeric rates classifies at all; a missing,
|
|
1429
|
+
* one-sided, non-numeric, or negative rate is "unknown" and therefore excluded from a free-only
|
|
1430
|
+
* filter. Showing a paid model under a Free filter spends the user's money, while hiding a free
|
|
1431
|
+
* one costs a click.
|
|
1432
|
+
*
|
|
1433
|
+
* Two things that look like evidence and are not. A `:free` id suffix is an OpenRouter naming
|
|
1434
|
+
* convention, not a price — Nous ships `:free` slugs on a provider whose `freeTier` is false on
|
|
1435
|
+
* purpose. And the operator's own `modelCosts` overlay is an estimate they typed, not something
|
|
1436
|
+
* the provider published, so a zeroed overlay never reaches this field either.
|
|
1437
|
+
*
|
|
1438
|
+
* Classification is on numeric zero and never on a unit conversion: OpenRouter quotes USD per
|
|
1439
|
+
* token while the cost overlays and the jawcode bundle quote per 1M, and zero is zero in both.
|
|
1440
|
+
*/
|
|
1441
|
+
export function discoveredPricingStatus(item: ProviderModelsApiItem): "free" | "paid" | "unknown" {
|
|
1442
|
+
const pricing = plainRecord(item.pricing) ?? plainRecord(plainRecord(item.metadata)?.pricing);
|
|
1443
|
+
if (!pricing) return "unknown";
|
|
1444
|
+
const prompt = discoveredPricingRate(pricing.prompt ?? pricing.input);
|
|
1445
|
+
const completion = discoveredPricingRate(pricing.completion ?? pricing.output);
|
|
1446
|
+
if (prompt === undefined || completion === undefined) return "unknown";
|
|
1447
|
+
return prompt === 0 && completion === 0 ? "free" : "paid";
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1403
1450
|
export function catalogHintsFromModelsApiItem(providerName: string, item: ProviderModelsApiItem): Partial<CatalogModel> {
|
|
1404
1451
|
const metadata = plainRecord(item.metadata);
|
|
1405
1452
|
const capabilityRecord = plainRecord(metadata?.capabilities) ?? plainRecord(item.capabilities);
|
|
@@ -1464,6 +1511,7 @@ export function catalogHintsFromModelsApiItem(providerName: string, item: Provid
|
|
|
1464
1511
|
: undefined;
|
|
1465
1512
|
const capabilities = modelCapabilities(item);
|
|
1466
1513
|
const inputModalities = modelInputModalities(item, capabilities);
|
|
1514
|
+
const pricingStatus = discoveredPricingStatus(item);
|
|
1467
1515
|
return {
|
|
1468
1516
|
...(contextWindow && contextWindow > 0 ? { contextWindow } : {}),
|
|
1469
1517
|
...(maxInputTokens && maxInputTokens > 0 ? { maxInputTokens } : {}),
|
|
@@ -1471,6 +1519,11 @@ export function catalogHintsFromModelsApiItem(providerName: string, item: Provid
|
|
|
1471
1519
|
...(reasoningEfforts !== undefined ? { reasoningEfforts } : {}),
|
|
1472
1520
|
...(inputModalities ? { inputModalities } : {}),
|
|
1473
1521
|
...(capabilities ? { capabilities } : {}),
|
|
1522
|
+
// Omitted when the classification is "unknown", following this function's existing
|
|
1523
|
+
// contract that an unknown property is absent rather than present-and-empty. Callers
|
|
1524
|
+
// that need to tell "provider published no prices" from "this build does not classify"
|
|
1525
|
+
// call discoveredPricingStatus directly.
|
|
1526
|
+
...(pricingStatus !== "unknown" ? { pricingStatus } : {}),
|
|
1474
1527
|
};
|
|
1475
1528
|
}
|
|
1476
1529
|
|
|
@@ -1871,7 +1924,14 @@ async function fetchProviderModelsWithAuth(
|
|
|
1871
1924
|
markProviderDiscoveryOk(name, live.length);
|
|
1872
1925
|
return observed(withConfiguredRetention(forCache, { warnDrops: true }), "authoritative");
|
|
1873
1926
|
}
|
|
1874
|
-
const
|
|
1927
|
+
const googleAiStudio = effectiveGoogleMode(name, prov) === "ai-studio"
|
|
1928
|
+
? extractGoogleAiStudioModelItems(bounded.value, discovery.maxModels)
|
|
1929
|
+
: undefined;
|
|
1930
|
+
// Native /v1beta/models wins; a google row served by an OpenAI-compatible
|
|
1931
|
+
// gateway keeps the generic data[] / top-level-array contract.
|
|
1932
|
+
const extracted = googleAiStudio?.ok
|
|
1933
|
+
? googleAiStudio
|
|
1934
|
+
: extractProviderModelItems(bounded.value, discovery);
|
|
1875
1935
|
if (!extracted.ok) {
|
|
1876
1936
|
const { models, fallback, shouldLog } = failedDiscoveryFallback({ reason: "invalid_response" });
|
|
1877
1937
|
const diagnostic: Record<ModelDiscoveryResponseFailure, string> = {
|
|
@@ -2622,7 +2682,16 @@ async function gatherRoutedModelsUncached(
|
|
|
2622
2682
|
return {
|
|
2623
2683
|
models: models.map(model => {
|
|
2624
2684
|
const displayName = aliasDisplayNames.get(`${model.provider}/${model.id}`);
|
|
2625
|
-
|
|
2685
|
+
// #1711: one stamping point for every row this gather produces — routed, combo, and custom
|
|
2686
|
+
// alike — because it is the only place that has both the finished list and the config the
|
|
2687
|
+
// quota rules need. A combo votes over its own targets; anything else votes over the single
|
|
2688
|
+
// provider that would serve it.
|
|
2689
|
+
const targets = model.provider === COMBO_NAMESPACE
|
|
2690
|
+
? config.combos?.[model.id]?.targets ?? []
|
|
2691
|
+
: [{ provider: model.provider }];
|
|
2692
|
+
const inactive = quotaInactiveReason(config, targets);
|
|
2693
|
+
const named = displayName && !model.displayName ? { ...model, displayName } : model;
|
|
2694
|
+
return inactive ? { ...named, quotaInactiveReason: inactive } : named;
|
|
2626
2695
|
}),
|
|
2627
2696
|
comboOmissions: localOmissions,
|
|
2628
2697
|
providerAuthOutcomes: localProviderAuthOutcomes,
|
|
@@ -99,6 +99,12 @@ export const PICKER_ORDER_PRIORITY_BASE = 1_000;
|
|
|
99
99
|
// independent of display order. It does not freeze native advertisements. Absent on unmoved rows.
|
|
100
100
|
export const SPAWN_PRIORITY_FIELD = "opencodex_spawn_priority";
|
|
101
101
|
|
|
102
|
+
// OpenCodex-private catalog field: this row is listed but currently unable to serve (#1711).
|
|
103
|
+
// Codex ignores unknown catalog fields (same as opencodex_catalog_kind and the spawn priority
|
|
104
|
+
// above) and ensureStrictCatalogFields does not strip extras, so this is invisible to the native
|
|
105
|
+
// picker and cannot change what Codex offers. It never touches `visibility`.
|
|
106
|
+
export const CATALOG_INACTIVE_REASON_FIELD = "opencodex_inactive_reason";
|
|
107
|
+
|
|
102
108
|
export type SpawnAgentSurface = "v1" | "v2";
|
|
103
109
|
|
|
104
110
|
export type SubagentRosterExclusionReason =
|
|
@@ -383,6 +389,10 @@ export function deriveEntry(
|
|
|
383
389
|
if (model) applyCatalogMetadata(e, model.provider, model.id, model.contextCap);
|
|
384
390
|
applyCatalogModelMetadata(e, model);
|
|
385
391
|
if (model?.catalogKind) e.opencodex_catalog_kind = model.catalogKind;
|
|
392
|
+
// Additive only. `visibility` is untouched: an inactive row must still be OFFERED, which is
|
|
393
|
+
// the whole point of #1711 — operator disable is what removes rows, and it stays a separate
|
|
394
|
+
// path from this one.
|
|
395
|
+
if (model?.quotaInactiveReason) e[CATALOG_INACTIVE_REASON_FIELD] = model.quotaInactiveReason;
|
|
386
396
|
} else {
|
|
387
397
|
applyNativeOpenAiContextOverride(e, contextCap);
|
|
388
398
|
if (isGpt56NativeSlug(slug)) ensureGpt56ReasoningLevels(e);
|
|
@@ -430,6 +440,10 @@ export function deriveEntry(
|
|
|
430
440
|
if (model && isRouted) applyCatalogMetadata(entry, model.provider, model.id, model.contextCap);
|
|
431
441
|
applyCatalogModelMetadata(entry, model);
|
|
432
442
|
if (model?.catalogKind) entry.opencodex_catalog_kind = model.catalogKind;
|
|
443
|
+
// Same additive stamp as the templated path above. A routed row that reaches the no-template
|
|
444
|
+
// fallback is still a served row, so omitting it here would make the field depend on whether a
|
|
445
|
+
// template happened to be cached — which is exactly what the regression test caught.
|
|
446
|
+
if (model?.quotaInactiveReason) entry[CATALOG_INACTIVE_REASON_FIELD] = model.quotaInactiveReason;
|
|
433
447
|
if (!isRouted) applyNativeOpenAiContextOverride(entry, contextCap);
|
|
434
448
|
return ensureStrictCatalogFields(normalizeServiceTiers(entry), {
|
|
435
449
|
preserveExactInputModalities: preserveExact,
|
package/src/codex/inject.ts
CHANGED
|
@@ -1430,7 +1430,7 @@ export async function injectCodexConfig(
|
|
|
1430
1430
|
const historyMessage =
|
|
1431
1431
|
keepRootOverrideAlongsideTable
|
|
1432
1432
|
? (keptUserBaseUrl
|
|
1433
|
-
? ` Codex resume history: left unchanged; threads already tagged openai follow your
|
|
1433
|
+
? ` Codex resume history: left unchanged; threads already tagged openai follow your configured root openai_base_url.\n`
|
|
1434
1434
|
: ` Codex resume history: left unchanged; existing threads keep reaching the proxy through the retained openai_base_url override.\n`)
|
|
1435
1435
|
: config?.syncResumeHistory === false
|
|
1436
1436
|
? ` Codex resume history: left unchanged (syncResumeHistory=false).\n`
|
|
@@ -1447,6 +1447,7 @@ export async function injectCodexConfig(
|
|
|
1447
1447
|
// The client-compaction form writes a provider table as well, so "nothing was injected" would
|
|
1448
1448
|
// misdescribe the file it just produced: new threads do use the injected table. Report that
|
|
1449
1449
|
// mixed result on its own terms, and never tell the operator to delete a setting of theirs.
|
|
1450
|
+
// Ownership alone says nothing about destination: their line may already target this proxy.
|
|
1450
1451
|
if (keptUserBaseUrl && keepRootOverrideAlongsideTable) {
|
|
1451
1452
|
return {
|
|
1452
1453
|
success: true,
|
|
@@ -1459,7 +1460,7 @@ export async function injectCodexConfig(
|
|
|
1459
1460
|
managedDefaultsMessage +
|
|
1460
1461
|
` New threads use the injected opencodex provider and route through the proxy.\n` +
|
|
1461
1462
|
` Threads already tagged openai resolve through Codex's built-in provider, which your root openai_base_url points at.\n` +
|
|
1462
|
-
`
|
|
1463
|
+
` No root URL change is required to enable client-side compaction for new threads.\n` +
|
|
1463
1464
|
` Fallback: codex --profile opencodex (same behavior)`,
|
|
1464
1465
|
};
|
|
1465
1466
|
}
|
|
@@ -8,7 +8,7 @@ import { isSelectableCodexPoolAccount } from "./account-id";
|
|
|
8
8
|
import { reconcileMainCodexAccountRuntimeState } from "./account-lifecycle";
|
|
9
9
|
import { isCodexAccountPaused } from "./account-pause";
|
|
10
10
|
import { isAccountNeedsReauth, markAccountNeedsReauth } from "./account-runtime-state";
|
|
11
|
-
import { getValidCodexToken, isCodexAccountGenerationLive } from "./account-store";
|
|
11
|
+
import { getValidCodexToken, isCodexAccountGenerationLive, readCodexAccountRecord } from "./account-store";
|
|
12
12
|
import { codexAccountLogLabel } from "./account-label";
|
|
13
13
|
import { getMainAccountToken, getValidMainAccountToken, MAIN_CODEX_ACCOUNT_ID } from "./main-account";
|
|
14
14
|
import { isMainAccountHardLocked } from "./main-account-hard-lock";
|
|
@@ -164,7 +164,11 @@ function mainWarmupRestricted(config: OcxConfig): boolean {
|
|
|
164
164
|
async function warmAccount(config: OcxConfig, accountId: string): Promise<void | false> {
|
|
165
165
|
const writerGeneration = captureConfigGeneration();
|
|
166
166
|
if (accountId !== MAIN_CODEX_ACCOUNT_ID) {
|
|
167
|
+
if (readCodexAccountRecord(accountId)?.codexValidationPending) return false;
|
|
167
168
|
const token = await getValidCodexToken(accountId);
|
|
169
|
+
const record = readCodexAccountRecord(accountId);
|
|
170
|
+
if (!record?.credential || record.deletedAt != null || record.codexValidationPending
|
|
171
|
+
|| record.generation !== token.generation) return false;
|
|
168
172
|
if (isCodexAccountPaused(config, accountId) || isAccountNeedsReauth(accountId)) return false;
|
|
169
173
|
try {
|
|
170
174
|
await warmCodexAccount({ ...token, onCompleted: headers => {
|
|
@@ -294,6 +298,7 @@ export async function runCodexQuotaAutoRefresh(
|
|
|
294
298
|
&& (accountId === MAIN_CODEX_ACCOUNT_ID || config.codexAccounts?.some(
|
|
295
299
|
account => account.id === accountId && isSelectableCodexPoolAccount(account)))
|
|
296
300
|
&& (setting?.fiveHour === true || setting?.weekly === true)
|
|
301
|
+
&& !(accountId !== MAIN_CODEX_ACCOUNT_ID && readCodexAccountRecord(accountId)?.codexValidationPending)
|
|
297
302
|
&& !isCodexAccountPaused(config, accountId) && !isAccountNeedsReauth(accountId)
|
|
298
303
|
&& !(accountId === MAIN_CODEX_ACCOUNT_ID && isMainAccountHardLocked(config));
|
|
299
304
|
};
|