

























@@ -0,0 +1,138 @@
1+import { normalizeProviderId } from "../provider-id.js";
2+import type { AuthProfileStore, ProfileUsageStats } from "./types.js";
3+4+export function isAuthCooldownBypassedForProvider(provider: string | undefined): boolean {
5+const normalized = normalizeProviderId(provider ?? "");
6+return normalized === "openrouter" || normalized === "kilocode";
7+}
8+9+export function resolveProfileUnusableUntil(
10+stats: Pick<ProfileUsageStats, "cooldownUntil" | "disabledUntil">,
11+): number | null {
12+const values = [stats.cooldownUntil, stats.disabledUntil]
13+.filter((value): value is number => typeof value === "number")
14+.filter((value) => Number.isFinite(value) && value > 0);
15+if (values.length === 0) {
16+return null;
17+}
18+return Math.max(...values);
19+}
20+21+export function isActiveUnusableWindow(until: number | undefined, now: number): boolean {
22+return typeof until === "number" && Number.isFinite(until) && until > 0 && now < until;
23+}
24+25+export function shouldBypassModelScopedCooldown(
26+stats: Pick<ProfileUsageStats, "cooldownReason" | "cooldownModel" | "disabledUntil">,
27+now: number,
28+forModel?: string,
29+): boolean {
30+return !!(
31+forModel &&
32+stats.cooldownReason === "rate_limit" &&
33+stats.cooldownModel &&
34+stats.cooldownModel !== forModel &&
35+!isActiveUnusableWindow(stats.disabledUntil, now)
36+);
37+}
38+39+/**
40+ * Check if a profile is currently in cooldown (due to rate limits, overload, or other transient failures).
41+ */
42+export function isProfileInCooldown(
43+store: AuthProfileStore,
44+profileId: string,
45+now?: number,
46+forModel?: string,
47+): boolean {
48+if (isAuthCooldownBypassedForProvider(store.profiles[profileId]?.provider)) {
49+return false;
50+}
51+const stats = store.usageStats?.[profileId];
52+if (!stats) {
53+return false;
54+}
55+const ts = now ?? Date.now();
56+// Model-aware bypass: if the cooldown was caused by a rate_limit on a
57+// specific model and the caller is requesting a *different* model, allow it.
58+// We still honour any active billing/auth disable (`disabledUntil`) — those
59+// are profile-wide and must not be short-circuited by model scoping.
60+if (shouldBypassModelScopedCooldown(stats, ts, forModel)) {
61+return false;
62+}
63+const unusableUntil = resolveProfileUnusableUntil(stats);
64+return unusableUntil ? ts < unusableUntil : false;
65+}
66+67+/**
68+ * Clear expired cooldowns from all profiles in the store.
69+ *
70+ * When `cooldownUntil` or `disabledUntil` has passed, the corresponding fields
71+ * are removed and error counters are reset so the profile gets a fresh start
72+ * (circuit-breaker half-open -> closed). Without this, a stale `errorCount`
73+ * causes the *next* transient failure to immediately escalate to a much longer
74+ * cooldown -- the root cause of profiles appearing "stuck" after rate limits.
75+ *
76+ * `cooldownUntil` and `disabledUntil` are handled independently: if a profile
77+ * has both and only one has expired, only that field is cleared.
78+ *
79+ * Mutates the in-memory store; disk persistence happens lazily on the next
80+ * store write (e.g. `markAuthProfileUsed` / `markAuthProfileFailure`), which
81+ * matches the existing save pattern throughout the auth-profiles module.
82+ *
83+ * @returns `true` if any profile was modified.
84+ */
85+export function clearExpiredCooldowns(store: AuthProfileStore, now?: number): boolean {
86+const usageStats = store.usageStats;
87+if (!usageStats) {
88+return false;
89+}
90+91+const ts = now ?? Date.now();
92+let mutated = false;
93+94+for (const [profileId, stats] of Object.entries(usageStats)) {
95+if (!stats) {
96+continue;
97+}
98+99+let profileMutated = false;
100+const cooldownExpired =
101+typeof stats.cooldownUntil === "number" &&
102+Number.isFinite(stats.cooldownUntil) &&
103+stats.cooldownUntil > 0 &&
104+ts >= stats.cooldownUntil;
105+const disabledExpired =
106+typeof stats.disabledUntil === "number" &&
107+Number.isFinite(stats.disabledUntil) &&
108+stats.disabledUntil > 0 &&
109+ts >= stats.disabledUntil;
110+111+if (cooldownExpired) {
112+stats.cooldownUntil = undefined;
113+stats.cooldownReason = undefined;
114+stats.cooldownModel = undefined;
115+profileMutated = true;
116+}
117+if (disabledExpired) {
118+stats.disabledUntil = undefined;
119+stats.disabledReason = undefined;
120+profileMutated = true;
121+}
122+123+// Reset error counters when ALL cooldowns have expired so the profile gets
124+// a fair retry window. Preserves lastFailureAt for the failureWindowMs
125+// decay check in computeNextProfileUsageStats.
126+if (profileMutated && !resolveProfileUnusableUntil(stats)) {
127+stats.errorCount = 0;
128+stats.failureCounts = undefined;
129+}
130+131+if (profileMutated) {
132+usageStats[profileId] = stats;
133+mutated = true;
134+}
135+}
136+137+return mutated;
138+}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。