refactor(quota): move provider quota logic into adapter layer, add unit tests

- Extract all Anthropic credential/API logic into claude-local/src/server/quota.ts
- Extract all OpenAI/WHAM credential/API logic into codex-local/src/server/quota.ts
- Add optional getQuotaWindows() to ServerAdapterModule in adapter-utils
- Rewrite quota-windows.ts as a 29-line thin aggregator with zero provider knowledge
- Wire getQuotaWindows into adapter registry for claude-local and codex-local
- Add 47 unit tests covering toPercent, secondsToWindowLabel, WHAM normalization,
  readClaudeToken, readCodexToken, fetchClaudeQuota, fetchCodexQuota, fetchWithTimeout
- Add 8 unit tests covering parseDateRange validation and byProvider pro-rata math

Adding a third provider now requires only touching that provider's adapter.
This commit is contained in:
Sai Shankar
2026-03-10 11:32:12 +05:30
committed by Dotta
parent f383a37b01
commit 656b4659fc
10 changed files with 1149 additions and 262 deletions

View File

@@ -1,6 +1,14 @@
export { execute, ensureCodexSkillsInjected } from "./execute.js";
export { testEnvironment } from "./test.js";
export { parseCodexJsonl, isCodexUnknownSessionError } from "./parse.js";
export {
getQuotaWindows,
readCodexToken,
fetchCodexQuota,
secondsToWindowLabel,
fetchWithTimeout,
codexHomeDir,
} from "./quota.js";
import type { AdapterSessionCodec } from "@paperclipai/adapter-utils";
function readNonEmptyString(value: unknown): string | null {

View File

@@ -0,0 +1,154 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import type { ProviderQuotaResult, QuotaWindow } from "@paperclipai/adapter-utils";
export function codexHomeDir(): string {
const fromEnv = process.env.CODEX_HOME;
if (typeof fromEnv === "string" && fromEnv.trim().length > 0) return fromEnv.trim();
return path.join(os.homedir(), ".codex");
}
interface CodexAuthFile {
accessToken?: string | null;
accountId?: string | null;
}
export async function readCodexToken(): Promise<{ token: string; accountId: string | null } | null> {
const authPath = path.join(codexHomeDir(), "auth.json");
let raw: string;
try {
raw = await fs.readFile(authPath, "utf8");
} catch {
return null;
}
let parsed: unknown;
try {
parsed = JSON.parse(raw);
} catch {
return null;
}
if (typeof parsed !== "object" || parsed === null) return null;
const obj = parsed as CodexAuthFile;
const token = obj.accessToken;
if (typeof token !== "string" || token.length === 0) return null;
const accountId =
typeof obj.accountId === "string" && obj.accountId.length > 0 ? obj.accountId : null;
return { token, accountId };
}
interface WhamWindow {
used_percent?: number | null;
limit_window_seconds?: number | null;
reset_at?: string | null;
}
interface WhamCredits {
balance?: number | null;
unlimited?: boolean | null;
}
interface WhamUsageResponse {
rate_limit?: {
primary_window?: WhamWindow | null;
secondary_window?: WhamWindow | null;
} | null;
credits?: WhamCredits | null;
}
/**
* Map a window duration in seconds to a human-readable label.
* Falls back to the provided fallback string when seconds is null/undefined.
*/
export function secondsToWindowLabel(
seconds: number | null | undefined,
fallback: string,
): string {
if (seconds == null) return fallback;
const hours = seconds / 3600;
if (hours < 6) return "5h";
if (hours <= 24) return "24h";
if (hours <= 168) return "7d";
// for windows larger than 7d, show the actual day count rather than silently mislabelling
return `${Math.round(hours / 24)}d`;
}
/** fetch with an abort-based timeout so a hanging provider api doesn't block the response indefinitely */
export async function fetchWithTimeout(
url: string,
init: RequestInit,
ms = 8000,
): Promise<Response> {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), ms);
try {
return await fetch(url, { ...init, signal: controller.signal });
} finally {
clearTimeout(timer);
}
}
export async function fetchCodexQuota(
token: string,
accountId: string | null,
): Promise<QuotaWindow[]> {
const headers: Record<string, string> = {
Authorization: `Bearer ${token}`,
};
if (accountId) headers["ChatGPT-Account-Id"] = accountId;
const resp = await fetchWithTimeout("https://chatgpt.com/backend-api/wham/usage", { headers });
if (!resp.ok) throw new Error(`chatgpt wham api returned ${resp.status}`);
const body = (await resp.json()) as WhamUsageResponse;
const windows: QuotaWindow[] = [];
const rateLimit = body.rate_limit;
if (rateLimit?.primary_window != null) {
const w = rateLimit.primary_window;
// wham used_percent is 0-100 (confirmed empirically); guard against 0-1 format just in case.
// use < 1 (not <= 1) so that 1% usage (rawPct=1) is not misclassified as 100%.
const rawPct = w.used_percent ?? null;
const usedPercent =
rawPct != null ? Math.min(100, Math.round(rawPct < 1 ? rawPct * 100 : rawPct)) : null;
windows.push({
label: secondsToWindowLabel(w.limit_window_seconds, "Primary"),
usedPercent,
resetsAt: w.reset_at ?? null,
valueLabel: null,
});
}
if (rateLimit?.secondary_window != null) {
const w = rateLimit.secondary_window;
// wham used_percent is 0-100 (confirmed empirically); guard against 0-1 format just in case.
// use < 1 (not <= 1) so that 1% usage (rawPct=1) is not misclassified as 100%.
const rawPct = w.used_percent ?? null;
const usedPercent =
rawPct != null ? Math.min(100, Math.round(rawPct < 1 ? rawPct * 100 : rawPct)) : null;
windows.push({
label: secondsToWindowLabel(w.limit_window_seconds, "Secondary"),
usedPercent,
resetsAt: w.reset_at ?? null,
valueLabel: null,
});
}
if (body.credits != null && body.credits.unlimited !== true) {
const balance = body.credits.balance;
const valueLabel = balance != null ? `$${(balance / 100).toFixed(2)} remaining` : "N/A";
windows.push({
label: "Credits",
usedPercent: null,
resetsAt: null,
valueLabel,
});
}
return windows;
}
export async function getQuotaWindows(): Promise<ProviderQuotaResult> {
const auth = await readCodexToken();
if (!auth) {
return { provider: "openai", ok: false, error: "no local codex auth token", windows: [] };
}
const windows = await fetchCodexQuota(auth.token, auth.accountId);
return { provider: "openai", ok: true, windows };
}