feat: live multi-provider quota panel (#1278)

* feat: live provider quota panel (Claude + Codex)

Adds Claude and Codex plan usage to the context window percentage circle tooltip, querying their respective APIs directly using existing CLI auth tokens.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: address review feedback - restore quota trigger, guard NaN date

- Re-add triggerFetch() on agent idle/error in agent status subscription
- Guard formatResetsAtLabel against NaN from malformed API date strings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: resolve composer conflict and stub subscribe in websocket tests

* feat(quota): add cursor, copilot, zai, grok, and kimi quota UI and improve type safety

* feat(quota): make Claude and Codex pluggable, and map additional quota/credits metrics

* security(quota): fix shell injection vulnerability by migrating exec to execFile

* revert: restore original scripts/dev.ps1 and packages/desktop/scripts/dev.ps1

* Fix i18n resource test expectation

* Fix quota tooltip empty-provider state

* Preserve zero values in Grok quota

* Fix empty quota fetch state

* Ignore quota frames in relay reconnect tests

* Persist refreshed Codex tokens to source auth file

* fix(quota): read Claude OAuth credentials from the macOS login Keychain

On macOS, Claude Code stores its OAuth credential in the login Keychain
(generic-password item, service "Claude Code-credentials"), not in
~/.claude/.credentials.json — that file usually does not exist there, so the
Claude provider's existsSync check failed and macOS users silently got no
Claude quota. Read it via the macOS `security` CLI (its ACL only trusts
/usr/bin/security to decrypt; a native Keychain read would prompt), and stay
read-only there since Claude Code owns the refresh/persist. Linux and Windows
keep using ~/.claude/.credentials.json unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Fix quota keychain timeout and local host reconciliation

* Fix terminal notification test quota stub

* Ignore quota frames in terminal notification tests

* fix(quota): bypass Claude token refresh on macOS Keychain-backed logins

* fix(dev): update local dev daemon port to 6768 on Windows scripts/dev.ps1

* fix(dev): bypass Unix dev-daemon.sh on Windows in dev.ps1

* fix(client): handle and dispatch provider_quota event in DaemonClient

* fix(desktop): fix PowerShell quote stripping in desktop dev.ps1 config seeding

* fix(desktop): execute config update via temp file to avoid PowerShell quoting bugs

* fix(desktop): fix concurrently command invocation on Windows in dev.ps1

* fix(desktop): resolve path escaping and environment setting syntax errors in dev.ps1 for Windows

* Add on-demand provider usage views

* Fix Cursor usage billing dates

* Move provider usage renderer coverage to e2e

* Use provider manifest for quota fetchers

* Add provider usage fetch timeouts

* Reshape provider usage fetchers

---------

Co-authored-by: ABorakati <ABorakati@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
Co-authored-by: lumingjun <lumingjun@bytedance.com>
This commit is contained in:
Aditya Borakati
2026-06-20 12:19:50 +08:00
committed by GitHub
co-authored by ABorakati Claude Sonnet 4.6 Mohamed Boudra lumingjun
parent 2b0740ff84
commit acea7f3d24
59 changed files with 3944 additions and 132 deletions
@@ -11,7 +11,7 @@ import type {
ResolveAgentCreateConfigInput,
} from "./agent-sdk-types.js";
import type { ManagedAgent } from "./agent-manager.js";
import { ProviderSnapshotManager } from "./provider-snapshot-manager.js";
import { ProviderSnapshotManager, resolveSnapshotCwd } from "./provider-snapshot-manager.js";
import { OpenCodeAgentClient } from "./providers/opencode-agent.js";
const TEST_CAPABILITIES = {
@@ -873,4 +873,13 @@ describe("ProviderSnapshotManager cwd routing", () => {
manager.destroy();
}
});
test("resolveSnapshotCwd normalizes pure drive letters to append backslash on Windows", () => {
const resolved = resolveSnapshotCwd("C:");
if (process.platform === "win32") {
expect(resolved).toBe("C:\\");
} else {
expect(resolved).toBeDefined();
}
});
});
@@ -766,9 +766,16 @@ export function resolveSnapshotCwd(cwd?: string | null): string {
if (!trimmed) {
return homedir();
}
const expanded =
let expanded =
trimmed === "~" || trimmed.startsWith("~/") ? `${homedir()}${trimmed.slice(1)}` : trimmed;
return resolve(expanded);
if (process.platform === "win32" && /^[A-Za-z]:$/.test(expanded)) {
expanded = `${expanded}\\`;
}
let resolved = resolve(expanded);
if (process.platform === "win32" && /^[A-Za-z]:$/.test(resolved)) {
resolved = `${resolved}\\`;
}
return resolved;
}
function entriesToArray(
@@ -35,7 +35,7 @@ export class MockSlowProviderClient implements AgentClient {
readonly capabilities = CAPABILITIES;
async isAvailable(): Promise<boolean> {
return true;
return process.env.PASEO_ENABLE_MOCK_SLOW === "true";
}
listModels(_options: ListModelsOptions): Promise<AgentModelDefinition[]> {
@@ -5,6 +5,8 @@ import { join } from "node:path";
import type { Logger } from "pino";
import { z } from "zod";
import { withTimeout } from "../../../../utils/promise-timeout.js";
import {
type AgentCapabilityFlags,
type AgentClient,
@@ -1996,21 +1998,33 @@ export class PiRpcAgentClient implements AgentClient {
}
async isAvailable(): Promise<boolean> {
const launch = await this.resolvePiLaunch();
const availability = await checkProviderLaunchAvailable(launch);
if (!availability.available) {
return false;
}
const runtimeSession = await this.runtime.startSession({ cwd: homedir() }).catch(() => null);
if (!runtimeSession) {
return false;
}
try {
return (await runtimeSession.getAvailableModels()).length > 0;
return await withTimeout(
(async () => {
const launch = await this.resolvePiLaunch();
const availability = await checkProviderLaunchAvailable(launch);
if (!availability.available) {
return false;
}
const runtimeSession = await this.runtime
.startSession({ cwd: homedir() })
.catch(() => null);
if (!runtimeSession) {
return false;
}
try {
return (await runtimeSession.getAvailableModels()).length > 0;
} catch {
return false;
} finally {
await runtimeSession.close().catch(() => undefined);
}
})(),
2000,
"Pi availability check timed out",
);
} catch {
return false;
} finally {
await runtimeSession.close().catch(() => undefined);
}
}
@@ -436,6 +436,19 @@ describe("paseo daemon bootstrap", () => {
});
});
test("parses IPv6 listen targets correctly", () => {
expect(parseListenString("[::1]:6767")).toEqual({
type: "tcp",
host: "::1",
port: 6767,
});
expect(parseListenString("[::]:6767")).toEqual({
type: "tcp",
host: "::",
port: 6767,
});
});
test("rejects Windows absolute paths that are not named pipes", () => {
// A Windows drive path like C:\daemon must NOT be silently parsed as TCP
// (split(":") would yield host="C" and port="\\daemon" which is nonsensical).
+5 -2
View File
@@ -66,12 +66,15 @@ export function parseListenString(listen: string): ListenTarget {
}
// 6. host:port — TCP
if (listen.includes(":")) {
const [host, portStr] = listen.split(":");
const lastColonIdx = listen.lastIndexOf(":");
const host = listen.slice(0, lastColonIdx);
const portStr = listen.slice(lastColonIdx + 1);
const parsedPort = parseInt(portStr, 10);
if (!Number.isFinite(parsedPort)) {
throw new Error(`Invalid port in listen string: ${listen}`);
}
return { type: "tcp", host: host || "127.0.0.1", port: parsedPort };
const cleanHost = host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
return { type: "tcp", host: cleanHost || "127.0.0.1", port: parsedPort };
}
throw new Error(`Invalid listen string: ${listen}`);
}
+35
View File
@@ -246,6 +246,7 @@ import {
type GitHubService,
type PullRequestTimelineItem,
} from "../services/github-service.js";
import type { ProviderUsageService } from "../services/quota-fetcher/service.js";
import {
summarizeFetchWorkspacesEntries,
workspaceIdsOnCheckout,
@@ -631,6 +632,7 @@ export interface SessionOptions {
tts: Resolvable<TextToSpeechProvider | null>;
terminalManager: TerminalManager | null;
providerSnapshotManager: ProviderSnapshotManager;
providerUsageService: ProviderUsageService;
serviceProxy?: ServiceProxySubsystem;
scriptRuntimeStore?: WorkspaceScriptRuntimeStore;
workspaceSetupSnapshots?: Map<string, WorkspaceSetupSnapshot>;
@@ -865,6 +867,7 @@ export class Session {
} | null = null;
private readonly terminalManager: TerminalManager | null;
private readonly providerSnapshotManager: ProviderSnapshotManager;
private readonly providerUsageService: ProviderUsageService;
private unsubscribeProviderSnapshotEvents: (() => void) | null = null;
private readonly serviceProxy: ServiceProxySubsystem | null;
private readonly scriptRuntimeStore: WorkspaceScriptRuntimeStore | null;
@@ -934,6 +937,7 @@ export class Session {
tts,
terminalManager,
providerSnapshotManager,
providerUsageService,
serviceProxy,
scriptRuntimeStore,
workspaceSetupSnapshots,
@@ -1027,6 +1031,7 @@ export class Session {
logger: this.sessionLogger,
});
this.providerSnapshotManager = providerSnapshotManager;
this.providerUsageService = providerUsageService;
this.serviceProxy = serviceProxy ?? null;
this.scriptRuntimeStore = scriptRuntimeStore ?? null;
this.workspaceSetupSnapshots = workspaceSetupSnapshots ?? new Map();
@@ -2282,6 +2287,8 @@ export class Session {
return this.handleRefreshProvidersSnapshotRequest(msg);
case "provider_diagnostic_request":
return this.handleProviderDiagnosticRequest(msg);
case "provider.usage.list.request":
return this.handleProviderUsageListRequest(msg);
default:
return undefined;
}
@@ -4394,6 +4401,34 @@ export class Session {
}
}
private async handleProviderUsageListRequest(
msg: Extract<SessionInboundMessage, { type: "provider.usage.list.request" }>,
): Promise<void> {
try {
const usage = await this.providerUsageService.listUsage();
this.emit({
type: "provider.usage.list.response",
payload: {
requestId: msg.requestId,
fetchedAt: usage.fetchedAt,
providers: usage.providers,
},
});
} catch (error) {
const err = error instanceof Error ? error : new Error(String(error));
this.sessionLogger.error({ err }, "Failed to list provider usage");
this.emit({
type: "rpc_error",
payload: {
requestId: msg.requestId,
requestType: msg.type,
error: `Failed to list provider usage: ${err.message}`,
code: "provider_usage_list_failed",
},
});
}
}
private assertSafeGitRef(ref: string, label: string): void {
if (!/^[A-Za-z0-9._/-]+$/.test(ref)) {
throw new Error(`Invalid ${label}: ${ref}`);
@@ -76,6 +76,7 @@ class RecordingPushNotificationSender implements PushNotificationSender {
function createServer(agentManagerOverrides?: Record<string, unknown>) {
const pushNotifications = new RecordingPushNotificationSender();
const agentManager = {
subscribe: vi.fn(() => () => {}),
setAgentAttentionCallback: vi.fn(),
getAgent: vi.fn(() => null),
getLastAssistantMessage: vi.fn(async () => null),
@@ -125,6 +125,30 @@ function parseSentEnvelope(data: unknown): z.infer<typeof WireEnvelopeSchema> {
return WireEnvelopeSchema.parse(JSON.parse(data));
}
function sentEnvelopes(socket: MockSocket): z.infer<typeof WireEnvelopeSchema>[] {
return socket.sent.filter((data) => typeof data === "string").map(parseSentEnvelope);
}
function sentServerInfoEnvelopes(socket: MockSocket): z.infer<typeof WireEnvelopeSchema>[] {
return sentEnvelopes(socket).filter(
(envelope) => parseServerInfoStatusPayload(envelope.message?.payload) !== null,
);
}
function sentBinaryFrames(socket: MockSocket): Uint8Array[] {
return socket.sent.map(asUint8Array).filter((frame): frame is Uint8Array => frame !== null);
}
function sentTerminalFrames(
socket: MockSocket,
): NonNullable<ReturnType<typeof decodeTerminalStreamFrame>>[] {
return sentBinaryFrames(socket)
.map(decodeTerminalStreamFrame)
.filter(
(frame): frame is NonNullable<ReturnType<typeof decodeTerminalStreamFrame>> => frame !== null,
);
}
const BinaryFrameSchema = z.object({
kind: z.literal("terminal"),
frame: z.object({
@@ -201,6 +225,7 @@ function createServer(options?: { speechReadiness?: SpeechReadinessSnapshot | nu
createStub<pino.Logger>(createLogger()),
"srv_test",
createStub<AgentManager>({
subscribe: vi.fn(() => () => {}),
setAgentAttentionCallback: vi.fn(),
getAgent: vi.fn(() => null),
getMetricsSnapshot: vi.fn(() => ({
@@ -705,20 +730,20 @@ describe("relay external socket reconnect behavior", () => {
socket,
clientId: "cid-server-info-broadcast",
});
expect(socket.sent).toHaveLength(1);
expect(sentServerInfoEnvelopes(socket)).toHaveLength(1);
const speechReadiness = createReadySpeechReadinessSnapshot();
server.publishSpeechReadiness(speechReadiness);
expect(socket.sent).toHaveLength(2);
expect(sentServerInfoEnvelopes(socket)).toHaveLength(2);
const secondEnvelope = parseSentEnvelope(socket.sent[1]);
const secondEnvelope = sentServerInfoEnvelopes(socket)[1];
const secondPayload = parseServerInfoStatusPayload(secondEnvelope.message?.payload);
expect(secondPayload?.capabilities?.voice?.dictation.enabled).toBe(true);
expect(secondPayload?.capabilities?.voice?.voice.enabled).toBe(true);
// Same readiness should not produce another server_info broadcast.
server.publishSpeechReadiness(speechReadiness);
expect(socket.sent).toHaveLength(2);
expect(sentServerInfoEnvelopes(socket)).toHaveLength(2);
await server.close();
});
@@ -731,12 +756,12 @@ describe("relay external socket reconnect behavior", () => {
socket,
clientId: "cid-server-info-download-guidance",
});
expect(socket.sent).toHaveLength(1);
expect(sentServerInfoEnvelopes(socket)).toHaveLength(1);
server.publishSpeechReadiness(createDownloadInProgressSpeechReadinessSnapshot());
expect(socket.sent).toHaveLength(2);
expect(sentServerInfoEnvelopes(socket)).toHaveLength(2);
const envelope = parseSentEnvelope(socket.sent[1]);
const envelope = sentServerInfoEnvelopes(socket)[1];
const payload = parseServerInfoStatusPayload(envelope.message?.payload);
expect(payload?.capabilities?.voice?.dictation.enabled).toBe(true);
expect(payload?.capabilities?.voice?.voice.enabled).toBe(true);
@@ -797,14 +822,12 @@ describe("relay external socket reconnect behavior", () => {
onBinaryMessage(new Uint8Array([TerminalStreamOpcode.Output, 12, 0x6f, 0x6b]));
}
expect(socket.sent).toHaveLength(2);
const binaryPayload = asUint8Array(socket.sent[1]);
expect(binaryPayload).not.toBeNull();
const frame = decodeTerminalStreamFrame(binaryPayload!);
expect(frame).not.toBeNull();
expect(frame!.opcode).toBe(TerminalStreamOpcode.Output);
expect(frame!.slot).toBe(12);
expect(new TextDecoder().decode(frame!.payload ?? new Uint8Array())).toBe("ok");
const terminalFrames = sentTerminalFrames(socket);
expect(terminalFrames).toHaveLength(1);
const frame = terminalFrames[0];
expect(frame.opcode).toBe(TerminalStreamOpcode.Output);
expect(frame.slot).toBe(12);
expect(new TextDecoder().decode(frame.payload ?? new Uint8Array())).toBe("ok");
await server.close();
});
@@ -111,6 +111,7 @@ function createServer(terminalManager: TerminalManager, workspaceRegistry?: Work
const pushNotifications = new RecordingPushNotificationSender();
const agentManager = {
setAgentAttentionCallback: vi.fn(),
subscribe: vi.fn(() => () => {}),
getAgent: vi.fn(() => null),
getLastAssistantMessage: vi.fn(async () => null),
getMetricsSnapshot: vi.fn(() => ({
@@ -200,22 +201,40 @@ function connectClient(server: VoiceAssistantWebSocketServer) {
return ws;
}
function sentTerminalAttentionMessages(ws: ReturnType<typeof createOpenSocket>) {
return ws.send.mock.calls
.map(([rawMessage]) => {
expect(typeof rawMessage).toBe("string");
if (typeof rawMessage !== "string") throw new Error("Expected string WebSocket frame");
return JSON.parse(rawMessage);
})
.filter(
(message) =>
message.type === "session" && message.message.type === "terminal_attention_required",
)
.map((message) => message.message.payload as TerminalAttentionPayload);
}
interface TerminalAttentionPayload {
terminalId: string;
cwd: string;
workspaceId?: string;
shouldNotify: boolean;
reason: "finished" | "needs_input";
title: string;
body: string;
}
function readTerminalAttentionMessage(ws: ReturnType<typeof createOpenSocket>) {
const rawMessage = ws.send.mock.calls[0]?.[0];
expect(typeof rawMessage).toBe("string");
if (typeof rawMessage !== "string") throw new Error("Expected string WebSocket frame");
const message = JSON.parse(rawMessage);
expect(message.type).toBe("session");
expect(message.message.type).toBe("terminal_attention_required");
return message.message.payload as {
terminalId: string;
cwd: string;
workspaceId?: string;
shouldNotify: boolean;
reason: "finished" | "needs_input";
title: string;
body: string;
};
const terminalMessages = sentTerminalAttentionMessages(ws);
expect(terminalMessages).toHaveLength(1);
const [payload] = terminalMessages;
if (!payload) throw new Error("Expected terminal attention message");
return payload;
}
function expectNoTerminalAttentionMessage(ws: ReturnType<typeof createOpenSocket>) {
expect(sentTerminalAttentionMessages(ws)).toHaveLength(0);
}
const CWD = "/home/user/project";
@@ -357,7 +376,7 @@ describe("VoiceAssistantWebSocketServer terminal attention notifications", () =>
await flushAsync();
expect(ws.send).not.toHaveBeenCalled();
expectNoTerminalAttentionMessage(ws);
});
it("does not broadcast on working -> unknown", async () => {
@@ -376,7 +395,7 @@ describe("VoiceAssistantWebSocketServer terminal attention notifications", () =>
await flushAsync();
expect(ws.send).not.toHaveBeenCalled();
expectNoTerminalAttentionMessage(ws);
});
it("broadcasts needs_input after working -> attention", async () => {
@@ -61,6 +61,7 @@ import {
WebSocketRuntimeMetricsWindow,
type WebSocketRuntimeCounters,
} from "./websocket/runtime-metrics.js";
import { ProviderUsageService } from "../services/quota-fetcher/service.js";
const WS_CLOSE_DAEMON_AUTH_FAILED = 4401;
@@ -407,6 +408,7 @@ export class VoiceAssistantWebSocketServer {
private eventLoopDelayMonitor: ReturnType<typeof monitorEventLoopDelay> | null = null;
private unsubscribeSpeechReadiness: (() => void) | null = null;
private unsubscribeDaemonConfigChange: (() => void) | null = null;
private readonly providerUsageService: ProviderUsageService;
private unsubscribeTerminalActivity: (() => void) | null = null;
constructor(
@@ -533,6 +535,10 @@ export class VoiceAssistantWebSocketServer {
});
});
this.providerUsageService = new ProviderUsageService({
logger: this.logger,
});
this.wss = this.createWebSocketServer(server, wsConfig, auth);
this.startRuntimeMetricsInterval();
@@ -977,6 +983,7 @@ export class VoiceAssistantWebSocketServer {
tts: () => this.speech?.resolveTts() ?? null,
terminalManager: this.terminalManager,
providerSnapshotManager: this.providerSnapshotManager,
providerUsageService: this.providerUsageService,
serviceProxy: this.serviceProxy ?? undefined,
scriptRuntimeStore: this.scriptRuntimeStore ?? undefined,
workspaceSetupSnapshots: this.workspaceSetupSnapshots,
@@ -1161,6 +1168,8 @@ export class VoiceAssistantWebSocketServer {
projectRemove: true,
// COMPAT(worktreeRestore): added in v0.1.97, drop the gate when floor >= v0.1.97
worktreeRestore: true,
// COMPAT(providerUsageList): added in v0.1.98, drop the gate when daemon floor >= v0.1.98.
providerUsageList: true,
// COMPAT(agentDetach): added in v0.1.98, remove gate after 2026-12-19 once daemon floor >= v0.1.98.
agentDetach: true,
},
@@ -0,0 +1,57 @@
import type {
ProviderUsageFetcher,
ProviderUsageFetcherFactoryOptions,
ProviderUsageFetcherManifestEntry,
} from "./provider.js";
import { ClaudeQuotaProvider } from "./providers/claude.js";
import { CodexQuotaProvider } from "./providers/codex.js";
import { CopilotQuotaProvider } from "./providers/copilot.js";
import { CursorQuotaProvider } from "./providers/cursor.js";
import { GrokQuotaProvider } from "./providers/grok.js";
import { KimiQuotaProvider } from "./providers/kimi.js";
import { ZaiQuotaProvider } from "./providers/zai.js";
export const PROVIDER_USAGE_FETCHERS: readonly ProviderUsageFetcherManifestEntry[] = [
{
providerId: "claude",
create: (options) =>
new ClaudeQuotaProvider({
logger: options.logger,
fetch: options.fetch,
}),
},
{
providerId: "codex",
create: (options) =>
new CodexQuotaProvider({
logger: options.logger,
fetch: options.fetch,
}),
},
{
providerId: "copilot",
create: (options) => new CopilotQuotaProvider({ logger: options.logger, fetch: options.fetch }),
},
{
providerId: "cursor",
create: (options) => new CursorQuotaProvider({ logger: options.logger, fetch: options.fetch }),
},
{
providerId: "zai",
create: (options) => new ZaiQuotaProvider({ logger: options.logger, fetch: options.fetch }),
},
{
providerId: "grok",
create: (options) => new GrokQuotaProvider({ logger: options.logger, fetch: options.fetch }),
},
{
providerId: "kimi",
create: (options) => new KimiQuotaProvider({ logger: options.logger, fetch: options.fetch }),
},
];
export function createProviderUsageFetchers(
options: ProviderUsageFetcherFactoryOptions,
): ProviderUsageFetcher[] {
return PROVIDER_USAGE_FETCHERS.map((entry) => entry.create(options));
}
@@ -0,0 +1,20 @@
import type { Logger } from "pino";
import type { ProviderUsage } from "../../server/messages.js";
export type ProviderApiFetch = typeof fetch;
export interface ProviderUsageFetcher {
readonly providerId: string;
readonly displayName: string;
fetchUsage(): Promise<ProviderUsage>;
}
export interface ProviderUsageFetcherFactoryOptions {
logger: Logger;
fetch?: ProviderApiFetch;
}
export interface ProviderUsageFetcherManifestEntry {
readonly providerId: string;
create(options: ProviderUsageFetcherFactoryOptions): ProviderUsageFetcher;
}
@@ -0,0 +1,293 @@
import { execFile } from "node:child_process";
import { existsSync, promises as fs } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { promisify } from "node:util";
import type { Logger } from "pino";
import { z } from "zod";
import type {
ProviderUsage,
ProviderUsageDetail,
ProviderUsageWindow,
} from "../../../server/messages.js";
import type { ProviderApiFetch, ProviderUsageFetcher } from "../provider.js";
import {
ApiNumberSchema,
fetchProviderApi,
unavailableUsage,
windowFromUsedPct,
} from "../usage.js";
const execFileAsync = promisify(execFile);
const CLAUDE_KEYCHAIN_TIMEOUT_MS = 2_000;
const CLAUDE_OAUTH_BETA = "oauth-2025-04-20";
const CLAUDE_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
const CLAUDE_KEYCHAIN_SERVICE = "Claude Code-credentials";
const ClaudeCredentialsSchema = z.object({
claudeAiOauth: z
.object({
accessToken: z.string().optional(),
refreshToken: z.string().optional(),
subscriptionType: z.string().optional(),
rateLimitTier: z.string().optional(),
})
.optional(),
});
const ClaudeUsageWindowSchema = z.object({
utilization: ApiNumberSchema,
resets_at: z.string().optional(),
});
const ClaudeUsageResponseSchema = z.object({
five_hour: ClaudeUsageWindowSchema.nullish(),
seven_day: ClaudeUsageWindowSchema.nullish(),
seven_day_opus: ClaudeUsageWindowSchema.nullish(),
seven_day_omelette: ClaudeUsageWindowSchema.nullish(),
extra_usage: z
.object({
is_enabled: z.boolean().optional(),
})
.nullish(),
});
const ClaudeTokenRefreshSchema = z.object({
access_token: z.string().optional(),
refresh_token: z.string().optional(),
});
type ClaudeCredentials = z.infer<typeof ClaudeCredentialsSchema>;
type ClaudeUsageResponse = z.infer<typeof ClaudeUsageResponseSchema>;
type ClaudeTokenRefresh = z.infer<typeof ClaudeTokenRefreshSchema>;
interface ClaudeCredentialRecord {
oauth: { accessToken: string } & NonNullable<ClaudeCredentials["claudeAiOauth"]>;
filePath: string | null;
}
interface ClaudeQuotaProviderOptions {
logger: Logger;
claudeHome?: string;
claudeKeychainReader?: () => Promise<unknown | null>;
platform?: typeof process.platform;
fetch?: ProviderApiFetch;
}
function buildClaudePlan(
subscriptionType: string | undefined,
rateLimitTier: string | undefined,
): string | null {
if (!subscriptionType) return null;
const label = subscriptionType.charAt(0).toUpperCase() + subscriptionType.slice(1);
const tier = rateLimitTier?.split("_").pop();
return tier ? `${label} ${tier}` : label;
}
async function readClaudeKeychainCredentials(): Promise<unknown | null> {
try {
const { stdout } = await execFileAsync(
"security",
["find-generic-password", "-s", CLAUDE_KEYCHAIN_SERVICE, "-w"],
{ timeout: CLAUDE_KEYCHAIN_TIMEOUT_MS },
);
const raw = stdout.trim();
if (!raw) return null;
return JSON.parse(raw);
} catch {
return null;
}
}
export class ClaudeQuotaProvider implements ProviderUsageFetcher {
readonly providerId = "claude";
readonly displayName = "Claude";
private readonly claudeHome: string;
private readonly readKeychainCredentials: () => Promise<unknown | null>;
private readonly platform: typeof process.platform;
private readonly fetchApi: ProviderApiFetch;
constructor(options: ClaudeQuotaProviderOptions) {
this.claudeHome =
options.claudeHome || process.env["CLAUDE_HOME"] || join(homedir(), ".claude");
this.readKeychainCredentials = options.claudeKeychainReader ?? readClaudeKeychainCredentials;
this.platform = options.platform ?? process.platform;
this.fetchApi = options.fetch ?? fetch;
}
async fetchUsage(): Promise<ProviderUsage> {
const credentials = await this.readCredentials();
if (!credentials) {
return unavailableUsage(this);
}
const { oauth, filePath } = credentials;
const plan = buildClaudePlan(oauth.subscriptionType, oauth.rateLimitTier);
let resp = await this.callClaudeApi(oauth.accessToken);
if (resp === "NEEDS_AUTH") {
if (!filePath || !oauth.refreshToken) {
return unavailableUsage(this);
}
const refreshed = await this.refreshClaudeToken(oauth.refreshToken);
if (!refreshed?.access_token) {
return unavailableUsage(this);
}
await this.saveClaudeCredentials(filePath, {
...oauth,
accessToken: refreshed.access_token,
refreshToken: refreshed.refresh_token ?? oauth.refreshToken,
});
resp = await this.callClaudeApi(refreshed.access_token);
if (resp === "NEEDS_AUTH") {
return unavailableUsage(this);
}
}
const windows: ProviderUsageWindow[] = [];
if (resp.five_hour) {
windows.push(
windowFromUsedPct({
id: "five_hour",
label: "Session",
utilizationPct: resp.five_hour.utilization,
resetsAt: resp.five_hour.resets_at ?? null,
tone: "ok",
}),
);
}
if (resp.seven_day) {
windows.push(
windowFromUsedPct({
id: "weekly",
label: "Weekly",
utilizationPct: resp.seven_day.utilization,
resetsAt: resp.seven_day.resets_at ?? null,
tone: "ok",
}),
);
}
if (resp.seven_day_opus) {
windows.push(
windowFromUsedPct({
id: "weekly_opus",
label: "Weekly · Opus",
utilizationPct: resp.seven_day_opus.utilization,
resetsAt: resp.seven_day_opus.resets_at ?? null,
tone: "ok",
}),
);
}
if (resp.seven_day_omelette) {
windows.push(
windowFromUsedPct({
id: "weekly_omelette",
label: "Weekly · Omelette",
utilizationPct: resp.seven_day_omelette.utilization,
resetsAt: resp.seven_day_omelette.resets_at ?? null,
tone: "ok",
}),
);
}
const details: ProviderUsageDetail[] = [];
const extraUsageEnabled = resp.extra_usage?.is_enabled;
if (extraUsageEnabled !== undefined) {
details.push({
id: "extra_usage",
label: "Extra usage",
value: extraUsageEnabled ? "Enabled" : "Disabled",
});
}
return {
providerId: this.providerId,
displayName: this.displayName,
status: "available",
planLabel: plan,
windows,
balances: [],
details,
error: null,
};
}
private async readCredentials(): Promise<ClaudeCredentialRecord | null> {
const credPath = join(this.claudeHome, ".credentials.json");
if (existsSync(credPath)) {
try {
const creds = ClaudeCredentialsSchema.parse(
JSON.parse(await fs.readFile(credPath, "utf8")),
);
const oauth = creds.claudeAiOauth;
if (oauth?.accessToken) {
return { oauth: { ...oauth, accessToken: oauth.accessToken }, filePath: credPath };
}
} catch {
// Fall through to the macOS Keychain below.
}
}
if (this.platform === "darwin") {
const creds = ClaudeCredentialsSchema.safeParse(await this.readKeychainCredentials());
const oauth = creds.success ? creds.data.claudeAiOauth : undefined;
if (oauth?.accessToken) {
return { oauth: { ...oauth, accessToken: oauth.accessToken }, filePath: null };
}
}
return null;
}
private async callClaudeApi(token: string): Promise<ClaudeUsageResponse | "NEEDS_AUTH"> {
const res = await fetchProviderApi(this.fetchApi, "https://api.anthropic.com/api/oauth/usage", {
headers: {
Authorization: `Bearer ${token}`,
Accept: "application/json",
"anthropic-beta": CLAUDE_OAUTH_BETA,
},
});
if (res.status === 401 || res.status === 403) return "NEEDS_AUTH";
if (!res.ok) throw new Error(`Claude usage API returned ${res.status}`);
return ClaudeUsageResponseSchema.parse(await res.json());
}
private async refreshClaudeToken(refreshToken: string): Promise<ClaudeTokenRefresh | null> {
const res = await fetchProviderApi(
this.fetchApi,
"https://platform.claude.com/v1/oauth/token",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
grant_type: "refresh_token",
refresh_token: refreshToken,
client_id: CLAUDE_CLIENT_ID,
scope: "user:profile user:inference user:sessions:claude_code user:mcp_servers",
}),
},
);
if (!res.ok) return null;
return ClaudeTokenRefreshSchema.parse(await res.json());
}
private async saveClaudeCredentials(
credPath: string,
oauth: ClaudeCredentials["claudeAiOauth"],
): Promise<void> {
try {
const existing = ClaudeCredentialsSchema.parse(
JSON.parse(await fs.readFile(credPath, "utf8")),
);
existing.claudeAiOauth = oauth;
await fs.writeFile(credPath, JSON.stringify(existing, null, 2), { mode: 0o600 });
} catch {
// Non-fatal; Claude Code can refresh again on its own next time.
}
}
}
@@ -0,0 +1,273 @@
import { existsSync, promises as fs } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import type { Logger } from "pino";
import { z } from "zod";
import type {
ProviderUsage,
ProviderUsageBalance,
ProviderUsageWindow,
} from "../../../server/messages.js";
import type { ProviderApiFetch, ProviderUsageFetcher } from "../provider.js";
import {
ApiNumberSchema,
balanceToneFromRemaining,
fetchProviderApi,
unavailableUsage,
windowFromUsedPct,
} from "../usage.js";
const CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
const CodexAuthSchema = z.object({
tokens: z
.object({
access_token: z.string().optional(),
refresh_token: z.string().optional(),
account_id: z.string().optional(),
})
.optional(),
});
const CodexWindowSchema = z.object({
used_percent: ApiNumberSchema.optional(),
reset_at: ApiNumberSchema.optional(),
});
const CodexUsageResponseSchema = z.object({
plan_type: z.string().optional(),
email: z.string().optional(),
rate_limit: z
.object({
primary_window: CodexWindowSchema.nullish(),
secondary_window: CodexWindowSchema.nullish(),
})
.nullish(),
code_review_rate_limit: z
.object({
primary_window: CodexWindowSchema.nullish(),
})
.nullish(),
credits: z
.object({
has_credits: z.boolean().optional(),
unlimited: z.boolean().optional(),
balance: ApiNumberSchema.optional(),
})
.nullish(),
});
const CodexTokenRefreshSchema = z.object({
access_token: z.string().optional(),
refresh_token: z.string().optional(),
});
type CodexAuth = z.infer<typeof CodexAuthSchema>;
type CodexWindow = z.infer<typeof CodexWindowSchema>;
type CodexUsageResponse = z.infer<typeof CodexUsageResponseSchema>;
type CodexTokenRefresh = z.infer<typeof CodexTokenRefreshSchema>;
interface CodexAuthRecord {
auth: CodexAuth;
path: string;
}
interface CodexQuotaProviderOptions {
logger: Logger;
codexHome?: string;
fetch?: ProviderApiFetch;
}
function codexWindow(
window: CodexWindow | null | undefined,
): { usedPct: number; resetsAt: string | null } | null {
if (!window) return null;
return {
usedPct: window.used_percent ?? 0,
resetsAt: window.reset_at != null ? new Date(window.reset_at * 1000).toISOString() : null,
};
}
export class CodexQuotaProvider implements ProviderUsageFetcher {
readonly providerId = "codex";
readonly displayName = "Codex";
private readonly codexHome: string;
private readonly fetchApi: ProviderApiFetch;
constructor(options: CodexQuotaProviderOptions) {
this.codexHome = options.codexHome || process.env["CODEX_HOME"] || join(homedir(), ".codex");
this.fetchApi = options.fetch ?? fetch;
}
async fetchUsage(): Promise<ProviderUsage> {
const authRecord = await this.readCodexAuth();
const auth = authRecord?.auth;
const accessToken = auth?.tokens?.access_token;
if (!authRecord || !auth || !accessToken) {
return unavailableUsage(this);
}
const { refresh_token, account_id } = auth.tokens ?? {};
let resp = await this.callCodexApi(accessToken, account_id);
if (resp === "NEEDS_AUTH") {
if (!refresh_token) {
return unavailableUsage(this);
}
const refreshed = await this.refreshCodexToken(refresh_token);
if (!refreshed?.access_token) {
return unavailableUsage(this);
}
await this.saveCodexAuth(authRecord.path, auth, refreshed);
resp = await this.callCodexApi(refreshed.access_token, account_id);
if (resp === "NEEDS_AUTH") {
return unavailableUsage(this);
}
}
return this.toUsage(resp);
}
private toUsage(resp: CodexUsageResponse): ProviderUsage {
const session = codexWindow(resp.rate_limit?.primary_window);
const weekly = codexWindow(resp.rate_limit?.secondary_window);
const codeReview = codexWindow(resp.code_review_rate_limit?.primary_window);
const windows: ProviderUsageWindow[] = [];
if (session) {
windows.push(
windowFromUsedPct({
id: "session",
label: "Session",
utilizationPct: session.usedPct,
resetsAt: session.resetsAt,
tone: "ok",
}),
);
}
if (weekly) {
windows.push(
windowFromUsedPct({
id: "weekly",
label: "Weekly",
utilizationPct: weekly.usedPct,
resetsAt: weekly.resetsAt,
tone: weekly.usedPct >= 70 ? "warning" : "ok",
}),
);
}
if (codeReview) {
windows.push(
windowFromUsedPct({
id: "code_review",
label: "Code review",
utilizationPct: codeReview.usedPct,
resetsAt: codeReview.resetsAt,
tone: codeReview.usedPct >= 70 ? "warning" : "ok",
}),
);
}
const balances: ProviderUsageBalance[] = [];
if (resp.credits?.balance !== undefined) {
balances.push({
id: "credits",
label: "Credits",
remaining: resp.credits.balance,
unit: "usd",
tone: balanceToneFromRemaining(resp.credits.balance),
});
}
return {
providerId: this.providerId,
displayName: this.displayName,
status: "available",
planLabel: resp.plan_type ?? null,
windows,
balances,
details: [],
error: null,
};
}
private async readCodexAuth(): Promise<CodexAuthRecord | null> {
const candidates = [
...(process.env["CODEX_HOME"] ? [join(process.env["CODEX_HOME"], "auth.json")] : []),
join(homedir(), ".config", "codex", "auth.json"),
join(this.codexHome, "auth.json"),
];
for (const path of candidates) {
if (!existsSync(path)) continue;
try {
const auth = CodexAuthSchema.parse(JSON.parse(await fs.readFile(path, "utf8")));
if (auth.tokens?.access_token) return { auth, path };
} catch {
continue;
}
}
return null;
}
private async callCodexApi(
token: string,
accountId?: string,
): Promise<CodexUsageResponse | "NEEDS_AUTH"> {
const headers: Record<string, string> = {
Authorization: `Bearer ${token}`,
Accept: "application/json",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
};
if (accountId) headers["ChatGPT-Account-Id"] = accountId;
const res = await fetchProviderApi(
this.fetchApi,
"https://chatgpt.com/backend-api/wham/usage",
{
headers,
},
);
if (res.status === 401 || res.status === 403) return "NEEDS_AUTH";
if (!res.ok) throw new Error(`Codex usage API returned ${res.status}`);
const text = await res.text();
if (text.trim().startsWith("<")) return "NEEDS_AUTH";
return CodexUsageResponseSchema.parse(JSON.parse(text));
}
private async refreshCodexToken(refreshToken: string): Promise<CodexTokenRefresh | null> {
const params = new URLSearchParams({
grant_type: "refresh_token",
client_id: CODEX_CLIENT_ID,
refresh_token: refreshToken,
});
const res = await fetchProviderApi(this.fetchApi, "https://auth.openai.com/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: params.toString(),
});
if (!res.ok) return null;
return CodexTokenRefreshSchema.parse(await res.json());
}
private async saveCodexAuth(
authPath: string,
original: CodexAuth,
refreshed: CodexTokenRefresh,
): Promise<void> {
try {
const updated: CodexAuth = {
...original,
tokens: {
...original.tokens,
access_token: refreshed.access_token ?? original.tokens?.access_token,
refresh_token: refreshed.refresh_token ?? original.tokens?.refresh_token,
},
};
await fs.writeFile(authPath, JSON.stringify(updated, null, 2), { mode: 0o600 });
} catch {
// Non-fatal; the next call can refresh again.
}
}
}
@@ -0,0 +1,97 @@
import { existsSync, promises as fs } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import type { Logger } from "pino";
import { z } from "zod";
import type { ProviderUsage, ProviderUsageDetail } from "../../../server/messages.js";
import type { ProviderApiFetch, ProviderUsageFetcher } from "../provider.js";
import { ApiOptionalStringSchema, fetchProviderApi, unavailableUsage } from "../usage.js";
const CopilotUsageResponseSchema = z.object({
copilot_plan: ApiOptionalStringSchema,
quota_reset_date: ApiOptionalStringSchema,
});
interface CopilotQuotaProviderOptions {
logger: Logger;
fetch?: ProviderApiFetch;
}
async function readGithubCliToken(): Promise<string | null> {
const candidates: string[] = [];
if (process.env["APPDATA"]) {
candidates.push(join(process.env["APPDATA"], "GitHub CLI", "hosts.yml"));
}
candidates.push(join(homedir(), ".config", "gh", "hosts.yml"));
for (const path of candidates) {
if (!existsSync(path)) continue;
try {
const raw = await fs.readFile(path, "utf8");
const match = raw.match(/oauth_token:\s*["']?([a-zA-Z0-9_-]+)["']?/);
if (match?.[1]) return match[1];
} catch {
continue;
}
}
return null;
}
export class CopilotQuotaProvider implements ProviderUsageFetcher {
readonly providerId = "copilot";
readonly displayName = "GitHub Copilot";
private readonly logger: Logger;
private readonly fetchApi: ProviderApiFetch;
constructor(options: CopilotQuotaProviderOptions) {
this.logger = options.logger;
this.fetchApi = options.fetch ?? fetch;
}
async fetchUsage(): Promise<ProviderUsage> {
const token =
process.env["COPILOT_TOKEN"] ||
process.env["GITHUB_TOKEN"] ||
process.env["GITHUB_PAT"] ||
(await readGithubCliToken());
if (!token) return unavailableUsage(this);
const res = await fetchProviderApi(
this.fetchApi,
"https://api.github.com/copilot_internal/user",
{
headers: {
Authorization: `token ${token}`,
Accept: "application/json",
"Editor-Version": "vscode/1.96.2",
"Editor-Plugin-Version": "copilot-chat/0.26.7",
"User-Agent": "GitHubCopilotChat/0.26.7",
"X-Github-Api-Version": "2025-04-01",
},
},
);
if (!res.ok) {
this.logger.debug({ status: res.status }, "Copilot usage fetch failed");
return unavailableUsage(this);
}
const resp = CopilotUsageResponseSchema.parse(await res.json());
const details: ProviderUsageDetail[] = resp.quota_reset_date
? [{ id: "reset", label: "Quota reset", value: resp.quota_reset_date }]
: [];
return {
providerId: this.providerId,
displayName: this.displayName,
status: "available",
planLabel: resp.copilot_plan || null,
windows: [],
balances: [],
details,
error: null,
};
}
}
@@ -0,0 +1,178 @@
import { execFile } from "node:child_process";
import { existsSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { promisify } from "node:util";
import type { Logger } from "pino";
import { z } from "zod";
import type { ProviderUsage, ProviderUsageBalance } from "../../../server/messages.js";
import type { ProviderApiFetch, ProviderUsageFetcher } from "../provider.js";
import {
ApiNullableNumberSchema,
balanceToneFromRemaining,
fetchProviderApi,
toIsoStringOrNull,
unavailableUsage,
} from "../usage.js";
const execFileAsync = promisify(execFile);
const CURSOR_SQLITE_TIMEOUT_MS = 2_000;
const CursorBillingCycleTimestampSchema = z.preprocess(
(value) => (typeof value === "string" || typeof value === "number" ? value : null),
z.union([z.string(), z.number()]).nullable(),
);
const CursorUsageResponseSchema = z.object({
planUsage: z
.object({
totalSpend: ApiNullableNumberSchema,
includedSpend: ApiNullableNumberSchema,
bonusSpend: ApiNullableNumberSchema,
remaining: ApiNullableNumberSchema,
limit: ApiNullableNumberSchema,
})
.nullish(),
billingCycleStart: CursorBillingCycleTimestampSchema,
billingCycleEnd: CursorBillingCycleTimestampSchema,
});
const CursorAuthStatusSchema = z.object({
accessToken: z.string().optional(),
});
type CursorUsageResponse = z.infer<typeof CursorUsageResponseSchema>;
interface CursorQuotaProviderOptions {
logger: Logger;
fetch?: ProviderApiFetch;
}
function parseCursorBillingCycleTimestamp(
value: CursorUsageResponse["billingCycleStart"],
): string | null {
if (value === null) return null;
const raw = String(value).trim();
if (!raw) return null;
const numeric = Number(raw);
if (Number.isFinite(numeric)) {
const timestampMs = Math.abs(numeric) < 10_000_000_000 ? numeric * 1000 : numeric;
return toIsoStringOrNull(timestampMs);
}
return toIsoStringOrNull(new Date(raw).getTime());
}
function centsToDollars(value: number | null): number | null {
return value === null ? null : value / 100;
}
async function readCursorTokenFromSqlite(): Promise<string | null> {
const dbPaths: string[] = [];
if (process.env["APPDATA"]) {
dbPaths.push(join(process.env["APPDATA"], "Cursor", "User", "globalStorage", "state.vscdb"));
}
dbPaths.push(
join(
homedir(),
"Library",
"Application Support",
"Cursor",
"User",
"globalStorage",
"state.vscdb",
),
);
dbPaths.push(join(homedir(), ".config", "Cursor", "User", "globalStorage", "state.vscdb"));
for (const path of dbPaths) {
if (!existsSync(path)) continue;
try {
const { stdout } = await execFileAsync(
"sqlite3",
[path, "SELECT value FROM ItemTable WHERE key = 'cursorAuthStatus'"],
{ timeout: CURSOR_SQLITE_TIMEOUT_MS },
);
if (stdout) {
const parsed = CursorAuthStatusSchema.parse(JSON.parse(stdout.trim()));
if (parsed.accessToken) return parsed.accessToken;
}
} catch {
continue;
}
}
return null;
}
export class CursorQuotaProvider implements ProviderUsageFetcher {
readonly providerId = "cursor";
readonly displayName = "Cursor";
private readonly logger: Logger;
private readonly fetchApi: ProviderApiFetch;
constructor(options: CursorQuotaProviderOptions) {
this.logger = options.logger;
this.fetchApi = options.fetch ?? fetch;
}
async fetchUsage(): Promise<ProviderUsage> {
const token =
process.env["CURSOR_ACCESS_TOKEN"] ||
process.env["CURSOR_TOKEN"] ||
(await readCursorTokenFromSqlite());
if (!token) return unavailableUsage(this);
const res = await fetchProviderApi(
this.fetchApi,
"https://api2.cursor.sh/aiserver.v1.DashboardService/GetCurrentPeriodUsage",
{
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
"Connect-Protocol-Version": "1",
},
body: JSON.stringify({}),
},
);
if (!res.ok) {
this.logger.debug({ status: res.status }, "Cursor usage fetch failed");
return unavailableUsage(this);
}
const resp = CursorUsageResponseSchema.parse(await res.json());
const billingCycleEnd = parseCursorBillingCycleTimestamp(resp.billingCycleEnd);
const balances: ProviderUsageBalance[] = [];
if (resp.planUsage) {
const totalSpend = centsToDollars(resp.planUsage.totalSpend);
const remaining = centsToDollars(resp.planUsage.remaining);
const limit = centsToDollars(resp.planUsage.limit);
balances.push({
id: "plan_usage",
label: "Plan usage",
used: totalSpend,
remaining,
limit,
unit: "usd",
resetsAt: billingCycleEnd,
tone: balanceToneFromRemaining(remaining),
});
}
return {
providerId: this.providerId,
displayName: this.displayName,
status: "available",
planLabel: null,
windows: [],
balances,
details: [],
error: null,
};
}
}
@@ -0,0 +1,118 @@
import { existsSync, promises as fs } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import type { Logger } from "pino";
import { z } from "zod";
import type { ProviderUsage, ProviderUsageBalance } from "../../../server/messages.js";
import type { ProviderApiFetch, ProviderUsageFetcher } from "../provider.js";
import {
ApiNumberSchema,
balanceToneFromRemaining,
fetchProviderApi,
unavailableUsage,
} from "../usage.js";
const GrokUsageResponseSchema = z.object({
config: z
.object({
monthlyLimit: z
.object({
val: ApiNumberSchema.optional(),
})
.nullish(),
})
.nullish(),
usage: z
.object({
creditUsage: ApiNumberSchema.optional(),
})
.nullish(),
});
const GrokAuthSchema = z.object({
access_token: z.string().optional(),
});
interface GrokQuotaProviderOptions {
logger: Logger;
fetch?: ProviderApiFetch;
}
export class GrokQuotaProvider implements ProviderUsageFetcher {
readonly providerId = "grok";
readonly displayName = "Grok";
private readonly logger: Logger;
private readonly fetchApi: ProviderApiFetch;
constructor(options: GrokQuotaProviderOptions) {
this.logger = options.logger;
this.fetchApi = options.fetch ?? fetch;
}
async fetchUsage(): Promise<ProviderUsage> {
const token =
process.env["GROK_API_KEY"] || process.env["GROK_TOKEN"] || (await this.readGrokToken());
if (!token) return unavailableUsage(this);
const res = await fetchProviderApi(
this.fetchApi,
"https://cli-chat-proxy.grok.com/v1/billing",
{
headers: {
Authorization: `Bearer ${token}`,
"X-XAI-Token-Auth": "xai-grok-cli",
Accept: "application/json",
},
},
);
if (!res.ok) {
this.logger.debug({ status: res.status }, "Grok usage fetch failed");
return unavailableUsage(this);
}
const resp = GrokUsageResponseSchema.parse(await res.json());
const monthlyLimit = resp.config?.monthlyLimit?.val ?? null;
const creditUsage = resp.usage?.creditUsage ?? null;
const balances: ProviderUsageBalance[] = [];
if (monthlyLimit !== null || creditUsage !== null) {
const remaining =
monthlyLimit !== null && creditUsage !== null
? Math.max(0, monthlyLimit - creditUsage)
: null;
balances.push({
id: "monthly_credits",
label: "Monthly credits",
used: creditUsage,
remaining,
limit: monthlyLimit,
unit: "credits",
tone: balanceToneFromRemaining(remaining),
});
}
return {
providerId: this.providerId,
displayName: this.displayName,
status: "available",
planLabel: null,
windows: [],
balances,
details: [],
error: null,
};
}
private async readGrokToken(): Promise<string | null> {
const path = join(homedir(), ".grok", "auth.json");
if (!existsSync(path)) return null;
try {
const auth = GrokAuthSchema.parse(JSON.parse(await fs.readFile(path, "utf8")));
return auth.access_token ?? null;
} catch {
return null;
}
}
}
@@ -0,0 +1,100 @@
import { existsSync, promises as fs } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import type { Logger } from "pino";
import { z } from "zod";
import type { ProviderUsage } from "../../../server/messages.js";
import type { ProviderApiFetch, ProviderUsageFetcher } from "../provider.js";
import { ApiOptionalStringSchema, fetchProviderApi, unavailableUsage } from "../usage.js";
const KimiUsageResponseSchema = z.object({
usage: z
.object({
limit: ApiOptionalStringSchema,
remaining: ApiOptionalStringSchema,
resetTime: ApiOptionalStringSchema,
})
.nullish(),
});
const KimiAuthSchema = z.object({
access_token: z.string().optional(),
});
interface KimiQuotaProviderOptions {
logger: Logger;
fetch?: ProviderApiFetch;
}
export class KimiQuotaProvider implements ProviderUsageFetcher {
readonly providerId = "kimi";
readonly displayName = "Kimi";
private readonly logger: Logger;
private readonly fetchApi: ProviderApiFetch;
constructor(options: KimiQuotaProviderOptions) {
this.logger = options.logger;
this.fetchApi = options.fetch ?? fetch;
}
async fetchUsage(): Promise<ProviderUsage> {
const token =
process.env["KIMI_TOKEN"] || process.env["KIMI_API_KEY"] || (await this.readKimiToken());
if (!token) return unavailableUsage(this);
const res = await fetchProviderApi(this.fetchApi, "https://api.kimi.com/coding/v1/usages", {
headers: {
Authorization: `Bearer ${token}`,
Accept: "application/json",
},
});
if (!res.ok) {
this.logger.debug({ status: res.status }, "Kimi usage fetch failed");
return unavailableUsage(this);
}
const resp = KimiUsageResponseSchema.parse(await res.json());
const limit = resp.usage?.limit === undefined ? null : Number(resp.usage.limit);
const remaining = resp.usage?.remaining === undefined ? null : Number(resp.usage.remaining);
const hasFiniteLimit = typeof limit === "number" && Number.isFinite(limit) && limit > 0;
const hasFiniteRemaining = typeof remaining === "number" && Number.isFinite(remaining);
const usedPct =
hasFiniteLimit && hasFiniteRemaining
? Math.max(0, Math.min(100, ((limit - remaining) / limit) * 100))
: null;
return {
providerId: this.providerId,
displayName: this.displayName,
status: "available",
planLabel: null,
windows: [
{
id: "coding_usage",
label: "Coding usage",
usedPct,
remainingPct: usedPct === null ? null : Math.max(0, 100 - usedPct),
resetsAt: resp.usage?.resetTime ?? null,
tone: "ok",
},
],
balances: [],
details: [],
error: null,
};
}
private async readKimiToken(): Promise<string | null> {
const path = join(homedir(), ".kimi", "credentials", "kimi-code.json");
if (!existsSync(path)) return null;
try {
const credentials = KimiAuthSchema.parse(JSON.parse(await fs.readFile(path, "utf8")));
return credentials.access_token ?? null;
} catch {
return null;
}
}
}
@@ -0,0 +1,79 @@
import type { Logger } from "pino";
import { z } from "zod";
import type { ProviderUsage, ProviderUsageDetail } from "../../../server/messages.js";
import type { ProviderApiFetch, ProviderUsageFetcher } from "../provider.js";
import { ApiOptionalStringSchema, fetchProviderApi, unavailableUsage } from "../usage.js";
const ZaiUsageResponseSchema = z.object({
data: z
.array(
z.object({
productName: ApiOptionalStringSchema,
status: ApiOptionalStringSchema,
purchaseTime: ApiOptionalStringSchema,
valid: ApiOptionalStringSchema,
}),
)
.optional(),
});
interface ZaiQuotaProviderOptions {
logger: Logger;
fetch?: ProviderApiFetch;
}
export class ZaiQuotaProvider implements ProviderUsageFetcher {
readonly providerId = "zai";
readonly displayName = "Z.ai";
private readonly logger: Logger;
private readonly fetchApi: ProviderApiFetch;
constructor(options: ZaiQuotaProviderOptions) {
this.logger = options.logger;
this.fetchApi = options.fetch ?? fetch;
}
async fetchUsage(): Promise<ProviderUsage> {
const token = process.env["ZAI_API_KEY"] || process.env["GLM_API_KEY"];
if (!token) return unavailableUsage(this);
const res = await fetchProviderApi(
this.fetchApi,
"https://api.z.ai/api/biz/subscription/list",
{
headers: {
Authorization: `Bearer ${token}`,
Accept: "application/json",
},
},
);
if (!res.ok) {
this.logger.debug({ status: res.status }, "Z.ai usage fetch failed");
return unavailableUsage(this);
}
const resp = ZaiUsageResponseSchema.parse(await res.json());
const sub = resp.data?.[0];
if (!sub) return unavailableUsage(this);
const details: ProviderUsageDetail[] = [];
if (sub.status) details.push({ id: "status", label: "Status", value: sub.status });
if (sub.valid) details.push({ id: "valid", label: "Valid", value: sub.valid });
if (sub.purchaseTime) {
details.push({ id: "purchase_time", label: "Purchased", value: sub.purchaseTime });
}
return {
providerId: this.providerId,
displayName: this.displayName,
status: "available",
planLabel: sub.productName || null,
windows: [],
balances: [],
details,
error: null,
};
}
}
@@ -0,0 +1,697 @@
import { readFileSync, writeFileSync } from "node:fs";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { ProviderUsage } from "../../server/messages.js";
import type { ProviderUsageFetcher } from "./provider.js";
import { ClaudeQuotaProvider } from "./providers/claude.js";
import { CodexQuotaProvider } from "./providers/codex.js";
import { CopilotQuotaProvider } from "./providers/copilot.js";
import { CursorQuotaProvider } from "./providers/cursor.js";
import { GrokQuotaProvider } from "./providers/grok.js";
import { KimiQuotaProvider } from "./providers/kimi.js";
import { ZaiQuotaProvider } from "./providers/zai.js";
import { ProviderUsageService } from "./service.js";
function writeClaudeCredentials(
dir: string,
accessToken: string,
refreshToken = "rt_test",
subscriptionType = "pro",
rateLimitTier = "default_1x",
): void {
writeFileSync(
join(dir, ".credentials.json"),
JSON.stringify({
claudeAiOauth: { accessToken, refreshToken, subscriptionType, rateLimitTier },
}),
);
}
function writeCodexAuth(dir: string, accessToken: string, refreshToken = "rt_codex"): void {
writeFileSync(
join(dir, "auth.json"),
JSON.stringify({ tokens: { access_token: accessToken, refresh_token: refreshToken } }),
);
}
function makeClaudeResponse(
overrides: Partial<{
five_hour: { utilization: number | string; resets_at: string };
seven_day: { utilization: number | string; resets_at: string };
seven_day_opus: { utilization: number | string; resets_at: string };
}> = {},
) {
return {
five_hour: { utilization: 11, resets_at: "2026-06-01T21:00:00Z" },
seven_day: { utilization: 1, resets_at: "2026-06-04T00:00:00Z" },
seven_day_opus: { utilization: 0.5, resets_at: "2026-06-04T00:00:00Z" },
...overrides,
};
}
function makeCodexResponse(overrides: object = {}) {
return {
plan_type: "plus",
email: "user@example.com",
rate_limit: {
primary_window: { used_percent: 42, reset_at: 1_748_812_800 },
secondary_window: { used_percent: 8, reset_at: 1_749_072_000 },
},
...overrides,
};
}
function mockFetch(handlers: Map<string, () => Response>): typeof fetch {
return vi.fn(async (url: RequestInfo | URL) => {
const key = url.toString();
const handler = handlers.get(key);
if (!handler) throw new Error(`Unmocked fetch: ${key}`);
return handler();
}) as unknown as typeof fetch;
}
function jsonResponse(body: unknown, status = 200): Response {
return new Response(JSON.stringify(body), {
status,
headers: { "Content-Type": "application/json" },
});
}
function createLogger() {
const logger = {
debug: vi.fn(),
warn: vi.fn(),
info: vi.fn(),
error: vi.fn(),
child: () => logger,
};
return logger as never;
}
function usageFetcher(usage: ProviderUsage): ProviderUsageFetcher {
return {
providerId: usage.providerId,
displayName: usage.displayName,
fetchUsage: async () => usage,
};
}
function findProvider(result: { providers: ProviderUsage[] }, providerId: string): ProviderUsage {
const provider = result.providers.find((candidate) => candidate.providerId === providerId);
if (!provider) {
throw new Error(`Missing provider ${providerId}`);
}
return provider;
}
describe("ProviderUsageService", () => {
it("returns arbitrary registered providers and windows as normalized usage data", async () => {
const service = new ProviderUsageService({
logger: createLogger(),
now: () => Date.parse("2026-06-19T00:00:00.000Z"),
fetchers: [
usageFetcher({
providerId: "glm",
displayName: "GLM coding plan",
status: "available",
planLabel: "GLM coding plan",
windows: [
{
id: "biweekly",
label: "Biweekly",
usedPct: 23,
remainingPct: 77,
resetsAt: "2026-07-03T00:00:00.000Z",
},
],
}),
],
});
await expect(service.listUsage()).resolves.toEqual({
fetchedAt: "2026-06-19T00:00:00.000Z",
providers: [
{
providerId: "glm",
displayName: "GLM coding plan",
status: "available",
planLabel: "GLM coding plan",
windows: [
{
id: "biweekly",
label: "Biweekly",
usedPct: 23,
remainingPct: 77,
resetsAt: "2026-07-03T00:00:00.000Z",
},
],
},
],
});
});
it("caches usage until forced to refresh", async () => {
let now = Date.parse("2026-06-19T00:00:00.000Z");
let calls = 0;
const service = new ProviderUsageService({
logger: createLogger(),
now: () => now,
cacheTtlMs: 60_000,
fetchers: [
{
providerId: "claude",
displayName: "Claude",
fetchUsage: async () => {
calls += 1;
return {
providerId: "claude",
displayName: "Claude",
status: "available",
planLabel: "Max 20x",
windows: [{ id: "session", label: "Session", usedPct: calls }],
};
},
},
],
});
const first = await service.listUsage();
now += 30_000;
const cached = await service.listUsage();
const refreshed = await service.listUsage({ forceRefresh: true });
expect(calls).toBe(2);
expect(cached).toBe(first);
expect(refreshed.providers[0]?.windows[0]?.usedPct).toBe(2);
});
it("deduplicates concurrent cache misses", async () => {
let calls = 0;
let resolveUsage: ((usage: ProviderUsage) => void) | null = null;
const service = new ProviderUsageService({
logger: createLogger(),
now: () => Date.parse("2026-06-19T00:00:00.000Z"),
fetchers: [
{
providerId: "claude",
displayName: "Claude",
fetchUsage: () => {
calls += 1;
return new Promise<ProviderUsage>((resolve) => {
resolveUsage = resolve;
});
},
},
],
});
const first = service.listUsage();
const second = service.listUsage();
expect(calls).toBe(1);
resolveUsage?.({
providerId: "claude",
displayName: "Claude",
status: "available",
planLabel: "Max 20x",
windows: [{ id: "session", label: "Session", usedPct: 12 }],
});
const [firstResult, secondResult] = await Promise.all([first, second]);
expect(firstResult).toBe(secondResult);
expect(calls).toBe(1);
});
it("isolates one provider error without dropping other providers", async () => {
const service = new ProviderUsageService({
logger: createLogger(),
now: () => Date.parse("2026-06-19T00:00:00.000Z"),
fetchers: [
{
providerId: "claude",
displayName: "Claude",
fetchUsage: async () => {
throw new Error("Claude auth expired");
},
},
usageFetcher({
providerId: "codex",
displayName: "Codex",
status: "available",
planLabel: "Pro 20x",
windows: [{ id: "weekly", label: "Weekly", usedPct: 29 }],
}),
],
});
await expect(service.listUsage()).resolves.toEqual({
fetchedAt: "2026-06-19T00:00:00.000Z",
providers: [
{
providerId: "claude",
displayName: "Claude",
status: "error",
planLabel: null,
windows: [],
balances: [],
details: [],
error: "Claude auth expired",
},
{
providerId: "codex",
displayName: "Codex",
status: "available",
planLabel: "Pro 20x",
windows: [{ id: "weekly", label: "Weekly", usedPct: 29 }],
},
],
});
});
});
describe("real provider usage fetchers", () => {
let claudeHome: string;
let codexHome: string;
let homeDir: string;
let fetchApi: typeof fetch;
let originalEnv: Record<string, string | undefined>;
beforeEach(() => {
claudeHome = mkdtempSync(join(tmpdir(), "usage-test-claude-"));
codexHome = mkdtempSync(join(tmpdir(), "usage-test-codex-"));
homeDir = mkdtempSync(join(tmpdir(), "usage-test-home-"));
fetchApi = mockFetch(new Map());
originalEnv = { ...process.env };
process.env["HOME"] = homeDir;
for (const key of [
"APPDATA",
"COPILOT_TOKEN",
"GITHUB_TOKEN",
"GITHUB_PAT",
"CURSOR_ACCESS_TOKEN",
"CURSOR_TOKEN",
"ZAI_API_KEY",
"GLM_API_KEY",
"GROK_API_KEY",
"GROK_TOKEN",
"KIMI_TOKEN",
"KIMI_API_KEY",
"CODEX_HOME",
]) {
delete process.env[key];
}
});
afterEach(() => {
rmSync(claudeHome, { recursive: true, force: true });
rmSync(codexHome, { recursive: true, force: true });
rmSync(homeDir, { recursive: true, force: true });
for (const key in originalEnv) {
process.env[key] = originalEnv[key];
}
for (const key of Object.keys(process.env)) {
if (!(key in originalEnv)) {
delete process.env[key];
}
}
});
function service(
options: { platform?: typeof process.platform; keychain?: () => Promise<unknown | null> } = {},
) {
const logger = createLogger();
const fetchThroughTestDouble = ((url: RequestInfo | URL, init?: RequestInit) =>
fetchApi(url, init)) as typeof fetch;
return new ProviderUsageService({
logger,
now: () => Date.parse("2026-06-19T00:00:00.000Z"),
fetchers: [
new ClaudeQuotaProvider({
logger,
claudeHome,
claudeKeychainReader: options.keychain ?? (async () => null),
platform: options.platform,
fetch: fetchThroughTestDouble,
}),
new CodexQuotaProvider({ logger, codexHome, fetch: fetchThroughTestDouble }),
new CopilotQuotaProvider({ logger, fetch: fetchThroughTestDouble }),
new CursorQuotaProvider({ logger, fetch: fetchThroughTestDouble }),
new ZaiQuotaProvider({ logger, fetch: fetchThroughTestDouble }),
new GrokQuotaProvider({ logger, fetch: fetchThroughTestDouble }),
new KimiQuotaProvider({ logger, fetch: fetchThroughTestDouble }),
],
cacheTtlMs: 0,
});
}
it("fetches Claude usage, coerces API numbers, and attaches HTTP timeout signals", async () => {
writeClaudeCredentials(claudeHome, "at_valid");
fetchApi = mockFetch(
new Map([
[
"https://api.anthropic.com/api/oauth/usage",
() =>
jsonResponse(
makeClaudeResponse({
five_hour: { utilization: "11", resets_at: "2026-06-01T21:00:00Z" },
}),
),
],
]),
);
const result = await service().listUsage();
const claude = findProvider(result, "claude");
expect(claude).toMatchObject({
status: "available",
planLabel: "Pro 1x",
windows: expect.arrayContaining([
expect.objectContaining({ id: "five_hour", usedPct: 11 }),
expect.objectContaining({ id: "weekly", usedPct: 1 }),
expect.objectContaining({ id: "weekly_opus", usedPct: 0.5 }),
]),
});
expect(fetchApi).toHaveBeenCalledWith(
"https://api.anthropic.com/api/oauth/usage",
expect.objectContaining({ signal: expect.any(AbortSignal) }),
);
});
it("returns unavailable Claude usage when credentials are missing", async () => {
fetchApi = vi.fn() as never;
const result = await service().listUsage();
const claude = findProvider(result, "claude");
expect(claude.status).toBe("unavailable");
expect(fetchApi).not.toHaveBeenCalled();
});
it("refreshes Claude access tokens on 401 and retries", async () => {
writeClaudeCredentials(claudeHome, "at_expired", "rt_valid");
let usageCalls = 0;
fetchApi = vi.fn(async (url: RequestInfo | URL) => {
const endpoint = url.toString();
if (endpoint === "https://api.anthropic.com/api/oauth/usage") {
usageCalls += 1;
if (usageCalls === 1) return new Response(null, { status: 401 });
return jsonResponse(makeClaudeResponse());
}
if (endpoint === "https://platform.claude.com/v1/oauth/token") {
return jsonResponse({ access_token: "at_refreshed", refresh_token: "rt_new" });
}
throw new Error(`Unmocked: ${endpoint}`);
}) as never;
const result = await service().listUsage();
expect(findProvider(result, "claude").status).toBe("available");
expect(usageCalls).toBe(2);
});
it("returns unavailable Claude usage when 401 persists after refresh", async () => {
writeClaudeCredentials(claudeHome, "at_bad", "rt_bad");
fetchApi = mockFetch(
new Map([
["https://api.anthropic.com/api/oauth/usage", () => new Response(null, { status: 401 })],
[
"https://platform.claude.com/v1/oauth/token",
() => jsonResponse({ access_token: "at_still_bad", refresh_token: "rt_still_bad" }),
],
]),
);
const result = await service().listUsage();
expect(findProvider(result, "claude").status).toBe("unavailable");
});
it("does not refresh Claude tokens read from the macOS Keychain", async () => {
const usageFetch = vi.fn(async () => new Response(null, { status: 401 }));
fetchApi = usageFetch as never;
const result = await service({
platform: "darwin",
keychain: async () => ({
claudeAiOauth: {
accessToken: "at_expired",
refreshToken: "rt_valid",
},
}),
}).listUsage();
expect(findProvider(result, "claude").status).toBe("unavailable");
expect(usageFetch).toHaveBeenCalledTimes(1);
expect(usageFetch).toHaveBeenCalledWith(
"https://api.anthropic.com/api/oauth/usage",
expect.objectContaining({
headers: expect.objectContaining({ Authorization: "Bearer at_expired" }),
}),
);
});
it("fetches Codex windows and coerces string credit balances", async () => {
writeCodexAuth(codexHome, "at_codex_valid");
fetchApi = mockFetch(
new Map([
[
"https://chatgpt.com/backend-api/wham/usage",
() =>
jsonResponse(
makeCodexResponse({
code_review_rate_limit: null,
credits: { balance: "0" },
}),
),
],
]),
);
const result = await service().listUsage();
const codex = findProvider(result, "codex");
expect(codex).toMatchObject({
status: "available",
planLabel: "plus",
windows: expect.arrayContaining([
expect.objectContaining({ id: "session", usedPct: 42 }),
expect.objectContaining({ id: "weekly", usedPct: 8 }),
]),
balances: [expect.objectContaining({ id: "credits", remaining: 0 })],
});
});
it("treats a Codex HTML usage response as auth failure", async () => {
writeCodexAuth(codexHome, "at_codex_stale");
fetchApi = mockFetch(
new Map([
[
"https://chatgpt.com/backend-api/wham/usage",
() => new Response("<html>Login</html>", { status: 200 }),
],
["https://auth.openai.com/oauth/token", () => new Response(null, { status: 401 })],
]),
);
const result = await service().listUsage();
expect(findProvider(result, "codex").status).toBe("unavailable");
});
it("persists refreshed Codex tokens to the auth file that was read", async () => {
const alternateCodexHome = mkdtempSync(join(tmpdir(), "usage-test-codex-alt-"));
process.env["CODEX_HOME"] = alternateCodexHome;
writeFileSync(join(alternateCodexHome, "auth.json"), JSON.stringify({ tokens: {} }));
writeCodexAuth(codexHome, "at_codex_stale", "rt_codex_valid");
let usageCalls = 0;
fetchApi = mockFetch(
new Map([
[
"https://chatgpt.com/backend-api/wham/usage",
() => {
usageCalls += 1;
if (usageCalls === 1) return new Response(null, { status: 401 });
return jsonResponse(makeCodexResponse());
},
],
[
"https://auth.openai.com/oauth/token",
() => jsonResponse({ access_token: "at_codex_fresh", refresh_token: "rt_codex_fresh" }),
],
]),
);
try {
const result = await service().listUsage();
const refreshedAuth = JSON.parse(readFileSync(join(codexHome, "auth.json"), "utf8"));
const untouchedAuth = JSON.parse(readFileSync(join(alternateCodexHome, "auth.json"), "utf8"));
expect(findProvider(result, "codex").status).toBe("available");
expect(refreshedAuth.tokens.access_token).toBe("at_codex_fresh");
expect(refreshedAuth.tokens.refresh_token).toBe("rt_codex_fresh");
expect(untouchedAuth.tokens.access_token).toBeUndefined();
} finally {
rmSync(alternateCodexHome, { recursive: true, force: true });
}
});
it("fetches Copilot usage from COPILOT_TOKEN", async () => {
process.env["COPILOT_TOKEN"] = "copilot_test_token";
fetchApi = mockFetch(
new Map([
[
"https://api.github.com/copilot_internal/user",
() =>
jsonResponse({
copilot_plan: "business",
quota_reset_date: "2026-07-01T00:00:00Z",
}),
],
]),
);
const copilot = findProvider(await service().listUsage(), "copilot");
expect(copilot).toMatchObject({
status: "available",
planLabel: "business",
details: [{ id: "reset", label: "Quota reset", value: "2026-07-01T00:00:00Z" }],
});
});
it("fetches Cursor usage and normalizes malformed billing dates to null", async () => {
process.env["CURSOR_ACCESS_TOKEN"] = "cursor_test_token";
fetchApi = mockFetch(
new Map([
[
"https://api2.cursor.sh/aiserver.v1.DashboardService/GetCurrentPeriodUsage",
() =>
jsonResponse({
planUsage: {
totalSpend: "1500",
includedSpend: "1000",
bonusSpend: "500",
remaining: "2500",
limit: "4000",
},
billingCycleStart: "2026-01-14T12:42:14.000Z",
billingCycleEnd: "not-a-date",
}),
],
]),
);
const cursor = findProvider(await service().listUsage(), "cursor");
expect(cursor).toMatchObject({
status: "available",
balances: [
expect.objectContaining({
id: "plan_usage",
used: 15,
remaining: 25,
limit: 40,
resetsAt: null,
}),
],
});
});
it("fetches Z.ai usage from ZAI_API_KEY", async () => {
process.env["ZAI_API_KEY"] = "zai_test_token";
fetchApi = mockFetch(
new Map([
[
"https://api.z.ai/api/biz/subscription/list",
() =>
jsonResponse({
data: [
{
productName: "GLM Coding Max",
status: "VALID",
purchaseTime: "2026-01-12 16:55:13",
valid: "2026-02-12 16:55:13-2026-03-12 16:55:13",
},
],
}),
],
]),
);
const zai = findProvider(await service().listUsage(), "zai");
expect(zai).toMatchObject({
status: "available",
planLabel: "GLM Coding Max",
details: expect.arrayContaining([{ id: "status", label: "Status", value: "VALID" }]),
});
});
it("fetches Grok usage and preserves zero values", async () => {
process.env["GROK_API_KEY"] = "grok_test_token";
fetchApi = mockFetch(
new Map([
[
"https://cli-chat-proxy.grok.com/v1/billing",
() =>
jsonResponse({
config: { monthlyLimit: { val: 0 } },
usage: { creditUsage: 0 },
}),
],
]),
);
const grok = findProvider(await service().listUsage(), "grok");
expect(grok).toMatchObject({
status: "available",
balances: [
expect.objectContaining({
id: "monthly_credits",
used: 0,
remaining: 0,
limit: 0,
}),
],
});
});
it("fetches Kimi usage from KIMI_TOKEN", async () => {
process.env["KIMI_TOKEN"] = "kimi_test_token";
fetchApi = mockFetch(
new Map([
[
"https://api.kimi.com/coding/v1/usages",
() =>
jsonResponse({
usage: {
limit: "100",
remaining: "74",
resetTime: "2026-02-11T17:32:50Z",
},
}),
],
]),
);
const kimi = findProvider(await service().listUsage(), "kimi");
expect(kimi).toMatchObject({
status: "available",
windows: [
expect.objectContaining({
id: "coding_usage",
usedPct: 26,
remainingPct: 74,
resetsAt: "2026-02-11T17:32:50Z",
}),
],
});
});
});
@@ -0,0 +1,89 @@
import type { Logger } from "pino";
import type { ProviderUsage } from "../../server/messages.js";
import { createProviderUsageFetchers } from "./manifest.js";
import type { ProviderApiFetch, ProviderUsageFetcher } from "./provider.js";
import { unavailableUsage } from "./usage.js";
export interface ProviderUsageServiceOptions {
logger: Logger;
fetchers?: ProviderUsageFetcher[];
fetch?: ProviderApiFetch;
cacheTtlMs?: number;
now?: () => number;
}
export interface ProviderUsageListResult {
fetchedAt: string;
providers: ProviderUsage[];
}
const DEFAULT_PROVIDER_USAGE_CACHE_TTL_MS = 5 * 60 * 1000;
export class ProviderUsageService {
private readonly logger: Logger;
private readonly fetchers: ProviderUsageFetcher[];
private readonly cacheTtlMs: number;
private readonly now: () => number;
private cached: { fetchedAtMs: number; result: ProviderUsageListResult } | null = null;
private inFlight: Promise<ProviderUsageListResult> | null = null;
constructor(options: ProviderUsageServiceOptions) {
this.logger = options.logger.child({ module: "provider-usage-service" });
this.fetchers =
options.fetchers ??
createProviderUsageFetchers({
logger: this.logger,
fetch: options.fetch,
});
this.cacheTtlMs = options.cacheTtlMs ?? DEFAULT_PROVIDER_USAGE_CACHE_TTL_MS;
this.now = options.now ?? Date.now;
}
async listUsage(options?: { forceRefresh?: boolean }): Promise<ProviderUsageListResult> {
const nowMs = this.now();
if (
!options?.forceRefresh &&
this.cached &&
nowMs - this.cached.fetchedAtMs < this.cacheTtlMs
) {
return this.cached.result;
}
if (this.inFlight) {
return this.inFlight;
}
const request = this.fetchFreshUsage(nowMs);
this.inFlight = request;
try {
return await request;
} finally {
if (this.inFlight === request) {
this.inFlight = null;
}
}
}
private async fetchFreshUsage(nowMs: number): Promise<ProviderUsageListResult> {
const settled = await Promise.allSettled(this.fetchers.map((fetcher) => fetcher.fetchUsage()));
const providers = settled.map((result, index) => {
const fetcher = this.fetchers[index];
if (result.status === "fulfilled") {
return result.value;
}
this.logger.debug(
{ err: result.reason, providerId: fetcher.providerId },
"Provider usage fetch failed",
);
return unavailableUsage({
providerId: fetcher.providerId,
displayName: fetcher.displayName,
error: result.reason instanceof Error ? result.reason.message : String(result.reason),
});
});
const result = { fetchedAt: new Date(nowMs).toISOString(), providers };
this.cached = { fetchedAtMs: nowMs, result };
return result;
}
}
@@ -0,0 +1,81 @@
import { z } from "zod";
import type {
ProviderUsage,
ProviderUsageBalance,
ProviderUsageWindow,
} from "../../server/messages.js";
import type { ProviderApiFetch } from "./provider.js";
const PROVIDER_HTTP_TIMEOUT_MS = 15_000;
export const ApiNumberSchema = z.coerce.number().finite();
export const ApiNullableNumberSchema = z.preprocess(
(value) => (value == null ? null : value),
ApiNumberSchema.nullable(),
);
export const ApiOptionalStringSchema = z.preprocess(
(value) => (value == null ? undefined : value),
z.coerce.string().optional(),
);
export function fetchProviderApi(
fetchApi: ProviderApiFetch,
input: RequestInfo | URL,
init: RequestInit = {},
): Promise<Response> {
return fetchApi(input, {
...init,
signal: init.signal ?? AbortSignal.timeout(PROVIDER_HTTP_TIMEOUT_MS),
});
}
export function unavailableUsage(provider: {
providerId: string;
displayName: string;
error?: string | null;
}): ProviderUsage {
return {
providerId: provider.providerId,
displayName: provider.displayName,
status: provider.error ? "error" : "unavailable",
planLabel: null,
windows: [],
balances: [],
details: [],
error: provider.error ?? null,
};
}
export function windowFromUsedPct(input: {
id: string;
label: string;
utilizationPct: number | null | undefined;
resetsAt?: string | null;
tone?: ProviderUsageWindow["tone"];
}): ProviderUsageWindow {
const usedPct = typeof input.utilizationPct === "number" ? input.utilizationPct : null;
const window: ProviderUsageWindow = {
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;
}
return window;
}
export function balanceToneFromRemaining(
remaining: number | null | undefined,
): ProviderUsageBalance["tone"] {
if (typeof remaining !== "number") return "default";
if (remaining <= 0) return "danger";
return "ok";
}
export function toIsoStringOrNull(timestampMs: number): string | null {
const date = new Date(timestampMs);
return Number.isFinite(date.getTime()) ? date.toISOString() : null;
}