@bacnh85/pi-sub 0.1.6 → 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 +42 -7
- package/index.ts +135 -26
- 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
|
-
|
|
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
|
|
|
@@ -26,6 +26,8 @@ pi -e ./extensions/pi-sub
|
|
|
26
26
|
|
|
27
27
|
## What it shows
|
|
28
28
|
|
|
29
|
+
### OpenAI Codex
|
|
30
|
+
|
|
29
31
|
The footer status appears after Pi's built-in status/token usage line and includes:
|
|
30
32
|
|
|
31
33
|
- active account email;
|
|
@@ -40,6 +42,22 @@ Example subscription line:
|
|
|
40
42
|
Sub · Plus · user@example.com · 5H 85% (18:12) · W 80% (08:00 on 2 Jul) · gpt-5-codex
|
|
41
43
|
```
|
|
42
44
|
|
|
45
|
+
### OpenCode Go
|
|
46
|
+
|
|
47
|
+
OpenCode Go does not expose usage windows, so the footer shows only the accumulated session cost:
|
|
48
|
+
|
|
49
|
+
```text
|
|
50
|
+
OpenCode Go $0.23
|
|
51
|
+
```
|
|
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
|
+
|
|
43
61
|
When the current model provider is not supported, `pi-sub` clears its subscription line and does not refresh subscription data.
|
|
44
62
|
|
|
45
63
|
## Commands
|
|
@@ -57,6 +75,24 @@ ACCOUNT PLAN 5H USAGE WEEKLY USAGE LAST ACTIVITY
|
|
|
57
75
|
* user@example.com Plus 85% (18:12) 80% (08:00 on 2 Jul) Now
|
|
58
76
|
```
|
|
59
77
|
|
|
78
|
+
For OpenCode Go, `/sub` shows the provider/model and session cost:
|
|
79
|
+
|
|
80
|
+
```text
|
|
81
|
+
Provider: OpenCode Go · Model: kimi-k2.6 · Fetched: 14:23
|
|
82
|
+
OpenCode Go does not expose usage windows.
|
|
83
|
+
Session cost: $0.23
|
|
84
|
+
```
|
|
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
|
+
|
|
60
96
|
## Refresh behavior
|
|
61
97
|
|
|
62
98
|
`pi-sub` refreshes usage data:
|
|
@@ -73,12 +109,11 @@ Refreshes are cached briefly to avoid excessive usage endpoint calls.
|
|
|
73
109
|
|
|
74
110
|
## Requirements and troubleshooting
|
|
75
111
|
|
|
76
|
-
- Pi auth must contain an `openai-codex` OAuth entry in `~/.pi/agent/auth.json` or `$PI_CODING_AGENT_DIR/auth.json`.
|
|
77
|
-
- The entry must
|
|
78
|
-
- `
|
|
79
|
-
|
|
80
|
-
If usage is unavailable, verify that Pi can use the `openai-codex` provider and that the auth file exists.
|
|
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.
|
|
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`).
|
|
115
|
+
- `pi-sub` redacts auth/token-related errors and never prints credentials.
|
|
81
116
|
|
|
82
117
|
## Design notes
|
|
83
118
|
|
|
84
|
-
The extension is named `pi-sub` rather than `pi-codex-usage` so future subscription providers can be added as separate adapters.
|
|
119
|
+
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) and OpenCode Go (session cost only).
|
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");
|
|
@@ -230,25 +236,6 @@ function parseUsageResponse(body: unknown): UsageApiSnapshot | undefined {
|
|
|
230
236
|
};
|
|
231
237
|
}
|
|
232
238
|
|
|
233
|
-
function parseOpcodeUsageResponse(body: unknown): UsageApiSnapshot | undefined {
|
|
234
|
-
if (!body || typeof body !== "object") return undefined;
|
|
235
|
-
const root = body as any;
|
|
236
|
-
const usage = root.usage ?? root;
|
|
237
|
-
const parseWindow = (window: any): UsageApiWindow | undefined => {
|
|
238
|
-
if (!window || typeof window !== "object" || typeof window.used_percent !== "number") return undefined;
|
|
239
|
-
return {
|
|
240
|
-
used_percent: window.used_percent,
|
|
241
|
-
limit_window_seconds: typeof window.limit_window_seconds === "number" ? window.limit_window_seconds : undefined,
|
|
242
|
-
reset_at: typeof window.reset_at === "number" ? window.reset_at : typeof window.reset_ts === "number" ? window.reset_ts : undefined,
|
|
243
|
-
};
|
|
244
|
-
};
|
|
245
|
-
return {
|
|
246
|
-
primary: parseWindow(usage.rolling ?? usage.primary_window),
|
|
247
|
-
secondary: parseWindow(usage.weekly ?? usage.secondary_window),
|
|
248
|
-
plan_type: typeof root.plan_type === "string" ? root.plan_type : typeof usage.plan_type === "string" ? usage.plan_type : undefined,
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
|
|
252
239
|
async function readPiCodexAuth(): Promise<PiAuthEntry & { accountId: string }> {
|
|
253
240
|
const auth = await readJsonFile<PiAuthFile>(piAuthPath());
|
|
254
241
|
const entry = auth[CODEX_PROVIDER];
|
|
@@ -257,11 +244,19 @@ async function readPiCodexAuth(): Promise<PiAuthEntry & { accountId: string }> {
|
|
|
257
244
|
return { ...entry, accountId };
|
|
258
245
|
}
|
|
259
246
|
|
|
260
|
-
async function readOpenCodeGoAuth(): Promise<{ key
|
|
247
|
+
async function readOpenCodeGoAuth(): Promise<{ key?: string; accountId?: string }> {
|
|
261
248
|
const auth = await readJsonFile<PiAuthFile>(piAuthPath());
|
|
262
249
|
const entry = auth[OPC_PROVIDER];
|
|
263
|
-
if (!entry?.key
|
|
264
|
-
return { key: entry.key };
|
|
250
|
+
if (!entry?.key && !entry?.accountId) throw new Error("Missing opencode-go API key or accountId in Pi auth");
|
|
251
|
+
if (typeof entry.key === "string") return { key: entry.key };
|
|
252
|
+
return { accountId: typeof entry.accountId === "string" ? entry.accountId : undefined };
|
|
253
|
+
}
|
|
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;
|
|
265
260
|
}
|
|
266
261
|
|
|
267
262
|
async function fetchUsageFromPiAuth(entry: PiAuthEntry, signal?: AbortSignal): Promise<UsageApiSnapshot | undefined> {
|
|
@@ -285,6 +280,7 @@ function redactedError(error: unknown, provider = "Codex"): string {
|
|
|
285
280
|
if (/ENOENT|no such file/i.test(message)) return "Pi auth not found";
|
|
286
281
|
if (/missing openai-codex/i.test(message)) return "openai-codex auth not found";
|
|
287
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";
|
|
288
284
|
if (/timed out|timeout|aborted/i.test(message)) return `${provider} usage refresh timed out`;
|
|
289
285
|
if (/401|403|auth|token|unauthorized|forbidden/i.test(message)) return `${provider} auth unavailable`;
|
|
290
286
|
return `${provider} usage unavailable`;
|
|
@@ -314,7 +310,7 @@ async function fetchCodexUsage(signal?: AbortSignal): Promise<SubscriptionUsageS
|
|
|
314
310
|
}
|
|
315
311
|
}
|
|
316
312
|
|
|
317
|
-
async function fetchOpenCodeGoUsage(
|
|
313
|
+
async function fetchOpenCodeGoUsage(_signal?: AbortSignal): Promise<SubscriptionUsageSnapshot> {
|
|
318
314
|
try {
|
|
319
315
|
await readOpenCodeGoAuth();
|
|
320
316
|
return {
|
|
@@ -322,7 +318,6 @@ async function fetchOpenCodeGoUsage(signal?: AbortSignal): Promise<SubscriptionU
|
|
|
322
318
|
providerDisplayName: "OpenCode Go",
|
|
323
319
|
accounts: [],
|
|
324
320
|
fetchedAt: Date.now(),
|
|
325
|
-
error: "OpenCode Go usage tracking not yet implemented",
|
|
326
321
|
};
|
|
327
322
|
} catch (error) {
|
|
328
323
|
return {
|
|
@@ -335,6 +330,109 @@ async function fetchOpenCodeGoUsage(signal?: AbortSignal): Promise<SubscriptionU
|
|
|
335
330
|
}
|
|
336
331
|
}
|
|
337
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
|
+
|
|
338
436
|
const codexAdapter: SubscriptionProviderAdapter = {
|
|
339
437
|
id: CODEX_PROVIDER,
|
|
340
438
|
displayName: "Codex",
|
|
@@ -349,7 +447,14 @@ const openCodeGoAdapter: SubscriptionProviderAdapter = {
|
|
|
349
447
|
fetchUsage: fetchOpenCodeGoUsage,
|
|
350
448
|
};
|
|
351
449
|
|
|
352
|
-
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];
|
|
353
458
|
|
|
354
459
|
function supportedAdapter(model: ModelLike): SubscriptionProviderAdapter | undefined {
|
|
355
460
|
return adapters.find((adapter) => adapter.isModelSupported(model));
|
|
@@ -511,7 +616,11 @@ function buildDetails(snapshot: SubscriptionUsageSnapshot | undefined, state: St
|
|
|
511
616
|
if (!state.adapter) return `Subscription tracking inactive for current model provider (${state.model?.provider ?? "unknown"}).`;
|
|
512
617
|
if (!snapshot) return "Subscription usage has not been loaded yet.";
|
|
513
618
|
if (snapshot.error) return `${snapshot.providerDisplayName}: ${snapshot.error}`;
|
|
514
|
-
if (snapshot.accounts.length === 0)
|
|
619
|
+
if (snapshot.accounts.length === 0) {
|
|
620
|
+
const costLine = snapshot.cost !== undefined && snapshot.cost > 0 ? `\nSession cost: $${snapshot.cost.toFixed(2)}` : "";
|
|
621
|
+
const modelInfo = state.model?.id ? ` · Model: ${state.model.id}` : "";
|
|
622
|
+
return `Provider: ${snapshot.providerDisplayName}${modelInfo} · Fetched: ${new Date(snapshot.fetchedAt).toLocaleTimeString()}\n${snapshot.providerDisplayName} does not expose usage windows.${costLine}`;
|
|
623
|
+
}
|
|
515
624
|
|
|
516
625
|
const columns: { key: string; label: string; get: (a: SubscriptionAccountSnapshot) => string }[] = [
|
|
517
626
|
{ key: "account", label: "ACCOUNT", get: (a) => a.accountLabel ?? "unknown" },
|