@coseung2/opencodex 2.8.0-cs.13 → 2.8.0-cs.14

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.
Files changed (60) hide show
  1. package/gui/dist/assets/index-MUpaVatk.js +67 -0
  2. package/gui/dist/index.html +1 -1
  3. package/package.json +3 -3
  4. package/packages/ocx-notch/README.md +2 -1
  5. package/src/adapters/cursor/discovery.ts +6 -2
  6. package/src/adapters/cursor/effort-map.ts +3 -0
  7. package/src/adapters/google-antigravity-replay.ts +24 -0
  8. package/src/adapters/google.ts +16 -11
  9. package/src/chat/inbound.ts +5 -11
  10. package/src/cli/account-api.ts +9 -1
  11. package/src/cli/account-extended.ts +4 -1
  12. package/src/codex/account-label.ts +14 -1
  13. package/src/codex/account-lifecycle.ts +12 -1
  14. package/src/codex/account-namespaces.ts +21 -0
  15. package/src/codex/account-priority.ts +49 -0
  16. package/src/codex/account-store.ts +2 -1
  17. package/src/codex/auth-api.ts +108 -17
  18. package/src/codex/auth-context.ts +61 -16
  19. package/src/codex/catalog/metadata.ts +34 -12
  20. package/src/codex/catalog/parsing.ts +8 -1
  21. package/src/codex/catalog/provider-fetch.ts +24 -6
  22. package/src/codex/catalog.ts +1 -1
  23. package/src/codex/pool-rotation.ts +51 -4
  24. package/src/codex/quota.ts +154 -35
  25. package/src/codex/routing.ts +133 -33
  26. package/src/codex/warmup.ts +193 -85
  27. package/src/config.ts +84 -1
  28. package/src/lib/bounded-body.ts +13 -6
  29. package/src/lib/bun-stream-caps.ts +5 -6
  30. package/src/lib/redact.ts +13 -0
  31. package/src/oauth/index.ts +79 -12
  32. package/src/oauth/log.ts +3 -1
  33. package/src/oauth/store.ts +31 -8
  34. package/src/providers/antigravity-models.ts +53 -24
  35. package/src/providers/codex-capacity.ts +303 -0
  36. package/src/providers/model-rename-migration.ts +147 -0
  37. package/src/providers/model-rename-startup.ts +29 -0
  38. package/src/providers/quota.ts +126 -16
  39. package/src/providers/registry.ts +258 -38
  40. package/src/responses/parser.ts +19 -12
  41. package/src/responses/spill-store.ts +14 -1
  42. package/src/responses/state.ts +108 -14
  43. package/src/server/index.ts +9 -1
  44. package/src/server/management/logs-usage-routes.ts +1 -0
  45. package/src/server/management/oauth-account-routes.ts +8 -1
  46. package/src/server/relay.ts +10 -42
  47. package/src/server/request-log.ts +42 -1
  48. package/src/server/responses/compact.ts +16 -4
  49. package/src/server/responses/core.ts +217 -59
  50. package/src/server/responses/empty-completion-guard.ts +275 -0
  51. package/src/server/responses/encrypted-payload.ts +54 -39
  52. package/src/server/responses/fetch-helpers.ts +24 -3
  53. package/src/server/responses/ws-upstream.ts +318 -0
  54. package/src/server/sse-frame-buffer.ts +292 -0
  55. package/src/server/ws-bridge.ts +17 -11
  56. package/src/types.ts +8 -0
  57. package/src/usage/log.ts +24 -0
  58. package/src/usage/summary.ts +152 -2
  59. package/vendor/ocx-notch/win32-x64/ocx-notch.exe +0 -0
  60. package/gui/dist/assets/index-BucjyD4I.js +0 -67
@@ -17,19 +17,36 @@ import {
17
17
  TokenRefreshError,
18
18
  } from "./account-store";
19
19
  import { deleteCodexAccount, reconcileMainCodexAccountRuntimeState } from "./account-lifecycle";
20
+ import {
21
+ appendDefaultCodexAccountNamespace,
22
+ initializeDefaultCodexAccountNamespaces,
23
+ } from "./account-namespaces";
20
24
  import { isCodexAccountPaused, setCodexAccountPaused } from "./account-pause";
25
+ import {
26
+ clearCodexAccountPin,
27
+ getCodexAccountPriority,
28
+ isCodexAccountPriorityKey,
29
+ pinnedCodexAccountId,
30
+ setCodexAccountPin,
31
+ setCodexAccountPriority,
32
+ } from "./account-priority";
21
33
  import {
22
34
  clearCodexAccountCooldown,
23
35
  clearThreadAccountMapForAccount,
24
36
  getEffectiveActiveCodexAccountId,
37
+ isEffectiveCodexAccountPinned,
25
38
  reconcileCodexActiveAfterExclusion,
26
39
  resetCodexRoutingForManualSelection,
27
40
  } from "./routing";
28
41
  import {
42
+ DEFAULT_ACCOUNT_PRIORITY,
43
+ MAX_ACCOUNT_PRIORITY,
44
+ MIN_ACCOUNT_PRIORITY,
29
45
  normalizeAccountPoolStickyLimit,
30
46
  normalizeAccountPoolStrategy,
31
47
  parseAccountPoolStickyLimit,
32
48
  parseAccountPoolStrategy,
49
+ parseAccountPriority,
33
50
  } from "./pool-rotation";
34
51
  import { checkAccountIdCollision, getMainChatgptAccountId, readCodexTokens, readCodexTokensResult } from "./auth-collision";
35
52
  export { checkAccountIdCollision, getMainChatgptAccountId } from "./auth-collision";
@@ -38,6 +55,7 @@ import { clearAccountNeedsReauth, isAccountNeedsReauth, markAccountNeedsReauth }
38
55
  import {
39
56
  clearAccountQuota,
40
57
  getAccountQuota,
58
+ isCodexFiveHourQuotaPlan,
41
59
  isCodexQuotaExhausted,
42
60
  listAccountQuotas,
43
61
  parseUsageQuota,
@@ -66,7 +84,7 @@ import {
66
84
  } from "./main-account-cache";
67
85
  export { clearMainAccountInfoCache } from "./main-account-cache";
68
86
  import { maskEmail } from "../lib/privacy";
69
- import { CodexWarmupError, codexWarmupFailureReason, isCodexWarmupQuotaFailure, warmCodexAccount } from "./warmup";
87
+ import { codexWarmupFailureReason, isCodexWarmupQuotaFailure, warmCodexAccount } from "./warmup";
70
88
  export { maskEmail } from "../lib/privacy";
71
89
  import type { CodexAccount, OcxConfig } from "../types";
72
90
  import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
@@ -169,6 +187,7 @@ function poolAccountDto(
169
187
  quotaResult: PoolQuotaResult,
170
188
  hasCredential: boolean,
171
189
  paused: boolean,
190
+ priority: number,
172
191
  ): CodexAuthAccountDto {
173
192
  const quota = quotaForPlan(quotaResult.quota, account.plan);
174
193
  const needsReauth = !hasCredential || quotaResult.needsReauth || isAccountNeedsReauth(account.id);
@@ -181,6 +200,7 @@ function poolAccountDto(
181
200
  ...(account.logLabel !== undefined ? { logLabel: account.logLabel } : {}),
182
201
  isMain: false,
183
202
  paused,
203
+ priority,
184
204
  quota: quota ? { ...quota } : null,
185
205
  needsReauth,
186
206
  hasCredential,
@@ -258,14 +278,11 @@ async function verifyCodexAccountWarmup(
258
278
  return { ok: true, validatedAt: Date.now() };
259
279
  } catch (err) {
260
280
  const reason = codexWarmupFailureReason(err);
261
- const upstream = err instanceof CodexWarmupError ? err.upstreamDetail : undefined;
262
281
  return {
263
282
  ok: false,
264
283
  error: err,
265
284
  response: jsonResponse({
266
- error: upstream
267
- ? `Codex account warmup failed: ${upstream}`
268
- : "Codex account warmup failed. Reauthenticate the account and try again.",
285
+ error: "Codex account warmup failed. Reauthenticate the account and try again.",
269
286
  code: "codex_warmup_failed",
270
287
  reason,
271
288
  accountId,
@@ -312,6 +329,9 @@ function exhaustedQuotaRetryAt(
312
329
  ): number | undefined {
313
330
  const monthlyOnly = isThirtyDayOnlyPlan(plan);
314
331
  const resetSeconds: number[] = [];
332
+ if (!monthlyOnly && isCodexFiveHourQuotaPlan(plan)
333
+ && quota.fiveHourPercent !== undefined && quota.fiveHourPercent >= 100
334
+ && quota.fiveHourResetAt !== undefined) resetSeconds.push(quota.fiveHourResetAt);
315
335
  if (!monthlyOnly && quota.weeklyPercent !== undefined && quota.weeklyPercent >= 100
316
336
  && quota.weeklyResetAt !== undefined) resetSeconds.push(quota.weeklyResetAt);
317
337
  if (quota.monthlyPercent !== undefined && quota.monthlyPercent >= 100
@@ -402,16 +422,23 @@ function commitCodexAccountConfig(
402
422
 
403
423
  const accounts = persisted.codexAccounts ?? [];
404
424
  const existingIdx = accounts.findIndex(candidate => candidate.id === account.id);
425
+ let committedAccount: CodexAccount;
405
426
  if (existingIdx >= 0) {
406
- accounts[existingIdx] = withCodexAccountLogLabel({
427
+ committedAccount = withCodexAccountLogLabel({
407
428
  ...accounts[existingIdx],
408
429
  ...account,
409
430
  isMain: false,
410
431
  }, accounts);
432
+ accounts[existingIdx] = committedAccount;
411
433
  } else {
412
- accounts.push(withCodexAccountLogLabel({ ...account, isMain: false }, accounts));
434
+ committedAccount = withCodexAccountLogLabel({ ...account, isMain: false }, accounts);
435
+ accounts.push(committedAccount);
413
436
  }
414
437
  persisted.codexAccounts = accounts;
438
+ if (mode === "create" && persisted.codexAccountPickerEnabled !== undefined) {
439
+ initializeDefaultCodexAccountNamespaces(persisted);
440
+ appendDefaultCodexAccountNamespace(persisted, committedAccount);
441
+ }
415
442
  if (quotaPause === "add") setCodexAccountPaused(persisted, account.id, true);
416
443
  else if (quotaPause === "release") setCodexAccountPaused(persisted, account.id, false);
417
444
  return { changed: true, value: { ok: true } as CodexAccountConfigCommit };
@@ -485,7 +512,7 @@ interface MainAccountInfoFetchResult {
485
512
  }
486
513
 
487
514
  export async function fetchMainAccountInfo(forceRefresh = false): Promise<MainAccountInfo> {
488
- const { info } = await fetchMainAccountInfoAttempt(forceRefresh, 1);
515
+ const { info } = await fetchMainAccountInfoAttempt(forceRefresh, 1, forceRefresh);
489
516
  return info;
490
517
  }
491
518
 
@@ -494,16 +521,21 @@ const EMPTY_MAIN_ACCOUNT_INFO: MainAccountInfo = { email: null, plan: null, quot
494
521
  async function retryMainAccountInfoIfIdentityChanged(
495
522
  requestAccountId: string | null,
496
523
  retriesRemaining: number,
524
+ explicitRefresh: boolean,
497
525
  ): Promise<MainAccountInfoFetchResult | null> {
498
526
  const currentAccountId = getMainChatgptAccountId();
499
527
  if (currentAccountId === null || currentAccountId === requestAccountId) return null;
500
528
  reconcileMainCodexAccountRuntimeState();
501
529
  return retriesRemaining > 0
502
- ? fetchMainAccountInfoAttempt(true, retriesRemaining - 1)
530
+ ? fetchMainAccountInfoAttempt(true, retriesRemaining - 1, explicitRefresh)
503
531
  : { info: EMPTY_MAIN_ACCOUNT_INFO };
504
532
  }
505
533
 
506
- async function fetchMainAccountInfoAttempt(forceRefresh: boolean, retriesRemaining: number): Promise<MainAccountInfoFetchResult> {
534
+ async function fetchMainAccountInfoAttempt(
535
+ forceRefresh: boolean,
536
+ retriesRemaining: number,
537
+ explicitRefresh = forceRefresh,
538
+ ): Promise<MainAccountInfoFetchResult> {
507
539
  const writerGeneration = captureConfigGeneration();
508
540
  reconcileMainCodexAccountRuntimeState();
509
541
  const tokenRead = readCodexTokensResult();
@@ -530,7 +562,7 @@ async function fetchMainAccountInfoAttempt(forceRefresh: boolean, retriesRemaini
530
562
  });
531
563
  if (!resp.ok) {
532
564
  const terminalAuthFailure = await isTerminalMainAuthResponse(resp);
533
- const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining);
565
+ const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, explicitRefresh);
534
566
  if (retried) return retried;
535
567
  if (terminalAuthFailure) {
536
568
  clearMainAccountInfoCache();
@@ -539,7 +571,7 @@ async function fetchMainAccountInfoAttempt(forceRefresh: boolean, retriesRemaini
539
571
  return { info: EMPTY_MAIN_ACCOUNT_INFO };
540
572
  }
541
573
  const data = (await resp.json()) as WhamUsageResponse;
542
- const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining);
574
+ const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, explicitRefresh);
543
575
  if (retried) return retried;
544
576
  const plan = nonEmptyPlan(data.plan_type) ?? nonEmptyPlan(cached?.plan) ?? nonEmptyPlan(getMainAccountPlan());
545
577
  const quota = parseUsageQuota({ ...data, ...(plan ? { plan_type: plan } : {}) });
@@ -551,7 +583,9 @@ async function fetchMainAccountInfoAttempt(forceRefresh: boolean, retriesRemaini
551
583
  ts: Date.now(),
552
584
  };
553
585
  setMainAccountInfoCache(result);
554
- clearAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
586
+ // A successful quota probe only proves /wham/usage accepted the token. Only an
587
+ // operator-requested refresh may retract a quarantine raised by Responses traffic.
588
+ if (explicitRefresh) clearAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
555
589
  // Mirror main quota + plan into the shared stores so the rotation engine can
556
590
  // score and auto-switch the main account exactly like a pool account (Option A).
557
591
  setMainAccountPlan(result.plan);
@@ -564,7 +598,7 @@ async function fetchMainAccountInfoAttempt(forceRefresh: boolean, retriesRemaini
564
598
  ...(freshResetCredits !== undefined ? { freshResetCredits } : {}),
565
599
  };
566
600
  } catch {
567
- const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining);
601
+ const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, explicitRefresh);
568
602
  return retried ?? { info: EMPTY_MAIN_ACCOUNT_INFO };
569
603
  }
570
604
  }
@@ -635,6 +669,8 @@ export interface CodexAuthAccountDto {
635
669
  logLabel?: string;
636
670
  isMain: boolean;
637
671
  paused: boolean;
672
+ /** Selection order; higher is considered first. */
673
+ priority: number;
638
674
  quota: (StoredAccountQuota | (Omit<StoredAccountQuota, "updatedAt"> & { updatedAt: number })) | null;
639
675
  needsReauth?: boolean;
640
676
  hasCredential: boolean;
@@ -871,6 +907,10 @@ export function clearCodexQuotaPrimeState(): void {
871
907
  primeInFlight = null;
872
908
  }
873
909
 
910
+ export function effectiveCodexAuthAccountId(config: OcxConfig): string {
911
+ return getEffectiveActiveCodexAccountId(config) ?? MAIN_CODEX_ACCOUNT_ID;
912
+ }
913
+
874
914
  export async function listCodexAuthAccounts(config: OcxConfig, forceRefresh = false): Promise<CodexAuthAccountDto[]> {
875
915
  const runtimeConfig = getRuntimeConfig(config);
876
916
  const poolAccounts = (runtimeConfig.codexAccounts ?? []).filter(isSelectableCodexPoolAccount);
@@ -917,6 +957,7 @@ export async function listCodexAuthAccounts(config: OcxConfig, forceRefresh = fa
917
957
  { quota: null, needsReauth: true },
918
958
  false,
919
959
  isCodexAccountPaused(runtimeConfig, accountId),
960
+ getCodexAccountPriority(runtimeConfig, accountId),
920
961
  )];
921
962
  }
922
963
  const resultGeneration = quotaResult.credentialGeneration ?? quotaResult.freshCredentialGeneration;
@@ -935,6 +976,7 @@ export async function listCodexAuthAccounts(config: OcxConfig, forceRefresh = fa
935
976
  effectiveQuotaResult,
936
977
  true,
937
978
  isCodexAccountPaused(runtimeConfig, accountId),
979
+ getCodexAccountPriority(runtimeConfig, accountId),
938
980
  )];
939
981
  });
940
982
  const hasMainCredential = readCodexTokens() !== null;
@@ -949,9 +991,15 @@ export async function listCodexAuthAccounts(config: OcxConfig, forceRefresh = fa
949
991
  plan: mainInfo.plan,
950
992
  isMain: true,
951
993
  paused: isCodexAccountPaused(runtimeConfig, MAIN_CODEX_ACCOUNT_ID),
994
+ priority: getCodexAccountPriority(runtimeConfig, MAIN_CODEX_ACCOUNT_ID),
952
995
  hasCredential: hasMainCredential,
953
996
  needsReauth: mainNeedsReauth,
954
- quota: mainInfo.quota ? { ...quotaForPlan({ ...mainInfo.quota, updatedAt: Date.now() }, mainInfo.plan) } : null,
997
+ quota: mainInfo.quota ? {
998
+ ...quotaForPlan({
999
+ ...mainInfo.quota,
1000
+ updatedAt: getAccountQuota(MAIN_CODEX_ACCOUNT_ID)?.updatedAt ?? Date.now(),
1001
+ }, mainInfo.plan),
1002
+ } : null,
955
1003
  ...oauthAccountHealthFields("codex", MAIN_CODEX_ACCOUNT_ID, mainHealth),
956
1004
  };
957
1005
  return [main, ...withQuota];
@@ -1174,6 +1222,42 @@ export async function handleCodexAuthAPI(
1174
1222
  });
1175
1223
  }
1176
1224
 
1225
+ if (url.pathname === "/api/codex-auth/accounts/priority" && req.method === "PUT") {
1226
+ let parsedBody: unknown;
1227
+ try { parsedBody = await req.json(); } catch { return jsonResponse({ error: "Invalid JSON" }, 400); }
1228
+ if (typeof parsedBody !== "object" || parsedBody === null || Array.isArray(parsedBody)) {
1229
+ return jsonResponse({ error: "body must be an object" }, 400);
1230
+ }
1231
+ const body = parsedBody as { id?: unknown; priority?: unknown };
1232
+ const id = typeof body.id === "string" ? body.id.trim() : "";
1233
+ if (!isCodexAccountPriorityKey(id)) return jsonResponse({ error: "Invalid account id format" }, 400);
1234
+
1235
+ let priority = DEFAULT_ACCOUNT_PRIORITY;
1236
+ if (body.priority !== null) {
1237
+ const parsed = parseAccountPriority(body.priority);
1238
+ if (parsed === null) {
1239
+ return jsonResponse({
1240
+ error: `priority must be null or an integer ${MIN_ACCOUNT_PRIORITY}-${MAX_ACCOUNT_PRIORITY}`,
1241
+ }, 400);
1242
+ }
1243
+ priority = parsed;
1244
+ }
1245
+
1246
+ const runtimeConfig = getRuntimeConfig(config);
1247
+ const exists = id === MAIN_CODEX_ACCOUNT_ID
1248
+ || (runtimeConfig.codexAccounts ?? []).some(account => isSelectableCodexPoolAccount(account) && account.id === id);
1249
+ if (!exists) return jsonResponse({ error: "Account not found" }, 404);
1250
+ setCodexAccountPriority(runtimeConfig, id, priority);
1251
+ clearCodexAccountPin(runtimeConfig);
1252
+ saveRuntimeConfig(config, runtimeConfig);
1253
+ return jsonResponse({
1254
+ ok: true,
1255
+ id,
1256
+ priority,
1257
+ activeCodexAccountId: getEffectiveActiveCodexAccountId(runtimeConfig) ?? null,
1258
+ });
1259
+ }
1260
+
1177
1261
  if (url.pathname === "/api/codex-auth/accounts/pause-exhausted" && req.method === "PUT") {
1178
1262
  const runtimeConfig = getRuntimeConfig(config);
1179
1263
  const result = await pauseExhaustedCodexAccounts(runtimeConfig);
@@ -1234,6 +1318,8 @@ export async function handleCodexAuthAPI(
1234
1318
  if (!exists) return jsonResponse({ error: "Account not found" }, 400);
1235
1319
  }
1236
1320
  runtimeConfig.activeCodexAccountId = body.accountId ?? undefined;
1321
+ if (body.accountId == null) clearCodexAccountPin(runtimeConfig);
1322
+ else setCodexAccountPin(runtimeConfig, targetAccountId);
1237
1323
  resetCodexRoutingForManualSelection(targetAccountId);
1238
1324
  saveRuntimeConfig(config, runtimeConfig);
1239
1325
  return jsonResponse({ ok: true, activeCodexAccountId: body.accountId, appliesImmediately: true });
@@ -1243,6 +1329,8 @@ export async function handleCodexAuthAPI(
1243
1329
  const runtimeConfig = getRuntimeConfig(config);
1244
1330
  return jsonResponse({
1245
1331
  activeCodexAccountId: getEffectiveActiveCodexAccountId(runtimeConfig) ?? null,
1332
+ pinned: isEffectiveCodexAccountPinned(runtimeConfig),
1333
+ pinnedAccountId: pinnedCodexAccountId(runtimeConfig) ?? null,
1246
1334
  autoSwitchThreshold: runtimeConfig.autoSwitchThreshold ?? 80,
1247
1335
  upstreamFailoverThreshold: runtimeConfig.upstreamFailoverThreshold ?? 3,
1248
1336
  accountPoolStrategy: normalizeAccountPoolStrategy(runtimeConfig.accountPoolStrategy),
@@ -1534,8 +1622,7 @@ export async function handleCodexAuthAPI(
1534
1622
 
1535
1623
  const warmup = await verifyCodexAccountWarmup(accountId, cred.access, oauthAccountId);
1536
1624
  const freshRegistrationQuota = quota ? { quota, ...(plan ? { plan } : {}) } : null;
1537
- const quotaDeferred = !reauth
1538
- && !warmup.ok
1625
+ const quotaDeferred = !warmup.ok
1539
1626
  && mayDeferQuotaWarmup(warmup, freshRegistrationQuota);
1540
1627
  if (!warmup.ok && !quotaDeferred) {
1541
1628
  const body = await warmup.response.json().catch(() => ({})) as { error?: string; reason?: string };
@@ -1569,6 +1656,9 @@ export async function handleCodexAuthAPI(
1569
1656
 
1570
1657
  const releaseOwnedQuotaPause = reauth
1571
1658
  && readCodexAccountRecord(accountId)?.codexQuotaPauseOwned === true;
1659
+ const quotaPauseOwned = !reauth
1660
+ || releaseOwnedQuotaPause
1661
+ || !isCodexAccountPaused(latestConfig, accountId);
1572
1662
  const credentialGeneration = saveCodexAccountCredential(accountId, {
1573
1663
  accessToken: cred.access,
1574
1664
  refreshToken: cred.refresh,
@@ -1605,6 +1695,7 @@ export async function handleCodexAuthAPI(
1605
1695
  accountId,
1606
1696
  codexWarmupFailureReason(warmup.error),
1607
1697
  exhaustedQuotaRetryAt(freshRegistrationQuota.quota, freshRegistrationQuota.plan),
1698
+ quotaPauseOwned,
1608
1699
  );
1609
1700
  } else if (warmup.ok) {
1610
1701
  markCodexAccountValidated(accountId, warmup.validatedAt, credentialGeneration);
@@ -7,7 +7,8 @@ import {
7
7
  isCodexAccountGenerationLive,
8
8
  } from "./account-store";
9
9
  import { ConfigMutationLockError } from "../config";
10
- import { markAccountNeedsReauth } from "./account-runtime-state";
10
+ import { isAccountNeedsReauth, markAccountNeedsReauth } from "./account-runtime-state";
11
+ import { isCodexAccountPaused } from "./account-pause";
11
12
  import { isCodexAccountUsable } from "./account-usability";
12
13
  import { reconcileMainCodexAccountRuntimeState } from "./account-lifecycle";
13
14
  import { MAIN_CODEX_ACCOUNT_ID, getMainAccountToken } from "./main-account";
@@ -38,6 +39,8 @@ export type CodexAuthContext =
38
39
  generation: number;
39
40
  accessToken: string;
40
41
  chatgptAccountId: string;
42
+ /** Exact account selector: suppresses Pool rotation and failover. */
43
+ fixedAccount?: boolean;
41
44
  /**
42
45
  * Set when this request was admitted through an active quota cooldown as
43
46
  * the account's single probe. Must be echoed into the upstream outcome so
@@ -57,6 +60,8 @@ export type CodexAuthContext =
57
60
  writerGeneration: number;
58
61
  accessToken: string;
59
62
  chatgptAccountId: string;
63
+ /** Exact account selector: suppresses Pool rotation and failover. */
64
+ fixedAccount?: boolean;
60
65
  /** See `pool.probeLeaseId`. */
61
66
  probeLeaseId?: string;
62
67
  quotaScope?: CodexQuotaScope;
@@ -100,8 +105,8 @@ export class CodexAuthContextError extends Error {
100
105
  }
101
106
 
102
107
  export class CodexPoolAuthenticationError extends Error {
103
- constructor() {
104
- super("OpenAI account pool has no usable account credential");
108
+ constructor(message = "OpenAI account pool has no usable account credential") {
109
+ super(message);
105
110
  this.name = "CodexPoolAuthenticationError";
106
111
  }
107
112
  }
@@ -154,22 +159,31 @@ export function cooldownAccountLabel(accountId: string): string {
154
159
  * as HTTP. The bare "cooling down" string left users with no route but commenting out the
155
160
  * injected `openai_base_url` in config.toml.
156
161
  */
157
- export function cooldownErrorMessage(err: CodexAccountCooldownError): string {
162
+ export function cooldownErrorMessage(err: CodexAccountCooldownError, accountSelector?: string): string {
158
163
  const until = new Date(err.cooldownUntil).toISOString();
159
164
  const scope = err.quotaScope === "spark"
160
165
  ? "Spark quota"
161
166
  : err.quotaScope === "shared"
162
167
  ? "shared native quota"
163
168
  : null;
164
- return `Selected Codex account (${cooldownAccountLabel(err.accountId)})${scope ? ` ${scope} is` : " is"} cooling down until ${until}`
165
- + ` (source: ${err.cooldownSource ?? "default"}).`
166
- + ` Run 'ocx account list openai' to find the id, then`
167
- + ` 'ocx account clear-cooldown openai <id>' to lift it, or switch accounts with 'ocx account use openai <id>'.`;
169
+ const selected = accountSelector
170
+ ? `Selected Codex account selector (${accountSelector})`
171
+ : `Selected Codex account (${cooldownAccountLabel(err.accountId)})`;
172
+ const recovery = accountSelector
173
+ ? " This request is pinned to that selector and will not switch accounts; choose another account-qualified model or retry later."
174
+ : " Run 'ocx account list openai' to find the id, then"
175
+ + " 'ocx account clear-cooldown openai <id>' to lift it, or switch accounts with 'ocx account use openai <id>'.";
176
+ return `${selected}${scope ? ` ${scope} is` : " is"} cooling down until ${until}`
177
+ + ` (source: ${err.cooldownSource ?? "default"}).${recovery}`;
168
178
  }
169
179
 
170
180
  /** HTTP form of {@link cooldownErrorMessage}, carrying Retry-After for well-behaved clients. */
171
- export function cooldownErrorResponse(err: CodexAccountCooldownError, now = Date.now()): Response {
172
- const res = formatErrorResponse(429, "rate_limit_error", cooldownErrorMessage(err));
181
+ export function cooldownErrorResponse(
182
+ err: CodexAccountCooldownError,
183
+ now = Date.now(),
184
+ accountSelector?: string,
185
+ ): Response {
186
+ const res = formatErrorResponse(429, "rate_limit_error", cooldownErrorMessage(err, accountSelector));
173
187
  const headers = new Headers(res.headers);
174
188
  headers.set("Retry-After", String(Math.max(1, Math.ceil((err.cooldownUntil - now) / 1000))));
175
189
  return new Response(res.body, { status: res.status, headers });
@@ -195,6 +209,8 @@ export function shouldMarkAccountNeedsReauthForCodexAuthFailure(cause: unknown):
195
209
 
196
210
  export interface ResolveCodexAuthContextOptions {
197
211
  excludeAccountId?: string;
212
+ /** Resolve exactly this account without consulting or mutating Pool selection. */
213
+ accountId?: string;
198
214
  /** Final native model selected for this request, used to select its quota group. */
199
215
  modelId?: string;
200
216
  }
@@ -206,14 +222,20 @@ export async function resolveCodexAuthContext(
206
222
  options: ResolveCodexAuthContextOptions = {},
207
223
  ): Promise<CodexAuthContext> {
208
224
  const writerGeneration = captureConfigGeneration();
209
- if (mode === "direct") {
225
+ const fixedAccountId = options.accountId;
226
+ if (fixedAccountId !== undefined && options.excludeAccountId !== undefined) {
227
+ throw new Error("Codex auth context cannot select and exclude an account simultaneously");
228
+ }
229
+ if (mode === "direct" && fixedAccountId === undefined) {
210
230
  if (!hasCallerCodexBearer(headers)) throw new CodexDirectAuthenticationError();
211
231
  return { kind: "main", accountId: null };
212
232
  }
213
233
  reconcileMainCodexAccountRuntimeState();
214
234
  const threadId = headers.get("x-codex-parent-thread-id");
215
235
  const quotaScope = codexQuotaScopeForModel(options.modelId);
216
- const resolution = options.excludeAccountId
236
+ const resolution = fixedAccountId !== undefined
237
+ ? { status: "selected" as const, accountId: fixedAccountId }
238
+ : options.excludeAccountId
217
239
  ? (() => {
218
240
  const accountId = pickAlternateCodexAccount(config, options.excludeAccountId!, Date.now(), quotaScope);
219
241
  return accountId
@@ -223,13 +245,28 @@ export async function resolveCodexAuthContext(
223
245
  : resolveCodexAccountForThreadDetailed(threadId, config, Date.now(), quotaScope);
224
246
  if (resolution.status === "expired") throw new CodexThreadAffinityExpiredError(resolution.accountId);
225
247
  let accountId = resolution.status === "selected" ? resolution.accountId : null;
226
- if (!accountId) throw new CodexPoolAuthenticationError();
248
+ if (!accountId) {
249
+ throw new CodexPoolAuthenticationError(
250
+ fixedAccountId !== undefined ? "Selected Codex account is unavailable" : undefined,
251
+ );
252
+ }
253
+ if (fixedAccountId !== undefined) {
254
+ if (isCodexAccountPaused(config, accountId)) {
255
+ throw new CodexPoolAuthenticationError("Selected Codex account is unavailable");
256
+ }
257
+ if (isAccountNeedsReauth(accountId)) {
258
+ throw new CodexPoolAuthenticationError("Selected Codex account needs reauthentication");
259
+ }
260
+ if (!isCodexAccountUsable(config, accountId)) {
261
+ throw new CodexPoolAuthenticationError("Selected Codex account is unavailable");
262
+ }
263
+ }
227
264
  // Lazy prime: if the selected account has no quota yet, the pool is likely
228
265
  // unprimed (dashboard never opened, or startup prime was blocked). Kick a
229
266
  // best-effort prime so the NEXT routing decision has real scores. This never
230
267
  // blocks the current request, and the helper's single-flight guard collapses
231
268
  // repeated triggers into one pass.
232
- if (!getAccountQuota(accountId)) {
269
+ if (fixedAccountId === undefined && !getAccountQuota(accountId)) {
233
270
  import("./auth-api")
234
271
  .then(({ primeCodexPoolQuotas }) => primeCodexPoolQuotas(config, "pre-route"))
235
272
  .catch(() => {});
@@ -244,6 +281,9 @@ export async function resolveCodexAuthContext(
244
281
  let probeLeaseId: string | undefined;
245
282
  let probeQuotaScope: CodexQuotaScope | undefined;
246
283
  if (cooldownUntil) {
284
+ if (fixedAccountId !== undefined) {
285
+ throw new CodexAccountCooldownError(accountId, cooldownUntil, cooldown?.cooldownSource, cooldown?.quotaScope);
286
+ }
247
287
  probeQuotaScope = cooldown?.quotaScope;
248
288
  probeLeaseId = probeQuotaScope
249
289
  ? tryAcquireCodexQuotaScopeProbeLease(accountId, probeQuotaScope) ?? undefined
@@ -260,7 +300,9 @@ export async function resolveCodexAuthContext(
260
300
  // Nothing will reach upstream, so give the probe back instead of burning it.
261
301
  if (probeLeaseId && probeQuotaScope) releaseCodexQuotaScopeProbeLease(accountId, probeQuotaScope, probeLeaseId);
262
302
  else if (probeLeaseId) releaseCodexQuotaProbeLease(accountId, probeLeaseId);
263
- throw new CodexPoolAuthenticationError();
303
+ throw new CodexPoolAuthenticationError(
304
+ fixedAccountId !== undefined ? "Selected Codex account is unavailable" : undefined,
305
+ );
264
306
  }
265
307
  return {
266
308
  kind: "main-pool",
@@ -268,6 +310,7 @@ export async function resolveCodexAuthContext(
268
310
  writerGeneration,
269
311
  accessToken: token.accessToken,
270
312
  chatgptAccountId: token.chatgptAccountId,
313
+ ...(fixedAccountId !== undefined ? { fixedAccount: true } : {}),
271
314
  ...(quotaScope ? { quotaScope } : {}),
272
315
  ...(probeLeaseId ? { probeLeaseId } : {}),
273
316
  ...(probeQuotaScope ? { probeQuotaScope } : {}),
@@ -283,6 +326,7 @@ export async function resolveCodexAuthContext(
283
326
  generation: token.generation,
284
327
  accessToken: token.accessToken,
285
328
  chatgptAccountId: token.chatgptAccountId,
329
+ ...(fixedAccountId !== undefined ? { fixedAccount: true } : {}),
286
330
  ...(quotaScope ? { quotaScope } : {}),
287
331
  ...(probeLeaseId ? { probeLeaseId } : {}),
288
332
  ...(probeQuotaScope ? { probeQuotaScope } : {}),
@@ -312,7 +356,8 @@ export function applyCodexAuthContextToProvider(
312
356
  ctx: CodexAuthContext,
313
357
  mode: CodexAccountMode | undefined,
314
358
  ): OcxRuntimeProviderConfig {
315
- if (mode !== "pool" || (ctx.kind !== "pool" && ctx.kind !== "main-pool") || provider.authMode !== "forward") return provider;
359
+ if ((ctx.kind !== "pool" && ctx.kind !== "main-pool") || provider.authMode !== "forward") return provider;
360
+ if (mode !== "pool" && ctx.fixedAccount !== true) return provider;
316
361
  return {
317
362
  ...provider,
318
363
  _codexAccountOverride: {
@@ -35,14 +35,18 @@ import { filterSupportedNativeSlugs } from "./parsing";
35
35
  import type { RawEntry } from "./parsing";
36
36
  import { readCurrentCatalogOrCache, unique } from "./bundled";
37
37
 
38
+ export const NATIVE_DAYBREAK_BLUE_MODEL = "gpt-daybreak-blue-latest";
39
+
38
40
  export const NATIVE_OPENAI_MODELS = [
39
41
  "gpt-5.5", "gpt-5.4", "gpt-5.4-mini", "gpt-5.3-codex-spark",
40
42
  "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna",
43
+ NATIVE_DAYBREAK_BLUE_MODEL,
41
44
  ];
42
45
 
43
46
  export const DOCUMENTED_NATIVE_OPENAI_ADDITIONS = [
44
47
  "gpt-5.3-codex-spark",
45
48
  "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna",
49
+ NATIVE_DAYBREAK_BLUE_MODEL,
46
50
  ];
47
51
 
48
52
  export const SUPPORTED_NATIVE_OPENAI_SLUGS = new Set(NATIVE_OPENAI_MODELS);
@@ -53,15 +57,18 @@ export function isUnsupportedOpenAiNativeSlug(slug: string): boolean {
53
57
  return /^(?:gpt|codex)-/.test(slug);
54
58
  }
55
59
 
56
- export const NATIVE_GPT56_CONTEXT_WINDOW = 372_000;
60
+ /** Measured native family contract: 922k input plus up to 128k output. */
61
+ export const NATIVE_GPT56_CONTEXT_WINDOW = 1_050_000;
62
+ export const NATIVE_GPT56_MAX_INPUT_TOKENS = 922_000;
57
63
 
58
- export const NATIVE_OPENAI_CONTEXT_OVERRIDES: Record<string, { contextWindow?: number; maxContextWindow?: number }> = {
64
+ export const NATIVE_OPENAI_CONTEXT_OVERRIDES: Record<string, { contextWindow?: number; maxContextWindow?: number; maxInputTokens?: number }> = {
59
65
  "gpt-5.5": { contextWindow: 272_000, maxContextWindow: 272_000 },
60
66
  "gpt-5.4": { contextWindow: 1_000_000, maxContextWindow: 1_000_000 },
61
67
  "gpt-5.3-codex-spark": { contextWindow: 100_000, maxContextWindow: 100_000 },
62
- "gpt-5.6-sol": { contextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxContextWindow: NATIVE_GPT56_CONTEXT_WINDOW },
63
- "gpt-5.6-terra": { contextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxContextWindow: NATIVE_GPT56_CONTEXT_WINDOW },
64
- "gpt-5.6-luna": { contextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxContextWindow: NATIVE_GPT56_CONTEXT_WINDOW },
68
+ "gpt-5.6-sol": { contextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxContextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxInputTokens: NATIVE_GPT56_MAX_INPUT_TOKENS },
69
+ "gpt-5.6-terra": { contextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxContextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxInputTokens: NATIVE_GPT56_MAX_INPUT_TOKENS },
70
+ "gpt-5.6-luna": { contextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxContextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxInputTokens: NATIVE_GPT56_MAX_INPUT_TOKENS },
71
+ [NATIVE_DAYBREAK_BLUE_MODEL]: { contextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxContextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxInputTokens: NATIVE_GPT56_MAX_INPUT_TOKENS },
65
72
  };
66
73
 
67
74
  export function nativeOpenAiContextWindow(slug: string): number | undefined {
@@ -71,6 +78,10 @@ export function nativeOpenAiContextWindow(slug: string): number | undefined {
71
78
  : undefined);
72
79
  }
73
80
 
81
+ export function nativeOpenAiMaxInputTokens(slug: string): number | undefined {
82
+ return NATIVE_OPENAI_CONTEXT_OVERRIDES[slug]?.maxInputTokens;
83
+ }
84
+
74
85
  export function nativeInputModalities(slug: string): string[] {
75
86
  const upstream = UPSTREAM_NATIVE_ENTRIES.get(slug);
76
87
  if (Array.isArray(upstream?.input_modalities) && upstream!.input_modalities!.length > 0) {
@@ -144,13 +155,24 @@ export function applyNativeVisibility(entries: RawEntry[], disabledNative: Set<s
144
155
  return entries;
145
156
  }
146
157
 
147
- export const UPSTREAM_NATIVE_ENTRIES: Map<string, RawEntry> = new Map(
148
- ((upstreamModelsSnapshot as unknown as { models?: RawEntry[] }).models ?? [])
149
- .filter(m => typeof m.slug === "string"
150
- && SUPPORTED_NATIVE_OPENAI_SLUGS.has(m.slug as string)
151
- && (m.slug as string).startsWith("gpt-5.6-"))
152
- .map(m => [m.slug as string, m]),
153
- );
158
+ export const UPSTREAM_NATIVE_ENTRIES: Map<string, RawEntry> = (() => {
159
+ const entries = new Map(
160
+ ((upstreamModelsSnapshot as unknown as { models?: RawEntry[] }).models ?? [])
161
+ .filter(m => typeof m.slug === "string"
162
+ && SUPPORTED_NATIVE_OPENAI_SLUGS.has(m.slug as string)
163
+ && (m.slug as string).startsWith("gpt-5.6-"))
164
+ .map(m => [m.slug as string, m] as const),
165
+ );
166
+ const sol = entries.get("gpt-5.6-sol");
167
+ if (sol) {
168
+ entries.set(NATIVE_DAYBREAK_BLUE_MODEL, {
169
+ ...JSON.parse(JSON.stringify(sol)) as RawEntry,
170
+ slug: NATIVE_DAYBREAK_BLUE_MODEL,
171
+ display_name: "GPT Daybreak Blue",
172
+ });
173
+ }
174
+ return entries;
175
+ })();
154
176
 
155
177
  export function upstreamNativeEntry(slug: string): RawEntry | null {
156
178
  const entry = UPSTREAM_NATIVE_ENTRIES.get(slug);
@@ -243,13 +243,20 @@ export function isNativeOpenAiEntry(entry: RawEntry): boolean {
243
243
  return typeof entry.slug === "string" && !entry.slug.includes("/");
244
244
  }
245
245
 
246
+ function nativeAutoCompactLimit(contextWindow: number, maxInputTokens?: number): number {
247
+ const ninetyPercent = Math.floor(contextWindow * 0.9);
248
+ return typeof maxInputTokens === "number" && maxInputTokens > 0
249
+ ? Math.min(ninetyPercent, maxInputTokens, contextWindow)
250
+ : ninetyPercent;
251
+ }
252
+
246
253
  export function applyNativeOpenAiContextOverride(entry: RawEntry): void {
247
254
  if (!isNativeOpenAiEntry(entry)) return;
248
255
  const override = NATIVE_OPENAI_CONTEXT_OVERRIDES[entry.slug as string];
249
256
  if (!override) return;
250
257
  if (typeof override.contextWindow === "number") {
251
258
  entry.context_window = override.contextWindow;
252
- entry.auto_compact_token_limit = Math.floor(override.contextWindow * 0.9);
259
+ entry.auto_compact_token_limit = nativeAutoCompactLimit(override.contextWindow, override.maxInputTokens);
253
260
  }
254
261
  if (typeof override.maxContextWindow === "number") {
255
262
  entry.max_context_window = override.maxContextWindow;