@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
@@ -2,6 +2,11 @@ import { randomUUID } from "node:crypto";
2
2
  import { saveConfigPreservingClaudeCode } from "../config";
3
3
  import { isCodexAccountGenerationLive, readCodexAccountRecord } from "./account-store";
4
4
  import { codexAccountLogLabel } from "./account-label";
5
+ import {
6
+ clearCodexAccountPin,
7
+ codexAccountPriorityLookup,
8
+ pinnedCodexAccountId,
9
+ } from "./account-priority";
5
10
  import { isCodexAccountPaused } from "./account-pause";
6
11
  import { isCodexAccountUsable } from "./account-usability";
7
12
  import { isAccountNeedsReauth, markAccountNeedsReauth } from "./account-runtime-state";
@@ -13,9 +18,10 @@ import {
13
18
  notePoolRotationSuccess,
14
19
  peekRoundRobinAccount,
15
20
  pickRoundRobinAccount,
21
+ selectPriorityTier,
16
22
  seedPoolRotationAccount,
17
23
  } from "./pool-rotation";
18
- import { CODEX_UNKNOWN_USAGE_SCORE, getAccountQuota } from "./quota";
24
+ import { CODEX_UNKNOWN_USAGE_SCORE, getAccountQuota, isCodexFiveHourQuotaPlan } from "./quota";
19
25
  import { MAIN_CODEX_ACCOUNT_ID, getMainAccountPlan } from "./main-account";
20
26
  import { isSelectableCodexPoolAccount } from "./account-id";
21
27
  import type { OcxConfig } from "../types";
@@ -177,6 +183,8 @@ export type CodexUpstreamOutcomeMeta = {
177
183
  modelId?: string;
178
184
  /** When set, clears affinity for this thread immediately on transient failure. */
179
185
  threadId?: string | null;
186
+ /** Suppress Pool rotation and affinity failover for an exact account selector. */
187
+ fixedAccount?: boolean;
180
188
  /**
181
189
  * Probe lease held by this request, when it was admitted through an active
182
190
  * quota cooldown. Only the outcome carrying the current lease may clear the
@@ -279,6 +287,7 @@ function deleteScopedHealth(accountId: string, scope: CodexQuotaScope): void {
279
287
  }
280
288
 
281
289
  export function computeCodexUsageScore(quota: {
290
+ fiveHourPercent?: number;
282
291
  weeklyPercent?: number;
283
292
  monthlyPercent?: number;
284
293
  } | null, plan?: string | null): number {
@@ -289,7 +298,11 @@ export function computeCodexUsageScore(quota: {
289
298
  ? quota.monthlyPercent
290
299
  : CODEX_UNKNOWN_USAGE_SCORE;
291
300
  }
292
- const values = [quota.weeklyPercent, quota.monthlyPercent]
301
+ const values = [
302
+ ...(isCodexFiveHourQuotaPlan(plan) ? [quota.fiveHourPercent] : []),
303
+ quota.weeklyPercent,
304
+ quota.monthlyPercent,
305
+ ]
293
306
  .filter((value): value is number => typeof value === "number" && Number.isFinite(value));
294
307
  return values.length > 0 ? Math.max(...values) : CODEX_UNKNOWN_USAGE_SCORE;
295
308
  }
@@ -736,7 +749,7 @@ function getEligiblePoolAccounts(
736
749
  excludeId?: string,
737
750
  now = Date.now(),
738
751
  quotaScope?: CodexQuotaScope,
739
- ): string[] {
752
+ ): readonly string[] {
740
753
  const ids = (config.codexAccounts ?? [])
741
754
  .filter(account => isSelectableCodexPoolAccount(account)
742
755
  && account.id !== excludeId
@@ -758,14 +771,20 @@ function getEligiblePoolAccounts(
758
771
  ) {
759
772
  ids.unshift(MAIN_CODEX_ACCOUNT_ID);
760
773
  }
761
- return ids;
774
+ if (!config.codexAccountPriorities) return ids;
775
+ return selectPriorityTier(
776
+ ids,
777
+ codexAccountPriorityLookup(config),
778
+ id => hasCodexQuotaHeadroom(config, id),
779
+ pinnedCodexAccountId(config),
780
+ );
762
781
  }
763
782
 
764
783
  function listEligibleCodexAccountIds(
765
784
  config: OcxConfig,
766
785
  now: number,
767
786
  quotaScope?: CodexQuotaScope,
768
- ): string[] {
787
+ ): readonly string[] {
769
788
  return getEligiblePoolAccounts(config, undefined, now, quotaScope);
770
789
  }
771
790
 
@@ -773,7 +792,7 @@ function stickyLimitForConfig(config: OcxConfig): number {
773
792
  return normalizeAccountPoolStickyLimit(config.accountPoolStickyLimit);
774
793
  }
775
794
 
776
- function isActiveUnderFillFirstThreshold(config: OcxConfig, accountId: string): boolean {
795
+ function hasCodexQuotaHeadroom(config: OcxConfig, accountId: string): boolean {
777
796
  const threshold = config.autoSwitchThreshold ?? 80;
778
797
  if (threshold <= 0) return true;
779
798
  const usage = computeCodexUsageScore(getAccountQuota(accountId), getPoolAccountPlan(config, accountId));
@@ -795,7 +814,7 @@ function pickFillFirstCodexAccount(
795
814
  if (eligible.length === 0) return null;
796
815
 
797
816
  const active = getEffectiveActiveCodexAccountId(config);
798
- if (active && eligible.includes(active) && isActiveUnderFillFirstThreshold(config, active)) {
817
+ if (active && eligible.includes(active) && hasCodexQuotaHeadroom(config, active)) {
799
818
  return active;
800
819
  }
801
820
 
@@ -806,7 +825,7 @@ function pickFillFirstCodexAccount(
806
825
  function pickNextFillFirstCodexAccount(
807
826
  config: OcxConfig,
808
827
  afterId: string | null,
809
- eligible = listEligibleCodexAccountIds(config, Date.now()),
828
+ eligible: readonly string[] = listEligibleCodexAccountIds(config, Date.now()),
810
829
  _now = Date.now(),
811
830
  ): string | null {
812
831
  if (eligible.length === 0) return null;
@@ -814,7 +833,7 @@ function pickNextFillFirstCodexAccount(
814
833
  if (!afterId) {
815
834
  // Prefer an under-threshold account when starting with no active cursor.
816
835
  for (const id of ordered) {
817
- if (isActiveUnderFillFirstThreshold(config, id)) return id;
836
+ if (hasCodexQuotaHeadroom(config, id)) return id;
818
837
  }
819
838
  return ordered[0] ?? null;
820
839
  }
@@ -829,7 +848,7 @@ function pickNextFillFirstCodexAccount(
829
848
  const startIdx = stableAll.indexOf(afterId);
830
849
  if (startIdx < 0) {
831
850
  for (const id of ordered) {
832
- if (isActiveUnderFillFirstThreshold(config, id)) return id;
851
+ if (hasCodexQuotaHeadroom(config, id)) return id;
833
852
  }
834
853
  return ordered[0] ?? null;
835
854
  }
@@ -840,7 +859,7 @@ function pickNextFillFirstCodexAccount(
840
859
  const candidate = stableAll[(startIdx + step) % stableAll.length]!;
841
860
  if (!eligible.includes(candidate)) continue;
842
861
  if (!fallback) fallback = candidate;
843
- if (isActiveUnderFillFirstThreshold(config, candidate)) return candidate;
862
+ if (hasCodexQuotaHeadroom(config, candidate)) return candidate;
844
863
  }
845
864
  return fallback ?? ordered[0] ?? null;
846
865
  }
@@ -955,11 +974,11 @@ export function pickAlternateCodexAccount(
955
974
  ): string | null {
956
975
  const strategy = normalizeAccountPoolStrategy(config.accountPoolStrategy);
957
976
  if (strategy === "round-robin") {
958
- const eligible = listEligibleCodexAccountIds(config, now, quotaScope).filter(id => id !== excludeId);
977
+ const eligible = getEligiblePoolAccounts(config, excludeId, now, quotaScope);
959
978
  return pickRoundRobinAccount(codexPoolKeyForScope(quotaScope), eligible, stickyLimitForConfig(config));
960
979
  }
961
980
  if (strategy === "fill-first") {
962
- const eligible = listEligibleCodexAccountIds(config, now, quotaScope).filter(id => id !== excludeId);
981
+ const eligible = getEligiblePoolAccounts(config, excludeId, now, quotaScope);
963
982
  return pickNextFillFirstCodexAccount(config, excludeId, eligible, now);
964
983
  }
965
984
  return pickLowestUsageCodexAccount(config, excludeId, now, quotaScope);
@@ -970,6 +989,11 @@ export function getEffectiveActiveCodexAccountId(config: OcxConfig): string | un
970
989
  return runtimeActiveCodexAccountId ?? config.activeCodexAccountId;
971
990
  }
972
991
 
992
+ export function isEffectiveCodexAccountPinned(config: OcxConfig): boolean {
993
+ const pinned = pinnedCodexAccountId(config);
994
+ return pinned !== undefined && pinned === getEffectiveActiveCodexAccountId(config);
995
+ }
996
+
973
997
  /**
974
998
  * Automatic strategy / failover cursor only — never mutates `config.activeCodexAccountId`
975
999
  * so an unrelated `saveConfig` cannot persist transient rotation as operator selection.
@@ -978,10 +1002,18 @@ function rememberActiveCodexAccount(_config: OcxConfig, accountId: string): void
978
1002
  runtimeActiveCodexAccountId = accountId;
979
1003
  }
980
1004
 
1005
+ function releaseCodexAccountPinFor(config: OcxConfig, accountId: string): boolean {
1006
+ const pinned = pinnedCodexAccountId(config);
1007
+ if (pinned === undefined || pinned === accountId) return false;
1008
+ clearCodexAccountPin(config);
1009
+ return true;
1010
+ }
1011
+
981
1012
  /** Persist operator (or quota-strategy) active selection to config + disk. */
982
1013
  function setActiveCodexAccount(config: OcxConfig, accountId: string): void {
983
1014
  runtimeActiveCodexAccountId = undefined;
984
- if (config.activeCodexAccountId === accountId) return;
1015
+ const releasedPin = releaseCodexAccountPinFor(config, accountId);
1016
+ if (config.activeCodexAccountId === accountId && !releasedPin) return;
985
1017
  config.activeCodexAccountId = accountId;
986
1018
  saveConfigPreservingClaudeCode(config);
987
1019
  }
@@ -992,6 +1024,7 @@ function promoteActiveCodexAccount(config: OcxConfig, accountId: string): void {
992
1024
  setActiveCodexAccount(config, accountId);
993
1025
  return;
994
1026
  }
1027
+ releaseCodexAccountPinFor(config, accountId);
995
1028
  rememberActiveCodexAccount(config, accountId);
996
1029
  }
997
1030
 
@@ -1010,6 +1043,7 @@ export function reconcileCodexActiveAfterExclusion(
1010
1043
  if (config.activeCodexAccountId === excludedAccountId) {
1011
1044
  config.activeCodexAccountId = undefined;
1012
1045
  }
1046
+ clearCodexAccountPin(config, excludedAccountId);
1013
1047
  if (!wasEffective) return getEffectiveActiveCodexAccountId(config) ?? null;
1014
1048
 
1015
1049
  runtimeActiveCodexAccountId = undefined;
@@ -1022,6 +1056,49 @@ function isUnknownUsage(usage: number): boolean {
1022
1056
  return usage >= CODEX_UNKNOWN_USAGE_SCORE;
1023
1057
  }
1024
1058
 
1059
+ function pickLowestUsageAmong(config: OcxConfig, ids: readonly string[]): string | null {
1060
+ let best: string | null = null;
1061
+ let bestUsage = Number.POSITIVE_INFINITY;
1062
+ for (const id of ids) {
1063
+ const usage = computeCodexUsageScore(getAccountQuota(id), getPoolAccountPlan(config, id));
1064
+ if (usage < bestUsage) {
1065
+ best = id;
1066
+ bestUsage = usage;
1067
+ }
1068
+ }
1069
+ return best;
1070
+ }
1071
+
1072
+ /** Move an unbound request up when a higher-priority tier regains headroom. */
1073
+ function pickPriorityPreemption(
1074
+ config: OcxConfig,
1075
+ active: string,
1076
+ now: number,
1077
+ quotaScope?: CodexQuotaScope,
1078
+ ): string | null {
1079
+ if (!config.codexAccountPriorities) return null;
1080
+ const eligible = getEligiblePoolAccounts(config, undefined, now, quotaScope);
1081
+ if (eligible.length === 0 || eligible.includes(active)) return null;
1082
+ const pinned = pinnedCodexAccountId(config);
1083
+ if (pinned !== undefined && eligible.includes(pinned) && hasCodexQuotaHeadroom(config, pinned)) return null;
1084
+ const priorityOf = codexAccountPriorityLookup(config);
1085
+ if (priorityOf(eligible[0]!) <= priorityOf(active)) return null;
1086
+ return pickLowestUsageAmong(config, eligible.filter(id => hasCodexQuotaHeadroom(config, id)));
1087
+ }
1088
+
1089
+ /** Retire a persisted manual pin only after its account is durably unavailable or drained. */
1090
+ function releaseDrainedCodexAccountPin(config: OcxConfig): void {
1091
+ const pinned = pinnedCodexAccountId(config);
1092
+ if (pinned === undefined) return;
1093
+ const drained = !isCodexAccountUsable(config, pinned)
1094
+ || isAccountNeedsReauth(pinned)
1095
+ || isCodexAccountPaused(config, pinned)
1096
+ || !hasCodexQuotaHeadroom(config, pinned);
1097
+ if (!drained) return;
1098
+ clearCodexAccountPin(config);
1099
+ saveConfigPreservingClaudeCode(config);
1100
+ }
1101
+
1025
1102
  function applyQuotaAutoSwitch(
1026
1103
  config: OcxConfig,
1027
1104
  active: string,
@@ -1053,11 +1130,16 @@ function shouldFailover(config: OcxConfig, accountId: string, now: number): bool
1053
1130
  return !!health && health.consecutiveFailures >= threshold;
1054
1131
  }
1055
1132
 
1056
- function applyFailureFailover(config: OcxConfig, active: string, now: number): string {
1133
+ function applyFailureFailover(
1134
+ config: OcxConfig,
1135
+ active: string,
1136
+ now: number,
1137
+ quotaScope?: CodexQuotaScope,
1138
+ ): string {
1057
1139
  if (!shouldFailover(config, active, now)) return active;
1058
- const best = pickAlternateCodexAccount(config, active, now);
1140
+ const best = pickAlternateCodexAccount(config, active, now, quotaScope);
1059
1141
  if (best) {
1060
- promoteActiveCodexAccount(config, best);
1142
+ if (!isIndependentCodexQuotaScope(quotaScope)) promoteActiveCodexAccount(config, best);
1061
1143
  return best;
1062
1144
  }
1063
1145
  return active;
@@ -1130,6 +1212,8 @@ export function previewCodexAccountForRequest(
1130
1212
  else return null;
1131
1213
  }
1132
1214
 
1215
+ active = pickPriorityPreemption(config, active, now, quotaScope) ?? active;
1216
+
1133
1217
  const threshold = config.autoSwitchThreshold ?? 80;
1134
1218
  if (threshold > 0) {
1135
1219
  const usage = computeCodexUsageScore(getAccountQuota(active), getPoolAccountPlan(config, active));
@@ -1157,6 +1241,7 @@ export function resolveCodexAccountForThreadDetailed(
1157
1241
  now = Date.now(),
1158
1242
  quotaScope?: CodexQuotaScope,
1159
1243
  ): CodexThreadResolution {
1244
+ if (!isIndependentCodexQuotaScope(quotaScope)) releaseDrainedCodexAccountPin(config);
1160
1245
  const entry = threadId ? getThreadAffinity(threadId, quotaScope) : undefined;
1161
1246
  if (threadId && entry) {
1162
1247
  if (isThreadAffinityExpired(entry, now)) {
@@ -1227,8 +1312,13 @@ export function resolveCodexAccountForThreadDetailed(
1227
1312
  return { status: "none" };
1228
1313
  }
1229
1314
  }
1315
+ const preempted = pickPriorityPreemption(config, active, now, quotaScope);
1316
+ if (preempted) {
1317
+ if (!isIndependentCodexQuotaScope(quotaScope)) rememberActiveCodexAccount(config, preempted);
1318
+ active = preempted;
1319
+ }
1230
1320
  active = applyQuotaAutoSwitch(config, active, now, quotaScope);
1231
- active = applyFailureFailover(config, active, now);
1321
+ active = applyFailureFailover(config, active, now, quotaScope);
1232
1322
  if (!isCodexAccountUsable(config, active)) {
1233
1323
  return hasConfiguredPoolAccount(config, active) ? { status: "selected", accountId: active } : { status: "none" };
1234
1324
  }
@@ -1354,7 +1444,7 @@ export function recordCodexUpstreamOutcome(
1354
1444
  // The shared native scope is the existing account-wide native behavior:
1355
1445
  // threads must leave it and new requests should prefer an eligible account.
1356
1446
  // Spark remains isolated so a same-account Terra/Luna combo fallback can run.
1357
- if (quotaScope === "shared") {
1447
+ if (quotaScope === "shared" && !meta.fixedAccount) {
1358
1448
  clearThreadAccountMapForAccount(accountId);
1359
1449
  notePoolRotationFailure(POOL_KEY_CODEX, accountId);
1360
1450
  if (getEffectiveActiveCodexAccountId(config) === accountId) {
@@ -1399,17 +1489,21 @@ export function recordCodexUpstreamOutcome(
1399
1489
  ...(prior?.lastProbeAt !== undefined ? { lastProbeAt: prior.lastProbeAt } : {}),
1400
1490
  }),
1401
1491
  });
1402
- clearThreadAccountMapForAccount(accountId);
1403
- notePoolRotationFailure(POOL_KEY_CODEX, accountId);
1404
- const effectiveActive = getEffectiveActiveCodexAccountId(config);
1405
- if (effectiveActive === accountId) {
1406
- // Same-request 429 retry already picked via excludeAccountId — reuse it so
1407
- // round-robin does not advance the ring a second time.
1408
- const reused = meta.promoteAccountId && meta.promoteAccountId !== accountId
1409
- ? meta.promoteAccountId
1410
- : null;
1411
- const fallback = reused ?? pickAlternateCodexAccount(config, accountId, now);
1412
- if (fallback) promoteActiveCodexAccount(config, fallback);
1492
+ if (!meta.fixedAccount) {
1493
+ clearThreadAccountMapForAccount(accountId);
1494
+ if (!isIndependentCodexQuotaScope(quotaScope)) {
1495
+ notePoolRotationFailure(POOL_KEY_CODEX, accountId);
1496
+ const effectiveActive = getEffectiveActiveCodexAccountId(config);
1497
+ if (effectiveActive === accountId) {
1498
+ // Same-request 429 retry already picked via excludeAccountId; reuse it so
1499
+ // round-robin does not advance the ring a second time.
1500
+ const reused = meta.promoteAccountId && meta.promoteAccountId !== accountId
1501
+ ? meta.promoteAccountId
1502
+ : null;
1503
+ const fallback = reused ?? pickAlternateCodexAccount(config, accountId, now, quotaScope);
1504
+ if (fallback) promoteActiveCodexAccount(config, fallback);
1505
+ }
1506
+ }
1413
1507
  }
1414
1508
  return;
1415
1509
  }
@@ -1454,15 +1548,21 @@ export function recordCodexUpstreamOutcome(
1454
1548
  // thread is still pinned to the FAILING account — a late failure from account A
1455
1549
  // must not delete a newer healthy binding to account B (race: T→A, A fails,
1456
1550
  // T→B, late A failure must not delete B's mapping).
1457
- if (failoverReady && meta.threadId) {
1551
+ if (!meta.fixedAccount && failoverReady && meta.threadId) {
1458
1552
  deleteThreadAffinitiesForAccount(meta.threadId, accountId);
1459
1553
  }
1460
1554
  // Once the account is past the failover streak, clear every thread still pinned
1461
1555
  // to it — matching 429 affinity behavior so "continue" cannot stay on a bad peer.
1462
- if (shouldFailover(config, accountId, now)) {
1556
+ if (!meta.fixedAccount && shouldFailover(config, accountId, now)) {
1463
1557
  clearThreadAccountMapForAccount(accountId);
1464
1558
  }
1465
- if (getEffectiveActiveCodexAccountId(config) === accountId) applyFailureFailover(config, accountId, now);
1559
+ if (
1560
+ !meta.fixedAccount
1561
+ && !isIndependentCodexQuotaScope(quotaScope)
1562
+ && getEffectiveActiveCodexAccountId(config) === accountId
1563
+ ) {
1564
+ applyFailureFailover(config, accountId, now, quotaScope);
1565
+ }
1466
1566
  }
1467
1567
 
1468
1568
  export function formatCodexProviderForLog(providerName: string, accountId: string | null, config: OcxConfig): string {