@latentminds/pi-quotas 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,163 @@
1
+ import type {
2
+ ExtensionAPI,
3
+ ExtensionContext,
4
+ } from "@mariozechner/pi-coding-agent";
5
+ import {
6
+ QUOTAS_CONFIG_UPDATED_EVENT,
7
+ QUOTAS_EXTENSIONS_REGISTER_EVENT,
8
+ QUOTAS_EXTENSIONS_REQUEST_EVENT,
9
+ type QuotasConfigUpdatedPayload,
10
+ configLoader,
11
+ } from "../../config.js";
12
+ import {
13
+ fetchProviderQuotas,
14
+ formatResetTime,
15
+ isSupportedProvider,
16
+ } from "../../lib/quotas.js";
17
+ import {
18
+ assessWindow,
19
+ } from "../../utils/quotas-severity.js";
20
+ import { formatWindowStatus, type WindowStatus } from "./format-status.js";
21
+
22
+ const EXTENSION_ID = "pi-quotas-usage";
23
+ const REFRESH_INTERVAL_MS = 60_000;
24
+
25
+ function formatStatus(ctx: ExtensionContext, windows: WindowStatus[]): string {
26
+ const theme = ctx.ui.theme;
27
+ return windows
28
+ .map((w) => {
29
+ const core = formatWindowStatus(theme, w);
30
+ const reset = w.resetsAt ? theme.fg("dim", ` (↺${formatResetTime(w.resetsAt)})`) : "";
31
+ return `${core}${reset}`;
32
+ })
33
+ .join(" ");
34
+ }
35
+
36
+ function createStatusRefresher() {
37
+ let refreshTimer: ReturnType<typeof setInterval> | undefined;
38
+ let activeContext: ExtensionContext | undefined;
39
+ let activeProvider: string | undefined;
40
+ let lastStatus: WindowStatus[] | undefined;
41
+ let inFlight = false;
42
+ let queued = false;
43
+
44
+ async function update(ctx: ExtensionContext): Promise<void> {
45
+ if (!ctx.hasUI || !activeProvider || !isSupportedProvider(activeProvider)) return;
46
+ if (inFlight) {
47
+ queued = true;
48
+ return;
49
+ }
50
+ inFlight = true;
51
+ try {
52
+ const result = await fetchProviderQuotas(ctx.modelRegistry.authStorage, activeProvider);
53
+ if (!result.success) {
54
+ ctx.ui.setStatus(EXTENSION_ID, ctx.ui.theme.fg("warning", "usage unavailable"));
55
+ return;
56
+ }
57
+ const windows: WindowStatus[] = result.data.windows.map((window) => ({
58
+ label: window.label,
59
+ usedPercent: window.usedPercent,
60
+ severity: assessWindow(window).severity,
61
+ resetsAt: window.resetsAt.toISOString(),
62
+ limited: window.limited ?? false,
63
+ isCurrency: window.isCurrency,
64
+ usedValue: window.usedValue,
65
+ limitValue: window.limitValue,
66
+ }));
67
+ lastStatus = windows;
68
+ ctx.ui.setStatus(EXTENSION_ID, formatStatus(ctx, windows));
69
+ } catch {
70
+ ctx.ui.setStatus(EXTENSION_ID, ctx.ui.theme.fg("warning", "usage unavailable"));
71
+ } finally {
72
+ inFlight = false;
73
+ if (queued) {
74
+ queued = false;
75
+ void update(ctx);
76
+ }
77
+ }
78
+ }
79
+
80
+ return {
81
+ async refreshFor(ctx: ExtensionContext): Promise<void> {
82
+ activeContext = ctx;
83
+ activeProvider = ctx.model?.provider;
84
+ if (!activeProvider || !isSupportedProvider(activeProvider)) {
85
+ ctx.ui.setStatus(EXTENSION_ID, undefined);
86
+ return;
87
+ }
88
+ await update(ctx);
89
+ },
90
+ start(): void {
91
+ if (refreshTimer) clearInterval(refreshTimer);
92
+ refreshTimer = setInterval(() => {
93
+ if (activeContext) void update(activeContext);
94
+ }, REFRESH_INTERVAL_MS);
95
+ refreshTimer.unref?.();
96
+ },
97
+ stop(ctx?: ExtensionContext): void {
98
+ if (refreshTimer) clearInterval(refreshTimer);
99
+ refreshTimer = undefined;
100
+ activeContext = undefined;
101
+ activeProvider = undefined;
102
+ lastStatus = undefined;
103
+ ctx?.ui.setStatus(EXTENSION_ID, undefined);
104
+ },
105
+ renderLast(ctx: ExtensionContext): boolean {
106
+ if (!lastStatus || !ctx.hasUI) return false;
107
+ ctx.ui.setStatus(EXTENSION_ID, formatStatus(ctx, lastStatus));
108
+ return true;
109
+ },
110
+ };
111
+ }
112
+
113
+ export default async function (pi: ExtensionAPI) {
114
+ await configLoader.load();
115
+ const refresher = createStatusRefresher();
116
+ let enabled = configLoader.getConfig().usageStatus;
117
+ let currentContext: ExtensionContext | undefined;
118
+
119
+ pi.events.on(QUOTAS_CONFIG_UPDATED_EVENT, (data: unknown) => {
120
+ enabled = (data as QuotasConfigUpdatedPayload).config.usageStatus;
121
+ if (!enabled) {
122
+ refresher.stop(currentContext);
123
+ return;
124
+ }
125
+ if (currentContext) {
126
+ refresher.start();
127
+ void refresher.refreshFor(currentContext);
128
+ }
129
+ });
130
+
131
+ pi.on("session_start", async (_event, ctx) => {
132
+ currentContext = ctx;
133
+ if (!enabled) return;
134
+ refresher.start();
135
+ await refresher.refreshFor(ctx);
136
+ });
137
+
138
+ pi.on("turn_end", async (_event, ctx) => {
139
+ currentContext = ctx;
140
+ if (!enabled) return;
141
+ await refresher.refreshFor(ctx);
142
+ });
143
+
144
+ pi.on("model_select", async (_event, ctx) => {
145
+ currentContext = ctx;
146
+ if (!enabled) {
147
+ refresher.stop(ctx);
148
+ return;
149
+ }
150
+ await refresher.refreshFor(ctx);
151
+ });
152
+
153
+ pi.on("session_shutdown", async (_event, ctx) => {
154
+ currentContext = undefined;
155
+ refresher.stop(ctx);
156
+ });
157
+
158
+ pi.events.on(QUOTAS_EXTENSIONS_REQUEST_EVENT, () => {
159
+ if (configLoader.getConfig().usageStatus) {
160
+ pi.events.emit(QUOTAS_EXTENSIONS_REGISTER_EVENT, { feature: "usageStatus" });
161
+ }
162
+ });
163
+ }
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export { default as coreExtension } from "./extensions/core/index.js";
2
+ export { default as commandQuotasExtension } from "./extensions/command-quotas/index.js";
3
+ export { default as usageStatusExtension } from "./extensions/usage-status/index.js";
4
+ export { default as quotaWarningsExtension } from "./extensions/quota-warnings/index.js";
@@ -0,0 +1,96 @@
1
+ import type { AuthStorage } from "@mariozechner/pi-coding-agent";
2
+ import { PROVIDER_FETCHERS } from "../providers/fetch.js";
3
+ import type { QuotasResult, SupportedQuotaProvider } from "../types/quotas.js";
4
+
5
+ export const SUPPORTED_PROVIDERS: SupportedQuotaProvider[] = [
6
+ "anthropic",
7
+ "openai-codex",
8
+ "github-copilot",
9
+ ];
10
+
11
+ export const PROVIDER_LABELS: Record<SupportedQuotaProvider, string> = {
12
+ anthropic: "Anthropic",
13
+ "openai-codex": "OpenAI Codex",
14
+ "github-copilot": "GitHub Copilot",
15
+ };
16
+
17
+ const PROVIDER_TTLS_MS: Record<SupportedQuotaProvider, number> = {
18
+ anthropic: 5 * 60_000,
19
+ "openai-codex": 60_000,
20
+ "github-copilot": 5 * 60_000,
21
+ };
22
+
23
+ type CacheEntry = {
24
+ result?: QuotasResult;
25
+ fetchedAt?: number;
26
+ inFlight?: Promise<QuotasResult>;
27
+ };
28
+
29
+ const cache = new Map<SupportedQuotaProvider, CacheEntry>();
30
+
31
+ export function isSupportedProvider(
32
+ provider: string | undefined,
33
+ ): provider is SupportedQuotaProvider {
34
+ return SUPPORTED_PROVIDERS.includes(provider as SupportedQuotaProvider);
35
+ }
36
+
37
+ export function clearQuotaCache(provider?: SupportedQuotaProvider): void {
38
+ if (provider) cache.delete(provider);
39
+ else cache.clear();
40
+ }
41
+
42
+ export async function fetchProviderQuotas(
43
+ authStorage: AuthStorage,
44
+ provider: SupportedQuotaProvider,
45
+ options?: { force?: boolean; signal?: AbortSignal },
46
+ ): Promise<QuotasResult> {
47
+ const entry = cache.get(provider) ?? {};
48
+ const now = Date.now();
49
+ const ttl = PROVIDER_TTLS_MS[provider];
50
+
51
+ if (!options?.force && entry.result && entry.fetchedAt && now - entry.fetchedAt < ttl) {
52
+ return entry.result;
53
+ }
54
+ if (!options?.force && entry.inFlight) return entry.inFlight;
55
+
56
+ const promise = PROVIDER_FETCHERS[provider](authStorage, options?.signal)
57
+ .then((result: QuotasResult) => {
58
+ cache.set(provider, { result, fetchedAt: Date.now() });
59
+ return result;
60
+ })
61
+ .finally(() => {
62
+ const current = cache.get(provider) ?? {};
63
+ delete current.inFlight;
64
+ cache.set(provider, current);
65
+ });
66
+
67
+ cache.set(provider, { ...entry, inFlight: promise });
68
+ return promise;
69
+ }
70
+
71
+ export async function fetchAllProviderQuotas(
72
+ authStorage: AuthStorage,
73
+ options?: { force?: boolean; signal?: AbortSignal },
74
+ ): Promise<Array<{ provider: SupportedQuotaProvider; result: QuotasResult }>> {
75
+ return Promise.all(
76
+ SUPPORTED_PROVIDERS.map(async (provider) => ({
77
+ provider,
78
+ result: await fetchProviderQuotas(authStorage, provider, options),
79
+ })),
80
+ );
81
+ }
82
+
83
+ export function formatResetTime(renewsAt: string): string {
84
+ const date = new Date(renewsAt);
85
+ const now = new Date();
86
+ const diffMs = date.getTime() - now.getTime();
87
+
88
+ if (diffMs <= 0) return "soon";
89
+
90
+ const diffHours = Math.ceil(diffMs / (1000 * 60 * 60));
91
+ const diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24));
92
+
93
+ if (diffHours < 24) return `in ${diffHours}h`;
94
+ if (diffDays < 7) return `in ${diffDays}d`;
95
+ return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
96
+ }
@@ -0,0 +1,137 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import {
3
+ fetchAnthropicQuotasWithToken,
4
+ fetchCodexQuotasWithToken,
5
+ fetchGitHubCopilotQuotasWithToken,
6
+ } from "./fetch.js";
7
+
8
+ const originalFetch = globalThis.fetch;
9
+
10
+ afterEach(() => {
11
+ globalThis.fetch = originalFetch;
12
+ vi.restoreAllMocks();
13
+ });
14
+
15
+ describe("fetchAnthropicQuotasWithToken", () => {
16
+ it("returns config error when token missing", async () => {
17
+ const result = await fetchAnthropicQuotasWithToken(undefined);
18
+ expect(result).toMatchObject({
19
+ success: false,
20
+ error: { kind: "config" },
21
+ });
22
+ });
23
+
24
+ it("fetches and parses quota windows", async () => {
25
+ globalThis.fetch = vi.fn().mockResolvedValue(
26
+ new Response(
27
+ JSON.stringify({
28
+ five_hour: { utilization: 21, resets_at: "2026-04-22T18:30:00Z" },
29
+ seven_day: { utilization: 9, resets_at: "2026-04-25T08:30:00Z" },
30
+ }),
31
+ { status: 200 },
32
+ ),
33
+ ) as any;
34
+
35
+ const result = await fetchAnthropicQuotasWithToken("token");
36
+ expect(result.success).toBe(true);
37
+ if (result.success) {
38
+ expect(result.data.provider).toBe("anthropic");
39
+ expect(result.data.windows).toHaveLength(2);
40
+ }
41
+ });
42
+ });
43
+
44
+ describe("fetchCodexQuotasWithToken", () => {
45
+ it("returns config error when account id missing", async () => {
46
+ const result = await fetchCodexQuotasWithToken("token", undefined);
47
+ expect(result).toMatchObject({
48
+ success: false,
49
+ error: { kind: "config" },
50
+ });
51
+ });
52
+
53
+ it("fetches and parses codex windows", async () => {
54
+ globalThis.fetch = vi.fn().mockResolvedValue(
55
+ new Response(
56
+ JSON.stringify({
57
+ rate_limit: {
58
+ primary_window: {
59
+ used_percent: 44,
60
+ reset_at: 1776880800,
61
+ limit_window_seconds: 18000,
62
+ },
63
+ secondary_window: {
64
+ used_percent: 12,
65
+ reset_at: 1777485600,
66
+ limit_window_seconds: 604800,
67
+ },
68
+ },
69
+ }),
70
+ { status: 200 },
71
+ ),
72
+ ) as any;
73
+
74
+ const result = await fetchCodexQuotasWithToken("token", "acct_123");
75
+ expect(result.success).toBe(true);
76
+ if (result.success) {
77
+ expect(result.data.provider).toBe("openai-codex");
78
+ expect(result.data.windows).toHaveLength(2);
79
+ }
80
+ });
81
+ });
82
+
83
+ describe("fetchGitHubCopilotQuotasWithToken", () => {
84
+ it("exchanges token then fetches usage on happy path", async () => {
85
+ globalThis.fetch = vi
86
+ .fn()
87
+ .mockResolvedValueOnce(
88
+ new Response(JSON.stringify({ token: "copilot-token" }), { status: 200 }),
89
+ )
90
+ .mockResolvedValueOnce(
91
+ new Response(
92
+ JSON.stringify({
93
+ quota_reset_date: "2026-05-01T00:00:00Z",
94
+ quota_snapshots: {
95
+ premium_interactions: {
96
+ entitlement: 300,
97
+ remaining: 240,
98
+ percent_remaining: 80,
99
+ },
100
+ },
101
+ }),
102
+ { status: 200 },
103
+ ),
104
+ ) as any;
105
+
106
+ const result = await fetchGitHubCopilotQuotasWithToken("gh-token");
107
+ expect(result.success).toBe(true);
108
+ if (result.success) {
109
+ expect(result.data.provider).toBe("github-copilot");
110
+ expect(result.data.windows).toHaveLength(1);
111
+ }
112
+ expect(globalThis.fetch).toHaveBeenCalledTimes(2);
113
+ });
114
+
115
+ it("falls back to direct token when exchange returns 401", async () => {
116
+ globalThis.fetch = vi
117
+ .fn()
118
+ .mockResolvedValueOnce(
119
+ new Response(JSON.stringify({ message: "Bad credentials" }), { status: 401 }),
120
+ )
121
+ .mockResolvedValueOnce(
122
+ new Response(
123
+ JSON.stringify({
124
+ quota_reset_date: "2026-05-01T00:00:00Z",
125
+ quota_snapshots: {
126
+ premium_interactions: { entitlement: 300, remaining: 293 },
127
+ },
128
+ }),
129
+ { status: 200 },
130
+ ),
131
+ ) as any;
132
+
133
+ const result = await fetchGitHubCopilotQuotasWithToken("gh-token");
134
+ expect(result.success).toBe(true);
135
+ expect(globalThis.fetch).toHaveBeenCalledTimes(2);
136
+ });
137
+ });
@@ -0,0 +1,245 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { readFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ import type { AuthStorage } from "@mariozechner/pi-coding-agent";
6
+ import type { QuotasResult, SupportedQuotaProvider } from "../types/quotas.js";
7
+ import {
8
+ parseAnthropicUsage,
9
+ parseCodexUsage,
10
+ parseGitHubCopilotUsage,
11
+ } from "./providers.js";
12
+
13
+ const FETCH_TIMEOUT_MS = 15_000;
14
+ const COPILOT_VERSION = "0.35.0";
15
+ const EDITOR_VERSION = "vscode/1.107.0";
16
+
17
+ function isTimeoutReason(reason: unknown): boolean {
18
+ return (
19
+ (reason instanceof DOMException && reason.name === "TimeoutError") ||
20
+ (reason instanceof Error && reason.name === "TimeoutError")
21
+ );
22
+ }
23
+
24
+ async function providerAccessToken(
25
+ authStorage: AuthStorage,
26
+ provider: string,
27
+ ): Promise<string | undefined> {
28
+ return authStorage.getApiKey(provider);
29
+ }
30
+
31
+ function codexAccountId(authStorage: AuthStorage): string | undefined {
32
+ const credential = authStorage.get("openai-codex") as any;
33
+ if (typeof credential?.accountId === "string") return credential.accountId;
34
+ try {
35
+ const authPath = join(homedir(), ".codex", "auth.json");
36
+ const data = JSON.parse(readFileSync(authPath, "utf8")) as any;
37
+ return data?.tokens?.account_id ?? data?.tokens?.accountId;
38
+ } catch {
39
+ return undefined;
40
+ }
41
+ }
42
+
43
+ type FetchJsonResult =
44
+ | { ok: true; data: any }
45
+ | {
46
+ ok: false;
47
+ status?: number;
48
+ message: string;
49
+ kind: "timeout" | "cancelled" | "http" | "network";
50
+ };
51
+
52
+ async function fetchJson(
53
+ url: string,
54
+ init: RequestInit,
55
+ signal?: AbortSignal,
56
+ ): Promise<FetchJsonResult> {
57
+ const signals: AbortSignal[] = [AbortSignal.timeout(FETCH_TIMEOUT_MS)];
58
+ if (signal) signals.push(signal);
59
+ const combined = AbortSignal.any(signals);
60
+
61
+ try {
62
+ const response = await fetch(url, { ...init, signal: combined });
63
+ if (!response.ok) {
64
+ const body = await response.text().catch(() => "");
65
+ return {
66
+ ok: false,
67
+ status: response.status,
68
+ message: body || response.statusText || `HTTP ${response.status}`,
69
+ kind: "http",
70
+ };
71
+ }
72
+ return { ok: true, data: await response.json() };
73
+ } catch (err: unknown) {
74
+ const isAbort =
75
+ combined.aborted ||
76
+ (err instanceof DOMException && err.name === "AbortError");
77
+ if (isAbort) {
78
+ if (isTimeoutReason(combined.reason)) {
79
+ return { ok: false, message: "Request timed out", kind: "timeout" };
80
+ }
81
+ return { ok: false, message: "Request cancelled", kind: "cancelled" };
82
+ }
83
+ const message = err instanceof Error ? err.message : "Unknown error";
84
+ return { ok: false, message, kind: "network" };
85
+ }
86
+ }
87
+
88
+ function success(provider: SupportedQuotaProvider, windows: ReturnType<typeof parseAnthropicUsage>): QuotasResult {
89
+ return { success: true, data: { provider, windows } };
90
+ }
91
+
92
+ function failure(message: string, kind: "cancelled" | "timeout" | "config" | "http" | "network"): QuotasResult {
93
+ return { success: false, error: { message, kind } };
94
+ }
95
+
96
+ export async function fetchAnthropicQuotasWithToken(
97
+ accessToken: string | undefined,
98
+ signal?: AbortSignal,
99
+ ): Promise<QuotasResult> {
100
+ if (!accessToken) return failure("No Anthropic OAuth token found", "config");
101
+ const result = await fetchJson(
102
+ "https://api.anthropic.com/api/oauth/usage",
103
+ {
104
+ headers: {
105
+ Authorization: `Bearer ${accessToken}`,
106
+ "anthropic-beta": "oauth-2025-04-20",
107
+ Accept: "application/json",
108
+ },
109
+ },
110
+ signal,
111
+ );
112
+ if (!result.ok) return failure(result.message, result.kind);
113
+ return success("anthropic", parseAnthropicUsage(result.data));
114
+ }
115
+
116
+ export async function fetchCodexQuotasWithToken(
117
+ accessToken: string | undefined,
118
+ accountId: string | undefined,
119
+ signal?: AbortSignal,
120
+ ): Promise<QuotasResult> {
121
+ if (!accessToken) return failure("No Codex access token found", "config");
122
+ if (!accountId) return failure("No Codex account id found", "config");
123
+ const result = await fetchJson(
124
+ "https://chatgpt.com/backend-api/wham/usage",
125
+ {
126
+ headers: {
127
+ Authorization: `Bearer ${accessToken}`,
128
+ "ChatGPT-Account-Id": accountId,
129
+ Accept: "application/json",
130
+ Origin: "https://chatgpt.com",
131
+ Referer: "https://chatgpt.com/",
132
+ "User-Agent": "Mozilla/5.0",
133
+ },
134
+ },
135
+ signal,
136
+ );
137
+ if (!result.ok) return failure(result.message, result.kind);
138
+ return success("openai-codex", parseCodexUsage(result.data));
139
+ }
140
+
141
+ function copilotHeaders(authHeader: string): Record<string, string> {
142
+ return {
143
+ Accept: "application/json",
144
+ Authorization: authHeader,
145
+ "User-Agent": `GitHubCopilotChat/${COPILOT_VERSION}`,
146
+ "Editor-Version": EDITOR_VERSION,
147
+ "Editor-Plugin-Version": `copilot-chat/${COPILOT_VERSION}`,
148
+ "Copilot-Integration-Id": "vscode-chat",
149
+ "Content-Type": "application/json",
150
+ };
151
+ }
152
+
153
+ /**
154
+ * Try to get a token from `gh auth token` CLI as fallback when the Pi-stored
155
+ * OAuth token is stale or the token exchange returns 401.
156
+ */
157
+ function ghCliToken(): string | undefined {
158
+ try {
159
+ return execFileSync("gh", ["auth", "token"], {
160
+ timeout: 5000,
161
+ encoding: "utf8",
162
+ stdio: ["ignore", "pipe", "ignore"],
163
+ }).trim() || undefined;
164
+ } catch {
165
+ return undefined;
166
+ }
167
+ }
168
+
169
+ async function tryGitHubUserEndpoint(
170
+ authHeader: string,
171
+ signal?: AbortSignal,
172
+ ): Promise<FetchJsonResult> {
173
+ return fetchJson(
174
+ "https://api.github.com/copilot_internal/user",
175
+ { headers: copilotHeaders(authHeader) },
176
+ signal,
177
+ );
178
+ }
179
+
180
+ export async function fetchGitHubCopilotQuotasWithToken(
181
+ accessToken: string | undefined,
182
+ signal?: AbortSignal,
183
+ ): Promise<QuotasResult> {
184
+ if (!accessToken) return failure("No GitHub Copilot OAuth token found", "config");
185
+
186
+ // 1) Try Copilot token exchange with stored Pi token
187
+ const exchange = await fetchJson(
188
+ "https://api.github.com/copilot_internal/v2/token",
189
+ { headers: copilotHeaders(`Bearer ${accessToken}`) },
190
+ signal,
191
+ );
192
+
193
+ if (exchange.ok && exchange.data?.token) {
194
+ const usage = await tryGitHubUserEndpoint(`Bearer ${exchange.data.token}`, signal);
195
+ if (usage.ok) return success("github-copilot", parseGitHubCopilotUsage(usage.data));
196
+ }
197
+
198
+ // 2) Try stored token directly
199
+ const directUsage = await tryGitHubUserEndpoint(`token ${accessToken}`, signal);
200
+ if (directUsage.ok) return success("github-copilot", parseGitHubCopilotUsage(directUsage.data));
201
+
202
+ // 3) Fallback: gh CLI token
203
+ const cliToken = ghCliToken();
204
+ if (cliToken && cliToken !== accessToken) {
205
+ const cliUsage = await tryGitHubUserEndpoint(`token ${cliToken}`, signal);
206
+ if (cliUsage.ok) return success("github-copilot", parseGitHubCopilotUsage(cliUsage.data));
207
+ return failure(cliUsage.message, cliUsage.kind);
208
+ }
209
+
210
+ return failure(directUsage.message, directUsage.kind);
211
+ }
212
+
213
+ export async function fetchAnthropicQuotas(
214
+ authStorage: AuthStorage,
215
+ signal?: AbortSignal,
216
+ ): Promise<QuotasResult> {
217
+ return fetchAnthropicQuotasWithToken(await providerAccessToken(authStorage, "anthropic"), signal);
218
+ }
219
+
220
+ export async function fetchCodexQuotas(
221
+ authStorage: AuthStorage,
222
+ signal?: AbortSignal,
223
+ ): Promise<QuotasResult> {
224
+ return fetchCodexQuotasWithToken(
225
+ await providerAccessToken(authStorage, "openai-codex"),
226
+ codexAccountId(authStorage),
227
+ signal,
228
+ );
229
+ }
230
+
231
+ export async function fetchGitHubCopilotQuotas(
232
+ authStorage: AuthStorage,
233
+ signal?: AbortSignal,
234
+ ): Promise<QuotasResult> {
235
+ return fetchGitHubCopilotQuotasWithToken(
236
+ await providerAccessToken(authStorage, "github-copilot"),
237
+ signal,
238
+ );
239
+ }
240
+
241
+ export const PROVIDER_FETCHERS = {
242
+ anthropic: fetchAnthropicQuotas,
243
+ "openai-codex": fetchCodexQuotas,
244
+ "github-copilot": fetchGitHubCopilotQuotas,
245
+ } as const;