pi-usage-hub
v1.1.3
Published
Usage hub for pi — provider quota/balance registry with cache, /usage-hub panel, and pull API
Downloads
636
Maintainers
Readme
pi-usage-hub
Track provider quotas and balances in pi — /usage-hub panel, session stats, and a pull API for footer integration.
Why
Checking separate dashboards for DeepSeek, NewAPI relays, OpenAI Codex, xAI, Kiro, OpenCode Go, and ARK breaks flow. pi-usage-hub brings their quotas and balances into one TUI panel while keeping footer integration optional and pull-based.
Install
pi install npm:pi-usage-hubCommands
| Command | Description |
|---------|-------------|
| /usage-hub | Show quotas and balances for detected providers |
| /usage-hub session | Show local session token and cost stats |
| /usage-hub login <name> | Open browser login for a cookie-based provider |
Inside the panel, Tab switches between Quota and Session. In the Quota view, ↑↓ scrolls when content overflows.


Configuration
Create ~/.pi/agent/pi-usage-hub.json. Provider order is preserved in the panel.
{
"providers": [
{
"type": "deepseek",
"apiKey": "sk-..."
},
{
"name": "xh",
"type": "newapi",
"matchProviders": ["xh-cc", "xh-glm"],
"host": "https://example.com",
"token": "...",
"userId": "1"
},
{
"name": "ocg",
"type": "opencode-go",
"workspaceId": "wrk_...",
"matchProviders": ["opencode-go"]
},
{ "type": "ark" },
{
"type": "xai",
"matchProviders": ["xai-auth", "xai", "grok-cli"]
},
{ "type": "kiro" },
{ "type": "kimi-coding" },
{ "type": "zai" },
{ "type": "codex" }
]
}Common fields
| Field | Required | Description |
|-------|----------|-------------|
| type | yes | Built-in provider factory |
| name | no | Instance key; defaults to type, then type-2, type-3, and so on |
| matchProviders | no | Additional model.provider values mapped to this entry |
| shortLabel / label | no | Override the footer label or panel title |
| hidden | no | Exclude from panel and footer; /usage-hub login still works |
| disabled | no | Do not register at all; on untyped entries, refuses that provider's registration |
Provider types
| Type | Credentials | Notes | Website |
|------|-------------|-------|---------|
| deepseek | apiKey | Account balance | deepseek |
| newapi | host, token, userId | Balance and today's spend; supports multiple instances | newapi |
| opencode-go | workspaceId; optional auth | Go plan five-hour/weekly/monthly spend; uses the configured console session cookie or macOS Chrome | opencode |
| ark | optional cookie, csrfToken | Uses the configured Cookie header or macOS Chrome | 火山引擎 |
| xai | — | Reads auth.json or ~/.grok/auth.json | x.ai/grok |
| kiro | — | Reads the Kiro OAuth entry from auth.json | kiro.dev |
| kimi-coding | optional apiKey | 5h rolling window + weekly quota; falls back to the kimi-coding entry in auth.json (Kimi Code key, not the open-platform key) | kimi.com/code |
| zai | optional apiKey | 5h window, weekly, and monthly search quota; falls back to the zai entry in auth.json | z.ai |
| codex | — | Codex rolling and 7-day usage windows; reads the openai-codex OAuth entry in auth.json | OpenAI Codex |
For NewAPI, token is the system access token (not a chat sk- token), and userId is sent as New-Api-User. See Authentication and Generate access token.
For ARK and OpenCode Go, configured cookie / auth values take priority over Chrome. Manual credentials disable /usage-hub login for that entry; replace them when they expire. Automatic Chrome cookie reading is macOS only.
Related auth packages
These are companion packages, not npm peer dependencies:
| Type | Companion | Purpose |
|------|-----------|---------|
| kiro | pi-provider-kiro-dev | Provides /login kiro, models, and the auth.json entry |
pi-usage-hub only reads those credentials; it does not run their OAuth flows.
Built-in providers cover the integrations used and smoke-tested by the author. To support another provider, register a custom provider from an extension, or fork the package and submit a PR.
Add a custom provider
See examples/custom-provider.ts for a self-contained extension that implements and registers a provider. Copy it into ~/.pi/agent/extensions/, then adapt the endpoint, credentials, response shape, and labels.
pi.events.on("pi-usage-hub:ready", (hub) => hub.register(myProvider));
pi.events.emit("pi-usage-hub:register", myProvider);
pi.events.emit("pi-usage-hub:unregister", { key: "my-relay" });Re-registering the same key overwrites (registration is idempotent), so the ready-listener + emit combination above never duplicates a provider.
Configure a custom provider from JSON
providers entries without type target an externally-registered provider by name. Meta fields override the provider's own values at register time; disabled: true refuses registration; all other fields pass through untouched to hub.getProviderConfig(name), so the extension can read its credentials from JSON instead of env vars:
{
"providers": [
{ "type": "kimi-coding" },
{
"name": "qianwen",
"shortLabel": "QW",
"hidden": false,
"cookie": "login_qianwenai_ticket=..."
}
]
}pi.events.on("pi-usage-hub:ready", (hub) => {
const cfg = hub.getProviderConfig("qianwen"); // { shortLabel, hidden, cookie, ... }
const cookie = typeof cfg?.cookie === "string" ? cfg.cookie : undefined;
});Untyped entries do not affect built-in name allocation; two untyped entries with the same name resolve last-write-wins. An untyped entry whose name matches a built-in entry also applies to it, so { "name": "kimi-coding", "hidden": true } hides the built-in without editing its own entry.
Register a built-in from an extension
hub.registerBuiltin(type, cfg?) instantiates any built-in factory with your own parameters (e.g. credentials obtained at runtime) and registers it, allocating type, type-2, … on key clash. Returns the registered key, or null for unknown types or disabled: true.
Pull API
The hub caches results for 60 seconds and deduplicates concurrent requests. It never pushes footer text: consumers start refreshes without blocking lifecycle events, read the cached summary, and re-render when notified.
| API | Role |
|-----|------|
| pi-usage-hub:ready | Provides the hub; also emitted on session_start |
| hub.refresh({ model?, force? }) | Refreshes the matching provider and returns its summary |
| hub.getSummary(model?) | Synchronously reads the cached one-line summary |
| pi-usage-hub:updated | Signals { key, summary } after a cache update |
Footer example

type UsageHub = {
getSummary(model?: { provider?: string }): string | null;
refresh(opts?: {
model?: { provider?: string };
force?: boolean;
}): Promise<string | null>;
};
let usageHub: UsageHub | null = null;
let requestRender: (() => void) | null = null;
const offReady = pi.events.on("pi-usage-hub:ready", (hub: UsageHub) => {
usageHub = hub;
requestRender?.();
});
const offUpdated = pi.events.on("pi-usage-hub:updated", () => {
requestRender?.();
});
// Pi awaits lifecycle handlers; refresh in the background and re-render on pi-usage-hub:updated.
pi.on("session_start", (_event, ctx) => {
void usageHub?.refresh({ model: ctx.model, force: true });
});
pi.on("model_select", (event) => {
void usageHub?.refresh({ model: event.model, force: true });
});
pi.on("agent_end", (_event, ctx) => {
void usageHub?.refresh({ model: ctx.model, force: true });
});
// Inside footer render():
// const usageText = usageHub?.getSummary(ctx.model);
// "XAI 87% · ↻ 3d 16h"
pi.on("session_shutdown", async () => {
offReady();
offUpdated();
usageHub = null;
});License
MIT
