@bacnh85/pi-sub 0.1.29 → 0.1.31

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 CHANGED
@@ -1,3 +1,55 @@
1
+ ## 0.1.31 (2026-08-22)
2
+
3
+ - **Fixed glm-cn quota via OmniRoute**: OmniRoute's connection slug for the
4
+ Z.ai (CN) upstream is `glm-cn`, not `zai-coding-cn` (a Pi provider id).
5
+ Wrong slug returned "No cached usage data", so the plain-retry fallback
6
+ showed the aggregate snapshot (e.g. R:100% W:0%) instead of the Z.ai windows.
7
+ Alias `glmcn` now maps to the correct slug; footer shows `R:99%/3H`-style
8
+ windows matching the direct Z.ai display.
9
+ - OmniRoute "reset in 2h 55m" countdowns are now converted into compact
10
+ footer labels (`/3H`), and "Unavailable" windows are skipped (previously
11
+ `0%`-style aggregates leaked in via the fallback).
12
+
13
+ ## 0.1.30 (2026-08-22)
14
+
15
+ - Router usage is now **provider-scoped**: the active router model's upstream
16
+ prefix (e.g. `command-code` from `router/command-code/deepseek/deepseek-v4-flash`)
17
+ is passed as `?provider=` to the OmniRoute usage API, so the footer shows that
18
+ upstream's quota (e.g. opencode-go session/weekly windows).
19
+ - Aliases normalized before the API call: `cmd`→`command-code`, `oc`→`opencode-go`,
20
+ `ds`→`deepseek`, `glmcn`/`glm-cn`→`zai-coding-cn`; generic router aliases
21
+ (auto/aug/no-think/tllm/combo/openrouter/nvidia/…) are filtered out.
22
+ - When the selected upstream has no cached quota ("No cached usage data"),
23
+ the adapter retries without `?provider=` so the footer still shows the best
24
+ snapshot instead of empty windows.
25
+ - Switching between router models with different upstreams now refreshes
26
+ immediately (adapter id includes the prefix, so stale in-flight results are
27
+ discarded via the generation guard).
28
+ - `/sub` breakdown shows only the Provider quota section — personal USD budget
29
+ lines are no longer surfaced.
30
+ - Self-check extended (17 assertions) and now exercises the real
31
+ `routerUpstreamPrefix` (aliases + generic filter).
32
+
33
+ ## 0.1.27 (2026-08-22)
34
+
35
+ - Router adapter now fetches real usage from OmniRoute instances: GET
36
+ `<origin>/api/usage/om-usage` (Bearer = router API key from auth.json or
37
+ ROUTER_API_KEY env). The plain-text report (Personal quota Daily/Weekly +
38
+ Provider quota Session/Weekly) is parsed into the footer R:/W: windows.
39
+ Non-OmniRoute routers 404 → fall back to the endpoint-only display.
40
+ When the per-key usage command is disabled, the footer shows a hint to
41
+ enable it in the OmniRoute dashboard (API Keys → the key).
42
+ - New pure parser `parseOmniUsageText` with an env-gated self-check
43
+ (PI_SUB_SELF_CHECK=1) — pi-sub stays pack-only for CI.
44
+
45
+ ## 0.1.26 (2026-08-21)
46
+
47
+ - Router adapter: reads `router.baseUrl` from `~/.pi/agent/settings.json`
48
+ (env `ROUTER_BASE_URL`/`NINE_ROUTER_BASE_URL`) now that pi-router moved the
49
+ endpoint URL out of the old `9router-config.json` (which is migrated away on
50
+ pi-router load). Footer shows for both new `router/` and legacy `9router/`
51
+ provider models.
52
+
1
53
  # Changelog
2
54
 
3
55
  ## 0.1.29 (2026-08-15)
package/README.md CHANGED
@@ -51,12 +51,27 @@ The built-in `zai-coding-cn` provider targets the domestic BigModel endpoint (`o
51
51
  (Z.ai (CN) key#1a2b3c4d) R:55%/2H W:80%/3D 42 tok/s
52
52
  ```
53
53
 
54
- ### 9router
54
+ ### Router (pi-router — formerly 9router)
55
55
 
56
- 9router is a proxy/router with no upstream usage API, so the footer shows the configured endpoint and last response speed:
56
+ For **OmniRoute** instances the footer shows real usage: `GET <origin>/api/usage/om-usage`
57
+ with the router API key (auth.json `router` credential from `/login router`, or
58
+ `ROUTER_API_KEY` env) returns the per-key report — Personal quota (daily/weekly
59
+ USD budgets) and Provider quota (session/weekly connection windows) — rendered
60
+ as `R:`/`W:` remaining-percent windows:
57
61
 
58
62
  ```text
59
- 9router (172.30.55.22:20128) 145 tok/s
63
+ Router usage R:80% W:28% 145 tok/s
64
+ ```
65
+
66
+ Requirements:
67
+ - The router instance must be OmniRoute (other routers 404 → endpoint-only fallback).
68
+ - The API key must have the **usage command enabled** in the OmniRoute dashboard
69
+ (API Keys → the key → enable "usage command"); the footer shows a hint when it's off.
70
+
71
+ The endpoint URL is read from `~/.pi/agent/settings.json` (`router.baseUrl`), env `ROUTER_BASE_URL` overrides:
72
+
73
+ ```text
74
+ Router (172.30.55.22:20128) 145 tok/s
60
75
  ```
61
76
 
62
77
  ### Command Code
@@ -16,8 +16,11 @@ const ZAI_PROVIDER = "zai";
16
16
  const ZAI_USAGE_URL = "https://api.z.ai/api/monitor/usage/quota/limit";
17
17
  const ZAI_CODING_CN_PROVIDER = "zai-coding-cn";
18
18
  const ZAI_CODING_CN_USAGE_URL = "https://open.bigmodel.cn/api/monitor/usage/quota/limit";
19
- const NINE_ROUTER_PROVIDER = "9router";
20
- const NINE_ROUTER_CONFIG_PATH = path.join(os.homedir(), ".pi", "agent", "9router-config.json");
19
+ const ROUTER_PROVIDER = "router";
20
+ const LEGACY_9ROUTER_PROVIDER = "9router";
21
+ // pi-router (formerly pi-9router): URL lives in settings.json `router.baseUrl`
22
+ // (env override ROUTER_BASE_URL), key in auth.json `router` credential.
23
+ const ROUTER_SETTINGS_PATH = path.join(os.homedir(), ".pi", "agent", "settings.json");
21
24
  const COMMAND_CODE_PROVIDER = "commandcode";
22
25
  const COMMAND_CODE_USAGE_URL = "https://api.commandcode.ai/alpha/billing/credits";
23
26
 
@@ -117,8 +120,8 @@ function isZaiCodingCnModel(model: ModelLike): boolean {
117
120
  return (model?.provider?.toLowerCase() ?? "") === ZAI_CODING_CN_PROVIDER;
118
121
  }
119
122
 
120
- function isNineRouterModel(model: ModelLike): boolean {
121
- return (model?.provider?.toLowerCase() ?? "") === NINE_ROUTER_PROVIDER;
123
+ function isRouterModel(model: ModelLike, provider: string = ROUTER_PROVIDER): boolean {
124
+ return (model?.provider?.toLowerCase() ?? "") === provider;
122
125
  }
123
126
 
124
127
  function isCommandCodeModel(model: ModelLike): boolean {
@@ -309,21 +312,137 @@ async function readZaiAuth(providerId: string, label: string): Promise<{ key: st
309
312
  return { key: entry.key, account: authAccountSnapshot(label, entry) };
310
313
  }
311
314
 
312
- // ponytail: duplicated from pi-9router — no shared config lib, 8 lines, acceptable
313
- function readNineRouterConfig(): { baseUrl: string; apiKey?: string } | null {
315
+ // ponytail: duplicated from pi-router — no shared config lib, ~12 lines, acceptable
316
+ function readRouterConfig(): { baseUrl: string } | null {
314
317
  try {
315
- const exists = (p: string) => { try { return fs.statSync(p).isFile(); } catch { return false; } };
316
- if (!exists(NINE_ROUTER_CONFIG_PATH)) return null;
317
- const data = JSON.parse(fs.readFileSync(NINE_ROUTER_CONFIG_PATH, "utf8")) as Record<string, unknown>;
318
- const baseUrl = process.env.NINE_ROUTER_BASE_URL || (typeof data.baseUrl === "string" ? data.baseUrl : undefined);
319
- const apiKey = process.env.NINE_ROUTER_API_KEY || (typeof data.apiKey === "string" ? data.apiKey : undefined);
320
- if (!baseUrl || typeof baseUrl !== "string") return null;
321
- return { baseUrl, apiKey };
318
+ let baseUrl = process.env.ROUTER_BASE_URL || process.env.NINE_ROUTER_BASE_URL;
319
+ if (!baseUrl && fs.statSync(ROUTER_SETTINGS_PATH).isFile()) {
320
+ const settings = JSON.parse(fs.readFileSync(ROUTER_SETTINGS_PATH, "utf8")) as Record<string, unknown>;
321
+ const router = settings.router as { baseUrl?: unknown } | undefined;
322
+ if (typeof router?.baseUrl === "string" && router.baseUrl) baseUrl = router.baseUrl;
323
+ }
324
+ return baseUrl ? { baseUrl } : null;
322
325
  } catch {
323
326
  return null;
324
327
  }
325
328
  }
326
329
 
330
+ /** Router API key: auth.json `router` credential (via /login), then env. */
331
+ function readRouterApiKey(): string | undefined {
332
+ const stored = readStoredCredential(ROUTER_PROVIDER, piAuthPath()) as PiAuthEntry | undefined;
333
+ if (stored?.key) return stored.key;
334
+ return process.env.ROUTER_API_KEY || process.env.NINE_ROUTER_API_KEY || undefined;
335
+ }
336
+
337
+ /** Strip a `/v1` suffix so management routes (under the origin) can be
338
+ * derived from the OpenAI-compatible baseUrl. */
339
+ function routerOrigin(baseUrl: string): string {
340
+ return baseUrl.replace(/\/v1\/?$/, "");
341
+ }
342
+
343
+ /** Parse OmniRoute's `/api/usage/om-usage` plain-text report into windows.
344
+ * Sections: "Personal quota" (per-key USD budgets: Daily/Weekly) and
345
+ * "Provider quota" (connection session/weekly). Lines: `<Label>`,
346
+ * `NN% left`, `⏱ reset in <countdown>`. Robust to missing/unknown blocks. */
347
+ /** Convert OmniRoute's "reset in 2h 55m" countdown into the compact footer
348
+ * label (e.g. 3H), matching the direct Z.ai display (R:99%/4H). */
349
+ function countdownToLabel(text: string): string | undefined {
350
+ const m = text.match(/(?:(\d+)\s*d)?\s*(?:(\d+)\s*h)?\s*(?:(\d+)\s*m)?/);
351
+ if (!m || (!m[1] && !m[2] && !m[3])) return undefined;
352
+ const secs = Number(m[1] || 0) * 86400 + Number(m[2] || 0) * 3600 + Number(m[3] || 0) * 60;
353
+ return secs ? formatRemainingTime(Date.now() / 1000 + secs) : undefined;
354
+ }
355
+
356
+ export function parseOmniUsageText(text: string): {
357
+ personalDaily?: UsageWindow;
358
+ personalWeekly?: UsageWindow;
359
+ session?: UsageWindow;
360
+ providerWeekly?: UsageWindow;
361
+ } {
362
+ const out: {
363
+ personalDaily?: UsageWindow;
364
+ personalWeekly?: UsageWindow;
365
+ session?: UsageWindow;
366
+ providerWeekly?: UsageWindow;
367
+ } = {};
368
+ const lines = text.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
369
+ let inPersonal = false;
370
+ for (let i = 0; i < lines.length; i++) {
371
+ const line = lines[i];
372
+ if (line.toLowerCase() === "personal quota") { inPersonal = true; continue; }
373
+ if (line.toLowerCase() === "provider quota") { inPersonal = false; continue; }
374
+ const usedMatch = line.match(/^(\d+)%\s*left$/);
375
+ if (!usedMatch) continue;
376
+ const label = (lines[i - 1] ?? "").toLowerCase();
377
+ const resetMatch = lines[i + 1]?.match(/reset in (.+)$/);
378
+ const remaining = Number(usedMatch[1]);
379
+ if (remaining < 0 || remaining > 100) continue;
380
+ const window: UsageWindow = { remaining };
381
+ if (resetMatch) {
382
+ window.resetLabel = `⏱ ${resetMatch[1].trim()}`;
383
+ window.remainingLabel = countdownToLabel(resetMatch[1]);
384
+ }
385
+ if (inPersonal) {
386
+ if (label.includes("daily")) out.personalDaily = window;
387
+ else if (label.includes("weekly")) out.personalWeekly = window;
388
+ } else {
389
+ if (label.includes("session")) out.session = window;
390
+ else if (label.includes("weekly")) out.providerWeekly = window;
391
+ }
392
+ }
393
+ return out;
394
+ }
395
+
396
+ // ponytail: runnable self-check (pi-sub has no test runner — pack gate only)
397
+ if (process.env.PI_SUB_SELF_CHECK === "1") {
398
+ const sample = [
399
+ "Personal quota", "Daily", "80% left", "⏱ reset in 15h 0m", "",
400
+ "Weekly", "90% left", "⏱ reset in 7d 0h 0m", "",
401
+ "Provider quota", "Session", "47% left", "⏱ reset in 9m", "",
402
+ "Weekly", "28% left", "⏱ reset in 1d 0h 0m",
403
+ ].join("\n");
404
+ const p = parseOmniUsageText(sample);
405
+ const assert = (cond: boolean, msg: string) => { if (!cond) throw new Error("pi-sub self-check: " + msg); };
406
+ assert(p.personalDaily?.remaining === 80, "personal daily 80");
407
+ // routerUpstreamPrefix: provider first segment, aliases + generic filtered
408
+ const rp = (id: string) => routerUpstreamPrefix({ id });
409
+ assert(rp("command-code/deepseek/deepseek-v4-flash") === "command-code", "prefix command-code");
410
+ assert(rp("cmd/deepseek/deepseek-v4-flash") === "command-code", "alias cmd → command-code");
411
+ assert(rp("oc/gpt-5") === "opencode-go", "alias oc → opencode-go");
412
+ // OmniRoute's connection slug is `glm-cn` (live-verified; zai-coding-cn is a
413
+ // Pi provider id, NOT an OmniRoute slug — wrong slug = no cached data).
414
+ assert(rp("glm-cn/glm-5.2") === "glm-cn", "glm-cn passes through");
415
+ assert(rp("glmcn/glm-5.2") === "glm-cn", "alias glmcn → glm-cn");
416
+ assert(rp("zai-coding/glm-5.2") === "zai-coding", "prefix zai-coding");
417
+ assert(rp("auto/best") === undefined, "generic auto filtered");
418
+ assert(rp("openrouter/gpt-5") === undefined, "generic openrouter filtered");
419
+ assert(rp("nvidia/gpt-5") === undefined, "generic nvidia filtered");
420
+ assert(p.personalWeekly?.remaining === 90, "personal weekly 90");
421
+ assert(p.session?.remaining === 47, "session 47");
422
+ assert(p.providerWeekly?.remaining === 28, "provider weekly 28");
423
+ assert(p.personalDaily?.resetLabel?.includes("15h"), "daily reset label");
424
+ const disabled = parseOmniUsageText("Usage command is disabled for this API key.");
425
+ assert(Object.keys(disabled).length === 0, "disabled text parses empty");
426
+ // Live-verified: provider without cached data → no windows (endpoint fallback).
427
+ const noCache = parseOmniUsageText("Provider quota\nNo cached usage data available.");
428
+ assert(Object.keys(noCache).length === 0, "no-cached-data parses empty");
429
+ // Live-verified: opencode-go quota via ?provider=opencode-go.
430
+ const live = parseOmniUsageText(
431
+ "Provider quota\nSession\n90% left\n⏱ reset in 1h 59m\n\nWeekly\n0% left\n⏱ reset in 1d 20h 26m"
432
+ );
433
+ assert(live.session?.remaining === 90, "session 90");
434
+ assert(live.providerWeekly?.remaining === 0, "weekly 0");
435
+ assert(live.session?.resetLabel?.includes("1h 59m"), "session reset");
436
+ // Live-verified 2026-08-22: glm-cn scoped quota — Session 99%, Weekly
437
+ // "Unavailable" (skipped, so W stays absent like the direct Z.ai footer).
438
+ const glmCn = parseOmniUsageText(
439
+ "Provider quota\nSession\n99% left\n⏱ reset in 2h 55m\n\nWeekly\nUnavailable\n⏱ reset in unknown"
440
+ );
441
+ assert(glmCn.session?.remaining === 99, "glm-cn session 99");
442
+ assert(glmCn.session?.remainingLabel === "3H", "glm-cn reset label 3H");
443
+ assert(glmCn.providerWeekly === undefined, "glm-cn weekly unavailable skipped");
444
+ }
445
+
327
446
  async function fetchUsageFromPiAuth(entry: PiAuthEntry, signal?: AbortSignal): Promise<UsageApiSnapshot | undefined> {
328
447
  const accountId = getCodexAccountId(entry) ?? entry.accountId;
329
448
  if (!accountId) throw new Error("Missing openai-codex OAuth entry in Pi auth");
@@ -395,27 +514,100 @@ async function fetchOpenCodeGoUsage(_signal?: AbortSignal): Promise<Subscription
395
514
  }
396
515
  }
397
516
 
398
- async function fetchNineRouterUsage(_signal?: AbortSignal): Promise<SubscriptionUsageSnapshot> {
399
- const cfg = readNineRouterConfig();
517
+ async function fetchRouterUsage(signal?: AbortSignal, provider?: string): Promise<SubscriptionUsageSnapshot> {
518
+ const cfg = readRouterConfig();
400
519
  const now = Date.now();
401
520
  if (!cfg) {
402
521
  return {
403
- providerDisplayName: "9router",
522
+ providerDisplayName: "Router",
404
523
  accounts: [],
405
524
  fetchedAt: now,
406
- error: "9router not configured — run /login-9router",
525
+ error: "router not configured — set router.baseUrl in ~/.pi/agent/settings.json",
407
526
  };
408
527
  }
409
- const account: SubscriptionAccountSnapshot = {
528
+ const apiKey = readRouterApiKey();
529
+ const baseAccount: SubscriptionAccountSnapshot = {
410
530
  id: cfg.baseUrl,
411
531
  isActive: true,
412
532
  accountLabel: cfg.baseUrl.replace(/^https?:\/\//, ""),
413
533
  lastActivity: "Now",
414
534
  };
535
+
536
+ // OmniRoute exposes per-key usage at GET <origin>/api/usage/om-usage
537
+ // (Bearer = the router API key). `?provider=` selects that upstream's quota
538
+ // (e.g. command-code/deepseek/deepseek-v4-flash → provider=command-code);
539
+ // without it the report shows the best/all snapshot. Plain text: Personal
540
+ // quota (Daily/Weekly USD budgets) + Provider quota (Session/Weekly).
541
+ // Non-OmniRoute routers 404 here — fall back to endpoint-only display.
542
+ if (apiKey) {
543
+ try {
544
+ const timeoutSignal = AbortSignal.timeout(7_000);
545
+ const combined = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
546
+ const url = `${routerOrigin(cfg.baseUrl)}/api/usage/om-usage` +
547
+ (provider ? `?provider=${encodeURIComponent(provider)}` : "");
548
+ const response = await fetch(url, {
549
+ headers: { Accept: "text/plain", Authorization: `Bearer ${apiKey}` },
550
+ signal: combined,
551
+ });
552
+ if (response.ok) {
553
+ let text = await response.text();
554
+ if (text && !text.includes("disabled")) {
555
+ let w = parseOmniUsageText(text);
556
+ // Provider-scoped call but no cached quota for that upstream — retry
557
+ // without ?provider= so the report shows the best/all snapshot.
558
+ if (provider && !w.session && !w.providerWeekly) {
559
+ const plain = await fetch(url.replace(/\?provider=.*$/, ""), {
560
+ headers: { Accept: "text/plain", Authorization: `Bearer ${apiKey}` },
561
+ signal: combined,
562
+ });
563
+ if (plain.ok) {
564
+ const plainText = await plain.text();
565
+ if (plainText && !plainText.includes("disabled")) {
566
+ w = parseOmniUsageText(plainText);
567
+ text = plainText;
568
+ }
569
+ }
570
+ }
571
+ // personalDaily = per-key budget (nearest reset → R slot),
572
+ // provider weekly/session = upstream quota (W slot). Fall back sensibly.
573
+ const account: SubscriptionAccountSnapshot = {
574
+ ...baseAccount,
575
+ plan: provider ? `Router · ${provider}` : "Router usage",
576
+ fiveHour: w.personalDaily ?? w.session,
577
+ weekly: w.providerWeekly ?? w.personalWeekly,
578
+ // breakdown keeps only the provider-quota section — the raw text can
579
+ // include personal USD budget lines (privacy) and is noisy.
580
+ usageBreakdown: providerQuotaSection(text),
581
+ };
582
+ return {
583
+ providerDisplayName: "Router",
584
+ accounts: [account],
585
+ activeAccount: account,
586
+ fetchedAt: Date.now(),
587
+ };
588
+ }
589
+ // Usage command exists but is disabled for this key — keep the footer
590
+ // clean (endpoint display) and surface the hint in /sub detail only.
591
+ const keyFp = apiKey ? `${apiKey.slice(0, 4)}…${apiKey.slice(-4)}` : "?";
592
+ const hintAccount: SubscriptionAccountSnapshot = {
593
+ ...baseAccount,
594
+ usageBreakdown: `OmniRoute usage command is disabled for the router key ${keyFp} — ` +
595
+ `enable it in the dashboard (API Keys → the key ending ${apiKey?.slice(-4)} → usage command).`,
596
+ };
597
+ return {
598
+ providerDisplayName: "Router",
599
+ accounts: [hintAccount],
600
+ activeAccount: hintAccount,
601
+ fetchedAt: Date.now(),
602
+ };
603
+ }
604
+ } catch { /* non-OmniRoute or transient — fall through to endpoint display */ }
605
+ }
606
+
415
607
  return {
416
- providerDisplayName: "9router",
417
- accounts: [account],
418
- activeAccount: account,
608
+ providerDisplayName: "Router",
609
+ accounts: [baseAccount],
610
+ activeAccount: baseAccount,
419
611
  fetchedAt: now,
420
612
  };
421
613
  }
@@ -704,11 +896,54 @@ function supportedAdapter(model: ModelLike): SubscriptionProviderAdapter | undef
704
896
  if (isOpenCodeGoModel(model)) return { id: OPC_PROVIDER, displayName: "OpenCode Go", fetchUsage: fetchOpenCodeGoUsage };
705
897
  if (isZaiModel(model)) return { id: ZAI_PROVIDER, displayName: "Z.ai", ...zaiUsageAdapter(ZAI_PROVIDER, ZAI_USAGE_URL, "Z.ai") };
706
898
  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)") };
707
- if (isNineRouterModel(model)) return { id: NINE_ROUTER_PROVIDER, displayName: "9router", fetchUsage: fetchNineRouterUsage };
899
+ if (isRouterModel(model)) {
900
+ const prefix = routerUpstreamPrefix(model);
901
+ return {
902
+ // Prefix in the id makes adapterChanged fire when switching upstreams
903
+ // (e.g. opencode-go → command-code), so the in-flight fetch from the
904
+ // previous model is discarded via the refreshGeneration guard.
905
+ id: prefix ? `${ROUTER_PROVIDER}:${prefix}` : ROUTER_PROVIDER,
906
+ displayName: "Router",
907
+ // ponytail: capture the upstream provider prefix so fetchRouterUsage can
908
+ // request that provider's quota from the OmniRoute usage API.
909
+ fetchUsage: (signal) => fetchRouterUsage(signal, prefix),
910
+ };
911
+ }
912
+ if (isRouterModel(model, LEGACY_9ROUTER_PROVIDER)) return { id: LEGACY_9ROUTER_PROVIDER, displayName: "9router (legacy)", fetchUsage: fetchRouterUsage };
708
913
  if (isCommandCodeModel(model)) return { id: COMMAND_CODE_PROVIDER, displayName: "Command Code", fetchUsage: fetchCommandCodeUsage };
709
914
  return undefined;
710
915
  }
711
916
 
917
+ /** Strip everything up to and including the "Provider quota" section header
918
+ * so /sub breakdown never shows personal USD budget lines. */
919
+ function providerQuotaSection(text: string): string | undefined {
920
+ const idx = text.indexOf("Provider quota");
921
+ if (idx < 0) return undefined;
922
+ const section = text.slice(idx);
923
+ return section.trim().length > 0 ? section : undefined;
924
+ }
925
+
926
+ /** First path segment of a router model id = the upstream provider OmniRoute
927
+ * routes to (e.g. `command-code/deepseek/deepseek-v4-flash` → `command-code`).
928
+ * `router/provider/model` in pi flattens to id `provider/model`, so the prefix
929
+ * is the first segment. Aliases normalize to the canonical provider id
930
+ * (`cmd` → `command-code`); generic router aliases carry no provider info —
931
+ * return undefined so the usage API picks the best snapshot. */
932
+ function routerUpstreamPrefix(model: ModelLike): string | undefined {
933
+ const id = model?.id ?? "";
934
+ const first = id.split("/")[0]?.toLowerCase();
935
+ if (!first) return undefined;
936
+ // Alias normalization: OmniRoute exposes the same upstream under several ids.
937
+ if (first === "cmd") return "command-code";
938
+ if (first === "oc") return "opencode-go";
939
+ if (first === "ds") return "deepseek";
940
+ if (first === "glmcn") return "glm-cn"; // OmniRoute connection slug (not the Pi provider id zai-coding-cn)
941
+ // Generic router aliases / upstreams without cached quota data — no provider
942
+ // selection; the usage API returns the best snapshot instead.
943
+ const generic = new Set(["auto", "aug", "no-think", "tllm", "combo", "openrouter", "nvidia", "felo", "pepper", "mcode", "ddgw", "veoaifree-web", "veo-free"]);
944
+ return generic.has(first) ? undefined : first;
945
+ }
946
+
712
947
  function formatRemaining(window: UsageWindow | undefined): string {
713
948
  if (!window) return "?";
714
949
  if (window.remainingLabel) return `${window.remaining}%/${window.remainingLabel}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bacnh85/pi-sub",
3
- "version": "0.1.29",
3
+ "version": "0.1.31",
4
4
  "description": "Pi extension showing subscription usage for supported model providers.",
5
5
  "type": "module",
6
6
  "license": "MIT",