@narumitw/pi-usage 0.52.3 → 0.53.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 +18 -4
- package/dist/index.ts +211 -12
- package/dist/index.ts.map +4 -4
- package/package.json +5 -3
- package/src/format.ts +27 -1
- package/src/index.ts +1 -0
- package/src/providers/zai.ts +150 -0
- package/src/query.ts +55 -0
- package/src/types.ts +5 -0
- package/src/usage.ts +9 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@narumitw/pi-usage",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Pi extension that shows current-account usage for Codex, GitHub Copilot, OpenRouter,
|
|
3
|
+
"version": "0.53.0",
|
|
4
|
+
"description": "Pi extension that shows current-account usage for Codex, GitHub Copilot, OpenRouter, OpenCode Zen, and Z.AI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"private": false,
|
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
"copilot",
|
|
16
16
|
"openrouter",
|
|
17
17
|
"opencode",
|
|
18
|
-
"zen"
|
|
18
|
+
"zen",
|
|
19
|
+
"zai",
|
|
20
|
+
"glm"
|
|
19
21
|
],
|
|
20
22
|
"files": [
|
|
21
23
|
"src",
|
package/src/format.ts
CHANGED
|
@@ -19,7 +19,9 @@ export function formatUsageReport(report: UsageReport, displayState: UsageDispla
|
|
|
19
19
|
else if (report.providerId === "github-copilot") formatGitHubCopilotReport(lines, report);
|
|
20
20
|
else if (report.providerId === "openrouter") formatOpenRouterReport(lines, report);
|
|
21
21
|
else if (report.providerId === "opencode-go") formatOpenCodeZenReport(lines, report);
|
|
22
|
-
else
|
|
22
|
+
else if (report.providerId === "zai" || report.providerId === "zai-coding-cn") {
|
|
23
|
+
formatZaiReport(lines, report);
|
|
24
|
+
} else formatGenericReport(lines, report);
|
|
23
25
|
|
|
24
26
|
if (report.notes) {
|
|
25
27
|
for (const note of report.notes) lines.push(note);
|
|
@@ -160,6 +162,30 @@ function formatOpenCodeZenStatusline(report: UsageReport): string | undefined {
|
|
|
160
162
|
return parts.length > 1 ? parts.join(" ") : undefined;
|
|
161
163
|
}
|
|
162
164
|
|
|
165
|
+
function formatZaiReport(lines: string[], report: UsageReport): void {
|
|
166
|
+
for (const bucket of report.buckets) {
|
|
167
|
+
const reset = bucket.resetsAt ? ` (resets ${formatReset(bucket.resetsAt)})` : "";
|
|
168
|
+
let value = "unavailable";
|
|
169
|
+
if (bucket.unit === "percent" && bucket.used !== undefined) {
|
|
170
|
+
value = `${bucket.used}% used`;
|
|
171
|
+
if (bucket.remaining !== undefined) value += ` · ${bucket.remaining}% left`;
|
|
172
|
+
} else if (bucket.used !== undefined && bucket.limit !== undefined) {
|
|
173
|
+
value = `${bucket.used} of ${bucket.limit} used`;
|
|
174
|
+
if (bucket.remaining !== undefined) value += ` · ${bucket.remaining} left`;
|
|
175
|
+
} else if (bucket.used !== undefined) {
|
|
176
|
+
value = `${bucket.used} used`;
|
|
177
|
+
} else if (bucket.remaining !== undefined) {
|
|
178
|
+
value = `${bucket.remaining} left`;
|
|
179
|
+
}
|
|
180
|
+
lines.push(`${`${bucket.label}:`.padEnd(VALUE_COLUMN)}${value}${reset}`);
|
|
181
|
+
}
|
|
182
|
+
for (const metric of report.metrics) {
|
|
183
|
+
lines.push(
|
|
184
|
+
`${`${metric.label}:`.padEnd(VALUE_COLUMN)}${formatMetricValue(metric.value, metric.unit)}`,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
163
189
|
function formatGenericReport(lines: string[], report: UsageReport): void {
|
|
164
190
|
for (const bucket of report.buckets) {
|
|
165
191
|
lines.push(
|
package/src/index.ts
CHANGED
|
@@ -36,6 +36,7 @@ export { normalizeCodexBackendPayload } from "./providers/codex.js";
|
|
|
36
36
|
export { normalizeGitHubCopilotUsagePayload } from "./providers/github-copilot.js";
|
|
37
37
|
export { normalizeOpenCodeZenPayload } from "./providers/opencode-zen.js";
|
|
38
38
|
export { normalizeOpenRouterKeyPayload } from "./providers/openrouter.js";
|
|
39
|
+
export { normalizeZaiQuotaPayload } from "./providers/zai.js";
|
|
39
40
|
export {
|
|
40
41
|
adapterForProvider,
|
|
41
42
|
isStaleExtensionContextError,
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { sanitizeDisplayText } from "../core.js";
|
|
2
|
+
import type { UsageBucket, UsageMetric, UsageReport, ZaiQuotaPayload } from "../types.js";
|
|
3
|
+
|
|
4
|
+
const FIVE_HOUR_WINDOW_MINUTES = 300;
|
|
5
|
+
const WEEKLY_WINDOW_MINUTES = 10_080;
|
|
6
|
+
|
|
7
|
+
export function normalizeZaiQuotaPayload(
|
|
8
|
+
providerId: string,
|
|
9
|
+
providerName: string,
|
|
10
|
+
payload: ZaiQuotaPayload,
|
|
11
|
+
capturedAt: number,
|
|
12
|
+
): UsageReport {
|
|
13
|
+
const data = asObject(payload.data);
|
|
14
|
+
if (!data) throw new Error("Z.AI quota response data was not an object.");
|
|
15
|
+
const limits = Array.isArray(data.limits) ? (data.limits as unknown[]) : [];
|
|
16
|
+
|
|
17
|
+
const buckets: UsageBucket[] = [];
|
|
18
|
+
const metrics: UsageMetric[] = [];
|
|
19
|
+
for (const raw of limits) {
|
|
20
|
+
const limit = asObject(raw);
|
|
21
|
+
if (!limit) continue;
|
|
22
|
+
const type = asString(limit.type);
|
|
23
|
+
const unit = asNonnegativeNumber(limit.unit);
|
|
24
|
+
const isPlanUsage = type === "TOKENS_LIMIT" || type === "CREDIT_LIMIT";
|
|
25
|
+
if (type === "TIME_LIMIT") {
|
|
26
|
+
addCountBucket(buckets, limit, "mcp-monthly", "MCP monthly allowance");
|
|
27
|
+
addUsageDetailMetrics(metrics, limit.usageDetails);
|
|
28
|
+
} else if (isPlanUsage && unit === 3) {
|
|
29
|
+
addPercentBucket(buckets, limit, "five-hour", "5h window", FIVE_HOUR_WINDOW_MINUTES);
|
|
30
|
+
} else if (isPlanUsage && unit === 6) {
|
|
31
|
+
const used = asNonnegativeNumber(limit.currentValue);
|
|
32
|
+
const quota = asNonnegativeNumber(limit.usage);
|
|
33
|
+
if (used !== undefined && quota !== undefined) {
|
|
34
|
+
addCountBucket(buckets, limit, "weekly", "Weekly window", WEEKLY_WINDOW_MINUTES);
|
|
35
|
+
} else {
|
|
36
|
+
addPercentBucket(buckets, limit, "weekly", "Weekly window", WEEKLY_WINDOW_MINUTES);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (buckets.length === 0) {
|
|
41
|
+
throw new Error("Z.AI quota endpoint returned no displayable usage data.");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const notes: string[] = [];
|
|
45
|
+
const level = asString(data.level);
|
|
46
|
+
if (level) notes.push(`Plan: ${level}`);
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
providerId,
|
|
50
|
+
providerName,
|
|
51
|
+
capturedAt,
|
|
52
|
+
source: "zai-quota",
|
|
53
|
+
semantics: { kind: "consumer-subscription", label: "GLM Coding Plan usage" },
|
|
54
|
+
buckets,
|
|
55
|
+
metrics,
|
|
56
|
+
...(notes.length > 0 ? { notes } : {}),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function addPercentBucket(
|
|
61
|
+
buckets: UsageBucket[],
|
|
62
|
+
limit: Record<string, unknown>,
|
|
63
|
+
id: string,
|
|
64
|
+
label: string,
|
|
65
|
+
windowMinutes: number,
|
|
66
|
+
): void {
|
|
67
|
+
const used = asNonnegativeNumber(limit.percentage);
|
|
68
|
+
if (used === undefined) return;
|
|
69
|
+
const percent = clampPercent(used);
|
|
70
|
+
const resetsAt = asEpochSeconds(limit.nextResetTime);
|
|
71
|
+
buckets.push({
|
|
72
|
+
id,
|
|
73
|
+
label,
|
|
74
|
+
used: percent,
|
|
75
|
+
remaining: 100 - percent,
|
|
76
|
+
limit: 100,
|
|
77
|
+
unit: "percent",
|
|
78
|
+
windowMinutes,
|
|
79
|
+
...(resetsAt !== undefined ? { resetsAt } : {}),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function addCountBucket(
|
|
84
|
+
buckets: UsageBucket[],
|
|
85
|
+
limit: Record<string, unknown>,
|
|
86
|
+
id: string,
|
|
87
|
+
label: string,
|
|
88
|
+
windowMinutes?: number,
|
|
89
|
+
): void {
|
|
90
|
+
const used = asNonnegativeNumber(limit.currentValue);
|
|
91
|
+
const quota = asNonnegativeNumber(limit.usage);
|
|
92
|
+
if (used === undefined || quota === undefined) return;
|
|
93
|
+
const resetsAt = asEpochSeconds(limit.nextResetTime);
|
|
94
|
+
buckets.push({
|
|
95
|
+
id,
|
|
96
|
+
label,
|
|
97
|
+
used,
|
|
98
|
+
remaining: Math.max(0, quota - used),
|
|
99
|
+
limit: quota,
|
|
100
|
+
unit: "count",
|
|
101
|
+
...(windowMinutes !== undefined ? { windowMinutes } : {}),
|
|
102
|
+
...(resetsAt !== undefined ? { resetsAt } : {}),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function addUsageDetailMetrics(metrics: UsageMetric[], value: unknown): void {
|
|
107
|
+
if (!Array.isArray(value)) return;
|
|
108
|
+
for (const raw of value) {
|
|
109
|
+
const detail = asObject(raw);
|
|
110
|
+
if (!detail) continue;
|
|
111
|
+
const label = asString(detail.modelCode);
|
|
112
|
+
const usage = asNonnegativeNumber(detail.usage);
|
|
113
|
+
if (!label || usage === undefined) continue;
|
|
114
|
+
metrics.push({ id: `mcp-${kebabCase(label)}`, label, value: usage, unit: "count" });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function asObject(value: unknown): Record<string, unknown> | undefined {
|
|
119
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
120
|
+
return value as Record<string, unknown>;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function asString(value: unknown): string | undefined {
|
|
124
|
+
if (typeof value !== "string") return undefined;
|
|
125
|
+
return sanitizeDisplayText(value, 80) || undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function asNonnegativeNumber(value: unknown): number | undefined {
|
|
129
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return undefined;
|
|
130
|
+
return value;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function asEpochSeconds(value: unknown): number | undefined {
|
|
134
|
+
const millis = asNonnegativeNumber(value);
|
|
135
|
+
if (millis === undefined) return undefined;
|
|
136
|
+
return Math.floor(millis / 1000);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function kebabCase(label: string): string {
|
|
140
|
+
return (
|
|
141
|
+
label
|
|
142
|
+
.toLowerCase()
|
|
143
|
+
.replace(/[^a-z0-9]+/gu, "-")
|
|
144
|
+
.replace(/^-+|-+$/gu, "") || "tool"
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function clampPercent(value: number): number {
|
|
149
|
+
return Math.min(100, Math.max(0, value));
|
|
150
|
+
}
|
package/src/query.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { normalizeCodexBackendPayload } from "./providers/codex.js";
|
|
|
9
9
|
import { normalizeGitHubCopilotUsagePayload } from "./providers/github-copilot.js";
|
|
10
10
|
import { normalizeOpenCodeZenPayload } from "./providers/opencode-zen.js";
|
|
11
11
|
import { normalizeOpenRouterKeyPayload } from "./providers/openrouter.js";
|
|
12
|
+
import { normalizeZaiQuotaPayload } from "./providers/zai.js";
|
|
12
13
|
import type {
|
|
13
14
|
CodexBackendPayload,
|
|
14
15
|
GitHubCopilotUsagePayload,
|
|
@@ -18,6 +19,7 @@ import type {
|
|
|
18
19
|
ResolvedUsageAuth,
|
|
19
20
|
UsageProviderAdapter,
|
|
20
21
|
UsageReport,
|
|
22
|
+
ZaiQuotaPayload,
|
|
21
23
|
} from "./types.js";
|
|
22
24
|
|
|
23
25
|
const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
@@ -96,6 +98,43 @@ export const SUPPORTED_ADAPTERS: readonly UsageProviderAdapter[] = [
|
|
|
96
98
|
return normalizeOpenCodeZenPayload(payload as OpenCodeZenPayload, Date.now());
|
|
97
99
|
},
|
|
98
100
|
},
|
|
101
|
+
{
|
|
102
|
+
id: "zai",
|
|
103
|
+
displayName: "Z.AI",
|
|
104
|
+
semantics: { kind: "consumer-subscription", label: "GLM Coding Plan usage" },
|
|
105
|
+
publishesStatusline: false,
|
|
106
|
+
async query(auth, signal, timeoutMs) {
|
|
107
|
+
const payload = await fetchProviderJson(
|
|
108
|
+
zaiMonitorUrl(auth.model.baseUrl),
|
|
109
|
+
zaiMonitorAuth(auth),
|
|
110
|
+
signal,
|
|
111
|
+
timeoutMs,
|
|
112
|
+
"Z.AI quota endpoint",
|
|
113
|
+
);
|
|
114
|
+
return normalizeZaiQuotaPayload("zai", "Z.AI", payload as ZaiQuotaPayload, Date.now());
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
id: "zai-coding-cn",
|
|
119
|
+
displayName: "Z.AI Coding CN",
|
|
120
|
+
semantics: { kind: "consumer-subscription", label: "GLM Coding Plan usage" },
|
|
121
|
+
publishesStatusline: false,
|
|
122
|
+
async query(auth, signal, timeoutMs) {
|
|
123
|
+
const payload = await fetchProviderJson(
|
|
124
|
+
zaiMonitorUrl(auth.model.baseUrl),
|
|
125
|
+
zaiMonitorAuth(auth),
|
|
126
|
+
signal,
|
|
127
|
+
timeoutMs,
|
|
128
|
+
"Z.AI Coding CN quota endpoint",
|
|
129
|
+
);
|
|
130
|
+
return normalizeZaiQuotaPayload(
|
|
131
|
+
"zai-coding-cn",
|
|
132
|
+
"Z.AI Coding CN",
|
|
133
|
+
payload as ZaiQuotaPayload,
|
|
134
|
+
Date.now(),
|
|
135
|
+
);
|
|
136
|
+
},
|
|
137
|
+
},
|
|
99
138
|
];
|
|
100
139
|
|
|
101
140
|
export function adapterForProvider(
|
|
@@ -470,6 +509,8 @@ function hasOfficialUrlOrigin(value: string, providerId: string): boolean {
|
|
|
470
509
|
if (providerId === "openai-codex") return url.origin === "https://chatgpt.com";
|
|
471
510
|
if (providerId === "openrouter") return url.origin === "https://openrouter.ai";
|
|
472
511
|
if (providerId === "opencode-go") return url.origin === "https://opencode.ai";
|
|
512
|
+
if (providerId === "zai") return url.origin === "https://api.z.ai";
|
|
513
|
+
if (providerId === "zai-coding-cn") return url.origin === "https://open.bigmodel.cn";
|
|
473
514
|
if (providerId === "github-copilot") {
|
|
474
515
|
return (
|
|
475
516
|
url.protocol === "https:" && /^api\.[a-z0-9-]+\.githubcopilot\.com$/u.test(url.hostname)
|
|
@@ -495,6 +536,20 @@ function hasHeader(headers: Record<string, string>, name: string): boolean {
|
|
|
495
536
|
return Object.keys(headers).some((key) => key.toLowerCase() === name.toLowerCase());
|
|
496
537
|
}
|
|
497
538
|
|
|
539
|
+
function zaiMonitorUrl(baseUrl: string | undefined): string {
|
|
540
|
+
const base = baseUrl?.trim();
|
|
541
|
+
if (!base) throw new Error("Z.AI model base URL is unavailable.");
|
|
542
|
+
return `${new URL(base).origin}/api/monitor/usage/quota/limit`;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function zaiMonitorAuth(auth: ResolvedUsageAuth): ResolvedUsageAuth {
|
|
546
|
+
const authorization = headerValue(auth.headers, "Authorization");
|
|
547
|
+
const token =
|
|
548
|
+
authorization === undefined ? undefined : (bearerToken(authorization) ?? authorization);
|
|
549
|
+
if (token === undefined || token === authorization) return auth;
|
|
550
|
+
return { ...auth, headers: { ...auth.headers, Authorization: token } };
|
|
551
|
+
}
|
|
552
|
+
|
|
498
553
|
function isAbortError(error: unknown): boolean {
|
|
499
554
|
return error instanceof Error && error.name === "AbortError";
|
|
500
555
|
}
|
package/src/types.ts
CHANGED
|
@@ -58,6 +58,7 @@ export interface UsageProviderAdapter {
|
|
|
58
58
|
id: string;
|
|
59
59
|
displayName: string;
|
|
60
60
|
semantics: UsageSemantics;
|
|
61
|
+
publishesStatusline?: boolean;
|
|
61
62
|
query(auth: ResolvedUsageAuth, signal: AbortSignal, timeoutMs: number): Promise<UsageReport>;
|
|
62
63
|
}
|
|
63
64
|
|
|
@@ -97,6 +98,10 @@ export type OpenCodeZenPayload = {
|
|
|
97
98
|
usage?: unknown;
|
|
98
99
|
};
|
|
99
100
|
|
|
101
|
+
export type ZaiQuotaPayload = {
|
|
102
|
+
data?: unknown;
|
|
103
|
+
};
|
|
104
|
+
|
|
100
105
|
export type CodexBackendPayload = {
|
|
101
106
|
plan_type?: unknown;
|
|
102
107
|
rate_limit?: unknown;
|
package/src/usage.ts
CHANGED
|
@@ -136,6 +136,11 @@ export default function usageExtension(
|
|
|
136
136
|
model: PiModel,
|
|
137
137
|
shouldSchedule: boolean,
|
|
138
138
|
) => {
|
|
139
|
+
if (adapterForProvider(model.provider)?.publishesStatusline === false) {
|
|
140
|
+
clearStatusTimer();
|
|
141
|
+
safeSetStatus(ctx, undefined);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
139
144
|
if (outcome.state.status === "unsupported") {
|
|
140
145
|
clearStatusTimer();
|
|
141
146
|
safeSetStatus(ctx, undefined);
|
|
@@ -345,6 +350,10 @@ export default function usageExtension(
|
|
|
345
350
|
clearStatus(ctx);
|
|
346
351
|
return;
|
|
347
352
|
}
|
|
353
|
+
if (adapter.publishesStatusline === false) {
|
|
354
|
+
clearStatus(ctx);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
348
357
|
statusGeneration += 1;
|
|
349
358
|
const generation = statusGeneration;
|
|
350
359
|
statusController?.abort();
|