@narumitw/pi-usage 0.58.0 → 0.60.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 +86 -7
- package/dist/index.ts +948 -235
- package/dist/index.ts.map +4 -4
- package/package.json +7 -1
- package/src/format.ts +176 -15
- package/src/index.ts +14 -1
- package/src/providers/baseten.ts +55 -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/providers/zai.ts +109 -5
- package/src/query.ts +276 -32
- package/src/types.ts +41 -0
- package/src/usage.ts +21 -13
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { MoonshotBalancePayload, UsageMetric, UsageReport } from "../types.js";
|
|
2
|
+
|
|
3
|
+
export type MoonshotProviderId = "moonshotai" | "moonshotai-cn";
|
|
4
|
+
|
|
5
|
+
const PROVIDERS = {
|
|
6
|
+
moonshotai: { name: "Moonshot AI", currency: "USD" },
|
|
7
|
+
"moonshotai-cn": { name: "Moonshot AI CN", currency: "CNY" },
|
|
8
|
+
} as const;
|
|
9
|
+
|
|
10
|
+
export function normalizeMoonshotBalancePayload(
|
|
11
|
+
providerId: MoonshotProviderId,
|
|
12
|
+
payload: MoonshotBalancePayload,
|
|
13
|
+
capturedAt: number,
|
|
14
|
+
): UsageReport {
|
|
15
|
+
if (payload.code !== 0 || payload.status !== true) {
|
|
16
|
+
throw new Error("Moonshot AI balance response did not report success.");
|
|
17
|
+
}
|
|
18
|
+
const data = asObject(payload.data);
|
|
19
|
+
if (!data) throw new Error("Moonshot AI balance response data was not an object.");
|
|
20
|
+
const provider = PROVIDERS[providerId];
|
|
21
|
+
const available = amount(data.available_balance, "available balance", false);
|
|
22
|
+
const voucher = amount(data.voucher_balance, "voucher balance", false);
|
|
23
|
+
const cash = amount(data.cash_balance, "cash balance", true);
|
|
24
|
+
const metrics: UsageMetric[] = [
|
|
25
|
+
currencyMetric("available-balance", "Available balance", available, provider.currency),
|
|
26
|
+
currencyMetric("voucher-balance", "Voucher balance", voucher, provider.currency),
|
|
27
|
+
currencyMetric("cash-balance", "Cash balance", cash, provider.currency),
|
|
28
|
+
];
|
|
29
|
+
return {
|
|
30
|
+
providerId,
|
|
31
|
+
providerName: provider.name,
|
|
32
|
+
capturedAt,
|
|
33
|
+
source: "moonshot-balance",
|
|
34
|
+
semantics: { kind: "api-key", label: "Moonshot API account balance" },
|
|
35
|
+
buckets: [],
|
|
36
|
+
metrics,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function currencyMetric(
|
|
41
|
+
id: string,
|
|
42
|
+
label: string,
|
|
43
|
+
value: string,
|
|
44
|
+
currency: "CNY" | "USD",
|
|
45
|
+
): UsageMetric {
|
|
46
|
+
return { id, label, value, unit: "currency", currency };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function asObject(value: unknown): Record<string, unknown> | undefined {
|
|
50
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
51
|
+
return value as Record<string, unknown>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function amount(value: unknown, label: string, allowNegative: boolean): string {
|
|
55
|
+
if (
|
|
56
|
+
typeof value !== "number" ||
|
|
57
|
+
!Number.isFinite(value) ||
|
|
58
|
+
(!allowNegative && value < 0) ||
|
|
59
|
+
Math.abs(value) > Number.MAX_SAFE_INTEGER
|
|
60
|
+
) {
|
|
61
|
+
throw new Error(`Moonshot AI ${label} was not a valid amount.`);
|
|
62
|
+
}
|
|
63
|
+
return String(value);
|
|
64
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { UsageMetric, UsageReport, VercelAIGatewayCreditsPayload } from "../types.js";
|
|
2
|
+
|
|
3
|
+
const DECIMAL_AMOUNT = /^(?:0|[1-9]\d*)(?:\.\d+)?$/u;
|
|
4
|
+
|
|
5
|
+
export function normalizeVercelAIGatewayCreditsPayload(
|
|
6
|
+
payload: VercelAIGatewayCreditsPayload,
|
|
7
|
+
capturedAt: number,
|
|
8
|
+
): UsageReport {
|
|
9
|
+
const balance = decimalAmount(payload.balance, "balance");
|
|
10
|
+
const totalUsed = decimalAmount(payload.total_used, "total used");
|
|
11
|
+
const metrics: UsageMetric[] = [
|
|
12
|
+
{
|
|
13
|
+
id: "credit-balance",
|
|
14
|
+
label: "Credit balance",
|
|
15
|
+
value: balance,
|
|
16
|
+
unit: "currency",
|
|
17
|
+
currency: "USD",
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: "lifetime-spend",
|
|
21
|
+
label: "Lifetime spend",
|
|
22
|
+
value: totalUsed,
|
|
23
|
+
unit: "currency",
|
|
24
|
+
currency: "USD",
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
return {
|
|
28
|
+
providerId: "vercel-ai-gateway",
|
|
29
|
+
providerName: "Vercel AI Gateway",
|
|
30
|
+
capturedAt,
|
|
31
|
+
source: "vercel-ai-gateway-credits",
|
|
32
|
+
semantics: { kind: "api-key", label: "AI Gateway credits and lifetime spend" },
|
|
33
|
+
buckets: [],
|
|
34
|
+
metrics,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function decimalAmount(value: unknown, label: string): string {
|
|
39
|
+
if (typeof value !== "string" || value.length > 64 || !DECIMAL_AMOUNT.test(value)) {
|
|
40
|
+
throw new Error(`Vercel AI Gateway ${label} was not a valid nonnegative amount.`);
|
|
41
|
+
}
|
|
42
|
+
return value;
|
|
43
|
+
}
|
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;
|