@diousk/pi-usage-fast 0.1.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.
@@ -0,0 +1,179 @@
1
+ import { sanitizeDisplayText } from "../core.js";
2
+ import type { UsageBucket, UsageMetric, UsageReport, XaiBillingPayload } from "../types.js";
3
+
4
+ const MAX_SAFE_CENTS = Number.MAX_SAFE_INTEGER;
5
+
6
+ export function normalizeXaiBillingPayload(
7
+ payload: XaiBillingPayload,
8
+ subscriptionTier: unknown,
9
+ capturedAt: number,
10
+ ): UsageReport {
11
+ const configValue = payload.config;
12
+ if (configValue !== null && configValue !== undefined && !isRecord(configValue)) {
13
+ throw new Error("xAI billing response config was not an object or null.");
14
+ }
15
+ const config = isRecord(configValue) ? configValue : undefined;
16
+ const buckets: UsageBucket[] = [];
17
+ const metrics: UsageMetric[] = [];
18
+ const notes: string[] = [];
19
+
20
+ if (config) {
21
+ const period = normalizePeriod(config.currentPeriod, config.billingPeriodStart, config.billingPeriodEnd);
22
+ const preferredPercent = optionalPercent(config.creditUsagePercent, "creditUsagePercent");
23
+ if (preferredPercent !== undefined) {
24
+ buckets.push({
25
+ id: "included-allowance",
26
+ label: "Included allowance",
27
+ used: preferredPercent,
28
+ remaining: 100 - preferredPercent,
29
+ unit: "percent",
30
+ ...period,
31
+ });
32
+ } else {
33
+ const limit = optionalUsd(config.monthlyLimit, "monthlyLimit");
34
+ const used = optionalUsd(config.used, "used");
35
+ if (limit !== undefined || used !== undefined) {
36
+ buckets.push({
37
+ id: "included-allowance",
38
+ label: "Included allowance",
39
+ ...(limit !== undefined ? { limit } : {}),
40
+ ...(used !== undefined ? { used } : {}),
41
+ ...(limit !== undefined && used !== undefined ? { remaining: limit - used } : {}),
42
+ unit: "usd",
43
+ ...period,
44
+ });
45
+ } else if (period.period || period.resetsAt !== undefined) {
46
+ buckets.push({
47
+ id: "included-allowance",
48
+ label: "Included allowance",
49
+ unit: "percent",
50
+ ...period,
51
+ });
52
+ }
53
+ }
54
+
55
+ const onDemandCap = optionalUsd(config.onDemandCap, "onDemandCap");
56
+ const onDemandUsed = optionalUsd(config.onDemandUsed, "onDemandUsed");
57
+ if (onDemandCap !== undefined || onDemandUsed !== undefined) {
58
+ buckets.push({
59
+ id: "on-demand",
60
+ label: "On-demand usage",
61
+ ...(onDemandCap !== undefined ? { limit: onDemandCap } : {}),
62
+ ...(onDemandUsed !== undefined ? { used: onDemandUsed } : {}),
63
+ ...(onDemandCap !== undefined && onDemandUsed !== undefined ? { remaining: onDemandCap - onDemandUsed } : {}),
64
+ unit: "usd",
65
+ });
66
+ }
67
+
68
+ const prepaidBalance = optionalUsd(config.prepaidBalance, "prepaidBalance");
69
+ if (prepaidBalance !== undefined) {
70
+ metrics.push({
71
+ id: "prepaid-balance",
72
+ label: "Prepaid balance",
73
+ value: prepaidBalance,
74
+ unit: "usd",
75
+ });
76
+ }
77
+ }
78
+
79
+ const tier = optionalTier(subscriptionTier);
80
+ if (tier) metrics.push({ id: "subscription-tier", label: "Plan tier", value: tier });
81
+ if (!config) notes.push("No xAI consumer billing configuration is available for this account.");
82
+ else if (buckets.length === 0 && metrics.length === 0) {
83
+ notes.push("The xAI consumer billing response contained no displayable usage fields.");
84
+ }
85
+
86
+ return {
87
+ providerId: "xai",
88
+ providerName: "xAI",
89
+ capturedAt,
90
+ source: "cli-chat-proxy.grok.com consumer billing",
91
+ semantics: {
92
+ kind: "consumer-subscription",
93
+ label: "xAI consumer subscription usage",
94
+ },
95
+ buckets,
96
+ metrics,
97
+ ...(notes.length > 0 ? { notes } : {}),
98
+ };
99
+ }
100
+
101
+ function normalizePeriod(
102
+ currentPeriod: unknown,
103
+ legacyStart: unknown,
104
+ legacyEnd: unknown,
105
+ ): Pick<UsageBucket, "period" | "resetsAt"> {
106
+ if (currentPeriod !== undefined && currentPeriod !== null && !isRecord(currentPeriod)) {
107
+ throw new Error("xAI billing currentPeriod was not an object or null.");
108
+ }
109
+ if (isRecord(currentPeriod)) {
110
+ const type = optionalString(currentPeriod.type, "currentPeriod.type");
111
+ const start = optionalTimestamp(currentPeriod.start, "currentPeriod.start");
112
+ const end = optionalTimestamp(currentPeriod.end, "currentPeriod.end");
113
+ return {
114
+ ...(periodLabel(type, start) ? { period: periodLabel(type, start) } : {}),
115
+ ...(end !== undefined ? { resetsAt: end } : {}),
116
+ };
117
+ }
118
+ const start = optionalTimestamp(legacyStart, "billingPeriodStart");
119
+ const end = optionalTimestamp(legacyEnd, "billingPeriodEnd");
120
+ return {
121
+ ...(start !== undefined ? { period: "Monthly" } : {}),
122
+ ...(end !== undefined ? { resetsAt: end } : {}),
123
+ };
124
+ }
125
+
126
+ function periodLabel(type: string | undefined, start: number | undefined): string | undefined {
127
+ if (type === "USAGE_PERIOD_TYPE_WEEKLY") return "Weekly";
128
+ if (type === "USAGE_PERIOD_TYPE_MONTHLY") return "Monthly";
129
+ if (type) return sanitizeDisplayText(type.replace(/^USAGE_PERIOD_TYPE_/u, "").replaceAll("_", " "), 40);
130
+ return start === undefined ? undefined : "Current period";
131
+ }
132
+
133
+ function optionalUsd(value: unknown, field: string): number | undefined {
134
+ if (value === undefined || value === null) return undefined;
135
+ if (!isRecord(value)) throw new Error(`xAI billing ${field} was not a cent wrapper.`);
136
+ const cents = value.val === undefined ? 0 : value.val;
137
+ if (!Number.isSafeInteger(cents) || Math.abs(cents as number) > MAX_SAFE_CENTS) {
138
+ throw new Error(`xAI billing ${field}.val was not a safe signed integer.`);
139
+ }
140
+ return (cents as number) / 100;
141
+ }
142
+
143
+ function optionalPercent(value: unknown, field: string): number | undefined {
144
+ if (value === undefined || value === null) return undefined;
145
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 100) {
146
+ throw new Error(`xAI billing ${field} was outside 0–100.`);
147
+ }
148
+ return value;
149
+ }
150
+
151
+ function optionalTimestamp(value: unknown, field: string): number | undefined {
152
+ if (value === undefined || value === null) return undefined;
153
+ if (typeof value !== "string" || value.length > 80) {
154
+ throw new Error(`xAI billing ${field} was not a bounded timestamp.`);
155
+ }
156
+ const milliseconds = Date.parse(value);
157
+ if (!Number.isFinite(milliseconds)) throw new Error(`xAI billing ${field} was invalid.`);
158
+ return Math.floor(milliseconds / 1000);
159
+ }
160
+
161
+ function optionalString(value: unknown, field: string): string | undefined {
162
+ if (value === undefined || value === null) return undefined;
163
+ if (typeof value !== "string" || value.length > 80) {
164
+ throw new Error(`xAI billing ${field} was not a bounded string.`);
165
+ }
166
+ return value;
167
+ }
168
+
169
+ function optionalTier(value: unknown): string | undefined {
170
+ if (value === undefined || value === null) return undefined;
171
+ if (typeof value !== "string" || value.length > 160) {
172
+ throw new Error("xAI subscription tier was not a bounded string or null.");
173
+ }
174
+ return sanitizeDisplayText(value, 80) || undefined;
175
+ }
176
+
177
+ function isRecord(value: unknown): value is Record<string, unknown> {
178
+ return typeof value === "object" && value !== null && !Array.isArray(value);
179
+ }
@@ -0,0 +1,94 @@
1
+ // Fixed messages from https://docs.z.ai/api-reference/api-code. Never interpolate provider text:
2
+ // error messages and their template parameters can contain credentials or terminal controls.
3
+ const ERROR_MESSAGES: Readonly<Record<string, string>> = {
4
+ "1000": "Authentication failed. Check your API key.",
5
+ "1001": "Authentication header missing. Check your API key.",
6
+ "1003": "Authentication token expired. Obtain a new token.",
7
+ "1005": "Two-factor authentication required.",
8
+ "1113": "Insufficient balance or no resource package. Recharge your account.",
9
+ "1200": "API call error. Try again later.",
10
+ "1210": "Invalid API parameter. Check the API documentation.",
11
+ "1211": "Unknown model. Check the model ID.",
12
+ "1212": "This model does not support the requested method.",
13
+ "1213": "A required parameter is missing.",
14
+ "1214": "Invalid parameter. Check the API documentation.",
15
+ "1215": "Conflicting parameters. Check the API documentation.",
16
+ "1220": "Access denied. Check your permissions.",
17
+ "1221": "This API has been taken offline.",
18
+ "1222": "This API does not exist.",
19
+ "1230": "API processing error. Try again later.",
20
+ "1234": "Network error. Try again later.",
21
+ "1261": "Prompt too long. Reduce the input length.",
22
+ "1301": "Content rejected by the safety policy.",
23
+ "1302": "Request rate limit reached. Try again later.",
24
+ "1305": "Service overloaded. Try again later.",
25
+ "1308": "Usage limit reached. Wait for the quota reset.",
26
+ "1309": "GLM Coding Plan expired. Renew your subscription.",
27
+ "1310": "Weekly or monthly limit exhausted. Wait for the quota reset.",
28
+ "1311": "Your subscription does not include this model.",
29
+ "1313": "Request frequency restricted by the Fair Usage Policy. Contact support.",
30
+ "1314": "Enterprise package expired. Contact your administrator.",
31
+ "1315": "This API key requires an enterprise coding package scenario. Replace the key.",
32
+ "1316": "5-hour limit reached; insufficient balance for extra usage. Wait for reset.",
33
+ "1317": "7-day limit reached; insufficient balance for extra usage. Wait for reset.",
34
+ "1318": "5-hour limit reached; extra usage blocked by monthly spend limit.",
35
+ "1319": "7-day limit reached; extra usage blocked by monthly spend limit.",
36
+ "1320": "5-hour limit reached; extra usage blocked by monthly spend limit.",
37
+ "1321": "7-day limit reached; extra usage blocked by monthly spend limit.",
38
+ };
39
+
40
+ const HTTP_MESSAGES: Readonly<Record<number, string>> = {
41
+ 400: "Invalid request. Check the API documentation.",
42
+ 401: "Authentication failed. Check your API key.",
43
+ 403: "Access denied. Check your permissions.",
44
+ 429: "Request or usage limit reached. Try again later.",
45
+ 500: "Internal error. Try again later.",
46
+ };
47
+
48
+ export function zaiPayloadError(payload: unknown): string | undefined {
49
+ const object = asObject(payload);
50
+ if (!object) return undefined;
51
+ const nested = asObject(object.error);
52
+ // Missing nested metadata must not hide a top-level code; explicit nested values retain priority.
53
+ const rawCode = nested?.code === undefined ? object.code : nested.code;
54
+ const code = errorCode(rawCode);
55
+ if (
56
+ object.error === undefined &&
57
+ object.success !== false &&
58
+ (rawCode === undefined || code === "0" || code === "200")
59
+ ) {
60
+ return undefined;
61
+ }
62
+ return code && code !== "0" && code !== "200"
63
+ ? `Z.AI ${code}: ${ERROR_MESSAGES[code] ?? "API request failed."}`
64
+ : "Z.AI: API request failed.";
65
+ }
66
+
67
+ export function zaiResponseError(status: number, text: string): string | undefined {
68
+ let payload: unknown;
69
+ try {
70
+ payload = JSON.parse(text) as unknown;
71
+ } catch {
72
+ // Do not expose JSON parser excerpts, response bodies, or statusText.
73
+ if (status >= 200 && status < 300) return "Z.AI: Invalid JSON response.";
74
+ }
75
+ const error = zaiPayloadError(payload);
76
+ if (error) return error;
77
+ if (status < 200 || status >= 300) {
78
+ return `Z.AI HTTP ${status}: ${HTTP_MESSAGES[status] ?? "API request failed."}`;
79
+ }
80
+ return undefined;
81
+ }
82
+
83
+ function errorCode(value: unknown): string | undefined {
84
+ if (typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 9999) {
85
+ return String(value);
86
+ }
87
+ return typeof value === "string" && /^(?:0|[1-9]\d{0,3})$/u.test(value) ? value : undefined;
88
+ }
89
+
90
+ function asObject(value: unknown): Record<string, unknown> | undefined {
91
+ return value !== null && typeof value === "object" && !Array.isArray(value)
92
+ ? (value as Record<string, unknown>)
93
+ : undefined;
94
+ }
@@ -0,0 +1,239 @@
1
+ import { sanitizeDisplayText } from "../core.js";
2
+ import type {
3
+ UsageBucket,
4
+ UsageMetric,
5
+ UsageReport,
6
+ ZaiPlanInfo,
7
+ ZaiQuotaPayload,
8
+ ZaiSubscriptionPayload,
9
+ } from "../types.js";
10
+
11
+ import { zaiPayloadError } from "./zai-errors.js";
12
+
13
+ const FIVE_HOUR_WINDOW_MINUTES = 300;
14
+ const WEEKLY_WINDOW_MINUTES = 10_080;
15
+
16
+ export function normalizeZaiQuotaPayload(
17
+ providerId: string,
18
+ providerName: string,
19
+ payload: ZaiQuotaPayload,
20
+ capturedAt: number,
21
+ plan?: ZaiPlanInfo,
22
+ ): UsageReport {
23
+ const error = zaiPayloadError(payload);
24
+ if (error) throw new Error(error);
25
+ const data = asObject(payload.data);
26
+ if (!data) throw new Error("Z.AI quota response data was not an object.");
27
+ const limits = Array.isArray(data.limits) ? (data.limits as unknown[]) : [];
28
+
29
+ const buckets: UsageBucket[] = [];
30
+ const metrics: UsageMetric[] = [];
31
+ for (const raw of limits) {
32
+ const limit = asObject(raw);
33
+ if (!limit) continue;
34
+ const type = asString(limit.type);
35
+ const unit = asNonnegativeNumber(limit.unit);
36
+ const isPlanUsage = type === "TOKENS_LIMIT" || type === "CREDIT_LIMIT";
37
+ if (type === "TIME_LIMIT") {
38
+ addCountBucket(buckets, limit, "mcp-monthly", "MCP monthly allowance");
39
+ addUsageDetailMetrics(metrics, limit.usageDetails);
40
+ } else if (isPlanUsage && unit === 3) {
41
+ addPercentBucket(buckets, limit, "five-hour", sessionWindowLabel(limit), sessionWindowMinutes(limit));
42
+ } else if (isPlanUsage && unit === 6) {
43
+ const used = asNonnegativeNumber(limit.currentValue);
44
+ const quota = asNonnegativeNumber(limit.usage);
45
+ if (used !== undefined && quota !== undefined) {
46
+ addCountBucket(buckets, limit, "weekly", "Weekly window", weeklyWindowMinutes(limit));
47
+ } else {
48
+ addPercentBucket(buckets, limit, "weekly", "Weekly window", weeklyWindowMinutes(limit));
49
+ }
50
+ }
51
+ }
52
+ if (buckets.length === 0) {
53
+ throw new Error("Z.AI quota endpoint returned no displayable usage data.");
54
+ }
55
+
56
+ const notes: string[] = [];
57
+ const level = asString(data.level);
58
+ const planLabel = plan?.name ?? level;
59
+ if (planLabel) {
60
+ notes.push(plan?.renewsAt ? `Plan: ${planLabel} · renews ${plan.renewsAt}` : `Plan: ${planLabel}`);
61
+ }
62
+
63
+ return {
64
+ providerId,
65
+ providerName,
66
+ capturedAt,
67
+ source: "zai-quota",
68
+ semantics: { kind: "consumer-subscription", label: "GLM Coding Plan usage" },
69
+ buckets,
70
+ metrics,
71
+ ...(notes.length > 0 ? { notes } : {}),
72
+ };
73
+ }
74
+
75
+ // The undocumented subscription endpoint can include historical products. Prefer the current,
76
+ // valid product and fail closed to the quota plan level when only explicitly inactive products exist.
77
+ export function normalizeZaiSubscriptionPayload(payload: ZaiSubscriptionPayload): ZaiPlanInfo | undefined {
78
+ if (zaiPayloadError(payload)) return undefined;
79
+ if (!Array.isArray(payload.data)) return undefined;
80
+
81
+ const candidates: Array<{
82
+ plan: ZaiPlanInfo;
83
+ status?: string;
84
+ inCurrentPeriod?: boolean;
85
+ }> = [];
86
+ for (const raw of payload.data) {
87
+ const entry = asObject(raw);
88
+ if (!entry) continue;
89
+ const name = asString(entry.productName);
90
+ if (!name) continue;
91
+ const renewsAt = planRenewalDate(entry.nextRenewTime);
92
+ const status = asString(entry.status)?.toUpperCase();
93
+ const inCurrentPeriod = asBoolean(entry.inCurrentPeriod);
94
+ candidates.push({
95
+ plan: { name, ...(renewsAt !== undefined ? { renewsAt } : {}) },
96
+ ...(status !== undefined ? { status } : {}),
97
+ ...(inCurrentPeriod !== undefined ? { inCurrentPeriod } : {}),
98
+ });
99
+ }
100
+
101
+ const hasStateMetadata = candidates.some(
102
+ (candidate) => candidate.status !== undefined || candidate.inCurrentPeriod !== undefined,
103
+ );
104
+ if (!hasStateMetadata) return candidates[0]?.plan;
105
+ return (
106
+ candidates.find((candidate) => candidate.inCurrentPeriod === true && candidate.status === "VALID")?.plan ??
107
+ candidates.find((candidate) => candidate.inCurrentPeriod === true && candidate.status === undefined)?.plan ??
108
+ candidates.find((candidate) => candidate.status === "VALID" && candidate.inCurrentPeriod === undefined)?.plan
109
+ );
110
+ }
111
+
112
+ function planRenewalDate(value: unknown): string | undefined {
113
+ if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}/u.test(value)) return value.slice(0, 10);
114
+ const millis = asNonnegativeNumber(value);
115
+ if (millis === undefined || millis === 0) return undefined;
116
+ return new Date(millis).toISOString().slice(0, 10);
117
+ }
118
+
119
+ // Z.AI encodes each window as a (unit, number) pair — unit 3 counts hours and unit 6 counts
120
+ // weeks — so the payload's number drives the window length and session label; the established
121
+ // 5-hour and weekly constants remain the fallback when the payload omits it.
122
+ function sessionWindowMinutes(limit: Record<string, unknown>): number {
123
+ const hours = asPositiveNumber(limit.number);
124
+ return hours === undefined ? FIVE_HOUR_WINDOW_MINUTES : Math.round(hours * 60);
125
+ }
126
+
127
+ function sessionWindowLabel(limit: Record<string, unknown>): string {
128
+ const minutes = sessionWindowMinutes(limit);
129
+ return minutes === FIVE_HOUR_WINDOW_MINUTES ? "5h window" : `${Math.round(minutes / 60)}h window`;
130
+ }
131
+
132
+ function weeklyWindowMinutes(limit: Record<string, unknown>): number {
133
+ const weeks = asPositiveNumber(limit.number);
134
+ return weeks === undefined ? WEEKLY_WINDOW_MINUTES : Math.round(weeks * WEEKLY_WINDOW_MINUTES);
135
+ }
136
+
137
+ function addPercentBucket(
138
+ buckets: UsageBucket[],
139
+ limit: Record<string, unknown>,
140
+ id: string,
141
+ label: string,
142
+ windowMinutes: number,
143
+ ): void {
144
+ const used = asNonnegativeNumber(limit.percentage);
145
+ if (used === undefined) return;
146
+ const percent = clampPercent(used);
147
+ const resetsAt = asEpochSeconds(limit.nextResetTime);
148
+ buckets.push({
149
+ id,
150
+ label,
151
+ used: percent,
152
+ remaining: 100 - percent,
153
+ limit: 100,
154
+ unit: "percent",
155
+ windowMinutes,
156
+ ...(resetsAt !== undefined ? { resetsAt } : {}),
157
+ });
158
+ }
159
+
160
+ function addCountBucket(
161
+ buckets: UsageBucket[],
162
+ limit: Record<string, unknown>,
163
+ id: string,
164
+ label: string,
165
+ windowMinutes?: number,
166
+ ): void {
167
+ const used = asNonnegativeNumber(limit.currentValue);
168
+ const quota = asNonnegativeNumber(limit.usage);
169
+ if (used === undefined || quota === undefined) return;
170
+ const resetsAt = asEpochSeconds(limit.nextResetTime);
171
+ buckets.push({
172
+ id,
173
+ label,
174
+ used,
175
+ remaining: Math.max(0, quota - used),
176
+ limit: quota,
177
+ unit: "count",
178
+ ...(windowMinutes !== undefined ? { windowMinutes } : {}),
179
+ ...(resetsAt !== undefined ? { resetsAt } : {}),
180
+ });
181
+ }
182
+
183
+ function addUsageDetailMetrics(metrics: UsageMetric[], value: unknown): void {
184
+ if (!Array.isArray(value)) return;
185
+ for (const raw of value) {
186
+ const detail = asObject(raw);
187
+ if (!detail) continue;
188
+ const label = asString(detail.modelCode);
189
+ const usage = asNonnegativeNumber(detail.usage);
190
+ if (!label || usage === undefined) continue;
191
+ metrics.push({ id: `mcp-${kebabCase(label)}`, label, value: usage, unit: "count" });
192
+ }
193
+ }
194
+
195
+ function asObject(value: unknown): Record<string, unknown> | undefined {
196
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
197
+ return value as Record<string, unknown>;
198
+ }
199
+
200
+ function asString(value: unknown): string | undefined {
201
+ if (typeof value !== "string") return undefined;
202
+ return sanitizeDisplayText(value, 80) || undefined;
203
+ }
204
+
205
+ function asNonnegativeNumber(value: unknown): number | undefined {
206
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return undefined;
207
+ return value;
208
+ }
209
+
210
+ function asPositiveNumber(value: unknown): number | undefined {
211
+ const number = asNonnegativeNumber(value);
212
+ return number !== undefined && number > 0 ? number : undefined;
213
+ }
214
+
215
+ function asBoolean(value: unknown): boolean | undefined {
216
+ if (typeof value === "boolean") return value;
217
+ if (value === 1) return true;
218
+ if (value === 0) return false;
219
+ return undefined;
220
+ }
221
+
222
+ function asEpochSeconds(value: unknown): number | undefined {
223
+ const millis = asNonnegativeNumber(value);
224
+ if (millis === undefined) return undefined;
225
+ return Math.floor(millis / 1000);
226
+ }
227
+
228
+ function kebabCase(label: string): string {
229
+ return (
230
+ label
231
+ .toLowerCase()
232
+ .replace(/[^a-z0-9]+/gu, "-")
233
+ .replace(/^-+|-+$/gu, "") || "tool"
234
+ );
235
+ }
236
+
237
+ function clampPercent(value: number): number {
238
+ return Math.min(100, Math.max(0, value));
239
+ }