Files
oh-my-pi/packages/ai/src/usage.ts
T
2026-06-13 00:03:27 +02:00

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;
}