No description
  • JavaScript 53.5%
  • TypeScript 46.5%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-24 23:23:57 -06:00
doc Rename the virtual provider to billing and emit included-allowance routing hints. 2026-09-24 21:40:12 -06:00
examples Rename the virtual provider to billing and emit included-allowance routing hints. 2026-09-24 21:40:12 -06:00
src Skip billing aliases for missing models 2026-09-24 23:23:57 -06:00
test Skip billing aliases for missing models 2026-09-24 23:23:57 -06:00
.gitignore Project scaffold, test harness & architecture/epics docs 2026-09-05 01:17:51 -06:00
.gitlab-ci.yml Docs, examples & CI (E9) 2026-09-05 01:19:15 -06:00
LICENSE Project scaffold, test harness & architecture/epics docs 2026-09-05 01:17:51 -06:00
notes.md Initial notes 2026-09-04 13:38:06 -06:00
package-lock.json Project scaffold, test harness & architecture/epics docs 2026-09-05 01:17:51 -06:00
package.json Rename the virtual provider to billing and emit included-allowance routing hints. 2026-09-24 21:40:12 -06:00
README.md Rename the virtual provider to billing and emit included-allowance routing hints. 2026-09-24 21:40:12 -06:00
tsconfig.json Project scaffold, test harness & architecture/epics docs 2026-09-05 01:17:51 -06:00

pi-billing-provider

pi-billing-provider registers the billing-provider virtual provider. Each configured real model gets an included route, which is allowed only while a local usage snapshot says the included allowance is available, and an api route, which always delegates to the real provider. This makes it possible to prefer included usage and fail over to API billing with ordinary Pi provider routing.

Install

Install the published package and restart Pi:

pi install npm:pi-billing-provider

For local development, install the checkout instead:

pi install /path/to/billing-provider

The extension delegates to existing real Pi providers. Configure and authenticate those providers normally; billing-provider does not own upstream credentials or collect quota data itself.

Model IDs

The virtual provider is always billing-provider. Its virtual model IDs have this form:

<realProvider>/<realModel>/<variant>

variant is exactly one of included or api. For example:

{ "provider": "billing", "model": "openai-codex/gpt-5.6-sol/included" }
{ "provider": "billing", "model": "openai-codex/gpt-5.6-sol/api" }

The parser splits on the last /; that suffix must be included or api. It then splits the remaining prefix on its first / into realProvider and realModel. Therefore real model IDs may contain /, while real provider IDs may not.

  • included reads the configured usage snapshot before delegation. A blocked route makes no upstream request.
  • api delegates unconditionally and never reads the snapshot.

Configuration

Create one global configuration file at:

<agentDir>/billing-provider/config.json

<agentDir> is Pi's agent directory (normally ~/.pi/agent). There is no project-local config search. models is an explicit allow-list: only these real provider/model pairs get virtual models. mode defaults to provider-wide; snapshotPath defaults to usage.json relative to <agentDir>/billing-provider/; stalenessMs defaults to 300000; and log defaults to true.

Provider-wide mode

Use provider-wide mode when one included allowance applies to every configured model:

{
  "mode": "provider-wide",
  "snapshotPath": "usage.json",
  "stalenessMs": 300000,
  "log": true,
  "models": [
    {
      "provider": "openai-codex",
      "model": "gpt-5.6-sol",
      "name": "GPT-5.6 Sol"
    },
    {
      "provider": "openai-codex",
      "model": "gpt-5.6-mini",
      "name": "GPT-5.6 Mini",
      "variants": ["included", "api"]
    }
  ]
}

variants is optional and defaults to ["included", "api"]. Set it to ["api"] to expose only the always-delegating API route for one model.

Tiered mode

Use tiered mode when availability differs by a flat named tier. Every configured model is matched against every tier pattern:

{
  "mode": "tiered",
  "snapshotPath": "usage.json",
  "stalenessMs": 300000,
  "log": true,
  "models": [
    {
      "provider": "openai-codex",
      "model": "gpt-5.6-sol",
      "name": "GPT-5.6 Sol"
    },
    {
      "provider": "openai-codex",
      "model": "gpt-5.6-mini",
      "name": "GPT-5.6 Mini"
    },
    {
      "provider": "anthropic",
      "model": "haiku-4.5",
      "name": "Claude Haiku 4.5"
    }
  ],
  "tiers": {
    "premium": ["gpt-5.6-sol"],
    "standard": ["gpt-5.6-mini", "anthropic/haiku-*"]
  }
}

A pattern containing / matches <realProvider>/<realModel>. A pattern without / matches only the bare <realModel>. A model matching no tier has its included route blocked; a model matching multiple tiers is a configuration error. api routes are unaffected in both cases.

Glob semantics

Tier patterns are case-sensitive, whole-string matches. They are flat strings rather than filesystem paths: * can cross /. Supported syntax is:

  • * — zero or more characters.
  • ? — exactly one character.
  • [abc] and [a-z] — character classes and ranges.
  • [!abc] and [^abc] — negated character classes.

All other regular-expression metacharacters are literal. An unterminated [ is a literal [. There is no path-segment behavior, brace expansion, extglob, or glob negation.

Snapshot and blocking behavior

A compatible statusline, job, or other writer supplies the local snapshot. See doc/snapshot-format.md for its schema and the exported atomic writer.

The included route fails closed: a missing, invalid, stale, exhausted, wrong-scope, or tier-missing snapshot blocks it without contacting the upstream API. In tiered mode, missing or stale data blocks only its assigned tier; file-level problems block every included route. The returned blocked error has a uniform message for a given real model:

[billing] included allowance unavailable for <realProvider>/<realModel>

The reason, tier, and snapshot-age detail are log-only. When a fresh snapshot confirms an exhausted allowance and supplies a future resetAt, the error also carries an optional generic routingHint: its exact candidates list contains all affected configured included routes, and until is that reset timestamp. Compatible routing engines can cool those exact routes until reset; otherwise the error remains an ordinary failed provider candidate. Tier conflicts block the affected included route; invalid variant lists are narrowed to the safe api route; and invalid model entries are omitted from the catalog. Other configuration errors are logged and surfaced once per session.

Status and logging

Run this Pi command to inspect the selected mode, resolved snapshot path, the current verdict for every registered included model, and each tier's observation age and reset countdown (or the provider-wide included allowance's age and reset), using updatedAt and resetAt from the snapshot:

/billing-provider-status

By default, operational events are appended best-effort to:

<agentDir>/billing-provider/billing-provider.log

Set "log": false to suppress ordinary lines. Configuration errors are always logged. The log includes block reasons and other diagnostic detail; successful API and successful included requests are not logged individually.

Cost handling

Virtual included models report zero cost. Virtual api models inherit the real model's cost metadata. This affects catalog/reporting metadata only; the routes still delegate to the same real provider and model when allowed.

pi-provider-manager failover example

With @jiajun0413/pi-provider-manager installed, use two ordered candidate tiers to prefer the included route and use API billing only after it fails or is unavailable. Put this in ~/.pi/agent/roundrobin/config.json:

{
  "candidates": [
    [
      {
        "provider": "billing",
        "model": "openai-codex/gpt-5.6-sol/included"
      }
    ],
    [
      {
        "provider": "billing",
        "model": "openai-codex/gpt-5.6-sol/api"
      }
    ]
  ],
  "strategy": "round-robin",
  "sticky": false
}

The outer arrays are priority tiers. A blocked included candidate fails normally, so pi-provider-manager can advance to the variant-last api candidate without parsing quota data.