@narumitw/pi-usage 0.59.0 → 0.60.1
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 +30 -25
- package/dist/index.ts +889 -338
- package/dist/index.ts.map +4 -4
- package/package.json +1 -1
- package/src/core.ts +28 -2
- package/src/format.ts +18 -8
- package/src/index.ts +14 -1
- package/src/providers/fireworks.ts +112 -0
- package/src/providers/zai.ts +109 -5
- package/src/query.ts +171 -175
- package/src/settings.ts +155 -8
- package/src/types.ts +50 -2
- package/src/usage-settings-ui.ts +88 -183
- package/src/usage-targets.ts +143 -0
- package/src/usage.ts +413 -99
package/src/query.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// This module intentionally keeps adapter transport beside resolved-auth and origin validation;
|
|
2
|
+
// separating that security boundary would duplicate request and redaction policy across providers.
|
|
1
3
|
import { randomBytes } from "node:crypto";
|
|
2
4
|
import { type ExtensionContext, readStoredCredential } from "@earendil-works/pi-coding-agent";
|
|
3
5
|
import { errorMessage, fingerprintResolvedAuth, redactUsageError } from "./core.js";
|
|
@@ -8,11 +10,7 @@ import {
|
|
|
8
10
|
import { normalizeBasetenBillingUsagePayload } from "./providers/baseten.js";
|
|
9
11
|
import { normalizeCodexBackendPayload } from "./providers/codex.js";
|
|
10
12
|
import { normalizeDeepSeekBalancePayload } from "./providers/deepseek.js";
|
|
11
|
-
import {
|
|
12
|
-
isFireworksAccountId,
|
|
13
|
-
normalizeFireworksAccountsPayload,
|
|
14
|
-
normalizeFireworksBillingSummaryPayload,
|
|
15
|
-
} from "./providers/fireworks.js";
|
|
13
|
+
import { createFireworksAdapter } from "./providers/fireworks.js";
|
|
16
14
|
import { normalizeGitHubCopilotUsagePayload } from "./providers/github-copilot.js";
|
|
17
15
|
import { normalizeKimiCodingUsagePayload } from "./providers/kimi-coding.js";
|
|
18
16
|
import {
|
|
@@ -25,13 +23,11 @@ import { normalizeOpenCodeZenPayload } from "./providers/opencode-zen.js";
|
|
|
25
23
|
import { normalizeOpenRouterKeyPayload } from "./providers/openrouter.js";
|
|
26
24
|
import { normalizeVercelAIGatewayCreditsPayload } from "./providers/vercel-ai-gateway.js";
|
|
27
25
|
import { normalizeXaiBillingPayload } from "./providers/xai.js";
|
|
28
|
-
import { normalizeZaiQuotaPayload } from "./providers/zai.js";
|
|
26
|
+
import { normalizeZaiQuotaPayload, normalizeZaiSubscriptionPayload } from "./providers/zai.js";
|
|
29
27
|
import type {
|
|
30
28
|
BasetenBillingUsagePayload,
|
|
31
29
|
CodexBackendPayload,
|
|
32
30
|
DeepSeekBalancePayload,
|
|
33
|
-
FireworksAccountsPayload,
|
|
34
|
-
FireworksBillingSummaryPayload,
|
|
35
31
|
GitHubCopilotUsagePayload,
|
|
36
32
|
KimiCodingUsagePayload,
|
|
37
33
|
MiniMaxUsagePayload,
|
|
@@ -43,19 +39,20 @@ import type {
|
|
|
43
39
|
UsageProviderAdapter,
|
|
44
40
|
UsageQuerySettings,
|
|
45
41
|
UsageReport,
|
|
42
|
+
UsageRequestGuard,
|
|
46
43
|
VercelAIGatewayCreditsPayload,
|
|
47
44
|
XaiBillingPayload,
|
|
48
45
|
XaiUserPayload,
|
|
46
|
+
ZaiPlanInfo,
|
|
49
47
|
ZaiQuotaPayload,
|
|
48
|
+
ZaiSubscriptionPayload,
|
|
50
49
|
} from "./types.js";
|
|
50
|
+
import { resolveUsageTarget } from "./usage-targets.js";
|
|
51
51
|
|
|
52
52
|
const BASETEN_BILLING_USAGE_URL = "https://api.baseten.co/v1/billing/usage_summary";
|
|
53
53
|
const BASETEN_USAGE_WINDOW_DAYS = 30;
|
|
54
54
|
const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
55
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;
|
|
59
56
|
const GITHUB_COPILOT_USAGE_URL = "https://api.github.com/copilot_internal/user";
|
|
60
57
|
const OPENROUTER_KEY_URL = "https://openrouter.ai/api/v1/key";
|
|
61
58
|
const VERCEL_AI_GATEWAY_CREDITS_URL = "https://ai-gateway.vercel.sh/v1/credits";
|
|
@@ -82,8 +79,6 @@ const MAX_ERROR_BODY_BYTES = 4 * 1024;
|
|
|
82
79
|
|
|
83
80
|
export const AUTH_FINGERPRINT_SALT = randomBytes(32);
|
|
84
81
|
|
|
85
|
-
export type UsageRequestGuard = () => Promise<void>;
|
|
86
|
-
|
|
87
82
|
export const SUPPORTED_ADAPTERS: readonly UsageProviderAdapter[] = [
|
|
88
83
|
{
|
|
89
84
|
id: "baseten",
|
|
@@ -199,35 +194,7 @@ export const SUPPORTED_ADAPTERS: readonly UsageProviderAdapter[] = [
|
|
|
199
194
|
return normalizeVercelAIGatewayCreditsPayload(payload, Date.now());
|
|
200
195
|
},
|
|
201
196
|
},
|
|
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
|
-
},
|
|
197
|
+
createFireworksAdapter(fetchProviderJson),
|
|
231
198
|
{
|
|
232
199
|
id: "opencode-go",
|
|
233
200
|
displayName: "OpenCode Go",
|
|
@@ -295,35 +262,16 @@ export const SUPPORTED_ADAPTERS: readonly UsageProviderAdapter[] = [
|
|
|
295
262
|
id: "zai",
|
|
296
263
|
displayName: "Z.AI",
|
|
297
264
|
semantics: { kind: "consumer-subscription", label: "GLM Coding Plan usage" },
|
|
298
|
-
async query(auth, signal, timeoutMs) {
|
|
299
|
-
|
|
300
|
-
zaiMonitorUrl(auth.model.baseUrl),
|
|
301
|
-
zaiMonitorAuth(auth),
|
|
302
|
-
signal,
|
|
303
|
-
timeoutMs,
|
|
304
|
-
"Z.AI quota endpoint",
|
|
305
|
-
);
|
|
306
|
-
return normalizeZaiQuotaPayload("zai", "Z.AI", payload as ZaiQuotaPayload, Date.now());
|
|
265
|
+
async query(auth, signal, timeoutMs, guard) {
|
|
266
|
+
return queryZaiUsage("zai", "Z.AI", auth, signal, timeoutMs, guard);
|
|
307
267
|
},
|
|
308
268
|
},
|
|
309
269
|
{
|
|
310
270
|
id: "zai-coding-cn",
|
|
311
271
|
displayName: "Z.AI Coding CN",
|
|
312
272
|
semantics: { kind: "consumer-subscription", label: "GLM Coding Plan usage" },
|
|
313
|
-
async query(auth, signal, timeoutMs) {
|
|
314
|
-
|
|
315
|
-
zaiMonitorUrl(auth.model.baseUrl),
|
|
316
|
-
zaiMonitorAuth(auth),
|
|
317
|
-
signal,
|
|
318
|
-
timeoutMs,
|
|
319
|
-
"Z.AI Coding CN quota endpoint",
|
|
320
|
-
);
|
|
321
|
-
return normalizeZaiQuotaPayload(
|
|
322
|
-
"zai-coding-cn",
|
|
323
|
-
"Z.AI Coding CN",
|
|
324
|
-
payload as ZaiQuotaPayload,
|
|
325
|
-
Date.now(),
|
|
326
|
-
);
|
|
273
|
+
async query(auth, signal, timeoutMs, guard) {
|
|
274
|
+
return queryZaiUsage("zai-coding-cn", "Z.AI Coding CN", auth, signal, timeoutMs, guard);
|
|
327
275
|
},
|
|
328
276
|
},
|
|
329
277
|
];
|
|
@@ -419,6 +367,12 @@ export async function resolveUsageAuth(
|
|
|
419
367
|
if (!model) return undefined;
|
|
420
368
|
// SAFETY: Pi exposes the required auth methods at runtime, and checks below narrow them before use.
|
|
421
369
|
const registry = ctx.modelRegistry as unknown as UsageAuthRegistry;
|
|
370
|
+
const provider = registry.getProvider?.(adapter.id);
|
|
371
|
+
if (provider?.baseUrl && !hasOfficialUrlOrigin(provider.baseUrl, adapter.id)) {
|
|
372
|
+
throw new Error(
|
|
373
|
+
`${adapter.displayName} usage cannot send an overridden provider credential to the official usage endpoint.`,
|
|
374
|
+
);
|
|
375
|
+
}
|
|
422
376
|
let modelAuth: RequestAuth | undefined;
|
|
423
377
|
const currentModel = ctx.model?.provider === adapter.id ? ctx.model : undefined;
|
|
424
378
|
const resolveCurrentModelAuth = async (): Promise<RequestAuth | undefined> => {
|
|
@@ -446,8 +400,46 @@ export async function resolveUsageAuth(
|
|
|
446
400
|
// Providers with credential-change retries read selected-model auth last so a rotation during
|
|
447
401
|
// provider-origin validation cannot leave the earlier credential queued for the usage request.
|
|
448
402
|
if (resolveSelectedAuthLast) modelAuth = await resolveCurrentModelAuth();
|
|
403
|
+
if (modelAuth?.baseUrl && !hasOfficialUrlOrigin(modelAuth.baseUrl, adapter.id)) {
|
|
404
|
+
throw new Error(
|
|
405
|
+
`${adapter.displayName} usage cannot send model-resolved proxy credentials to the official usage endpoint.`,
|
|
406
|
+
);
|
|
407
|
+
}
|
|
449
408
|
const auth = modelAuth ?? providerResult?.auth;
|
|
450
409
|
if (!auth) return undefined;
|
|
410
|
+
const finalize = (resolved: ResolvedUsageAuth): ResolvedUsageAuth => {
|
|
411
|
+
const preservedAuth = { ...(providerResult?.auth ?? auth) };
|
|
412
|
+
const env = providerResult?.env ?? modelAuth?.env;
|
|
413
|
+
const source = providerResult?.source;
|
|
414
|
+
const effectiveBaseUrl =
|
|
415
|
+
modelAuth?.baseUrl ?? providerResult?.auth.baseUrl ?? provider?.baseUrl ?? model.baseUrl;
|
|
416
|
+
const redactionInputs = [
|
|
417
|
+
preservedAuth.apiKey,
|
|
418
|
+
...Object.values(preservedAuth.headers ?? {}),
|
|
419
|
+
...Object.values(env ?? {}),
|
|
420
|
+
modelAuth?.apiKey,
|
|
421
|
+
...Object.values(modelAuth?.headers ?? {}),
|
|
422
|
+
].filter((value): value is string => typeof value === "string" && value.length > 0);
|
|
423
|
+
return {
|
|
424
|
+
...resolved,
|
|
425
|
+
auth: preservedAuth,
|
|
426
|
+
...(env ? { env: { ...env } } : {}),
|
|
427
|
+
...(source ? { source } : {}),
|
|
428
|
+
effectiveBaseUrl,
|
|
429
|
+
secrets: [...new Set([...resolved.secrets, ...redactionInputs])],
|
|
430
|
+
fingerprint: fingerprintResolvedAuth(
|
|
431
|
+
{
|
|
432
|
+
apiKey: resolved.apiKey,
|
|
433
|
+
headers: resolved.headers,
|
|
434
|
+
baseUrl: effectiveBaseUrl,
|
|
435
|
+
env,
|
|
436
|
+
source,
|
|
437
|
+
providerAuth: preservedAuth,
|
|
438
|
+
},
|
|
439
|
+
salt,
|
|
440
|
+
),
|
|
441
|
+
};
|
|
442
|
+
};
|
|
451
443
|
if (adapter.id === "github-copilot") {
|
|
452
444
|
const offered = candidateReader
|
|
453
445
|
? candidateReader(ctx, adapter.id)
|
|
@@ -455,12 +447,14 @@ export async function resolveUsageAuth(
|
|
|
455
447
|
if (!offered.ok) {
|
|
456
448
|
throw new Error("GitHub Copilot OAuth credential discovery failed closed.");
|
|
457
449
|
}
|
|
458
|
-
return
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
450
|
+
return finalize(
|
|
451
|
+
resolveGitHubCopilotUsageAuth(
|
|
452
|
+
auth,
|
|
453
|
+
model,
|
|
454
|
+
salt,
|
|
455
|
+
offered.candidates,
|
|
456
|
+
offered.offeredCount === 0,
|
|
457
|
+
),
|
|
464
458
|
);
|
|
465
459
|
}
|
|
466
460
|
if (adapter.id === "xai") {
|
|
@@ -468,7 +462,7 @@ export async function resolveUsageAuth(
|
|
|
468
462
|
? candidateReader(ctx, adapter.id)
|
|
469
463
|
: fallbackOAuthCredentialCandidates(adapter.id, credentialReader);
|
|
470
464
|
if (!offered.ok) throw new Error("xAI OAuth credential discovery failed closed.");
|
|
471
|
-
return resolveXaiUsageAuth(auth, model, salt, offered.candidates);
|
|
465
|
+
return finalize(resolveXaiUsageAuth(auth, model, salt, offered.candidates));
|
|
472
466
|
}
|
|
473
467
|
if (adapter.id === "deepseek") {
|
|
474
468
|
const resolvedAuthorization = authorizationFrom(auth);
|
|
@@ -476,10 +470,10 @@ export async function resolveUsageAuth(
|
|
|
476
470
|
if (!access) throw new Error("DeepSeek API balance requires Bearer authentication.");
|
|
477
471
|
const authorization = `Bearer ${access}`;
|
|
478
472
|
const headers = { Authorization: authorization };
|
|
479
|
-
return {
|
|
473
|
+
return finalize({
|
|
480
474
|
apiKey: access,
|
|
481
475
|
headers,
|
|
482
|
-
fingerprint:
|
|
476
|
+
fingerprint: "",
|
|
483
477
|
secrets: [
|
|
484
478
|
access,
|
|
485
479
|
auth.apiKey,
|
|
@@ -488,7 +482,7 @@ export async function resolveUsageAuth(
|
|
|
488
482
|
authorization,
|
|
489
483
|
].filter((value): value is string => Boolean(value)),
|
|
490
484
|
model,
|
|
491
|
-
};
|
|
485
|
+
});
|
|
492
486
|
}
|
|
493
487
|
const authorization = authorizationFrom(auth);
|
|
494
488
|
if (!authorization) return undefined;
|
|
@@ -496,13 +490,13 @@ export async function resolveUsageAuth(
|
|
|
496
490
|
const secrets = [auth.apiKey, headerValue(auth.headers, "Authorization"), authorization].filter(
|
|
497
491
|
(value): value is string => Boolean(value),
|
|
498
492
|
);
|
|
499
|
-
return {
|
|
493
|
+
return finalize({
|
|
500
494
|
apiKey: auth.apiKey,
|
|
501
495
|
headers,
|
|
502
|
-
fingerprint:
|
|
496
|
+
fingerprint: "",
|
|
503
497
|
secrets,
|
|
504
498
|
model,
|
|
505
|
-
};
|
|
499
|
+
});
|
|
506
500
|
}
|
|
507
501
|
|
|
508
502
|
export async function queryProviderUsage(
|
|
@@ -511,10 +505,46 @@ export async function queryProviderUsage(
|
|
|
511
505
|
signal: AbortSignal,
|
|
512
506
|
timeoutMs: number,
|
|
513
507
|
guard?: UsageRequestGuard,
|
|
514
|
-
|
|
508
|
+
targetOrSettings?: string | Readonly<UsageQuerySettings>,
|
|
515
509
|
): Promise<UsageReport> {
|
|
510
|
+
const startedAt = Date.now();
|
|
511
|
+
let targetId =
|
|
512
|
+
typeof targetOrSettings === "string"
|
|
513
|
+
? targetOrSettings
|
|
514
|
+
: adapter.id === "fireworks"
|
|
515
|
+
? targetOrSettings?.fireworksAccountId
|
|
516
|
+
: undefined;
|
|
517
|
+
let resolvedLegacyFireworksTarget = false;
|
|
516
518
|
try {
|
|
517
|
-
|
|
519
|
+
if (
|
|
520
|
+
adapter.id === "fireworks" &&
|
|
521
|
+
typeof targetOrSettings !== "string" &&
|
|
522
|
+
adapter.targets &&
|
|
523
|
+
guard
|
|
524
|
+
) {
|
|
525
|
+
const target = await resolveUsageTarget(
|
|
526
|
+
adapter,
|
|
527
|
+
auth,
|
|
528
|
+
targetId,
|
|
529
|
+
signal,
|
|
530
|
+
remainingTimeout(timeoutMs, startedAt, "resolving the Fireworks account"),
|
|
531
|
+
guard,
|
|
532
|
+
);
|
|
533
|
+
if (target.kind === "selection-required") {
|
|
534
|
+
throw new Error("Fireworks account selection is required.");
|
|
535
|
+
}
|
|
536
|
+
targetId = target.targetId;
|
|
537
|
+
resolvedLegacyFireworksTarget = true;
|
|
538
|
+
}
|
|
539
|
+
return await adapter.query(
|
|
540
|
+
auth,
|
|
541
|
+
signal,
|
|
542
|
+
resolvedLegacyFireworksTarget
|
|
543
|
+
? remainingTimeout(timeoutMs, startedAt, `querying ${adapter.displayName} usage`)
|
|
544
|
+
: timeoutMs,
|
|
545
|
+
guard,
|
|
546
|
+
targetId,
|
|
547
|
+
);
|
|
518
548
|
} catch (error) {
|
|
519
549
|
if (isStaleExtensionContextError(error) || isAbortError(error)) throw error;
|
|
520
550
|
throw new Error(redactUsageError(errorMessage(error), auth.secrets));
|
|
@@ -713,17 +743,22 @@ async function readBoundedResponse(
|
|
|
713
743
|
type RequestAuth = {
|
|
714
744
|
apiKey?: string;
|
|
715
745
|
headers?: Record<string, string | null>;
|
|
746
|
+
baseUrl?: string;
|
|
747
|
+
env?: Record<string, string>;
|
|
716
748
|
};
|
|
717
749
|
|
|
718
750
|
type StoredCredentialReader = (providerId: string) => unknown;
|
|
719
751
|
|
|
720
752
|
type UsageAuthRegistry = {
|
|
753
|
+
getProvider?(providerId: string): { baseUrl?: string } | undefined;
|
|
721
754
|
getApiKeyAndHeaders?(
|
|
722
755
|
model: PiModel,
|
|
723
756
|
): Promise<({ ok: true } & RequestAuth) | { ok: false; error: string }>;
|
|
724
757
|
getProviderAuth?(providerId: string): Promise<
|
|
725
758
|
| {
|
|
726
|
-
auth: RequestAuth
|
|
759
|
+
auth: RequestAuth;
|
|
760
|
+
env?: Record<string, string>;
|
|
761
|
+
source?: string;
|
|
727
762
|
}
|
|
728
763
|
| undefined
|
|
729
764
|
>;
|
|
@@ -919,7 +954,7 @@ function hasOfficialUrlOrigin(value: string, providerId: string): boolean {
|
|
|
919
954
|
}
|
|
920
955
|
if (providerId === "openai-codex") return url.origin === "https://chatgpt.com";
|
|
921
956
|
if (providerId === "deepseek") return url.origin === "https://api.deepseek.com";
|
|
922
|
-
if (providerId === "fireworks") return url.origin ===
|
|
957
|
+
if (providerId === "fireworks") return url.origin === "https://api.fireworks.ai";
|
|
923
958
|
if (providerId === "openrouter") return url.origin === "https://openrouter.ai";
|
|
924
959
|
if (providerId === "vercel-ai-gateway") return url.origin === "https://ai-gateway.vercel.sh";
|
|
925
960
|
if (providerId === "opencode-go") return url.origin === "https://opencode.ai";
|
|
@@ -1031,102 +1066,14 @@ function remainingTimeout(
|
|
|
1031
1066
|
return remaining;
|
|
1032
1067
|
}
|
|
1033
1068
|
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
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();
|
|
1069
|
+
function zaiOrigin(baseUrl: string | undefined): string {
|
|
1070
|
+
const base = baseUrl?.trim();
|
|
1071
|
+
if (!base) throw new Error("Z.AI model base URL is unavailable.");
|
|
1072
|
+
return new URL(base).origin;
|
|
1124
1073
|
}
|
|
1125
1074
|
|
|
1126
1075
|
function zaiMonitorUrl(baseUrl: string | undefined): string {
|
|
1127
|
-
|
|
1128
|
-
if (!base) throw new Error("Z.AI model base URL is unavailable.");
|
|
1129
|
-
return `${new URL(base).origin}/api/monitor/usage/quota/limit`;
|
|
1076
|
+
return `${zaiOrigin(baseUrl)}/api/monitor/usage/quota/limit`;
|
|
1130
1077
|
}
|
|
1131
1078
|
|
|
1132
1079
|
function zaiMonitorAuth(auth: ResolvedUsageAuth): ResolvedUsageAuth {
|
|
@@ -1137,6 +1084,55 @@ function zaiMonitorAuth(auth: ResolvedUsageAuth): ResolvedUsageAuth {
|
|
|
1137
1084
|
return { ...auth, headers: { ...auth.headers, Authorization: token } };
|
|
1138
1085
|
}
|
|
1139
1086
|
|
|
1087
|
+
async function queryZaiUsage(
|
|
1088
|
+
providerId: "zai" | "zai-coding-cn",
|
|
1089
|
+
providerName: string,
|
|
1090
|
+
auth: ResolvedUsageAuth,
|
|
1091
|
+
signal: AbortSignal,
|
|
1092
|
+
timeoutMs: number,
|
|
1093
|
+
guard: UsageRequestGuard | undefined,
|
|
1094
|
+
): Promise<UsageReport> {
|
|
1095
|
+
if (!guard) throw new Error("Z.AI usage requires request-boundary revalidation.");
|
|
1096
|
+
const startedAt = Date.now();
|
|
1097
|
+
await guard();
|
|
1098
|
+
const payload = (await fetchProviderJson(
|
|
1099
|
+
zaiMonitorUrl(auth.model.baseUrl),
|
|
1100
|
+
zaiMonitorAuth(auth),
|
|
1101
|
+
signal,
|
|
1102
|
+
remainingTimeout(timeoutMs, startedAt, `fetching ${providerName} quota`),
|
|
1103
|
+
`${providerName} quota endpoint`,
|
|
1104
|
+
)) as ZaiQuotaPayload;
|
|
1105
|
+
await guard();
|
|
1106
|
+
const planTimeoutMs = timeoutMs - (Date.now() - startedAt);
|
|
1107
|
+
const plan = await fetchZaiPlan(providerName, auth, signal, planTimeoutMs);
|
|
1108
|
+
return normalizeZaiQuotaPayload(providerId, providerName, payload, Date.now(), plan);
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
// The subscription endpoint is undocumented and may not exist on every official origin. It only
|
|
1112
|
+
// contributes the plan name and renewal date, so any non-abort failure is swallowed instead of
|
|
1113
|
+
// blanking the required quota report.
|
|
1114
|
+
async function fetchZaiPlan(
|
|
1115
|
+
providerName: string,
|
|
1116
|
+
auth: ResolvedUsageAuth,
|
|
1117
|
+
signal: AbortSignal,
|
|
1118
|
+
timeoutMs: number,
|
|
1119
|
+
): Promise<ZaiPlanInfo | undefined> {
|
|
1120
|
+
if (timeoutMs <= 0 || signal.aborted) return undefined;
|
|
1121
|
+
try {
|
|
1122
|
+
const payload = (await fetchProviderJson(
|
|
1123
|
+
`${zaiOrigin(auth.model.baseUrl)}/api/biz/subscription/list`,
|
|
1124
|
+
zaiMonitorAuth(auth),
|
|
1125
|
+
signal,
|
|
1126
|
+
timeoutMs,
|
|
1127
|
+
`${providerName} plan endpoint`,
|
|
1128
|
+
)) as ZaiSubscriptionPayload;
|
|
1129
|
+
return normalizeZaiSubscriptionPayload(payload);
|
|
1130
|
+
} catch (error) {
|
|
1131
|
+
if (isAbortError(error)) throw error;
|
|
1132
|
+
return undefined;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1140
1136
|
function isAbortError(error: unknown): boolean {
|
|
1141
1137
|
return error instanceof Error && error.name === "AbortError";
|
|
1142
1138
|
}
|