@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.
- package/gui/dist/assets/index-MUpaVatk.js +67 -0
- package/gui/dist/index.html +1 -1
- package/package.json +3 -3
- package/packages/ocx-notch/README.md +2 -1
- package/src/adapters/cursor/discovery.ts +6 -2
- package/src/adapters/cursor/effort-map.ts +3 -0
- package/src/adapters/google-antigravity-replay.ts +24 -0
- package/src/adapters/google.ts +16 -11
- package/src/chat/inbound.ts +5 -11
- package/src/cli/account-api.ts +9 -1
- package/src/cli/account-extended.ts +4 -1
- package/src/codex/account-label.ts +14 -1
- package/src/codex/account-lifecycle.ts +12 -1
- package/src/codex/account-namespaces.ts +21 -0
- package/src/codex/account-priority.ts +49 -0
- package/src/codex/account-store.ts +2 -1
- package/src/codex/auth-api.ts +108 -17
- package/src/codex/auth-context.ts +61 -16
- package/src/codex/catalog/metadata.ts +34 -12
- package/src/codex/catalog/parsing.ts +8 -1
- package/src/codex/catalog/provider-fetch.ts +24 -6
- package/src/codex/catalog.ts +1 -1
- package/src/codex/pool-rotation.ts +51 -4
- package/src/codex/quota.ts +154 -35
- package/src/codex/routing.ts +133 -33
- package/src/codex/warmup.ts +193 -85
- package/src/config.ts +84 -1
- package/src/lib/bounded-body.ts +13 -6
- package/src/lib/bun-stream-caps.ts +5 -6
- package/src/lib/redact.ts +13 -0
- package/src/oauth/index.ts +79 -12
- package/src/oauth/log.ts +3 -1
- package/src/oauth/store.ts +31 -8
- package/src/providers/antigravity-models.ts +53 -24
- package/src/providers/codex-capacity.ts +303 -0
- package/src/providers/model-rename-migration.ts +147 -0
- package/src/providers/model-rename-startup.ts +29 -0
- package/src/providers/quota.ts +126 -16
- package/src/providers/registry.ts +258 -38
- package/src/responses/parser.ts +19 -12
- package/src/responses/spill-store.ts +14 -1
- package/src/responses/state.ts +108 -14
- package/src/server/index.ts +9 -1
- package/src/server/management/logs-usage-routes.ts +1 -0
- package/src/server/management/oauth-account-routes.ts +8 -1
- package/src/server/relay.ts +10 -42
- package/src/server/request-log.ts +42 -1
- package/src/server/responses/compact.ts +16 -4
- package/src/server/responses/core.ts +217 -59
- package/src/server/responses/empty-completion-guard.ts +275 -0
- package/src/server/responses/encrypted-payload.ts +54 -39
- package/src/server/responses/fetch-helpers.ts +24 -3
- package/src/server/responses/ws-upstream.ts +318 -0
- package/src/server/sse-frame-buffer.ts +292 -0
- package/src/server/ws-bridge.ts +17 -11
- package/src/types.ts +8 -0
- package/src/usage/log.ts +24 -0
- package/src/usage/summary.ts +152 -2
- package/vendor/ocx-notch/win32-x64/ocx-notch.exe +0 -0
- package/gui/dist/assets/index-BucjyD4I.js +0 -67
package/src/codex/routing.ts
CHANGED
|
@@ -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 = [
|
|
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
|
|
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) &&
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
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 (
|
|
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 {
|