@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.
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.
Reports 0
No reports yet.
Readme
piagent-realtime-provider-cost
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
- Installation
- Setup
- Colours
- Commands
- Configuration
- How it works
- Background: where the data comes from
- Dependencies
- Changelog
- License
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:
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 pricebase colour Thresholds via
deviationThresholdsor/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 styleadds SGR attributes to the icon:plain(default),bold,reverse.Base/switch colour –
color(defaultwhite) for everything else;switchColor(defaultbold:#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
lookupUpstreamProvideris on. Every other model keeps Pi's catalogue value. sessionCostBasisdecides what a BYOK call counts with:upstream(default) is what your provider bills for your own key,openroutercounts the$0OpenRouter 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 withallow_fallbacks: false- otherwise another provider may serve even a singleonlyentry. - 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) andendpoint-pricing.json(provider price lists, 24 h)./provider-cost lookup refreshclears 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_endonly, 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 bypatchSessionCost(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 -pincluded).
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