@bacnh85/pi-sub 0.1.13 → 0.1.15
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 +13 -2
- package/extensions/index.ts +79 -76
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Pi extension that shows subscription usage for the currently selected supported model provider.
|
|
4
4
|
|
|
5
|
-
Supports OpenAI Codex (`openai-codex`) with live usage windows from ChatGPT's usage endpoint, OpenCode Go (`opencode-go`) with session cost tracking, and Z.ai (`zai`)
|
|
5
|
+
Supports OpenAI Codex (`openai-codex`) with live usage windows from ChatGPT's usage endpoint, OpenCode Go (`opencode-go`) with session cost tracking, and Z.ai GLM Coding Plan — both the international (`zai`) and China (`zai-coding-cn`, `open.bigmodel.cn`) endpoints — with quota monitoring. Displays a subscription footer status after Pi's built-in status/token usage line.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -53,6 +53,14 @@ Z.ai (GLM Coding Plan) shows the active account/key label, 5-hour rolling and we
|
|
|
53
53
|
(Z.ai key#1a2b3c4d) R:55%/2H W:80%/3D 42 tok/s
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
+
### Z.ai Coding Plan (China)
|
|
57
|
+
|
|
58
|
+
The built-in `zai-coding-cn` provider targets the domestic BigModel endpoint (`open.bigmodel.cn`) and returns the same GLM Coding Plan quota format as the international `zai` provider, so the footer and `/sub` detail behave identically, distinguished only by the `Z.ai (CN)` label:
|
|
59
|
+
|
|
60
|
+
```text
|
|
61
|
+
(Z.ai (CN) key#1a2b3c4d) R:55%/2H W:80%/3D 42 tok/s
|
|
62
|
+
```
|
|
63
|
+
|
|
56
64
|
### Tokens per second
|
|
57
65
|
|
|
58
66
|
`pi-sub` tracks each response's tokens-per-second (tok/s) speed by measuring the time from provider request to message completion against the response's output token count. The last response's speed is shown in the footer next to usage data. The `/sub` detail view shows both the last response speed and the session-wide average.
|
|
@@ -104,6 +112,8 @@ Last response: 42 tok/s · Session avg: 39 tok/s
|
|
|
104
112
|
* Z.ai key#1a2b3c4d Pro 55%/2H 80%/3D Now
|
|
105
113
|
```
|
|
106
114
|
|
|
115
|
+
For Z.ai Coding Plan (China), the `/sub` detail is the same with a `Z.ai (CN)` provider/account label:
|
|
116
|
+
|
|
107
117
|
## Refresh behavior
|
|
108
118
|
|
|
109
119
|
`pi-sub` refreshes usage data:
|
|
@@ -123,9 +133,10 @@ Refreshes are cached briefly to avoid excessive usage endpoint calls.
|
|
|
123
133
|
- **OpenAI Codex**: Pi auth must contain an `openai-codex` OAuth entry in `~/.pi/agent/auth.json` or `$PI_CODING_AGENT_DIR/auth.json`. The entry must include `access` and `accountId` fields.
|
|
124
134
|
- **OpenCode Go**: Pi auth must contain an `opencode-go` API key entry (via `/login` or env var). The entry must have a `key` field or an `accountId` field. OpenCode Go/Zen usage windows and Zen balance are not shown because no public API is currently documented for those values.
|
|
125
135
|
- **Z.ai**: Pi auth must contain a `zai` entry in `auth.json` with a `key` field (the same API key used for Z.ai model access via `@czottmann/pi-zai-api`). The Z.ai provider must be registered (e.g., `pi install npm:@czottmann/pi-zai-api`).
|
|
136
|
+
- **Z.ai Coding Plan (China)**: The built-in `zai-coding-cn` provider targets `https://open.bigmodel.cn/api/coding/paas/v4`. Pi auth must contain a `zai-coding-cn` entry with a `key` field (set via `/login` or the `ZAI_CODING_CN_API_KEY` env var). Quota is read from the BigModel endpoint `https://open.bigmodel.cn/api/monitor/usage/quota/limit`.
|
|
126
137
|
- For API-key-only providers, account labels come from stored auth metadata (`email`, `label`, `name`, or `accountId`) when available; otherwise `pi-sub` displays a non-secret SHA-256 key fingerprint such as `Z.ai key#1a2b3c4d`.
|
|
127
138
|
- `pi-sub` redacts auth/token-related errors and never prints credentials.
|
|
128
139
|
|
|
129
140
|
## Design notes
|
|
130
141
|
|
|
131
|
-
The extension is named `pi-sub` rather than `pi-codex-usage` so future subscription providers can be added as separate adapters. Supports OpenAI Codex (live usage API)
|
|
142
|
+
The extension is named `pi-sub` rather than `pi-codex-usage` so future subscription providers can be added as separate adapters. Supports OpenAI Codex (live usage API), OpenCode Go (session cost only), and the Z.ai GLM Coding Plan (international `zai` and China `zai-coding-cn`, which share a quota response format and are served by one parameterized adapter).
|
package/extensions/index.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { readStoredCredential, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
-
import fs from "node:fs/promises";
|
|
4
3
|
import os from "node:os";
|
|
5
4
|
import path from "node:path";
|
|
6
5
|
|
|
@@ -14,6 +13,8 @@ const CODEX_PROVIDER = "openai-codex";
|
|
|
14
13
|
const OPC_PROVIDER = "opencode-go";
|
|
15
14
|
const ZAI_PROVIDER = "zai";
|
|
16
15
|
const ZAI_USAGE_URL = "https://api.z.ai/api/monitor/usage/quota/limit";
|
|
16
|
+
const ZAI_CODING_CN_PROVIDER = "zai-coding-cn";
|
|
17
|
+
const ZAI_CODING_CN_USAGE_URL = "https://open.bigmodel.cn/api/monitor/usage/quota/limit";
|
|
17
18
|
|
|
18
19
|
type ModelLike = { provider?: string; id?: string } | undefined;
|
|
19
20
|
|
|
@@ -28,8 +29,6 @@ type UsageApiSnapshot = {
|
|
|
28
29
|
plan_type?: string;
|
|
29
30
|
};
|
|
30
31
|
|
|
31
|
-
type PiAuthFile = Record<string, PiAuthEntry | undefined>;
|
|
32
|
-
|
|
33
32
|
type PiAuthEntry = {
|
|
34
33
|
type?: string;
|
|
35
34
|
access?: string;
|
|
@@ -104,15 +103,15 @@ function isZaiModel(model: ModelLike): boolean {
|
|
|
104
103
|
return (model?.provider?.toLowerCase() ?? "") === ZAI_PROVIDER;
|
|
105
104
|
}
|
|
106
105
|
|
|
106
|
+
function isZaiCodingCnModel(model: ModelLike): boolean {
|
|
107
|
+
return (model?.provider?.toLowerCase() ?? "") === ZAI_CODING_CN_PROVIDER;
|
|
108
|
+
}
|
|
109
|
+
|
|
107
110
|
function piAuthPath(): string {
|
|
108
111
|
const configDir = process.env.PI_CODING_AGENT_DIR?.trim() || path.join(os.homedir(), ".pi", "agent");
|
|
109
112
|
return path.join(configDir, "auth.json");
|
|
110
113
|
}
|
|
111
114
|
|
|
112
|
-
async function readJsonFile<T>(file: string): Promise<T> {
|
|
113
|
-
return JSON.parse(await fs.readFile(file, "utf8")) as T;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
115
|
function decodeJwtPayload(token: string | undefined): Record<string, any> | undefined {
|
|
117
116
|
if (!token) return undefined;
|
|
118
117
|
const parts = token.split(".");
|
|
@@ -274,25 +273,22 @@ function parseUsageResponse(body: unknown): UsageApiSnapshot | undefined {
|
|
|
274
273
|
}
|
|
275
274
|
|
|
276
275
|
async function readPiCodexAuth(): Promise<PiAuthEntry & { accountId: string }> {
|
|
277
|
-
const
|
|
278
|
-
const entry = auth[CODEX_PROVIDER];
|
|
276
|
+
const entry = readStoredCredential(CODEX_PROVIDER, piAuthPath()) as PiAuthEntry | undefined;
|
|
279
277
|
const accountId = getCodexAccountId(entry);
|
|
280
278
|
if (!entry?.access || !accountId) throw new Error("Missing openai-codex OAuth entry in Pi auth");
|
|
281
279
|
return { ...entry, accountId };
|
|
282
280
|
}
|
|
283
281
|
|
|
284
282
|
async function readOpenCodeGoAuth(): Promise<SubscriptionAccountSnapshot> {
|
|
285
|
-
const
|
|
286
|
-
const entry = auth[OPC_PROVIDER];
|
|
283
|
+
const entry = readStoredCredential(OPC_PROVIDER, piAuthPath()) as PiAuthEntry | undefined;
|
|
287
284
|
if (!entry?.key && !entry?.accountId) throw new Error("Missing opencode-go API key or accountId in Pi auth");
|
|
288
285
|
return authAccountSnapshot("OpenCode Go", entry, { plan: "Go" });
|
|
289
286
|
}
|
|
290
287
|
|
|
291
|
-
async function readZaiAuth(): Promise<{ key: string; account: SubscriptionAccountSnapshot }> {
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
return { key: entry.key, account: authAccountSnapshot("Z.ai", entry) };
|
|
288
|
+
async function readZaiAuth(providerId: string, label: string): Promise<{ key: string; account: SubscriptionAccountSnapshot }> {
|
|
289
|
+
const entry = readStoredCredential(providerId, piAuthPath()) as PiAuthEntry | undefined;
|
|
290
|
+
if (!entry?.key) throw new Error(`Missing ${providerId} API key in Pi auth`);
|
|
291
|
+
return { key: entry.key, account: authAccountSnapshot(label, entry) };
|
|
296
292
|
}
|
|
297
293
|
|
|
298
294
|
async function fetchUsageFromPiAuth(entry: PiAuthEntry, signal?: AbortSignal): Promise<UsageApiSnapshot | undefined> {
|
|
@@ -382,6 +378,7 @@ interface ZaiUsageApiResponse {
|
|
|
382
378
|
plan?: string;
|
|
383
379
|
plan_type?: string;
|
|
384
380
|
packageName?: string;
|
|
381
|
+
level?: string;
|
|
385
382
|
};
|
|
386
383
|
}
|
|
387
384
|
|
|
@@ -409,75 +406,81 @@ function zaiLimitToUsageWindow(limit: ZaiLimitEntry): UsageWindow | undefined {
|
|
|
409
406
|
|
|
410
407
|
function zaiPlanLabel(response: ZaiUsageApiResponse): string | undefined {
|
|
411
408
|
const data = response.data;
|
|
412
|
-
return planLabel(firstString(data?.planName, data?.plan, data?.plan_type, data?.packageName));
|
|
413
|
-
}
|
|
409
|
+
return planLabel(firstString(data?.planName, data?.plan, data?.plan_type, data?.packageName, data?.level));
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Factory: the international `zai` and China `zai-coding-cn` endpoints share an
|
|
413
|
+
// identical quota response; only the provider id, host, and label differ.
|
|
414
|
+
function zaiUsageAdapter(providerId: string, usageUrl: string, displayName: string): { fetchUsage(signal?: AbortSignal): Promise<SubscriptionUsageSnapshot> } {
|
|
415
|
+
async function fetchUsage(signal?: AbortSignal): Promise<SubscriptionUsageSnapshot> {
|
|
416
|
+
try {
|
|
417
|
+
const { key: apiKey, account: authAccount } = await readZaiAuth(providerId, displayName);
|
|
418
|
+
const timeoutSignal = AbortSignal.timeout(7_000);
|
|
419
|
+
const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
420
|
+
|
|
421
|
+
const response = await fetch(usageUrl, {
|
|
422
|
+
headers: {
|
|
423
|
+
Accept: "application/json",
|
|
424
|
+
Authorization: `Bearer ${apiKey}`,
|
|
425
|
+
"User-Agent": "pi-sub/0.1.0",
|
|
426
|
+
},
|
|
427
|
+
signal: combinedSignal,
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
const body = await response.json();
|
|
431
|
+
|
|
432
|
+
// Z.ai / BigModel return HTTP 200 even on auth errors: {"code":401,"msg":"...","success":false}
|
|
433
|
+
// Also handle missing success field, empty msg, or presence of code.
|
|
434
|
+
const apiError = body as ZaiUsageApiError;
|
|
435
|
+
if (apiError.code >= 400 || (typeof apiError.success === "boolean" && !apiError.success) || (apiError.msg && apiError.msg.length > 0 && apiError.success === undefined)) {
|
|
436
|
+
const message = apiError.msg || `HTTP status ${apiError.code}`;
|
|
437
|
+
throw new Error(`${displayName} API error: ${message}`);
|
|
438
|
+
}
|
|
414
439
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
420
|
-
|
|
421
|
-
const response = await fetch(ZAI_USAGE_URL, {
|
|
422
|
-
headers: {
|
|
423
|
-
Accept: "application/json",
|
|
424
|
-
Authorization: `Bearer ${apiKey}`,
|
|
425
|
-
"User-Agent": "pi-sub/0.1.0",
|
|
426
|
-
},
|
|
427
|
-
signal: combinedSignal,
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
const body = await response.json();
|
|
431
|
-
|
|
432
|
-
// Z.ai returns HTTP 200 even on auth errors: {"code":401,"msg":"...","success":false}
|
|
433
|
-
// Also handle missing success field, empty msg, or presence of code.
|
|
434
|
-
const apiError = body as ZaiUsageApiError;
|
|
435
|
-
if (apiError.code >= 400 || (typeof apiError.success === "boolean" && !apiError.success) || (apiError.msg && apiError.msg.length > 0 && apiError.success === undefined)) {
|
|
436
|
-
const message = apiError.msg || `HTTP status ${apiError.code}`;
|
|
437
|
-
throw new Error(`Z.ai API error: ${message}`);
|
|
438
|
-
}
|
|
440
|
+
const parsed = body as ZaiUsageApiResponse;
|
|
441
|
+
const tokenLimits = (parsed.data?.limits ?? [])
|
|
442
|
+
.filter((l) => l.type === "TOKENS_LIMIT")
|
|
443
|
+
.sort((a, b) => (a.nextResetTime ?? 0) - (b.nextResetTime ?? 0));
|
|
439
444
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
.sort((a, b) => (a.nextResetTime ?? 0) - (b.nextResetTime ?? 0));
|
|
445
|
+
if (tokenLimits.length === 0) {
|
|
446
|
+
throw new Error(`No TOKENS_LIMIT entries in ${displayName} usage response`);
|
|
447
|
+
}
|
|
444
448
|
|
|
445
|
-
|
|
446
|
-
|
|
449
|
+
// The limit with the nearest reset is the 5-hour rolling window;
|
|
450
|
+
// the next one (if present) is the weekly window.
|
|
451
|
+
const fiveHour = zaiLimitToUsageWindow(tokenLimits[0]);
|
|
452
|
+
const weekly = tokenLimits.length >= 2 ? zaiLimitToUsageWindow(tokenLimits[1]) : undefined;
|
|
453
|
+
|
|
454
|
+
const account: SubscriptionAccountSnapshot = {
|
|
455
|
+
...authAccount,
|
|
456
|
+
plan: zaiPlanLabel(parsed) ?? authAccount.plan,
|
|
457
|
+
fiveHour,
|
|
458
|
+
weekly,
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
return {
|
|
462
|
+
providerDisplayName: displayName,
|
|
463
|
+
accounts: [account],
|
|
464
|
+
activeAccount: account,
|
|
465
|
+
fetchedAt: Date.now(),
|
|
466
|
+
};
|
|
467
|
+
} catch (error) {
|
|
468
|
+
return {
|
|
469
|
+
providerDisplayName: displayName,
|
|
470
|
+
accounts: [],
|
|
471
|
+
fetchedAt: Date.now(),
|
|
472
|
+
error: redactedError(error, displayName),
|
|
473
|
+
};
|
|
447
474
|
}
|
|
448
|
-
|
|
449
|
-
// The limit with the nearest reset is the 5-hour rolling window;
|
|
450
|
-
// the next one (if present) is the weekly window.
|
|
451
|
-
const fiveHour = zaiLimitToUsageWindow(tokenLimits[0]);
|
|
452
|
-
const weekly = tokenLimits.length >= 2 ? zaiLimitToUsageWindow(tokenLimits[1]) : undefined;
|
|
453
|
-
|
|
454
|
-
const account: SubscriptionAccountSnapshot = {
|
|
455
|
-
...authAccount,
|
|
456
|
-
plan: zaiPlanLabel(parsed) ?? authAccount.plan,
|
|
457
|
-
fiveHour,
|
|
458
|
-
weekly,
|
|
459
|
-
};
|
|
460
|
-
|
|
461
|
-
return {
|
|
462
|
-
providerDisplayName: "Z.ai",
|
|
463
|
-
accounts: [account],
|
|
464
|
-
activeAccount: account,
|
|
465
|
-
fetchedAt: Date.now(),
|
|
466
|
-
};
|
|
467
|
-
} catch (error) {
|
|
468
|
-
return {
|
|
469
|
-
providerDisplayName: "Z.ai",
|
|
470
|
-
accounts: [],
|
|
471
|
-
fetchedAt: Date.now(),
|
|
472
|
-
error: redactedError(error, "Z.ai"),
|
|
473
|
-
};
|
|
474
475
|
}
|
|
476
|
+
return { fetchUsage };
|
|
475
477
|
}
|
|
476
478
|
|
|
477
479
|
function supportedAdapter(model: ModelLike): SubscriptionProviderAdapter | undefined {
|
|
478
480
|
if (isCodexModel(model)) return { id: CODEX_PROVIDER, displayName: "Codex", fetchUsage: fetchCodexUsage };
|
|
479
481
|
if (isOpenCodeGoModel(model)) return { id: OPC_PROVIDER, displayName: "OpenCode Go", fetchUsage: fetchOpenCodeGoUsage };
|
|
480
|
-
if (isZaiModel(model)) return { id: ZAI_PROVIDER, displayName: "Z.ai",
|
|
482
|
+
if (isZaiModel(model)) return { id: ZAI_PROVIDER, displayName: "Z.ai", ...zaiUsageAdapter(ZAI_PROVIDER, ZAI_USAGE_URL, "Z.ai") };
|
|
483
|
+
if (isZaiCodingCnModel(model)) return { id: ZAI_CODING_CN_PROVIDER, displayName: "Z.ai (CN)", ...zaiUsageAdapter(ZAI_CODING_CN_PROVIDER, ZAI_CODING_CN_USAGE_URL, "Z.ai (CN)") };
|
|
481
484
|
return undefined;
|
|
482
485
|
}
|
|
483
486
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bacnh85/pi-sub",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"description": "Pi extension showing subscription usage for supported model providers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -38,6 +38,6 @@
|
|
|
38
38
|
"node": ">=20.3.0"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
|
-
"@earendil-works/pi-coding-agent": "
|
|
41
|
+
"@earendil-works/pi-coding-agent": ">=0.80.8 <0.81.0"
|
|
42
42
|
}
|
|
43
43
|
}
|