@narumitw/pi-usage 0.60.7 → 0.60.8

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.
@@ -1,11 +1,11 @@
1
1
  import { sanitizeDisplayText } from "../core.js";
2
2
  import type {
3
- UsageBucket,
4
- UsageMetric,
5
- UsageReport,
6
- ZaiPlanInfo,
7
- ZaiQuotaPayload,
8
- ZaiSubscriptionPayload,
3
+ UsageBucket,
4
+ UsageMetric,
5
+ UsageReport,
6
+ ZaiPlanInfo,
7
+ ZaiQuotaPayload,
8
+ ZaiSubscriptionPayload,
9
9
  } from "../types.js";
10
10
 
11
11
  import { zaiPayloadError } from "./zai-errors.js";
@@ -14,242 +14,226 @@ const FIVE_HOUR_WINDOW_MINUTES = 300;
14
14
  const WEEKLY_WINDOW_MINUTES = 10_080;
15
15
 
16
16
  export function normalizeZaiQuotaPayload(
17
- providerId: string,
18
- providerName: string,
19
- payload: ZaiQuotaPayload,
20
- capturedAt: number,
21
- plan?: ZaiPlanInfo,
17
+ providerId: string,
18
+ providerName: string,
19
+ payload: ZaiQuotaPayload,
20
+ capturedAt: number,
21
+ plan?: ZaiPlanInfo,
22
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(
42
- buckets,
43
- limit,
44
- "five-hour",
45
- sessionWindowLabel(limit),
46
- sessionWindowMinutes(limit),
47
- );
48
- } else if (isPlanUsage && unit === 6) {
49
- const used = asNonnegativeNumber(limit.currentValue);
50
- const quota = asNonnegativeNumber(limit.usage);
51
- if (used !== undefined && quota !== undefined) {
52
- addCountBucket(buckets, limit, "weekly", "Weekly window", weeklyWindowMinutes(limit));
53
- } else {
54
- addPercentBucket(buckets, limit, "weekly", "Weekly window", weeklyWindowMinutes(limit));
55
- }
56
- }
57
- }
58
- if (buckets.length === 0) {
59
- throw new Error("Z.AI quota endpoint returned no displayable usage data.");
60
- }
61
-
62
- const notes: string[] = [];
63
- const level = asString(data.level);
64
- const planLabel = plan?.name ?? level;
65
- if (planLabel) {
66
- notes.push(
67
- plan?.renewsAt ? `Plan: ${planLabel} · renews ${plan.renewsAt}` : `Plan: ${planLabel}`,
68
- );
69
- }
70
-
71
- return {
72
- providerId,
73
- providerName,
74
- capturedAt,
75
- source: "zai-quota",
76
- semantics: { kind: "consumer-subscription", label: "GLM Coding Plan usage" },
77
- buckets,
78
- metrics,
79
- ...(notes.length > 0 ? { notes } : {}),
80
- };
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
+ };
81
73
  }
82
74
 
83
75
  // The undocumented subscription endpoint can include historical products. Prefer the current,
84
76
  // valid product and fail closed to the quota plan level when only explicitly inactive products exist.
85
- export function normalizeZaiSubscriptionPayload(
86
- payload: ZaiSubscriptionPayload,
87
- ): ZaiPlanInfo | undefined {
88
- if (zaiPayloadError(payload)) return undefined;
89
- if (!Array.isArray(payload.data)) return undefined;
90
-
91
- const candidates: Array<{
92
- plan: ZaiPlanInfo;
93
- status?: string;
94
- inCurrentPeriod?: boolean;
95
- }> = [];
96
- for (const raw of payload.data) {
97
- const entry = asObject(raw);
98
- if (!entry) continue;
99
- const name = asString(entry.productName);
100
- if (!name) continue;
101
- const renewsAt = planRenewalDate(entry.nextRenewTime);
102
- const status = asString(entry.status)?.toUpperCase();
103
- const inCurrentPeriod = asBoolean(entry.inCurrentPeriod);
104
- candidates.push({
105
- plan: { name, ...(renewsAt !== undefined ? { renewsAt } : {}) },
106
- ...(status !== undefined ? { status } : {}),
107
- ...(inCurrentPeriod !== undefined ? { inCurrentPeriod } : {}),
108
- });
109
- }
110
-
111
- const hasStateMetadata = candidates.some(
112
- (candidate) => candidate.status !== undefined || candidate.inCurrentPeriod !== undefined,
113
- );
114
- if (!hasStateMetadata) return candidates[0]?.plan;
115
- return (
116
- candidates.find(
117
- (candidate) => candidate.inCurrentPeriod === true && candidate.status === "VALID",
118
- )?.plan ??
119
- candidates.find(
120
- (candidate) => candidate.inCurrentPeriod === true && candidate.status === undefined,
121
- )?.plan ??
122
- candidates.find(
123
- (candidate) => candidate.status === "VALID" && candidate.inCurrentPeriod === undefined,
124
- )?.plan
125
- );
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
+ );
126
110
  }
127
111
 
128
112
  function planRenewalDate(value: unknown): string | undefined {
129
- if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}/u.test(value)) return value.slice(0, 10);
130
- const millis = asNonnegativeNumber(value);
131
- if (millis === undefined || millis === 0) return undefined;
132
- return new Date(millis).toISOString().slice(0, 10);
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);
133
117
  }
134
118
 
135
119
  // Z.AI encodes each window as a (unit, number) pair — unit 3 counts hours and unit 6 counts
136
120
  // weeks — so the payload's number drives the window length and session label; the established
137
121
  // 5-hour and weekly constants remain the fallback when the payload omits it.
138
122
  function sessionWindowMinutes(limit: Record<string, unknown>): number {
139
- const hours = asPositiveNumber(limit.number);
140
- return hours === undefined ? FIVE_HOUR_WINDOW_MINUTES : Math.round(hours * 60);
123
+ const hours = asPositiveNumber(limit.number);
124
+ return hours === undefined ? FIVE_HOUR_WINDOW_MINUTES : Math.round(hours * 60);
141
125
  }
142
126
 
143
127
  function sessionWindowLabel(limit: Record<string, unknown>): string {
144
- const minutes = sessionWindowMinutes(limit);
145
- return minutes === FIVE_HOUR_WINDOW_MINUTES ? "5h window" : `${Math.round(minutes / 60)}h window`;
128
+ const minutes = sessionWindowMinutes(limit);
129
+ return minutes === FIVE_HOUR_WINDOW_MINUTES ? "5h window" : `${Math.round(minutes / 60)}h window`;
146
130
  }
147
131
 
148
132
  function weeklyWindowMinutes(limit: Record<string, unknown>): number {
149
- const weeks = asPositiveNumber(limit.number);
150
- return weeks === undefined ? WEEKLY_WINDOW_MINUTES : Math.round(weeks * WEEKLY_WINDOW_MINUTES);
133
+ const weeks = asPositiveNumber(limit.number);
134
+ return weeks === undefined ? WEEKLY_WINDOW_MINUTES : Math.round(weeks * WEEKLY_WINDOW_MINUTES);
151
135
  }
152
136
 
153
137
  function addPercentBucket(
154
- buckets: UsageBucket[],
155
- limit: Record<string, unknown>,
156
- id: string,
157
- label: string,
158
- windowMinutes: number,
138
+ buckets: UsageBucket[],
139
+ limit: Record<string, unknown>,
140
+ id: string,
141
+ label: string,
142
+ windowMinutes: number,
159
143
  ): void {
160
- const used = asNonnegativeNumber(limit.percentage);
161
- if (used === undefined) return;
162
- const percent = clampPercent(used);
163
- const resetsAt = asEpochSeconds(limit.nextResetTime);
164
- buckets.push({
165
- id,
166
- label,
167
- used: percent,
168
- remaining: 100 - percent,
169
- limit: 100,
170
- unit: "percent",
171
- windowMinutes,
172
- ...(resetsAt !== undefined ? { resetsAt } : {}),
173
- });
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
+ });
174
158
  }
175
159
 
176
160
  function addCountBucket(
177
- buckets: UsageBucket[],
178
- limit: Record<string, unknown>,
179
- id: string,
180
- label: string,
181
- windowMinutes?: number,
161
+ buckets: UsageBucket[],
162
+ limit: Record<string, unknown>,
163
+ id: string,
164
+ label: string,
165
+ windowMinutes?: number,
182
166
  ): void {
183
- const used = asNonnegativeNumber(limit.currentValue);
184
- const quota = asNonnegativeNumber(limit.usage);
185
- if (used === undefined || quota === undefined) return;
186
- const resetsAt = asEpochSeconds(limit.nextResetTime);
187
- buckets.push({
188
- id,
189
- label,
190
- used,
191
- remaining: Math.max(0, quota - used),
192
- limit: quota,
193
- unit: "count",
194
- ...(windowMinutes !== undefined ? { windowMinutes } : {}),
195
- ...(resetsAt !== undefined ? { resetsAt } : {}),
196
- });
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
+ });
197
181
  }
198
182
 
199
183
  function addUsageDetailMetrics(metrics: UsageMetric[], value: unknown): void {
200
- if (!Array.isArray(value)) return;
201
- for (const raw of value) {
202
- const detail = asObject(raw);
203
- if (!detail) continue;
204
- const label = asString(detail.modelCode);
205
- const usage = asNonnegativeNumber(detail.usage);
206
- if (!label || usage === undefined) continue;
207
- metrics.push({ id: `mcp-${kebabCase(label)}`, label, value: usage, unit: "count" });
208
- }
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
+ }
209
193
  }
210
194
 
211
195
  function asObject(value: unknown): Record<string, unknown> | undefined {
212
- if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
213
- return value as Record<string, unknown>;
196
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
197
+ return value as Record<string, unknown>;
214
198
  }
215
199
 
216
200
  function asString(value: unknown): string | undefined {
217
- if (typeof value !== "string") return undefined;
218
- return sanitizeDisplayText(value, 80) || undefined;
201
+ if (typeof value !== "string") return undefined;
202
+ return sanitizeDisplayText(value, 80) || undefined;
219
203
  }
220
204
 
221
205
  function asNonnegativeNumber(value: unknown): number | undefined {
222
- if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return undefined;
223
- return value;
206
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return undefined;
207
+ return value;
224
208
  }
225
209
 
226
210
  function asPositiveNumber(value: unknown): number | undefined {
227
- const number = asNonnegativeNumber(value);
228
- return number !== undefined && number > 0 ? number : undefined;
211
+ const number = asNonnegativeNumber(value);
212
+ return number !== undefined && number > 0 ? number : undefined;
229
213
  }
230
214
 
231
215
  function asBoolean(value: unknown): boolean | undefined {
232
- if (typeof value === "boolean") return value;
233
- if (value === 1) return true;
234
- if (value === 0) return false;
235
- return undefined;
216
+ if (typeof value === "boolean") return value;
217
+ if (value === 1) return true;
218
+ if (value === 0) return false;
219
+ return undefined;
236
220
  }
237
221
 
238
222
  function asEpochSeconds(value: unknown): number | undefined {
239
- const millis = asNonnegativeNumber(value);
240
- if (millis === undefined) return undefined;
241
- return Math.floor(millis / 1000);
223
+ const millis = asNonnegativeNumber(value);
224
+ if (millis === undefined) return undefined;
225
+ return Math.floor(millis / 1000);
242
226
  }
243
227
 
244
228
  function kebabCase(label: string): string {
245
- return (
246
- label
247
- .toLowerCase()
248
- .replace(/[^a-z0-9]+/gu, "-")
249
- .replace(/^-+|-+$/gu, "") || "tool"
250
- );
229
+ return (
230
+ label
231
+ .toLowerCase()
232
+ .replace(/[^a-z0-9]+/gu, "-")
233
+ .replace(/^-+|-+$/gu, "") || "tool"
234
+ );
251
235
  }
252
236
 
253
237
  function clampPercent(value: number): number {
254
- return Math.min(100, Math.max(0, value));
238
+ return Math.min(100, Math.max(0, value));
255
239
  }