feat(antigravity): quota-aware routing with reset-aware fallback

On a 409/429 from Antigravity, fetch live quota to learn the exact
per-model resetAt instead of guessing a backoff, then skip only the
exhausted account/model pair until that time.

- antigravityQuota.js: in-memory quota cache, coalesced concurrent
  refreshes, 30s throttle per connection (applied to failures too),
  keeps known cache when upstream returns 401/403 error payloads
- auth.js: pre-filter exhausted account/model pairs; report the
  earliest quota reset when every account is blocked; skip the
  30-minute cooldown cap so the upstream resetAt is not truncated
- chat.js: antigravity 409/429 falls back on the RAM cache only, no
  persistent modelLock_* for this path
- Logs identify accounts by id prefix, never email or name

Closes #3561
This commit is contained in:
Nim committed 2026-08-27 16:18:08 +07:00
1 parent f0a6d35818
commit 1a3db1efae
4 files changed
+314 -5

No files matched your search

+17 -2
View File
@@ -7,6 +7,7 @@ import {
extractApiKey,
isValidApiKey,
} from "../services/auth.js";
import { handleAntigravityQuotaError } from "../services/antigravityQuota.js";
import { getSettings } from "@/lib/localDb";
import { getModelInfo, getComboModels } from "../services/model.js";
import { handleChatCore } from "open-sse/handlers/chatCore.js";
@@ -300,8 +301,22 @@ async function handleSingleModelChat(body, modelStr, clientRawRequest = null, re
if (result.success) return result.response;
// Mark account unavailable (auto-calculates cooldown with exponential backoff, or precise resetsAtMs)
const { shouldFallback } = await markAccountUnavailable(credentials.connectionId, result.status, result.error, provider, model, result.resetsAtMs);
// Antigravity 409/429: refresh live quota to get exact resetAt before locking
let quotaResetMs = null;
let resetsAtMs = result.resetsAtMs;
if (provider === "antigravity" && (result.status === 409 || result.status === 429)) {
quotaResetMs = await handleAntigravityQuotaError(
credentials.connectionId, result.status, model,
refreshedCredentials.accessToken, credentials.providerSpecificData
);
if (quotaResetMs) resetsAtMs = quotaResetMs;
}
// Exhausted Antigravity model is blocked only in RAM cache until upstream resetAt.
// Do not persist a modelLock_* for this path.
const shouldFallback = provider === "antigravity" && quotaResetMs
? true
: (await markAccountUnavailable(credentials.connectionId, result.status, result.error, provider, model, resetsAtMs)).shouldFallback;
if (shouldFallback) {
log.warn("FALLBACK", `⇄ ACC:${credentials.connectionName} UNAVAILABLE (${result.status}) → NEXT ACCOUNT`);