feat(providers): live monthly credit quota for Firecrawl (#8759)

* feat(providers): live monthly credit quota for Firecrawl

Wire Firecrawl team credits into Provider Limits / preflight via
GET /v2/team/credit-usage (Bearer API key)

- firecrawlQuotaFetcher + usage/firecrawl leaf
- USAGE_FETCHER_PROVIDERS + USAGE_SUPPORTED_PROVIDERS + apikey allowlist
- register via quotaTrackersBatch
- unit tests for fetcher + usage dispatch

* chore(changelog) - add changelog on live monthly credit quota for Firecrawl

* fix(providers): satisfy provider limits file-size gate

---------

Co-authored-by: allanvb <allanvb@users.noreply.github.com>
This commit is contained in:
Alan Vb
2026-07-27 23:25:47 +03:00
committed by GitHub
parent 2f4aff381c
commit 6bdfd540ba
11 changed files with 579 additions and 2 deletions

View File

@@ -0,0 +1 @@
- **feat(providers):** live monthly credit quota for Firecrawl (`GET /v2/team/credit-usage`) in Provider Limits / preflight ([#8759](https://github.com/diegosouzapw/OmniRoute/pull/8759)) — thanks @allanvb

View File

@@ -0,0 +1,159 @@
/**
* firecrawlQuotaFetcher.ts — Firecrawl team credit quota
*
* Live credit pool for the `firecrawl` connection (web fetch + firecrawl-search).
*
* Endpoint:
* GET https://api.firecrawl.dev/v2/team/credit-usage
* Authorization: Bearer <apiKey>
*
* Response:
* {
* success: true,
* data: {
* remainingCredits: number,
* planCredits: number,
* billingPeriodStart?: string,
* billingPeriodEnd?: string
* }
* }
*
* Fail-open: missing key or upstream errors return null.
* Cache: 60s in-memory TTL keyed by connectionId.
*/
import { registerQuotaFetcher, type QuotaInfo } from "./quotaPreflight.ts";
import { registerMonitorFetcher } from "./quotaMonitor.ts";
import { throttleQuotaFetch } from "./quotaFetchThrottle.ts";
import { toNumberOrNull } from "@/shared/utils/numeric";
const CREDIT_USAGE_URL = "https://api.firecrawl.dev/v2/team/credit-usage";
const CACHE_TTL_MS = 60_000;
const REQUEST_TIMEOUT_MS = 8_000;
export interface FirecrawlQuota extends QuotaInfo {
remainingCredits: number;
planCredits: number;
extraCreditsInferred: number;
overPlan: boolean;
limitReached: boolean;
}
interface CacheEntry {
quota: FirecrawlQuota | null;
fetchedAt: number;
}
const quotaCache = new Map<string, CacheEntry>();
const _cacheCleanup = setInterval(() => {
const now = Date.now();
for (const [key, entry] of quotaCache) {
if (now - entry.fetchedAt > CACHE_TTL_MS * 5) {
quotaCache.delete(key);
}
}
}, 5 * 60_000);
if (typeof _cacheCleanup === "object" && _cacheCleanup && "unref" in _cacheCleanup) {
(_cacheCleanup as { unref?: () => void }).unref?.();
}
function toRecord(value: unknown): Record<string, unknown> {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: {};
}
export function extractFirecrawlApiKey(connection?: Record<string, unknown>): string | null {
if (typeof connection?.apiKey === "string" && connection.apiKey.trim()) {
return connection.apiKey.trim();
}
const credentials = toRecord(connection?.credentials);
if (typeof credentials.apiKey === "string" && credentials.apiKey.trim()) {
return credentials.apiKey.trim();
}
return null;
}
export function parseFirecrawlCreditUsage(data: unknown): FirecrawlQuota | null {
const root = toRecord(data);
const payload = toRecord(root.data);
const remaining = toNumberOrNull(payload.remainingCredits ?? payload.remaining_credits);
const plan = toNumberOrNull(payload.planCredits ?? payload.plan_credits);
if (remaining === null || plan === null || plan < 0) return null;
const planCredits = Math.max(0, plan);
const remainingCredits = Math.max(0, remaining);
const extraCreditsInferred = Math.max(0, remainingCredits - planCredits);
const overPlan = extraCreditsInferred > 0;
const used = planCredits > 0 ? Math.max(0, planCredits - remainingCredits) : 0;
const percentUsed = planCredits > 0 ? used / planCredits : remainingCredits <= 0 ? 1 : 0;
const resetRaw = payload.billingPeriodEnd ?? payload.billing_period_end;
const resetAt = typeof resetRaw === "string" && resetRaw.trim() ? resetRaw.trim() : null;
return {
used,
total: planCredits,
percentUsed,
resetAt,
remainingCredits,
planCredits,
extraCreditsInferred,
overPlan,
limitReached: remainingCredits <= 0 || percentUsed >= 1,
};
}
export async function fetchFirecrawlQuota(
connectionId: string,
connection?: Record<string, unknown>
): Promise<QuotaInfo | null> {
const cached = quotaCache.get(connectionId);
if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) {
return cached.quota;
}
const apiKey = extractFirecrawlApiKey(connection);
if (!apiKey) {
quotaCache.set(connectionId, { quota: null, fetchedAt: Date.now() });
return null;
}
try {
await throttleQuotaFetch();
const response = await fetch(CREDIT_USAGE_URL, {
method: "GET",
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json",
},
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
});
if (response.status === 401 || response.status === 403) {
quotaCache.set(connectionId, { quota: null, fetchedAt: Date.now() });
return null;
}
if (!response.ok) {
return null;
}
const body = await response.json();
const quota = parseFirecrawlCreditUsage(body);
quotaCache.set(connectionId, { quota, fetchedAt: Date.now() });
return quota;
} catch {
quotaCache.set(connectionId, { quota: null, fetchedAt: Date.now() });
return null;
}
}
export function invalidateFirecrawlQuotaCache(connectionId: string): void {
quotaCache.delete(connectionId);
}
export function registerFirecrawlQuotaFetcher(): void {
registerQuotaFetcher("firecrawl", fetchFirecrawlQuota);
registerMonitorFetcher("firecrawl", fetchFirecrawlQuota);
}

View File

@@ -1,6 +1,6 @@
/**
* quotaTrackersBatch.ts — startup registration for batch quota trackers
* (AgentRouter, v0-vercel, freemodel-dev, grok-cli, xai-oauth).
* (AgentRouter, v0-vercel, freemodel-dev, grok-cli, xai-oauth, firecrawl).
*
* Kept in a dedicated module (rather than adding more inline calls to
* `src/sse/handlers/chat.ts`, which is a frozen file at its LOC baseline) so the
@@ -12,6 +12,7 @@ import { registerV0QuotaFetcher } from "./v0QuotaFetcher.ts";
import { registerFreeModelQuotaFetcher } from "./freeModelQuotaFetcher.ts";
import { registerGrokCliQuotaFetcher } from "./grokCliQuotaFetcher.ts";
import { registerXaiOauthQuotaFetcher } from "./xaiOauthQuotaFetcher.ts";
import { registerFirecrawlQuotaFetcher } from "./firecrawlQuotaFetcher.ts";
export function registerQuotaTrackersBatch(): void {
registerAgentrouterQuotaFetcher();
@@ -19,6 +20,7 @@ export function registerQuotaTrackersBatch(): void {
registerFreeModelQuotaFetcher();
registerGrokCliQuotaFetcher();
registerXaiOauthQuotaFetcher();
registerFirecrawlQuotaFetcher();
}
// Side-effect registration at module load, mirroring the sibling

View File

@@ -66,6 +66,7 @@ import { getVertexUsage } from "./usage/vertex.ts";
import { getXiaomiMimoUsage } from "./usage/xiaomi-mimo.ts";
import { getXaiUsage } from "./usage/xai.ts";
import { getXaiOauthUsage } from "./usage/xaiOauth.ts";
import { getFirecrawlUsage } from "./usage/firecrawl.ts";
type JsonRecord = Record<string, unknown>;
type UsageProviderConnection = JsonRecord & {
@@ -125,6 +126,8 @@ export const USAGE_FETCHER_PROVIDERS = [
// HyperAgent billing usage (creditBlocks USD)
"hyperagent",
"ha",
// Firecrawl team credits (GET /v2/team/credit-usage)
"firecrawl",
] as const;
export type UsageFetcherProvider = (typeof USAGE_FETCHER_PROVIDERS)[number];
@@ -220,6 +223,8 @@ export async function getUsageForProvider(
case "hyperagent":
case "ha":
return await getHyperAgentUsage(apiKey || accessToken, providerSpecificData);
case "firecrawl":
return await getFirecrawlUsage(id || "", apiKey);
default:
return { message: `Usage API not implemented for ${provider}` };
}
@@ -249,6 +254,7 @@ export const __testing = {
getXiaomiMimoUsage,
getXaiUsage,
getXaiOauthUsage,
getFirecrawlUsage,
getVertexUsage,
getMiniMaxAuthErrorMessage,
getMiniMaxErrorSummary,

View File

@@ -0,0 +1,60 @@
/**
* usage/firecrawl.ts — Firecrawl team credit usage for Provider Limits.
*
* GET /v2/team/credit-usage via firecrawlQuotaFetcher; shapes remaining/plan
* credits into the standard `{ plan, quotas }` response.
*/
import { fetchFirecrawlQuota, type FirecrawlQuota } from "../firecrawlQuotaFetcher.ts";
import { createQuotaFromUsage, parseResetTime } from "./quota.ts";
function createFirecrawlPlanQuota(q: FirecrawlQuota) {
if (q.overPlan) {
return {
used: 0,
total: q.planCredits,
remaining: q.remainingCredits,
remainingPercentage: q.planCredits > 0 ? (q.remainingCredits / q.planCredits) * 100 : 100,
resetAt: parseResetTime(q.resetAt),
unlimited: false,
extraCreditsInferred: q.extraCreditsInferred,
overPlan: true,
};
}
return {
...createQuotaFromUsage(q.used, q.total, q.resetAt),
extraCreditsInferred: 0,
overPlan: false,
};
}
export async function getFirecrawlUsage(connectionId: string, apiKey?: string) {
if (!connectionId) {
return { message: "Firecrawl: connection id unavailable." };
}
try {
const live = await fetchFirecrawlQuota(connectionId, { apiKey });
if (!live) {
return { message: "Firecrawl API key not available or credit usage unavailable." };
}
const q = live as FirecrawlQuota;
const monthly = createFirecrawlPlanQuota(q);
return {
plan: "Firecrawl · Monthly credits",
quotas: {
monthly,
},
remainingCredits: q.remainingCredits,
planCredits: q.planCredits,
extraCreditsInferred: q.extraCreditsInferred,
overPlan: q.overPlan,
limitReached: q.limitReached,
};
} catch (error) {
return { message: `Firecrawl usage error: ${(error as Error).message}` };
}
}

View File

@@ -54,15 +54,21 @@ function getResetAdjustedQuota(quota: any) {
function normalizeQuotaEntry(name: string, quota: any = {}, extras: any = {}) {
const adjusted = getResetAdjustedQuota(quota);
const remaining = Number(quota?.remaining);
return {
name,
used: Number.isFinite(adjusted.used) ? adjusted.used : 0,
total: adjusted.total,
...(Number.isFinite(remaining) ? { remaining } : {}),
resetAt: quota?.resetAt || null,
staleAfterReset: adjusted.staleAfterReset,
...(adjusted.remainingPercentage !== undefined
? { remainingPercentage: adjusted.remainingPercentage }
: {}),
...(quota?.extraCreditsInferred !== undefined
? { extraCreditsInferred: Number(quota.extraCreditsInferred) || 0 }
: {}),
...(quota?.overPlan !== undefined ? { overPlan: quota.overPlan === true } : {}),
...extras,
};
}

View File

@@ -36,7 +36,6 @@ import {
sanitizeUsageQuotasForProvider,
} from "./providerLimits/quotaNormalize";
import { syncInChunksWithSpacing } from "./providerLimits/chunkedSpacingSync";
type JsonRecord = Record<string, unknown>;
type SyncSource = "manual" | "scheduled";
@@ -88,6 +87,7 @@ const PROVIDER_LIMITS_APIKEY_PROVIDERS = new Set([
// HyperAgent session cookie → billing/usage creditBlocks
"hyperagent",
"ha",
"firecrawl",
]);
const DEFAULT_PROVIDER_LIMITS_SYNC_INTERVAL_MINUTES = 70;
const PROVIDER_LIMITS_AUTO_SYNC_SETTING_KEY = "provider_limits_auto_sync_last_run";

View File

@@ -452,6 +452,8 @@ export const USAGE_SUPPORTED_PROVIDERS = [
// xAI OAuth (Grok) weekly quota (id + public alias, same pattern as ha/agy)
"xai-oauth",
"xao",
// Firecrawl team credits (GET /v2/team/credit-usage)
"firecrawl",
];
// ── Zod validation at module load (Phase 7.2) ──

View File

@@ -0,0 +1,155 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
extractFirecrawlApiKey,
fetchFirecrawlQuota,
invalidateFirecrawlQuotaCache,
parseFirecrawlCreditUsage,
registerFirecrawlQuotaFetcher,
} from "../../open-sse/services/firecrawlQuotaFetcher.ts";
import { preflightQuota } from "../../open-sse/services/quotaPreflight.ts";
import { clearQuotaMonitors } from "../../open-sse/services/quotaMonitor.ts";
import { clearSessions } from "../../open-sse/services/sessionManager.ts";
const originalFetch = globalThis.fetch;
test.afterEach(() => {
globalThis.fetch = originalFetch;
clearQuotaMonitors();
clearSessions();
});
function creditUsageResponse(remaining: number, plan: number, endIso?: string) {
return new Response(
JSON.stringify({
success: true,
data: {
remainingCredits: remaining,
planCredits: plan,
billingPeriodStart: "2026-07-01T00:00:00Z",
billingPeriodEnd: endIso || "2026-07-31T23:59:59Z",
},
}),
{ status: 200, headers: { "content-type": "application/json" } }
);
}
test("extractFirecrawlApiKey reads root and credentials shapes", () => {
assert.equal(extractFirecrawlApiKey(undefined), null);
assert.equal(extractFirecrawlApiKey({}), null);
assert.equal(extractFirecrawlApiKey({ apiKey: " fc-key " }), "fc-key");
assert.equal(extractFirecrawlApiKey({ credentials: { apiKey: "nested-key" } }), "nested-key");
});
test("parseFirecrawlCreditUsage maps remaining/plan to percent used", () => {
const q = parseFirecrawlCreditUsage({
success: true,
data: {
remainingCredits: 250,
planCredits: 1000,
billingPeriodEnd: "2026-08-01T00:00:00Z",
},
});
assert.ok(q);
assert.equal(q!.used, 750);
assert.equal(q!.total, 1000);
assert.equal(q!.percentUsed, 0.75);
assert.equal(q!.remainingCredits, 250);
assert.equal(q!.limitReached, false);
assert.equal(q!.resetAt, "2026-08-01T00:00:00Z");
});
test("parseFirecrawlCreditUsage preserves the plan baseline when credits are over plan", () => {
const q = parseFirecrawlCreditUsage({
data: { remainingCredits: 1500, planCredits: 1000 },
});
assert.ok(q);
assert.equal(q!.used, 0);
assert.equal(q!.total, 1000);
assert.equal(q!.percentUsed, 0);
assert.equal(q!.remainingCredits, 1500);
assert.equal(q!.extraCreditsInferred, 500);
assert.equal(q!.overPlan, true);
assert.equal(q!.limitReached, false);
});
test("parseFirecrawlCreditUsage marks exhausted when remaining is 0", () => {
const q = parseFirecrawlCreditUsage({
data: { remainingCredits: 0, planCredits: 1000 },
});
assert.ok(q);
assert.equal(q!.percentUsed, 1);
assert.equal(q!.limitReached, true);
});
test("fetchFirecrawlQuota returns null when no API key", async () => {
const connectionId = `fc-missing-${Date.now()}`;
const quota = await fetchFirecrawlQuota(connectionId, {});
assert.equal(quota, null);
invalidateFirecrawlQuotaCache(connectionId);
});
test("fetchFirecrawlQuota calls credit-usage with Bearer and maps credits", async () => {
const connectionId = `fc-live-${Date.now()}`;
const calls: Array<{ url: string; init?: RequestInit }> = [];
globalThis.fetch = async (url, init) => {
calls.push({ url: String(url), init });
return creditUsageResponse(700, 1000);
};
const quota = await fetchFirecrawlQuota(connectionId, { apiKey: "fc-test-key" });
assert.ok(quota);
assert.equal(quota!.used, 300);
assert.equal(quota!.total, 1000);
assert.equal(quota!.percentUsed, 0.3);
assert.ok(quota!.resetAt?.includes("2026-07-31"));
assert.equal(calls.length, 1);
assert.equal(calls[0].url, "https://api.firecrawl.dev/v2/team/credit-usage");
const headers = calls[0].init?.headers as Record<string, string>;
assert.equal(headers.Authorization, "Bearer fc-test-key");
invalidateFirecrawlQuotaCache(connectionId);
});
test("fetchFirecrawlQuota fail-opens on 401", async () => {
const connectionId = `fc-401-${Date.now()}`;
globalThis.fetch = async () => new Response("Unauthorized", { status: 401 });
const quota = await fetchFirecrawlQuota(connectionId, { apiKey: "dead" });
assert.equal(quota, null);
invalidateFirecrawlQuotaCache(connectionId);
});
test("fetchFirecrawlQuota caches results for 60s", async () => {
const connectionId = `fc-cache-${Date.now()}`;
let fetchCount = 0;
globalThis.fetch = async () => {
fetchCount += 1;
return creditUsageResponse(500, 1000);
};
const q1 = await fetchFirecrawlQuota(connectionId, { apiKey: "tok" });
const q2 = await fetchFirecrawlQuota(connectionId, { apiKey: "tok" });
assert.equal(fetchCount, 1);
assert.equal(q1!.percentUsed, q2!.percentUsed);
assert.equal(q1!.percentUsed, 0.5);
invalidateFirecrawlQuotaCache(connectionId);
});
test("registerFirecrawlQuotaFetcher registers firecrawl for preflight", async () => {
registerFirecrawlQuotaFetcher();
globalThis.fetch = async () => creditUsageResponse(900, 1000);
const connectionId = `fc-reg-${Date.now()}`;
const result = await preflightQuota("firecrawl", connectionId, {
provider: "firecrawl",
id: connectionId,
apiKey: "tok",
});
assert.equal(result.proceed, true);
invalidateFirecrawlQuotaCache(connectionId);
});

View File

@@ -0,0 +1,162 @@
/**
* tests/unit/firecrawl-usage.test.ts
*
* Firecrawl usage.ts dispatch + Provider Limits allowlists for team credits.
*/
import { describe, it, afterEach } from "node:test";
import assert from "node:assert/strict";
const { __testing, USAGE_FETCHER_PROVIDERS, getUsageForProvider } =
await import("../../open-sse/services/usage.ts");
const { invalidateFirecrawlQuotaCache } =
await import("../../open-sse/services/firecrawlQuotaFetcher.ts");
const { USAGE_SUPPORTED_PROVIDERS } = await import("../../src/shared/constants/providers.ts");
const { isSupportedUsageConnection } = await import("../../src/lib/usage/providerLimits.ts");
const { getFirecrawlUsage } = __testing;
const originalFetch = globalThis.fetch;
function creditUsageResponse(remaining: number, plan: number, endIso?: string) {
return new Response(
JSON.stringify({
success: true,
data: {
remainingCredits: remaining,
planCredits: plan,
billingPeriodEnd: endIso || "2026-07-31T23:59:59Z",
},
}),
{ status: 200, headers: { "content-type": "application/json" } }
);
}
describe("Firecrawl usage dispatch", () => {
afterEach(() => {
globalThis.fetch = originalFetch;
invalidateFirecrawlQuotaCache("conn-fc");
invalidateFirecrawlQuotaCache("conn-live");
});
it("registers firecrawl in USAGE_FETCHER_PROVIDERS and USAGE_SUPPORTED_PROVIDERS", () => {
assert.ok((USAGE_FETCHER_PROVIDERS as readonly string[]).includes("firecrawl"));
assert.ok((USAGE_SUPPORTED_PROVIDERS as readonly string[]).includes("firecrawl"));
});
it("isSupportedUsageConnection accepts firecrawl apikey connections", () => {
assert.equal(
isSupportedUsageConnection({
id: "c1",
provider: "firecrawl",
authType: "apikey",
}),
true
);
});
it("getFirecrawlUsage maps live credit-usage into monthly quota", async () => {
globalThis.fetch = async () => creditUsageResponse(250, 1000);
const r = (await getFirecrawlUsage("conn-live", "fc-key")) as {
plan?: string;
message?: string;
quotas?: {
monthly?: {
used: number;
total: number;
remaining?: number;
resetAt: string | null;
};
};
remainingCredits?: number;
};
assert.ok(r.quotas?.monthly, `expected quotas, got: ${JSON.stringify(r)}`);
assert.match(r.plan || "", /Firecrawl/i);
assert.equal(r.quotas!.monthly!.used, 750);
assert.equal(r.quotas!.monthly!.total, 1000);
assert.equal(r.quotas!.monthly!.remaining, 250);
assert.equal(r.remainingCredits, 250);
assert.ok(r.quotas!.monthly!.resetAt?.includes("2026-07-31"));
});
it("getFirecrawlUsage reports over-plan remaining credits as more than 100% left", async () => {
globalThis.fetch = async () => creditUsageResponse(1500, 1000);
const r = (await getFirecrawlUsage("conn-live", "fc-key")) as {
quotas?: {
monthly?: {
used: number;
total: number;
remaining?: number;
remainingPercentage?: number;
extraCreditsInferred?: number;
overPlan?: boolean;
};
};
remainingCredits?: number;
planCredits?: number;
extraCreditsInferred?: number;
};
assert.equal(r.quotas?.monthly?.used, 0);
assert.equal(r.quotas?.monthly?.total, 1000);
assert.equal(r.quotas?.monthly?.remaining, 1500);
assert.equal(r.quotas?.monthly?.remainingPercentage, 150);
assert.equal(r.quotas?.monthly?.overPlan, true);
assert.equal(r.quotas?.monthly?.extraCreditsInferred, 500);
assert.equal(r.extraCreditsInferred, 500);
});
it("getFirecrawlUsage keeps the plan baseline stable as over-plan credits are spent", async () => {
globalThis.fetch = async () => creditUsageResponse(1450, 1000);
const r = (await getFirecrawlUsage("conn-live", "fc-key")) as {
quotas?: {
monthly?: {
used: number;
total: number;
remaining?: number;
remainingPercentage?: number;
extraCreditsInferred?: number;
};
};
};
assert.equal(r.quotas?.monthly?.used, 0);
assert.equal(r.quotas?.monthly?.total, 1000);
assert.equal(r.quotas?.monthly?.remaining, 1450);
assert.equal(r.quotas?.monthly?.remainingPercentage, 145);
assert.equal(r.quotas?.monthly?.extraCreditsInferred, 450);
});
it("getFirecrawlUsage returns message when live quota unavailable", async () => {
globalThis.fetch = async () => new Response("Unauthorized", { status: 401 });
const r = (await getFirecrawlUsage("conn-fc", "dead")) as {
message?: string;
quotas?: unknown;
};
assert.ok(r.message && !r.quotas);
});
it("getFirecrawlUsage returns message when connection id missing", async () => {
const r = (await getFirecrawlUsage("", "key")) as { message?: string; quotas?: unknown };
assert.ok(r.message && !r.quotas);
});
it("getUsageForProvider('firecrawl', ...) delegates to getFirecrawlUsage", async () => {
globalThis.fetch = async () => creditUsageResponse(100, 500);
const r = (await getUsageForProvider({
id: "conn-live",
provider: "firecrawl",
apiKey: "dispatch-key",
} as Parameters<typeof getUsageForProvider>[0])) as {
quotas?: { monthly?: { used: number; total: number; remaining?: number } };
};
assert.equal(r.quotas?.monthly?.used, 400);
assert.equal(r.quotas?.monthly?.total, 500);
assert.equal(r.quotas?.monthly?.remaining, 100);
});
});

View File

@@ -171,6 +171,30 @@ test("percentage-only quotas hide redundant usage counts while counted quotas ke
assert.equal(providerLimitUtils.shouldShowQuotaUsageCount(counted[0]), true);
});
test("Firecrawl over-plan quota displays remaining credits against the plan baseline", () => {
const parsed = providerLimitUtils.parseQuotaData("firecrawl", {
quotas: {
monthly: {
used: 0,
total: 1000,
remaining: 1450,
remainingPercentage: 145,
extraCreditsInferred: 450,
overPlan: true,
},
},
});
assert.equal(parsed.length, 1);
assert.equal(parsed[0].used, 0);
assert.equal(parsed[0].total, 1000);
assert.equal(parsed[0].remaining, 1450);
assert.equal(providerLimitUtils.getQuotaRemainingPercentage(parsed[0]), 145);
assert.equal(parsed[0].extraCreditsInferred, 450);
assert.equal(parsed[0].overPlan, true);
assert.equal(providerLimitUtils.shouldShowQuotaUsageCount(parsed[0]), true);
});
test("Codex banked reset credits parse as an integer reset-credit counter", () => {
const parsed = providerLimitUtils.parseQuotaData("codex", {
quotas: {