275 lines
8.8 KiB
TypeScript
275 lines
8.8 KiB
TypeScript
/**
|
|
* Usage reporting types for provider quota/limit endpoints.
|
|
*
|
|
* Provides a normalized schema to represent multiple limit windows, model tiers,
|
|
* and shared quotas across providers.
|
|
*/
|
|
import { z } from "zod/v4";
|
|
import type { FetchImpl, Provider } from "./types";
|
|
export type UsageUnit = "percent" | "tokens" | "requests" | "usd" | "minutes" | "bytes" | "unknown";
|
|
|
|
export type UsageStatus = "ok" | "warning" | "exhausted" | "unknown";
|
|
|
|
/** Time window for a limit (e.g. 5h, 7d, monthly). */
|
|
export interface UsageWindow {
|
|
/** Stable identifier (e.g. "5h", "7d", "monthly"). */
|
|
id: string;
|
|
/** Human label (e.g. "5 Hour", "7 Day"). */
|
|
label: string;
|
|
/** Window duration in milliseconds, when known. */
|
|
durationMs?: number;
|
|
/** Absolute reset timestamp in milliseconds since epoch. */
|
|
resetsAt?: number;
|
|
}
|
|
|
|
/** Quantitative usage data. */
|
|
export interface UsageAmount {
|
|
/** Amount used in the given unit. */
|
|
used?: number;
|
|
/** Maximum limit in the given unit. */
|
|
limit?: number;
|
|
/** Remaining amount in the given unit. */
|
|
remaining?: number;
|
|
/** Fraction used (0..1). */
|
|
usedFraction?: number;
|
|
/** Fraction remaining (0..1). */
|
|
remainingFraction?: number;
|
|
/** Unit for the amounts (percent, tokens, etc.). */
|
|
unit: UsageUnit;
|
|
}
|
|
|
|
/** Scope metadata describing what the limit applies to. */
|
|
export interface UsageScope {
|
|
provider: Provider;
|
|
accountId?: string;
|
|
projectId?: string;
|
|
orgId?: string;
|
|
modelId?: string;
|
|
tier?: string;
|
|
windowId?: string;
|
|
shared?: boolean;
|
|
}
|
|
|
|
/** Normalized limit entry for a single window or quota bucket. */
|
|
export interface UsageLimit {
|
|
/** Stable identifier for this limit entry. */
|
|
id: string;
|
|
/** Human label for display. */
|
|
label: string;
|
|
scope: UsageScope;
|
|
window?: UsageWindow;
|
|
amount: UsageAmount;
|
|
status?: UsageStatus;
|
|
notes?: string[];
|
|
}
|
|
|
|
/**
|
|
* Saved/banked rate-limit resets an account can redeem on demand.
|
|
*
|
|
* Surfaced by providers that let users defer a usage-window reset and spend it
|
|
* later (OpenAI Codex "saved rate limit resets"). The redeem itself is a
|
|
* separate, provider-specific action; this is the read-only count for display.
|
|
*/
|
|
export interface UsageResetCredits {
|
|
/** Number of resets available to redeem right now. */
|
|
availableCount: number;
|
|
}
|
|
|
|
/** Aggregated usage report for a provider. */
|
|
export interface UsageReport {
|
|
provider: Provider;
|
|
fetchedAt: number;
|
|
limits: UsageLimit[];
|
|
/** Saved rate-limit resets the account can redeem, when the provider reports them. */
|
|
resetCredits?: UsageResetCredits;
|
|
metadata?: Record<string, unknown>;
|
|
raw?: unknown;
|
|
}
|
|
|
|
/**
|
|
* Resolve a limit's used fraction (0..1; >1 means overage) from whichever
|
|
* amount fields the provider populated. Precedence mirrors the usage UIs:
|
|
* explicit fraction > used/limit > percent-unit used > inverted remaining.
|
|
*/
|
|
export function resolveUsedFraction(limit: UsageLimit): number | undefined {
|
|
const amount = limit.amount;
|
|
if (amount.usedFraction !== undefined) return amount.usedFraction;
|
|
if (amount.used !== undefined && amount.limit !== undefined && amount.limit > 0) {
|
|
return amount.used / amount.limit;
|
|
}
|
|
if (amount.unit === "percent" && amount.used !== undefined) return amount.used / 100;
|
|
if (amount.remainingFraction !== undefined) return Math.max(0, 1 - amount.remainingFraction);
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* One recorded usage-limit snapshot: a single limit window of one account at
|
|
* a point in time. The usage cache itself is latest-snapshot-only; history
|
|
* rows are appended by the auth storage layer whenever a fresh report is
|
|
* fetched, so limit utilization stays inspectable over time.
|
|
*/
|
|
export interface UsageHistoryEntry {
|
|
/** Epoch ms the report was fetched. */
|
|
recordedAt: number;
|
|
provider: Provider;
|
|
/** Stable credential identity key (account/email/project derived). */
|
|
accountKey: string;
|
|
email?: string;
|
|
accountId?: string;
|
|
/** {@link UsageLimit.id} of the recorded window. */
|
|
limitId: string;
|
|
/** Human label of the limit. */
|
|
label: string;
|
|
windowLabel?: string;
|
|
/** Used fraction (0..1) when resolvable. */
|
|
usedFraction?: number;
|
|
status?: UsageStatus;
|
|
/** Epoch ms the window resets, when known. */
|
|
resetsAt?: number;
|
|
}
|
|
|
|
/** Filter for reading recorded usage history. */
|
|
export interface UsageHistoryQuery {
|
|
provider?: string;
|
|
/** Inclusive lower bound on {@link UsageHistoryEntry.recordedAt} (epoch ms). */
|
|
sinceMs?: number;
|
|
}
|
|
|
|
// ─── Zod schemas (wire-shape validation for the broker `/v1/usage` endpoint) ─
|
|
|
|
export const usageUnitSchema = z.enum(["percent", "tokens", "requests", "usd", "minutes", "bytes", "unknown"]);
|
|
export const usageStatusSchema = z.enum(["ok", "warning", "exhausted", "unknown"]);
|
|
|
|
export const usageWindowSchema = z.object({
|
|
id: z.string(),
|
|
label: z.string(),
|
|
durationMs: z.number().optional(),
|
|
resetsAt: z.number().optional(),
|
|
});
|
|
|
|
export const usageAmountSchema = z.object({
|
|
used: z.number().optional(),
|
|
limit: z.number().optional(),
|
|
remaining: z.number().optional(),
|
|
usedFraction: z.number().optional(),
|
|
remainingFraction: z.number().optional(),
|
|
unit: usageUnitSchema,
|
|
});
|
|
|
|
export const usageScopeSchema = z.object({
|
|
provider: z.string(),
|
|
accountId: z.string().optional(),
|
|
projectId: z.string().optional(),
|
|
orgId: z.string().optional(),
|
|
modelId: z.string().optional(),
|
|
tier: z.string().optional(),
|
|
windowId: z.string().optional(),
|
|
shared: z.boolean().optional(),
|
|
});
|
|
|
|
export const usageLimitSchema = z.object({
|
|
id: z.string(),
|
|
label: z.string(),
|
|
scope: usageScopeSchema,
|
|
window: usageWindowSchema.optional(),
|
|
amount: usageAmountSchema,
|
|
status: usageStatusSchema.optional(),
|
|
notes: z.array(z.string()).optional(),
|
|
});
|
|
|
|
export const usageResetCreditsSchema = z.object({
|
|
availableCount: z.number(),
|
|
});
|
|
|
|
export const usageReportSchema = z.object({
|
|
provider: z.string(),
|
|
fetchedAt: z.number(),
|
|
limits: z.array(usageLimitSchema),
|
|
resetCredits: usageResetCreditsSchema.optional(),
|
|
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
// `raw` is provider-specific and may be anything; the broker strips it before
|
|
// sending the report over the wire, so accept-but-ignore here.
|
|
raw: z.unknown().optional(),
|
|
});
|
|
|
|
/** Optional logger for usage fetchers. */
|
|
export interface UsageLogger {
|
|
debug(message: string, meta?: Record<string, unknown>): void;
|
|
warn(message: string, meta?: Record<string, unknown>): void;
|
|
}
|
|
|
|
/** Credential bundle for usage endpoints. */
|
|
export interface UsageCredential {
|
|
type: "api_key" | "oauth";
|
|
apiKey?: string;
|
|
accessToken?: string;
|
|
refreshToken?: string;
|
|
expiresAt?: number;
|
|
accountId?: string;
|
|
projectId?: string;
|
|
email?: string;
|
|
enterpriseUrl?: string;
|
|
metadata?: Record<string, unknown>;
|
|
}
|
|
|
|
/** Parameters provided to a usage fetcher. */
|
|
export interface UsageFetchParams {
|
|
provider: Provider;
|
|
credential: UsageCredential;
|
|
baseUrl?: string;
|
|
signal?: AbortSignal;
|
|
}
|
|
|
|
/** Shared runtime utilities for fetchers. */
|
|
export interface UsageFetchContext {
|
|
fetch: FetchImpl;
|
|
logger?: UsageLogger;
|
|
retryWait?: (delayMs: number, signal?: AbortSignal) => Promise<void>;
|
|
}
|
|
|
|
/** Provider implementation for fetching usage information. */
|
|
export interface UsageProvider {
|
|
id: Provider;
|
|
fetchUsage(params: UsageFetchParams, ctx: UsageFetchContext): Promise<UsageReport | null>;
|
|
/** Parse provider rate-limit response headers (lowercased keys) into a usage report, if supported. */
|
|
parseRateLimitHeaders?(headers: Record<string, string>, now?: number): UsageReport | null;
|
|
supports?(params: UsageFetchParams): boolean;
|
|
}
|
|
|
|
/** Request context used when ranking usage for a specific model. */
|
|
export interface CredentialRankingContext {
|
|
/** Provider model id, when the caller is selecting a credential for one model. */
|
|
modelId?: string;
|
|
}
|
|
|
|
/** Strategy for usage-based credential ranking. Providers implement this to opt into smart credential selection. */
|
|
export interface CredentialRankingStrategy {
|
|
/** Extract the primary (short) and secondary (long) window limits from a usage report. */
|
|
findWindowLimits(
|
|
report: UsageReport,
|
|
context?: CredentialRankingContext,
|
|
): {
|
|
primary?: UsageLimit;
|
|
secondary?: UsageLimit;
|
|
};
|
|
/**
|
|
* Restrict limits to the ones relevant for the requested model before
|
|
* credential-wide exhaustion checks and ranking. Providers with shared
|
|
* account-wide quotas can omit this and use all limits.
|
|
*/
|
|
scopeLimits?(report: UsageReport, context?: CredentialRankingContext): UsageLimit[];
|
|
/**
|
|
* Return a provider-local backoff scope for the requested model. Providers
|
|
* with backend-specific quotas use this so one exhausted model family does
|
|
* not block unrelated families on the same OAuth credential.
|
|
*/
|
|
blockScope?(context?: CredentialRankingContext): string | undefined;
|
|
/** Fallback window durations (ms) when limits don't specify durationMs. */
|
|
windowDefaults: {
|
|
primaryMs: number;
|
|
secondaryMs: number;
|
|
};
|
|
/** Optional: priority boost for specific credential states (e.g., fresh 5h ticker start). */
|
|
hasPriorityBoost?(primary: UsageLimit | undefined): boolean;
|
|
}
|