@bacnh85/pi-sub 0.1.26 → 0.1.27
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 +12 -0
- package/README.md +16 -1
- package/extensions/index.ts +128 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## 0.1.27 (2026-08-22)
|
|
2
|
+
|
|
3
|
+
- Router adapter now fetches real usage from OmniRoute instances: GET
|
|
4
|
+
`<origin>/api/usage/om-usage` (Bearer = router API key from auth.json or
|
|
5
|
+
ROUTER_API_KEY env). The plain-text report (Personal quota Daily/Weekly +
|
|
6
|
+
Provider quota Session/Weekly) is parsed into the footer R:/W: windows.
|
|
7
|
+
Non-OmniRoute routers 404 → fall back to the endpoint-only display.
|
|
8
|
+
When the per-key usage command is disabled, the footer shows a hint to
|
|
9
|
+
enable it in the OmniRoute dashboard (API Keys → the key).
|
|
10
|
+
- New pure parser `parseOmniUsageText` with an env-gated self-check
|
|
11
|
+
(PI_SUB_SELF_CHECK=1) — pi-sub stays pack-only for CI.
|
|
12
|
+
|
|
1
13
|
## 0.1.26 (2026-08-21)
|
|
2
14
|
|
|
3
15
|
- Router adapter: reads `router.baseUrl` from `~/.pi/agent/settings.json`
|
package/README.md
CHANGED
|
@@ -53,7 +53,22 @@ The built-in `zai-coding-cn` provider targets the domestic BigModel endpoint (`o
|
|
|
53
53
|
|
|
54
54
|
### Router (pi-router — formerly 9router)
|
|
55
55
|
|
|
56
|
-
|
|
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:
|
|
61
|
+
|
|
62
|
+
```text
|
|
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:
|
|
57
72
|
|
|
58
73
|
```text
|
|
59
74
|
Router (172.30.55.22:20128) 145 tok/s
|
package/extensions/index.ts
CHANGED
|
@@ -327,6 +327,79 @@ function readRouterConfig(): { baseUrl: string } | null {
|
|
|
327
327
|
}
|
|
328
328
|
}
|
|
329
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
|
+
export function parseOmniUsageText(text: string): {
|
|
348
|
+
personalDaily?: UsageWindow;
|
|
349
|
+
personalWeekly?: UsageWindow;
|
|
350
|
+
session?: UsageWindow;
|
|
351
|
+
providerWeekly?: UsageWindow;
|
|
352
|
+
} {
|
|
353
|
+
const out: {
|
|
354
|
+
personalDaily?: UsageWindow;
|
|
355
|
+
personalWeekly?: UsageWindow;
|
|
356
|
+
session?: UsageWindow;
|
|
357
|
+
providerWeekly?: UsageWindow;
|
|
358
|
+
} = {};
|
|
359
|
+
const lines = text.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
360
|
+
let inPersonal = false;
|
|
361
|
+
for (let i = 0; i < lines.length; i++) {
|
|
362
|
+
const line = lines[i];
|
|
363
|
+
if (line.toLowerCase() === "personal quota") { inPersonal = true; continue; }
|
|
364
|
+
if (line.toLowerCase() === "provider quota") { inPersonal = false; continue; }
|
|
365
|
+
const usedMatch = line.match(/^(\d+)%\s*left$/);
|
|
366
|
+
if (!usedMatch) continue;
|
|
367
|
+
const label = (lines[i - 1] ?? "").toLowerCase();
|
|
368
|
+
const resetMatch = lines[i + 1]?.match(/reset in (.+)$/);
|
|
369
|
+
const remaining = Number(usedMatch[1]);
|
|
370
|
+
if (remaining < 0 || remaining > 100) continue;
|
|
371
|
+
const window: UsageWindow = { remaining };
|
|
372
|
+
if (resetMatch) window.resetLabel = `⏱ ${resetMatch[1].trim()}`;
|
|
373
|
+
if (inPersonal) {
|
|
374
|
+
if (label.includes("daily")) out.personalDaily = window;
|
|
375
|
+
else if (label.includes("weekly")) out.personalWeekly = window;
|
|
376
|
+
} else {
|
|
377
|
+
if (label.includes("session")) out.session = window;
|
|
378
|
+
else if (label.includes("weekly")) out.providerWeekly = window;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return out;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ponytail: runnable self-check (pi-sub has no test runner — pack gate only)
|
|
385
|
+
if (process.env.PI_SUB_SELF_CHECK === "1") {
|
|
386
|
+
const sample = [
|
|
387
|
+
"Personal quota", "Daily", "80% left", "⏱ reset in 15h 0m", "",
|
|
388
|
+
"Weekly", "90% left", "⏱ reset in 7d 0h 0m", "",
|
|
389
|
+
"Provider quota", "Session", "47% left", "⏱ reset in 9m", "",
|
|
390
|
+
"Weekly", "28% left", "⏱ reset in 1d 0h 0m",
|
|
391
|
+
].join("\n");
|
|
392
|
+
const p = parseOmniUsageText(sample);
|
|
393
|
+
const assert = (cond: boolean, msg: string) => { if (!cond) throw new Error("pi-sub self-check: " + msg); };
|
|
394
|
+
assert(p.personalDaily?.remaining === 80, "personal daily 80");
|
|
395
|
+
assert(p.personalWeekly?.remaining === 90, "personal weekly 90");
|
|
396
|
+
assert(p.session?.remaining === 47, "session 47");
|
|
397
|
+
assert(p.providerWeekly?.remaining === 28, "provider weekly 28");
|
|
398
|
+
assert(p.personalDaily?.resetLabel?.includes("15h"), "daily reset label");
|
|
399
|
+
const disabled = parseOmniUsageText("Usage command is disabled for this API key.");
|
|
400
|
+
assert(Object.keys(disabled).length === 0, "disabled text parses empty");
|
|
401
|
+
}
|
|
402
|
+
|
|
330
403
|
async function fetchUsageFromPiAuth(entry: PiAuthEntry, signal?: AbortSignal): Promise<UsageApiSnapshot | undefined> {
|
|
331
404
|
const accountId = getCodexAccountId(entry) ?? entry.accountId;
|
|
332
405
|
if (!accountId) throw new Error("Missing openai-codex OAuth entry in Pi auth");
|
|
@@ -398,7 +471,7 @@ async function fetchOpenCodeGoUsage(_signal?: AbortSignal): Promise<Subscription
|
|
|
398
471
|
}
|
|
399
472
|
}
|
|
400
473
|
|
|
401
|
-
async function fetchRouterUsage(
|
|
474
|
+
async function fetchRouterUsage(signal?: AbortSignal): Promise<SubscriptionUsageSnapshot> {
|
|
402
475
|
const cfg = readRouterConfig();
|
|
403
476
|
const now = Date.now();
|
|
404
477
|
if (!cfg) {
|
|
@@ -409,16 +482,67 @@ async function fetchRouterUsage(_signal?: AbortSignal): Promise<SubscriptionUsag
|
|
|
409
482
|
error: "router not configured — set router.baseUrl in ~/.pi/agent/settings.json",
|
|
410
483
|
};
|
|
411
484
|
}
|
|
412
|
-
const
|
|
485
|
+
const apiKey = readRouterApiKey();
|
|
486
|
+
const baseAccount: SubscriptionAccountSnapshot = {
|
|
413
487
|
id: cfg.baseUrl,
|
|
414
488
|
isActive: true,
|
|
415
489
|
accountLabel: cfg.baseUrl.replace(/^https?:\/\//, ""),
|
|
416
490
|
lastActivity: "Now",
|
|
417
491
|
};
|
|
492
|
+
|
|
493
|
+
// OmniRoute exposes per-key usage at GET <origin>/api/usage/om-usage
|
|
494
|
+
// (Bearer = the router API key). The report is plain text: Personal quota
|
|
495
|
+
// (Daily/Weekly USB budgets) + Provider quota (Session/Weekly connections).
|
|
496
|
+
// Non-OmniRoute routers 404 here — fall back to endpoint-only display.
|
|
497
|
+
if (apiKey) {
|
|
498
|
+
try {
|
|
499
|
+
const timeoutSignal = AbortSignal.timeout(7_000);
|
|
500
|
+
const combined = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
501
|
+
const response = await fetch(`${routerOrigin(cfg.baseUrl)}/api/usage/om-usage`, {
|
|
502
|
+
headers: { Accept: "text/plain", Authorization: `Bearer ${apiKey}` },
|
|
503
|
+
signal: combined,
|
|
504
|
+
});
|
|
505
|
+
if (response.ok) {
|
|
506
|
+
const text = await response.text();
|
|
507
|
+
if (text && !text.includes("disabled")) {
|
|
508
|
+
const w = parseOmniUsageText(text);
|
|
509
|
+
// personalDaily = per-key budget (nearest reset → R slot),
|
|
510
|
+
// provider weekly = connection quota (W slot). Fall back sensibly.
|
|
511
|
+
const account: SubscriptionAccountSnapshot = {
|
|
512
|
+
...baseAccount,
|
|
513
|
+
plan: "Router usage",
|
|
514
|
+
fiveHour: w.personalDaily ?? w.session,
|
|
515
|
+
weekly: w.providerWeekly ?? w.personalWeekly,
|
|
516
|
+
usageBreakdown: text.length > 120 ? text : undefined,
|
|
517
|
+
};
|
|
518
|
+
return {
|
|
519
|
+
providerDisplayName: "Router",
|
|
520
|
+
accounts: [account],
|
|
521
|
+
activeAccount: account,
|
|
522
|
+
fetchedAt: Date.now(),
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
// Usage command exists but is disabled for this key — keep the footer
|
|
526
|
+
// clean (endpoint display) and surface the hint in /sub detail only.
|
|
527
|
+
const hintAccount: SubscriptionAccountSnapshot = {
|
|
528
|
+
...baseAccount,
|
|
529
|
+
usageBreakdown: "OmniRoute usage command is disabled for this API key — " +
|
|
530
|
+
"enable it in the dashboard (API Keys → this key → usage command).",
|
|
531
|
+
};
|
|
532
|
+
return {
|
|
533
|
+
providerDisplayName: "Router",
|
|
534
|
+
accounts: [hintAccount],
|
|
535
|
+
activeAccount: hintAccount,
|
|
536
|
+
fetchedAt: Date.now(),
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
} catch { /* non-OmniRoute or transient — fall through to endpoint display */ }
|
|
540
|
+
}
|
|
541
|
+
|
|
418
542
|
return {
|
|
419
543
|
providerDisplayName: "Router",
|
|
420
|
-
accounts: [
|
|
421
|
-
activeAccount:
|
|
544
|
+
accounts: [baseAccount],
|
|
545
|
+
activeAccount: baseAccount,
|
|
422
546
|
fetchedAt: now,
|
|
423
547
|
};
|
|
424
548
|
}
|