@bacnh85/pi-sub 0.1.30 → 0.1.32
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 +26 -0
- package/extensions/index.ts +129 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,29 @@
|
|
|
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. Needs `ROUTER_MGMT_TOKEN` (an `oma_` CLI token or manage-scope key) in
|
|
8
|
+
env / `~/.pi/agent/.env.local`; without it, the footer degrades to the clean
|
|
9
|
+
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
|
+
|
|
15
|
+
## 0.1.31 (2026-08-22)
|
|
16
|
+
|
|
17
|
+
- **Fixed glm-cn quota via OmniRoute**: OmniRoute's connection slug for the
|
|
18
|
+
Z.ai (CN) upstream is `glm-cn`, not `zai-coding-cn` (a Pi provider id).
|
|
19
|
+
Wrong slug returned "No cached usage data", so the plain-retry fallback
|
|
20
|
+
showed the aggregate snapshot (e.g. R:100% W:0%) instead of the Z.ai windows.
|
|
21
|
+
Alias `glmcn` now maps to the correct slug; footer shows `R:99%/3H`-style
|
|
22
|
+
windows matching the direct Z.ai display.
|
|
23
|
+
- OmniRoute "reset in 2h 55m" countdowns are now converted into compact
|
|
24
|
+
footer labels (`/3H`), and "Unavailable" windows are skipped (previously
|
|
25
|
+
`0%`-style aggregates leaked in via the fallback).
|
|
26
|
+
|
|
1
27
|
## 0.1.30 (2026-08-22)
|
|
2
28
|
|
|
3
29
|
- Router usage is now **provider-scoped**: the active router model's upstream
|
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,10 +362,26 @@ function routerOrigin(baseUrl: string): string {
|
|
|
340
362
|
return baseUrl.replace(/\/v1\/?$/, "");
|
|
341
363
|
}
|
|
342
364
|
|
|
365
|
+
/** OmniRoute management token (oma_ CLI token or manage-scope key) from env —
|
|
366
|
+
* unlocks /api/usage/<connectionId> which carries the raw USD balance for
|
|
367
|
+
* credit-based upstreams (deepseek) that the key-authable endpoints strip. */
|
|
368
|
+
function readRouterMgmtToken(): string | undefined {
|
|
369
|
+
return process.env.ROUTER_MGMT_TOKEN || process.env.OMNIROUTE_MGMT_TOKEN || undefined;
|
|
370
|
+
}
|
|
371
|
+
|
|
343
372
|
/** Parse OmniRoute's `/api/usage/om-usage` plain-text report into windows.
|
|
344
373
|
* Sections: "Personal quota" (per-key USD budgets: Daily/Weekly) and
|
|
345
374
|
* "Provider quota" (connection session/weekly). Lines: `<Label>`,
|
|
346
375
|
* `NN% left`, `⏱ reset in <countdown>`. Robust to missing/unknown blocks. */
|
|
376
|
+
/** Convert OmniRoute's "reset in 2h 55m" countdown into the compact footer
|
|
377
|
+
* label (e.g. 3H), matching the direct Z.ai display (R:99%/4H). */
|
|
378
|
+
function countdownToLabel(text: string): string | undefined {
|
|
379
|
+
const m = text.match(/(?:(\d+)\s*d)?\s*(?:(\d+)\s*h)?\s*(?:(\d+)\s*m)?/);
|
|
380
|
+
if (!m || (!m[1] && !m[2] && !m[3])) return undefined;
|
|
381
|
+
const secs = Number(m[1] || 0) * 86400 + Number(m[2] || 0) * 3600 + Number(m[3] || 0) * 60;
|
|
382
|
+
return secs ? formatRemainingTime(Date.now() / 1000 + secs) : undefined;
|
|
383
|
+
}
|
|
384
|
+
|
|
347
385
|
export function parseOmniUsageText(text: string): {
|
|
348
386
|
personalDaily?: UsageWindow;
|
|
349
387
|
personalWeekly?: UsageWindow;
|
|
@@ -369,7 +407,10 @@ export function parseOmniUsageText(text: string): {
|
|
|
369
407
|
const remaining = Number(usedMatch[1]);
|
|
370
408
|
if (remaining < 0 || remaining > 100) continue;
|
|
371
409
|
const window: UsageWindow = { remaining };
|
|
372
|
-
|
|
410
|
+
if (resetMatch) {
|
|
411
|
+
window.resetLabel = `⏱ ${resetMatch[1].trim()}`;
|
|
412
|
+
window.remainingLabel = countdownToLabel(resetMatch[1]);
|
|
413
|
+
}
|
|
373
414
|
if (inPersonal) {
|
|
374
415
|
if (label.includes("daily")) out.personalDaily = window;
|
|
375
416
|
else if (label.includes("weekly")) out.personalWeekly = window;
|
|
@@ -397,6 +438,10 @@ if (process.env.PI_SUB_SELF_CHECK === "1") {
|
|
|
397
438
|
assert(rp("command-code/deepseek/deepseek-v4-flash") === "command-code", "prefix command-code");
|
|
398
439
|
assert(rp("cmd/deepseek/deepseek-v4-flash") === "command-code", "alias cmd → command-code");
|
|
399
440
|
assert(rp("oc/gpt-5") === "opencode-go", "alias oc → opencode-go");
|
|
441
|
+
// OmniRoute's connection slug is `glm-cn` (live-verified; zai-coding-cn is a
|
|
442
|
+
// Pi provider id, NOT an OmniRoute slug — wrong slug = no cached data).
|
|
443
|
+
assert(rp("glm-cn/glm-5.2") === "glm-cn", "glm-cn passes through");
|
|
444
|
+
assert(rp("glmcn/glm-5.2") === "glm-cn", "alias glmcn → glm-cn");
|
|
400
445
|
assert(rp("zai-coding/glm-5.2") === "zai-coding", "prefix zai-coding");
|
|
401
446
|
assert(rp("auto/best") === undefined, "generic auto filtered");
|
|
402
447
|
assert(rp("openrouter/gpt-5") === undefined, "generic openrouter filtered");
|
|
@@ -404,7 +449,7 @@ if (process.env.PI_SUB_SELF_CHECK === "1") {
|
|
|
404
449
|
assert(p.personalWeekly?.remaining === 90, "personal weekly 90");
|
|
405
450
|
assert(p.session?.remaining === 47, "session 47");
|
|
406
451
|
assert(p.providerWeekly?.remaining === 28, "provider weekly 28");
|
|
407
|
-
assert(p.personalDaily?.resetLabel?.includes("15h"), "daily reset label");
|
|
452
|
+
assert(p.personalDaily?.resetLabel?.includes("15h") === true, "daily reset label");
|
|
408
453
|
const disabled = parseOmniUsageText("Usage command is disabled for this API key.");
|
|
409
454
|
assert(Object.keys(disabled).length === 0, "disabled text parses empty");
|
|
410
455
|
// Live-verified: provider without cached data → no windows (endpoint fallback).
|
|
@@ -416,7 +461,15 @@ if (process.env.PI_SUB_SELF_CHECK === "1") {
|
|
|
416
461
|
);
|
|
417
462
|
assert(live.session?.remaining === 90, "session 90");
|
|
418
463
|
assert(live.providerWeekly?.remaining === 0, "weekly 0");
|
|
419
|
-
assert(live.session?.resetLabel?.includes("1h 59m"), "session reset");
|
|
464
|
+
assert(live.session?.resetLabel?.includes("1h 59m") === true, "session reset");
|
|
465
|
+
// Live-verified 2026-08-22: glm-cn scoped quota — Session 99%, Weekly
|
|
466
|
+
// "Unavailable" (skipped, so W stays absent like the direct Z.ai footer).
|
|
467
|
+
const glmCn = parseOmniUsageText(
|
|
468
|
+
"Provider quota\nSession\n99% left\n⏱ reset in 2h 55m\n\nWeekly\nUnavailable\n⏱ reset in unknown"
|
|
469
|
+
);
|
|
470
|
+
assert(glmCn.session?.remaining === 99, "glm-cn session 99");
|
|
471
|
+
assert(glmCn.session?.remainingLabel === "3H", "glm-cn reset label 3H");
|
|
472
|
+
assert(glmCn.providerWeekly === undefined, "glm-cn weekly unavailable skipped");
|
|
420
473
|
}
|
|
421
474
|
|
|
422
475
|
async function fetchUsageFromPiAuth(entry: PiAuthEntry, signal?: AbortSignal): Promise<UsageApiSnapshot | undefined> {
|
|
@@ -529,9 +582,11 @@ async function fetchRouterUsage(signal?: AbortSignal, provider?: string): Promis
|
|
|
529
582
|
let text = await response.text();
|
|
530
583
|
if (text && !text.includes("disabled")) {
|
|
531
584
|
let w = parseOmniUsageText(text);
|
|
532
|
-
//
|
|
533
|
-
//
|
|
534
|
-
|
|
585
|
+
// "No cached usage data" = unknown/wrong slug — retry without
|
|
586
|
+
// ?provider= for the best/all snapshot. "Unavailable" windows mean a
|
|
587
|
+
// known credit-based upstream (deepseek) — keep them empty so the
|
|
588
|
+
// USD-balance path below takes over instead of showing the aggregate.
|
|
589
|
+
if (provider && !w.session && !w.providerWeekly && text.includes("No cached usage data")) {
|
|
535
590
|
const plain = await fetch(url.replace(/\?provider=.*$/, ""), {
|
|
536
591
|
headers: { Accept: "text/plain", Authorization: `Bearer ${apiKey}` },
|
|
537
592
|
signal: combined,
|
|
@@ -544,6 +599,23 @@ async function fetchRouterUsage(signal?: AbortSignal, provider?: string): Promis
|
|
|
544
599
|
}
|
|
545
600
|
}
|
|
546
601
|
}
|
|
602
|
+
// Credit-based upstreams (deepseek): the usage text prints "Unavailable"
|
|
603
|
+
// windows — pull the real USD balance from the management API instead.
|
|
604
|
+
const credits = await fetchRouterCredits(provider, w);
|
|
605
|
+
if (credits) {
|
|
606
|
+
const account: SubscriptionAccountSnapshot = {
|
|
607
|
+
...baseAccount,
|
|
608
|
+
plan: `Router · ${provider}`,
|
|
609
|
+
monthlyCredits: credits.balanceUsd,
|
|
610
|
+
usageBreakdown: credits.breakdown,
|
|
611
|
+
};
|
|
612
|
+
return {
|
|
613
|
+
providerDisplayName: "Router",
|
|
614
|
+
accounts: [account],
|
|
615
|
+
activeAccount: account,
|
|
616
|
+
fetchedAt: Date.now(),
|
|
617
|
+
};
|
|
618
|
+
}
|
|
547
619
|
// personalDaily = per-key budget (nearest reset → R slot),
|
|
548
620
|
// provider weekly/session = upstream quota (W slot). Fall back sensibly.
|
|
549
621
|
const account: SubscriptionAccountSnapshot = {
|
|
@@ -588,6 +660,56 @@ async function fetchRouterUsage(signal?: AbortSignal, provider?: string): Promis
|
|
|
588
660
|
};
|
|
589
661
|
}
|
|
590
662
|
|
|
663
|
+
interface RouterCredits {
|
|
664
|
+
balanceUsd: number;
|
|
665
|
+
breakdown: string;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/** Fetch the raw USD balance for credit-based upstreams (deepseek: `credits_usd`)
|
|
669
|
+
* via OmniRoute's management usage API. Only called when the key-authable
|
|
670
|
+
* om-usage text reports no usable windows — the API-key surface normalizes
|
|
671
|
+
* credits to meaningless percentages, so this needs ROUTER_MGMT_TOKEN.
|
|
672
|
+
* Uses connection discovery from /api/v1/me/status (key-authable) +
|
|
673
|
+
* /api/usage/<id> (management token). */
|
|
674
|
+
async function fetchRouterCredits(provider: string | undefined, w: ReturnType<typeof parseOmniUsageText>): Promise<RouterCredits | undefined> {
|
|
675
|
+
if (!provider || w.session || w.providerWeekly) return undefined;
|
|
676
|
+
const cfg = readRouterConfig();
|
|
677
|
+
const apiKey = readRouterApiKey();
|
|
678
|
+
const mgmtToken = readRouterMgmtToken();
|
|
679
|
+
if (!cfg || !apiKey || !mgmtToken) return undefined;
|
|
680
|
+
const origin = routerOrigin(cfg.baseUrl);
|
|
681
|
+
try {
|
|
682
|
+
const combined = AbortSignal.timeout(7_000);
|
|
683
|
+
// 1. Connection id for this upstream via the key-authable status endpoint.
|
|
684
|
+
const statusRes = await fetch(`${origin}/api/v1/me/status`, {
|
|
685
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
686
|
+
signal: combined,
|
|
687
|
+
});
|
|
688
|
+
if (!statusRes.ok) return undefined;
|
|
689
|
+
const status = (await statusRes.json()) as { accountQuotas?: Array<{ provider?: string; connectionId?: string }> };
|
|
690
|
+
const connectionId = status.accountQuotas?.find((q) => q.provider === provider)?.connectionId;
|
|
691
|
+
if (!connectionId) return undefined;
|
|
692
|
+
// 2. Raw usage (management token) — quotas.credits_usd.remaining is the USD balance.
|
|
693
|
+
const usageRes = await fetch(`${origin}/api/usage/${connectionId}`, {
|
|
694
|
+
headers: { Authorization: `Bearer ${mgmtToken}` },
|
|
695
|
+
signal: combined,
|
|
696
|
+
});
|
|
697
|
+
if (!usageRes.ok) return undefined;
|
|
698
|
+
const usage = (await usageRes.json()) as { quotas?: Record<string, { remaining?: number }> };
|
|
699
|
+
const credits = usage.quotas?.credits_usd ?? usage.quotas?.credits;
|
|
700
|
+
const remaining = credits?.remaining;
|
|
701
|
+
if (typeof remaining !== "number" || !Number.isFinite(remaining)) return undefined;
|
|
702
|
+
const cny = usage.quotas?.credits_cny?.remaining;
|
|
703
|
+
return {
|
|
704
|
+
balanceUsd: remaining,
|
|
705
|
+
breakdown: `🪙 Balance (USD) $${remaining.toFixed(2)}` +
|
|
706
|
+
(typeof cny === "number" ? ` · ¥${cny.toFixed(2)} CNY` : ""),
|
|
707
|
+
};
|
|
708
|
+
} catch {
|
|
709
|
+
return undefined; // no mgmt token / upstream down — fall back to endpoint display
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
591
713
|
// Command Code's /alpha/billing/credits endpoint (auth: same Provider API key
|
|
592
714
|
// as /provider/v1 models) returns live 5-hour and weekly rolling windows plus
|
|
593
715
|
// the monthly credit balance — the same data as the cmd /usage CLI.
|
|
@@ -913,7 +1035,7 @@ function routerUpstreamPrefix(model: ModelLike): string | undefined {
|
|
|
913
1035
|
if (first === "cmd") return "command-code";
|
|
914
1036
|
if (first === "oc") return "opencode-go";
|
|
915
1037
|
if (first === "ds") return "deepseek";
|
|
916
|
-
if (first === "glmcn"
|
|
1038
|
+
if (first === "glmcn") return "glm-cn"; // OmniRoute connection slug (not the Pi provider id zai-coding-cn)
|
|
917
1039
|
// Generic router aliases / upstreams without cached quota data — no provider
|
|
918
1040
|
// selection; the usage API returns the best snapshot instead.
|
|
919
1041
|
const generic = new Set(["auto", "aug", "no-think", "tllm", "combo", "openrouter", "nvidia", "felo", "pepper", "mcode", "ddgw", "veoaifree-web", "veo-free"]);
|