import type { AuthResult } from "@earendil-works/pi-ai"; import * as j from "@char/justin"; import type { Provider, SubscriptionUsage, UsageWindow } from "./types.ts"; const object = (value: unknown): Record => value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record : {}; const finite = (value: unknown): value is number => typeof value === "number" && Number.isFinite(value); const goWindowOf = j.validation.compile(j.union( j.obj({ percent: j.number, resetsAt: j.optional(j.union(j.string, j.null)) }), j.obj({ usagePercent: j.number, resetInSec: j.optional(j.union(j.number, j.null)) }), )); export function usageWindows(provider: Provider, value: unknown, now = Date.now()): UsageWindow[] { const data = object(value); const windows: UsageWindow[] = []; const add = (label: string, percent: unknown, reset: unknown) => { if (!finite(percent) || percent < 0) return; const date = new Date( typeof reset === "string" ? Date.parse(reset) : finite(reset) && reset > 0 ? reset * 1000 : NaN, ); windows.push({ label, usedPercent: percent, resetsAt: Number.isFinite(date.getTime()) ? date.toISOString() : null, }); }; if (provider === "anthropic") { for ( const [key, label] of [ ["five_hour", "5 hours"], ["seven_day", "Weekly"], ["seven_day_opus", "Weekly · Opus"], ["seven_day_sonnet", "Weekly · Sonnet"], ["seven_day_oauth_apps", "Weekly · OAuth apps"], ] ) { const window = object(data[key]); add(label, window.utilization, window.resets_at); } if (Array.isArray(data.limits)) { for (const raw of data.limits) { const limit = object(raw); if (limit.is_active === false) continue; const model = object(object(limit.scope).model).display_name; if (typeof model !== "string") continue; const label = `Weekly · ${model}`; if (!windows.some((window) => window.label === label)) { add(label, limit.percent, limit.resets_at); } } } } else if (provider === "opencode-go") { const usage = object(data.usage); for ( const [key, label] of [["rolling", "5 hours"], ["weekly", "Weekly"], ["monthly", "Monthly"]] ) { // older Go endpoints report relative reset times and use top-level *Usage fields. const window = goWindowOf(usage[key] ?? data[`${key}Usage`]).value; if (!window) continue; if ("percent" in window) { add(label, window.percent, window.resetsAt); } else { const seconds = window.resetInSec; add( label, window.usagePercent, finite(seconds) && seconds >= 0 ? now / 1000 + seconds : null, ); } } } else { const limits = [{ label: "", rate_limit: data.rate_limit }]; if (Array.isArray(data.additional_rate_limits)) { for (const raw of data.additional_rate_limits) { const extra = object(raw); const label = extra.limit_name ?? extra.metered_feature; if (typeof label === "string") limits.push({ label, rate_limit: extra.rate_limit }); } } for (const { label, rate_limit } of limits) { const rate = object(rate_limit); for ( const [key, fallback] of [["primary_window", "Session"], ["secondary_window", "Weekly"]] ) { const window = object(rate[key]); const seconds = window.limit_window_seconds; const duration = finite(seconds) && seconds > 0 ? seconds % 86400 === 0 ? `${seconds / 86400} days` : `${seconds / 3600} hours` : fallback; add(label ? `${label} · ${duration}` : duration, window.used_percent, window.reset_at); } } } return windows; } export function subscriptionUsage({ auth, fetch: request = fetch, now = Date.now, }: { auth: (provider: string, signal?: AbortSignal) => Promise; fetch?: typeof fetch; now?: () => number; }) { const cache = new Map(); const pending = new Map>(); async function load(provider: Provider): Promise { const signal = AbortSignal.timeout(15_000); let resolved: AuthResult | undefined; try { resolved = await auth(provider, signal); } catch { return { status: "error", message: "Could not refresh login. Try signing in again in Settings.", }; } if (!resolved?.auth.apiKey || (provider !== "opencode-go" && resolved.source !== "OAuth")) { cache.delete(provider); return { status: "unavailable", message: provider === "opencode-go" ? "Add your OpenCode Go API key in Settings to view subscription limits." : "Subscription limits require an OAuth login in Settings; API keys do not expose them.", }; } const token = resolved.auth.apiKey; const cached = cache.get(provider); if (cached?.token === token && cached.until > now()) return cached.result; let result: SubscriptionUsage; let ttl = 60_000; try { const headers = new Headers({ authorization: `Bearer ${token}`, accept: "application/json" }); const url = provider === "anthropic" ? "https://api.anthropic.com/api/oauth/usage" : provider === "opencode-go" ? "https://opencode.ai/inference/go/v1/usage" : "https://chatgpt.com/backend-api/wham/usage"; if (provider === "anthropic") { headers.set("anthropic-beta", "oauth-2025-04-20"); } else if (provider !== "opencode-go") { try { const payload = JSON.parse( atob(token.split(".")[1].replaceAll("-", "+").replaceAll("_", "/")), ); const account = payload["https://api.openai.com/auth"]?.chatgpt_account_id; if (typeof account !== "string" || !account) throw new Error("Missing account"); headers.set("ChatGPT-Account-Id", account); } catch { return { status: "error", message: "Could not identify the ChatGPT account. Sign in again in Settings.", }; } } const response = await request(url, { headers, signal, redirect: "error" }); if (!response.ok) { const message = response.status === 401 || response.status === 403 ? provider === "opencode-go" ? "Usage access was denied. Check your OpenCode Go API key and subscription in Settings." : provider === "anthropic" ? "Usage access was denied. Sign in again in Settings; Claude may need the user:profile scope." : "Usage access was denied. Sign in again in Settings." : response.status === 429 ? "Usage is rate limited. Try again in a few minutes." : `Usage service returned HTTP ${response.status}.`; if (response.status === 429) { const retry = response.headers.get("retry-after") ?? ""; const seconds = Number(retry); const until = retry.trim() && Number.isFinite(seconds) ? now() + seconds * 1000 : Date.parse(retry); ttl = Math.max(5 * 60_000, Number.isFinite(until) ? until - now() : 0); } await response.body?.cancel(); result = { status: "error", message }; } else { const windows = usageWindows(provider, await response.json(), now()); result = windows.length ? { status: "ok", fetchedAt: new Date(now()).toISOString(), windows } : { status: "unavailable", message: "No subscription usage windows were reported for this account.", }; } } catch { result = { status: "error", message: "Could not reach the usage service. Try again shortly.", }; } cache.set(provider, { token, until: now() + ttl, result }); return result; } return (provider: Provider): Promise => { const existing = pending.get(provider); if (existing) return existing; const next = load(provider).finally(() => pending.delete(provider)); pending.set(provider, next); return next; }; }