432 lines
14 KiB
TypeScript
432 lines
14 KiB
TypeScript
// Live Command Code quota from the alpha billing API. Shared by the TUI plugin
|
|
// (loaded from source) and scripts/quota.mjs. Endpoints, validated against
|
|
// api.commandcode.ai:
|
|
//
|
|
// GET /alpha/whoami
|
|
// GET /alpha/billing/credits?orgId=
|
|
// GET /alpha/billing/subscriptions?orgId=
|
|
// GET /alpha/usage/summary?orgId=
|
|
//
|
|
// These are the same endpoints the `cmd` CLI `/usage` command reads. This module uses
|
|
// global fetch only, so the built provider stays runtime-dependency-free.
|
|
|
|
import { DEFAULT_BASE_URL } from "./constants.js";
|
|
import { redact } from "./redact.js";
|
|
|
|
export const QUOTA_TIMEOUT_MS = 15_000;
|
|
|
|
export type QuotaWindowId = "fiveHour" | "weekly" | "monthly";
|
|
|
|
export type QuotaWindow = {
|
|
id: QuotaWindowId;
|
|
label: string;
|
|
used: number;
|
|
cap: number;
|
|
resetAtMs: number | null;
|
|
exceeded: boolean;
|
|
};
|
|
|
|
export type QuotaCredits = {
|
|
monthly: number;
|
|
purchased: number;
|
|
free: number;
|
|
remaining: number;
|
|
};
|
|
|
|
export type QuotaAccount = {
|
|
login: string;
|
|
keyName?: string;
|
|
orgId: string | null;
|
|
};
|
|
|
|
export type QuotaPlan = {
|
|
id: string;
|
|
status: string;
|
|
currentPeriodStart?: string;
|
|
currentPeriodEnd?: string;
|
|
};
|
|
|
|
export type QuotaSummary = {
|
|
totalCost: number;
|
|
totalCount: number;
|
|
totalTokens?: number;
|
|
periodBasis?: string;
|
|
};
|
|
|
|
export type Quota = {
|
|
account: QuotaAccount;
|
|
plan: QuotaPlan | null;
|
|
credits: QuotaCredits | null;
|
|
summary: QuotaSummary | null;
|
|
windows: QuotaWindow[];
|
|
unavailable: QuotaWindowId[];
|
|
fetchedAt: number;
|
|
};
|
|
|
|
export type QuotaErrorKind = "config" | "http" | "network" | "timeout";
|
|
|
|
export type QuotaResult =
|
|
| { ok: true; quota: Quota }
|
|
| { ok: false; error: { kind: QuotaErrorKind; message: string } };
|
|
|
|
export type QuotaFetchOptions = {
|
|
apiKey: string;
|
|
baseURL?: string;
|
|
orgId?: string;
|
|
timeoutMs?: number;
|
|
fetchImpl?: typeof fetch;
|
|
};
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
}
|
|
|
|
function numberValue(value: unknown): number | undefined {
|
|
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
}
|
|
|
|
function stringValue(value: unknown): string | undefined {
|
|
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
}
|
|
|
|
/** Epoch ms. Upstream has shipped both seconds and ms; >= 1e12 is treated as ms. */
|
|
function resetAtMs(value: unknown): number | null {
|
|
let n: number | undefined;
|
|
if (typeof value === "number" && Number.isFinite(value)) n = value;
|
|
else if (typeof value === "string" && value.trim().length > 0) {
|
|
const text = value.trim();
|
|
n = /^\d+$/.test(text) ? Number(text) : Date.parse(text);
|
|
}
|
|
if (n === undefined || !Number.isFinite(n) || n < 0) return null;
|
|
return n >= 1e12 ? Math.round(n) : Math.round(n * 1000);
|
|
}
|
|
|
|
function parseWindow(id: QuotaWindowId, label: string, value: unknown): QuotaWindow | null {
|
|
if (!isRecord(value)) return null;
|
|
const used = numberValue(value.used);
|
|
const cap = numberValue(value.cap);
|
|
if (used === undefined || cap === undefined || (used === 0 && cap === 0)) return null;
|
|
return {
|
|
id,
|
|
label,
|
|
used,
|
|
cap,
|
|
resetAtMs: resetAtMs(value.resetAt),
|
|
exceeded: value.exceeded === true || (cap > 0 && used >= cap),
|
|
};
|
|
}
|
|
|
|
function parseCredits(value: unknown): { credits: QuotaCredits; windows: QuotaWindow[] } | null {
|
|
if (!isRecord(value) || !isRecord(value.credits)) return null;
|
|
const c = value.credits;
|
|
const monthly = numberValue(c.monthlyCredits) ?? 0;
|
|
const purchased = numberValue(c.purchasedCredits) ?? 0;
|
|
const free = numberValue(c.freeCredits) ?? 0;
|
|
const limits = isRecord(value.windowLimits) ? value.windowLimits : {};
|
|
const windows: QuotaWindow[] = [];
|
|
const fiveHour = parseWindow("fiveHour", "5-hour", limits.fiveHour);
|
|
const weekly = parseWindow("weekly", "Weekly", limits.weekly);
|
|
if (fiveHour) windows.push(fiveHour);
|
|
if (weekly) windows.push(weekly);
|
|
return { credits: { monthly, purchased, free, remaining: monthly + purchased + free }, windows };
|
|
}
|
|
|
|
function parseAccount(value: unknown): QuotaAccount | null {
|
|
if (!isRecord(value)) return null;
|
|
const org = isRecord(value.org) ? value.org : undefined;
|
|
const user = isRecord(value.user) ? value.user : undefined;
|
|
const login =
|
|
(user ? (stringValue(user.userName) ?? stringValue(user.name)) : undefined) ??
|
|
(org ? stringValue(org.login) : undefined);
|
|
if (!login) return null;
|
|
const keyName = user ? (stringValue(user.keyName) ?? stringValue(user.displayName)) : undefined;
|
|
return { login, orgId: org ? (stringValue(org.id) ?? null) : null, ...(keyName ? { keyName } : {}) };
|
|
}
|
|
|
|
function parsePlan(value: unknown): QuotaPlan | null {
|
|
if (!isRecord(value) || !isRecord(value.data)) return null;
|
|
const data = value.data;
|
|
const id = stringValue(data.planId);
|
|
const status = stringValue(data.status);
|
|
if (!id && !status) return null;
|
|
const start = stringValue(data.currentPeriodStart);
|
|
const end = stringValue(data.currentPeriodEnd);
|
|
return {
|
|
id: id ?? "unknown",
|
|
status: status ?? "unknown",
|
|
...(start ? { currentPeriodStart: start } : {}),
|
|
...(end ? { currentPeriodEnd: end } : {}),
|
|
};
|
|
}
|
|
|
|
function parseSummary(value: unknown): QuotaSummary | null {
|
|
if (!isRecord(value)) return null;
|
|
const totalCost = numberValue(value.totalCost);
|
|
const totalCount = numberValue(value.totalCount);
|
|
if (totalCost === undefined || totalCount === undefined) return null;
|
|
const totalTokens = numberValue(value.totalTokens);
|
|
const periodBasis = stringValue(value.periodBasis);
|
|
return {
|
|
totalCost,
|
|
totalCount,
|
|
...(totalTokens === undefined ? {} : { totalTokens }),
|
|
...(periodBasis === undefined ? {} : { periodBasis }),
|
|
};
|
|
}
|
|
|
|
function periodEndMs(plan: QuotaPlan | null): number | null {
|
|
if (!plan?.currentPeriodEnd) return null;
|
|
return resetAtMs(plan.currentPeriodEnd);
|
|
}
|
|
|
|
class QuotaHttpError extends Error {
|
|
readonly status: number;
|
|
readonly body: string;
|
|
constructor(status: number, body: string) {
|
|
super(`HTTP ${status}`);
|
|
this.name = "QuotaHttpError";
|
|
this.status = status;
|
|
this.body = body;
|
|
}
|
|
}
|
|
|
|
class QuotaTimeoutError extends Error {
|
|
constructor() {
|
|
super("Command Code quota request timed out");
|
|
this.name = "QuotaTimeoutError";
|
|
}
|
|
}
|
|
|
|
function isAuthError(error: unknown): boolean {
|
|
return error instanceof QuotaHttpError && (error.status === 401 || error.status === 403);
|
|
}
|
|
|
|
export async function fetchQuota(options: QuotaFetchOptions): Promise<QuotaResult> {
|
|
if (!options.apiKey) {
|
|
return { ok: false, error: { kind: "config", message: "No Command Code API key found" } };
|
|
}
|
|
|
|
const baseURL = (options.baseURL ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
const timeoutMs = options.timeoutMs ?? QUOTA_TIMEOUT_MS;
|
|
const controller = new AbortController();
|
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
const headers = {
|
|
accept: "application/json",
|
|
authorization: /^Bearer\s/i.test(options.apiKey) ? options.apiKey : `Bearer ${options.apiKey}`,
|
|
};
|
|
|
|
const request = async (path: string, params?: Record<string, string | undefined>): Promise<unknown> => {
|
|
if (controller.signal.aborted) throw new QuotaTimeoutError();
|
|
const search = new URLSearchParams();
|
|
for (const [key, value] of Object.entries(params ?? {})) if (value) search.set(key, value);
|
|
const query = search.toString();
|
|
let response: Response;
|
|
try {
|
|
response = await fetchImpl(`${baseURL}${path}${query ? `?${query}` : ""}`, {
|
|
method: "GET",
|
|
headers,
|
|
signal: controller.signal,
|
|
});
|
|
} catch (error) {
|
|
if (controller.signal.aborted) throw new QuotaTimeoutError();
|
|
throw error;
|
|
}
|
|
if (!response.ok) throw new QuotaHttpError(response.status, await response.text().catch(() => ""));
|
|
return (await response.json()) as unknown;
|
|
};
|
|
|
|
try {
|
|
const whoamiRaw = await request("/alpha/whoami");
|
|
const account = parseAccount(whoamiRaw);
|
|
if (!account) {
|
|
return {
|
|
ok: false,
|
|
error: { kind: "http", message: "Command Code returned an unrecognized account response" },
|
|
};
|
|
}
|
|
|
|
const orgId = options.orgId ?? account.orgId ?? undefined;
|
|
const unavailable: QuotaWindowId[] = [];
|
|
|
|
let credits: QuotaCredits | null = null;
|
|
let windows: QuotaWindow[] = [];
|
|
try {
|
|
const parsed = parseCredits(await request("/alpha/billing/credits", { orgId }));
|
|
if (parsed) {
|
|
credits = parsed.credits;
|
|
windows = parsed.windows;
|
|
}
|
|
} catch (error) {
|
|
if (isAuthError(error) || error instanceof QuotaTimeoutError) {
|
|
return { ok: false, error: quotaError(error) };
|
|
}
|
|
}
|
|
|
|
let plan: QuotaPlan | null = null;
|
|
try {
|
|
plan = parsePlan(await request("/alpha/billing/subscriptions", { orgId }));
|
|
} catch (error) {
|
|
if (isAuthError(error) || error instanceof QuotaTimeoutError) {
|
|
return { ok: false, error: quotaError(error) };
|
|
}
|
|
}
|
|
|
|
let summary: QuotaSummary | null = null;
|
|
try {
|
|
summary = parseSummary(await request("/alpha/usage/summary", { orgId, since: plan?.currentPeriodStart }));
|
|
} catch (error) {
|
|
if (isAuthError(error) || error instanceof QuotaTimeoutError) {
|
|
return { ok: false, error: quotaError(error) };
|
|
}
|
|
}
|
|
|
|
if (credits) {
|
|
const spent = summary?.totalCost;
|
|
if (spent !== undefined) {
|
|
windows.push({
|
|
id: "monthly",
|
|
label: "Monthly",
|
|
used: spent,
|
|
cap: spent + credits.remaining,
|
|
resetAtMs: periodEndMs(plan),
|
|
exceeded: credits.remaining <= 0,
|
|
});
|
|
} else {
|
|
unavailable.push("monthly");
|
|
}
|
|
} else {
|
|
unavailable.push("monthly");
|
|
}
|
|
|
|
if (!credits && !summary) {
|
|
return {
|
|
ok: false,
|
|
error: {
|
|
kind: "http",
|
|
message: "Command Code returned no recognized usage data for the account",
|
|
},
|
|
};
|
|
}
|
|
|
|
return {
|
|
ok: true,
|
|
quota: {
|
|
account,
|
|
plan,
|
|
credits,
|
|
summary,
|
|
windows,
|
|
unavailable,
|
|
fetchedAt: Date.now(),
|
|
},
|
|
};
|
|
} catch (error) {
|
|
return { ok: false, error: quotaError(error) };
|
|
} finally {
|
|
clearTimeout(timer);
|
|
}
|
|
}
|
|
|
|
function quotaError(error: unknown): { kind: QuotaErrorKind; message: string } {
|
|
if (error instanceof QuotaTimeoutError) return { kind: "timeout", message: error.message };
|
|
if (error instanceof QuotaHttpError) {
|
|
const detail = error.body.trim().slice(0, 200);
|
|
const hint = error.status === 401 || error.status === 403 ? " (check the API key)" : "";
|
|
return {
|
|
kind: "http",
|
|
message: redact(`Command Code quota request failed (${error.status})${hint}: ${detail}`),
|
|
};
|
|
}
|
|
return {
|
|
kind: "network",
|
|
message: redact(`Command Code quota request failed: ${error instanceof Error ? error.message : String(error)}`),
|
|
};
|
|
}
|
|
|
|
export function percent(used: number, cap: number): number {
|
|
if (!(cap > 0)) return 0;
|
|
return Math.round((used / cap) * 100);
|
|
}
|
|
|
|
export function quotaBar(used: number, cap: number, width = 10): string {
|
|
const fraction = cap > 0 ? Math.min(1, Math.max(0, used / cap)) : 0;
|
|
const filled = Math.round(fraction * width);
|
|
return `${"█".repeat(filled)}${"░".repeat(width - filled)}`;
|
|
}
|
|
|
|
/** Compact countdown, e.g. `3h 12m` / `2d 4h`. Empty when there is no reset. */
|
|
export function formatReset(resetAtMs: number | null, now: number = Date.now()): string {
|
|
if (resetAtMs === null) return "";
|
|
const diff = resetAtMs - now;
|
|
if (diff <= 0) return "soon";
|
|
const minutes = Math.ceil(diff / 60_000);
|
|
if (minutes < 60) return `${minutes}m`;
|
|
const hours = Math.floor(minutes / 60);
|
|
const mins = minutes % 60;
|
|
if (hours < 24) return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`;
|
|
const days = Math.floor(hours / 24);
|
|
const rem = hours % 24;
|
|
return rem > 0 ? `${days}d ${rem}h` : `${days}d`;
|
|
}
|
|
|
|
function formatTokens(tokens: number): string {
|
|
if (tokens >= 1_000_000_000) return `${(tokens / 1_000_000_000).toFixed(1)}B`;
|
|
if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}M`;
|
|
if (tokens >= 1_000) return `${(tokens / 1_000).toFixed(1)}k`;
|
|
return String(tokens);
|
|
}
|
|
|
|
function formatWindowLine(w: QuotaWindow, now: number): string {
|
|
const pct = Math.min(percent(w.used, w.cap), 999);
|
|
const reset = formatReset(w.resetAtMs, now);
|
|
const amount =
|
|
w.id === "monthly"
|
|
? `$${w.used.toFixed(2)} / $${w.cap.toFixed(2)}`
|
|
: `${w.used.toFixed(2)} / ${w.cap.toFixed(2)} credits`;
|
|
return `${w.label.padEnd(8)} ${quotaBar(w.used, w.cap)} ${String(pct).padStart(3)}% ${amount}${
|
|
reset ? ` (resets in ${reset})` : ""
|
|
}`;
|
|
}
|
|
|
|
function planLine(plan: QuotaPlan, now: number): string {
|
|
const name = plan.id.replace(/[_-]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
const status = plan.status ? ` · ${plan.status}` : "";
|
|
const end = periodEndMs(plan);
|
|
if (end === null) return `Plan: ${name}${status}`;
|
|
const remaining = end - now;
|
|
const days = Math.ceil(remaining / 86_400_000);
|
|
const date = new Date(end).toISOString().slice(0, 10);
|
|
const when = days > 0 ? `renews ${date} (${days}d)` : days === 0 ? `renews ${date} (today)` : `renewed ${date}`;
|
|
return `Plan: ${name}${status} · ${when}`;
|
|
}
|
|
|
|
/** Human-readable multi-line quota summary for the `/cc-usage` command and CLI. */
|
|
export function formatQuota(quota: Quota, now: number = Date.now()): string {
|
|
const lines: string[] = [];
|
|
lines.push(quota.account.keyName ?? quota.account.login);
|
|
if (quota.plan) lines.push(planLine(quota.plan, now));
|
|
|
|
if (quota.windows.length > 0) {
|
|
lines.push("");
|
|
for (const w of quota.windows) lines.push(formatWindowLine(w, now));
|
|
}
|
|
|
|
if (quota.credits) {
|
|
const parts = [`monthly $${quota.credits.monthly.toFixed(2)}`, `purchased $${quota.credits.purchased.toFixed(2)}`];
|
|
if (quota.credits.free > 0) parts.push(`free $${quota.credits.free.toFixed(2)}`);
|
|
lines.push("", `Credits: ${parts.join(" / ")}`);
|
|
}
|
|
|
|
if (quota.summary) {
|
|
const period = quota.plan?.currentPeriodStart ? "this billing period" : "total";
|
|
const tokens = quota.summary.totalTokens === undefined ? "" : ` · ${formatTokens(quota.summary.totalTokens)} tokens`;
|
|
lines.push(
|
|
`Usage: ${quota.summary.totalCount.toLocaleString("en-US")} requests · $${quota.summary.totalCost.toFixed(2)} (${period})${tokens}`,
|
|
);
|
|
}
|
|
|
|
if (quota.unavailable.length > 0) lines.push("", `Unavailable: ${quota.unavailable.join(", ")}`);
|
|
return lines.join("\n");
|
|
}
|