diff --git a/src/providers/quota.ts b/src/providers/quota.ts index 3ff9396c8..01516340a 100644 --- a/src/providers/quota.ts +++ b/src/providers/quota.ts @@ -64,6 +64,15 @@ export interface ProviderQuotaWindow { resetAt?: number; } +export interface ProviderQuotaCreditsUsd { + used: number; + limit: number; + remaining: number; + percent: number; + expiresAt?: number; + unlimited?: boolean; +} + export interface ProviderQuota { fiveHourPercent?: number; fiveHourResetAt?: number; @@ -72,6 +81,7 @@ export interface ProviderQuota { monthlyPercent?: number; monthlyResetAt?: number; customWindows?: ProviderQuotaWindow[]; + creditsUsd?: ProviderQuotaCreditsUsd; updatedAt: number; } @@ -203,6 +213,8 @@ function hasQuotaRows(quota: ProviderQuota | null | undefined): quota is Provide return typeof quota.fiveHourPercent === "number" || typeof quota.weeklyPercent === "number" || typeof quota.monthlyPercent === "number" + || quota.creditsUsd?.unlimited === true + || typeof quota.creditsUsd?.percent === "number" || !!quota.customWindows?.some(window => typeof window.percent === "number"); } @@ -340,6 +352,27 @@ async function fetchA6apiQuota(provider: string, config: OcxProviderConfig): Pro } const subscription = a6apiPayload(await subscriptionResponse.json().catch(() => null)); const token = a6apiPayload(await tokenResponse.json().catch(() => null)); + const unlimited = token?.unlimited_quota === true + || token?.unlimited_quota === 1 + || token?.unlimited_quota === "true"; + const normalizedExpiry = normalizeResetAt(token?.expires_at); + const expiry = normalizedExpiry && normalizedExpiry > 0 + ? { expiresAt: normalizedExpiry } + : {}; + if (unlimited) { + return report(provider, "a6api:billing", { + creditsUsd: { + used: 0, + limit: 0, + remaining: 0, + percent: 0, + unlimited: true, + ...expiry, + }, + customWindows: [{ label: "Unlimited API credits", percent: 0 }], + updatedAt: Date.now(), + }); + } const limitUsd = firstFinite(subscription, ["hard_limit_usd"]); const grantedUnits = firstFinite(token, ["total_granted"]); const usedUnits = firstFinite(token, ["total_used"]); @@ -362,6 +395,13 @@ async function fetchA6apiQuota(provider: string, config: OcxProviderConfig): Pro if (percent === undefined) return TERMINAL_QUOTA_FAILURE; const label = `API credits ($${remainingUsd.toFixed(2)} of $${limitUsd.toFixed(2)} remaining)`; return report(provider, "a6api:billing", { + creditsUsd: { + used: usedUsd, + limit: limitUsd, + remaining: remainingUsd, + percent, + ...expiry, + }, customWindows: [{ label, percent }], updatedAt: Date.now(), }); diff --git a/tests/provider-quota.test.ts b/tests/provider-quota.test.ts index 873a7af54..4a0ed3b1c 100644 --- a/tests/provider-quota.test.ts +++ b/tests/provider-quota.test.ts @@ -282,6 +282,13 @@ describe("fetchProviderQuotaReports", () => { label: "API credits ($15.00 of $20.00 remaining)", percent: 25, }]); + expect(result.reports[0]?.quota.creditsUsd).toEqual({ + used: 5, + limit: 20, + remaining: 15, + percent: 25, + expiresAt: Date.parse("2026-08-01T00:00:00Z"), + }); expect(seen.map(row => row.url).sort()).toEqual([ "https://api.a6api.com/api/usage/token/", "https://api.a6api.com/dashboard/billing/subscription", @@ -290,6 +297,36 @@ describe("fetchProviderQuotaReports", () => { expect(seen.every(row => row.redirect === "error")).toBe(true); }); + test("A6API unlimited keys remain visible even when all finite credit totals are zero", async () => { + globalThis.fetch = (async (input: RequestInfo | URL) => new Response(JSON.stringify( + String(input).includes("subscription") + ? { data: { hard_limit_usd: 100_000_000 } } + : { data: { + total_granted: 0, + total_used: 0, + total_available: 0, + unlimited_quota: true, + expires_at: "2027-01-01T00:00:00Z", + } }, + ), { status: 200 })) as typeof fetch; + + const result = await fetchProviderQuotaReports(a6apiOnlyConfig(), true); + + expect(result.reports).toHaveLength(1); + expect(result.reports[0]?.quota.creditsUsd).toEqual({ + used: 0, + limit: 0, + remaining: 0, + percent: 0, + unlimited: true, + expiresAt: Date.parse("2027-01-01T00:00:00Z"), + }); + expect(result.reports[0]?.quota.customWindows).toEqual([{ + label: "Unlimited API credits", + percent: 0, + }]); + }); + test("A6API quota never sends API keys to a non-canonical base URL", async () => { const seen: string[] = []; globalThis.fetch = (async (input: RequestInfo | URL) => {