@bacnh85/pi-sub 0.1.20 → 0.1.21

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 CHANGED
@@ -61,6 +61,14 @@ The built-in `zai-coding-cn` provider targets the domestic BigModel endpoint (`o
61
61
  (Z.ai (CN) key#1a2b3c4d) R:55%/2H W:80%/3D 42 tok/s
62
62
  ```
63
63
 
64
+ ### 9router
65
+
66
+ 9router is a proxy/router with no upstream usage API, so the footer shows the configured endpoint and last response speed:
67
+
68
+ ```text
69
+ 9router (172.30.55.22:20128) 145 tok/s
70
+ ```
71
+
64
72
  ### Tokens per second
65
73
 
66
74
  `pi-sub` tracks each response's tokens-per-second (tok/s) speed by measuring the time from provider request to message completion against the response's output token count. The last response's speed is shown in the footer next to usage data. The `/sub` detail view shows both the last response speed and the session-wide average.
@@ -1,5 +1,6 @@
1
1
  import { readStoredCredential, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import { createHash } from "node:crypto";
3
+ import fs from "node:fs";
3
4
  import os from "node:os";
4
5
  import path from "node:path";
5
6
 
@@ -15,6 +16,8 @@ const ZAI_PROVIDER = "zai";
15
16
  const ZAI_USAGE_URL = "https://api.z.ai/api/monitor/usage/quota/limit";
16
17
  const ZAI_CODING_CN_PROVIDER = "zai-coding-cn";
17
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");
18
21
 
19
22
  type ModelLike = { provider?: string; id?: string } | undefined;
20
23
 
@@ -110,6 +113,10 @@ function isZaiCodingCnModel(model: ModelLike): boolean {
110
113
  return (model?.provider?.toLowerCase() ?? "") === ZAI_CODING_CN_PROVIDER;
111
114
  }
112
115
 
116
+ function isNineRouterModel(model: ModelLike): boolean {
117
+ return (model?.provider?.toLowerCase() ?? "") === NINE_ROUTER_PROVIDER;
118
+ }
119
+
113
120
  function piAuthPath(): string {
114
121
  const configDir = process.env.PI_CODING_AGENT_DIR?.trim() || path.join(os.homedir(), ".pi", "agent");
115
122
  return path.join(configDir, "auth.json");
@@ -294,6 +301,21 @@ async function readZaiAuth(providerId: string, label: string): Promise<{ key: st
294
301
  return { key: entry.key, account: authAccountSnapshot(label, entry) };
295
302
  }
296
303
 
304
+ // ponytail: duplicated from pi-9router — no shared config lib, 8 lines, acceptable
305
+ function readNineRouterConfig(): { baseUrl: string; apiKey?: string } | null {
306
+ try {
307
+ const exists = (p: string) => { try { return fs.statSync(p).isFile(); } catch { return false; } };
308
+ if (!exists(NINE_ROUTER_CONFIG_PATH)) return null;
309
+ const data = JSON.parse(fs.readFileSync(NINE_ROUTER_CONFIG_PATH, "utf8")) as Record<string, unknown>;
310
+ const baseUrl = process.env.NINE_ROUTER_BASE_URL || (typeof data.baseUrl === "string" ? data.baseUrl : undefined);
311
+ const apiKey = process.env.NINE_ROUTER_API_KEY || (typeof data.apiKey === "string" ? data.apiKey : undefined);
312
+ if (!baseUrl || typeof baseUrl !== "string") return null;
313
+ return { baseUrl, apiKey };
314
+ } catch {
315
+ return null;
316
+ }
317
+ }
318
+
297
319
  async function fetchUsageFromPiAuth(entry: PiAuthEntry, signal?: AbortSignal): Promise<UsageApiSnapshot | undefined> {
298
320
  const accountId = getCodexAccountId(entry) ?? entry.accountId;
299
321
  if (!accountId) throw new Error("Missing openai-codex OAuth entry in Pi auth");
@@ -364,6 +386,31 @@ async function fetchOpenCodeGoUsage(_signal?: AbortSignal): Promise<Subscription
364
386
  }
365
387
  }
366
388
 
389
+ async function fetchNineRouterUsage(_signal?: AbortSignal): Promise<SubscriptionUsageSnapshot> {
390
+ const cfg = readNineRouterConfig();
391
+ const now = Date.now();
392
+ if (!cfg) {
393
+ return {
394
+ providerDisplayName: "9router",
395
+ accounts: [],
396
+ fetchedAt: now,
397
+ error: "9router not configured — run /login-9router",
398
+ };
399
+ }
400
+ const account: SubscriptionAccountSnapshot = {
401
+ id: cfg.baseUrl,
402
+ isActive: true,
403
+ accountLabel: cfg.baseUrl.replace(/^https?:\/\//, ""),
404
+ lastActivity: "Now",
405
+ };
406
+ return {
407
+ providerDisplayName: "9router",
408
+ accounts: [account],
409
+ activeAccount: account,
410
+ fetchedAt: now,
411
+ };
412
+ }
413
+
367
414
  // ---------------------------------------------------------------------------
368
415
  // Z.ai adapter
369
416
  // ---------------------------------------------------------------------------
@@ -552,6 +599,7 @@ function supportedAdapter(model: ModelLike): SubscriptionProviderAdapter | undef
552
599
  if (isOpenCodeGoModel(model)) return { id: OPC_PROVIDER, displayName: "OpenCode Go", fetchUsage: fetchOpenCodeGoUsage };
553
600
  if (isZaiModel(model)) return { id: ZAI_PROVIDER, displayName: "Z.ai", ...zaiUsageAdapter(ZAI_PROVIDER, ZAI_USAGE_URL, "Z.ai") };
554
601
  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)") };
602
+ if (isNineRouterModel(model)) return { id: NINE_ROUTER_PROVIDER, displayName: "9router", fetchUsage: fetchNineRouterUsage };
555
603
  return undefined;
556
604
  }
557
605
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bacnh85/pi-sub",
3
- "version": "0.1.20",
3
+ "version": "0.1.21",
4
4
  "description": "Pi extension showing subscription usage for supported model providers.",
5
5
  "type": "module",
6
6
  "license": "MIT",