@bitkyc08/opencodex 2.7.42 → 2.7.43
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/README.md +8 -1
- package/bin/ocx.mjs +13 -14
- package/gui/dist/assets/index-Czw-jpTU.css +1 -0
- package/gui/dist/assets/index-cmds12BG.js +67 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/AGENTS.md +28 -0
- package/src/adapters/anthropic.ts +15 -6
- package/src/adapters/cursor/native-exec-shell.ts +18 -6
- package/src/adapters/cursor/protobuf-events.ts +24 -2
- package/src/adapters/cursor/protobuf-request.ts +1 -2
- package/src/adapters/cursor/tool-definitions.ts +68 -29
- package/src/adapters/google-wire-compiler.ts +4 -0
- package/src/adapters/google.ts +128 -2
- package/src/adapters/identity.ts +12 -2
- package/src/adapters/kiro.ts +49 -6
- package/src/adapters/mimo-free.ts +2 -0
- package/src/adapters/openai-responses.ts +246 -59
- package/src/claude/agents-inject.ts +5 -0
- package/src/claude/inbound.ts +26 -9
- package/src/cli/account-auth.ts +1 -1
- package/src/cli/agent-driven.ts +37 -0
- package/src/cli/claude.ts +3 -3
- package/src/cli/doctor.ts +23 -18
- package/src/cli/help.ts +42 -6
- package/src/cli/index.ts +88 -19
- package/src/cli/interactive-confirm.ts +129 -0
- package/src/cli/opencode.ts +701 -0
- package/src/cli/provider-runtime.ts +3 -0
- package/src/cli/provider.ts +31 -10
- package/src/cli/star-prompt.ts +55 -16
- package/src/cli/status.ts +47 -13
- package/src/codex/account-id.ts +34 -0
- package/src/codex/account-lifecycle.ts +4 -1
- package/src/codex/account-namespace-match.ts +63 -0
- package/src/codex/account-namespaces.ts +149 -0
- package/src/codex/account-pause.ts +20 -0
- package/src/codex/account-usability.ts +6 -1
- package/src/codex/app-server-processes.ts +511 -0
- package/src/codex/auth-api.ts +293 -34
- package/src/codex/auth-collision.ts +2 -1
- package/src/codex/auth-context.ts +60 -17
- package/src/codex/catalog/parsing.ts +16 -1
- package/src/codex/catalog/provider-fetch.ts +48 -12
- package/src/codex/catalog/sync.ts +18 -3
- package/src/codex/catalog.ts +1 -1
- package/src/codex/history-provider.ts +145 -1
- package/src/codex/inject.ts +114 -14
- package/src/codex/main-account.ts +2 -8
- package/src/codex/pool-rotation.ts +186 -0
- package/src/codex/quota.ts +15 -0
- package/src/codex/routing.ts +695 -106
- package/src/codex/shim.ts +4 -1
- package/src/codex/subagent-defaults.ts +550 -0
- package/src/codex/subagent-model-fallback.ts +2 -0
- package/src/codex/sync.ts +3 -0
- package/src/config.ts +566 -25
- package/src/images/artifacts.ts +516 -0
- package/src/images/fulfill-video.ts +163 -0
- package/src/images/fulfill.ts +111 -0
- package/src/images/index.ts +4 -0
- package/src/images/loop.ts +789 -0
- package/src/images/plan.ts +133 -0
- package/src/images/synthetic-tool.ts +133 -0
- package/src/images/types.ts +41 -0
- package/src/images/xai-client.ts +141 -0
- package/src/images/xai-video-client.ts +163 -0
- package/src/lib/admin-secrets.ts +25 -0
- package/src/lib/config-ownership.ts +327 -0
- package/src/lib/crash-guard.ts +2 -0
- package/src/lib/destination-policy.ts +122 -4
- package/src/lib/pinned-http.ts +151 -0
- package/src/lib/process-control.ts +2 -2
- package/src/lib/provider-outbound.ts +164 -0
- package/src/lib/provider-url.ts +14 -0
- package/src/lib/proxy-env.ts +18 -0
- package/src/lib/windows-elevation.ts +81 -3
- package/src/lib/windows-secret-acl.ts +189 -12
- package/src/lib/winsw.ts +2 -0
- package/src/oauth/anthropic-routing.ts +570 -0
- package/src/oauth/health.ts +6 -0
- package/src/oauth/index.ts +283 -72
- package/src/oauth/key-providers.ts +17 -6
- package/src/oauth/kimi.ts +2 -0
- package/src/oauth/kiro-credentials.ts +322 -10
- package/src/oauth/kiro.ts +423 -43
- package/src/oauth/login-cli.ts +32 -5
- package/src/oauth/store.ts +54 -4
- package/src/oauth/types.ts +11 -0
- package/src/providers/alibaba-region-migration.ts +16 -3
- package/src/providers/antigravity-models.ts +3 -0
- package/src/providers/api-keys.ts +13 -6
- package/src/providers/derive.ts +6 -0
- package/src/providers/key-failover.ts +24 -4
- package/src/providers/quota.ts +233 -29
- package/src/providers/registry.ts +11 -3
- package/src/responses/parser.ts +11 -0
- package/src/responses/state.ts +22 -8
- package/src/responses/tool-groups.ts +19 -0
- package/src/router.ts +14 -4
- package/src/server/auth-cors.ts +110 -22
- package/src/server/claude-messages.ts +8 -1
- package/src/server/gui-static.ts +30 -6
- package/src/server/images.ts +303 -9
- package/src/server/index.ts +77 -9
- package/src/server/lifecycle.ts +25 -1
- package/src/server/management/agent-settings-routes.ts +28 -4
- package/src/server/management/combo-routes.ts +7 -0
- package/src/server/management/config-routes.ts +5 -2
- package/src/server/management/logs-usage-routes.ts +167 -3
- package/src/server/management/oauth-account-routes.ts +163 -17
- package/src/server/management/provider-routes.ts +35 -7
- package/src/server/management/system-restart.ts +172 -0
- package/src/server/management/system-routes.ts +33 -10
- package/src/server/management-api.ts +2 -2
- package/src/server/management-auth.ts +216 -0
- package/src/server/proxy-liveness.ts +14 -3
- package/src/server/responses/compact.ts +21 -13
- package/src/server/responses/core.ts +583 -152
- package/src/server/responses-image-gen-repair.ts +118 -0
- package/src/server/responses-item-id-repair.ts +10 -85
- package/src/server/sse-payload-rewrite.ts +116 -0
- package/src/server/system-env.ts +28 -10
- package/src/service.ts +49 -2
- package/src/storage/cleanup-job.ts +57 -0
- package/src/storage/cleanup.ts +1504 -28
- package/src/storage/policy-job.ts +366 -0
- package/src/storage/policy-scheduler.ts +40 -0
- package/src/storage/policy-worker.ts +53 -0
- package/src/storage/policy.ts +522 -0
- package/src/storage/restore-job.ts +242 -0
- package/src/storage/restore-worker.ts +52 -0
- package/src/storage/storage-mutation-coordinator.ts +109 -0
- package/src/tray/windows.ts +2 -0
- package/src/types.ts +96 -1
- package/src/update/index.ts +36 -18
- package/src/update/job.ts +103 -12
- package/src/update/npm-invocation.d.mts +23 -0
- package/src/update/npm-invocation.mjs +94 -0
- package/src/usage/debug.ts +2 -0
- package/src/usage/log.ts +4 -0
- package/gui/dist/assets/index-Bl_VBGoI.js +0 -65
- package/gui/dist/assets/index-DfVGuN88.css +0 -1
package/src/codex/auth-api.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { withCodexAccountLogLabel } from "./account-label";
|
|
|
3
3
|
import {
|
|
4
4
|
getCodexAccountCredential,
|
|
5
5
|
getValidCodexToken,
|
|
6
|
+
isCodexAccountGenerationLive,
|
|
6
7
|
markCodexAccountValidated,
|
|
7
8
|
saveCodexAccountCredential,
|
|
8
9
|
CodexCredentialGenerationConflictError,
|
|
@@ -10,7 +11,20 @@ import {
|
|
|
10
11
|
TokenRefreshError,
|
|
11
12
|
} from "./account-store";
|
|
12
13
|
import { deleteCodexAccount, reconcileMainCodexAccountRuntimeState } from "./account-lifecycle";
|
|
13
|
-
import {
|
|
14
|
+
import { isCodexAccountPaused, setCodexAccountPaused } from "./account-pause";
|
|
15
|
+
import {
|
|
16
|
+
clearCodexAccountCooldown,
|
|
17
|
+
clearThreadAccountMapForAccount,
|
|
18
|
+
getEffectiveActiveCodexAccountId,
|
|
19
|
+
reconcileCodexActiveAfterExclusion,
|
|
20
|
+
resetCodexRoutingForManualSelection,
|
|
21
|
+
} from "./routing";
|
|
22
|
+
import {
|
|
23
|
+
normalizeAccountPoolStickyLimit,
|
|
24
|
+
normalizeAccountPoolStrategy,
|
|
25
|
+
parseAccountPoolStickyLimit,
|
|
26
|
+
parseAccountPoolStrategy,
|
|
27
|
+
} from "./pool-rotation";
|
|
14
28
|
import { checkAccountIdCollision, getMainChatgptAccountId, readCodexTokens, readCodexTokensResult } from "./auth-collision";
|
|
15
29
|
export { checkAccountIdCollision, getMainChatgptAccountId } from "./auth-collision";
|
|
16
30
|
export { clearAccountNeedsReauth, isAccountNeedsReauth, markAccountNeedsReauth } from "./account-runtime-state";
|
|
@@ -18,6 +32,7 @@ import { clearAccountNeedsReauth, isAccountNeedsReauth, markAccountNeedsReauth }
|
|
|
18
32
|
import {
|
|
19
33
|
clearAccountQuota,
|
|
20
34
|
getAccountQuota,
|
|
35
|
+
isCodexQuotaExhausted,
|
|
21
36
|
listAccountQuotas,
|
|
22
37
|
parseUsageQuota,
|
|
23
38
|
setAccountQuotaFromParsed,
|
|
@@ -34,7 +49,7 @@ export {
|
|
|
34
49
|
updateAccountQuota,
|
|
35
50
|
} from "./quota";
|
|
36
51
|
import { extractAccountId, decodeJwtPayload } from "../oauth/chatgpt";
|
|
37
|
-
import { MAIN_CODEX_ACCOUNT_ID, setMainAccountPlan } from "./main-account";
|
|
52
|
+
import { getMainAccountPlan, MAIN_CODEX_ACCOUNT_ID, setMainAccountPlan } from "./main-account";
|
|
38
53
|
import {
|
|
39
54
|
clearMainAccountInfoCache,
|
|
40
55
|
getMainAccountInfoCache,
|
|
@@ -55,6 +70,13 @@ import {
|
|
|
55
70
|
type OAuthAccountHealth,
|
|
56
71
|
type OAuthHealthLabel,
|
|
57
72
|
} from "../oauth/health";
|
|
73
|
+
import {
|
|
74
|
+
CODEX_ACCOUNT_ID_RE,
|
|
75
|
+
hasLegacyMainCodexPoolAccount,
|
|
76
|
+
isSelectableCodexPoolAccount,
|
|
77
|
+
isValidCodexAccountId,
|
|
78
|
+
} from "./account-id";
|
|
79
|
+
import { codexAccountIdNamespaceCollisionError } from "./account-namespace-match";
|
|
58
80
|
|
|
59
81
|
function jsonResponse(data: unknown, status = 200): Response {
|
|
60
82
|
return new Response(JSON.stringify(data), {
|
|
@@ -63,14 +85,32 @@ function jsonResponse(data: unknown, status = 200): Response {
|
|
|
63
85
|
});
|
|
64
86
|
}
|
|
65
87
|
|
|
66
|
-
const ACCOUNT_ID_RE = /^[a-zA-Z0-9._-]{1,64}$/;
|
|
67
88
|
const MANUAL_IMPORT_ENV = "OPENCODEX_ENABLE_UNVERIFIED_CODEX_IMPORT";
|
|
68
89
|
|
|
69
90
|
const codexAuthLoginState = new Map<string, { status: string; accountId?: string; email?: string; error?: string; doneAt?: number }>();
|
|
70
91
|
|
|
71
92
|
function configuredPoolAccount(config: OcxConfig, accountId: string): CodexAccount | null {
|
|
72
|
-
if (!
|
|
73
|
-
return (config.codexAccounts ?? [])
|
|
93
|
+
if (!isValidCodexAccountId(accountId)) return null;
|
|
94
|
+
return (config.codexAccounts ?? [])
|
|
95
|
+
.find(account => account.id === accountId && isSelectableCodexPoolAccount(account)) ?? null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function codexAccountPersistenceConflict(
|
|
99
|
+
config: OcxConfig,
|
|
100
|
+
accountId: string,
|
|
101
|
+
mode: "create" | "reauth",
|
|
102
|
+
): string | undefined {
|
|
103
|
+
if (mode === "reauth") {
|
|
104
|
+
return configuredPoolAccount(config, accountId)
|
|
105
|
+
? undefined
|
|
106
|
+
: "Pool account was removed while login was in progress. Add it again as a new account.";
|
|
107
|
+
}
|
|
108
|
+
const namespaceCollision = codexAccountIdNamespaceCollisionError(config.codexAccountNamespaces, accountId);
|
|
109
|
+
if (namespaceCollision) return namespaceCollision;
|
|
110
|
+
return (config.codexAccounts ?? []).some(account => account.id === accountId)
|
|
111
|
+
|| Boolean(getCodexAccountCredential(accountId))
|
|
112
|
+
? `Account id already exists: ${accountId}`
|
|
113
|
+
: undefined;
|
|
74
114
|
}
|
|
75
115
|
|
|
76
116
|
function isThirtyDayOnlyPlan(plan: string | null | undefined): boolean {
|
|
@@ -95,6 +135,7 @@ function poolAccountDto(
|
|
|
95
135
|
account: CodexAccount,
|
|
96
136
|
quotaResult: PoolQuotaResult,
|
|
97
137
|
hasCredential: boolean,
|
|
138
|
+
paused: boolean,
|
|
98
139
|
): CodexAuthAccountDto {
|
|
99
140
|
const quota = quotaForPlan(quotaResult.quota, account.plan);
|
|
100
141
|
const needsReauth = !hasCredential || quotaResult.needsReauth || isAccountNeedsReauth(account.id);
|
|
@@ -106,6 +147,7 @@ function poolAccountDto(
|
|
|
106
147
|
...(account.plan !== undefined ? { plan: account.plan } : {}),
|
|
107
148
|
...(account.logLabel !== undefined ? { logLabel: account.logLabel } : {}),
|
|
108
149
|
isMain: false,
|
|
150
|
+
paused,
|
|
109
151
|
quota: quota ? { ...quota } : null,
|
|
110
152
|
needsReauth,
|
|
111
153
|
hasCredential,
|
|
@@ -121,11 +163,14 @@ async function resolveResetCreditAuth(
|
|
|
121
163
|
| { ok: false; response: Response }
|
|
122
164
|
> {
|
|
123
165
|
if (accountId === MAIN_CODEX_ACCOUNT_ID) {
|
|
166
|
+
if (hasLegacyMainCodexPoolAccount(runtimeConfig.codexAccounts)) {
|
|
167
|
+
return { ok: false, response: jsonResponse({ error: "Remove the legacy __main__ pool row before using the Desktop account" }, 409) };
|
|
168
|
+
}
|
|
124
169
|
const tokens = readCodexTokens();
|
|
125
170
|
if (!tokens) return { ok: false, response: jsonResponse({ error: "Main Codex account not logged in" }, 401) };
|
|
126
171
|
return { ok: true, isMain: true, accessToken: tokens.access_token, chatgptAccountId: tokens.account_id };
|
|
127
172
|
}
|
|
128
|
-
if (!
|
|
173
|
+
if (!isValidCodexAccountId(accountId)) {
|
|
129
174
|
return { ok: false, response: jsonResponse({ error: "Invalid account id format" }, 400) };
|
|
130
175
|
}
|
|
131
176
|
if (!configuredPoolAccount(runtimeConfig, accountId)) {
|
|
@@ -208,6 +253,10 @@ const MAIN_CACHE_TTL = 5 * 60_000;
|
|
|
208
253
|
const POOL_CACHE_TTL = 5 * 60_000;
|
|
209
254
|
const POOL_QUOTA_REFRESH_CONCURRENCY = 4;
|
|
210
255
|
|
|
256
|
+
function nonEmptyPlan(value: unknown): string | null {
|
|
257
|
+
return typeof value === "string" && value.trim() !== "" ? value : null;
|
|
258
|
+
}
|
|
259
|
+
|
|
211
260
|
function isRuntimeConfig(config: OcxConfig): boolean {
|
|
212
261
|
return !!config && typeof config === "object" && !!config.providers;
|
|
213
262
|
}
|
|
@@ -271,6 +320,8 @@ async function isTerminalMainAuthResponse(resp: Response): Promise<boolean> {
|
|
|
271
320
|
|
|
272
321
|
interface MainAccountInfoFetchResult {
|
|
273
322
|
info: MainAccountInfo;
|
|
323
|
+
/** Present only when this call freshly parsed a WHAM usage response. */
|
|
324
|
+
freshQuota?: Omit<StoredAccountQuota, "updatedAt">;
|
|
274
325
|
/** Present only when this call's WHAM response included `rate_limit_reset_credits.available_count`. */
|
|
275
326
|
freshResetCredits?: number;
|
|
276
327
|
}
|
|
@@ -331,11 +382,12 @@ async function fetchMainAccountInfoAttempt(forceRefresh: boolean, retriesRemaini
|
|
|
331
382
|
const data = (await resp.json()) as WhamUsageResponse;
|
|
332
383
|
const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining);
|
|
333
384
|
if (retried) return retried;
|
|
334
|
-
const
|
|
385
|
+
const plan = nonEmptyPlan(data.plan_type) ?? nonEmptyPlan(cached?.plan) ?? nonEmptyPlan(getMainAccountPlan());
|
|
386
|
+
const quota = parseUsageQuota({ ...data, ...(plan ? { plan_type: plan } : {}) });
|
|
335
387
|
const freshResetCredits = quota?.resetCredits;
|
|
336
388
|
const result = {
|
|
337
389
|
email: data.email ?? null,
|
|
338
|
-
plan
|
|
390
|
+
plan,
|
|
339
391
|
quota,
|
|
340
392
|
ts: Date.now(),
|
|
341
393
|
};
|
|
@@ -349,6 +401,7 @@ async function fetchMainAccountInfoAttempt(forceRefresh: boolean, retriesRemaini
|
|
|
349
401
|
}
|
|
350
402
|
return {
|
|
351
403
|
info: result,
|
|
404
|
+
...(quota ? { freshQuota: quota } : {}),
|
|
352
405
|
...(freshResetCredits !== undefined ? { freshResetCredits } : {}),
|
|
353
406
|
};
|
|
354
407
|
} catch {
|
|
@@ -360,6 +413,12 @@ async function fetchMainAccountInfoAttempt(forceRefresh: boolean, retriesRemaini
|
|
|
360
413
|
interface PoolQuotaResult {
|
|
361
414
|
quota: StoredAccountQuota | null;
|
|
362
415
|
needsReauth: boolean;
|
|
416
|
+
/** Present only when this call freshly parsed a WHAM usage response. */
|
|
417
|
+
freshQuota?: Omit<StoredAccountQuota, "updatedAt">;
|
|
418
|
+
/** Present only when this call's WHAM response included a non-empty `plan_type`. */
|
|
419
|
+
freshPlan?: string;
|
|
420
|
+
/** Credential generation used by this fresh quota request. */
|
|
421
|
+
freshCredentialGeneration?: number;
|
|
363
422
|
/** Present only when this call's WHAM response included `rate_limit_reset_credits.available_count`. */
|
|
364
423
|
freshResetCredits?: number;
|
|
365
424
|
}
|
|
@@ -371,6 +430,7 @@ export interface CodexAuthAccountDto {
|
|
|
371
430
|
plan?: string | null;
|
|
372
431
|
logLabel?: string;
|
|
373
432
|
isMain: boolean;
|
|
433
|
+
paused: boolean;
|
|
374
434
|
quota: (StoredAccountQuota | (Omit<StoredAccountQuota, "updatedAt"> & { updatedAt: number })) | null;
|
|
375
435
|
needsReauth?: boolean;
|
|
376
436
|
hasCredential: boolean;
|
|
@@ -386,20 +446,27 @@ async function fetchPoolAccountQuota(accountId: string, forceRefresh = false, co
|
|
|
386
446
|
return { quota: existing, needsReauth: false };
|
|
387
447
|
}
|
|
388
448
|
try {
|
|
389
|
-
const { accessToken, chatgptAccountId } = await getValidCodexToken(accountId);
|
|
449
|
+
const { accessToken, chatgptAccountId, generation } = await getValidCodexToken(accountId);
|
|
390
450
|
const resp = await fetch("https://chatgpt.com/backend-api/wham/usage", {
|
|
391
451
|
headers: { Authorization: `Bearer ${accessToken}`, "ChatGPT-Account-Id": chatgptAccountId },
|
|
392
452
|
signal: AbortSignal.timeout(8000),
|
|
393
453
|
});
|
|
394
454
|
if (!resp.ok) return { quota: existing ?? null, needsReauth: resp.status === 401 };
|
|
395
455
|
const data = (await resp.json()) as WhamUsageResponse;
|
|
396
|
-
const
|
|
456
|
+
const freshPlan = nonEmptyPlan(data.plan_type) ?? undefined;
|
|
457
|
+
const quota = parseUsageQuota({ ...data, plan_type: freshPlan ?? configuredPlan });
|
|
397
458
|
const freshResetCredits = quota?.resetCredits;
|
|
398
459
|
if (!quota) return { quota: existing ?? null, needsReauth: false };
|
|
460
|
+
if (!isCodexAccountGenerationLive(accountId, generation)) {
|
|
461
|
+
return { quota: getAccountQuota(accountId), needsReauth: false };
|
|
462
|
+
}
|
|
399
463
|
setAccountQuotaFromParsed(accountId, quota);
|
|
400
464
|
return {
|
|
401
465
|
quota: getAccountQuota(accountId),
|
|
402
466
|
needsReauth: false,
|
|
467
|
+
freshQuota: quota,
|
|
468
|
+
freshCredentialGeneration: generation,
|
|
469
|
+
...(freshPlan !== undefined ? { freshPlan } : {}),
|
|
403
470
|
...(freshResetCredits !== undefined ? { freshResetCredits } : {}),
|
|
404
471
|
};
|
|
405
472
|
} catch (e) {
|
|
@@ -441,7 +508,7 @@ export async function primeCodexPoolQuotas(config: OcxConfig, reason: string): P
|
|
|
441
508
|
reconcileMainCodexAccountRuntimeState();
|
|
442
509
|
primeInFlight = (async () => {
|
|
443
510
|
const runtimeConfig = getRuntimeConfig(config);
|
|
444
|
-
const pool = (runtimeConfig.codexAccounts ?? []).filter(
|
|
511
|
+
const pool = (runtimeConfig.codexAccounts ?? []).filter(isSelectableCodexPoolAccount);
|
|
445
512
|
const stale = pool.filter(a => {
|
|
446
513
|
const q = getAccountQuota(a.id);
|
|
447
514
|
return !q || Date.now() - q.updatedAt >= POOL_CACHE_TTL;
|
|
@@ -473,14 +540,14 @@ export function clearCodexQuotaPrimeState(): void {
|
|
|
473
540
|
|
|
474
541
|
export async function listCodexAuthAccounts(config: OcxConfig, forceRefresh = false): Promise<CodexAuthAccountDto[]> {
|
|
475
542
|
const runtimeConfig = getRuntimeConfig(config);
|
|
476
|
-
const poolAccounts = (runtimeConfig.codexAccounts ?? []).filter(
|
|
543
|
+
const poolAccounts = (runtimeConfig.codexAccounts ?? []).filter(isSelectableCodexPoolAccount);
|
|
477
544
|
const mainInfo = await fetchMainAccountInfo(forceRefresh);
|
|
478
545
|
const withQuota = await mapWithConcurrency(poolAccounts, POOL_QUOTA_REFRESH_CONCURRENCY, async a => {
|
|
479
546
|
const cred = getCodexAccountCredential(a.id);
|
|
480
547
|
const quotaResult = cred
|
|
481
548
|
? await fetchPoolAccountQuota(a.id, forceRefresh, a.plan)
|
|
482
549
|
: { quota: null, needsReauth: true };
|
|
483
|
-
return poolAccountDto(a, quotaResult, !!cred);
|
|
550
|
+
return poolAccountDto(a, quotaResult, !!cred, isCodexAccountPaused(runtimeConfig, a.id));
|
|
484
551
|
});
|
|
485
552
|
const hasMainCredential = readCodexTokens() !== null;
|
|
486
553
|
const mainNeedsReauth = !hasMainCredential || isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
|
|
@@ -493,6 +560,7 @@ export async function listCodexAuthAccounts(config: OcxConfig, forceRefresh = fa
|
|
|
493
560
|
email: maskEmail(mainInfo.email) ?? "Codex App login",
|
|
494
561
|
plan: mainInfo.plan,
|
|
495
562
|
isMain: true,
|
|
563
|
+
paused: isCodexAccountPaused(runtimeConfig, MAIN_CODEX_ACCOUNT_ID),
|
|
496
564
|
hasCredential: hasMainCredential,
|
|
497
565
|
needsReauth: mainNeedsReauth,
|
|
498
566
|
quota: mainInfo.quota ? { ...quotaForPlan({ ...mainInfo.quota, updatedAt: Date.now() }, mainInfo.plan) } : null,
|
|
@@ -501,6 +569,69 @@ export async function listCodexAuthAccounts(config: OcxConfig, forceRefresh = fa
|
|
|
501
569
|
return [main, ...withQuota];
|
|
502
570
|
}
|
|
503
571
|
|
|
572
|
+
interface PauseExhaustedResult {
|
|
573
|
+
pausedAccountIds: string[];
|
|
574
|
+
checkedAccountCount: number;
|
|
575
|
+
failedAccountCount: number;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function selectFallbackAfterPause(config: OcxConfig, pausedActiveId: string): void {
|
|
579
|
+
reconcileCodexActiveAfterExclusion(config, pausedActiveId);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
async function pauseExhaustedCodexAccounts(config: OcxConfig): Promise<PauseExhaustedResult> {
|
|
583
|
+
const poolAccounts = (config.codexAccounts ?? []).filter(account => !account.isMain);
|
|
584
|
+
const mainAttempted = readCodexTokens() !== null;
|
|
585
|
+
const [mainResult, poolResults] = await Promise.all([
|
|
586
|
+
fetchMainAccountInfoAttempt(true, 1),
|
|
587
|
+
mapWithConcurrency(poolAccounts, POOL_QUOTA_REFRESH_CONCURRENCY, async account => {
|
|
588
|
+
if (!getCodexAccountCredential(account.id)) return { account, quotaResult: null };
|
|
589
|
+
return {
|
|
590
|
+
account,
|
|
591
|
+
quotaResult: await fetchPoolAccountQuota(account.id, true, account.plan),
|
|
592
|
+
};
|
|
593
|
+
}),
|
|
594
|
+
]);
|
|
595
|
+
|
|
596
|
+
let checkedAccountCount = 0;
|
|
597
|
+
let failedAccountCount = 0;
|
|
598
|
+
const exhaustedIds: string[] = [];
|
|
599
|
+
if (mainAttempted) {
|
|
600
|
+
if (mainResult.freshQuota && mainResult.info.plan) {
|
|
601
|
+
checkedAccountCount += 1;
|
|
602
|
+
if (
|
|
603
|
+
!isCodexAccountPaused(config, MAIN_CODEX_ACCOUNT_ID)
|
|
604
|
+
&& isCodexQuotaExhausted(mainResult.freshQuota, mainResult.info.plan)
|
|
605
|
+
) {
|
|
606
|
+
exhaustedIds.push(MAIN_CODEX_ACCOUNT_ID);
|
|
607
|
+
}
|
|
608
|
+
} else {
|
|
609
|
+
failedAccountCount += 1;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
for (const { account, quotaResult } of poolResults) {
|
|
613
|
+
const currentAccount = (config.codexAccounts ?? []).find(candidate => candidate.id === account.id && !candidate.isMain);
|
|
614
|
+
if (!currentAccount) continue;
|
|
615
|
+
const generation = quotaResult?.freshCredentialGeneration;
|
|
616
|
+
const plan = quotaResult?.freshPlan ?? currentAccount.plan;
|
|
617
|
+
if (!quotaResult?.freshQuota || generation === undefined || !isCodexAccountGenerationLive(account.id, generation) || !plan) {
|
|
618
|
+
failedAccountCount += 1;
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
checkedAccountCount += 1;
|
|
622
|
+
if (!isCodexAccountPaused(config, account.id) && isCodexQuotaExhausted(quotaResult.freshQuota, plan)) {
|
|
623
|
+
exhaustedIds.push(account.id);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
for (const id of exhaustedIds) {
|
|
628
|
+
setCodexAccountPaused(config, id, true);
|
|
629
|
+
clearThreadAccountMapForAccount(id);
|
|
630
|
+
}
|
|
631
|
+
for (const id of exhaustedIds) selectFallbackAfterPause(config, id);
|
|
632
|
+
return { pausedAccountIds: exhaustedIds, checkedAccountCount, failedAccountCount };
|
|
633
|
+
}
|
|
634
|
+
|
|
504
635
|
export async function handleCodexAuthAPI(
|
|
505
636
|
req: Request,
|
|
506
637
|
url: URL,
|
|
@@ -520,17 +651,15 @@ export async function handleCodexAuthAPI(
|
|
|
520
651
|
if (!body.id || !body.email || !body.accessToken || !body.refreshToken || !body.chatgptAccountId) {
|
|
521
652
|
return jsonResponse({ error: "Missing required fields" }, 400);
|
|
522
653
|
}
|
|
523
|
-
if (!
|
|
654
|
+
if (!isValidCodexAccountId(body.id)) {
|
|
524
655
|
return jsonResponse({ error: "Invalid account id format" }, 400);
|
|
525
656
|
}
|
|
526
657
|
if (body.accessToken.length > 10_000 || body.refreshToken.length > 10_000) {
|
|
527
658
|
return jsonResponse({ error: "Input too large" }, 400);
|
|
528
659
|
}
|
|
529
660
|
const runtimeConfig = getRuntimeConfig(config);
|
|
530
|
-
const
|
|
531
|
-
if (
|
|
532
|
-
return jsonResponse({ error: `Account id already exists: ${body.id}` }, 400);
|
|
533
|
-
}
|
|
661
|
+
const preflightConflict = codexAccountPersistenceConflict(runtimeConfig, body.id, "create");
|
|
662
|
+
if (preflightConflict) return jsonResponse({ error: preflightConflict }, 400);
|
|
534
663
|
// 1.1: Duplicate check is scoped by personal vs workspace plan bucket.
|
|
535
664
|
const derivedAccountId = extractAccountId(undefined, body.accessToken) ?? body.chatgptAccountId;
|
|
536
665
|
const collision = checkAccountIdCollision(derivedAccountId, body.email, body.plan);
|
|
@@ -542,6 +671,9 @@ export async function handleCodexAuthAPI(
|
|
|
542
671
|
const exp = typeof payload?.exp === "number" ? payload.exp * 1000 : Date.now() + 3600_000;
|
|
543
672
|
const warmup = await verifyCodexAccountWarmup(body.id, body.accessToken, derivedAccountId);
|
|
544
673
|
if (!warmup.ok) return warmup.response;
|
|
674
|
+
const latestConfig = getRuntimeConfig(config);
|
|
675
|
+
const commitConflict = codexAccountPersistenceConflict(latestConfig, body.id, "create");
|
|
676
|
+
if (commitConflict) return jsonResponse({ error: commitConflict }, 400);
|
|
545
677
|
saveCodexAccountCredential(body.id, {
|
|
546
678
|
accessToken: body.accessToken,
|
|
547
679
|
refreshToken: body.refreshToken,
|
|
@@ -550,9 +682,10 @@ export async function handleCodexAuthAPI(
|
|
|
550
682
|
});
|
|
551
683
|
markCodexAccountValidated(body.id, warmup.validatedAt);
|
|
552
684
|
clearAccountNeedsReauth(body.id);
|
|
685
|
+
const accounts = latestConfig.codexAccounts ?? [];
|
|
553
686
|
accounts.push(withCodexAccountLogLabel({ id: body.id, email: body.email, plan: body.plan, isMain: false }, accounts));
|
|
554
|
-
|
|
555
|
-
saveRuntimeConfig(config,
|
|
687
|
+
latestConfig.codexAccounts = accounts;
|
|
688
|
+
saveRuntimeConfig(config, latestConfig);
|
|
556
689
|
return jsonResponse({ ok: true });
|
|
557
690
|
}
|
|
558
691
|
|
|
@@ -560,6 +693,11 @@ export async function handleCodexAuthAPI(
|
|
|
560
693
|
const id = url.searchParams.get("id");
|
|
561
694
|
if (!id) return jsonResponse({ error: "Missing id" }, 400);
|
|
562
695
|
const runtimeConfig = getRuntimeConfig(config);
|
|
696
|
+
const isLegacyPoolAccount = CODEX_ACCOUNT_ID_RE.test(id)
|
|
697
|
+
&& (runtimeConfig.codexAccounts ?? []).some(account => !account.isMain && account.id === id);
|
|
698
|
+
if (!isValidCodexAccountId(id) && !isLegacyPoolAccount) {
|
|
699
|
+
return jsonResponse({ error: "Invalid account id format" }, 400);
|
|
700
|
+
}
|
|
563
701
|
deleteCodexAccount(runtimeConfig, id);
|
|
564
702
|
saveRuntimeConfig(config, runtimeConfig);
|
|
565
703
|
return jsonResponse({ ok: true });
|
|
@@ -569,8 +707,8 @@ export async function handleCodexAuthAPI(
|
|
|
569
707
|
const body = await req.json().catch(() => ({})) as { id?: unknown; alias?: unknown };
|
|
570
708
|
const id = typeof body.id === "string" ? body.id.trim() : "";
|
|
571
709
|
const alias = typeof body.alias === "string" ? body.alias.trim() : "";
|
|
572
|
-
if (!id || !ACCOUNT_ID_RE.test(id)) return jsonResponse({ error: "Invalid account id format" }, 400);
|
|
573
710
|
if (id === MAIN_CODEX_ACCOUNT_ID) return jsonResponse({ error: "Main Codex account alias is not configurable" }, 400);
|
|
711
|
+
if (!isValidCodexAccountId(id)) return jsonResponse({ error: "Invalid account id format" }, 400);
|
|
574
712
|
if (typeof body.alias !== "string" || alias.length > 80 || /[\x00-\x1f\x7f]/.test(alias)) {
|
|
575
713
|
return jsonResponse({ error: "Alias must be a string of at most 80 printable characters" }, 400);
|
|
576
714
|
}
|
|
@@ -583,6 +721,59 @@ export async function handleCodexAuthAPI(
|
|
|
583
721
|
return jsonResponse({ ok: true, id, alias: alias || null });
|
|
584
722
|
}
|
|
585
723
|
|
|
724
|
+
if (url.pathname === "/api/codex-auth/accounts/pause" && req.method === "PUT") {
|
|
725
|
+
const body = await req.json().catch(() => ({})) as { id?: unknown; paused?: unknown };
|
|
726
|
+
const id = typeof body.id === "string" ? body.id.trim() : "";
|
|
727
|
+
if (id !== MAIN_CODEX_ACCOUNT_ID && !isValidCodexAccountId(id)) {
|
|
728
|
+
return jsonResponse({ error: "Invalid account id format" }, 400);
|
|
729
|
+
}
|
|
730
|
+
if (typeof body.paused !== "boolean") return jsonResponse({ error: "paused must be a boolean" }, 400);
|
|
731
|
+
|
|
732
|
+
const runtimeConfig = getRuntimeConfig(config);
|
|
733
|
+
const exists = id === MAIN_CODEX_ACCOUNT_ID
|
|
734
|
+
|| (runtimeConfig.codexAccounts ?? []).some(account => isSelectableCodexPoolAccount(account) && account.id === id);
|
|
735
|
+
if (!exists) return jsonResponse({ error: "Account not found" }, 404);
|
|
736
|
+
|
|
737
|
+
setCodexAccountPaused(runtimeConfig, id, body.paused);
|
|
738
|
+
if (body.paused) {
|
|
739
|
+
clearThreadAccountMapForAccount(id);
|
|
740
|
+
selectFallbackAfterPause(runtimeConfig, id);
|
|
741
|
+
}
|
|
742
|
+
saveRuntimeConfig(config, runtimeConfig);
|
|
743
|
+
return jsonResponse({
|
|
744
|
+
ok: true,
|
|
745
|
+
id,
|
|
746
|
+
paused: body.paused,
|
|
747
|
+
activeCodexAccountId: getEffectiveActiveCodexAccountId(runtimeConfig) ?? null,
|
|
748
|
+
appliesImmediately: true,
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
if (url.pathname === "/api/codex-auth/accounts/pause-exhausted" && req.method === "PUT") {
|
|
753
|
+
const runtimeConfig = getRuntimeConfig(config);
|
|
754
|
+
const result = await pauseExhaustedCodexAccounts(runtimeConfig);
|
|
755
|
+
const { pausedAccountIds, checkedAccountCount, failedAccountCount } = result;
|
|
756
|
+
if (checkedAccountCount === 0 && failedAccountCount > 0) {
|
|
757
|
+
return jsonResponse({
|
|
758
|
+
ok: false,
|
|
759
|
+
error: "Failed to refresh any Codex account quota",
|
|
760
|
+
checkedAccountCount,
|
|
761
|
+
failedAccountCount,
|
|
762
|
+
}, 502);
|
|
763
|
+
}
|
|
764
|
+
if (pausedAccountIds.length > 0) saveRuntimeConfig(config, runtimeConfig);
|
|
765
|
+
return jsonResponse({
|
|
766
|
+
ok: true,
|
|
767
|
+
pausedAccountIds,
|
|
768
|
+
pausedCount: pausedAccountIds.length,
|
|
769
|
+
checkedAccountCount,
|
|
770
|
+
failedAccountCount,
|
|
771
|
+
complete: failedAccountCount === 0,
|
|
772
|
+
activeCodexAccountId: getEffectiveActiveCodexAccountId(runtimeConfig) ?? null,
|
|
773
|
+
appliesImmediately: true,
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
|
|
586
777
|
// Manual escape from a quota cooldown. Injected Codex routing makes this proxy the only
|
|
587
778
|
// model path for Codex Desktop, so a cooldown that outlives the real upstream limit
|
|
588
779
|
// otherwise leaves editing config.toml as the user's only recovery.
|
|
@@ -594,7 +785,9 @@ export async function handleCodexAuthAPI(
|
|
|
594
785
|
if (url.pathname === "/api/codex-auth/accounts/clear-cooldown" && req.method === "POST") {
|
|
595
786
|
const body = await req.json().catch(() => ({})) as { id?: unknown };
|
|
596
787
|
const id = typeof body.id === "string" ? body.id.trim() : "";
|
|
597
|
-
if (
|
|
788
|
+
if (id !== MAIN_CODEX_ACCOUNT_ID && !isValidCodexAccountId(id)) {
|
|
789
|
+
return jsonResponse({ error: "Invalid account id format" }, 400);
|
|
790
|
+
}
|
|
598
791
|
return jsonResponse({ ok: true, id, cleared: clearCodexAccountCooldown(id) });
|
|
599
792
|
}
|
|
600
793
|
|
|
@@ -602,12 +795,21 @@ export async function handleCodexAuthAPI(
|
|
|
602
795
|
let body: { accountId: string | null };
|
|
603
796
|
try { body = (await req.json()) as typeof body; } catch { return jsonResponse({ error: "Invalid JSON" }, 400); }
|
|
604
797
|
const runtimeConfig = getRuntimeConfig(config);
|
|
798
|
+
const targetAccountId = body.accountId ?? MAIN_CODEX_ACCOUNT_ID;
|
|
799
|
+
if (body.accountId === MAIN_CODEX_ACCOUNT_ID && hasLegacyMainCodexPoolAccount(runtimeConfig.codexAccounts)) {
|
|
800
|
+
return jsonResponse({ error: "Remove the legacy __main__ pool row before selecting the Desktop account" }, 409);
|
|
801
|
+
}
|
|
802
|
+
if (isCodexAccountPaused(runtimeConfig, targetAccountId)) {
|
|
803
|
+
return jsonResponse({ error: "Account is paused" }, 409);
|
|
804
|
+
}
|
|
605
805
|
if (body.accountId != null && body.accountId !== MAIN_CODEX_ACCOUNT_ID) {
|
|
606
|
-
|
|
806
|
+
if (!isValidCodexAccountId(body.accountId)) return jsonResponse({ error: "Invalid account id format" }, 400);
|
|
807
|
+
const exists = (runtimeConfig.codexAccounts ?? [])
|
|
808
|
+
.some(account => isSelectableCodexPoolAccount(account) && account.id === body.accountId);
|
|
607
809
|
if (!exists) return jsonResponse({ error: "Account not found" }, 400);
|
|
608
810
|
}
|
|
609
811
|
runtimeConfig.activeCodexAccountId = body.accountId ?? undefined;
|
|
610
|
-
resetCodexRoutingForManualSelection(
|
|
812
|
+
resetCodexRoutingForManualSelection(targetAccountId);
|
|
611
813
|
saveRuntimeConfig(config, runtimeConfig);
|
|
612
814
|
return jsonResponse({ ok: true, activeCodexAccountId: body.accountId, appliesImmediately: true });
|
|
613
815
|
}
|
|
@@ -615,9 +817,11 @@ export async function handleCodexAuthAPI(
|
|
|
615
817
|
if (url.pathname === "/api/codex-auth/active" && req.method === "GET") {
|
|
616
818
|
const runtimeConfig = getRuntimeConfig(config);
|
|
617
819
|
return jsonResponse({
|
|
618
|
-
activeCodexAccountId: runtimeConfig
|
|
820
|
+
activeCodexAccountId: getEffectiveActiveCodexAccountId(runtimeConfig) ?? null,
|
|
619
821
|
autoSwitchThreshold: runtimeConfig.autoSwitchThreshold ?? 80,
|
|
620
822
|
upstreamFailoverThreshold: runtimeConfig.upstreamFailoverThreshold ?? 3,
|
|
823
|
+
accountPoolStrategy: normalizeAccountPoolStrategy(runtimeConfig.accountPoolStrategy),
|
|
824
|
+
accountPoolStickyLimit: normalizeAccountPoolStickyLimit(runtimeConfig.accountPoolStickyLimit),
|
|
621
825
|
});
|
|
622
826
|
}
|
|
623
827
|
|
|
@@ -633,6 +837,46 @@ export async function handleCodexAuthAPI(
|
|
|
633
837
|
return jsonResponse({ ok: true });
|
|
634
838
|
}
|
|
635
839
|
|
|
840
|
+
if (
|
|
841
|
+
url.pathname === "/api/codex-auth/pool-strategy"
|
|
842
|
+
&& (req.method === "PUT" || req.method === "PATCH")
|
|
843
|
+
) {
|
|
844
|
+
let parsedBody: unknown;
|
|
845
|
+
try { parsedBody = await req.json(); } catch { return jsonResponse({ error: "Invalid JSON" }, 400); }
|
|
846
|
+
if (typeof parsedBody !== "object" || parsedBody === null || Array.isArray(parsedBody)) {
|
|
847
|
+
return jsonResponse({ error: "body must be an object" }, 400);
|
|
848
|
+
}
|
|
849
|
+
const body = parsedBody as { strategy?: unknown; stickyLimit?: unknown };
|
|
850
|
+
if (body.strategy === undefined && body.stickyLimit === undefined) {
|
|
851
|
+
return jsonResponse({ error: "strategy or stickyLimit required" }, 400);
|
|
852
|
+
}
|
|
853
|
+
const runtimeConfig = getRuntimeConfig(config);
|
|
854
|
+
let nextStrategy: NonNullable<ReturnType<typeof parseAccountPoolStrategy>> | undefined;
|
|
855
|
+
let nextSticky: NonNullable<ReturnType<typeof parseAccountPoolStickyLimit>> | undefined;
|
|
856
|
+
if (body.strategy !== undefined) {
|
|
857
|
+
const parsed = parseAccountPoolStrategy(body.strategy);
|
|
858
|
+
if (parsed === null) {
|
|
859
|
+
return jsonResponse({ error: 'strategy must be one of: quota, round-robin, fill-first' }, 400);
|
|
860
|
+
}
|
|
861
|
+
nextStrategy = parsed;
|
|
862
|
+
}
|
|
863
|
+
if (body.stickyLimit !== undefined) {
|
|
864
|
+
const parsed = parseAccountPoolStickyLimit(body.stickyLimit);
|
|
865
|
+
if (parsed === null) {
|
|
866
|
+
return jsonResponse({ error: "stickyLimit must be an integer 1-100" }, 400);
|
|
867
|
+
}
|
|
868
|
+
nextSticky = parsed;
|
|
869
|
+
}
|
|
870
|
+
if (nextStrategy !== undefined) runtimeConfig.accountPoolStrategy = nextStrategy;
|
|
871
|
+
if (nextSticky !== undefined) runtimeConfig.accountPoolStickyLimit = nextSticky;
|
|
872
|
+
saveRuntimeConfig(config, runtimeConfig);
|
|
873
|
+
return jsonResponse({
|
|
874
|
+
ok: true,
|
|
875
|
+
accountPoolStrategy: normalizeAccountPoolStrategy(runtimeConfig.accountPoolStrategy),
|
|
876
|
+
accountPoolStickyLimit: normalizeAccountPoolStickyLimit(runtimeConfig.accountPoolStickyLimit),
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
|
|
636
880
|
if (url.pathname === "/api/codex-auth/failover" && req.method === "PUT") {
|
|
637
881
|
let body: { threshold: number };
|
|
638
882
|
try { body = (await req.json()) as typeof body; } catch { return jsonResponse({ error: "Invalid JSON" }, 400); }
|
|
@@ -734,15 +978,15 @@ export async function handleCodexAuthAPI(
|
|
|
734
978
|
const body = (await req.json().catch(() => ({}))) as { id?: string; reauth?: boolean };
|
|
735
979
|
const requestedAccountId = body.id?.trim();
|
|
736
980
|
const reauth = body.reauth === true;
|
|
737
|
-
if (requestedAccountId && !
|
|
981
|
+
if (requestedAccountId && !isValidCodexAccountId(requestedAccountId)) {
|
|
738
982
|
return jsonResponse({ error: "Invalid account id format" }, 400);
|
|
739
983
|
}
|
|
740
984
|
const accountId = requestedAccountId || `chatgpt-${Date.now()}`;
|
|
741
985
|
const runtimeConfig = getRuntimeConfig(config);
|
|
742
|
-
const
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
}
|
|
986
|
+
const preflightConflict = !reauth
|
|
987
|
+
? codexAccountPersistenceConflict(runtimeConfig, accountId, "create")
|
|
988
|
+
: undefined;
|
|
989
|
+
if (preflightConflict) return jsonResponse({ error: preflightConflict }, 400);
|
|
746
990
|
if (reauth) {
|
|
747
991
|
if (!requestedAccountId) return jsonResponse({ error: "id required for reauth" }, 400);
|
|
748
992
|
if (!configuredPoolAccount(runtimeConfig, accountId)) {
|
|
@@ -860,6 +1104,24 @@ export async function handleCodexAuthAPI(
|
|
|
860
1104
|
break;
|
|
861
1105
|
}
|
|
862
1106
|
|
|
1107
|
+
const latestConfig = getRuntimeConfig(config);
|
|
1108
|
+
const accounts = latestConfig.codexAccounts ?? [];
|
|
1109
|
+
const existingIdx = accounts.findIndex(account => account.id === accountId);
|
|
1110
|
+
const commitConflict = codexAccountPersistenceConflict(
|
|
1111
|
+
latestConfig,
|
|
1112
|
+
accountId,
|
|
1113
|
+
reauth ? "reauth" : "create",
|
|
1114
|
+
);
|
|
1115
|
+
if (commitConflict) {
|
|
1116
|
+
codexAuthLoginState.set(flowId, {
|
|
1117
|
+
status: "error",
|
|
1118
|
+
error: commitConflict,
|
|
1119
|
+
doneAt: Date.now(),
|
|
1120
|
+
});
|
|
1121
|
+
completed = true;
|
|
1122
|
+
break;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
863
1125
|
saveCodexAccountCredential(accountId, {
|
|
864
1126
|
accessToken: cred.access,
|
|
865
1127
|
refreshToken: cred.refresh,
|
|
@@ -872,15 +1134,12 @@ export async function handleCodexAuthAPI(
|
|
|
872
1134
|
setAccountQuotaFromParsed(accountId, quota);
|
|
873
1135
|
}
|
|
874
1136
|
|
|
875
|
-
const latestConfig = getRuntimeConfig(config);
|
|
876
|
-
const accounts = latestConfig.codexAccounts ?? [];
|
|
877
|
-
const existingIdx = accounts.findIndex(a => a.id === accountId);
|
|
878
1137
|
if (existingIdx >= 0) {
|
|
879
1138
|
// Keep the pool id stable; refresh display metadata after a successful login/reauth.
|
|
880
1139
|
accounts[existingIdx] = withCodexAccountLogLabel({
|
|
881
1140
|
...accounts[existingIdx],
|
|
882
1141
|
email,
|
|
883
|
-
plan,
|
|
1142
|
+
plan: plan ?? accounts[existingIdx].plan,
|
|
884
1143
|
isMain: false,
|
|
885
1144
|
}, accounts);
|
|
886
1145
|
latestConfig.codexAccounts = accounts;
|
|
@@ -4,6 +4,7 @@ import { getCodexAccountCredential } from "./account-store";
|
|
|
4
4
|
import { loadConfig } from "../config";
|
|
5
5
|
import { resolveCodexHomeDir } from "./home";
|
|
6
6
|
import { extractAccountId } from "../oauth/chatgpt";
|
|
7
|
+
import { isSelectableCodexPoolAccount } from "./account-id";
|
|
7
8
|
|
|
8
9
|
export interface CodexTokens {
|
|
9
10
|
access_token: string;
|
|
@@ -94,7 +95,7 @@ export function checkAccountIdCollision(
|
|
|
94
95
|
const candidateWorkspace = isWorkspacePlan(plan);
|
|
95
96
|
for (const account of loadConfig().codexAccounts ?? []) {
|
|
96
97
|
if (excludeAccountId && account.id === excludeAccountId) continue;
|
|
97
|
-
if (account
|
|
98
|
+
if (!isSelectableCodexPoolAccount(account)) continue;
|
|
98
99
|
if (isWorkspacePlan(account.plan) !== candidateWorkspace) continue;
|
|
99
100
|
const cred = getCodexAccountCredential(account.id);
|
|
100
101
|
const poolEmail = normalizedEmail(account.email);
|