@floez-werk/piagent-realtime-provider-cost

Pi extension: shows the effective token prices (input/output, per 1M tokens) of the provider/model of the last API call in the status bar.

Packages

Package details

extension

Install @floez-werk/piagent-realtime-provider-cost from npm and Pi will load the resources declared by the package manifest.

$ pi install npm:@floez-werk/piagent-realtime-provider-cost
Package
@floez-werk/piagent-realtime-provider-cost
Version
0.12.1
Published
Sep 20, 2026
Downloads
511/mo · 511/wk
Author
floezwerk
License
MIT
Types
extension
Size
108.8 KB
Dependencies
0 dependencies · 3 peers
Pi manifest JSON
{
  "extensions": [
    "./extensions"
  ]
}

Security note

Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.

README

piagent-realtime-provider-cost

npm license CI changelog

Shows the effective token prices (input/output, per 1M tokens) of the last API call in the Pi status bar – right next to the session cost sum.

Only OpenRouter is supported today. Real billed rates and the serving-provider tag are resolved exclusively through OpenRouter's generation API – other providers are not queried. See Supported providers.

Unlike the core footer's cost sum, these are the rates actually billed by OpenRouter (including provider routing, discounts and peak overrides), not the catalogue prices from models-store.json. For OpenRouter models the serving provider is appended as a tag.

↑$2/↓$12 (Fir)    # arrows (Unicode, full size in every font), tag = Fireworks
in:$2/out:$12     # ASCII mode (icons: ascii)
↑$2/↓$12 (⟳)      # provider/costs are currently resolved via the generation API
↑$1.5/↓$6 (?)     # model just switched: catalogue prices, provider not known yet

Colour: the in/out icons are coloured by their deviation from the catalogue price (green/yellow/orange/red, see Colours); the numbers and the provider tag stay in the base colour (white). When a provider switch is detected, the whole item is drawn bold gold (bold:#ffd700) for one prompt – the deviation colours do not apply then.

All values are per 1M tokens in the configured currency.

Contents

Supported providers

Currently only OpenRouter is fully supported. The real billed rate and the serving-provider tag come from OpenRouter's generation API; no other provider is queried. On every other provider the item still renders, but it falls back to Pi's catalogue computation (usage.cost.*) and shows no provider tag.

Provider Status Billed rate Serving-provider tag
OpenRouter (openrouter) ✅ Full support Real amount from the generation API (total_cost), catalogue rates only as fallback/preview
Any other provider ⚠️ Fallback only Pi catalogue (usage.cost.*) – no real billed rate, no generation-API call
Subscription-backed (OAuth, kimi-coding) ⛔ Hidden

Adding another provider requires a billing/provider source equivalent to OpenRouter's generation API plus the endpoint price lists; see How it works.

Installation

From npm (scoped as @floez-werk):

pi install npm:@floez-werk/piagent-realtime-provider-cost

Or directly from the repository:

pi install git:git@github.com:FloezWerk/piagent-realtime-provider-cost.git

This repository is developed against a local Gitea instance and mirrored to the public GitHub repository FloezWerk/piagent-realtime-provider-cost. Installation instructions always reference the GitHub URL – the Gitea path is internal and must not appear in user-facing docs.

Local/development:

pi -e ./extensions/realtime-provider-cost.ts

After changes in a running session: /reload.

Afterwards the extension is active immediately – without further configuration the value appears in the footer next to the cost sum. If you use the Powerline bar, the item should additionally be hooked in there (next section).

Setup

Without pi-powerline-footer

Works standalone: ctx.ui.setStatus is a core API, the core footer shows the value as its own line below the status line.

With pi-powerline-footer (recommended)

Add a custom item that reads the realtime-provider-cost status channel:

{
  "powerline": {
    "preset": "default",
    "customItems": [
      {
        "id": "provider-cost",
        "statusKey": "realtime-provider-cost",
        "position": "right",
        "color": "warning",
        "selfColorize": true,
        "hideWhenMissing": true,
        "excludeFromExtensionStatuses": true
      }
    ]
  }
}

Explicit positioning right next to cost via powerline.layout:

{
  "powerline": {
    "layout": {
      "left": ["model", "thinking", "shell_mode", "path", "git", "queue", "context_pct", "cache_read", "cost", "custom:provider-cost"]
    },
    "customItems": [
      { "id": "provider-cost", "statusKey": "realtime-provider-cost", "selfColorize": true }
    ]
  }
}

Important: set selfColorize: true. Otherwise Powerline strips the item's ANSI colour codes and colours it itself – the dynamic white/gold switch on a provider change would be lost.

Colours

There are two layers:

  1. Deviation colour coding. The effective rate is compared with the model's catalogue price (models-store.json); the colour shows the deviation. What gets coloured are the icons/arrows (in and out separately); the numbers themselves stay in the base colour so they remain readable on dark backgrounds. The provider tag is also in the base colour.

    Thresholds are configurable (setting deviationThresholds, command /provider-cost threshold …); the percentages below are the defaults:

    Deviation from catalogue price Colour
    more than green % cheaper (< −10 %) green
    up to yellow % more expensive (0 % < x ≤ 10 %) yellow
    yelloworange % more expensive (> 10 % and ≤ 20 %) orange (256-colour 208)
    more than orange % more expensive (> 20 %) red
    otherwise (0 %, up to green % cheaper, or no catalogue price known) base colour

    The comparison uses the displayed precision: both rates are rounded to the 4 decimals the status line shows (in the display currency) before the deviation is computed. A rate that renders identically to the catalogue price therefore keeps the base colour - this swallows the binary-float noise of the invoice-derived rates (e.g. an effective 0.20000000000000004 versus the catalogue 0.2, both shown as $0.2). A difference must be visible in the numbers to get a colour.

    In and out are coloured individually (e.g. input arrow green, output arrow red). If there is no effective price or no catalogue price (e.g. ?), the base colour stays.

    Example: green $2 / red $12 (numbers white).

    Readability. A terminal cannot draw an outline/stroke around glyphs (pure font rendering). Therefore the icon carries the deviation colour and the number stays neutral. deviationStyle controls the icon's SGR attributes:

    deviationStyle Effect
    plain (default) plain foreground colour on the arrow
    bold arrow bolder/brighter (SGR 1)
    reverse arrow as a coloured block (SGR 7)

    Set via /provider-cost style <plain|bold|reverse> or setting deviationStyle.

  2. Base/switch colour for everything else. White by default; when a provider switch is detected the whole item is drawn bold gold (bold:#ffd700) for one prompt and overrides the deviation colours. Set via /provider-cost color … / /provider-cost switchColor … or the settings color / switchColor.

Colour specs are freely choosable:

Syntax Example Result
Palette white, yellow, orange, red, green, cyan, magenta, blue, gray, none SGR 97/93/38;5;208/91/92/96/95/94/90
Hex (truecolor) #ffd700, #fd0 38;2;r;g;b
256-colour 226 (0-255) 38;5;n
Bold bold:yellow, bold:#ffd700, bold:226 1;<colour>
Reverse reverse:red, reverse:orange 7;<colour> (colour becomes the background)
Combined bold:reverse:red 1;7;<colour>

These are not CSS names and not theme names (warning, error, …) from the Pi/Powerline theme world – the extension colours in ANSI itself so it can switch dynamically (see selfColorize above).

Common alternatives for the switch highlight:

/provider-cost switchColor bold:#ffd700   # default: bold gold (truecolor)
/provider-cost switchColor bold:220       # gold, 256-colour (available everywhere)
/provider-cost switchColor bold:226       # pure yellow, 256-colour
/provider-cost switchColor bold:yellow    # bold bright yellow
/provider-cost color none                 # no colouring at all

Commands

Command Effect
/provider-cost or /provider-cost status State, currency, icons, colours, lookup+refresh interval, display, tag/source, cache age in prompts
/provider-cost on Enable the display (persisted)
/provider-cost off Disable the display (persisted)
/provider-cost toggle Toggle (persisted)
/provider-cost refresh Reload exchange rates and re-resolve provider/costs via the generation API (if possible)
/provider-cost currency <CODE> Set the display currency (persisted)
/provider-cost icons <auto|nerd|ascii> Set the icon mode (persisted)
/provider-cost color <spec> Set the base colour, e.g. white, #ffd700, 226, bold:yellow (persisted, see Colours)
/provider-cost switchColor <spec> Set the switch colour (provider change) (persisted, see Colours)
/provider-cost style <plain|bold|reverse> Attributes of the deviation colour on the in/out icon (persisted; default plain)
/provider-cost threshold <green|yellow|orange> <pct> Set a deviation threshold in percent (persisted; defaults 10/10/20)
/provider-cost lookup <on|off|refresh> Provider resolution on/off; refresh clears the provider and pricing cache and re-resolves
/provider-cost notify <on|off> Notification for every automatic generation-API request (persisted; default off)

Configuration

In ~/.pi/agent/settings.json under the root key realtime-provider-cost (matching the extension name). Other keys are left untouched; every value can also be set via a command.

{
  "realtime-provider-cost": {
    "enabled": true,
    "currency": "EUR",
    "icons": "nerd",
    "color": "white",
    "switchColor": "bold:#ffd700",
    "deviationStyle": "plain",
    "deviationThresholds": { "green": 10, "yellow": 10, "orange": 20 },
    "lookupUpstreamProvider": true,
    "providerCacheRefreshPrompts": 10,
    "notifyGenerationLookup": false
  }
}
Field Default Description
enabled true Display on/off
currency "USD" USD, CNY, EUR, GBP, JPY, CAD, AUD, CHF, INR, KRW
icons "auto" auto (terminal heuristic), nerd, asciinerd/auto use /, ascii uses in:/out:
color "white" Base colour: palette name, #rrggbb or 0-255, optionally with bold:
switchColor "bold:#ffd700" Colour right after a detected provider change
deviationStyle "plain" SGR attributes of the deviation colour on the icon: plain, bold, reverse
deviationThresholds {green:10, yellow:10, orange:20} Percentage thresholds: below -green green, up to yellow yellow, up to orange orange, above red (all ≥ 0)
lookupUpstreamProvider true Provider/cost resolution active (routing constraint + cache + generation API)
providerCacheRefreshPrompts 10 After this many prompts (user turns) on the same model a generation cache entry is refreshed; 0 = always re-resolve
notifyGenerationLookup false Notify before every automatic generation-API request (with reason)

Icons

  1. Env PROVIDER_COST_NERD_FONTS=1 (nerd) / =0 (ascii)
  2. Config icons
  3. auto: heuristic like Powerline (GHOSTTY_RESOURCES_DIR or TERM_PROGRAM/TERM ∈ iterm, wezterm, kitty, ghostty, alacritty, kaku)

Many terminals only set TERM=xterm-256colorauto yields ASCII (in:/out:); for icons use icons: "nerd" or /provider-cost icons nerd.

The previously used Nerd Font arrows (U+F090/U+F08B) were replaced by / because private-use glyphs are rendered noticeably smaller.

Rounding

Rounded to at most 4 decimal places, trailing zeros removed ($2, $12.5, $0.2896).

How it works

  • Real billing instead of catalogue price. The source of the numbers is OpenRouter: the generation API returns total_cost (actually charged) and the token counts, and the endpoint prices provide the bucket ratio (input/output/cache) of the provider that actually served the request:

    modelled = prompt*in + completion*out
             + cacheWrite*cacheWriteTokens + cacheRead*cacheReadTokens   (from endpoint prices)
    factor   = total_cost / modelled          # discounts, peak overrides, price changes
    in-rate  = prompt * factor                (USD per 1M tokens)
    out-rate = completion * factor
    

    The factor makes the display match the invoice even when endpoint prices do not (yet) exactly match the billed rate. As long as no API data is available, the approximation from usage.cost.* (Pi catalogue) is shown.

    All billed buckets count towards modelled, including the cached prompt tokens: OpenRouter reports the uncached prompt part as input, so a call whose prompt is mostly a cache write would otherwise produce a factor far above 1 (and therefore absurdly high displayed rates).

  • Resolution. Order:

    1. Routing constraint from models.json (providers.openrouter.modelOverrides.<model>.compat.openRouterRouting.only) – only counts as certain when allow_fallbacks: false is set. With allow_fallbacks: true (OpenRouter default) another provider may serve even if only names exactly one.
    2. Persistent rate cache (~/.pi/agent/realtime-provider-cost/provider-cache.json), key = request model. Entries expire after providerCacheRefreshPrompts prompts on the same model (not by time), so activity on other models does not age them. The file also stores, per model, the prompt counter of the last generation-API attempt (any outcome).
    3. Generation API GET https://openrouter.ai/api/v1/generation?id=<responseId> – returns the provider and the real amount, at most 1 call per model at a time. Data is only available a few seconds after the call → retry with backoff (1s/2s/4s/8s). Meanwhile an "update in progress" icon () is shown instead of the provider tag.

    When is the API called?

    • Provider certain (one only entry and allow_fallbacks: false) → once on the first call, afterwards only every N prompts on that model (rate cache).
    • Provider not certain (allow_fallbacks: true or no only) → also once per cache window: the cached entry covers the whole window, because it reflects the provider that actually served the request.
    • Free models (:free, zero endpoint prices) → same as above. No per-token rate can be derived from a zero invoice, so the entry is cached without rates and stays valid for the full window instead of forcing a request per prompt.
    • Model switch (different request model than the previous call) → forced refresh of provider and costs, even if the cache would still be fresh. A session restore with the same model is not a switch.
    • Failed lookup (endpoint returns nothing: 404 right after the call, timeout, ...) → the attempt re-arms the cache window too, so a request that yielded no result is retried at most once per window, not once per prompt. The last known entry stays in use meanwhile.

    Notification. (Optional, default off: setting notifyGenerationLookup or /provider-cost notify on.) Every automatically triggered generation-API request is reported as a notify, including the reason in parentheses, e.g. Generation API: resolving provider/costs for deepseek/… (cache miss). Reasons: cache miss, cache expired (N prompts), cache without rates, stale cache, model switch, manual refresh (/provider-cost refresh), cache cleared (/provider-cost lookup refresh).

  • Preview on model switch. When the model is switched (model_select), the catalogue prices (models-store.json) of the new model are shown immediately. The serving provider is not known at that point yet (the first call of the new model is still running) → the tag shows ?. As soon as the first response arrives, the real value (including provider tag, or while resolving) replaces the preview. Tiered pricing is not applied in the preview – without token counts only the base rates are known.

    • /provider-cost refresh → reloads the exchange rates and forces a generation-API call (provider + costs) when possible (OpenRouter, lookupUpstreamProvider active, responseId present, no lookup already running). Clear the cache first with /provider-cost lookup refresh.

    The prompt counters are persisted per model in the cache and survive restarts. Example with providerCacheRefreshPrompts: 10: resolution on the 1st prompt of a model, then again after the 10th further prompt on that model.

  • Invalidation by call counter: there is no signal that reveals a provider switch per response (model slug, system_fingerprint, native_finish_reason, service_tier are provider-independent). With a certain provider it is stable in the short term, so resolving every N prompts suffices; otherwise it is queried per response.

  • Two caches: provider-cache.json (provider + rates per model) and endpoint-pricing.json (provider price lists, 24 h). /provider-cost lookup refresh clears both.

  • No batch endpoint: OpenRouter offers neither multiple IDs nor a generations list; /api/v1/activity requires a management key.

  • While streaming the last known value stays; it is only updated on message_end.

  • Failover: if one side is not computable (e.g. usage.input == 0) or the conversion rate is missing, ? is shown per side.

  • Subscription providers (OAuth or kimi-coding) → the item is hidden.

  • Free models are shown as $0/$0.

  • Currency conversion mirrors pi-powerline-footer (same source, 24h cache, own file …/realtime-provider-cost/currency-rates.json); independent of Powerline.

  • No interference with Pi: the extension replaces neither the footer nor the cost calculation; the session cost sum next to it remains Pi's catalogue value.

Background: why the generation API detour?

OpenRouter delivers the serving provider in the stream chunk as a provider field, but Pi (pi-ai) discards it (it only reads chunk.id and chunk.model). Pi likewise discards the actual cost amount (usage.cost) and computes costs from the price table (models-store.json) – which reflects the model base price, not the price of the routed provider (example deepseek/deepseek-v4.1-flash via Fireworks: catalogue 0.15/0.60 vs. real 0.22/0.66). No provider header exists (X-Provider-Name is only listed as "exposed" but is not sent). The generation API is therefore the only reliable source for provider and billed amount.

Dependencies

@earendil-works/pi-ai, @earendil-works/pi-coding-agent and @earendil-works/pi-tui are bundled by Pi and are therefore only declared as optional peerDependencies. No runtime dependencies on other extensions.

Changelog

User-facing changes per version are tracked in CHANGELOG.md (Keep a Changelog format).

0.12.1 - 2026-09-20

Added

  • The README now states up front that only OpenRouter is supported, with a Supported providers table that lists the current status per provider (OpenRouter: full support; other providers: catalogue fallback without provider tag; subscriptions: hidden).

Full history and all versions: CHANGELOG.md

License

MIT – see LICENSE.