mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-18 21:22:28 +03:00
fix(sse): clear quota_exhausted cooldown when real window recovers (#10534)
* fix(sse): clear quota_exhausted cooldown when real window recovers The claude-token-fallback combo was not auto-returning to Sonnet/Opus after a subscription 429 recovered. maybeClearRecoveredQuotaState() was honoring the synthetic 1h cooldown (SUBSCRIPTION_QUOTA_COOLDOWN_MS, persisted when no upstream reset was parseable) instead of the REAL per-window resetAt returned by the scheduled quota poller, so the connection stayed locked long past the actual quota reset. Add windowStillExhaustedAfterRealReset() and use it to decide recovery per-quota-window: a quota_exhausted connection now clears as soon as no governing window is still exhausted with a future-or-unknown real reset, instead of waiting out the synthetic cooldown. Falls back to the previous synthetic-cooldown guard when the fetch has no quota object at all (degraded/failed shape) so existing behavior is unchanged there. Preserves the existing kimi-coding partial-refresh semantics: an exhausted window with no parseable resetAt still blocks recovery. * fix(sse): preserve Claude extra-usage block from general quota recovery maybeClearRecoveredQuotaState()'s new per-window recovery check (added in this branch) only inspected usage.quotas, so a Claude connection blocked by the extra-usage guard (lastErrorSource: "extra_usage") could be released just because the session/weekly quota windows looked recovered, even while extraUsage.queued was still true. Extra-usage blocking is orthogonal to quota-window exhaustion and must only be released by syncClaudeExtraUsageStateIfNeeded (buildClaudeExtraUsageConnectionUpdate). Add a guard that keeps the connection locked when lastErrorSource is "extra_usage", the blockExtraUsage policy is still enabled, and the fresh usage snapshot still reports extraUsage.queued === true. Add an integration test walking the real fetchLiveProviderLimitsWithOptions -> syncClaudeExtraUsageStateIfNeeded -> maybeClearRecoveredQuotaState call chain with recovered quota windows but extraUsage.queued=true, asserting the connection stays unavailable with lastErrorSource still "extra_usage". Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
This commit is contained in:
@@ -13,7 +13,12 @@ import {
|
||||
} from "@/lib/db/providerLimits";
|
||||
import { syncToCloud } from "@/lib/cloudSync";
|
||||
import { setQuotaCache } from "@/domain/quotaCache";
|
||||
import { buildClaudeExtraUsageConnectionUpdate } from "@/lib/providers/claudeExtraUsage";
|
||||
import {
|
||||
buildClaudeExtraUsageConnectionUpdate,
|
||||
CLAUDE_EXTRA_USAGE_ERROR_SOURCE,
|
||||
isClaudeExtraUsageBlockEnabled,
|
||||
isClaudeExtraUsageQueued,
|
||||
} from "@/lib/providers/claudeExtraUsage";
|
||||
import { clearRecoveredProviderState } from "@/sse/services/auth";
|
||||
import { getMachineId } from "@/shared/utils/machine";
|
||||
import { USAGE_SUPPORTED_PROVIDERS } from "@/shared/constants/providers";
|
||||
@@ -433,18 +438,66 @@ export function hasUsableQuota(usage: JsonRecord): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
// A window "still blocks" recovery when it governs quota and is either still
|
||||
// exhausted with a real reset that hasn't passed yet, or exhausted with no
|
||||
// parseable real reset at all (unknown-reset windows stay locked, matching
|
||||
// the pre-existing kimi-coding partial-refresh semantics).
|
||||
function windowStillExhaustedAfterRealReset(value: unknown, nowMs: number): boolean {
|
||||
if (!isRecord(value)) return false;
|
||||
if (value.unlimited === true) return false;
|
||||
const remaining =
|
||||
typeof value.remaining === "number"
|
||||
? value.remaining
|
||||
: typeof value.remainingPercentage === "number"
|
||||
? value.remainingPercentage
|
||||
: null;
|
||||
if (remaining !== null && remaining > 0) return false;
|
||||
if (value.resetAt == null) return true;
|
||||
const resetMs = Date.parse(String(value.resetAt));
|
||||
if (Number.isNaN(resetMs)) return true;
|
||||
return resetMs > nowMs;
|
||||
}
|
||||
|
||||
export async function maybeClearRecoveredQuotaState(
|
||||
connection: ProviderConnectionLike,
|
||||
usage: JsonRecord
|
||||
): Promise<ProviderConnectionLike> {
|
||||
if (!hasUsableQuota(usage)) return connection;
|
||||
if (isTerminalStatusForQuotaRecovery(connection.testStatus)) return connection;
|
||||
if (
|
||||
connection.lastErrorType === "quota_exhausted" &&
|
||||
connection.rateLimitedUntil &&
|
||||
new Date(connection.rateLimitedUntil).getTime() > Date.now()
|
||||
) {
|
||||
return connection;
|
||||
if (connection.lastErrorType === "quota_exhausted") {
|
||||
if (
|
||||
connection.lastErrorSource === CLAUDE_EXTRA_USAGE_ERROR_SOURCE &&
|
||||
isClaudeExtraUsageBlockEnabled(connection.provider, connection.providerSpecificData) &&
|
||||
isClaudeExtraUsageQueued(usage)
|
||||
) {
|
||||
// Claude's pay-as-you-go extra-usage block is orthogonal to the
|
||||
// session/weekly quota windows checked below: the upstream can report a
|
||||
// fully recovered quota window while extraUsage.queued is still true.
|
||||
// Only syncClaudeExtraUsageStateIfNeeded (buildClaudeExtraUsageConnectionUpdate)
|
||||
// owns clearing this specific state — the general window-recovery logic
|
||||
// below must not release it just because some quota window looks fresh.
|
||||
return connection;
|
||||
}
|
||||
|
||||
const quotas = usage?.quotas;
|
||||
if (isRecord(quotas)) {
|
||||
// Honor the REAL per-window resetAt from the freshly fetched quota
|
||||
// instead of the synthetic cooldown persisted at failure time (e.g.
|
||||
// Claude's flat 1h SUBSCRIPTION_QUOTA_COOLDOWN_MS when no upstream
|
||||
// reset was parseable). Only stay locked if some window that governs
|
||||
// this connection's quota is still demonstrably exhausted.
|
||||
const anyStillBlocking = Object.values(quotas).some((value) =>
|
||||
windowStillExhaustedAfterRealReset(value, Date.now())
|
||||
);
|
||||
if (anyStillBlocking) return connection;
|
||||
} else if (
|
||||
connection.rateLimitedUntil &&
|
||||
new Date(connection.rateLimitedUntil).getTime() > Date.now()
|
||||
) {
|
||||
// No quota object at all (degraded/failed fetch shape) — fall back to
|
||||
// the previous synthetic-cooldown guard.
|
||||
return connection;
|
||||
}
|
||||
}
|
||||
|
||||
const hasTransientState =
|
||||
|
||||
Reference in New Issue
Block a user