@eiei114/pi-sub-core
Shared usage data core for pi extensions
Package details
Install @eiei114/pi-sub-core from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@eiei114/pi-sub-core- Package
@eiei114/pi-sub-core- Version
2.4.0- Published
- Sep 8, 2026
- Downloads
- 1,586/mo · 466/wk
- Author
- eiei114
- License
- MIT
- Types
- extension
- Size
- 378.7 KB
- Dependencies
- 1 dependency · 2 peers
Pi manifest JSON
{
"extensions": [
"./index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
sub-core
Shared usage data core for pi extensions. Sub-core owns fetching, caching, provider selection, and emits usage updates via pi.events for the wider sub-* ecosystem (UI and non-UI clients).
Overview
- Fetches usage + status data from providers
- Deduplicates requests via shared cache/lock
- Emits updates for display-focused extensions (e.g.
sub-bar) and non-UI tooling extensions - Supports Antigravity usage via auth.json (
google-antigravity)
Installation
Install via the pi package manager (recommended):
pi install npm:@eiei114/pi-sub-core
Use -l to install into project settings instead of global:
pi install -l npm:@eiei114/pi-sub-core
For a UI, also install a display extension like sub-bar from the same repo (see the root README for the full setup).
Manual install (local development):
git clone https://github.com/eiei114/pi-sub.git
ln -s /path/to/pi-sub/packages/sub-core ~/.pi/agent/extensions/sub-core
Alternative (no symlink): add it to ~/.pi/agent/settings.json:
{
"extensions": ["/path/to/pi-sub/packages/sub-core/index.ts"]
}
Tool Access
Tool registration is gated by tools in ~/.pi/agent/pi-sub-core-settings.json.
By default, both tools are off. To enable them, set:
{
"tools": {
"usageTool": true,
"allUsageTool": true
}
}
Then run /reload (tool registration only happens on load). You can also toggle these in /sub-core:settings → Tool Settings.
When enabled, sub-core registers tools to expose usage snapshots to Pi:
sub_get_usage/get_current_usage– refreshes usage (forced by default) and returns{ provider, usage }.sub_get_all_usage/get_all_usage– refreshes and returns all enabled provider entries (auto-enabled providers require credentials).
Settings
Use sub-core:settings to configure shared provider settings plus Usage Refresh Settings and Status Refresh Settings. Provider enablement supports auto (default), on, and off — auto enables a provider only when credentials are detected.
Usage refresh controls cache/usage updates, while status refresh controls incident polling (you can keep status on a slower interval). The Minimum Refresh Interval caps how often refresh triggers can fetch new data even if you refresh every turn.
Antigravity usage requires an OAuth token in ~/.pi/agent/auth.json under the google-antigravity key.
Anthropic extra usage formatting is controlled in Provider Settings (currency symbol + decimal separator).
OpenRouter
OpenRouter usage is read from two endpoints with the same inference credential:
| Endpoint | Scope | Required? |
|---|---|---|
GET /api/v1/key |
The key doing the request: its spending cap (limit / limit_remaining) and its spend (usage) |
Yes — authoritative |
GET /api/v1/credits |
The account wallet (total_credits / total_usage) |
No — best effort |
/credits is documented as needing a management key. Some ordinary keys are
served it anyway, so it is still attempted, but a failure (typically 403)
never discards the key data: the snapshot keeps the key numbers and reports the
wallet as unavailable rather than reusing an older wallet reading.
The two are surfaced separately and are never mixed:
keyLimit/keyRemaining/keyUsagedescribe this key. AKey limitwindow is only produced for a real numeric cap; a zero cap counts as fully used.limit: nullmeans the key itself is uncapped — not that the wallet is unlimited — so it yields no percentage at all.creditTotal/creditUsage/creditRemainingstay account-level. A wallet with zero total credit counts as fully used.
Two fields are deliberately ignored: limit_reset is a period name such as
daily, not a timestamp, so no reset date is ever shown for a key cap or for
the top-up wallet; rate_limit is deprecated.
Credential lookup order is OPENROUTER_API_KEY, then OPENROUTER_KEY, then
~/.pi/agent/auth.json (openrouter.access / .key / .apiKey). Blank and
non-string values are skipped, and a !command value is not a credential
because no command is executed to resolve it. Both hosts are fixed constants —
there is no base-URL override — and requests refuse redirects so a bearer token
cannot follow a hop to another host. Picking a specific account out of a
multi-account auth file is a separate concern and is not handled here.
Settings are stored in ~/.pi/agent/pi-sub-core-settings.json (migrated from the legacy extension settings.json when present; the legacy file is removed after a successful migration).
Settings migrations: settings are merged with defaults on load, but renames/removals are not migrated automatically. When adding new settings or changing schema, update the defaults/merge logic and provide a migration (or instruct users to reset pi-sub-core-settings.json).
Cache
Sub-core stores a shared cache and lock file:
~/.pi/agent/cache/sub-core/cache.json~/.pi/agent/cache/sub-core/cache.lock
Legacy cache files next to the extension entry or in the agent root are migrated to the cache directory and removed on first run.
Security notes
- Keep
~/.pi/agent/auth.jsonreadable only by your user (e.g.chmod 600 ~/.pi/agent/auth.json). - Avoid logging token-bearing headers or auth config when troubleshooting provider calls.
Provider comparison
| Provider | Usage Data | Status Page | Notes |
|---|---|---|---|
| Anthropic (Claude) | 5h/Week windows, extra usage | ✅ | Extra usage on/off state |
| GitHub Copilot | Monthly quota, requests | ✅ | Request multiplier support |
| Google Gemini | Pro/Flash quotas | ✅ | Aggregated by model family |
| Antigravity | Model quotas | ✅ | Sandbox Cloud Code Assist quotas (tested) |
| OpenAI Codex | Primary/secondary windows | ✅ | Credits not yet supported (PRs welcome!) |
| OpenCode | 5h/Week/Month windows | - | Go subscription quotas from opencode.ai usage API; unofficial |
| AWS Kiro | Credits | - | kiro-cli /usage (stdout+stderr); tested on Windows |
| z.ai | Tokens/monthly limits | - | API quota limits |
| Kimi for Coding | Week + 5h rolling windows | - | OAuth; tested with pi-provider-kimi-code |
| OpenRouter | Key spending cap + account credit | - | /api/v1/key (authoritative) plus best-effort /api/v1/credits |
| xAI (Grok) | Subscription quota window (Week/Month) | - | SuperGrok/Grok plan quota via an undocumented CLI billing endpoint; OAuth only; base xai account only |
xAI (Grok) subscription usage
- What it shows: the subscription quota percentage of the current billing period plus its reset time, as one window (
Week,Month, orUsagewhen the period type is unknown). Nothing else is derived from the payload — prepaid balance, on-demand spend, credit/unit counts, and plan names are deliberately not shown because their meaning in this undocumented response is unverified. - Credentials: the OAuth access token from
~/.pi/agent/auth.jsonunderxai(only when the entry has"type": "oauth"and a non-blankaccess), or theXAI_OAUTH_TOKENoverride. Tokens are never refreshed and no login flow is triggered; when the token is rejected the widget reports the HTTP status and you re-authenticate with pi's normal/login. - API keys are not a substitute:
XAI_API_KEYandapi_keyauth entries are never used here. They are valid credentials for the xAI developer API, but that is a different billing bucket and cannot report subscription quota. - Single account: usage is read for the base
xaicredential only. Numbered aliases (xai-2, …) resolve to no provider rather than displaying the base account's quota. - Unofficial endpoint: the endpoint and its client headers are derived from client behavior, not from public docs, and can break at any time. HTTP 426 means the pinned client version is no longer accepted; it is reported as a plain error and never retried with a spoofed version. All failures soft-error with a static message plus HTTP status — response bodies are never surfaced or logged.
Development
Packaging notes (pi install compatibility)
Pi packages use a pi field in package.json plus the pi-package keyword for discoverability. This repo already declares pi.extensions, so you can install via:
pi install npm:@eiei114/pi-sub-core
Manual paths/symlinks still work for local development as documented above.
Tested providers
Tested so far: Anthropic (Claude), OpenAI Codex, GitHub Copilot, Kimi for Coding, OpenRouter. Other providers are implemented but not yet verified in production.
Adding a Provider
You need to update both sub-core (fetch layer) and sub-bar (display layer).
Feature placement (core vs UI)
- sub-core: fetching, caching, provider detection/selection, status polling, tools/events, and shared settings.
- sub-bar: formatting, widget layout, UI-only toggles, and display-specific behavior.
- sub-shared: shared types/constants for anything referenced by both layers.
See the root README “Developer guide” for the decision checklist and examples.
sub-core (fetch + status)
- Add provider name to
src/types.ts(PROVIDERS,ProviderName). - Implement fetcher in
src/providers/impl/<provider>.ts. - Register provider in
src/providers/registry.ts. - Add detection + status config in
src/providers/metadata.ts. - Add provider settings defaults in
src/settings-types.ts.
sub-bar (display + UI)
- Add provider name to
src/types.ts. - Add display rules + labels in
src/providers/metadata.ts. - Add window visibility in
src/providers/windows.ts. - Add extras in
src/providers/extras.ts(if needed). - Add settings UI + defaults in
src/providers/settings.tsandsrc/settings-types.ts.
Events (public contract)
Sub-core uses pi.events as an in-process pub/sub bus. Any sub-* extension can subscribe to updates (UI or headless). Sub-core is the source of truth for provider selection and refresh behavior; clients observe state and optionally request changes.
Broadcasts
sub-core:ready→{ state, settings }(first load)sub-core:update-current→{ state }(cache hit or fresh fetch)sub-core:update-all→{ state }(cached entries + current provider)sub-core:settings:updated→{ settings }
update-current state is { provider, usage }.
update-all state is { provider, entries }, where entries are cached provider snapshots.
Requests (pull current state)
sub-core:request→{ reply, includeSettings? }sub-core:request→{ type: "entries", reply, force? }(bulk usage entries)
The reply callback receives { state } or { entries } immediately if available.
Selective read-only usage (optional v1 contract)
sub-core:usage-request:v1 accepts { provider, signal?, reply }. It replies with
{ version: 1, provider, usage?, error?: { code } }. Types and the event constant
are exported by sub-shared as ScopedUsageRequest, ScopedUsageResponse and
SCOPED_USAGE_EVENT.
Allowed exact base Pi identities: anthropic, github-copilot, zai,
openrouter, opencode-go, xai. An identity also requires an installed adapter.
The current provider set includes xAI when its adapter is installed. This event
does not change provider response semantics; enhanced OpenRouter key/wallet
support is provided separately (#65).
The outer provider echoes the requested Pi ID;
usage.provider is the adapter ID (copilot / opencode where applicable).
Aliases and Codex are deliberately rejected: this is not an account-resolution
contract. Codex consumers must retain their account-bound credential path.
Each request reads only the selected provider through its existing credential
resolver and adapter. It honors disabled settings, uses GET only with redirects
prohibited, and composes caller cancellation, a 15-second deadline and runtime
shutdown. Native credential subprocesses receive a requested timeout of at most
five seconds (or the remaining deadline). This is not a hard wall-clock guarantee:
Node can wait for child termination and synchronous filesystem work is not
preemptible. Cancellation takes effect when synchronous work yields.
Requests before normal session initialization return FETCH_FAILED without
starting migrations; retry after session_start finishes.
It does not select a provider/model, read/write shared usage cache,
broadcast updates, refresh status, or spend credits. Base provider resolution
may use the adapter's normal native CLI credential fallback; it does not assert
that every custom model credential override identifies that same account.
Consumers must filter project permissions before emitting, validate both identities, bound their own wait (older cores have no listener), and ignore late responses after disposal. Never fall back to bulk entries to satisfy a scoped read. Existing autonomous sub-core refresh behavior is unchanged by this API.
Actions (mutate core state)
sub-core:settings:patch→{ patch }(updates refresh interval/provider settings and persists)sub-core:action→{ type: "refresh" | "cycleProvider", force? }
After an action, sub-core emits sub-core:update-current with the new state.
Credits
- Hannes Januschka (barts, @hjanuschka)
- Peter Steinberger (CodexBar, @steipete)
Status
Active. Used by sub-bar for display.