@narumitw/pi-usage 0.57.0 → 0.59.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.
- package/README.md +118 -2
- package/dist/index.ts +1247 -240
- package/dist/index.ts.map +4 -4
- package/package.json +8 -1
- package/src/codex-fast-runtime.ts +34 -26
- package/src/format.ts +189 -3
- package/src/index.ts +20 -0
- package/src/providers/baseten.ts +55 -0
- package/src/providers/fireworks.ts +198 -0
- package/src/providers/minimax.ts +264 -0
- package/src/providers/moonshot.ts +64 -0
- package/src/providers/vercel-ai-gateway.ts +43 -0
- package/src/query.ts +355 -9
- package/src/settings.ts +16 -1
- package/src/types.ts +44 -0
- package/src/usage-settings-ui.ts +125 -33
- package/src/usage.ts +55 -23
package/src/query.ts
CHANGED
|
@@ -5,36 +5,71 @@ import {
|
|
|
5
5
|
fallbackOAuthCredentialCandidates,
|
|
6
6
|
type OAuthCredentialCandidateReader,
|
|
7
7
|
} from "./oauth-credential-source.js";
|
|
8
|
+
import { normalizeBasetenBillingUsagePayload } from "./providers/baseten.js";
|
|
8
9
|
import { normalizeCodexBackendPayload } from "./providers/codex.js";
|
|
9
10
|
import { normalizeDeepSeekBalancePayload } from "./providers/deepseek.js";
|
|
11
|
+
import {
|
|
12
|
+
isFireworksAccountId,
|
|
13
|
+
normalizeFireworksAccountsPayload,
|
|
14
|
+
normalizeFireworksBillingSummaryPayload,
|
|
15
|
+
} from "./providers/fireworks.js";
|
|
10
16
|
import { normalizeGitHubCopilotUsagePayload } from "./providers/github-copilot.js";
|
|
11
17
|
import { normalizeKimiCodingUsagePayload } from "./providers/kimi-coding.js";
|
|
18
|
+
import {
|
|
19
|
+
type MiniMaxProviderId,
|
|
20
|
+
miniMaxUsageKind,
|
|
21
|
+
normalizeMiniMaxUsagePayload,
|
|
22
|
+
} from "./providers/minimax.js";
|
|
23
|
+
import { normalizeMoonshotBalancePayload } from "./providers/moonshot.js";
|
|
12
24
|
import { normalizeOpenCodeZenPayload } from "./providers/opencode-zen.js";
|
|
13
25
|
import { normalizeOpenRouterKeyPayload } from "./providers/openrouter.js";
|
|
26
|
+
import { normalizeVercelAIGatewayCreditsPayload } from "./providers/vercel-ai-gateway.js";
|
|
14
27
|
import { normalizeXaiBillingPayload } from "./providers/xai.js";
|
|
15
28
|
import { normalizeZaiQuotaPayload } from "./providers/zai.js";
|
|
16
29
|
import type {
|
|
30
|
+
BasetenBillingUsagePayload,
|
|
17
31
|
CodexBackendPayload,
|
|
18
32
|
DeepSeekBalancePayload,
|
|
33
|
+
FireworksAccountsPayload,
|
|
34
|
+
FireworksBillingSummaryPayload,
|
|
19
35
|
GitHubCopilotUsagePayload,
|
|
20
36
|
KimiCodingUsagePayload,
|
|
37
|
+
MiniMaxUsagePayload,
|
|
38
|
+
MoonshotBalancePayload,
|
|
21
39
|
OpenCodeZenPayload,
|
|
22
40
|
OpenRouterKeyPayload,
|
|
23
41
|
PiModel,
|
|
24
42
|
ResolvedUsageAuth,
|
|
25
43
|
UsageProviderAdapter,
|
|
44
|
+
UsageQuerySettings,
|
|
26
45
|
UsageReport,
|
|
46
|
+
VercelAIGatewayCreditsPayload,
|
|
27
47
|
XaiBillingPayload,
|
|
28
48
|
XaiUserPayload,
|
|
29
49
|
ZaiQuotaPayload,
|
|
30
50
|
} from "./types.js";
|
|
31
51
|
|
|
52
|
+
const BASETEN_BILLING_USAGE_URL = "https://api.baseten.co/v1/billing/usage_summary";
|
|
53
|
+
const BASETEN_USAGE_WINDOW_DAYS = 30;
|
|
32
54
|
const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
33
55
|
const DEEPSEEK_BALANCE_URL = "https://api.deepseek.com/user/balance";
|
|
56
|
+
const FIREWORKS_BILLING_SUMMARY_ORIGIN = "https://api.fireworks.ai";
|
|
57
|
+
const FIREWORKS_SPEND_WINDOW_DAYS = 30;
|
|
58
|
+
const FIREWORKS_MAX_ACCOUNT_PAGES = 5;
|
|
34
59
|
const GITHUB_COPILOT_USAGE_URL = "https://api.github.com/copilot_internal/user";
|
|
35
60
|
const OPENROUTER_KEY_URL = "https://openrouter.ai/api/v1/key";
|
|
61
|
+
const VERCEL_AI_GATEWAY_CREDITS_URL = "https://ai-gateway.vercel.sh/v1/credits";
|
|
36
62
|
const OPENCODE_GO_USAGE_URL = "https://opencode.ai/zen/go/v1/usage";
|
|
37
63
|
const KIMI_CODING_USAGE_URL = "https://api.kimi.com/coding/v1/usages";
|
|
64
|
+
const MINIMAX_API_ROOTS = Object.freeze({
|
|
65
|
+
minimax: "https://api.minimax.io",
|
|
66
|
+
"minimax-cn": "https://api.minimaxi.com",
|
|
67
|
+
});
|
|
68
|
+
const MOONSHOT_BALANCE_URLS = Object.freeze({
|
|
69
|
+
moonshotai: "https://api.moonshot.ai/v1/users/me/balance",
|
|
70
|
+
"moonshotai-cn": "https://api.moonshot.cn/v1/users/me/balance",
|
|
71
|
+
});
|
|
72
|
+
const SHARED_MOONSHOT_ENV_VAR = "MOONSHOT_API_KEY";
|
|
38
73
|
const XAI_USER_URL = "https://cli-chat-proxy.grok.com/v1/user?include=subscription";
|
|
39
74
|
const XAI_BILLING_URL = "https://cli-chat-proxy.grok.com/v1/billing?format=credits";
|
|
40
75
|
const XAI_CLIENT_HEADERS = Object.freeze({
|
|
@@ -50,6 +85,27 @@ export const AUTH_FINGERPRINT_SALT = randomBytes(32);
|
|
|
50
85
|
export type UsageRequestGuard = () => Promise<void>;
|
|
51
86
|
|
|
52
87
|
export const SUPPORTED_ADAPTERS: readonly UsageProviderAdapter[] = [
|
|
88
|
+
{
|
|
89
|
+
id: "baseten",
|
|
90
|
+
displayName: "Baseten",
|
|
91
|
+
semantics: { kind: "api-key", label: "Organization Model APIs spend" },
|
|
92
|
+
async query(auth, signal, timeoutMs, guard) {
|
|
93
|
+
if (!guard) throw new Error("Baseten billing usage requires request-boundary revalidation.");
|
|
94
|
+
const startedAt = Date.now();
|
|
95
|
+
await guard();
|
|
96
|
+
const windowAt = Date.now();
|
|
97
|
+
const payload = (await fetchProviderJson(
|
|
98
|
+
basetenBillingUsageUrl(windowAt),
|
|
99
|
+
auth,
|
|
100
|
+
signal,
|
|
101
|
+
remainingTimeout(timeoutMs, startedAt, "fetching Baseten billing usage"),
|
|
102
|
+
"Baseten billing usage endpoint",
|
|
103
|
+
{ redirect: "error" },
|
|
104
|
+
)) as BasetenBillingUsagePayload;
|
|
105
|
+
await guard();
|
|
106
|
+
return normalizeBasetenBillingUsagePayload(payload, Date.now());
|
|
107
|
+
},
|
|
108
|
+
},
|
|
53
109
|
{
|
|
54
110
|
id: "openai-codex",
|
|
55
111
|
displayName: "OpenAI Codex",
|
|
@@ -122,6 +178,56 @@ export const SUPPORTED_ADAPTERS: readonly UsageProviderAdapter[] = [
|
|
|
122
178
|
return normalizeOpenRouterKeyPayload(payload as OpenRouterKeyPayload, Date.now());
|
|
123
179
|
},
|
|
124
180
|
},
|
|
181
|
+
{
|
|
182
|
+
id: "vercel-ai-gateway",
|
|
183
|
+
displayName: "Vercel AI Gateway",
|
|
184
|
+
semantics: { kind: "api-key", label: "AI Gateway credits and lifetime spend" },
|
|
185
|
+
async query(auth, signal, timeoutMs, guard) {
|
|
186
|
+
if (!guard)
|
|
187
|
+
throw new Error("Vercel AI Gateway usage requires request-boundary revalidation.");
|
|
188
|
+
const startedAt = Date.now();
|
|
189
|
+
await guard();
|
|
190
|
+
const payload = (await fetchProviderJson(
|
|
191
|
+
VERCEL_AI_GATEWAY_CREDITS_URL,
|
|
192
|
+
auth,
|
|
193
|
+
signal,
|
|
194
|
+
remainingTimeout(timeoutMs, startedAt, "fetching Vercel AI Gateway credits"),
|
|
195
|
+
"Vercel AI Gateway credits endpoint",
|
|
196
|
+
{ redirect: "error" },
|
|
197
|
+
)) as VercelAIGatewayCreditsPayload;
|
|
198
|
+
await guard();
|
|
199
|
+
return normalizeVercelAIGatewayCreditsPayload(payload, Date.now());
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
id: "fireworks",
|
|
204
|
+
displayName: "Fireworks",
|
|
205
|
+
semantics: { kind: "api-key", label: "Fireworks API spend" },
|
|
206
|
+
async query(auth, signal, timeoutMs, guard, settings) {
|
|
207
|
+
if (!guard) throw new Error("Fireworks API spend requires request-boundary revalidation.");
|
|
208
|
+
const startedAt = Date.now();
|
|
209
|
+
await guard();
|
|
210
|
+
const accountId = await resolveFireworksAccountId(
|
|
211
|
+
auth,
|
|
212
|
+
signal,
|
|
213
|
+
remainingTimeout(timeoutMs, startedAt, "resolving the Fireworks account"),
|
|
214
|
+
guard,
|
|
215
|
+
settings?.fireworksAccountId,
|
|
216
|
+
);
|
|
217
|
+
await guard();
|
|
218
|
+
const billingWindowAt = Date.now();
|
|
219
|
+
const payload = (await fetchProviderJson(
|
|
220
|
+
fireworksBillingSummaryUrl(accountId, billingWindowAt),
|
|
221
|
+
auth,
|
|
222
|
+
signal,
|
|
223
|
+
remainingTimeout(timeoutMs, startedAt, "fetching Fireworks rated spend"),
|
|
224
|
+
"Fireworks billing summary endpoint",
|
|
225
|
+
{ redirect: "error" },
|
|
226
|
+
)) as FireworksBillingSummaryPayload;
|
|
227
|
+
await guard();
|
|
228
|
+
return normalizeFireworksBillingSummaryPayload(payload, accountId, Date.now());
|
|
229
|
+
},
|
|
230
|
+
},
|
|
125
231
|
{
|
|
126
232
|
id: "opencode-go",
|
|
127
233
|
displayName: "OpenCode Go",
|
|
@@ -153,6 +259,38 @@ export const SUPPORTED_ADAPTERS: readonly UsageProviderAdapter[] = [
|
|
|
153
259
|
return normalizeKimiCodingUsagePayload(payload as KimiCodingUsagePayload, Date.now());
|
|
154
260
|
},
|
|
155
261
|
},
|
|
262
|
+
{
|
|
263
|
+
id: "minimax",
|
|
264
|
+
displayName: "MiniMax",
|
|
265
|
+
semantics: { kind: "consumer-subscription", label: "MiniMax usage" },
|
|
266
|
+
async query(auth, signal, timeoutMs, guard) {
|
|
267
|
+
return queryMiniMaxUsage("minimax", auth, signal, timeoutMs, guard);
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
id: "minimax-cn",
|
|
272
|
+
displayName: "MiniMax CN",
|
|
273
|
+
semantics: { kind: "consumer-subscription", label: "MiniMax usage" },
|
|
274
|
+
async query(auth, signal, timeoutMs, guard) {
|
|
275
|
+
return queryMiniMaxUsage("minimax-cn", auth, signal, timeoutMs, guard);
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
id: "moonshotai",
|
|
280
|
+
displayName: "Moonshot AI",
|
|
281
|
+
semantics: { kind: "api-key", label: "Moonshot API account balance" },
|
|
282
|
+
async query(auth, signal, timeoutMs, guard) {
|
|
283
|
+
return queryMoonshotBalance("moonshotai", auth, signal, timeoutMs, guard);
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
id: "moonshotai-cn",
|
|
288
|
+
displayName: "Moonshot AI CN",
|
|
289
|
+
semantics: { kind: "api-key", label: "Moonshot API account balance" },
|
|
290
|
+
async query(auth, signal, timeoutMs, guard) {
|
|
291
|
+
return queryMoonshotBalance("moonshotai-cn", auth, signal, timeoutMs, guard);
|
|
292
|
+
},
|
|
293
|
+
},
|
|
156
294
|
{
|
|
157
295
|
id: "zai",
|
|
158
296
|
displayName: "Z.AI",
|
|
@@ -289,11 +427,14 @@ export async function resolveUsageAuth(
|
|
|
289
427
|
if (!result.ok) throw new Error(redactUsageError(result.error));
|
|
290
428
|
return authorizationFrom(result) ? result : undefined;
|
|
291
429
|
};
|
|
292
|
-
|
|
430
|
+
const resolveSelectedAuthLast = ["deepseek", "minimax", "minimax-cn"].includes(adapter.id);
|
|
431
|
+
if (!resolveSelectedAuthLast) modelAuth = await resolveCurrentModelAuth();
|
|
293
432
|
if (typeof registry.getProviderAuth !== "function") {
|
|
294
433
|
throw new Error("pi-usage requires Pi 0.81.0 or newer to validate resolved provider auth.");
|
|
295
434
|
}
|
|
435
|
+
if (!moonshotProviderAuthIsAllowed(ctx, adapter.id)) return undefined;
|
|
296
436
|
const providerResult = await registry.getProviderAuth(adapter.id);
|
|
437
|
+
if (!moonshotProviderAuthIsAllowed(ctx, adapter.id)) return undefined;
|
|
297
438
|
if (
|
|
298
439
|
providerResult?.auth.baseUrl &&
|
|
299
440
|
!hasOfficialUrlOrigin(providerResult.auth.baseUrl, adapter.id)
|
|
@@ -302,9 +443,9 @@ export async function resolveUsageAuth(
|
|
|
302
443
|
`${adapter.displayName} usage cannot send a proxy-resolved credential to the official usage endpoint.`,
|
|
303
444
|
);
|
|
304
445
|
}
|
|
305
|
-
//
|
|
306
|
-
// cannot leave the earlier credential queued for the
|
|
307
|
-
if (
|
|
446
|
+
// Providers with credential-change retries read selected-model auth last so a rotation during
|
|
447
|
+
// provider-origin validation cannot leave the earlier credential queued for the usage request.
|
|
448
|
+
if (resolveSelectedAuthLast) modelAuth = await resolveCurrentModelAuth();
|
|
308
449
|
const auth = modelAuth ?? providerResult?.auth;
|
|
309
450
|
if (!auth) return undefined;
|
|
310
451
|
if (adapter.id === "github-copilot") {
|
|
@@ -370,9 +511,10 @@ export async function queryProviderUsage(
|
|
|
370
511
|
signal: AbortSignal,
|
|
371
512
|
timeoutMs: number,
|
|
372
513
|
guard?: UsageRequestGuard,
|
|
514
|
+
settings?: Readonly<UsageQuerySettings>,
|
|
373
515
|
): Promise<UsageReport> {
|
|
374
516
|
try {
|
|
375
|
-
return await adapter.query(auth, signal, timeoutMs, guard);
|
|
517
|
+
return await adapter.query(auth, signal, timeoutMs, guard, settings);
|
|
376
518
|
} catch (error) {
|
|
377
519
|
if (isStaleExtensionContextError(error) || isAbortError(error)) throw error;
|
|
378
520
|
throw new Error(redactUsageError(errorMessage(error), auth.secrets));
|
|
@@ -381,12 +523,53 @@ export async function queryProviderUsage(
|
|
|
381
523
|
|
|
382
524
|
export function providerIsConfigured(ctx: ExtensionContext, providerId: string): boolean {
|
|
383
525
|
try {
|
|
384
|
-
|
|
526
|
+
const status = ctx.modelRegistry.getProviderAuthStatus(providerId);
|
|
527
|
+
return (
|
|
528
|
+
status.configured &&
|
|
529
|
+
moonshotProviderAuthSourceIsAllowed(ctx, providerId, status.source, status.label)
|
|
530
|
+
);
|
|
531
|
+
} catch {
|
|
532
|
+
return (
|
|
533
|
+
!isMoonshotSiblingProvider(ctx, providerId) && candidateModels(ctx, providerId).length > 0
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function moonshotProviderAuthIsAllowed(ctx: ExtensionContext, providerId: string): boolean {
|
|
539
|
+
if (!isMoonshotSiblingProvider(ctx, providerId)) return true;
|
|
540
|
+
try {
|
|
541
|
+
const status = ctx.modelRegistry.getProviderAuthStatus(providerId);
|
|
542
|
+
return moonshotProviderAuthSourceIsAllowed(ctx, providerId, status.source, status.label);
|
|
385
543
|
} catch {
|
|
386
|
-
return
|
|
544
|
+
return false;
|
|
387
545
|
}
|
|
388
546
|
}
|
|
389
547
|
|
|
548
|
+
function moonshotProviderAuthSourceIsAllowed(
|
|
549
|
+
ctx: ExtensionContext,
|
|
550
|
+
providerId: string,
|
|
551
|
+
source: string | undefined,
|
|
552
|
+
label: string | undefined,
|
|
553
|
+
): boolean {
|
|
554
|
+
if (!isMoonshotSiblingProvider(ctx, providerId)) return true;
|
|
555
|
+
if (source === undefined) return false;
|
|
556
|
+
if (source !== "environment") return true;
|
|
557
|
+
return (
|
|
558
|
+
label !== undefined &&
|
|
559
|
+
!label
|
|
560
|
+
.split(",")
|
|
561
|
+
.map((name) => name.trim())
|
|
562
|
+
.includes(SHARED_MOONSHOT_ENV_VAR)
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function isMoonshotSiblingProvider(ctx: ExtensionContext, providerId: string): boolean {
|
|
567
|
+
return (
|
|
568
|
+
(providerId === "moonshotai" || providerId === "moonshotai-cn") &&
|
|
569
|
+
ctx.model?.provider !== providerId
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
|
|
390
573
|
function candidateModels(ctx: ExtensionContext, providerId: string): PiModel[] {
|
|
391
574
|
const candidates: PiModel[] = [];
|
|
392
575
|
const seen = new Set<string>();
|
|
@@ -731,11 +914,20 @@ function hasOfficialOrigin(model: PiModel, providerId: string): boolean {
|
|
|
731
914
|
function hasOfficialUrlOrigin(value: string, providerId: string): boolean {
|
|
732
915
|
try {
|
|
733
916
|
const url = new URL(value);
|
|
917
|
+
if (providerId === "baseten") {
|
|
918
|
+
return ["https://inference.baseten.co", "https://api.baseten.co"].includes(url.origin);
|
|
919
|
+
}
|
|
734
920
|
if (providerId === "openai-codex") return url.origin === "https://chatgpt.com";
|
|
735
921
|
if (providerId === "deepseek") return url.origin === "https://api.deepseek.com";
|
|
922
|
+
if (providerId === "fireworks") return url.origin === FIREWORKS_BILLING_SUMMARY_ORIGIN;
|
|
736
923
|
if (providerId === "openrouter") return url.origin === "https://openrouter.ai";
|
|
924
|
+
if (providerId === "vercel-ai-gateway") return url.origin === "https://ai-gateway.vercel.sh";
|
|
737
925
|
if (providerId === "opencode-go") return url.origin === "https://opencode.ai";
|
|
738
926
|
if (providerId === "kimi-coding") return url.origin === "https://api.kimi.com";
|
|
927
|
+
if (providerId === "minimax") return url.origin === "https://api.minimax.io";
|
|
928
|
+
if (providerId === "minimax-cn") return url.origin === "https://api.minimaxi.com";
|
|
929
|
+
if (providerId === "moonshotai") return url.origin === "https://api.moonshot.ai";
|
|
930
|
+
if (providerId === "moonshotai-cn") return url.origin === "https://api.moonshot.cn";
|
|
739
931
|
if (providerId === "xai") return url.origin === "https://api.x.ai";
|
|
740
932
|
if (providerId === "zai") return url.origin === "https://api.z.ai";
|
|
741
933
|
if (providerId === "zai-coding-cn") return url.origin === "https://open.bigmodel.cn";
|
|
@@ -771,12 +963,166 @@ function validatedXaiUserId(value: unknown): string {
|
|
|
771
963
|
return value;
|
|
772
964
|
}
|
|
773
965
|
|
|
774
|
-
function
|
|
966
|
+
function basetenBillingUsageUrl(windowAt: number): string {
|
|
967
|
+
const url = new URL(BASETEN_BILLING_USAGE_URL);
|
|
968
|
+
url.searchParams.set(
|
|
969
|
+
"start_date",
|
|
970
|
+
new Date(windowAt - BASETEN_USAGE_WINDOW_DAYS * 24 * 60 * 60 * 1_000).toISOString(),
|
|
971
|
+
);
|
|
972
|
+
url.searchParams.set("end_date", new Date(windowAt).toISOString());
|
|
973
|
+
return url.toString();
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
async function queryMiniMaxUsage(
|
|
977
|
+
providerId: MiniMaxProviderId,
|
|
978
|
+
auth: ResolvedUsageAuth,
|
|
979
|
+
signal: AbortSignal,
|
|
980
|
+
timeoutMs: number,
|
|
981
|
+
guard: UsageRequestGuard | undefined,
|
|
982
|
+
): Promise<UsageReport> {
|
|
983
|
+
if (!guard) throw new Error("MiniMax usage requires request-boundary revalidation.");
|
|
984
|
+
const apiKey = bearerToken(headerValue(auth.headers, "Authorization")) ?? auth.apiKey;
|
|
985
|
+
if (!apiKey) throw new Error("MiniMax runtime API key was unavailable.");
|
|
986
|
+
const kind = miniMaxUsageKind(apiKey);
|
|
987
|
+
const path = kind === "account-balance" ? "/account/query_balance" : "/v1/token_plan/remains";
|
|
988
|
+
const startedAt = Date.now();
|
|
989
|
+
await guard();
|
|
990
|
+
const payload = (await fetchProviderJson(
|
|
991
|
+
`${MINIMAX_API_ROOTS[providerId]}${path}`,
|
|
992
|
+
auth,
|
|
993
|
+
signal,
|
|
994
|
+
remainingTimeout(timeoutMs, startedAt, "fetching MiniMax usage"),
|
|
995
|
+
"MiniMax usage endpoint",
|
|
996
|
+
{ redirect: "error" },
|
|
997
|
+
)) as MiniMaxUsagePayload;
|
|
998
|
+
await guard();
|
|
999
|
+
return normalizeMiniMaxUsagePayload(providerId, kind, payload, Date.now());
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
async function queryMoonshotBalance(
|
|
1003
|
+
providerId: "moonshotai" | "moonshotai-cn",
|
|
1004
|
+
auth: ResolvedUsageAuth,
|
|
1005
|
+
signal: AbortSignal,
|
|
1006
|
+
timeoutMs: number,
|
|
1007
|
+
guard: UsageRequestGuard | undefined,
|
|
1008
|
+
): Promise<UsageReport> {
|
|
1009
|
+
if (!guard) throw new Error("Moonshot AI balance requires request-boundary revalidation.");
|
|
1010
|
+
const startedAt = Date.now();
|
|
1011
|
+
await guard();
|
|
1012
|
+
const payload = (await fetchProviderJson(
|
|
1013
|
+
MOONSHOT_BALANCE_URLS[providerId],
|
|
1014
|
+
auth,
|
|
1015
|
+
signal,
|
|
1016
|
+
remainingTimeout(timeoutMs, startedAt, "fetching Moonshot AI balance"),
|
|
1017
|
+
"Moonshot AI balance endpoint",
|
|
1018
|
+
{ redirect: "error" },
|
|
1019
|
+
)) as MoonshotBalancePayload;
|
|
1020
|
+
await guard();
|
|
1021
|
+
return normalizeMoonshotBalancePayload(providerId, payload, Date.now());
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function remainingTimeout(
|
|
1025
|
+
timeoutMs: number,
|
|
1026
|
+
startedAt: number,
|
|
1027
|
+
description = "fetching xAI consumer usage",
|
|
1028
|
+
): number {
|
|
775
1029
|
const remaining = timeoutMs - (Date.now() - startedAt);
|
|
776
|
-
if (remaining <= 0) throw new Error(
|
|
1030
|
+
if (remaining <= 0) throw new Error(`Timed out while ${description}.`);
|
|
777
1031
|
return remaining;
|
|
778
1032
|
}
|
|
779
1033
|
|
|
1034
|
+
// Fireworks requires an account slug for its billing endpoints; discover it through the
|
|
1035
|
+
// documented account listing, requiring an explicit slug when a key can see several accounts.
|
|
1036
|
+
async function resolveFireworksAccountId(
|
|
1037
|
+
auth: ResolvedUsageAuth,
|
|
1038
|
+
signal: AbortSignal,
|
|
1039
|
+
timeoutMs: number,
|
|
1040
|
+
guard: () => Promise<void>,
|
|
1041
|
+
configuredAccountId: string | undefined,
|
|
1042
|
+
): Promise<string> {
|
|
1043
|
+
if (configuredAccountId !== undefined && !isFireworksAccountId(configuredAccountId)) {
|
|
1044
|
+
throw new Error("The Fireworks account setting was not a safe account slug.");
|
|
1045
|
+
}
|
|
1046
|
+
const startedAt = Date.now();
|
|
1047
|
+
const accounts: string[] = [];
|
|
1048
|
+
let pageToken: string | undefined;
|
|
1049
|
+
for (let page = 0; page < FIREWORKS_MAX_ACCOUNT_PAGES; page += 1) {
|
|
1050
|
+
await guard();
|
|
1051
|
+
const payload = (await fetchProviderJson(
|
|
1052
|
+
fireworksAccountsUrl(pageToken),
|
|
1053
|
+
auth,
|
|
1054
|
+
signal,
|
|
1055
|
+
remainingTimeout(timeoutMs, startedAt, "fetching Fireworks accounts"),
|
|
1056
|
+
"Fireworks accounts endpoint",
|
|
1057
|
+
{ redirect: "error" },
|
|
1058
|
+
)) as FireworksAccountsPayload;
|
|
1059
|
+
for (const accountId of normalizeFireworksAccountsPayload(
|
|
1060
|
+
payload as FireworksAccountsPayload,
|
|
1061
|
+
)) {
|
|
1062
|
+
if (accounts.includes(accountId)) {
|
|
1063
|
+
throw new Error(`Fireworks accounts listing repeated ${accountId}.`);
|
|
1064
|
+
}
|
|
1065
|
+
accounts.push(accountId);
|
|
1066
|
+
if (configuredAccountId === accountId) return accountId;
|
|
1067
|
+
}
|
|
1068
|
+
pageToken = fireworksNextPageToken(payload.nextPageToken);
|
|
1069
|
+
if (!pageToken) break;
|
|
1070
|
+
}
|
|
1071
|
+
if (pageToken) {
|
|
1072
|
+
throw new Error(
|
|
1073
|
+
configuredAccountId
|
|
1074
|
+
? `The configured Fireworks account was not found within the first ${FIREWORKS_MAX_ACCOUNT_PAGES} listing pages.`
|
|
1075
|
+
: `Fireworks account listing exceeded ${FIREWORKS_MAX_ACCOUNT_PAGES} pages; set fireworksAccountId in pi-usage.json to an account returned in those pages.`,
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
if (accounts.length === 0) {
|
|
1079
|
+
throw new Error("Fireworks account discovery returned no accounts for this API key.");
|
|
1080
|
+
}
|
|
1081
|
+
if (configuredAccountId) {
|
|
1082
|
+
throw new Error(
|
|
1083
|
+
"The configured Fireworks account does not match an account visible to this API key.",
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
if (accounts.length === 1) return accounts[0] as string;
|
|
1087
|
+
const preview = accounts.slice(0, 8).join(", ");
|
|
1088
|
+
const suffix = accounts.length > 8 ? ` …and ${accounts.length - 8} more` : "";
|
|
1089
|
+
throw new Error(
|
|
1090
|
+
`The Fireworks key can see ${accounts.length} accounts (${preview}${suffix}); set fireworksAccountId in pi-usage.json to one of them.`,
|
|
1091
|
+
);
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
function fireworksAccountsUrl(pageToken: string | undefined): string {
|
|
1095
|
+
const url = new URL("/v1/accounts", FIREWORKS_BILLING_SUMMARY_ORIGIN);
|
|
1096
|
+
url.searchParams.set("pageSize", "200");
|
|
1097
|
+
if (pageToken !== undefined) url.searchParams.set("pageToken", pageToken);
|
|
1098
|
+
return url.toString();
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
function fireworksNextPageToken(value: unknown): string | undefined {
|
|
1102
|
+
if (value === undefined || value === null) return undefined;
|
|
1103
|
+
if (typeof value !== "string" || !value || value.length > 512) {
|
|
1104
|
+
throw new Error("Fireworks accounts listing returned an invalid page token.");
|
|
1105
|
+
}
|
|
1106
|
+
return value;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
function fireworksBillingSummaryUrl(accountId: string, startedAt: number): string {
|
|
1110
|
+
const dayMs = 24 * 60 * 60 * 1000;
|
|
1111
|
+
const dayFloor = (time: number) => `${new Date(time).toISOString().slice(0, 10)}T00:00:00Z`;
|
|
1112
|
+
const url = new URL(
|
|
1113
|
+
`/v1/accounts/${accountId}/billing/summary`,
|
|
1114
|
+
FIREWORKS_BILLING_SUMMARY_ORIGIN,
|
|
1115
|
+
);
|
|
1116
|
+
// The endpoint aggregates by UTC date; endTime is exclusive, so the window includes today
|
|
1117
|
+
// plus the preceding 29 dates.
|
|
1118
|
+
url.searchParams.set(
|
|
1119
|
+
"startTime",
|
|
1120
|
+
dayFloor(startedAt - (FIREWORKS_SPEND_WINDOW_DAYS - 1) * dayMs),
|
|
1121
|
+
);
|
|
1122
|
+
url.searchParams.set("endTime", dayFloor(startedAt + dayMs));
|
|
1123
|
+
return url.toString();
|
|
1124
|
+
}
|
|
1125
|
+
|
|
780
1126
|
function zaiMonitorUrl(baseUrl: string | undefined): string {
|
|
781
1127
|
const base = baseUrl?.trim();
|
|
782
1128
|
if (!base) throw new Error("Z.AI model base URL is unavailable.");
|
package/src/settings.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { constants } from "node:fs";
|
|
|
3
3
|
import { chmod, mkdir, open, rename, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import { basename, dirname, join } from "node:path";
|
|
5
5
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { isFireworksAccountId } from "./providers/fireworks.js";
|
|
6
7
|
|
|
7
8
|
export const USAGE_SETTINGS_FILE = "pi-usage.json";
|
|
8
9
|
export const MAX_USAGE_SETTINGS_BYTES = 64 * 1024;
|
|
@@ -10,6 +11,7 @@ export const MAX_USAGE_SETTINGS_BYTES = 64 * 1024;
|
|
|
10
11
|
export interface UsageSettings {
|
|
11
12
|
codexFastMode: boolean;
|
|
12
13
|
codexStatusResetCountdown: boolean;
|
|
14
|
+
fireworksAccountId?: string;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
export const DEFAULT_USAGE_SETTINGS: Readonly<UsageSettings> = Object.freeze({
|
|
@@ -60,6 +62,12 @@ export function normalizeUsageSettings(value: unknown): UsageSettings | undefine
|
|
|
60
62
|
) {
|
|
61
63
|
return undefined;
|
|
62
64
|
}
|
|
65
|
+
if (
|
|
66
|
+
Object.hasOwn(value, "fireworksAccountId") &&
|
|
67
|
+
!isFireworksAccountId(value.fireworksAccountId)
|
|
68
|
+
) {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
63
71
|
return {
|
|
64
72
|
codexFastMode:
|
|
65
73
|
typeof value.codexFastMode === "boolean"
|
|
@@ -69,6 +77,9 @@ export function normalizeUsageSettings(value: unknown): UsageSettings | undefine
|
|
|
69
77
|
typeof value.codexStatusResetCountdown === "boolean"
|
|
70
78
|
? value.codexStatusResetCountdown
|
|
71
79
|
: DEFAULT_USAGE_SETTINGS.codexStatusResetCountdown,
|
|
80
|
+
...(isFireworksAccountId(value.fireworksAccountId)
|
|
81
|
+
? { fireworksAccountId: value.fireworksAccountId }
|
|
82
|
+
: {}),
|
|
72
83
|
};
|
|
73
84
|
}
|
|
74
85
|
|
|
@@ -172,7 +183,11 @@ async function saveUsageSettingsPatch(
|
|
|
172
183
|
if (latest.kind === "invalid") {
|
|
173
184
|
throw new Error("Cannot overwrite an invalid pi-usage.json; repair it and reload first");
|
|
174
185
|
}
|
|
175
|
-
const document = { ...latest.document
|
|
186
|
+
const document = { ...latest.document };
|
|
187
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
188
|
+
if (value === undefined) delete document[key];
|
|
189
|
+
else document[key] = value;
|
|
190
|
+
}
|
|
176
191
|
const settings = normalizeUsageSettings(document);
|
|
177
192
|
if (!settings) throw new Error("Refusing to save invalid pi-usage settings");
|
|
178
193
|
const directory = dirname(path);
|
package/src/types.ts
CHANGED
|
@@ -55,6 +55,10 @@ export interface ResolvedUsageAuth {
|
|
|
55
55
|
model: PiModel;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
export interface UsageQuerySettings {
|
|
59
|
+
fireworksAccountId?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
58
62
|
export interface UsageProviderAdapter {
|
|
59
63
|
id: string;
|
|
60
64
|
displayName: string;
|
|
@@ -65,6 +69,7 @@ export interface UsageProviderAdapter {
|
|
|
65
69
|
signal: AbortSignal,
|
|
66
70
|
timeoutMs: number,
|
|
67
71
|
guard?: () => Promise<void>,
|
|
72
|
+
settings?: Readonly<UsageQuerySettings>,
|
|
68
73
|
): Promise<UsageReport>;
|
|
69
74
|
}
|
|
70
75
|
|
|
@@ -84,11 +89,26 @@ export type ProviderUsageState =
|
|
|
84
89
|
message: string;
|
|
85
90
|
};
|
|
86
91
|
|
|
92
|
+
export type BasetenBillingUsagePayload = {
|
|
93
|
+
dedicated_usage?: unknown;
|
|
94
|
+
model_apis_usage?: unknown;
|
|
95
|
+
training_usage?: unknown;
|
|
96
|
+
};
|
|
97
|
+
|
|
87
98
|
export type DeepSeekBalancePayload = {
|
|
88
99
|
is_available?: unknown;
|
|
89
100
|
balance_infos?: unknown;
|
|
90
101
|
};
|
|
91
102
|
|
|
103
|
+
export type FireworksAccountsPayload = {
|
|
104
|
+
accounts?: unknown;
|
|
105
|
+
nextPageToken?: unknown;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export type FireworksBillingSummaryPayload = {
|
|
109
|
+
lineItems?: unknown;
|
|
110
|
+
};
|
|
111
|
+
|
|
92
112
|
export type GitHubCopilotUsagePayload = {
|
|
93
113
|
login?: unknown;
|
|
94
114
|
copilot_plan?: unknown;
|
|
@@ -105,6 +125,30 @@ export type OpenRouterKeyPayload = {
|
|
|
105
125
|
data?: unknown;
|
|
106
126
|
};
|
|
107
127
|
|
|
128
|
+
export type MiniMaxUsagePayload = {
|
|
129
|
+
available_amount?: unknown;
|
|
130
|
+
balance_alert_switch?: unknown;
|
|
131
|
+
balance_alert_threshold?: unknown;
|
|
132
|
+
base_resp?: unknown;
|
|
133
|
+
cash_balance?: unknown;
|
|
134
|
+
credit_balance?: unknown;
|
|
135
|
+
model_remains?: unknown;
|
|
136
|
+
owed_amount?: unknown;
|
|
137
|
+
voucher_balance?: unknown;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export type MoonshotBalancePayload = {
|
|
141
|
+
code?: unknown;
|
|
142
|
+
data?: unknown;
|
|
143
|
+
scode?: unknown;
|
|
144
|
+
status?: unknown;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export type VercelAIGatewayCreditsPayload = {
|
|
148
|
+
balance?: unknown;
|
|
149
|
+
total_used?: unknown;
|
|
150
|
+
};
|
|
151
|
+
|
|
108
152
|
export type OpenCodeZenPayload = {
|
|
109
153
|
usage?: unknown;
|
|
110
154
|
};
|