@coseung2/opencodex 2.8.0-cs.13 → 2.8.0-cs.15
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-BhXIu7c0.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 +139 -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,17 @@ export function computeCodexUsageScore(quota: {
|
|
|
289
298
|
? quota.monthlyPercent
|
|
290
299
|
: CODEX_UNKNOWN_USAGE_SCORE;
|
|
291
300
|
}
|
|
292
|
-
|
|
301
|
+
if (isCodexFiveHourQuotaPlan(plan)
|
|
302
|
+
&& typeof quota.fiveHourPercent === "number"
|
|
303
|
+
&& Number.isFinite(quota.fiveHourPercent)
|
|
304
|
+
&& quota.fiveHourPercent >= 0
|
|
305
|
+
&& quota.fiveHourPercent <= 100) {
|
|
306
|
+
return quota.fiveHourPercent;
|
|
307
|
+
}
|
|
308
|
+
const values = [
|
|
309
|
+
quota.weeklyPercent,
|
|
310
|
+
quota.monthlyPercent,
|
|
311
|
+
]
|
|
293
312
|
.filter((value): value is number => typeof value === "number" && Number.isFinite(value));
|
|
294
313
|
return values.length > 0 ? Math.max(...values) : CODEX_UNKNOWN_USAGE_SCORE;
|
|
295
314
|
}
|
|
@@ -736,7 +755,7 @@ function getEligiblePoolAccounts(
|
|
|
736
755
|
excludeId?: string,
|
|
737
756
|
now = Date.now(),
|
|
738
757
|
quotaScope?: CodexQuotaScope,
|
|
739
|
-
): string[] {
|
|
758
|
+
): readonly string[] {
|
|
740
759
|
const ids = (config.codexAccounts ?? [])
|
|
741
760
|
.filter(account => isSelectableCodexPoolAccount(account)
|
|
742
761
|
&& account.id !== excludeId
|
|
@@ -758,14 +777,20 @@ function getEligiblePoolAccounts(
|
|
|
758
777
|
) {
|
|
759
778
|
ids.unshift(MAIN_CODEX_ACCOUNT_ID);
|
|
760
779
|
}
|
|
761
|
-
return ids;
|
|
780
|
+
if (!config.codexAccountPriorities) return ids;
|
|
781
|
+
return selectPriorityTier(
|
|
782
|
+
ids,
|
|
783
|
+
codexAccountPriorityLookup(config),
|
|
784
|
+
id => hasCodexQuotaHeadroom(config, id),
|
|
785
|
+
pinnedCodexAccountId(config),
|
|
786
|
+
);
|
|
762
787
|
}
|
|
763
788
|
|
|
764
789
|
function listEligibleCodexAccountIds(
|
|
765
790
|
config: OcxConfig,
|
|
766
791
|
now: number,
|
|
767
792
|
quotaScope?: CodexQuotaScope,
|
|
768
|
-
): string[] {
|
|
793
|
+
): readonly string[] {
|
|
769
794
|
return getEligiblePoolAccounts(config, undefined, now, quotaScope);
|
|
770
795
|
}
|
|
771
796
|
|
|
@@ -773,7 +798,7 @@ function stickyLimitForConfig(config: OcxConfig): number {
|
|
|
773
798
|
return normalizeAccountPoolStickyLimit(config.accountPoolStickyLimit);
|
|
774
799
|
}
|
|
775
800
|
|
|
776
|
-
function
|
|
801
|
+
function hasCodexQuotaHeadroom(config: OcxConfig, accountId: string): boolean {
|
|
777
802
|
const threshold = config.autoSwitchThreshold ?? 80;
|
|
778
803
|
if (threshold <= 0) return true;
|
|
779
804
|
const usage = computeCodexUsageScore(getAccountQuota(accountId), getPoolAccountPlan(config, accountId));
|
|
@@ -795,7 +820,7 @@ function pickFillFirstCodexAccount(
|
|
|
795
820
|
if (eligible.length === 0) return null;
|
|
796
821
|
|
|
797
822
|
const active = getEffectiveActiveCodexAccountId(config);
|
|
798
|
-
if (active && eligible.includes(active) &&
|
|
823
|
+
if (active && eligible.includes(active) && hasCodexQuotaHeadroom(config, active)) {
|
|
799
824
|
return active;
|
|
800
825
|
}
|
|
801
826
|
|
|
@@ -806,7 +831,7 @@ function pickFillFirstCodexAccount(
|
|
|
806
831
|
function pickNextFillFirstCodexAccount(
|
|
807
832
|
config: OcxConfig,
|
|
808
833
|
afterId: string | null,
|
|
809
|
-
eligible = listEligibleCodexAccountIds(config, Date.now()),
|
|
834
|
+
eligible: readonly string[] = listEligibleCodexAccountIds(config, Date.now()),
|
|
810
835
|
_now = Date.now(),
|
|
811
836
|
): string | null {
|
|
812
837
|
if (eligible.length === 0) return null;
|
|
@@ -814,7 +839,7 @@ function pickNextFillFirstCodexAccount(
|
|
|
814
839
|
if (!afterId) {
|
|
815
840
|
// Prefer an under-threshold account when starting with no active cursor.
|
|
816
841
|
for (const id of ordered) {
|
|
817
|
-
if (
|
|
842
|
+
if (hasCodexQuotaHeadroom(config, id)) return id;
|
|
818
843
|
}
|
|
819
844
|
return ordered[0] ?? null;
|
|
820
845
|
}
|
|
@@ -829,7 +854,7 @@ function pickNextFillFirstCodexAccount(
|
|
|
829
854
|
const startIdx = stableAll.indexOf(afterId);
|
|
830
855
|
if (startIdx < 0) {
|
|
831
856
|
for (const id of ordered) {
|
|
832
|
-
if (
|
|
857
|
+
if (hasCodexQuotaHeadroom(config, id)) return id;
|
|
833
858
|
}
|
|
834
859
|
return ordered[0] ?? null;
|
|
835
860
|
}
|
|
@@ -840,7 +865,7 @@ function pickNextFillFirstCodexAccount(
|
|
|
840
865
|
const candidate = stableAll[(startIdx + step) % stableAll.length]!;
|
|
841
866
|
if (!eligible.includes(candidate)) continue;
|
|
842
867
|
if (!fallback) fallback = candidate;
|
|
843
|
-
if (
|
|
868
|
+
if (hasCodexQuotaHeadroom(config, candidate)) return candidate;
|
|
844
869
|
}
|
|
845
870
|
return fallback ?? ordered[0] ?? null;
|
|
846
871
|
}
|
|
@@ -955,11 +980,11 @@ export function pickAlternateCodexAccount(
|
|
|
955
980
|
): string | null {
|
|
956
981
|
const strategy = normalizeAccountPoolStrategy(config.accountPoolStrategy);
|
|
957
982
|
if (strategy === "round-robin") {
|
|
958
|
-
const eligible =
|
|
983
|
+
const eligible = getEligiblePoolAccounts(config, excludeId, now, quotaScope);
|
|
959
984
|
return pickRoundRobinAccount(codexPoolKeyForScope(quotaScope), eligible, stickyLimitForConfig(config));
|
|
960
985
|
}
|
|
961
986
|
if (strategy === "fill-first") {
|
|
962
|
-
const eligible =
|
|
987
|
+
const eligible = getEligiblePoolAccounts(config, excludeId, now, quotaScope);
|
|
963
988
|
return pickNextFillFirstCodexAccount(config, excludeId, eligible, now);
|
|
964
989
|
}
|
|
965
990
|
return pickLowestUsageCodexAccount(config, excludeId, now, quotaScope);
|
|
@@ -970,6 +995,11 @@ export function getEffectiveActiveCodexAccountId(config: OcxConfig): string | un
|
|
|
970
995
|
return runtimeActiveCodexAccountId ?? config.activeCodexAccountId;
|
|
971
996
|
}
|
|
972
997
|
|
|
998
|
+
export function isEffectiveCodexAccountPinned(config: OcxConfig): boolean {
|
|
999
|
+
const pinned = pinnedCodexAccountId(config);
|
|
1000
|
+
return pinned !== undefined && pinned === getEffectiveActiveCodexAccountId(config);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
973
1003
|
/**
|
|
974
1004
|
* Automatic strategy / failover cursor only — never mutates `config.activeCodexAccountId`
|
|
975
1005
|
* so an unrelated `saveConfig` cannot persist transient rotation as operator selection.
|
|
@@ -978,10 +1008,18 @@ function rememberActiveCodexAccount(_config: OcxConfig, accountId: string): void
|
|
|
978
1008
|
runtimeActiveCodexAccountId = accountId;
|
|
979
1009
|
}
|
|
980
1010
|
|
|
1011
|
+
function releaseCodexAccountPinFor(config: OcxConfig, accountId: string): boolean {
|
|
1012
|
+
const pinned = pinnedCodexAccountId(config);
|
|
1013
|
+
if (pinned === undefined || pinned === accountId) return false;
|
|
1014
|
+
clearCodexAccountPin(config);
|
|
1015
|
+
return true;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
981
1018
|
/** Persist operator (or quota-strategy) active selection to config + disk. */
|
|
982
1019
|
function setActiveCodexAccount(config: OcxConfig, accountId: string): void {
|
|
983
1020
|
runtimeActiveCodexAccountId = undefined;
|
|
984
|
-
|
|
1021
|
+
const releasedPin = releaseCodexAccountPinFor(config, accountId);
|
|
1022
|
+
if (config.activeCodexAccountId === accountId && !releasedPin) return;
|
|
985
1023
|
config.activeCodexAccountId = accountId;
|
|
986
1024
|
saveConfigPreservingClaudeCode(config);
|
|
987
1025
|
}
|
|
@@ -992,6 +1030,7 @@ function promoteActiveCodexAccount(config: OcxConfig, accountId: string): void {
|
|
|
992
1030
|
setActiveCodexAccount(config, accountId);
|
|
993
1031
|
return;
|
|
994
1032
|
}
|
|
1033
|
+
releaseCodexAccountPinFor(config, accountId);
|
|
995
1034
|
rememberActiveCodexAccount(config, accountId);
|
|
996
1035
|
}
|
|
997
1036
|
|
|
@@ -1010,6 +1049,7 @@ export function reconcileCodexActiveAfterExclusion(
|
|
|
1010
1049
|
if (config.activeCodexAccountId === excludedAccountId) {
|
|
1011
1050
|
config.activeCodexAccountId = undefined;
|
|
1012
1051
|
}
|
|
1052
|
+
clearCodexAccountPin(config, excludedAccountId);
|
|
1013
1053
|
if (!wasEffective) return getEffectiveActiveCodexAccountId(config) ?? null;
|
|
1014
1054
|
|
|
1015
1055
|
runtimeActiveCodexAccountId = undefined;
|
|
@@ -1022,6 +1062,49 @@ function isUnknownUsage(usage: number): boolean {
|
|
|
1022
1062
|
return usage >= CODEX_UNKNOWN_USAGE_SCORE;
|
|
1023
1063
|
}
|
|
1024
1064
|
|
|
1065
|
+
function pickLowestUsageAmong(config: OcxConfig, ids: readonly string[]): string | null {
|
|
1066
|
+
let best: string | null = null;
|
|
1067
|
+
let bestUsage = Number.POSITIVE_INFINITY;
|
|
1068
|
+
for (const id of ids) {
|
|
1069
|
+
const usage = computeCodexUsageScore(getAccountQuota(id), getPoolAccountPlan(config, id));
|
|
1070
|
+
if (usage < bestUsage) {
|
|
1071
|
+
best = id;
|
|
1072
|
+
bestUsage = usage;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
return best;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
/** Move an unbound request up when a higher-priority tier regains headroom. */
|
|
1079
|
+
function pickPriorityPreemption(
|
|
1080
|
+
config: OcxConfig,
|
|
1081
|
+
active: string,
|
|
1082
|
+
now: number,
|
|
1083
|
+
quotaScope?: CodexQuotaScope,
|
|
1084
|
+
): string | null {
|
|
1085
|
+
if (!config.codexAccountPriorities) return null;
|
|
1086
|
+
const eligible = getEligiblePoolAccounts(config, undefined, now, quotaScope);
|
|
1087
|
+
if (eligible.length === 0 || eligible.includes(active)) return null;
|
|
1088
|
+
const pinned = pinnedCodexAccountId(config);
|
|
1089
|
+
if (pinned !== undefined && eligible.includes(pinned) && hasCodexQuotaHeadroom(config, pinned)) return null;
|
|
1090
|
+
const priorityOf = codexAccountPriorityLookup(config);
|
|
1091
|
+
if (priorityOf(eligible[0]!) <= priorityOf(active)) return null;
|
|
1092
|
+
return pickLowestUsageAmong(config, eligible.filter(id => hasCodexQuotaHeadroom(config, id)));
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
/** Retire a persisted manual pin only after its account is durably unavailable or drained. */
|
|
1096
|
+
function releaseDrainedCodexAccountPin(config: OcxConfig): void {
|
|
1097
|
+
const pinned = pinnedCodexAccountId(config);
|
|
1098
|
+
if (pinned === undefined) return;
|
|
1099
|
+
const drained = !isCodexAccountUsable(config, pinned)
|
|
1100
|
+
|| isAccountNeedsReauth(pinned)
|
|
1101
|
+
|| isCodexAccountPaused(config, pinned)
|
|
1102
|
+
|| !hasCodexQuotaHeadroom(config, pinned);
|
|
1103
|
+
if (!drained) return;
|
|
1104
|
+
clearCodexAccountPin(config);
|
|
1105
|
+
saveConfigPreservingClaudeCode(config);
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1025
1108
|
function applyQuotaAutoSwitch(
|
|
1026
1109
|
config: OcxConfig,
|
|
1027
1110
|
active: string,
|
|
@@ -1053,11 +1136,16 @@ function shouldFailover(config: OcxConfig, accountId: string, now: number): bool
|
|
|
1053
1136
|
return !!health && health.consecutiveFailures >= threshold;
|
|
1054
1137
|
}
|
|
1055
1138
|
|
|
1056
|
-
function applyFailureFailover(
|
|
1139
|
+
function applyFailureFailover(
|
|
1140
|
+
config: OcxConfig,
|
|
1141
|
+
active: string,
|
|
1142
|
+
now: number,
|
|
1143
|
+
quotaScope?: CodexQuotaScope,
|
|
1144
|
+
): string {
|
|
1057
1145
|
if (!shouldFailover(config, active, now)) return active;
|
|
1058
|
-
const best = pickAlternateCodexAccount(config, active, now);
|
|
1146
|
+
const best = pickAlternateCodexAccount(config, active, now, quotaScope);
|
|
1059
1147
|
if (best) {
|
|
1060
|
-
promoteActiveCodexAccount(config, best);
|
|
1148
|
+
if (!isIndependentCodexQuotaScope(quotaScope)) promoteActiveCodexAccount(config, best);
|
|
1061
1149
|
return best;
|
|
1062
1150
|
}
|
|
1063
1151
|
return active;
|
|
@@ -1130,6 +1218,8 @@ export function previewCodexAccountForRequest(
|
|
|
1130
1218
|
else return null;
|
|
1131
1219
|
}
|
|
1132
1220
|
|
|
1221
|
+
active = pickPriorityPreemption(config, active, now, quotaScope) ?? active;
|
|
1222
|
+
|
|
1133
1223
|
const threshold = config.autoSwitchThreshold ?? 80;
|
|
1134
1224
|
if (threshold > 0) {
|
|
1135
1225
|
const usage = computeCodexUsageScore(getAccountQuota(active), getPoolAccountPlan(config, active));
|
|
@@ -1157,6 +1247,7 @@ export function resolveCodexAccountForThreadDetailed(
|
|
|
1157
1247
|
now = Date.now(),
|
|
1158
1248
|
quotaScope?: CodexQuotaScope,
|
|
1159
1249
|
): CodexThreadResolution {
|
|
1250
|
+
if (!isIndependentCodexQuotaScope(quotaScope)) releaseDrainedCodexAccountPin(config);
|
|
1160
1251
|
const entry = threadId ? getThreadAffinity(threadId, quotaScope) : undefined;
|
|
1161
1252
|
if (threadId && entry) {
|
|
1162
1253
|
if (isThreadAffinityExpired(entry, now)) {
|
|
@@ -1227,8 +1318,13 @@ export function resolveCodexAccountForThreadDetailed(
|
|
|
1227
1318
|
return { status: "none" };
|
|
1228
1319
|
}
|
|
1229
1320
|
}
|
|
1321
|
+
const preempted = pickPriorityPreemption(config, active, now, quotaScope);
|
|
1322
|
+
if (preempted) {
|
|
1323
|
+
if (!isIndependentCodexQuotaScope(quotaScope)) rememberActiveCodexAccount(config, preempted);
|
|
1324
|
+
active = preempted;
|
|
1325
|
+
}
|
|
1230
1326
|
active = applyQuotaAutoSwitch(config, active, now, quotaScope);
|
|
1231
|
-
active = applyFailureFailover(config, active, now);
|
|
1327
|
+
active = applyFailureFailover(config, active, now, quotaScope);
|
|
1232
1328
|
if (!isCodexAccountUsable(config, active)) {
|
|
1233
1329
|
return hasConfiguredPoolAccount(config, active) ? { status: "selected", accountId: active } : { status: "none" };
|
|
1234
1330
|
}
|
|
@@ -1354,7 +1450,7 @@ export function recordCodexUpstreamOutcome(
|
|
|
1354
1450
|
// The shared native scope is the existing account-wide native behavior:
|
|
1355
1451
|
// threads must leave it and new requests should prefer an eligible account.
|
|
1356
1452
|
// Spark remains isolated so a same-account Terra/Luna combo fallback can run.
|
|
1357
|
-
if (quotaScope === "shared") {
|
|
1453
|
+
if (quotaScope === "shared" && !meta.fixedAccount) {
|
|
1358
1454
|
clearThreadAccountMapForAccount(accountId);
|
|
1359
1455
|
notePoolRotationFailure(POOL_KEY_CODEX, accountId);
|
|
1360
1456
|
if (getEffectiveActiveCodexAccountId(config) === accountId) {
|
|
@@ -1399,17 +1495,21 @@ export function recordCodexUpstreamOutcome(
|
|
|
1399
1495
|
...(prior?.lastProbeAt !== undefined ? { lastProbeAt: prior.lastProbeAt } : {}),
|
|
1400
1496
|
}),
|
|
1401
1497
|
});
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1498
|
+
if (!meta.fixedAccount) {
|
|
1499
|
+
clearThreadAccountMapForAccount(accountId);
|
|
1500
|
+
if (!isIndependentCodexQuotaScope(quotaScope)) {
|
|
1501
|
+
notePoolRotationFailure(POOL_KEY_CODEX, accountId);
|
|
1502
|
+
const effectiveActive = getEffectiveActiveCodexAccountId(config);
|
|
1503
|
+
if (effectiveActive === accountId) {
|
|
1504
|
+
// Same-request 429 retry already picked via excludeAccountId; reuse it so
|
|
1505
|
+
// round-robin does not advance the ring a second time.
|
|
1506
|
+
const reused = meta.promoteAccountId && meta.promoteAccountId !== accountId
|
|
1507
|
+
? meta.promoteAccountId
|
|
1508
|
+
: null;
|
|
1509
|
+
const fallback = reused ?? pickAlternateCodexAccount(config, accountId, now, quotaScope);
|
|
1510
|
+
if (fallback) promoteActiveCodexAccount(config, fallback);
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1413
1513
|
}
|
|
1414
1514
|
return;
|
|
1415
1515
|
}
|
|
@@ -1454,15 +1554,21 @@ export function recordCodexUpstreamOutcome(
|
|
|
1454
1554
|
// thread is still pinned to the FAILING account — a late failure from account A
|
|
1455
1555
|
// must not delete a newer healthy binding to account B (race: T→A, A fails,
|
|
1456
1556
|
// T→B, late A failure must not delete B's mapping).
|
|
1457
|
-
if (failoverReady && meta.threadId) {
|
|
1557
|
+
if (!meta.fixedAccount && failoverReady && meta.threadId) {
|
|
1458
1558
|
deleteThreadAffinitiesForAccount(meta.threadId, accountId);
|
|
1459
1559
|
}
|
|
1460
1560
|
// Once the account is past the failover streak, clear every thread still pinned
|
|
1461
1561
|
// to it — matching 429 affinity behavior so "continue" cannot stay on a bad peer.
|
|
1462
|
-
if (shouldFailover(config, accountId, now)) {
|
|
1562
|
+
if (!meta.fixedAccount && shouldFailover(config, accountId, now)) {
|
|
1463
1563
|
clearThreadAccountMapForAccount(accountId);
|
|
1464
1564
|
}
|
|
1465
|
-
if (
|
|
1565
|
+
if (
|
|
1566
|
+
!meta.fixedAccount
|
|
1567
|
+
&& !isIndependentCodexQuotaScope(quotaScope)
|
|
1568
|
+
&& getEffectiveActiveCodexAccountId(config) === accountId
|
|
1569
|
+
) {
|
|
1570
|
+
applyFailureFailover(config, accountId, now, quotaScope);
|
|
1571
|
+
}
|
|
1466
1572
|
}
|
|
1467
1573
|
|
|
1468
1574
|
export function formatCodexProviderForLog(providerName: string, accountId: string | null, config: OcxConfig): string {
|