@bacnh85/pi-sub 0.1.31 → 0.1.33
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/CHANGELOG.md +20 -0
- package/extensions/index.ts +111 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## 0.1.32 (2026-08-22)
|
|
2
|
+
|
|
3
|
+
- **DeepSeek via OmniRoute now shows the real USD balance** (e.g. `M:$18.25`)
|
|
4
|
+
instead of the misleading aggregate windows (R:100%/5H W:0%/2D). Credit-based
|
|
5
|
+
upstreams report "Unavailable" session/weekly in the usage text; pi-sub now
|
|
6
|
+
recognizes that shape and fetches `credits_usd` from the management usage
|
|
7
|
+
API — grant the router key the `manage` scope in the OmniRoute dashboard
|
|
8
|
+
(done), or set `ROUTER_MGMT_TOKEN`. Without it, the footer degrades to the
|
|
9
|
+
clean endpoint display (no aggregate leak).
|
|
10
|
+
- The plain-retry fallback (drop `?provider=`) now fires only on "No cached
|
|
11
|
+
usage data" (wrong/unknown slug), not on "Unavailable" windows.
|
|
12
|
+
- pi-sub now reads `.env.local`/`.env` (cwd then `~/.pi/agent`) for its own
|
|
13
|
+
env vars — same discovery chain as pi-munin, stdlib parser, no deps.
|
|
14
|
+
|
|
1
15
|
## 0.1.31 (2026-08-22)
|
|
2
16
|
|
|
3
17
|
- **Fixed glm-cn quota via OmniRoute**: OmniRoute's connection slug for the
|
|
@@ -52,6 +66,12 @@
|
|
|
52
66
|
|
|
53
67
|
# Changelog
|
|
54
68
|
|
|
69
|
+
## 0.1.33 (2026-08-29)
|
|
70
|
+
|
|
71
|
+
### Added
|
|
72
|
+
|
|
73
|
+
- `/sub` argument completion offers `refresh`.
|
|
74
|
+
|
|
55
75
|
## 0.1.29 (2026-08-15)
|
|
56
76
|
|
|
57
77
|
### Features
|
package/extensions/index.ts
CHANGED
|
@@ -4,6 +4,28 @@ import fs from "node:fs";
|
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
|
|
7
|
+
/** Pi config dirs + .env.local/.env discovery (pi-munin convention, stdlib parse). */
|
|
8
|
+
function loadEnvFiles(): void {
|
|
9
|
+
const dirs = process.env.PI_CODING_AGENT_DIR
|
|
10
|
+
? [process.env.PI_CODING_AGENT_DIR]
|
|
11
|
+
: [path.join(os.homedir(), ".pi", "agent"), path.join(os.homedir(), ".pi", "agents")];
|
|
12
|
+
const candidates = [path.resolve(process.cwd(), ".env.local"), path.resolve(process.cwd(), ".env")]
|
|
13
|
+
.concat(dirs.flatMap((d) => [path.join(d, ".env.local"), path.join(d, ".env")]));
|
|
14
|
+
for (const file of candidates) {
|
|
15
|
+
try {
|
|
16
|
+
const text = fs.readFileSync(file, "utf8");
|
|
17
|
+
for (const line of text.split(/\r?\n/)) {
|
|
18
|
+
const m = line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)\s*$/);
|
|
19
|
+
if (!m) continue;
|
|
20
|
+
let v = m[2].trim();
|
|
21
|
+
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) v = v.slice(1, -1);
|
|
22
|
+
if (process.env[m[1]] === undefined) process.env[m[1]] = v;
|
|
23
|
+
}
|
|
24
|
+
} catch { /* optional file */ }
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
loadEnvFiles();
|
|
28
|
+
|
|
7
29
|
const STATUS_KEY = "pi-sub";
|
|
8
30
|
const MESSAGE_TYPE = "pi-sub-status";
|
|
9
31
|
const USAGE_ENDPOINT = "https://chatgpt.com/backend-api/wham/usage";
|
|
@@ -340,6 +362,15 @@ function routerOrigin(baseUrl: string): string {
|
|
|
340
362
|
return baseUrl.replace(/\/v1\/?$/, "");
|
|
341
363
|
}
|
|
342
364
|
|
|
365
|
+
/** OmniRoute management credential (manage-scope key or oma_ CLI token) from
|
|
366
|
+
* env — optional override. The router key itself works when it holds the
|
|
367
|
+
* `manage` scope (API Keys dashboard), which unlocks
|
|
368
|
+
* /api/usage/<connectionId> carrying the raw USD balance for credit-based
|
|
369
|
+
* upstreams (deepseek) that the key-authable endpoints normalize away. */
|
|
370
|
+
function readRouterMgmtToken(apiKey: string | undefined): string | undefined {
|
|
371
|
+
return process.env.ROUTER_MGMT_TOKEN || process.env.OMNIROUTE_MGMT_TOKEN || apiKey;
|
|
372
|
+
}
|
|
373
|
+
|
|
343
374
|
/** Parse OmniRoute's `/api/usage/om-usage` plain-text report into windows.
|
|
344
375
|
* Sections: "Personal quota" (per-key USD budgets: Daily/Weekly) and
|
|
345
376
|
* "Provider quota" (connection session/weekly). Lines: `<Label>`,
|
|
@@ -420,7 +451,7 @@ if (process.env.PI_SUB_SELF_CHECK === "1") {
|
|
|
420
451
|
assert(p.personalWeekly?.remaining === 90, "personal weekly 90");
|
|
421
452
|
assert(p.session?.remaining === 47, "session 47");
|
|
422
453
|
assert(p.providerWeekly?.remaining === 28, "provider weekly 28");
|
|
423
|
-
assert(p.personalDaily?.resetLabel?.includes("15h"), "daily reset label");
|
|
454
|
+
assert(p.personalDaily?.resetLabel?.includes("15h") === true, "daily reset label");
|
|
424
455
|
const disabled = parseOmniUsageText("Usage command is disabled for this API key.");
|
|
425
456
|
assert(Object.keys(disabled).length === 0, "disabled text parses empty");
|
|
426
457
|
// Live-verified: provider without cached data → no windows (endpoint fallback).
|
|
@@ -432,7 +463,7 @@ if (process.env.PI_SUB_SELF_CHECK === "1") {
|
|
|
432
463
|
);
|
|
433
464
|
assert(live.session?.remaining === 90, "session 90");
|
|
434
465
|
assert(live.providerWeekly?.remaining === 0, "weekly 0");
|
|
435
|
-
assert(live.session?.resetLabel?.includes("1h 59m"), "session reset");
|
|
466
|
+
assert(live.session?.resetLabel?.includes("1h 59m") === true, "session reset");
|
|
436
467
|
// Live-verified 2026-08-22: glm-cn scoped quota — Session 99%, Weekly
|
|
437
468
|
// "Unavailable" (skipped, so W stays absent like the direct Z.ai footer).
|
|
438
469
|
const glmCn = parseOmniUsageText(
|
|
@@ -553,9 +584,11 @@ async function fetchRouterUsage(signal?: AbortSignal, provider?: string): Promis
|
|
|
553
584
|
let text = await response.text();
|
|
554
585
|
if (text && !text.includes("disabled")) {
|
|
555
586
|
let w = parseOmniUsageText(text);
|
|
556
|
-
//
|
|
557
|
-
//
|
|
558
|
-
|
|
587
|
+
// "No cached usage data" = unknown/wrong slug — retry without
|
|
588
|
+
// ?provider= for the best/all snapshot. "Unavailable" windows mean a
|
|
589
|
+
// known credit-based upstream (deepseek) — keep them empty so the
|
|
590
|
+
// USD-balance path below takes over instead of showing the aggregate.
|
|
591
|
+
if (provider && !w.session && !w.providerWeekly && text.includes("No cached usage data")) {
|
|
559
592
|
const plain = await fetch(url.replace(/\?provider=.*$/, ""), {
|
|
560
593
|
headers: { Accept: "text/plain", Authorization: `Bearer ${apiKey}` },
|
|
561
594
|
signal: combined,
|
|
@@ -568,6 +601,23 @@ async function fetchRouterUsage(signal?: AbortSignal, provider?: string): Promis
|
|
|
568
601
|
}
|
|
569
602
|
}
|
|
570
603
|
}
|
|
604
|
+
// Credit-based upstreams (deepseek): the usage text prints "Unavailable"
|
|
605
|
+
// windows — pull the real USD balance from the management API instead.
|
|
606
|
+
const credits = await fetchRouterCredits(provider, w);
|
|
607
|
+
if (credits) {
|
|
608
|
+
const account: SubscriptionAccountSnapshot = {
|
|
609
|
+
...baseAccount,
|
|
610
|
+
plan: `Router · ${provider}`,
|
|
611
|
+
monthlyCredits: credits.balanceUsd,
|
|
612
|
+
usageBreakdown: credits.breakdown,
|
|
613
|
+
};
|
|
614
|
+
return {
|
|
615
|
+
providerDisplayName: "Router",
|
|
616
|
+
accounts: [account],
|
|
617
|
+
activeAccount: account,
|
|
618
|
+
fetchedAt: Date.now(),
|
|
619
|
+
};
|
|
620
|
+
}
|
|
571
621
|
// personalDaily = per-key budget (nearest reset → R slot),
|
|
572
622
|
// provider weekly/session = upstream quota (W slot). Fall back sensibly.
|
|
573
623
|
const account: SubscriptionAccountSnapshot = {
|
|
@@ -612,6 +662,56 @@ async function fetchRouterUsage(signal?: AbortSignal, provider?: string): Promis
|
|
|
612
662
|
};
|
|
613
663
|
}
|
|
614
664
|
|
|
665
|
+
interface RouterCredits {
|
|
666
|
+
balanceUsd: number;
|
|
667
|
+
breakdown: string;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/** Fetch the raw USD balance for credit-based upstreams (deepseek: `credits_usd`)
|
|
671
|
+
* via OmniRoute's management usage API. Only called when the key-authable
|
|
672
|
+
* om-usage text reports no usable windows — that surface normalizes credits
|
|
673
|
+
* to meaningless percentages. Needs the router key to hold the `manage`
|
|
674
|
+
* scope (or ROUTER_MGMT_TOKEN as override). Connection discovery comes from
|
|
675
|
+
* /api/v1/me/status (key-authable); the balance from /api/usage/<id>. */
|
|
676
|
+
async function fetchRouterCredits(provider: string | undefined, w: ReturnType<typeof parseOmniUsageText>): Promise<RouterCredits | undefined> {
|
|
677
|
+
if (!provider || w.session || w.providerWeekly) return undefined;
|
|
678
|
+
const cfg = readRouterConfig();
|
|
679
|
+
const apiKey = readRouterApiKey();
|
|
680
|
+
const mgmtToken = readRouterMgmtToken(apiKey);
|
|
681
|
+
if (!cfg || !apiKey || !mgmtToken) return undefined;
|
|
682
|
+
const origin = routerOrigin(cfg.baseUrl);
|
|
683
|
+
try {
|
|
684
|
+
const combined = AbortSignal.timeout(7_000);
|
|
685
|
+
// 1. Connection id for this upstream via the key-authable status endpoint.
|
|
686
|
+
const statusRes = await fetch(`${origin}/api/v1/me/status`, {
|
|
687
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
688
|
+
signal: combined,
|
|
689
|
+
});
|
|
690
|
+
if (!statusRes.ok) return undefined;
|
|
691
|
+
const status = (await statusRes.json()) as { accountQuotas?: Array<{ provider?: string; connectionId?: string }> };
|
|
692
|
+
const connectionId = status.accountQuotas?.find((q) => q.provider === provider)?.connectionId;
|
|
693
|
+
if (!connectionId) return undefined;
|
|
694
|
+
// 2. Raw usage (management token) — quotas.credits_usd.remaining is the USD balance.
|
|
695
|
+
const usageRes = await fetch(`${origin}/api/usage/${connectionId}`, {
|
|
696
|
+
headers: { Authorization: `Bearer ${mgmtToken}` },
|
|
697
|
+
signal: combined,
|
|
698
|
+
});
|
|
699
|
+
if (!usageRes.ok) return undefined;
|
|
700
|
+
const usage = (await usageRes.json()) as { quotas?: Record<string, { remaining?: number }> };
|
|
701
|
+
const credits = usage.quotas?.credits_usd ?? usage.quotas?.credits;
|
|
702
|
+
const remaining = credits?.remaining;
|
|
703
|
+
if (typeof remaining !== "number" || !Number.isFinite(remaining)) return undefined;
|
|
704
|
+
const cny = usage.quotas?.credits_cny?.remaining;
|
|
705
|
+
return {
|
|
706
|
+
balanceUsd: remaining,
|
|
707
|
+
breakdown: `🪙 Balance (USD) $${remaining.toFixed(2)}` +
|
|
708
|
+
(typeof cny === "number" ? ` · ¥${cny.toFixed(2)} CNY` : ""),
|
|
709
|
+
};
|
|
710
|
+
} catch {
|
|
711
|
+
return undefined; // no mgmt token / upstream down — fall back to endpoint display
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
615
715
|
// Command Code's /alpha/billing/credits endpoint (auth: same Provider API key
|
|
616
716
|
// as /provider/v1 models) returns live 5-hour and weekly rolling windows plus
|
|
617
717
|
// the monthly credit balance — the same data as the cmd /usage CLI.
|
|
@@ -1195,6 +1295,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
1195
1295
|
|
|
1196
1296
|
pi.registerCommand("sub", {
|
|
1197
1297
|
description: "Show subscription usage for the current supported model provider (use /sub refresh to force refresh).",
|
|
1298
|
+
getArgumentCompletions: (prefix) => {
|
|
1299
|
+
const items = ["refresh"]
|
|
1300
|
+
.filter((k) => k.startsWith(String(prefix || "").trim().toLowerCase()))
|
|
1301
|
+
.map((k) => ({ value: k, label: k, description: "force refresh" }));
|
|
1302
|
+
return items.length > 0 ? items : null;
|
|
1303
|
+
},
|
|
1198
1304
|
handler: async (args, ctx) => {
|
|
1199
1305
|
updateActiveAdapter(ctx, state, ctx.model);
|
|
1200
1306
|
const command = args.trim().toLowerCase();
|