@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/package.json
CHANGED
package/src/core.ts
CHANGED
|
@@ -54,13 +54,39 @@ export class UsageCache {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
export function fingerprintResolvedAuth(
|
|
57
|
-
auth: {
|
|
57
|
+
auth: {
|
|
58
|
+
apiKey?: string;
|
|
59
|
+
headers?: Record<string, string | null>;
|
|
60
|
+
baseUrl?: string;
|
|
61
|
+
env?: Record<string, string>;
|
|
62
|
+
source?: string;
|
|
63
|
+
providerAuth?: {
|
|
64
|
+
apiKey?: string;
|
|
65
|
+
headers?: Record<string, string | null>;
|
|
66
|
+
baseUrl?: string;
|
|
67
|
+
};
|
|
68
|
+
},
|
|
58
69
|
salt: Uint8Array,
|
|
59
70
|
): string {
|
|
60
71
|
const headers = Object.entries(auth.headers ?? {})
|
|
61
72
|
.map(([name, value]) => [name.toLowerCase(), value] as const)
|
|
62
73
|
.sort(([left], [right]) => left.localeCompare(right));
|
|
63
|
-
const
|
|
74
|
+
const env = Object.entries(auth.env ?? {}).sort(([left], [right]) => left.localeCompare(right));
|
|
75
|
+
const providerHeaders = Object.entries(auth.providerAuth?.headers ?? {})
|
|
76
|
+
.map(([name, value]) => [name.toLowerCase(), value] as const)
|
|
77
|
+
.sort(([left], [right]) => left.localeCompare(right));
|
|
78
|
+
const canonical = JSON.stringify({
|
|
79
|
+
apiKey: auth.apiKey ?? "",
|
|
80
|
+
headers,
|
|
81
|
+
baseUrl: auth.baseUrl ?? "",
|
|
82
|
+
env,
|
|
83
|
+
source: auth.source ?? "",
|
|
84
|
+
providerAuth: {
|
|
85
|
+
apiKey: auth.providerAuth?.apiKey ?? "",
|
|
86
|
+
headers: providerHeaders,
|
|
87
|
+
baseUrl: auth.providerAuth?.baseUrl ?? "",
|
|
88
|
+
},
|
|
89
|
+
});
|
|
64
90
|
return createHmac("sha256", salt).update(canonical).digest("hex");
|
|
65
91
|
}
|
|
66
92
|
|
package/src/format.ts
CHANGED
|
@@ -94,6 +94,9 @@ export function formatProviderStates(states: readonly ProviderUsageState[]): str
|
|
|
94
94
|
.map((state) => {
|
|
95
95
|
if (state.status === "ready") return formatUsageReport(state.report, state.displayState);
|
|
96
96
|
const label = state.displayState === "current" ? "Current" : "Configured";
|
|
97
|
+
if (state.status === "selection-required") {
|
|
98
|
+
return `${state.providerName} · ${label}\nSelection required: choose this provider's ${state.singularLabel} by viewing it individually.`;
|
|
99
|
+
}
|
|
97
100
|
const status =
|
|
98
101
|
state.status === "auth-unavailable"
|
|
99
102
|
? "Authentication unavailable"
|
|
@@ -269,6 +272,10 @@ function formatOpenRouterReport(lines: string[], report: UsageReport): void {
|
|
|
269
272
|
|
|
270
273
|
function formatOpenCodeZenReport(lines: string[], report: UsageReport): void {
|
|
271
274
|
for (const bucket of report.buckets) {
|
|
275
|
+
if (bucket.unit === "percent" && bucket.used !== undefined) {
|
|
276
|
+
lines.push(`${`${bucket.label}:`.padEnd(VALUE_COLUMN)}${formatPercentBucket(bucket)}`);
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
272
279
|
const reset = bucket.resetsAt ? ` (resets ${formatReset(bucket.resetsAt)})` : "";
|
|
273
280
|
const used = bucket.used ?? "unavailable";
|
|
274
281
|
lines.push(`${`${bucket.label}:`.padEnd(VALUE_COLUMN)}${used}% used${reset}`);
|
|
@@ -469,8 +476,7 @@ function formatXaiReport(lines: string[], report: UsageReport): void {
|
|
|
469
476
|
if (included) {
|
|
470
477
|
let value = "unavailable";
|
|
471
478
|
if (included.unit === "percent" && included.used !== undefined) {
|
|
472
|
-
value =
|
|
473
|
-
if (included.remaining !== undefined) value += ` · ${included.remaining}% left`;
|
|
479
|
+
value = formatPercentBar(included);
|
|
474
480
|
} else if (included.used !== undefined) {
|
|
475
481
|
value = `${formatUsd(included.used)} used`;
|
|
476
482
|
if (included.limit !== undefined) value += ` of ${formatUsd(included.limit)}`;
|
|
@@ -497,12 +503,13 @@ function formatXaiReport(lines: string[], report: UsageReport): void {
|
|
|
497
503
|
|
|
498
504
|
function formatZaiReport(lines: string[], report: UsageReport): void {
|
|
499
505
|
for (const bucket of report.buckets) {
|
|
506
|
+
if (bucket.unit === "percent" && bucket.used !== undefined) {
|
|
507
|
+
lines.push(`${`${bucket.label}:`.padEnd(VALUE_COLUMN)}${formatPercentBucket(bucket)}`);
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
500
510
|
const reset = bucket.resetsAt ? ` (resets ${formatReset(bucket.resetsAt)})` : "";
|
|
501
511
|
let value = "unavailable";
|
|
502
|
-
if (bucket.
|
|
503
|
-
value = `${bucket.used}% used`;
|
|
504
|
-
if (bucket.remaining !== undefined) value += ` · ${bucket.remaining}% left`;
|
|
505
|
-
} else if (bucket.used !== undefined && bucket.limit !== undefined) {
|
|
512
|
+
if (bucket.used !== undefined && bucket.limit !== undefined) {
|
|
506
513
|
value = `${bucket.used} of ${bucket.limit} used`;
|
|
507
514
|
if (bucket.remaining !== undefined) value += ` · ${bucket.remaining} left`;
|
|
508
515
|
} else if (bucket.used !== undefined) {
|
|
@@ -637,10 +644,13 @@ function compactLimitLabel(label: string): string {
|
|
|
637
644
|
}
|
|
638
645
|
|
|
639
646
|
function formatPercentBucket(bucket: UsageBucket): string {
|
|
647
|
+
return `${formatPercentBar(bucket)}${bucket.resetsAt ? ` (resets ${formatReset(bucket.resetsAt)})` : ""}`;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function formatPercentBar(bucket: UsageBucket): string {
|
|
640
651
|
const remaining = clampPercent(bucket.remaining ?? 0);
|
|
641
652
|
const filled = Math.round((remaining / 100) * BAR_SEGMENTS);
|
|
642
|
-
|
|
643
|
-
return `[${"█".repeat(filled)}${"░".repeat(BAR_SEGMENTS - filled)}] ${remaining.toFixed(0)}% left${reset}`;
|
|
653
|
+
return `[${"█".repeat(filled)}${"░".repeat(BAR_SEGMENTS - filled)}] ${remaining.toFixed(0)}% left`;
|
|
644
654
|
}
|
|
645
655
|
|
|
646
656
|
function formatWindowLabel(
|
package/src/index.ts
CHANGED
|
@@ -36,6 +36,7 @@ export { normalizeBasetenBillingUsagePayload } from "./providers/baseten.js";
|
|
|
36
36
|
export { normalizeCodexBackendPayload } from "./providers/codex.js";
|
|
37
37
|
export { normalizeDeepSeekBalancePayload } from "./providers/deepseek.js";
|
|
38
38
|
export {
|
|
39
|
+
createFireworksAdapter,
|
|
39
40
|
normalizeFireworksAccountsPayload,
|
|
40
41
|
normalizeFireworksBillingSummaryPayload,
|
|
41
42
|
} from "./providers/fireworks.js";
|
|
@@ -52,7 +53,7 @@ export { normalizeOpenCodeZenPayload } from "./providers/opencode-zen.js";
|
|
|
52
53
|
export { normalizeOpenRouterKeyPayload } from "./providers/openrouter.js";
|
|
53
54
|
export { normalizeVercelAIGatewayCreditsPayload } from "./providers/vercel-ai-gateway.js";
|
|
54
55
|
export { normalizeXaiBillingPayload } from "./providers/xai.js";
|
|
55
|
-
export { normalizeZaiQuotaPayload } from "./providers/zai.js";
|
|
56
|
+
export { normalizeZaiQuotaPayload, normalizeZaiSubscriptionPayload } from "./providers/zai.js";
|
|
56
57
|
export {
|
|
57
58
|
adapterForProvider,
|
|
58
59
|
isStaleExtensionContextError,
|
|
@@ -67,6 +68,7 @@ export type {
|
|
|
67
68
|
UsageSettings,
|
|
68
69
|
UsageSettingsRuntime,
|
|
69
70
|
UsageSettingsState,
|
|
71
|
+
UsageTargetPublicationCheck,
|
|
70
72
|
} from "./settings.js";
|
|
71
73
|
export {
|
|
72
74
|
createUsageSettingsRuntime,
|
|
@@ -90,13 +92,24 @@ export type {
|
|
|
90
92
|
UsageMetric,
|
|
91
93
|
UsageModel,
|
|
92
94
|
UsageProviderAdapter,
|
|
95
|
+
UsageProviderTarget,
|
|
93
96
|
UsageQuerySettings,
|
|
94
97
|
UsageReport,
|
|
98
|
+
UsageRequestGuard,
|
|
95
99
|
UsageSemantics,
|
|
96
100
|
UsageSemanticsKind,
|
|
101
|
+
UsageTargetResolver,
|
|
97
102
|
UsageUnit,
|
|
98
103
|
VercelAIGatewayCreditsPayload,
|
|
99
104
|
XaiBillingPayload,
|
|
100
105
|
XaiUserPayload,
|
|
101
106
|
} from "./types.js";
|
|
102
107
|
export { default } from "./usage.js";
|
|
108
|
+
export type { UsageTargetResolution, UsageTargetSelectOptions } from "./usage-targets.js";
|
|
109
|
+
export {
|
|
110
|
+
createUsageTargetSelectOptions,
|
|
111
|
+
isBoundedTargetId,
|
|
112
|
+
listUsageTargets,
|
|
113
|
+
normalizeUsageTargets,
|
|
114
|
+
resolveUsageTarget,
|
|
115
|
+
} from "./usage-targets.js";
|
|
@@ -2,7 +2,9 @@ import { sanitizeDisplayText } from "../core.js";
|
|
|
2
2
|
import type {
|
|
3
3
|
FireworksAccountsPayload,
|
|
4
4
|
FireworksBillingSummaryPayload,
|
|
5
|
+
ResolvedUsageAuth,
|
|
5
6
|
UsageMetric,
|
|
7
|
+
UsageProviderAdapter,
|
|
6
8
|
UsageReport,
|
|
7
9
|
} from "../types.js";
|
|
8
10
|
|
|
@@ -14,6 +16,9 @@ const INT64_MIN = -(2n ** 63n);
|
|
|
14
16
|
const INT64_MAX = 2n ** 63n - 1n;
|
|
15
17
|
const MAX_UNITS_CHARS = 20;
|
|
16
18
|
const MAX_NANOS_CHARS = 11;
|
|
19
|
+
const FIREWORKS_BILLING_SUMMARY_ORIGIN = "https://api.fireworks.ai";
|
|
20
|
+
const FIREWORKS_SPEND_WINDOW_DAYS = 30;
|
|
21
|
+
const FIREWORKS_MAX_ACCOUNT_PAGES = 5;
|
|
17
22
|
|
|
18
23
|
const SERIES_KEYS = ["serverless", "dedicated", "training", "other"] as const;
|
|
19
24
|
type SeriesKey = (typeof SERIES_KEYS)[number];
|
|
@@ -53,6 +58,77 @@ export function normalizeFireworksAccountsPayload(payload: FireworksAccountsPayl
|
|
|
53
58
|
return accounts;
|
|
54
59
|
}
|
|
55
60
|
|
|
61
|
+
type FetchProviderJson = (
|
|
62
|
+
url: string,
|
|
63
|
+
auth: ResolvedUsageAuth,
|
|
64
|
+
signal: AbortSignal,
|
|
65
|
+
timeoutMs: number,
|
|
66
|
+
description: string,
|
|
67
|
+
request?: { redirect?: RequestRedirect },
|
|
68
|
+
) => Promise<Record<string, unknown>>;
|
|
69
|
+
|
|
70
|
+
export function createFireworksAdapter(fetchProviderJson: FetchProviderJson): UsageProviderAdapter {
|
|
71
|
+
return {
|
|
72
|
+
id: "fireworks",
|
|
73
|
+
displayName: "Fireworks",
|
|
74
|
+
semantics: { kind: "api-key", label: "Fireworks API spend" },
|
|
75
|
+
targets: {
|
|
76
|
+
singularLabel: "account",
|
|
77
|
+
pluralLabel: "accounts",
|
|
78
|
+
async list(auth, signal, timeoutMs, guard) {
|
|
79
|
+
const startedAt = Date.now();
|
|
80
|
+
const accounts: string[] = [];
|
|
81
|
+
let pageToken: string | undefined;
|
|
82
|
+
for (let page = 0; page < FIREWORKS_MAX_ACCOUNT_PAGES; page += 1) {
|
|
83
|
+
await guard();
|
|
84
|
+
const payload = (await fetchProviderJson(
|
|
85
|
+
fireworksAccountsUrl(pageToken),
|
|
86
|
+
auth,
|
|
87
|
+
signal,
|
|
88
|
+
remainingTimeout(timeoutMs, startedAt, "fetching Fireworks accounts"),
|
|
89
|
+
"Fireworks accounts endpoint",
|
|
90
|
+
{ redirect: "error" },
|
|
91
|
+
)) as FireworksAccountsPayload;
|
|
92
|
+
await guard();
|
|
93
|
+
for (const accountId of normalizeFireworksAccountsPayload(payload)) {
|
|
94
|
+
if (accounts.includes(accountId)) {
|
|
95
|
+
throw new Error(`Fireworks accounts listing repeated ${accountId}.`);
|
|
96
|
+
}
|
|
97
|
+
accounts.push(accountId);
|
|
98
|
+
}
|
|
99
|
+
pageToken = fireworksNextPageToken(payload.nextPageToken);
|
|
100
|
+
if (!pageToken) break;
|
|
101
|
+
}
|
|
102
|
+
if (pageToken) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`Fireworks account listing exceeded ${FIREWORKS_MAX_ACCOUNT_PAGES} pages.`,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
return accounts.map((id) => ({ id, label: id }));
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
async query(auth, signal, timeoutMs, guard, targetId) {
|
|
111
|
+
if (!guard) throw new Error("Fireworks API spend requires request-boundary revalidation.");
|
|
112
|
+
if (!isFireworksAccountId(targetId)) {
|
|
113
|
+
throw new Error("Fireworks billing requires a safe selected account slug.");
|
|
114
|
+
}
|
|
115
|
+
const startedAt = Date.now();
|
|
116
|
+
await guard();
|
|
117
|
+
const billingWindowAt = Date.now();
|
|
118
|
+
const payload = (await fetchProviderJson(
|
|
119
|
+
fireworksBillingSummaryUrl(targetId, billingWindowAt),
|
|
120
|
+
auth,
|
|
121
|
+
signal,
|
|
122
|
+
remainingTimeout(timeoutMs, startedAt, "fetching Fireworks rated spend"),
|
|
123
|
+
"Fireworks billing summary endpoint",
|
|
124
|
+
{ redirect: "error" },
|
|
125
|
+
)) as FireworksBillingSummaryPayload;
|
|
126
|
+
await guard();
|
|
127
|
+
return normalizeFireworksBillingSummaryPayload(payload, targetId, Date.now());
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
56
132
|
export function normalizeFireworksBillingSummaryPayload(
|
|
57
133
|
payload: FireworksBillingSummaryPayload,
|
|
58
134
|
accountId: string,
|
|
@@ -192,6 +268,42 @@ function formatMoneyAmount(amount: bigint): string {
|
|
|
192
268
|
return `${negative ? "-" : ""}${units.toString()}${nanos ? `.${nanos}` : ""}`;
|
|
193
269
|
}
|
|
194
270
|
|
|
271
|
+
function fireworksAccountsUrl(pageToken: string | undefined): string {
|
|
272
|
+
const url = new URL("/v1/accounts", FIREWORKS_BILLING_SUMMARY_ORIGIN);
|
|
273
|
+
url.searchParams.set("pageSize", "200");
|
|
274
|
+
if (pageToken !== undefined) url.searchParams.set("pageToken", pageToken);
|
|
275
|
+
return url.toString();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function fireworksNextPageToken(value: unknown): string | undefined {
|
|
279
|
+
if (value === undefined || value === null) return undefined;
|
|
280
|
+
if (typeof value !== "string" || !value || value.length > 512) {
|
|
281
|
+
throw new Error("Fireworks accounts listing returned an invalid page token.");
|
|
282
|
+
}
|
|
283
|
+
return value;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function fireworksBillingSummaryUrl(accountId: string, startedAt: number): string {
|
|
287
|
+
const dayMs = 24 * 60 * 60 * 1000;
|
|
288
|
+
const dayFloor = (time: number) => `${new Date(time).toISOString().slice(0, 10)}T00:00:00Z`;
|
|
289
|
+
const url = new URL(
|
|
290
|
+
`/v1/accounts/${accountId}/billing/summary`,
|
|
291
|
+
FIREWORKS_BILLING_SUMMARY_ORIGIN,
|
|
292
|
+
);
|
|
293
|
+
url.searchParams.set(
|
|
294
|
+
"startTime",
|
|
295
|
+
dayFloor(startedAt - (FIREWORKS_SPEND_WINDOW_DAYS - 1) * dayMs),
|
|
296
|
+
);
|
|
297
|
+
url.searchParams.set("endTime", dayFloor(startedAt + dayMs));
|
|
298
|
+
return url.toString();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function remainingTimeout(timeoutMs: number, startedAt: number, description: string): number {
|
|
302
|
+
const remaining = timeoutMs - (Date.now() - startedAt);
|
|
303
|
+
if (remaining <= 0) throw new Error(`Timed out while ${description}.`);
|
|
304
|
+
return remaining;
|
|
305
|
+
}
|
|
306
|
+
|
|
195
307
|
function asObject(value: unknown): Record<string, unknown> | undefined {
|
|
196
308
|
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
197
309
|
return value as Record<string, unknown>;
|
package/src/providers/zai.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { sanitizeDisplayText } from "../core.js";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
UsageBucket,
|
|
4
|
+
UsageMetric,
|
|
5
|
+
UsageReport,
|
|
6
|
+
ZaiPlanInfo,
|
|
7
|
+
ZaiQuotaPayload,
|
|
8
|
+
ZaiSubscriptionPayload,
|
|
9
|
+
} from "../types.js";
|
|
3
10
|
|
|
4
11
|
const FIVE_HOUR_WINDOW_MINUTES = 300;
|
|
5
12
|
const WEEKLY_WINDOW_MINUTES = 10_080;
|
|
@@ -9,6 +16,7 @@ export function normalizeZaiQuotaPayload(
|
|
|
9
16
|
providerName: string,
|
|
10
17
|
payload: ZaiQuotaPayload,
|
|
11
18
|
capturedAt: number,
|
|
19
|
+
plan?: ZaiPlanInfo,
|
|
12
20
|
): UsageReport {
|
|
13
21
|
const data = asObject(payload.data);
|
|
14
22
|
if (!data) throw new Error("Z.AI quota response data was not an object.");
|
|
@@ -26,14 +34,20 @@ export function normalizeZaiQuotaPayload(
|
|
|
26
34
|
addCountBucket(buckets, limit, "mcp-monthly", "MCP monthly allowance");
|
|
27
35
|
addUsageDetailMetrics(metrics, limit.usageDetails);
|
|
28
36
|
} else if (isPlanUsage && unit === 3) {
|
|
29
|
-
addPercentBucket(
|
|
37
|
+
addPercentBucket(
|
|
38
|
+
buckets,
|
|
39
|
+
limit,
|
|
40
|
+
"five-hour",
|
|
41
|
+
sessionWindowLabel(limit),
|
|
42
|
+
sessionWindowMinutes(limit),
|
|
43
|
+
);
|
|
30
44
|
} else if (isPlanUsage && unit === 6) {
|
|
31
45
|
const used = asNonnegativeNumber(limit.currentValue);
|
|
32
46
|
const quota = asNonnegativeNumber(limit.usage);
|
|
33
47
|
if (used !== undefined && quota !== undefined) {
|
|
34
|
-
addCountBucket(buckets, limit, "weekly", "Weekly window",
|
|
48
|
+
addCountBucket(buckets, limit, "weekly", "Weekly window", weeklyWindowMinutes(limit));
|
|
35
49
|
} else {
|
|
36
|
-
addPercentBucket(buckets, limit, "weekly", "Weekly window",
|
|
50
|
+
addPercentBucket(buckets, limit, "weekly", "Weekly window", weeklyWindowMinutes(limit));
|
|
37
51
|
}
|
|
38
52
|
}
|
|
39
53
|
}
|
|
@@ -43,7 +57,12 @@ export function normalizeZaiQuotaPayload(
|
|
|
43
57
|
|
|
44
58
|
const notes: string[] = [];
|
|
45
59
|
const level = asString(data.level);
|
|
46
|
-
|
|
60
|
+
const planLabel = plan?.name ?? level;
|
|
61
|
+
if (planLabel) {
|
|
62
|
+
notes.push(
|
|
63
|
+
plan?.renewsAt ? `Plan: ${planLabel} · renews ${plan.renewsAt}` : `Plan: ${planLabel}`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
47
66
|
|
|
48
67
|
return {
|
|
49
68
|
providerId,
|
|
@@ -57,6 +76,79 @@ export function normalizeZaiQuotaPayload(
|
|
|
57
76
|
};
|
|
58
77
|
}
|
|
59
78
|
|
|
79
|
+
// The undocumented subscription endpoint can include historical products. Prefer the current,
|
|
80
|
+
// valid product and fail closed to the quota plan level when only explicitly inactive products exist.
|
|
81
|
+
export function normalizeZaiSubscriptionPayload(
|
|
82
|
+
payload: ZaiSubscriptionPayload,
|
|
83
|
+
): ZaiPlanInfo | undefined {
|
|
84
|
+
if (payload.success === false) return undefined;
|
|
85
|
+
if (typeof payload.code === "number" && payload.code !== 0 && payload.code !== 200) {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
if (!Array.isArray(payload.data)) return undefined;
|
|
89
|
+
|
|
90
|
+
const candidates: Array<{
|
|
91
|
+
plan: ZaiPlanInfo;
|
|
92
|
+
status?: string;
|
|
93
|
+
inCurrentPeriod?: boolean;
|
|
94
|
+
}> = [];
|
|
95
|
+
for (const raw of payload.data) {
|
|
96
|
+
const entry = asObject(raw);
|
|
97
|
+
if (!entry) continue;
|
|
98
|
+
const name = asString(entry.productName);
|
|
99
|
+
if (!name) continue;
|
|
100
|
+
const renewsAt = planRenewalDate(entry.nextRenewTime);
|
|
101
|
+
const status = asString(entry.status)?.toUpperCase();
|
|
102
|
+
const inCurrentPeriod = asBoolean(entry.inCurrentPeriod);
|
|
103
|
+
candidates.push({
|
|
104
|
+
plan: { name, ...(renewsAt !== undefined ? { renewsAt } : {}) },
|
|
105
|
+
...(status !== undefined ? { status } : {}),
|
|
106
|
+
...(inCurrentPeriod !== undefined ? { inCurrentPeriod } : {}),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const hasStateMetadata = candidates.some(
|
|
111
|
+
(candidate) => candidate.status !== undefined || candidate.inCurrentPeriod !== undefined,
|
|
112
|
+
);
|
|
113
|
+
if (!hasStateMetadata) return candidates[0]?.plan;
|
|
114
|
+
return (
|
|
115
|
+
candidates.find(
|
|
116
|
+
(candidate) => candidate.inCurrentPeriod === true && candidate.status === "VALID",
|
|
117
|
+
)?.plan ??
|
|
118
|
+
candidates.find(
|
|
119
|
+
(candidate) => candidate.inCurrentPeriod === true && candidate.status === undefined,
|
|
120
|
+
)?.plan ??
|
|
121
|
+
candidates.find(
|
|
122
|
+
(candidate) => candidate.status === "VALID" && candidate.inCurrentPeriod === undefined,
|
|
123
|
+
)?.plan
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function planRenewalDate(value: unknown): string | undefined {
|
|
128
|
+
if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}/u.test(value)) return value.slice(0, 10);
|
|
129
|
+
const millis = asNonnegativeNumber(value);
|
|
130
|
+
if (millis === undefined || millis === 0) return undefined;
|
|
131
|
+
return new Date(millis).toISOString().slice(0, 10);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Z.AI encodes each window as a (unit, number) pair — unit 3 counts hours and unit 6 counts
|
|
135
|
+
// weeks — so the payload's number drives the window length and session label; the established
|
|
136
|
+
// 5-hour and weekly constants remain the fallback when the payload omits it.
|
|
137
|
+
function sessionWindowMinutes(limit: Record<string, unknown>): number {
|
|
138
|
+
const hours = asPositiveNumber(limit.number);
|
|
139
|
+
return hours === undefined ? FIVE_HOUR_WINDOW_MINUTES : Math.round(hours * 60);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function sessionWindowLabel(limit: Record<string, unknown>): string {
|
|
143
|
+
const minutes = sessionWindowMinutes(limit);
|
|
144
|
+
return minutes === FIVE_HOUR_WINDOW_MINUTES ? "5h window" : `${Math.round(minutes / 60)}h window`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function weeklyWindowMinutes(limit: Record<string, unknown>): number {
|
|
148
|
+
const weeks = asPositiveNumber(limit.number);
|
|
149
|
+
return weeks === undefined ? WEEKLY_WINDOW_MINUTES : Math.round(weeks * WEEKLY_WINDOW_MINUTES);
|
|
150
|
+
}
|
|
151
|
+
|
|
60
152
|
function addPercentBucket(
|
|
61
153
|
buckets: UsageBucket[],
|
|
62
154
|
limit: Record<string, unknown>,
|
|
@@ -130,6 +222,18 @@ function asNonnegativeNumber(value: unknown): number | undefined {
|
|
|
130
222
|
return value;
|
|
131
223
|
}
|
|
132
224
|
|
|
225
|
+
function asPositiveNumber(value: unknown): number | undefined {
|
|
226
|
+
const number = asNonnegativeNumber(value);
|
|
227
|
+
return number !== undefined && number > 0 ? number : undefined;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function asBoolean(value: unknown): boolean | undefined {
|
|
231
|
+
if (typeof value === "boolean") return value;
|
|
232
|
+
if (value === 1) return true;
|
|
233
|
+
if (value === 0) return false;
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
|
|
133
237
|
function asEpochSeconds(value: unknown): number | undefined {
|
|
134
238
|
const millis = asNonnegativeNumber(value);
|
|
135
239
|
if (millis === undefined) return undefined;
|