UNPKG

@juspay/neurolink

Version:

The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002

302 lines (301 loc) • 11.9 kB
/** * On-Demand Account Usage Fetch * * Lightweight account-limit refresh: queries Anthropic's OAuth usage endpoint * (the same call Claude Code's /usage makes) to get fresh session / weekly / * model-scoped windows without consuming tokens or starting a 5h session. * * This complements — never replaces — the passive header capture in * accountQuota.ts: `usageToQuota` normalizes the endpoint payload into the * same `AccountQuota` shape so refreshed data flows through the existing * save/merge/cooldown chain. Manual refresh and adaptive proxy prewarming use * the same transport through the route-owned single-flight coordinator. * * Only OAuth (Bearer) accounts have subscription windows; api_key accounts * are skipped and keep their header-derived absolute limits. */ import { logger } from "../utils/logger.js"; import { tokenStore } from "../auth/tokenStore.js"; import { CLAUDE_CLI_USER_AGENT, OAUTH_BETA_HEADERS, } from "../auth/anthropicOAuth.js"; import { needsRefresh, refreshTokenFromLatest, isPermanentRefreshFailure, persistTokens, } from "./tokenRefresh.js"; import { isAccountAllowed } from "./accountSelection.js"; export const ANTHROPIC_USAGE_URL = "https://api.anthropic.com/api/oauth/usage"; const USAGE_FETCH_TIMEOUT_MS = 10_000; /** * Enumerate stored Anthropic accounts for a usage refresh. * Read-only: no token refresh, no disable side effects — those decisions * belong to the caller (fetchAccountUsage / the routing path respectively). */ export async function listAnthropicAccountsForUsage(allowlist) { const accounts = []; const compoundKeys = await tokenStore.listByPrefix("anthropic:"); for (const key of compoundKeys) { if (!isAccountAllowed(key, allowlist)) { continue; } if (await tokenStore.isDisabled(key)) { continue; } const tokens = await tokenStore.loadTokens(key); if (!tokens) { continue; } // Full suffix after the first ":" — a label may itself contain colons, // and the quota store and `auth list` rendering key by the full label. const separatorIndex = key.indexOf(":"); accounts.push({ key, label: separatorIndex >= 0 ? key.slice(separatorIndex + 1) : key, token: tokens.accessToken, refreshToken: tokens.refreshToken, expiresAt: tokens.expiresAt, type: tokens.tokenType === "Bearer" ? "oauth" : "api_key", persistTarget: { providerKey: key }, }); } return accounts; } async function requestUsage(token) { try { const response = await fetch(ANTHROPIC_USAGE_URL, { method: "GET", headers: { authorization: `Bearer ${token}`, "anthropic-beta": OAUTH_BETA_HEADERS, "user-agent": CLAUDE_CLI_USER_AGENT, accept: "application/json", }, signal: AbortSignal.timeout(USAGE_FETCH_TIMEOUT_MS), }); return { response }; } catch (err) { return { networkError: err instanceof Error ? err.message : String(err) }; } } /** * Fetch fresh usage windows for one account. Return-not-throw. * Refreshes an expiring/expired token first (de-duped, rotation-safe), and * retries once through a forced refresh on a 401. */ export async function fetchAccountUsage(account) { if (account.type !== "oauth") { return { ok: false, reason: "not_oauth", error: "api_key accounts have no subscription usage windows", }; } const runRefresh = async () => { const result = await refreshTokenFromLatest(account, account.persistTarget); if (result.success) { if (account.persistTarget) { await persistTokens(account.persistTarget, account); } return null; } if (isPermanentRefreshFailure(result)) { return { ok: false, reason: "auth", error: "reauth required (refresh token rejected)", status: result.status, }; } const tokenExpired = account.expiresAt ? account.expiresAt <= Date.now() : false; return tokenExpired ? { ok: false, reason: "auth", error: "token expired and refresh failed", status: result.status, } : null; // token still valid — attempt the fetch with it }; let refreshedThisCall = false; if (needsRefresh(account)) { refreshedThisCall = true; const failure = await runRefresh(); if (failure) { return failure; } } let { response, networkError } = await requestUsage(account.token); if (response?.status === 401 && account.refreshToken && !refreshedThisCall) { const failure = await runRefresh(); if (failure) { return failure; } ({ response, networkError } = await requestUsage(account.token)); } if (!response) { return { ok: false, reason: "network", error: networkError ?? "network failure", }; } if (!response.ok) { const body = await response.text().catch(() => ""); logger.debug(`[account-usage] usage fetch failed for ${account.label}`, { status: response.status, body: body.slice(0, 300), }); return { ok: false, reason: response.status === 401 || response.status === 403 ? "auth" : "http", error: `usage endpoint returned HTTP ${response.status}`, status: response.status, }; } try { const usage = (await response.json()); return { ok: true, usage }; } catch (err) { return { ok: false, reason: "parse", error: err instanceof Error ? err.message : String(err), }; } } // --------------------------------------------------------------------------- // Normalization (pure CPU — unit-testable, no I/O) // --------------------------------------------------------------------------- const REJECTED_SEVERITIES = new Set([ "rejected", "exceeded", "over_limit", "blocked", ]); /** * "rejected" iff the window is fully used or the provider severity marks it * exhausted; anything unknown stays "allowed". Conservative on purpose: a * wrong "rejected" would park an account, a wrong "allowed" just delays * parking until the passive capture or the reset. Raw severity is preserved * on the window so nothing is lost. */ function deriveWindowStatus(percent, severity) { const sev = severity?.trim().toLowerCase(); if ((typeof percent === "number" && percent >= 100) || (sev !== undefined && REJECTED_SEVERITIES.has(sev))) { return "rejected"; } return "allowed"; } /** ISO-8601 → unix seconds; 0 when absent/unparseable (matches headers). */ function isoToEpochSeconds(value) { if (!value) { return 0; } const ms = Date.parse(value); return Number.isNaN(ms) ? 0 : Math.floor(ms / 1000); } function toFraction(percent) { return typeof percent === "number" && Number.isFinite(percent) ? percent / 100 : undefined; } function mapUsageLimit(entry, now) { const window = { kind: entry.kind ?? "unknown", used: toFraction(entry.percent) ?? 0, status: deriveWindowStatus(entry.percent, entry.severity), resetsAt: isoToEpochSeconds(entry.resets_at), source: "usage-api", updatedAt: now, }; if (entry.group !== undefined) { window.group = entry.group; } if (entry.severity !== null && entry.severity !== undefined) { window.severity = entry.severity; } if (entry.is_active !== null && entry.is_active !== undefined) { window.isActive = entry.is_active; } const scopeModel = entry.scope?.model?.display_name; if (scopeModel) { window.scopeModel = scopeModel; } // The wire id matches a request's `model` exactly, so keeping it lets routing // skip the fuzzy display-name match ("Fable" vs "claude-fable-5-20260115"). // Often null in practice, which is why the display-name path still exists. const scopeModelId = entry.scope?.model?.id; if (scopeModelId) { window.scopeModelId = scopeModelId; } const scopeSurface = entry.scope?.surface; if (scopeSurface) { window.scopeSurface = scopeSurface; } return window; } /** * Normalize a usage-endpoint payload into the `AccountQuota` shape used by * the passive header path, so a refresh writes through the exact same * save/merge/cooldown chain. * * `prior` supplies fields the usage payload cannot express (fallback and * upgrade-path signals) so `isQuotaOverageAvailable` behaves identically * before and after a refresh. `unifiedStatus` is deliberately never set: * fabricating it would let the reconcile path treat a refresh like a * unified-rejected 429. */ export function usageToQuota(usage, opts) { const limits = Array.isArray(usage.limits) ? usage.limits : []; if (!usage.five_hour && !usage.seven_day && limits.length === 0) { return null; } const { now, prior } = opts; const windows = limits.map((entry) => mapUsageLimit(entry, now)); const sessionLimit = limits.find((entry) => entry.kind === "session"); const weeklyLimit = limits.find((entry) => entry.kind === "weekly_all"); const sessionPct = usage.five_hour?.utilization ?? sessionLimit?.percent ?? undefined; const weeklyPct = usage.seven_day?.utilization ?? weeklyLimit?.percent ?? undefined; // A refresh must never replace a known reset with 0: the payload omits // `resets_at` for untouched windows, and a zero reset would stop // reconcileCooldownFromQuota from parking a rejected window and make the // routing metrics treat it as non-ticking. A stale prior reset degrades // gracefully (past resets are ignored / freshened downstream). const sessionResetAt = isoToEpochSeconds(usage.five_hour?.resets_at) || isoToEpochSeconds(sessionLimit?.resets_at) || (prior?.sessionResetAt ?? 0); const weeklyResetAt = isoToEpochSeconds(usage.seven_day?.resets_at) || isoToEpochSeconds(weeklyLimit?.resets_at) || (prior?.weeklyResetAt ?? 0); const overageEnabled = usage.extra_usage?.is_enabled; const overageStatus = overageEnabled === true ? "allowed" : overageEnabled === false ? "rejected" : (prior?.overageStatus ?? "unknown"); const quota = { sessionUsed: toFraction(sessionPct) ?? prior?.sessionUsed ?? 0, sessionStatus: deriveWindowStatus(sessionPct, sessionLimit?.severity), sessionResetAt, weeklyUsed: toFraction(weeklyPct) ?? prior?.weeklyUsed ?? 0, weeklyStatus: deriveWindowStatus(weeklyPct, weeklyLimit?.severity), weeklyResetAt, fallbackPercentage: prior?.fallbackPercentage ?? 0, overageStatus, // Authoritative: the header trio the legacy overage checks rely on is only // ever sent on a served response, so an account refreshed but not yet used // would otherwise look overage-ineligible even with extra usage switched on. ...(typeof overageEnabled === "boolean" ? { overageEnabled } : {}), lastUpdated: now, windows, windowsUpdatedAt: now, source: "usage-api", }; if (prior?.fallbackStatus !== undefined) { quota.fallbackStatus = prior.fallbackStatus; } if (prior?.upgradePaths !== undefined) { quota.upgradePaths = prior.upgradePaths; } return quota; }