@bacnh85/pi-sub 0.1.14 → 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 +76 -63
- package/package.json +1 -1
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
|
@@ -13,6 +13,8 @@ const CODEX_PROVIDER = "openai-codex";
|
|
|
13
13
|
const OPC_PROVIDER = "opencode-go";
|
|
14
14
|
const ZAI_PROVIDER = "zai";
|
|
15
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";
|
|
16
18
|
|
|
17
19
|
type ModelLike = { provider?: string; id?: string } | undefined;
|
|
18
20
|
|
|
@@ -101,6 +103,10 @@ function isZaiModel(model: ModelLike): boolean {
|
|
|
101
103
|
return (model?.provider?.toLowerCase() ?? "") === ZAI_PROVIDER;
|
|
102
104
|
}
|
|
103
105
|
|
|
106
|
+
function isZaiCodingCnModel(model: ModelLike): boolean {
|
|
107
|
+
return (model?.provider?.toLowerCase() ?? "") === ZAI_CODING_CN_PROVIDER;
|
|
108
|
+
}
|
|
109
|
+
|
|
104
110
|
function piAuthPath(): string {
|
|
105
111
|
const configDir = process.env.PI_CODING_AGENT_DIR?.trim() || path.join(os.homedir(), ".pi", "agent");
|
|
106
112
|
return path.join(configDir, "auth.json");
|
|
@@ -279,10 +285,10 @@ async function readOpenCodeGoAuth(): Promise<SubscriptionAccountSnapshot> {
|
|
|
279
285
|
return authAccountSnapshot("OpenCode Go", entry, { plan: "Go" });
|
|
280
286
|
}
|
|
281
287
|
|
|
282
|
-
async function readZaiAuth(): Promise<{ key: string; account: SubscriptionAccountSnapshot }> {
|
|
283
|
-
const entry = readStoredCredential(
|
|
284
|
-
if (!entry?.key) throw new Error(
|
|
285
|
-
return { key: entry.key, account: authAccountSnapshot(
|
|
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) };
|
|
286
292
|
}
|
|
287
293
|
|
|
288
294
|
async function fetchUsageFromPiAuth(entry: PiAuthEntry, signal?: AbortSignal): Promise<UsageApiSnapshot | undefined> {
|
|
@@ -372,6 +378,7 @@ interface ZaiUsageApiResponse {
|
|
|
372
378
|
plan?: string;
|
|
373
379
|
plan_type?: string;
|
|
374
380
|
packageName?: string;
|
|
381
|
+
level?: string;
|
|
375
382
|
};
|
|
376
383
|
}
|
|
377
384
|
|
|
@@ -399,75 +406,81 @@ function zaiLimitToUsageWindow(limit: ZaiLimitEntry): UsageWindow | undefined {
|
|
|
399
406
|
|
|
400
407
|
function zaiPlanLabel(response: ZaiUsageApiResponse): string | undefined {
|
|
401
408
|
const data = response.data;
|
|
402
|
-
return planLabel(firstString(data?.planName, data?.plan, data?.plan_type, data?.packageName));
|
|
403
|
-
}
|
|
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
|
+
}
|
|
404
439
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
410
|
-
|
|
411
|
-
const response = await fetch(ZAI_USAGE_URL, {
|
|
412
|
-
headers: {
|
|
413
|
-
Accept: "application/json",
|
|
414
|
-
Authorization: `Bearer ${apiKey}`,
|
|
415
|
-
"User-Agent": "pi-sub/0.1.0",
|
|
416
|
-
},
|
|
417
|
-
signal: combinedSignal,
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
const body = await response.json();
|
|
421
|
-
|
|
422
|
-
// Z.ai returns HTTP 200 even on auth errors: {"code":401,"msg":"...","success":false}
|
|
423
|
-
// Also handle missing success field, empty msg, or presence of code.
|
|
424
|
-
const apiError = body as ZaiUsageApiError;
|
|
425
|
-
if (apiError.code >= 400 || (typeof apiError.success === "boolean" && !apiError.success) || (apiError.msg && apiError.msg.length > 0 && apiError.success === undefined)) {
|
|
426
|
-
const message = apiError.msg || `HTTP status ${apiError.code}`;
|
|
427
|
-
throw new Error(`Z.ai API error: ${message}`);
|
|
428
|
-
}
|
|
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));
|
|
429
444
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
.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
|
+
}
|
|
434
448
|
|
|
435
|
-
|
|
436
|
-
|
|
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
|
+
};
|
|
437
474
|
}
|
|
438
|
-
|
|
439
|
-
// The limit with the nearest reset is the 5-hour rolling window;
|
|
440
|
-
// the next one (if present) is the weekly window.
|
|
441
|
-
const fiveHour = zaiLimitToUsageWindow(tokenLimits[0]);
|
|
442
|
-
const weekly = tokenLimits.length >= 2 ? zaiLimitToUsageWindow(tokenLimits[1]) : undefined;
|
|
443
|
-
|
|
444
|
-
const account: SubscriptionAccountSnapshot = {
|
|
445
|
-
...authAccount,
|
|
446
|
-
plan: zaiPlanLabel(parsed) ?? authAccount.plan,
|
|
447
|
-
fiveHour,
|
|
448
|
-
weekly,
|
|
449
|
-
};
|
|
450
|
-
|
|
451
|
-
return {
|
|
452
|
-
providerDisplayName: "Z.ai",
|
|
453
|
-
accounts: [account],
|
|
454
|
-
activeAccount: account,
|
|
455
|
-
fetchedAt: Date.now(),
|
|
456
|
-
};
|
|
457
|
-
} catch (error) {
|
|
458
|
-
return {
|
|
459
|
-
providerDisplayName: "Z.ai",
|
|
460
|
-
accounts: [],
|
|
461
|
-
fetchedAt: Date.now(),
|
|
462
|
-
error: redactedError(error, "Z.ai"),
|
|
463
|
-
};
|
|
464
475
|
}
|
|
476
|
+
return { fetchUsage };
|
|
465
477
|
}
|
|
466
478
|
|
|
467
479
|
function supportedAdapter(model: ModelLike): SubscriptionProviderAdapter | undefined {
|
|
468
480
|
if (isCodexModel(model)) return { id: CODEX_PROVIDER, displayName: "Codex", fetchUsage: fetchCodexUsage };
|
|
469
481
|
if (isOpenCodeGoModel(model)) return { id: OPC_PROVIDER, displayName: "OpenCode Go", fetchUsage: fetchOpenCodeGoUsage };
|
|
470
|
-
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)") };
|
|
471
484
|
return undefined;
|
|
472
485
|
}
|
|
473
486
|
|