# 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):

```bash
pi install npm:@eiei114/pi-sub-core
```

Use `-l` to install into project settings instead of global:

```bash
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):

```bash
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`:

```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:

```json
{
  "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`](https://openrouter.ai/docs/api-reference/limits) | The key doing the request: its spending cap (`limit` / `limit_remaining`) and its spend (`usage`) | Yes — authoritative |
| [`GET /api/v1/credits`](https://openrouter.ai/docs/api-reference/get-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` / `keyUsage` describe **this key**. A `Key limit`
  window is only produced for a real numeric cap; a zero cap counts as fully
  used. `limit: null` means the key itself is uncapped — not that the wallet is
  unlimited — so it yields no percentage at all.
- `creditTotal` / `creditUsage` / `creditRemaining` stay **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.

Current-session Codex usage is an exception: it resolves credentials from the selected Pi model, including numbered multi-pass providers such as `openai-codex-2`, and keeps a credential-scoped in-memory cache rather than sharing the base provider's disk entry. Switching models clears the old account's display immediately; late responses and reload callbacks cannot restore it. Authentication failure does not fall back to another account. The sub-bar label identifies the selected Codex provider. This does not add an all-account quota API or change other providers' credential selection.

## Security notes

- Keep `~/.pi/agent/auth.json` readable 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`, or `Usage` when 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.json` under `xai` (only when the entry has `"type": "oauth"` and a non-blank `access`), or the `XAI_OAUTH_TOKEN` override. 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_KEY` and `api_key` auth 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 `xai` credential 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:

```bash
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)
1. Add provider name to `src/types.ts` (`PROVIDERS`, `ProviderName`).
2. Implement fetcher in `src/providers/impl/<provider>.ts`.
3. Register provider in `src/providers/registry.ts`.
4. Add detection + status config in `src/providers/metadata.ts`.
5. Add provider settings defaults in `src/settings-types.ts`.

#### sub-bar (display + UI)
1. Add provider name to `src/types.ts`.
2. Add display rules + labels in `src/providers/metadata.ts`.
3. Add window visibility in `src/providers/windows.ts`.
4. Add extras in `src/providers/extras.ts` (if needed).
5. Add settings UI + defaults in `src/providers/settings.ts` and `src/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](https://github.com/eiei114/pi-sub/pull/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](https://github.com/hjanuschka/shitty-extensions?tab=readme-ov-file#usage-barts), [@hjanuschka](https://x.com/hjanuschka))
- Peter Steinberger ([CodexBar](https://github.com/steipete/CodexBar), [@steipete](https://x.com/steipete))

## Status

Active. Used by `sub-bar` for display.
