@narumitw/pi-usage 0.60.7 → 0.60.8

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.
package/src/query.ts CHANGED
@@ -3,21 +3,14 @@
3
3
  import { randomBytes } from "node:crypto";
4
4
  import { type ExtensionContext, readStoredCredential } from "@earendil-works/pi-coding-agent";
5
5
  import { errorMessage, fingerprintResolvedAuth, redactUsageError } from "./core.js";
6
- import {
7
- fallbackOAuthCredentialCandidates,
8
- type OAuthCredentialCandidateReader,
9
- } from "./oauth-credential-source.js";
6
+ import { fallbackOAuthCredentialCandidates, type OAuthCredentialCandidateReader } from "./oauth-credential-source.js";
10
7
  import { normalizeBasetenBillingUsagePayload } from "./providers/baseten.js";
11
8
  import { normalizeCodexBackendPayload } from "./providers/codex.js";
12
9
  import { normalizeDeepSeekBalancePayload } from "./providers/deepseek.js";
13
10
  import { createFireworksAdapter } from "./providers/fireworks.js";
14
11
  import { normalizeGitHubCopilotUsagePayload } from "./providers/github-copilot.js";
15
12
  import { normalizeKimiCodingUsagePayload } from "./providers/kimi-coding.js";
16
- import {
17
- type MiniMaxProviderId,
18
- miniMaxUsageKind,
19
- normalizeMiniMaxUsagePayload,
20
- } from "./providers/minimax.js";
13
+ import { type MiniMaxProviderId, miniMaxUsageKind, normalizeMiniMaxUsagePayload } from "./providers/minimax.js";
21
14
  import { normalizeMoonshotBalancePayload } from "./providers/moonshot.js";
22
15
  import { normalizeOpenCodeZenPayload } from "./providers/opencode-zen.js";
23
16
  import { normalizeOpenRouterKeyPayload } from "./providers/openrouter.js";
@@ -26,27 +19,27 @@ import { normalizeXaiBillingPayload } from "./providers/xai.js";
26
19
  import { normalizeZaiQuotaPayload, normalizeZaiSubscriptionPayload } from "./providers/zai.js";
27
20
  import { zaiResponseError } from "./providers/zai-errors.js";
28
21
  import type {
29
- BasetenBillingUsagePayload,
30
- CodexBackendPayload,
31
- DeepSeekBalancePayload,
32
- GitHubCopilotUsagePayload,
33
- KimiCodingUsagePayload,
34
- MiniMaxUsagePayload,
35
- MoonshotBalancePayload,
36
- OpenCodeZenPayload,
37
- OpenRouterKeyPayload,
38
- PiModel,
39
- ResolvedUsageAuth,
40
- UsageProviderAdapter,
41
- UsageQuerySettings,
42
- UsageReport,
43
- UsageRequestGuard,
44
- VercelAIGatewayCreditsPayload,
45
- XaiBillingPayload,
46
- XaiUserPayload,
47
- ZaiPlanInfo,
48
- ZaiQuotaPayload,
49
- ZaiSubscriptionPayload,
22
+ BasetenBillingUsagePayload,
23
+ CodexBackendPayload,
24
+ DeepSeekBalancePayload,
25
+ GitHubCopilotUsagePayload,
26
+ KimiCodingUsagePayload,
27
+ MiniMaxUsagePayload,
28
+ MoonshotBalancePayload,
29
+ OpenCodeZenPayload,
30
+ OpenRouterKeyPayload,
31
+ PiModel,
32
+ ResolvedUsageAuth,
33
+ UsageProviderAdapter,
34
+ UsageQuerySettings,
35
+ UsageReport,
36
+ UsageRequestGuard,
37
+ VercelAIGatewayCreditsPayload,
38
+ XaiBillingPayload,
39
+ XaiUserPayload,
40
+ ZaiPlanInfo,
41
+ ZaiQuotaPayload,
42
+ ZaiSubscriptionPayload,
50
43
  } from "./types.js";
51
44
  import { resolveUsageTarget } from "./usage-targets.js";
52
45
 
@@ -60,20 +53,20 @@ const VERCEL_AI_GATEWAY_CREDITS_URL = "https://ai-gateway.vercel.sh/v1/credits";
60
53
  const OPENCODE_GO_USAGE_URL = "https://opencode.ai/zen/go/v1/usage";
61
54
  const KIMI_CODING_USAGE_URL = "https://api.kimi.com/coding/v1/usages";
62
55
  const MINIMAX_API_ROOTS = Object.freeze({
63
- minimax: "https://api.minimax.io",
64
- "minimax-cn": "https://api.minimaxi.com",
56
+ minimax: "https://api.minimax.io",
57
+ "minimax-cn": "https://api.minimaxi.com",
65
58
  });
66
59
  const MOONSHOT_BALANCE_URLS = Object.freeze({
67
- moonshotai: "https://api.moonshot.ai/v1/users/me/balance",
68
- "moonshotai-cn": "https://api.moonshot.cn/v1/users/me/balance",
60
+ moonshotai: "https://api.moonshot.ai/v1/users/me/balance",
61
+ "moonshotai-cn": "https://api.moonshot.cn/v1/users/me/balance",
69
62
  });
70
63
  const SHARED_MOONSHOT_ENV_VAR = "MOONSHOT_API_KEY";
71
64
  const XAI_USER_URL = "https://cli-chat-proxy.grok.com/v1/user?include=subscription";
72
65
  const XAI_BILLING_URL = "https://cli-chat-proxy.grok.com/v1/billing?format=credits";
73
66
  const XAI_CLIENT_HEADERS = Object.freeze({
74
- "X-XAI-Token-Auth": "xai-grok-cli",
75
- "x-grok-client-version": "1.0.10",
76
- "x-grok-client-mode": "interactive",
67
+ "X-XAI-Token-Auth": "xai-grok-cli",
68
+ "x-grok-client-version": "1.0.10",
69
+ "x-grok-client-mode": "interactive",
77
70
  });
78
71
  const MAX_SUCCESS_BODY_BYTES = 64 * 1024;
79
72
  const MAX_ERROR_BODY_BYTES = 4 * 1024;
@@ -81,202 +74,189 @@ const MAX_ERROR_BODY_BYTES = 4 * 1024;
81
74
  export const AUTH_FINGERPRINT_SALT = randomBytes(32);
82
75
 
83
76
  export const SUPPORTED_ADAPTERS: readonly UsageProviderAdapter[] = [
84
- {
85
- id: "baseten",
86
- displayName: "Baseten",
87
- semantics: { kind: "api-key", label: "Organization Model APIs spend" },
88
- async query(auth, signal, timeoutMs, guard) {
89
- if (!guard) throw new Error("Baseten billing usage requires request-boundary revalidation.");
90
- const startedAt = Date.now();
91
- await guard();
92
- const windowAt = Date.now();
93
- const payload = (await fetchProviderJson(
94
- basetenBillingUsageUrl(windowAt),
95
- auth,
96
- signal,
97
- remainingTimeout(timeoutMs, startedAt, "fetching Baseten billing usage"),
98
- "Baseten billing usage endpoint",
99
- { redirect: "error" },
100
- )) as BasetenBillingUsagePayload;
101
- await guard();
102
- return normalizeBasetenBillingUsagePayload(payload, Date.now());
103
- },
104
- },
105
- {
106
- id: "openai-codex",
107
- displayName: "OpenAI Codex",
108
- semantics: {
109
- kind: "consumer-subscription",
110
- label: "ChatGPT subscription limits",
111
- },
112
- async query(auth, signal, timeoutMs) {
113
- const payload = await fetchProviderJson(
114
- CODEX_USAGE_URL,
115
- auth,
116
- signal,
117
- timeoutMs,
118
- "Codex usage endpoint",
119
- );
120
- return normalizeCodexBackendPayload(payload as CodexBackendPayload, Date.now());
121
- },
122
- },
123
- {
124
- id: "deepseek",
125
- displayName: "DeepSeek",
126
- semantics: { kind: "api-key", label: "DeepSeek API balance" },
127
- async query(auth, signal, timeoutMs, guard) {
128
- if (!guard) throw new Error("DeepSeek API balance requires request-boundary revalidation.");
129
- const startedAt = Date.now();
130
- await guard();
131
- const remainingMs = timeoutMs - (Date.now() - startedAt);
132
- if (remainingMs <= 0) throw new Error("Timed out while revalidating DeepSeek runtime auth.");
133
- const payload = await fetchProviderJson(
134
- DEEPSEEK_BALANCE_URL,
135
- auth,
136
- signal,
137
- remainingMs,
138
- "DeepSeek API balance endpoint",
139
- { redirect: "error" },
140
- );
141
- return normalizeDeepSeekBalancePayload(payload as DeepSeekBalancePayload, Date.now());
142
- },
143
- },
144
- {
145
- id: "github-copilot",
146
- displayName: "GitHub Copilot",
147
- semantics: {
148
- kind: "consumer-subscription",
149
- label: "GitHub Copilot account allowance",
150
- },
151
- async query(auth, signal, timeoutMs) {
152
- const payload = await fetchProviderJson(
153
- GITHUB_COPILOT_USAGE_URL,
154
- auth,
155
- signal,
156
- timeoutMs,
157
- "GitHub Copilot usage endpoint",
158
- );
159
- return normalizeGitHubCopilotUsagePayload(payload as GitHubCopilotUsagePayload, Date.now());
160
- },
161
- },
162
- {
163
- id: "openrouter",
164
- displayName: "OpenRouter",
165
- semantics: { kind: "api-key", label: "API-key spend limits" },
166
- async query(auth, signal, timeoutMs) {
167
- const payload = await fetchProviderJson(
168
- OPENROUTER_KEY_URL,
169
- auth,
170
- signal,
171
- timeoutMs,
172
- "OpenRouter key endpoint",
173
- );
174
- return normalizeOpenRouterKeyPayload(payload as OpenRouterKeyPayload, Date.now());
175
- },
176
- },
177
- {
178
- id: "vercel-ai-gateway",
179
- displayName: "Vercel AI Gateway",
180
- semantics: { kind: "api-key", label: "AI Gateway credits and lifetime spend" },
181
- async query(auth, signal, timeoutMs, guard) {
182
- if (!guard)
183
- throw new Error("Vercel AI Gateway usage requires request-boundary revalidation.");
184
- const startedAt = Date.now();
185
- await guard();
186
- const payload = (await fetchProviderJson(
187
- VERCEL_AI_GATEWAY_CREDITS_URL,
188
- auth,
189
- signal,
190
- remainingTimeout(timeoutMs, startedAt, "fetching Vercel AI Gateway credits"),
191
- "Vercel AI Gateway credits endpoint",
192
- { redirect: "error" },
193
- )) as VercelAIGatewayCreditsPayload;
194
- await guard();
195
- return normalizeVercelAIGatewayCreditsPayload(payload, Date.now());
196
- },
197
- },
198
- createFireworksAdapter(fetchProviderJson),
199
- {
200
- id: "opencode-go",
201
- displayName: "OpenCode Go",
202
- semantics: { kind: "consumer-subscription", label: "OpenCode Zen plan usage" },
203
- async query(auth, signal, timeoutMs) {
204
- const payload = await fetchProviderJson(
205
- OPENCODE_GO_USAGE_URL,
206
- auth,
207
- signal,
208
- timeoutMs,
209
- "OpenCode Zen usage endpoint",
210
- );
211
- return normalizeOpenCodeZenPayload(payload as OpenCodeZenPayload, Date.now());
212
- },
213
- },
214
- {
215
- id: "kimi-coding",
216
- displayName: "Kimi For Coding",
217
- semantics: { kind: "consumer-subscription", label: "Kimi Coding Plan usage" },
218
- async query(auth, signal, timeoutMs) {
219
- const payload = await fetchProviderJson(
220
- KIMI_CODING_USAGE_URL,
221
- auth,
222
- signal,
223
- timeoutMs,
224
- "Kimi Coding usage endpoint",
225
- { redirect: "error" },
226
- );
227
- return normalizeKimiCodingUsagePayload(payload as KimiCodingUsagePayload, Date.now());
228
- },
229
- },
230
- {
231
- id: "minimax",
232
- displayName: "MiniMax",
233
- semantics: { kind: "consumer-subscription", label: "MiniMax usage" },
234
- async query(auth, signal, timeoutMs, guard) {
235
- return queryMiniMaxUsage("minimax", auth, signal, timeoutMs, guard);
236
- },
237
- },
238
- {
239
- id: "minimax-cn",
240
- displayName: "MiniMax CN",
241
- semantics: { kind: "consumer-subscription", label: "MiniMax usage" },
242
- async query(auth, signal, timeoutMs, guard) {
243
- return queryMiniMaxUsage("minimax-cn", auth, signal, timeoutMs, guard);
244
- },
245
- },
246
- {
247
- id: "moonshotai",
248
- displayName: "Moonshot AI",
249
- semantics: { kind: "api-key", label: "Moonshot API account balance" },
250
- async query(auth, signal, timeoutMs, guard) {
251
- return queryMoonshotBalance("moonshotai", auth, signal, timeoutMs, guard);
252
- },
253
- },
254
- {
255
- id: "moonshotai-cn",
256
- displayName: "Moonshot AI CN",
257
- semantics: { kind: "api-key", label: "Moonshot API account balance" },
258
- async query(auth, signal, timeoutMs, guard) {
259
- return queryMoonshotBalance("moonshotai-cn", auth, signal, timeoutMs, guard);
260
- },
261
- },
262
- {
263
- id: "zai",
264
- displayName: "Z.AI",
265
- invalidateCacheOnFailure: true,
266
- semantics: { kind: "consumer-subscription", label: "GLM Coding Plan usage" },
267
- async query(auth, signal, timeoutMs, guard) {
268
- return queryZaiUsage("zai", "Z.AI", auth, signal, timeoutMs, guard);
269
- },
270
- },
271
- {
272
- id: "zai-coding-cn",
273
- displayName: "Z.AI Coding CN",
274
- invalidateCacheOnFailure: true,
275
- semantics: { kind: "consumer-subscription", label: "GLM Coding Plan usage" },
276
- async query(auth, signal, timeoutMs, guard) {
277
- return queryZaiUsage("zai-coding-cn", "Z.AI Coding CN", auth, signal, timeoutMs, guard);
278
- },
279
- },
77
+ {
78
+ id: "baseten",
79
+ displayName: "Baseten",
80
+ semantics: { kind: "api-key", label: "Organization Model APIs spend" },
81
+ async query(auth, signal, timeoutMs, guard) {
82
+ if (!guard) throw new Error("Baseten billing usage requires request-boundary revalidation.");
83
+ const startedAt = Date.now();
84
+ await guard();
85
+ const windowAt = Date.now();
86
+ const payload = (await fetchProviderJson(
87
+ basetenBillingUsageUrl(windowAt),
88
+ auth,
89
+ signal,
90
+ remainingTimeout(timeoutMs, startedAt, "fetching Baseten billing usage"),
91
+ "Baseten billing usage endpoint",
92
+ { redirect: "error" },
93
+ )) as BasetenBillingUsagePayload;
94
+ await guard();
95
+ return normalizeBasetenBillingUsagePayload(payload, Date.now());
96
+ },
97
+ },
98
+ {
99
+ id: "openai-codex",
100
+ displayName: "OpenAI Codex",
101
+ semantics: {
102
+ kind: "consumer-subscription",
103
+ label: "ChatGPT subscription limits",
104
+ },
105
+ async query(auth, signal, timeoutMs) {
106
+ const payload = await fetchProviderJson(CODEX_USAGE_URL, auth, signal, timeoutMs, "Codex usage endpoint");
107
+ return normalizeCodexBackendPayload(payload as CodexBackendPayload, Date.now());
108
+ },
109
+ },
110
+ {
111
+ id: "deepseek",
112
+ displayName: "DeepSeek",
113
+ semantics: { kind: "api-key", label: "DeepSeek API balance" },
114
+ async query(auth, signal, timeoutMs, guard) {
115
+ if (!guard) throw new Error("DeepSeek API balance requires request-boundary revalidation.");
116
+ const startedAt = Date.now();
117
+ await guard();
118
+ const remainingMs = timeoutMs - (Date.now() - startedAt);
119
+ if (remainingMs <= 0) throw new Error("Timed out while revalidating DeepSeek runtime auth.");
120
+ const payload = await fetchProviderJson(
121
+ DEEPSEEK_BALANCE_URL,
122
+ auth,
123
+ signal,
124
+ remainingMs,
125
+ "DeepSeek API balance endpoint",
126
+ { redirect: "error" },
127
+ );
128
+ return normalizeDeepSeekBalancePayload(payload as DeepSeekBalancePayload, Date.now());
129
+ },
130
+ },
131
+ {
132
+ id: "github-copilot",
133
+ displayName: "GitHub Copilot",
134
+ semantics: {
135
+ kind: "consumer-subscription",
136
+ label: "GitHub Copilot account allowance",
137
+ },
138
+ async query(auth, signal, timeoutMs) {
139
+ const payload = await fetchProviderJson(
140
+ GITHUB_COPILOT_USAGE_URL,
141
+ auth,
142
+ signal,
143
+ timeoutMs,
144
+ "GitHub Copilot usage endpoint",
145
+ );
146
+ return normalizeGitHubCopilotUsagePayload(payload as GitHubCopilotUsagePayload, Date.now());
147
+ },
148
+ },
149
+ {
150
+ id: "openrouter",
151
+ displayName: "OpenRouter",
152
+ semantics: { kind: "api-key", label: "API-key spend limits" },
153
+ async query(auth, signal, timeoutMs) {
154
+ const payload = await fetchProviderJson(OPENROUTER_KEY_URL, auth, signal, timeoutMs, "OpenRouter key endpoint");
155
+ return normalizeOpenRouterKeyPayload(payload as OpenRouterKeyPayload, Date.now());
156
+ },
157
+ },
158
+ {
159
+ id: "vercel-ai-gateway",
160
+ displayName: "Vercel AI Gateway",
161
+ semantics: { kind: "api-key", label: "AI Gateway credits and lifetime spend" },
162
+ async query(auth, signal, timeoutMs, guard) {
163
+ if (!guard) throw new Error("Vercel AI Gateway usage requires request-boundary revalidation.");
164
+ const startedAt = Date.now();
165
+ await guard();
166
+ const payload = (await fetchProviderJson(
167
+ VERCEL_AI_GATEWAY_CREDITS_URL,
168
+ auth,
169
+ signal,
170
+ remainingTimeout(timeoutMs, startedAt, "fetching Vercel AI Gateway credits"),
171
+ "Vercel AI Gateway credits endpoint",
172
+ { redirect: "error" },
173
+ )) as VercelAIGatewayCreditsPayload;
174
+ await guard();
175
+ return normalizeVercelAIGatewayCreditsPayload(payload, Date.now());
176
+ },
177
+ },
178
+ createFireworksAdapter(fetchProviderJson),
179
+ {
180
+ id: "opencode-go",
181
+ displayName: "OpenCode Go",
182
+ semantics: { kind: "consumer-subscription", label: "OpenCode Zen plan usage" },
183
+ async query(auth, signal, timeoutMs) {
184
+ const payload = await fetchProviderJson(
185
+ OPENCODE_GO_USAGE_URL,
186
+ auth,
187
+ signal,
188
+ timeoutMs,
189
+ "OpenCode Zen usage endpoint",
190
+ );
191
+ return normalizeOpenCodeZenPayload(payload as OpenCodeZenPayload, Date.now());
192
+ },
193
+ },
194
+ {
195
+ id: "kimi-coding",
196
+ displayName: "Kimi For Coding",
197
+ semantics: { kind: "consumer-subscription", label: "Kimi Coding Plan usage" },
198
+ async query(auth, signal, timeoutMs) {
199
+ const payload = await fetchProviderJson(
200
+ KIMI_CODING_USAGE_URL,
201
+ auth,
202
+ signal,
203
+ timeoutMs,
204
+ "Kimi Coding usage endpoint",
205
+ { redirect: "error" },
206
+ );
207
+ return normalizeKimiCodingUsagePayload(payload as KimiCodingUsagePayload, Date.now());
208
+ },
209
+ },
210
+ {
211
+ id: "minimax",
212
+ displayName: "MiniMax",
213
+ semantics: { kind: "consumer-subscription", label: "MiniMax usage" },
214
+ async query(auth, signal, timeoutMs, guard) {
215
+ return queryMiniMaxUsage("minimax", auth, signal, timeoutMs, guard);
216
+ },
217
+ },
218
+ {
219
+ id: "minimax-cn",
220
+ displayName: "MiniMax CN",
221
+ semantics: { kind: "consumer-subscription", label: "MiniMax usage" },
222
+ async query(auth, signal, timeoutMs, guard) {
223
+ return queryMiniMaxUsage("minimax-cn", auth, signal, timeoutMs, guard);
224
+ },
225
+ },
226
+ {
227
+ id: "moonshotai",
228
+ displayName: "Moonshot AI",
229
+ semantics: { kind: "api-key", label: "Moonshot API account balance" },
230
+ async query(auth, signal, timeoutMs, guard) {
231
+ return queryMoonshotBalance("moonshotai", auth, signal, timeoutMs, guard);
232
+ },
233
+ },
234
+ {
235
+ id: "moonshotai-cn",
236
+ displayName: "Moonshot AI CN",
237
+ semantics: { kind: "api-key", label: "Moonshot API account balance" },
238
+ async query(auth, signal, timeoutMs, guard) {
239
+ return queryMoonshotBalance("moonshotai-cn", auth, signal, timeoutMs, guard);
240
+ },
241
+ },
242
+ {
243
+ id: "zai",
244
+ displayName: "Z.AI",
245
+ invalidateCacheOnFailure: true,
246
+ semantics: { kind: "consumer-subscription", label: "GLM Coding Plan usage" },
247
+ async query(auth, signal, timeoutMs, guard) {
248
+ return queryZaiUsage("zai", "Z.AI", auth, signal, timeoutMs, guard);
249
+ },
250
+ },
251
+ {
252
+ id: "zai-coding-cn",
253
+ displayName: "Z.AI Coding CN",
254
+ invalidateCacheOnFailure: true,
255
+ semantics: { kind: "consumer-subscription", label: "GLM Coding Plan usage" },
256
+ async query(auth, signal, timeoutMs, guard) {
257
+ return queryZaiUsage("zai-coding-cn", "Z.AI Coding CN", auth, signal, timeoutMs, guard);
258
+ },
259
+ },
280
260
  ];
281
261
 
282
262
  // Reviewed contract pins:
@@ -290,857 +270,797 @@ export const SUPPORTED_ADAPTERS: readonly UsageProviderAdapter[] = [
290
270
  // 723dd2aa22d17be35617463837dc47cda008d90e.
291
271
  // x-userid remains attached only to billing to bind the proxy-canonical identity as Grok Build does.
292
272
  export const XAI_ADAPTER: UsageProviderAdapter = {
293
- id: "xai",
294
- displayName: "xAI",
295
- semantics: {
296
- kind: "consumer-subscription",
297
- label: "xAI consumer subscription usage",
298
- },
299
- publishesStatusline: false,
300
- async query(auth, signal, timeoutMs, guard) {
301
- if (!guard) throw new Error("xAI usage requires request-boundary revalidation.");
302
- const startedAt = Date.now();
303
- const clientAuth = {
304
- ...auth,
305
- headers: { ...auth.headers, ...XAI_CLIENT_HEADERS },
306
- };
307
- await guard();
308
- const userPayload = (await fetchProviderJson(
309
- XAI_USER_URL,
310
- clientAuth,
311
- signal,
312
- remainingTimeout(timeoutMs, startedAt),
313
- "xAI consumer identity endpoint",
314
- { redirect: "error", userAgent: false },
315
- )) as XaiUserPayload;
316
- await guard();
317
- const userId = validatedXaiUserId(userPayload.userId);
318
- const billingAuth = {
319
- ...clientAuth,
320
- headers: { ...clientAuth.headers, "x-userid": userId },
321
- secrets: [...clientAuth.secrets, userId],
322
- };
323
- await guard();
324
- const billingPayload = (await fetchProviderJson(
325
- XAI_BILLING_URL,
326
- billingAuth,
327
- signal,
328
- remainingTimeout(timeoutMs, startedAt),
329
- "xAI consumer billing endpoint",
330
- { redirect: "error", userAgent: false },
331
- )) as XaiBillingPayload;
332
- await guard();
333
- return normalizeXaiBillingPayload(billingPayload, userPayload.subscriptionTier, Date.now());
334
- },
273
+ id: "xai",
274
+ displayName: "xAI",
275
+ semantics: {
276
+ kind: "consumer-subscription",
277
+ label: "xAI consumer subscription usage",
278
+ },
279
+ publishesStatusline: false,
280
+ async query(auth, signal, timeoutMs, guard) {
281
+ if (!guard) throw new Error("xAI usage requires request-boundary revalidation.");
282
+ const startedAt = Date.now();
283
+ const clientAuth = {
284
+ ...auth,
285
+ headers: { ...auth.headers, ...XAI_CLIENT_HEADERS },
286
+ };
287
+ await guard();
288
+ const userPayload = (await fetchProviderJson(
289
+ XAI_USER_URL,
290
+ clientAuth,
291
+ signal,
292
+ remainingTimeout(timeoutMs, startedAt),
293
+ "xAI consumer identity endpoint",
294
+ { redirect: "error", userAgent: false },
295
+ )) as XaiUserPayload;
296
+ await guard();
297
+ const userId = validatedXaiUserId(userPayload.userId);
298
+ const billingAuth = {
299
+ ...clientAuth,
300
+ headers: { ...clientAuth.headers, "x-userid": userId },
301
+ secrets: [...clientAuth.secrets, userId],
302
+ };
303
+ await guard();
304
+ const billingPayload = (await fetchProviderJson(
305
+ XAI_BILLING_URL,
306
+ billingAuth,
307
+ signal,
308
+ remainingTimeout(timeoutMs, startedAt),
309
+ "xAI consumer billing endpoint",
310
+ { redirect: "error", userAgent: false },
311
+ )) as XaiBillingPayload;
312
+ await guard();
313
+ return normalizeXaiBillingPayload(billingPayload, userPayload.subscriptionTier, Date.now());
314
+ },
335
315
  };
336
316
 
337
317
  export function usageAdapters(): readonly UsageProviderAdapter[] {
338
- return [...SUPPORTED_ADAPTERS, XAI_ADAPTER];
318
+ return [...SUPPORTED_ADAPTERS, XAI_ADAPTER];
339
319
  }
340
320
 
341
- export function adapterForProvider(
342
- providerId: string | undefined,
343
- ): UsageProviderAdapter | undefined {
344
- return usageAdapters().find((adapter) => adapter.id === providerId);
321
+ export function adapterForProvider(providerId: string | undefined): UsageProviderAdapter | undefined {
322
+ return usageAdapters().find((adapter) => adapter.id === providerId);
345
323
  }
346
324
 
347
325
  export function isStaleExtensionContextError(error: unknown): boolean {
348
- return (
349
- error instanceof Error &&
350
- error.message.includes("This extension ctx is stale after session replacement or reload")
351
- );
326
+ return (
327
+ error instanceof Error && error.message.includes("This extension ctx is stale after session replacement or reload")
328
+ );
352
329
  }
353
330
 
354
331
  export async function resolveUsageAuth(
355
- ctx: ExtensionContext,
356
- adapter: UsageProviderAdapter,
357
- salt: Uint8Array = AUTH_FINGERPRINT_SALT,
358
- credentialReader: StoredCredentialReader = readStoredCredential,
359
- candidateReader?: OAuthCredentialCandidateReader,
332
+ ctx: ExtensionContext,
333
+ adapter: UsageProviderAdapter,
334
+ salt: Uint8Array = AUTH_FINGERPRINT_SALT,
335
+ credentialReader: StoredCredentialReader = readStoredCredential,
336
+ candidateReader?: OAuthCredentialCandidateReader,
360
337
  ): Promise<ResolvedUsageAuth | undefined> {
361
- if (ctx.model?.provider === adapter.id && !hasOfficialOrigin(ctx.model, adapter.id)) {
362
- throw new Error(
363
- `${adapter.displayName} usage cannot send a custom provider base URL credential to the official usage endpoint.`,
364
- );
365
- }
338
+ if (ctx.model?.provider === adapter.id && !hasOfficialOrigin(ctx.model, adapter.id)) {
339
+ throw new Error(
340
+ `${adapter.displayName} usage cannot send a custom provider base URL credential to the official usage endpoint.`,
341
+ );
342
+ }
366
343
 
367
- const model = candidateModels(ctx, adapter.id).find((candidate) =>
368
- hasOfficialOrigin(candidate, adapter.id),
369
- );
370
- if (!model) return undefined;
371
- // SAFETY: Pi exposes the required auth methods at runtime, and checks below narrow them before use.
372
- const registry = ctx.modelRegistry as unknown as UsageAuthRegistry;
373
- const provider = registry.getProvider?.(adapter.id);
374
- if (provider?.baseUrl && !hasOfficialUrlOrigin(provider.baseUrl, adapter.id)) {
375
- throw new Error(
376
- `${adapter.displayName} usage cannot send an overridden provider credential to the official usage endpoint.`,
377
- );
378
- }
379
- let modelAuth: RequestAuth | undefined;
380
- const currentModel = ctx.model?.provider === adapter.id ? ctx.model : undefined;
381
- const resolveCurrentModelAuth = async (): Promise<RequestAuth | undefined> => {
382
- if (!currentModel || typeof registry.getApiKeyAndHeaders !== "function") return undefined;
383
- const result = await registry.getApiKeyAndHeaders(currentModel);
384
- if (!result.ok) throw new Error(redactUsageError(result.error));
385
- return authorizationFrom(result) ? result : undefined;
386
- };
387
- const resolveSelectedAuthLast = ["deepseek", "minimax", "minimax-cn"].includes(adapter.id);
388
- if (!resolveSelectedAuthLast) modelAuth = await resolveCurrentModelAuth();
389
- if (typeof registry.getProviderAuth !== "function") {
390
- throw new Error("pi-usage requires Pi 0.81.0 or newer to validate resolved provider auth.");
391
- }
392
- if (!moonshotProviderAuthIsAllowed(ctx, adapter.id)) return undefined;
393
- const providerResult = await registry.getProviderAuth(adapter.id);
394
- if (!moonshotProviderAuthIsAllowed(ctx, adapter.id)) return undefined;
395
- if (
396
- providerResult?.auth.baseUrl &&
397
- !hasOfficialUrlOrigin(providerResult.auth.baseUrl, adapter.id)
398
- ) {
399
- throw new Error(
400
- `${adapter.displayName} usage cannot send a proxy-resolved credential to the official usage endpoint.`,
401
- );
402
- }
403
- // Providers with credential-change retries read selected-model auth last so a rotation during
404
- // provider-origin validation cannot leave the earlier credential queued for the usage request.
405
- if (resolveSelectedAuthLast) modelAuth = await resolveCurrentModelAuth();
406
- if (modelAuth?.baseUrl && !hasOfficialUrlOrigin(modelAuth.baseUrl, adapter.id)) {
407
- throw new Error(
408
- `${adapter.displayName} usage cannot send model-resolved proxy credentials to the official usage endpoint.`,
409
- );
410
- }
411
- const auth = modelAuth ?? providerResult?.auth;
412
- if (!auth) return undefined;
413
- const finalize = (resolved: ResolvedUsageAuth): ResolvedUsageAuth => {
414
- const preservedAuth = { ...(providerResult?.auth ?? auth) };
415
- const env = providerResult?.env ?? modelAuth?.env;
416
- const source = providerResult?.source;
417
- const effectiveBaseUrl =
418
- modelAuth?.baseUrl ?? providerResult?.auth.baseUrl ?? provider?.baseUrl ?? model.baseUrl;
419
- const redactionInputs = [
420
- preservedAuth.apiKey,
421
- ...Object.values(preservedAuth.headers ?? {}),
422
- ...Object.values(env ?? {}),
423
- modelAuth?.apiKey,
424
- ...Object.values(modelAuth?.headers ?? {}),
425
- ].filter((value): value is string => typeof value === "string" && value.length > 0);
426
- return {
427
- ...resolved,
428
- auth: preservedAuth,
429
- ...(env ? { env: { ...env } } : {}),
430
- ...(source ? { source } : {}),
431
- effectiveBaseUrl,
432
- secrets: [...new Set([...resolved.secrets, ...redactionInputs])],
433
- fingerprint: fingerprintResolvedAuth(
434
- {
435
- apiKey: resolved.apiKey,
436
- headers: resolved.headers,
437
- baseUrl: effectiveBaseUrl,
438
- env,
439
- source,
440
- providerAuth: preservedAuth,
441
- },
442
- salt,
443
- ),
444
- };
445
- };
446
- if (adapter.id === "github-copilot") {
447
- const offered = candidateReader
448
- ? candidateReader(ctx, adapter.id)
449
- : fallbackOAuthCredentialCandidates(adapter.id, credentialReader);
450
- if (!offered.ok) {
451
- throw new Error("GitHub Copilot OAuth credential discovery failed closed.");
452
- }
453
- return finalize(
454
- resolveGitHubCopilotUsageAuth(
455
- auth,
456
- model,
457
- salt,
458
- offered.candidates,
459
- offered.offeredCount === 0,
460
- ),
461
- );
462
- }
463
- if (adapter.id === "xai") {
464
- const offered = candidateReader
465
- ? candidateReader(ctx, adapter.id)
466
- : fallbackOAuthCredentialCandidates(adapter.id, credentialReader);
467
- if (!offered.ok) throw new Error("xAI OAuth credential discovery failed closed.");
468
- return finalize(resolveXaiUsageAuth(auth, model, salt, offered.candidates));
469
- }
470
- if (adapter.id === "deepseek") {
471
- const resolvedAuthorization = authorizationFrom(auth);
472
- const access = bearerToken(resolvedAuthorization);
473
- if (!access) throw new Error("DeepSeek API balance requires Bearer authentication.");
474
- const authorization = `Bearer ${access}`;
475
- const headers = { Authorization: authorization };
476
- return finalize({
477
- apiKey: access,
478
- headers,
479
- fingerprint: "",
480
- secrets: [
481
- access,
482
- auth.apiKey,
483
- headerValue(auth.headers, "Authorization"),
484
- resolvedAuthorization,
485
- authorization,
486
- ].filter((value): value is string => Boolean(value)),
487
- model,
488
- });
489
- }
490
- const authorization = authorizationFrom(auth);
491
- if (!authorization) return undefined;
492
- const headers = { Authorization: authorization };
493
- const secrets = [auth.apiKey, headerValue(auth.headers, "Authorization"), authorization].filter(
494
- (value): value is string => Boolean(value),
495
- );
496
- return finalize({
497
- apiKey: auth.apiKey,
498
- headers,
499
- fingerprint: "",
500
- secrets,
501
- model,
502
- });
344
+ const model = candidateModels(ctx, adapter.id).find((candidate) => hasOfficialOrigin(candidate, adapter.id));
345
+ if (!model) return undefined;
346
+ // SAFETY: Pi exposes the required auth methods at runtime, and checks below narrow them before use.
347
+ const registry = ctx.modelRegistry as unknown as UsageAuthRegistry;
348
+ const provider = registry.getProvider?.(adapter.id);
349
+ if (provider?.baseUrl && !hasOfficialUrlOrigin(provider.baseUrl, adapter.id)) {
350
+ throw new Error(
351
+ `${adapter.displayName} usage cannot send an overridden provider credential to the official usage endpoint.`,
352
+ );
353
+ }
354
+ let modelAuth: RequestAuth | undefined;
355
+ const currentModel = ctx.model?.provider === adapter.id ? ctx.model : undefined;
356
+ const resolveCurrentModelAuth = async (): Promise<RequestAuth | undefined> => {
357
+ if (!currentModel || typeof registry.getApiKeyAndHeaders !== "function") return undefined;
358
+ const result = await registry.getApiKeyAndHeaders(currentModel);
359
+ if (!result.ok) throw new Error(redactUsageError(result.error));
360
+ return authorizationFrom(result) ? result : undefined;
361
+ };
362
+ const resolveSelectedAuthLast = ["deepseek", "minimax", "minimax-cn"].includes(adapter.id);
363
+ if (!resolveSelectedAuthLast) modelAuth = await resolveCurrentModelAuth();
364
+ if (typeof registry.getProviderAuth !== "function") {
365
+ throw new Error("pi-usage requires Pi 0.81.0 or newer to validate resolved provider auth.");
366
+ }
367
+ if (!moonshotProviderAuthIsAllowed(ctx, adapter.id)) return undefined;
368
+ const providerResult = await registry.getProviderAuth(adapter.id);
369
+ if (!moonshotProviderAuthIsAllowed(ctx, adapter.id)) return undefined;
370
+ if (providerResult?.auth.baseUrl && !hasOfficialUrlOrigin(providerResult.auth.baseUrl, adapter.id)) {
371
+ throw new Error(
372
+ `${adapter.displayName} usage cannot send a proxy-resolved credential to the official usage endpoint.`,
373
+ );
374
+ }
375
+ // Providers with credential-change retries read selected-model auth last so a rotation during
376
+ // provider-origin validation cannot leave the earlier credential queued for the usage request.
377
+ if (resolveSelectedAuthLast) modelAuth = await resolveCurrentModelAuth();
378
+ if (modelAuth?.baseUrl && !hasOfficialUrlOrigin(modelAuth.baseUrl, adapter.id)) {
379
+ throw new Error(
380
+ `${adapter.displayName} usage cannot send model-resolved proxy credentials to the official usage endpoint.`,
381
+ );
382
+ }
383
+ const auth = modelAuth ?? providerResult?.auth;
384
+ if (!auth) return undefined;
385
+ const finalize = (resolved: ResolvedUsageAuth): ResolvedUsageAuth => {
386
+ const preservedAuth = { ...(providerResult?.auth ?? auth) };
387
+ const env = providerResult?.env ?? modelAuth?.env;
388
+ const source = providerResult?.source;
389
+ const effectiveBaseUrl = modelAuth?.baseUrl ?? providerResult?.auth.baseUrl ?? provider?.baseUrl ?? model.baseUrl;
390
+ const redactionInputs = [
391
+ preservedAuth.apiKey,
392
+ ...Object.values(preservedAuth.headers ?? {}),
393
+ ...Object.values(env ?? {}),
394
+ modelAuth?.apiKey,
395
+ ...Object.values(modelAuth?.headers ?? {}),
396
+ ].filter((value): value is string => typeof value === "string" && value.length > 0);
397
+ return {
398
+ ...resolved,
399
+ auth: preservedAuth,
400
+ ...(env ? { env: { ...env } } : {}),
401
+ ...(source ? { source } : {}),
402
+ effectiveBaseUrl,
403
+ secrets: [...new Set([...resolved.secrets, ...redactionInputs])],
404
+ fingerprint: fingerprintResolvedAuth(
405
+ {
406
+ apiKey: resolved.apiKey,
407
+ headers: resolved.headers,
408
+ baseUrl: effectiveBaseUrl,
409
+ env,
410
+ source,
411
+ providerAuth: preservedAuth,
412
+ },
413
+ salt,
414
+ ),
415
+ };
416
+ };
417
+ if (adapter.id === "github-copilot") {
418
+ const offered = candidateReader
419
+ ? candidateReader(ctx, adapter.id)
420
+ : fallbackOAuthCredentialCandidates(adapter.id, credentialReader);
421
+ if (!offered.ok) {
422
+ throw new Error("GitHub Copilot OAuth credential discovery failed closed.");
423
+ }
424
+ return finalize(resolveGitHubCopilotUsageAuth(auth, model, salt, offered.candidates, offered.offeredCount === 0));
425
+ }
426
+ if (adapter.id === "xai") {
427
+ const offered = candidateReader
428
+ ? candidateReader(ctx, adapter.id)
429
+ : fallbackOAuthCredentialCandidates(adapter.id, credentialReader);
430
+ if (!offered.ok) throw new Error("xAI OAuth credential discovery failed closed.");
431
+ return finalize(resolveXaiUsageAuth(auth, model, salt, offered.candidates));
432
+ }
433
+ if (adapter.id === "deepseek") {
434
+ const resolvedAuthorization = authorizationFrom(auth);
435
+ const access = bearerToken(resolvedAuthorization);
436
+ if (!access) throw new Error("DeepSeek API balance requires Bearer authentication.");
437
+ const authorization = `Bearer ${access}`;
438
+ const headers = { Authorization: authorization };
439
+ return finalize({
440
+ apiKey: access,
441
+ headers,
442
+ fingerprint: "",
443
+ secrets: [
444
+ access,
445
+ auth.apiKey,
446
+ headerValue(auth.headers, "Authorization"),
447
+ resolvedAuthorization,
448
+ authorization,
449
+ ].filter((value): value is string => Boolean(value)),
450
+ model,
451
+ });
452
+ }
453
+ const authorization = authorizationFrom(auth);
454
+ if (!authorization) return undefined;
455
+ const headers = { Authorization: authorization };
456
+ const secrets = [auth.apiKey, headerValue(auth.headers, "Authorization"), authorization].filter(
457
+ (value): value is string => Boolean(value),
458
+ );
459
+ return finalize({
460
+ apiKey: auth.apiKey,
461
+ headers,
462
+ fingerprint: "",
463
+ secrets,
464
+ model,
465
+ });
503
466
  }
504
467
 
505
468
  export async function queryProviderUsage(
506
- adapter: UsageProviderAdapter,
507
- auth: ResolvedUsageAuth,
508
- signal: AbortSignal,
509
- timeoutMs: number,
510
- guard?: UsageRequestGuard,
511
- targetOrSettings?: string | Readonly<UsageQuerySettings>,
469
+ adapter: UsageProviderAdapter,
470
+ auth: ResolvedUsageAuth,
471
+ signal: AbortSignal,
472
+ timeoutMs: number,
473
+ guard?: UsageRequestGuard,
474
+ targetOrSettings?: string | Readonly<UsageQuerySettings>,
512
475
  ): Promise<UsageReport> {
513
- const startedAt = Date.now();
514
- let targetId =
515
- typeof targetOrSettings === "string"
516
- ? targetOrSettings
517
- : adapter.id === "fireworks"
518
- ? targetOrSettings?.fireworksAccountId
519
- : undefined;
520
- let resolvedLegacyFireworksTarget = false;
521
- try {
522
- if (
523
- adapter.id === "fireworks" &&
524
- typeof targetOrSettings !== "string" &&
525
- adapter.targets &&
526
- guard
527
- ) {
528
- const target = await resolveUsageTarget(
529
- adapter,
530
- auth,
531
- targetId,
532
- signal,
533
- remainingTimeout(timeoutMs, startedAt, "resolving the Fireworks account"),
534
- guard,
535
- );
536
- if (target.kind === "selection-required") {
537
- throw new Error("Fireworks account selection is required.");
538
- }
539
- targetId = target.targetId;
540
- resolvedLegacyFireworksTarget = true;
541
- }
542
- return await adapter.query(
543
- auth,
544
- signal,
545
- resolvedLegacyFireworksTarget
546
- ? remainingTimeout(timeoutMs, startedAt, `querying ${adapter.displayName} usage`)
547
- : timeoutMs,
548
- guard,
549
- targetId,
550
- );
551
- } catch (error) {
552
- if (isStaleExtensionContextError(error) || isAbortError(error)) throw error;
553
- throw new Error(redactUsageError(errorMessage(error), auth.secrets));
554
- }
476
+ const startedAt = Date.now();
477
+ let targetId =
478
+ typeof targetOrSettings === "string"
479
+ ? targetOrSettings
480
+ : adapter.id === "fireworks"
481
+ ? targetOrSettings?.fireworksAccountId
482
+ : undefined;
483
+ let resolvedLegacyFireworksTarget = false;
484
+ try {
485
+ if (adapter.id === "fireworks" && typeof targetOrSettings !== "string" && adapter.targets && guard) {
486
+ const target = await resolveUsageTarget(
487
+ adapter,
488
+ auth,
489
+ targetId,
490
+ signal,
491
+ remainingTimeout(timeoutMs, startedAt, "resolving the Fireworks account"),
492
+ guard,
493
+ );
494
+ if (target.kind === "selection-required") {
495
+ throw new Error("Fireworks account selection is required.");
496
+ }
497
+ targetId = target.targetId;
498
+ resolvedLegacyFireworksTarget = true;
499
+ }
500
+ return await adapter.query(
501
+ auth,
502
+ signal,
503
+ resolvedLegacyFireworksTarget
504
+ ? remainingTimeout(timeoutMs, startedAt, `querying ${adapter.displayName} usage`)
505
+ : timeoutMs,
506
+ guard,
507
+ targetId,
508
+ );
509
+ } catch (error) {
510
+ if (isStaleExtensionContextError(error) || isAbortError(error)) throw error;
511
+ throw new Error(redactUsageError(errorMessage(error), auth.secrets));
512
+ }
555
513
  }
556
514
 
557
515
  export function providerIsConfigured(ctx: ExtensionContext, providerId: string): boolean {
558
- try {
559
- const status = ctx.modelRegistry.getProviderAuthStatus(providerId);
560
- return (
561
- status.configured &&
562
- moonshotProviderAuthSourceIsAllowed(ctx, providerId, status.source, status.label)
563
- );
564
- } catch {
565
- return (
566
- !isMoonshotSiblingProvider(ctx, providerId) && candidateModels(ctx, providerId).length > 0
567
- );
568
- }
516
+ try {
517
+ const status = ctx.modelRegistry.getProviderAuthStatus(providerId);
518
+ return status.configured && moonshotProviderAuthSourceIsAllowed(ctx, providerId, status.source, status.label);
519
+ } catch {
520
+ return !isMoonshotSiblingProvider(ctx, providerId) && candidateModels(ctx, providerId).length > 0;
521
+ }
569
522
  }
570
523
 
571
524
  function moonshotProviderAuthIsAllowed(ctx: ExtensionContext, providerId: string): boolean {
572
- if (!isMoonshotSiblingProvider(ctx, providerId)) return true;
573
- try {
574
- const status = ctx.modelRegistry.getProviderAuthStatus(providerId);
575
- return moonshotProviderAuthSourceIsAllowed(ctx, providerId, status.source, status.label);
576
- } catch {
577
- return false;
578
- }
525
+ if (!isMoonshotSiblingProvider(ctx, providerId)) return true;
526
+ try {
527
+ const status = ctx.modelRegistry.getProviderAuthStatus(providerId);
528
+ return moonshotProviderAuthSourceIsAllowed(ctx, providerId, status.source, status.label);
529
+ } catch {
530
+ return false;
531
+ }
579
532
  }
580
533
 
581
534
  function moonshotProviderAuthSourceIsAllowed(
582
- ctx: ExtensionContext,
583
- providerId: string,
584
- source: string | undefined,
585
- label: string | undefined,
535
+ ctx: ExtensionContext,
536
+ providerId: string,
537
+ source: string | undefined,
538
+ label: string | undefined,
586
539
  ): boolean {
587
- if (!isMoonshotSiblingProvider(ctx, providerId)) return true;
588
- if (source === undefined) return false;
589
- if (source !== "environment") return true;
590
- return (
591
- label !== undefined &&
592
- !label
593
- .split(",")
594
- .map((name) => name.trim())
595
- .includes(SHARED_MOONSHOT_ENV_VAR)
596
- );
540
+ if (!isMoonshotSiblingProvider(ctx, providerId)) return true;
541
+ if (source === undefined) return false;
542
+ if (source !== "environment") return true;
543
+ return (
544
+ label !== undefined &&
545
+ !label
546
+ .split(",")
547
+ .map((name) => name.trim())
548
+ .includes(SHARED_MOONSHOT_ENV_VAR)
549
+ );
597
550
  }
598
551
 
599
552
  function isMoonshotSiblingProvider(ctx: ExtensionContext, providerId: string): boolean {
600
- return (
601
- (providerId === "moonshotai" || providerId === "moonshotai-cn") &&
602
- ctx.model?.provider !== providerId
603
- );
553
+ return (providerId === "moonshotai" || providerId === "moonshotai-cn") && ctx.model?.provider !== providerId;
604
554
  }
605
555
 
606
556
  function candidateModels(ctx: ExtensionContext, providerId: string): PiModel[] {
607
- const candidates: PiModel[] = [];
608
- const seen = new Set<string>();
609
- const add = (model: PiModel | undefined) => {
610
- if (!model || model.provider !== providerId) return;
611
- const key = `${model.provider}/${model.id}`;
612
- if (seen.has(key)) return;
613
- seen.add(key);
614
- candidates.push(model);
615
- };
616
- add(ctx.model);
617
- for (const model of ctx.modelRegistry.getAvailable()) add(model);
618
- for (const model of ctx.modelRegistry.getAll()) add(model);
619
- return candidates;
557
+ const candidates: PiModel[] = [];
558
+ const seen = new Set<string>();
559
+ const add = (model: PiModel | undefined) => {
560
+ if (!model || model.provider !== providerId) return;
561
+ const key = `${model.provider}/${model.id}`;
562
+ if (seen.has(key)) return;
563
+ seen.add(key);
564
+ candidates.push(model);
565
+ };
566
+ add(ctx.model);
567
+ for (const model of ctx.modelRegistry.getAvailable()) add(model);
568
+ for (const model of ctx.modelRegistry.getAll()) add(model);
569
+ return candidates;
620
570
  }
621
571
 
622
572
  export async function fetchProviderJson(
623
- url: string,
624
- auth: ResolvedUsageAuth,
625
- signal: AbortSignal,
626
- timeoutMs: number,
627
- description: string,
628
- request: {
629
- method?: "GET" | "POST";
630
- body?: Record<string, unknown>;
631
- redirect?: RequestRedirect;
632
- userAgent?: boolean;
633
- responseError?: (status: number, text: string) => string | undefined;
634
- } = {},
573
+ url: string,
574
+ auth: ResolvedUsageAuth,
575
+ signal: AbortSignal,
576
+ timeoutMs: number,
577
+ description: string,
578
+ request: {
579
+ method?: "GET" | "POST";
580
+ body?: Record<string, unknown>;
581
+ redirect?: RequestRedirect;
582
+ userAgent?: boolean;
583
+ responseError?: (status: number, text: string) => string | undefined;
584
+ } = {},
635
585
  ): Promise<Record<string, unknown>> {
636
- const controller = new AbortController();
637
- let timedOut = false;
638
- const abortFromCaller = () => controller.abort();
639
- if (signal.aborted) controller.abort();
640
- else signal.addEventListener("abort", abortFromCaller, { once: true });
641
- const timeout = setTimeout(() => {
642
- timedOut = true;
643
- controller.abort();
644
- }, timeoutMs);
645
- try {
646
- const headers = { ...auth.headers };
647
- if (request.userAgent !== false && !hasHeader(headers, "User-Agent")) {
648
- headers["User-Agent"] = "pi-usage";
649
- }
650
- if (request.body && !hasHeader(headers, "Content-Type")) {
651
- headers["Content-Type"] = "application/json";
652
- }
653
- const response = await fetch(url, {
654
- method: request.method ?? "GET",
655
- headers,
656
- ...(request.body ? { body: JSON.stringify(request.body) } : {}),
657
- ...(request.redirect ? { redirect: request.redirect } : {}),
658
- signal: controller.signal,
659
- });
660
- if (response.redirected) throw new Error(`${description} refused a redirected response.`);
661
- if (controller.signal.aborted)
662
- throw Object.assign(new Error("Usage query aborted."), { name: "AbortError" });
663
- const text = await readBoundedResponse(
664
- response,
665
- response.ok ? MAX_SUCCESS_BODY_BYTES : MAX_ERROR_BODY_BYTES,
666
- !response.ok,
667
- description,
668
- controller.signal,
669
- );
670
- if (controller.signal.aborted)
671
- throw Object.assign(new Error("Usage query aborted."), { name: "AbortError" });
672
- const responseError = request.responseError?.(response.status, text);
673
- if (responseError) throw new Error(responseError);
674
- if (!response.ok) {
675
- throw new Error(
676
- `${description} returned ${response.status} ${response.statusText}: ${redactUsageError(text, auth.secrets)}`,
677
- );
678
- }
679
- let parsed: unknown;
680
- try {
681
- parsed = JSON.parse(text) as unknown;
682
- } catch (error) {
683
- throw new Error(`${description} returned invalid JSON: ${errorMessage(error)}`);
684
- }
685
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
686
- throw new Error(`${description} response was not an object.`);
687
- }
688
- return parsed as Record<string, unknown>;
689
- } catch (error) {
690
- if (timedOut) {
691
- throw new Error(`Timed out after ${Math.round(timeoutMs / 1000)}s while fetching usage.`);
692
- }
693
- if (signal.aborted)
694
- throw Object.assign(new Error("Usage query aborted."), { name: "AbortError" });
695
- throw error;
696
- } finally {
697
- clearTimeout(timeout);
698
- signal.removeEventListener("abort", abortFromCaller);
699
- }
586
+ const controller = new AbortController();
587
+ let timedOut = false;
588
+ const abortFromCaller = () => controller.abort();
589
+ if (signal.aborted) controller.abort();
590
+ else signal.addEventListener("abort", abortFromCaller, { once: true });
591
+ const timeout = setTimeout(() => {
592
+ timedOut = true;
593
+ controller.abort();
594
+ }, timeoutMs);
595
+ try {
596
+ const headers = { ...auth.headers };
597
+ if (request.userAgent !== false && !hasHeader(headers, "User-Agent")) {
598
+ headers["User-Agent"] = "pi-usage";
599
+ }
600
+ if (request.body && !hasHeader(headers, "Content-Type")) {
601
+ headers["Content-Type"] = "application/json";
602
+ }
603
+ const response = await fetch(url, {
604
+ method: request.method ?? "GET",
605
+ headers,
606
+ ...(request.body ? { body: JSON.stringify(request.body) } : {}),
607
+ ...(request.redirect ? { redirect: request.redirect } : {}),
608
+ signal: controller.signal,
609
+ });
610
+ if (response.redirected) throw new Error(`${description} refused a redirected response.`);
611
+ if (controller.signal.aborted) throw Object.assign(new Error("Usage query aborted."), { name: "AbortError" });
612
+ const text = await readBoundedResponse(
613
+ response,
614
+ response.ok ? MAX_SUCCESS_BODY_BYTES : MAX_ERROR_BODY_BYTES,
615
+ !response.ok,
616
+ description,
617
+ controller.signal,
618
+ );
619
+ if (controller.signal.aborted) throw Object.assign(new Error("Usage query aborted."), { name: "AbortError" });
620
+ const responseError = request.responseError?.(response.status, text);
621
+ if (responseError) throw new Error(responseError);
622
+ if (!response.ok) {
623
+ throw new Error(
624
+ `${description} returned ${response.status} ${response.statusText}: ${redactUsageError(text, auth.secrets)}`,
625
+ );
626
+ }
627
+ let parsed: unknown;
628
+ try {
629
+ parsed = JSON.parse(text) as unknown;
630
+ } catch (error) {
631
+ throw new Error(`${description} returned invalid JSON: ${errorMessage(error)}`);
632
+ }
633
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
634
+ throw new Error(`${description} response was not an object.`);
635
+ }
636
+ return parsed as Record<string, unknown>;
637
+ } catch (error) {
638
+ if (timedOut) {
639
+ throw new Error(`Timed out after ${Math.round(timeoutMs / 1000)}s while fetching usage.`);
640
+ }
641
+ if (signal.aborted) throw Object.assign(new Error("Usage query aborted."), { name: "AbortError" });
642
+ throw error;
643
+ } finally {
644
+ clearTimeout(timeout);
645
+ signal.removeEventListener("abort", abortFromCaller);
646
+ }
700
647
  }
701
648
 
702
649
  async function readBoundedResponse(
703
- response: Response,
704
- maxBytes: number,
705
- truncateOverflow: boolean,
706
- description: string,
707
- signal: AbortSignal,
650
+ response: Response,
651
+ maxBytes: number,
652
+ truncateOverflow: boolean,
653
+ description: string,
654
+ signal: AbortSignal,
708
655
  ): Promise<string> {
709
- if (!response.body) return "";
710
- const reader = response.body.getReader();
711
- const chunks: Uint8Array[] = [];
712
- let total = 0;
713
- let truncated = false;
714
- const abort = () => void reader.cancel().catch(() => undefined);
715
- if (signal.aborted) abort();
716
- else signal.addEventListener("abort", abort, { once: true });
717
- try {
718
- while (true) {
719
- const { done, value } = await reader.read();
720
- if (done) break;
721
- const remaining = maxBytes - total;
722
- if (value.byteLength > remaining) {
723
- if (remaining > 0) chunks.push(value.subarray(0, remaining));
724
- total = maxBytes;
725
- truncated = true;
726
- await reader.cancel();
727
- break;
728
- }
729
- chunks.push(value);
730
- total += value.byteLength;
731
- }
732
- } finally {
733
- signal.removeEventListener("abort", abort);
734
- reader.releaseLock();
735
- }
736
- if (truncated && !truncateOverflow) {
737
- throw new Error(`${description} response exceeded ${maxBytes} bytes.`);
738
- }
739
- const body = new Uint8Array(total);
740
- let offset = 0;
741
- for (const chunk of chunks) {
742
- body.set(chunk, offset);
743
- offset += chunk.byteLength;
744
- }
745
- const text = new TextDecoder().decode(body);
746
- return truncated ? `${text}…` : text;
656
+ if (!response.body) return "";
657
+ const reader = response.body.getReader();
658
+ const chunks: Uint8Array[] = [];
659
+ let total = 0;
660
+ let truncated = false;
661
+ const abort = () => void reader.cancel().catch(() => undefined);
662
+ if (signal.aborted) abort();
663
+ else signal.addEventListener("abort", abort, { once: true });
664
+ try {
665
+ while (true) {
666
+ const { done, value } = await reader.read();
667
+ if (done) break;
668
+ const remaining = maxBytes - total;
669
+ if (value.byteLength > remaining) {
670
+ if (remaining > 0) chunks.push(value.subarray(0, remaining));
671
+ total = maxBytes;
672
+ truncated = true;
673
+ await reader.cancel();
674
+ break;
675
+ }
676
+ chunks.push(value);
677
+ total += value.byteLength;
678
+ }
679
+ } finally {
680
+ signal.removeEventListener("abort", abort);
681
+ reader.releaseLock();
682
+ }
683
+ if (truncated && !truncateOverflow) {
684
+ throw new Error(`${description} response exceeded ${maxBytes} bytes.`);
685
+ }
686
+ const body = new Uint8Array(total);
687
+ let offset = 0;
688
+ for (const chunk of chunks) {
689
+ body.set(chunk, offset);
690
+ offset += chunk.byteLength;
691
+ }
692
+ const text = new TextDecoder().decode(body);
693
+ return truncated ? `${text}…` : text;
747
694
  }
748
695
 
749
696
  type RequestAuth = {
750
- apiKey?: string;
751
- headers?: Record<string, string | null>;
752
- baseUrl?: string;
753
- env?: Record<string, string>;
697
+ apiKey?: string;
698
+ headers?: Record<string, string | null>;
699
+ baseUrl?: string;
700
+ env?: Record<string, string>;
754
701
  };
755
702
 
756
703
  type StoredCredentialReader = (providerId: string) => unknown;
757
704
 
758
705
  type UsageAuthRegistry = {
759
- getProvider?(providerId: string): { baseUrl?: string } | undefined;
760
- getApiKeyAndHeaders?(
761
- model: PiModel,
762
- ): Promise<({ ok: true } & RequestAuth) | { ok: false; error: string }>;
763
- getProviderAuth?(providerId: string): Promise<
764
- | {
765
- auth: RequestAuth;
766
- env?: Record<string, string>;
767
- source?: string;
768
- }
769
- | undefined
770
- >;
706
+ getProvider?(providerId: string): { baseUrl?: string } | undefined;
707
+ getApiKeyAndHeaders?(model: PiModel): Promise<({ ok: true } & RequestAuth) | { ok: false; error: string }>;
708
+ getProviderAuth?(providerId: string): Promise<
709
+ | {
710
+ auth: RequestAuth;
711
+ env?: Record<string, string>;
712
+ source?: string;
713
+ }
714
+ | undefined
715
+ >;
771
716
  };
772
717
 
773
718
  function resolveXaiUsageAuth(
774
- auth: RequestAuth,
775
- model: PiModel,
776
- salt: Uint8Array,
777
- candidates: readonly unknown[],
719
+ auth: RequestAuth,
720
+ model: PiModel,
721
+ salt: Uint8Array,
722
+ candidates: readonly unknown[],
778
723
  ): ResolvedUsageAuth {
779
- const resolvedAccess = bearerToken(headerValue(auth.headers, "Authorization")) ?? auth.apiKey;
780
- if (!resolvedAccess) throw new Error("xAI runtime authentication was incomplete.");
781
- let sawOAuth = false;
782
- let sawMatchingAccess = false;
783
- let sawIncompleteMatch = false;
784
- const matches: Array<{ access: string; refresh: string }> = [];
785
- for (const candidate of candidates) {
786
- try {
787
- const credential = asObject(candidate);
788
- if (credential?.type !== "oauth") continue;
789
- sawOAuth = true;
790
- if (credential.access !== resolvedAccess) continue;
791
- sawMatchingAccess = true;
792
- if (
793
- typeof credential.access !== "string" ||
794
- !credential.access ||
795
- typeof credential.refresh !== "string" ||
796
- !credential.refresh ||
797
- typeof credential.expires !== "number" ||
798
- !Number.isFinite(credential.expires)
799
- ) {
800
- sawIncompleteMatch = true;
801
- continue;
802
- }
803
- matches.push({ access: credential.access, refresh: credential.refresh });
804
- } catch {
805
- // Malformed candidates never authorize a consumer-proxy request.
806
- }
807
- }
808
- if (sawIncompleteMatch) throw new Error("The matching xAI OAuth credential was incomplete.");
809
- if (matches.length > 1) {
810
- throw new Error("Multiple OAuth credentials match the active xAI runtime account.");
811
- }
812
- const match = matches[0];
813
- if (!match) {
814
- if (!sawOAuth) {
815
- throw new Error(
816
- "xAI consumer usage requires the OAuth subscription account configured through Pi /login; XAI_API_KEY users can review API spend at console.x.ai.",
817
- );
818
- }
819
- if (sawMatchingAccess) throw new Error("The matching xAI OAuth credential was incomplete.");
820
- throw new Error("The active xAI runtime account does not match Pi's stored OAuth account.");
821
- }
822
- const authorization = `Bearer ${match.access}`;
823
- const headers = { Authorization: authorization };
824
- return {
825
- apiKey: match.access,
826
- headers,
827
- fingerprint: fingerprintResolvedAuth({ headers }, salt),
828
- secrets: [
829
- match.access,
830
- match.refresh,
831
- resolvedAccess,
832
- auth.apiKey,
833
- headerValue(auth.headers, "Authorization"),
834
- authorization,
835
- ].filter((value): value is string => Boolean(value)),
836
- model,
837
- };
724
+ const resolvedAccess = bearerToken(headerValue(auth.headers, "Authorization")) ?? auth.apiKey;
725
+ if (!resolvedAccess) throw new Error("xAI runtime authentication was incomplete.");
726
+ let sawOAuth = false;
727
+ let sawMatchingAccess = false;
728
+ let sawIncompleteMatch = false;
729
+ const matches: Array<{ access: string; refresh: string }> = [];
730
+ for (const candidate of candidates) {
731
+ try {
732
+ const credential = asObject(candidate);
733
+ if (credential?.type !== "oauth") continue;
734
+ sawOAuth = true;
735
+ if (credential.access !== resolvedAccess) continue;
736
+ sawMatchingAccess = true;
737
+ if (
738
+ typeof credential.access !== "string" ||
739
+ !credential.access ||
740
+ typeof credential.refresh !== "string" ||
741
+ !credential.refresh ||
742
+ typeof credential.expires !== "number" ||
743
+ !Number.isFinite(credential.expires)
744
+ ) {
745
+ sawIncompleteMatch = true;
746
+ continue;
747
+ }
748
+ matches.push({ access: credential.access, refresh: credential.refresh });
749
+ } catch {
750
+ // Malformed candidates never authorize a consumer-proxy request.
751
+ }
752
+ }
753
+ if (sawIncompleteMatch) throw new Error("The matching xAI OAuth credential was incomplete.");
754
+ if (matches.length > 1) {
755
+ throw new Error("Multiple OAuth credentials match the active xAI runtime account.");
756
+ }
757
+ const match = matches[0];
758
+ if (!match) {
759
+ if (!sawOAuth) {
760
+ throw new Error(
761
+ "xAI consumer usage requires the OAuth subscription account configured through Pi /login; XAI_API_KEY users can review API spend at console.x.ai.",
762
+ );
763
+ }
764
+ if (sawMatchingAccess) throw new Error("The matching xAI OAuth credential was incomplete.");
765
+ throw new Error("The active xAI runtime account does not match Pi's stored OAuth account.");
766
+ }
767
+ const authorization = `Bearer ${match.access}`;
768
+ const headers = { Authorization: authorization };
769
+ return {
770
+ apiKey: match.access,
771
+ headers,
772
+ fingerprint: fingerprintResolvedAuth({ headers }, salt),
773
+ secrets: [
774
+ match.access,
775
+ match.refresh,
776
+ resolvedAccess,
777
+ auth.apiKey,
778
+ headerValue(auth.headers, "Authorization"),
779
+ authorization,
780
+ ].filter((value): value is string => Boolean(value)),
781
+ model,
782
+ };
838
783
  }
839
784
 
840
785
  function resolveGitHubCopilotUsageAuth(
841
- auth: RequestAuth,
842
- model: PiModel,
843
- salt: Uint8Array,
844
- candidates: readonly unknown[],
845
- standaloneFallback: boolean,
786
+ auth: RequestAuth,
787
+ model: PiModel,
788
+ salt: Uint8Array,
789
+ candidates: readonly unknown[],
790
+ standaloneFallback: boolean,
846
791
  ): ResolvedUsageAuth {
847
- const resolvedAccess = bearerToken(headerValue(auth.headers, "Authorization")) ?? auth.apiKey;
848
- if (!resolvedAccess) throw new Error("GitHub Copilot OAuth credentials were incomplete.");
849
- let sawOAuth = false;
850
- let sawMatchingAccess = false;
851
- let sawIncompleteMatch = false;
852
- let sawEnterpriseMatch = false;
853
- const matches = new Map<string, { refresh: string; storedAccess: string }>();
854
- for (const candidate of candidates) {
855
- try {
856
- const credential = asObject(candidate);
857
- if (credential?.type !== "oauth") continue;
858
- sawOAuth = true;
859
- const storedAccess =
860
- typeof credential.access === "string" && credential.access ? credential.access : undefined;
861
- if (storedAccess !== resolvedAccess) continue;
862
- sawMatchingAccess = true;
863
- const enterpriseUrl = credential.enterpriseUrl;
864
- if (
865
- typeof enterpriseUrl === "string" &&
866
- enterpriseUrl &&
867
- !isPublicGitHubDomain(enterpriseUrl)
868
- ) {
869
- sawEnterpriseMatch = true;
870
- continue;
871
- }
872
- const refresh =
873
- typeof credential.refresh === "string" && credential.refresh
874
- ? credential.refresh
875
- : undefined;
876
- if (!refresh) {
877
- sawIncompleteMatch = true;
878
- continue;
879
- }
880
- matches.set(`${storedAccess.length}:${storedAccess}${refresh}`, { refresh, storedAccess });
881
- } catch {
882
- // Malformed candidates never authorize a provider request.
883
- }
884
- }
885
- if (sawEnterpriseMatch) {
886
- throw new Error("GitHub Copilot usage does not yet support GitHub Enterprise accounts.");
887
- }
888
- if (sawIncompleteMatch) throw new Error("GitHub Copilot OAuth credentials were incomplete.");
889
- if (matches.size > 1) {
890
- throw new Error(
891
- "Conflicting OAuth credentials match the active GitHub Copilot runtime account.",
892
- );
893
- }
894
- const match = matches.values().next().value;
895
- if (!match) {
896
- if (!sawOAuth) {
897
- throw new Error(
898
- standaloneFallback
899
- ? "GitHub Copilot usage requires the OAuth account configured through Pi /login."
900
- : "GitHub Copilot usage requires an OAuth account configured through Pi /login or a compatible credential source.",
901
- );
902
- }
903
- if (sawMatchingAccess) throw new Error("GitHub Copilot OAuth credentials were incomplete.");
904
- throw new Error(
905
- standaloneFallback
906
- ? "The active GitHub Copilot runtime account does not match Pi's stored OAuth account."
907
- : "The active GitHub Copilot runtime account does not match any available OAuth account.",
908
- );
909
- }
910
- const { refresh, storedAccess } = match;
911
- const authorization = `Bearer ${refresh}`;
912
- const headers = {
913
- Authorization: authorization,
914
- "X-GitHub-Api-Version": "2025-05-01",
915
- };
916
- return {
917
- apiKey: refresh,
918
- headers,
919
- fingerprint: fingerprintResolvedAuth({ headers }, salt),
920
- secrets: [refresh, storedAccess, resolvedAccess, authorization],
921
- model,
922
- };
792
+ const resolvedAccess = bearerToken(headerValue(auth.headers, "Authorization")) ?? auth.apiKey;
793
+ if (!resolvedAccess) throw new Error("GitHub Copilot OAuth credentials were incomplete.");
794
+ let sawOAuth = false;
795
+ let sawMatchingAccess = false;
796
+ let sawIncompleteMatch = false;
797
+ let sawEnterpriseMatch = false;
798
+ const matches = new Map<string, { refresh: string; storedAccess: string }>();
799
+ for (const candidate of candidates) {
800
+ try {
801
+ const credential = asObject(candidate);
802
+ if (credential?.type !== "oauth") continue;
803
+ sawOAuth = true;
804
+ const storedAccess = typeof credential.access === "string" && credential.access ? credential.access : undefined;
805
+ if (storedAccess !== resolvedAccess) continue;
806
+ sawMatchingAccess = true;
807
+ const enterpriseUrl = credential.enterpriseUrl;
808
+ if (typeof enterpriseUrl === "string" && enterpriseUrl && !isPublicGitHubDomain(enterpriseUrl)) {
809
+ sawEnterpriseMatch = true;
810
+ continue;
811
+ }
812
+ const refresh = typeof credential.refresh === "string" && credential.refresh ? credential.refresh : undefined;
813
+ if (!refresh) {
814
+ sawIncompleteMatch = true;
815
+ continue;
816
+ }
817
+ matches.set(`${storedAccess.length}:${storedAccess}${refresh}`, { refresh, storedAccess });
818
+ } catch {
819
+ // Malformed candidates never authorize a provider request.
820
+ }
821
+ }
822
+ if (sawEnterpriseMatch) {
823
+ throw new Error("GitHub Copilot usage does not yet support GitHub Enterprise accounts.");
824
+ }
825
+ if (sawIncompleteMatch) throw new Error("GitHub Copilot OAuth credentials were incomplete.");
826
+ if (matches.size > 1) {
827
+ throw new Error("Conflicting OAuth credentials match the active GitHub Copilot runtime account.");
828
+ }
829
+ const match = matches.values().next().value;
830
+ if (!match) {
831
+ if (!sawOAuth) {
832
+ throw new Error(
833
+ standaloneFallback
834
+ ? "GitHub Copilot usage requires the OAuth account configured through Pi /login."
835
+ : "GitHub Copilot usage requires an OAuth account configured through Pi /login or a compatible credential source.",
836
+ );
837
+ }
838
+ if (sawMatchingAccess) throw new Error("GitHub Copilot OAuth credentials were incomplete.");
839
+ throw new Error(
840
+ standaloneFallback
841
+ ? "The active GitHub Copilot runtime account does not match Pi's stored OAuth account."
842
+ : "The active GitHub Copilot runtime account does not match any available OAuth account.",
843
+ );
844
+ }
845
+ const { refresh, storedAccess } = match;
846
+ const authorization = `Bearer ${refresh}`;
847
+ const headers = {
848
+ Authorization: authorization,
849
+ "X-GitHub-Api-Version": "2025-05-01",
850
+ };
851
+ return {
852
+ apiKey: refresh,
853
+ headers,
854
+ fingerprint: fingerprintResolvedAuth({ headers }, salt),
855
+ secrets: [refresh, storedAccess, resolvedAccess, authorization],
856
+ model,
857
+ };
923
858
  }
924
859
 
925
860
  function authorizationFrom(auth: RequestAuth): string | undefined {
926
- return (
927
- headerValue(auth.headers, "Authorization") ??
928
- (auth.apiKey ? `Bearer ${auth.apiKey}` : undefined)
929
- );
861
+ return headerValue(auth.headers, "Authorization") ?? (auth.apiKey ? `Bearer ${auth.apiKey}` : undefined);
930
862
  }
931
863
 
932
864
  function bearerToken(authorization: string | undefined): string | undefined {
933
- const match = /^Bearer\s+(.+)$/iu.exec(authorization ?? "");
934
- return match?.[1];
865
+ const match = /^Bearer\s+(.+)$/iu.exec(authorization ?? "");
866
+ return match?.[1];
935
867
  }
936
868
 
937
869
  function asObject(value: unknown): Record<string, unknown> | undefined {
938
- if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
939
- return value as Record<string, unknown>;
870
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
871
+ return value as Record<string, unknown>;
940
872
  }
941
873
 
942
874
  function isPublicGitHubDomain(value: string): boolean {
943
- try {
944
- const url = new URL(value.includes("://") ? value : `https://${value}`);
945
- return url.hostname.toLowerCase() === "github.com";
946
- } catch {
947
- return false;
948
- }
875
+ try {
876
+ const url = new URL(value.includes("://") ? value : `https://${value}`);
877
+ return url.hostname.toLowerCase() === "github.com";
878
+ } catch {
879
+ return false;
880
+ }
949
881
  }
950
882
 
951
883
  function hasOfficialOrigin(model: PiModel, providerId: string): boolean {
952
- return hasOfficialUrlOrigin(model.baseUrl, providerId);
884
+ return hasOfficialUrlOrigin(model.baseUrl, providerId);
953
885
  }
954
886
 
955
887
  function hasOfficialUrlOrigin(value: string, providerId: string): boolean {
956
- try {
957
- const url = new URL(value);
958
- if (providerId === "baseten") {
959
- return ["https://inference.baseten.co", "https://api.baseten.co"].includes(url.origin);
960
- }
961
- if (providerId === "openai-codex") return url.origin === "https://chatgpt.com";
962
- if (providerId === "deepseek") return url.origin === "https://api.deepseek.com";
963
- if (providerId === "fireworks") return url.origin === "https://api.fireworks.ai";
964
- if (providerId === "openrouter") return url.origin === "https://openrouter.ai";
965
- if (providerId === "vercel-ai-gateway") return url.origin === "https://ai-gateway.vercel.sh";
966
- if (providerId === "opencode-go") return url.origin === "https://opencode.ai";
967
- if (providerId === "kimi-coding") return url.origin === "https://api.kimi.com";
968
- if (providerId === "minimax") return url.origin === "https://api.minimax.io";
969
- if (providerId === "minimax-cn") return url.origin === "https://api.minimaxi.com";
970
- if (providerId === "moonshotai") return url.origin === "https://api.moonshot.ai";
971
- if (providerId === "moonshotai-cn") return url.origin === "https://api.moonshot.cn";
972
- if (providerId === "xai") return url.origin === "https://api.x.ai";
973
- if (providerId === "zai") return url.origin === "https://api.z.ai";
974
- if (providerId === "zai-coding-cn") return url.origin === "https://open.bigmodel.cn";
975
- if (providerId === "github-copilot") {
976
- return (
977
- url.protocol === "https:" && /^api\.[a-z0-9-]+\.githubcopilot\.com$/u.test(url.hostname)
978
- );
979
- }
980
- return false;
981
- } catch {
982
- return false;
983
- }
888
+ try {
889
+ const url = new URL(value);
890
+ if (providerId === "baseten") {
891
+ return ["https://inference.baseten.co", "https://api.baseten.co"].includes(url.origin);
892
+ }
893
+ if (providerId === "openai-codex") return url.origin === "https://chatgpt.com";
894
+ if (providerId === "deepseek") return url.origin === "https://api.deepseek.com";
895
+ if (providerId === "fireworks") return url.origin === "https://api.fireworks.ai";
896
+ if (providerId === "openrouter") return url.origin === "https://openrouter.ai";
897
+ if (providerId === "vercel-ai-gateway") return url.origin === "https://ai-gateway.vercel.sh";
898
+ if (providerId === "opencode-go") return url.origin === "https://opencode.ai";
899
+ if (providerId === "kimi-coding") return url.origin === "https://api.kimi.com";
900
+ if (providerId === "minimax") return url.origin === "https://api.minimax.io";
901
+ if (providerId === "minimax-cn") return url.origin === "https://api.minimaxi.com";
902
+ if (providerId === "moonshotai") return url.origin === "https://api.moonshot.ai";
903
+ if (providerId === "moonshotai-cn") return url.origin === "https://api.moonshot.cn";
904
+ if (providerId === "xai") return url.origin === "https://api.x.ai";
905
+ if (providerId === "zai") return url.origin === "https://api.z.ai";
906
+ if (providerId === "zai-coding-cn") return url.origin === "https://open.bigmodel.cn";
907
+ if (providerId === "github-copilot") {
908
+ return url.protocol === "https:" && /^api\.[a-z0-9-]+\.githubcopilot\.com$/u.test(url.hostname);
909
+ }
910
+ return false;
911
+ } catch {
912
+ return false;
913
+ }
984
914
  }
985
915
 
986
- function headerValue(
987
- headers: Record<string, string | null> | undefined,
988
- name: string,
989
- ): string | undefined {
990
- const entry = Object.entries(headers ?? {}).find(
991
- ([candidate]) => candidate.toLowerCase() === name.toLowerCase(),
992
- );
993
- return entry?.[1] ?? undefined;
916
+ function headerValue(headers: Record<string, string | null> | undefined, name: string): string | undefined {
917
+ const entry = Object.entries(headers ?? {}).find(([candidate]) => candidate.toLowerCase() === name.toLowerCase());
918
+ return entry?.[1] ?? undefined;
994
919
  }
995
920
 
996
921
  function hasHeader(headers: Record<string, string>, name: string): boolean {
997
- return Object.keys(headers).some((key) => key.toLowerCase() === name.toLowerCase());
922
+ return Object.keys(headers).some((key) => key.toLowerCase() === name.toLowerCase());
998
923
  }
999
924
 
1000
925
  function validatedXaiUserId(value: unknown): string {
1001
- if (typeof value !== "string" || !/^[A-Za-z0-9._~-]{1,128}$/u.test(value)) {
1002
- throw new Error("xAI consumer identity returned an unsafe canonical user ID.");
1003
- }
1004
- return value;
926
+ if (typeof value !== "string" || !/^[A-Za-z0-9._~-]{1,128}$/u.test(value)) {
927
+ throw new Error("xAI consumer identity returned an unsafe canonical user ID.");
928
+ }
929
+ return value;
1005
930
  }
1006
931
 
1007
932
  function basetenBillingUsageUrl(windowAt: number): string {
1008
- const url = new URL(BASETEN_BILLING_USAGE_URL);
1009
- url.searchParams.set(
1010
- "start_date",
1011
- new Date(windowAt - BASETEN_USAGE_WINDOW_DAYS * 24 * 60 * 60 * 1_000).toISOString(),
1012
- );
1013
- url.searchParams.set("end_date", new Date(windowAt).toISOString());
1014
- return url.toString();
933
+ const url = new URL(BASETEN_BILLING_USAGE_URL);
934
+ url.searchParams.set(
935
+ "start_date",
936
+ new Date(windowAt - BASETEN_USAGE_WINDOW_DAYS * 24 * 60 * 60 * 1_000).toISOString(),
937
+ );
938
+ url.searchParams.set("end_date", new Date(windowAt).toISOString());
939
+ return url.toString();
1015
940
  }
1016
941
 
1017
942
  async function queryMiniMaxUsage(
1018
- providerId: MiniMaxProviderId,
1019
- auth: ResolvedUsageAuth,
1020
- signal: AbortSignal,
1021
- timeoutMs: number,
1022
- guard: UsageRequestGuard | undefined,
943
+ providerId: MiniMaxProviderId,
944
+ auth: ResolvedUsageAuth,
945
+ signal: AbortSignal,
946
+ timeoutMs: number,
947
+ guard: UsageRequestGuard | undefined,
1023
948
  ): Promise<UsageReport> {
1024
- if (!guard) throw new Error("MiniMax usage requires request-boundary revalidation.");
1025
- const apiKey = bearerToken(headerValue(auth.headers, "Authorization")) ?? auth.apiKey;
1026
- if (!apiKey) throw new Error("MiniMax runtime API key was unavailable.");
1027
- const kind = miniMaxUsageKind(apiKey);
1028
- const path = kind === "account-balance" ? "/account/query_balance" : "/v1/token_plan/remains";
1029
- const startedAt = Date.now();
1030
- await guard();
1031
- const payload = (await fetchProviderJson(
1032
- `${MINIMAX_API_ROOTS[providerId]}${path}`,
1033
- auth,
1034
- signal,
1035
- remainingTimeout(timeoutMs, startedAt, "fetching MiniMax usage"),
1036
- "MiniMax usage endpoint",
1037
- { redirect: "error" },
1038
- )) as MiniMaxUsagePayload;
1039
- await guard();
1040
- return normalizeMiniMaxUsagePayload(providerId, kind, payload, Date.now());
949
+ if (!guard) throw new Error("MiniMax usage requires request-boundary revalidation.");
950
+ const apiKey = bearerToken(headerValue(auth.headers, "Authorization")) ?? auth.apiKey;
951
+ if (!apiKey) throw new Error("MiniMax runtime API key was unavailable.");
952
+ const kind = miniMaxUsageKind(apiKey);
953
+ const path = kind === "account-balance" ? "/account/query_balance" : "/v1/token_plan/remains";
954
+ const startedAt = Date.now();
955
+ await guard();
956
+ const payload = (await fetchProviderJson(
957
+ `${MINIMAX_API_ROOTS[providerId]}${path}`,
958
+ auth,
959
+ signal,
960
+ remainingTimeout(timeoutMs, startedAt, "fetching MiniMax usage"),
961
+ "MiniMax usage endpoint",
962
+ { redirect: "error" },
963
+ )) as MiniMaxUsagePayload;
964
+ await guard();
965
+ return normalizeMiniMaxUsagePayload(providerId, kind, payload, Date.now());
1041
966
  }
1042
967
 
1043
968
  async function queryMoonshotBalance(
1044
- providerId: "moonshotai" | "moonshotai-cn",
1045
- auth: ResolvedUsageAuth,
1046
- signal: AbortSignal,
1047
- timeoutMs: number,
1048
- guard: UsageRequestGuard | undefined,
969
+ providerId: "moonshotai" | "moonshotai-cn",
970
+ auth: ResolvedUsageAuth,
971
+ signal: AbortSignal,
972
+ timeoutMs: number,
973
+ guard: UsageRequestGuard | undefined,
1049
974
  ): Promise<UsageReport> {
1050
- if (!guard) throw new Error("Moonshot AI balance requires request-boundary revalidation.");
1051
- const startedAt = Date.now();
1052
- await guard();
1053
- const payload = (await fetchProviderJson(
1054
- MOONSHOT_BALANCE_URLS[providerId],
1055
- auth,
1056
- signal,
1057
- remainingTimeout(timeoutMs, startedAt, "fetching Moonshot AI balance"),
1058
- "Moonshot AI balance endpoint",
1059
- { redirect: "error" },
1060
- )) as MoonshotBalancePayload;
1061
- await guard();
1062
- return normalizeMoonshotBalancePayload(providerId, payload, Date.now());
975
+ if (!guard) throw new Error("Moonshot AI balance requires request-boundary revalidation.");
976
+ const startedAt = Date.now();
977
+ await guard();
978
+ const payload = (await fetchProviderJson(
979
+ MOONSHOT_BALANCE_URLS[providerId],
980
+ auth,
981
+ signal,
982
+ remainingTimeout(timeoutMs, startedAt, "fetching Moonshot AI balance"),
983
+ "Moonshot AI balance endpoint",
984
+ { redirect: "error" },
985
+ )) as MoonshotBalancePayload;
986
+ await guard();
987
+ return normalizeMoonshotBalancePayload(providerId, payload, Date.now());
1063
988
  }
1064
989
 
1065
- function remainingTimeout(
1066
- timeoutMs: number,
1067
- startedAt: number,
1068
- description = "fetching xAI consumer usage",
1069
- ): number {
1070
- const remaining = timeoutMs - (Date.now() - startedAt);
1071
- if (remaining <= 0) throw new Error(`Timed out while ${description}.`);
1072
- return remaining;
990
+ function remainingTimeout(timeoutMs: number, startedAt: number, description = "fetching xAI consumer usage"): number {
991
+ const remaining = timeoutMs - (Date.now() - startedAt);
992
+ if (remaining <= 0) throw new Error(`Timed out while ${description}.`);
993
+ return remaining;
1073
994
  }
1074
995
 
1075
996
  function zaiOrigin(baseUrl: string | undefined): string {
1076
- const base = baseUrl?.trim();
1077
- if (!base) throw new Error("Z.AI model base URL is unavailable.");
1078
- return new URL(base).origin;
997
+ const base = baseUrl?.trim();
998
+ if (!base) throw new Error("Z.AI model base URL is unavailable.");
999
+ return new URL(base).origin;
1079
1000
  }
1080
1001
 
1081
1002
  function zaiMonitorUrl(baseUrl: string | undefined): string {
1082
- return `${zaiOrigin(baseUrl)}/api/monitor/usage/quota/limit`;
1003
+ return `${zaiOrigin(baseUrl)}/api/monitor/usage/quota/limit`;
1083
1004
  }
1084
1005
 
1085
1006
  function zaiMonitorAuth(auth: ResolvedUsageAuth): ResolvedUsageAuth {
1086
- const authorization = headerValue(auth.headers, "Authorization");
1087
- const token =
1088
- authorization === undefined ? undefined : (bearerToken(authorization) ?? authorization);
1089
- if (token === undefined || token === authorization) return auth;
1090
- return { ...auth, headers: { ...auth.headers, Authorization: token } };
1007
+ const authorization = headerValue(auth.headers, "Authorization");
1008
+ const token = authorization === undefined ? undefined : (bearerToken(authorization) ?? authorization);
1009
+ if (token === undefined || token === authorization) return auth;
1010
+ return { ...auth, headers: { ...auth.headers, Authorization: token } };
1091
1011
  }
1092
1012
 
1093
1013
  async function queryZaiUsage(
1094
- providerId: "zai" | "zai-coding-cn",
1095
- providerName: string,
1096
- auth: ResolvedUsageAuth,
1097
- signal: AbortSignal,
1098
- timeoutMs: number,
1099
- guard: UsageRequestGuard | undefined,
1014
+ providerId: "zai" | "zai-coding-cn",
1015
+ providerName: string,
1016
+ auth: ResolvedUsageAuth,
1017
+ signal: AbortSignal,
1018
+ timeoutMs: number,
1019
+ guard: UsageRequestGuard | undefined,
1100
1020
  ): Promise<UsageReport> {
1101
- if (!guard) throw new Error("Z.AI usage requires request-boundary revalidation.");
1102
- const startedAt = Date.now();
1103
- await guard();
1104
- const payload = (await fetchProviderJson(
1105
- zaiMonitorUrl(auth.model.baseUrl),
1106
- zaiMonitorAuth(auth),
1107
- signal,
1108
- remainingTimeout(timeoutMs, startedAt, `fetching ${providerName} quota`),
1109
- `${providerName} quota endpoint`,
1110
- { responseError: zaiResponseError },
1111
- )) as ZaiQuotaPayload;
1112
- await guard();
1113
- const planTimeoutMs = timeoutMs - (Date.now() - startedAt);
1114
- const plan = await fetchZaiPlan(providerName, auth, signal, planTimeoutMs);
1115
- return normalizeZaiQuotaPayload(providerId, providerName, payload, Date.now(), plan);
1021
+ if (!guard) throw new Error("Z.AI usage requires request-boundary revalidation.");
1022
+ const startedAt = Date.now();
1023
+ await guard();
1024
+ const payload = (await fetchProviderJson(
1025
+ zaiMonitorUrl(auth.model.baseUrl),
1026
+ zaiMonitorAuth(auth),
1027
+ signal,
1028
+ remainingTimeout(timeoutMs, startedAt, `fetching ${providerName} quota`),
1029
+ `${providerName} quota endpoint`,
1030
+ { responseError: zaiResponseError },
1031
+ )) as ZaiQuotaPayload;
1032
+ await guard();
1033
+ const planTimeoutMs = timeoutMs - (Date.now() - startedAt);
1034
+ const plan = await fetchZaiPlan(providerName, auth, signal, planTimeoutMs);
1035
+ return normalizeZaiQuotaPayload(providerId, providerName, payload, Date.now(), plan);
1116
1036
  }
1117
1037
 
1118
1038
  // The subscription endpoint is undocumented and may not exist on every official origin. It only
1119
1039
  // contributes the plan name and renewal date, so any non-abort failure is swallowed instead of
1120
1040
  // blanking the required quota report.
1121
1041
  async function fetchZaiPlan(
1122
- providerName: string,
1123
- auth: ResolvedUsageAuth,
1124
- signal: AbortSignal,
1125
- timeoutMs: number,
1042
+ providerName: string,
1043
+ auth: ResolvedUsageAuth,
1044
+ signal: AbortSignal,
1045
+ timeoutMs: number,
1126
1046
  ): Promise<ZaiPlanInfo | undefined> {
1127
- if (timeoutMs <= 0 || signal.aborted) return undefined;
1128
- try {
1129
- const payload = (await fetchProviderJson(
1130
- `${zaiOrigin(auth.model.baseUrl)}/api/biz/subscription/list`,
1131
- zaiMonitorAuth(auth),
1132
- signal,
1133
- timeoutMs,
1134
- `${providerName} plan endpoint`,
1135
- { responseError: zaiResponseError },
1136
- )) as ZaiSubscriptionPayload;
1137
- return normalizeZaiSubscriptionPayload(payload);
1138
- } catch (error) {
1139
- if (isAbortError(error)) throw error;
1140
- return undefined;
1141
- }
1047
+ if (timeoutMs <= 0 || signal.aborted) return undefined;
1048
+ try {
1049
+ const payload = (await fetchProviderJson(
1050
+ `${zaiOrigin(auth.model.baseUrl)}/api/biz/subscription/list`,
1051
+ zaiMonitorAuth(auth),
1052
+ signal,
1053
+ timeoutMs,
1054
+ `${providerName} plan endpoint`,
1055
+ { responseError: zaiResponseError },
1056
+ )) as ZaiSubscriptionPayload;
1057
+ return normalizeZaiSubscriptionPayload(payload);
1058
+ } catch (error) {
1059
+ if (isAbortError(error)) throw error;
1060
+ return undefined;
1061
+ }
1142
1062
  }
1143
1063
 
1144
1064
  function isAbortError(error: unknown): boolean {
1145
- return error instanceof Error && error.name === "AbortError";
1065
+ return error instanceof Error && error.name === "AbortError";
1146
1066
  }