Files
paseo/packages/plugin/src/server/usage.ts
T
Mohamed Boudra 2e53a5b9b5 refactor(usage): shape sources as fetch functions
Keep the plugin usage API limited to stable report and presentation helpers. Source-local parsers and timeout handling preserve credential and response parity while the cases move alongside each source.
2026-09-26 20:52:37 +02:00

120 lines
3.4 KiB
TypeScript

import type { ZodType } from "zod";
import type { JsonValue } from "@getpaseo/protocol/agent-types";
export interface UsageWindow {
id: string;
label: string;
usedPct?: number | null;
remainingPct?: number | null;
resetsAt?: string | null;
runsOutAt?: string | null;
shortfallPct?: number | null;
tone?: "default" | "ok" | "warning" | "danger";
headline?: boolean;
}
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 {
account: { key: string; label?: string };
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;
fetch(input: unknown): Promise<UsageReport>;
discover?(): Promise<JsonValue[]>;
}
export function windowFromUsedPct(input: {
id: string;
label: string;
utilizationPct: number | null | undefined;
resetsAt?: string | null;
tone?: UsageWindow["tone"];
headline?: boolean;
}): 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.tone) window.tone = input.tone;
if (input.headline) window.headline = true;
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 unavailableUsage(accountKey = "default"): UsageReport {
return {
account: { key: accountKey },
status: "unavailable",
windows: [],
balances: [],
details: [],
};
}