fix(ai): keep header ingests from clearing usage-fetch cooldowns

This commit is contained in:
LunarECL
2026-07-23 22:08:54 +09:00
parent 7f4c3bfba3
commit 2e138ee45d
2 changed files with 40 additions and 4 deletions
+8 -4
View File
@@ -3172,7 +3172,8 @@ export class AuthStorage {
}
if (this.#fetchUsageReportsOverride || this.#store.fetchUsageReports) return false;
const prior = this.#usageCache.getStale<UsageReport | null>(cacheKey)?.value;
const priorEntry = this.#usageCache.getStale<UsageReport | null>(cacheKey);
const prior = priorEntry?.value;
let merged = report;
if (prior && Array.isArray(prior.limits)) {
const headerLimitsById = new Map(report.limits.map(limit => [limit.id, limit]));
@@ -3203,9 +3204,12 @@ export class AuthStorage {
}
// Header-only reports are last-good hints, not completed usage fetches.
// Keep them durable but stale so the next poll probes the provider; a
// failed probe still applies the normal short backoff in #fetchUsageCached.
const expiresAt = merged.metadata?.source === "ratelimit-headers" ? now - 1 : now + USAGE_REPORT_TTL_MS;
// Keep them durable but stale so the next poll probes the provider, while
// preserving any active failure backoff installed by #fetchUsageCached.
const expiresAt =
merged.metadata?.source === "ratelimit-headers"
? Math.max(priorEntry?.expiresAt ?? now - 1, now - 1)
: now + USAGE_REPORT_TTL_MS;
this.#usageCache.set(cacheKey, { value: merged, expiresAt });
this.#usageHeaderIngestAt.set(cacheKey, now);
return true;
@@ -460,6 +460,38 @@ describe("AuthStorage usage cache: header ingestion", () => {
expect(storage.ingestUsageHeaders("anthropic", usageHeaders("0.05", "0.6"), { sessionId: "s" })).toBe(false);
});
it("preserves a failed fetch cooldown across exhausted header ingestion, then retries after expiry", async () => {
const start = Date.now();
const now = vi.spyOn(Date, "now").mockReturnValue(start);
let calls = 0;
vi.spyOn(claudeUsage.claudeUsageProvider, "fetchUsage").mockImplementation(async () => {
calls += 1;
return null;
});
expect(await storage.getApiKey("anthropic", "cooldown-session")).toBe("oat-1");
expect(
storage.ingestUsageHeaders("anthropic", usageHeaders("0.02", "0.3"), {
sessionId: "cooldown-session",
}),
).toBe(true);
expect(await storage.fetchUsageReports()).toHaveLength(1);
expect(calls).toBe(1);
now.mockReturnValue(start + 1_000);
expect(
storage.ingestUsageHeaders("anthropic", usageHeaders("1", "0.3"), {
sessionId: "cooldown-session",
}),
).toBe(true);
expect(await storage.fetchUsageReports()).toHaveLength(1);
expect(calls).toBe(1);
now.mockReturnValue(start + 12_501);
expect(await storage.fetchUsageReports()).toHaveLength(1);
expect(calls).toBe(2);
});
it("merges header umbrella windows onto the last real report and preserves tier limits", async () => {
const realReport = makeTieredReport("a@example.com");
let calls = 0;