@bacnh85/pi-sub 0.1.7 → 0.1.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/README.md +20 -1
- package/index.ts +125 -1
- 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,
|
|
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`) with GLM Coding Plan quota monitoring. Displays a subscription footer status after Pi's built-in status/token usage line.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -50,6 +50,14 @@ OpenCode Go does not expose usage windows, so the footer shows only the accumula
|
|
|
50
50
|
OpenCode Go $0.23
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
+
### Z.ai
|
|
54
|
+
|
|
55
|
+
Z.ai (GLM Coding Plan) shows 5-hour rolling and weekly quota percentages with reset times:
|
|
56
|
+
|
|
57
|
+
```text
|
|
58
|
+
R:55%(18:12) W:80%(08:00 on 2 Jul)
|
|
59
|
+
```
|
|
60
|
+
|
|
53
61
|
When the current model provider is not supported, `pi-sub` clears its subscription line and does not refresh subscription data.
|
|
54
62
|
|
|
55
63
|
## Commands
|
|
@@ -75,6 +83,16 @@ OpenCode Go does not expose usage windows.
|
|
|
75
83
|
Session cost: $0.23
|
|
76
84
|
```
|
|
77
85
|
|
|
86
|
+
For Z.ai, `/sub` shows the rolling and weekly quota windows:
|
|
87
|
+
|
|
88
|
+
```text
|
|
89
|
+
Provider: Z.ai · Model: glm-5.2 · Fetched: 14:23
|
|
90
|
+
|
|
91
|
+
ACCOUNT PLAN ROLLING WEEKLY LAST ACTIVITY
|
|
92
|
+
------------------------------------------------------------------------
|
|
93
|
+
* Z.ai subscription ? 55%/18:12 80%/08:00 on 2 Jul Now
|
|
94
|
+
```
|
|
95
|
+
|
|
78
96
|
## Refresh behavior
|
|
79
97
|
|
|
80
98
|
`pi-sub` refreshes usage data:
|
|
@@ -93,6 +111,7 @@ Refreshes are cached briefly to avoid excessive usage endpoint calls.
|
|
|
93
111
|
|
|
94
112
|
- **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.
|
|
95
113
|
- **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.
|
|
114
|
+
- **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`).
|
|
96
115
|
- `pi-sub` redacts auth/token-related errors and never prints credentials.
|
|
97
116
|
|
|
98
117
|
## Design notes
|
package/index.ts
CHANGED
|
@@ -12,6 +12,8 @@ const REFRESH_TTL_MS = 30_000;
|
|
|
12
12
|
const REFRESH_DEBOUNCE_MS = 2_000;
|
|
13
13
|
const CODEX_PROVIDER = "openai-codex";
|
|
14
14
|
const OPC_PROVIDER = "opencode-go";
|
|
15
|
+
const ZAI_PROVIDER = "zai";
|
|
16
|
+
const ZAI_USAGE_URL = "https://api.z.ai/api/monitor/usage/quota/limit";
|
|
15
17
|
|
|
16
18
|
type ModelLike = { provider?: string; id?: string } | undefined;
|
|
17
19
|
|
|
@@ -98,6 +100,10 @@ function isOpenCodeGoModel(model: ModelLike): boolean {
|
|
|
98
100
|
return (model?.provider?.toLowerCase() ?? "") === OPC_PROVIDER;
|
|
99
101
|
}
|
|
100
102
|
|
|
103
|
+
function isZaiModel(model: ModelLike): boolean {
|
|
104
|
+
return (model?.provider?.toLowerCase() ?? "") === ZAI_PROVIDER;
|
|
105
|
+
}
|
|
106
|
+
|
|
101
107
|
function piAuthPath(): string {
|
|
102
108
|
const configDir = process.env.PI_CODING_AGENT_DIR?.trim() || path.join(os.homedir(), ".pi", "agent");
|
|
103
109
|
return path.join(configDir, "auth.json");
|
|
@@ -246,6 +252,13 @@ async function readOpenCodeGoAuth(): Promise<{ key?: string; accountId?: string
|
|
|
246
252
|
return { accountId: typeof entry.accountId === "string" ? entry.accountId : undefined };
|
|
247
253
|
}
|
|
248
254
|
|
|
255
|
+
async function readZaiAuth(): Promise<string> {
|
|
256
|
+
const auth = await readJsonFile<PiAuthFile>(piAuthPath());
|
|
257
|
+
const entry = auth[ZAI_PROVIDER];
|
|
258
|
+
if (!entry?.key) throw new Error("Missing zai API key in Pi auth");
|
|
259
|
+
return entry.key;
|
|
260
|
+
}
|
|
261
|
+
|
|
249
262
|
async function fetchUsageFromPiAuth(entry: PiAuthEntry, signal?: AbortSignal): Promise<UsageApiSnapshot | undefined> {
|
|
250
263
|
const timeoutSignal = AbortSignal.timeout(7_000);
|
|
251
264
|
const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
@@ -267,6 +280,7 @@ function redactedError(error: unknown, provider = "Codex"): string {
|
|
|
267
280
|
if (/ENOENT|no such file/i.test(message)) return "Pi auth not found";
|
|
268
281
|
if (/missing openai-codex/i.test(message)) return "openai-codex auth not found";
|
|
269
282
|
if (/missing opencode-go/i.test(message)) return "opencode-go auth not found";
|
|
283
|
+
if (/missing zai/i.test(message)) return "zai auth not found";
|
|
270
284
|
if (/timed out|timeout|aborted/i.test(message)) return `${provider} usage refresh timed out`;
|
|
271
285
|
if (/401|403|auth|token|unauthorized|forbidden/i.test(message)) return `${provider} auth unavailable`;
|
|
272
286
|
return `${provider} usage unavailable`;
|
|
@@ -316,6 +330,109 @@ async function fetchOpenCodeGoUsage(_signal?: AbortSignal): Promise<Subscription
|
|
|
316
330
|
}
|
|
317
331
|
}
|
|
318
332
|
|
|
333
|
+
// ---------------------------------------------------------------------------
|
|
334
|
+
// Z.ai adapter
|
|
335
|
+
// ---------------------------------------------------------------------------
|
|
336
|
+
|
|
337
|
+
interface ZaiLimitEntry {
|
|
338
|
+
type: string;
|
|
339
|
+
percentage: number;
|
|
340
|
+
nextResetTime?: number;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
interface ZaiUsageApiResponse {
|
|
344
|
+
data?: {
|
|
345
|
+
limits?: ZaiLimitEntry[];
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
interface ZaiUsageApiError {
|
|
350
|
+
code: number;
|
|
351
|
+
msg: string;
|
|
352
|
+
success: boolean;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function zaiLimitToUsageWindow(limit: ZaiLimitEntry): UsageWindow | undefined {
|
|
356
|
+
if (typeof limit.percentage !== "number") return undefined;
|
|
357
|
+
const percent = Math.round(limit.percentage);
|
|
358
|
+
const remaining = Math.max(0, 100 - percent);
|
|
359
|
+
// Z.ai returns nextResetTime in epoch milliseconds; format helpers expect seconds.
|
|
360
|
+
const resetAtSec = limit.nextResetTime ? limit.nextResetTime / 1000 : undefined;
|
|
361
|
+
const resetLabel = formatReset(resetAtSec);
|
|
362
|
+
const remainingLabel = formatRemainingTime(resetAtSec);
|
|
363
|
+
return {
|
|
364
|
+
percent,
|
|
365
|
+
remaining,
|
|
366
|
+
remainingLabel,
|
|
367
|
+
resetLabel,
|
|
368
|
+
resetsAt: limit.nextResetTime,
|
|
369
|
+
label: remainingLabel ? `${remaining}% (${remainingLabel})` : `${remaining}%`,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async function fetchZaiUsage(signal?: AbortSignal): Promise<SubscriptionUsageSnapshot> {
|
|
374
|
+
try {
|
|
375
|
+
const apiKey = await readZaiAuth();
|
|
376
|
+
const timeoutSignal = AbortSignal.timeout(7_000);
|
|
377
|
+
const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
378
|
+
|
|
379
|
+
const response = await fetch(ZAI_USAGE_URL, {
|
|
380
|
+
headers: {
|
|
381
|
+
Accept: "application/json",
|
|
382
|
+
Authorization: `Bearer ${apiKey}`,
|
|
383
|
+
"User-Agent": "pi-sub/0.1.0",
|
|
384
|
+
},
|
|
385
|
+
signal: combinedSignal,
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
const body = await response.json();
|
|
389
|
+
|
|
390
|
+
// Z.ai returns HTTP 200 even on auth errors: {"code":401,"msg":"...","success":false}
|
|
391
|
+
const apiError = body as ZaiUsageApiError;
|
|
392
|
+
if (typeof apiError.success === "boolean" && !apiError.success && apiError.msg) {
|
|
393
|
+
throw new Error(`Z.ai API error: ${apiError.msg}`);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const parsed = body as ZaiUsageApiResponse;
|
|
397
|
+
const tokenLimits = (parsed.data?.limits ?? [])
|
|
398
|
+
.filter((l) => l.type === "TOKENS_LIMIT")
|
|
399
|
+
.sort((a, b) => (a.nextResetTime ?? 0) - (b.nextResetTime ?? 0));
|
|
400
|
+
|
|
401
|
+
if (tokenLimits.length === 0) {
|
|
402
|
+
throw new Error("No TOKENS_LIMIT entries in Z.ai usage response");
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// The limit with the nearest reset is the 5-hour rolling window;
|
|
406
|
+
// the next one (if present) is the weekly window.
|
|
407
|
+
const fiveHour = zaiLimitToUsageWindow(tokenLimits[0]);
|
|
408
|
+
const weekly = tokenLimits.length >= 2 ? zaiLimitToUsageWindow(tokenLimits[1]) : undefined;
|
|
409
|
+
|
|
410
|
+
const account: SubscriptionAccountSnapshot = {
|
|
411
|
+
isActive: true,
|
|
412
|
+
accountLabel: "Z.ai subscription",
|
|
413
|
+
fiveHour,
|
|
414
|
+
weekly,
|
|
415
|
+
lastActivity: "Now",
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
return {
|
|
419
|
+
providerId: ZAI_PROVIDER,
|
|
420
|
+
providerDisplayName: "Z.ai",
|
|
421
|
+
accounts: [account],
|
|
422
|
+
activeAccount: account,
|
|
423
|
+
fetchedAt: Date.now(),
|
|
424
|
+
};
|
|
425
|
+
} catch (error) {
|
|
426
|
+
return {
|
|
427
|
+
providerId: ZAI_PROVIDER,
|
|
428
|
+
providerDisplayName: "Z.ai",
|
|
429
|
+
accounts: [],
|
|
430
|
+
fetchedAt: Date.now(),
|
|
431
|
+
error: redactedError(error, "Z.ai"),
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
319
436
|
const codexAdapter: SubscriptionProviderAdapter = {
|
|
320
437
|
id: CODEX_PROVIDER,
|
|
321
438
|
displayName: "Codex",
|
|
@@ -330,7 +447,14 @@ const openCodeGoAdapter: SubscriptionProviderAdapter = {
|
|
|
330
447
|
fetchUsage: fetchOpenCodeGoUsage,
|
|
331
448
|
};
|
|
332
449
|
|
|
333
|
-
const
|
|
450
|
+
const zaiAdapter: SubscriptionProviderAdapter = {
|
|
451
|
+
id: ZAI_PROVIDER,
|
|
452
|
+
displayName: "Z.ai",
|
|
453
|
+
isModelSupported: isZaiModel,
|
|
454
|
+
fetchUsage: fetchZaiUsage,
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
const adapters = [codexAdapter, openCodeGoAdapter, zaiAdapter];
|
|
334
458
|
|
|
335
459
|
function supportedAdapter(model: ModelLike): SubscriptionProviderAdapter | undefined {
|
|
336
460
|
return adapters.find((adapter) => adapter.isModelSupported(model));
|