@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.
- package/dist/index.ts +91 -339
- package/dist/index.ts.map +2 -2
- package/docs/providers.md +2 -1
- package/package.json +73 -74
- package/src/codex-fast-runtime.ts +148 -184
- package/src/codex-fast.ts +74 -83
- package/src/codex-resets.ts +249 -278
- package/src/core.ts +204 -206
- package/src/format.ts +519 -574
- package/src/index.ts +77 -77
- package/src/oauth-credential-source.ts +57 -65
- package/src/providers/baseten.ts +32 -36
- package/src/providers/codex.ts +109 -120
- package/src/providers/deepseek.ts +59 -64
- package/src/providers/fireworks.ts +219 -238
- package/src/providers/github-copilot.ts +89 -95
- package/src/providers/kimi-coding.ts +202 -213
- package/src/providers/minimax.ts +214 -219
- package/src/providers/moonshot.ts +41 -46
- package/src/providers/opencode-zen.ts +56 -59
- package/src/providers/openrouter.ts +50 -53
- package/src/providers/vercel-ai-gateway.ts +33 -33
- package/src/providers/xai.ts +141 -148
- package/src/providers/zai-errors.ts +75 -75
- package/src/providers/zai.ts +173 -189
- package/src/query.ts +887 -967
- package/src/settings.ts +275 -304
- package/src/types.ts +145 -145
- package/src/usage-helpers.ts +18 -18
- package/src/usage-settings-ui.ts +117 -126
- package/src/usage-targets.ts +98 -114
- package/src/usage.ts +1264 -1426
package/src/providers/zai.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { sanitizeDisplayText } from "../core.js";
|
|
2
2
|
import type {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
providerId: string,
|
|
18
|
+
providerName: string,
|
|
19
|
+
payload: ZaiQuotaPayload,
|
|
20
|
+
capturedAt: number,
|
|
21
|
+
plan?: ZaiPlanInfo,
|
|
22
22
|
): UsageReport {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
87
|
-
)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
140
|
-
|
|
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
|
-
|
|
145
|
-
|
|
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
|
-
|
|
150
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
138
|
+
buckets: UsageBucket[],
|
|
139
|
+
limit: Record<string, unknown>,
|
|
140
|
+
id: string,
|
|
141
|
+
label: string,
|
|
142
|
+
windowMinutes: number,
|
|
159
143
|
): void {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
161
|
+
buckets: UsageBucket[],
|
|
162
|
+
limit: Record<string, unknown>,
|
|
163
|
+
id: string,
|
|
164
|
+
label: string,
|
|
165
|
+
windowMinutes?: number,
|
|
182
166
|
): void {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
213
|
-
|
|
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
|
-
|
|
218
|
-
|
|
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
|
-
|
|
223
|
-
|
|
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
|
-
|
|
228
|
-
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
238
|
+
return Math.min(100, Math.max(0, value));
|
|
255
239
|
}
|