omp packages

@floez-werk/piagent-realtime-provider-cost0.14.1

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.

GitHub Actions Published 6 days ago MIT ~1.1K downloads/mo

Untested on omp

This is a pi package. No one has reported whether it works on omp yet. If you try it, add your result below.

Did it work for you?

Result

One report per person. A new report replaces your old one.

Reports 0

No reports yet.

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. With the default patchSessionCost the sum itself is corrected to the real billed amount, see Session cost.

Only OpenRouter is supported today. Real billed rates and the serving-provider tag come from the OpenRouter response itself (chunk metadata); 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 provider inside OpenRouter that served the call is appended as a tag (e.g. Fir for Fireworks).

↑$2/↓$12 (Fir)    # arrows (Unicode, full size in every font), tag = Fireworks
↑$2/↓$12 (Fir🔑)  # BYOK: the call ran on your own provider key (`*` in ASCII mode)
in:$2/out:$12     # ASCII mode (icons: ascii)
↑$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 response metadata; 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 response (usage.cost, BYOK: upstream_inference_cost), catalogue rates only as fallback/preview ✅
Any other provider ⚠️ Fallback only Pi catalogue (usage.cost.*) – no real billed rate ❌
Subscription-backed (OAuth, kimi-coding) ⛔ Hidden – –

Adding another provider requires the same metadata in its stream chunks plus per-provider 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

Two layers, set via /provider-cost or the settings:

  1. Deviation colouring – the effective rate is compared with the model's catalogue price (models-store.json). The icons carry the colour (in and out separately); the numbers and the provider tag stay in the base colour, which keeps them readable on dark backgrounds.

    Deviation from catalogue price Colour
    more than green % cheaper (default > 10 %) green
    up to yellow % more expensive (default ≤ 10 %) yellow
    up to orange % more expensive (default ≤ 20 %) orange (256-colour 208)
    above orange % red
    0 %, less than green % cheaper, or no catalogue price base colour

    Thresholds via deviationThresholds or /provider-cost threshold <green|yellow|orange> <pct>. Compared at the displayed precision (4 decimals in the display currency), so a rate that renders like the catalogue price keeps the base colour – that swallows the binary-float noise of the invoice-derived rates. deviationStyle / /provider-cost style adds SGR attributes to the icon: plain (default), bold, reverse.

  2. Base/switch colour – color (default white) for everything else; switchColor (default bold:#ffd700) paints the whole item when a provider switch is detected, for one prompt, overriding the deviation colours.

Specs: palette (white, yellow, orange, red, green, cyan, magenta, blue, gray, none), hex (#ffd700, #fd0), 256-colour (226), with attributes (bold:…, reverse:…, combinable: bold:reverse:red). These are ANSI specs, not CSS or Pi/Powerline theme names (warning, error, …): the extension colours its own text so the switch can be dynamic – inside pi-powerline-footer that needs selfColorize: true (see Setup).

Commands

Command Effect
/provider-cost or /provider-cost status State, currency, icons, colours, lookup state, display, tag/source, cache entry
/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 of the last call
/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 session <on|off|toggle> Correct the session cost sum with the real billed amounts (persisted, default on)
/provider-cost session basis <upstream|openrouter> Which amount BYOK calls count with (persisted, default upstream)

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,
    "patchSessionCost": true,
    "sessionCostBasis": "upstream"
  }
}
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, ascii – nerd/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 Resolve the serving provider and the real prices (response stream, routing constraint, cache, generation API for a restored session). off = catalogue prices, no provider tag, no session-cost correction
patchSessionCost true Replace the catalogue cost of each OpenRouter call with the real billed amount, so Pi's session cost sum matches the actual spend (see Session cost)
sessionCostBasis "upstream" Amount a BYOK call counts with: upstream (what your provider bills) or openrouter (what OpenRouter charged, 0 for BYOK)

Icons

icons Arrows
auto (default) ↑/↓ when the terminal looks Nerd-Font capable, otherwise in:/out:
nerd ↑/↓ always
ascii in:/out: always

auto uses Powerline's heuristic: GHOSTTY_RESOURCES_DIR set, or TERM_PROGRAM/TERM ∈ iterm, wezterm, kitty, ghostty, alacritty, kaku. Many terminals only set TERM=xterm-256color and therefore get ASCII - pick nerd explicitly in that case.

The BYOK marker follows the same mode: 🔑 next to the arrows, * in ASCII mode (see How it works).

Set the mode with the icons setting or /provider-cost icons <mode>. The env var wins for a single run: PROVIDER_COST_NERD_FONTS=1 forces nerd, =0 forces ascii.

Plain arrows instead of Nerd Font glyphs: private-use icons (U+F090/U+F08B) are rendered noticeably smaller by most fonts.

Rounding

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

Session cost

Pi's session cost sum (footer, /session, /cost, cache notices, HTML export) and pi-powerline-footer's cost segment add up usage.cost.total of every session entry - computed from the catalogue prices, so they ignore the provider OpenRouter actually routed to. With patchSessionCost (default on) this extension replaces that amount with the real billed one, so those sums show the actual spend instead of the catalogue estimate. The token buckets of the entry are scaled along, which keeps the derived cache statistics coherent; the in/out split of the extension's own item still comes from the provider-side invoice.

  • Applies to OpenRouter calls only, and only while lookupUpstreamProvider is on. Every other model keeps Pi's catalogue value.
  • sessionCostBasis decides what a BYOK call counts with: upstream (default) is what your provider bills for your own key, openrouter counts the $0 OpenRouter charged.
  • On a credits call the sum is what OpenRouter charged, which can be below the provider-side invoice the item's in/out rates are derived from - each figure is correct in its own right (the sum is your spend, the rates are the provider's invoice).
  • Always USD, like Pi's own sums - the display currency of the item is a separate conversion.
  • Takes effect for the calls that happen while it is on: Pi computes and stores the cost when a message is finalized, so already persisted turns and the restored last call of a resumed session keep the catalogue value.

Switch it with patchSessionCost / /provider-cost session on|off|toggle and the basis with /provider-cost session basis <upstream|openrouter>.

How it works

The numbers come from the response itself, not from a price list: OpenRouter puts the serving provider into every stream chunk and the billed amount with its prompt/completion split into the final usage chunk. Pi drops both while normalizing - provider_stream_event (pi 0.99+) hands them over before that, so provider and prices are known per response, without a request. The same amount corrects Pi's session cost sum (message_end replaces the finalized message before it is persisted), see Session cost.

flowchart TD
  A[Response] -->|"stream data"| Z["Display: provider tag + real rates<br/>per response, no request"]
  A -->|"no stream data (restored session)"| C["Routing constraint<br/>only + allow_fallbacks: false"]
  C --> D["Provider cache<br/>last known provider/rates"]
  D -->|"complete entry"| Z
  D -->|"nothing to answer"| E["Generation API<br/>one attempt, restore only"]
  E --> Z

Rates from the invoice. The billed amount arrives split into its prompt and completion part (usage.cost_details.*), so the rates are exact:

out-rate = completions_cost / completion_tokens            (USD per 1M tokens)
in-rate  = prompt_cost / (in + wWrite*cacheWrite + wRead*cacheRead)

prompt_cost covers every prompt token, so cached tokens are weighted by their price relation to the input price (wRead/wWrite, from the endpoint prices). Without cached prompt tokens nothing has to be looked up:

sequenceDiagram
  participant S as Provider stream
  participant E as Extension
  participant O as OpenRouter
  S->>E: provider, billed amount, split (final chunk)
  E->>E: switch decision and rates, synchronous
  opt cached prompt tokens
    E->>O: endpoint prices (ratios only)
  end

Without such a split (restore, generation API) the billed total is split by the provider's endpoint prices instead: factor = billed / modelled, rate = price * factor. Those prices are used at the long-context tier that applies to the call (pricing.overrides), and every billed bucket counts - including the cached prompt tokens, which OpenRouter reports outside input.

Details

  • BYOK (request served through your own provider key): OpenRouter charges nothing and the provider invoices you directly, so the upstream amount is used - never $0/$0. Such a call is marked inside the provider tag ((Fir🔑), * in ASCII mode); the flag is stored in the cache, so the marker survives a session restore.
  • Routing constraint (models.json → providers.openrouter.modelOverrides.<model>.compat.openRouterRouting.only): counts as certain only with allow_fallbacks: false - otherwise another provider may serve even a single only entry.
  • Generation API (/api/v1/generation?id=<responseId>): restore-only - one attempt, only when the cache has nothing to answer with (it covers one response at a time). Live calls, model switches and aborted streams never trigger it; a failed lookup keeps the last known values.
  • Caches: provider-cache.json (provider + rates per model; every response overwrites its entry, so nothing expires) and endpoint-pricing.json (provider price lists, 24 h). /provider-cost lookup refresh clears both.
  • Model switch: the catalogue prices of the new model appear immediately with a ? tag until its first response replaces them.
  • Update timing: the item changes on message_end only, when the final chunk has been seen - while streaming the last known value stays.
  • Not computable (e.g. usage.input == 0, missing exchange rate) → ? per side; free models → $0/$0; subscription providers → item hidden.
  • Currency mirrors pi-powerline-footer (same source, 24 h cache, own file); the session cost sum next to it is corrected to the real billed amount by patchSessionCost (see Session cost), otherwise it stays Pi's own catalogue value.
  • Session cost patch: the finalized message is replaced on message_end, which Pi applies in place before persisting the entry and before its own listeners run - so footer, /session, /cost, the session file and pi-powerline-footer all see the real amount.
  • Background work: a resolution runs in the background and stops rendering once the session is gone, so it can never affect a run (pi -p included).

Background: where the data comes from

OpenRouter delivers the provider that served the request in the stream chunk as a provider field and the amount it charged in the final usage chunk. Pi (pi-ai) drops both while normalizing: it reads only chunk.id and chunk.model from a chunk, and computes costs from its own 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 either (X-Provider-Name is only listed as "exposed" but is not sent).

Since pi 0.99 the raw chunk is reachable before normalization through the provider_stream_event extension event, which is what this extension uses. The generation API is kept for the one case without stream data - the last call of a restored session; it needs a separate request, is published with a delay of up to a minute and returns the same provider and 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.14.1 - 2026-10-13

Fixed

  • Claude models routed through OpenRouter (Anthropic message API) now show their serving provider and real prices, instead of no provider tag.

Full history and all versions: CHANGELOG.md

License

MIT – see LICENSE.