import { createHash } from "node:crypto"; import type { ZodType } from "zod"; import type { JsonValue } from "@getpaseo/protocol/agent-types"; export interface UsageWindow { id: string; label: string; /** * A few characters naming the window where space is tight, e.g. "5h" or "wk". An empty string * shows the percent alone; leaving it out shows `label`. */ shortLabel?: string; /** Shown in the usage summary until the user pins windows of their own. */ summary?: boolean; usedPct?: number | null; remainingPct?: number | null; resetsAt?: string | null; runsOutAt?: string | null; shortfallPct?: number | null; tone?: "default" | "ok" | "warning" | "danger"; } export interface UsageBalance { id: string; label: string; used?: number | null; remaining?: number | null; limit?: number | null; unit: "usd" | "credits" | "requests" | "tokens"; resetsAt?: string | null; tone?: UsageWindow["tone"]; } export interface UsageDetail { id: string; label: string; value: string; tone?: UsageWindow["tone"]; } export interface UsageReport { status: "available" | "unavailable" | "error"; planLabel?: string; windows: UsageWindow[]; balances?: UsageBalance[]; details?: UsageDetail[]; error?: string; } export interface UsageSourceRegistration { id: string; label: string; icon?: string; input: ZodType; /** Stable account identity, resolved without fetching usage. */ identify(input: unknown): Promise<{ key: string; label?: string } | null>; fetch(input: unknown): Promise; discover(): Promise; } export function windowFromUsedPct(input: { id: string; label: string; shortLabel?: string; summary?: boolean; utilizationPct: number | null | undefined; resetsAt?: string | null; tone?: UsageWindow["tone"]; }): UsageWindow { const usedPct = typeof input.utilizationPct === "number" ? input.utilizationPct : null; const window: UsageWindow = { id: input.id, label: input.label, usedPct, remainingPct: usedPct === null ? null : Math.max(0, 100 - usedPct), resetsAt: input.resetsAt ?? null, }; if (input.shortLabel !== undefined) window.shortLabel = input.shortLabel; if (input.summary) window.summary = true; if (input.tone) window.tone = input.tone; return window; } /** * The tone scale for anything measured against a known limit, windows and balances alike. * * Thresholds match `deriveTone` in the app's provider-usage/tone.ts, which is what the * client falls back to when a window arrives without a tone. Healthy is "ok" rather than * "default" because that is what every provider setting a tone has always sent, and it is * what the bars render today below their thresholds. */ export function toneFromUsedPct(usedPct: number | null | undefined): UsageWindow["tone"] { if (typeof usedPct !== "number") return "default"; if (usedPct > 90) return "danger"; if (usedPct >= 70) return "warning"; return "ok"; } /** * Tone for a balance with no known limit, where a percentage cannot be computed and the * only signal is whether anything is left. Prefer `toneFromUsedPct` when a limit exists: * this one stays "ok" until the balance is completely spent. */ export function balanceToneFromRemaining( remaining: number | null | undefined, ): UsageBalance["tone"] { if (typeof remaining !== "number") return "default"; if (remaining <= 0) return "danger"; return "ok"; } /** Percentage of a limit consumed, or null when either side is unknown. */ export function usedPctOf( used: number | null | undefined, limit: number | null | undefined, ): number | null { if (typeof used !== "number" || typeof limit !== "number" || limit <= 0) return null; return (used / limit) * 100; } export function hashAccountKey(value: string): string { return createHash("sha256").update(value).digest("hex"); } export function unavailableUsage(): UsageReport { return { status: "unavailable", windows: [], balances: [], details: [], }; }