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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 FloezWerk
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,374 @@
1
+ # piagent-realtime-provider-cost
2
+
3
+ Shows the **effective token prices (input/output, per 1M tokens)** of the
4
+ **last API call** in the Pi status bar – right next to the session cost sum.
5
+
6
+ Unlike the core footer's cost sum, these are the rates **actually billed by
7
+ OpenRouter** (including provider routing, discounts and peak overrides), not the
8
+ catalogue prices from `models-store.json`. For OpenRouter models the **serving
9
+ provider** is appended as a tag.
10
+
11
+ ```
12
+ ↑$2/↓$12 (Fir) # arrows (Unicode, full size in every font), tag = Fireworks
13
+ in:$2/out:$12 # ASCII mode (icons: ascii)
14
+ ↑$2/↓$12 (⟳) # provider/costs are currently resolved via the generation API
15
+ ↑$1.5/↓$6 (?) # model just switched: catalogue prices, provider not known yet
16
+ ```
17
+
18
+ **Colour:** the in/out **icons** are **coloured by their deviation from the
19
+ catalogue price** (green/yellow/orange/red, see [Colours](#colours)); the numbers
20
+ and the provider tag stay in the base colour (**white**). When a **provider
21
+ switch** is detected, the **whole** item is drawn **bold gold** (`bold:#ffd700`)
22
+ for one prompt – the deviation colours do not apply then.
23
+
24
+ All values are **per 1M tokens** in the configured currency.
25
+
26
+ ## Contents
27
+
28
+ - [Installation](#installation)
29
+ - [Setup](#setup)
30
+ - [Without pi-powerline-footer](#without-pi-powerline-footer)
31
+ - [With pi-powerline-footer (recommended)](#with-pi-powerline-footer-recommended)
32
+ - [Colours](#colours)
33
+ - [Commands](#commands)
34
+ - [Configuration](#configuration)
35
+ - [Icons](#icons)
36
+ - [Rounding](#rounding)
37
+ - [How it works](#how-it-works)
38
+ - [Background: why the generation API detour?](#background-why-the-generation-api-detour)
39
+ - [Dependencies](#dependencies)
40
+ - [License](#license)
41
+
42
+ ## Installation
43
+
44
+ From npm (scoped as `@floez-werk`):
45
+
46
+ ```bash
47
+ pi install npm:@floez-werk/piagent-realtime-provider-cost
48
+ ```
49
+
50
+ Or directly from the repository:
51
+
52
+ ```bash
53
+ pi install git:git@github.com:FloezWerk/piagent-realtime-provider-cost.git
54
+ ```
55
+
56
+ > This repository is developed against a local Gitea instance and mirrored to
57
+ > the public GitHub repository `FloezWerk/piagent-realtime-provider-cost`.
58
+ > Installation instructions always reference the **GitHub** URL – the Gitea path
59
+ > is internal and must not appear in user-facing docs.
60
+
61
+ Local/development:
62
+
63
+ ```bash
64
+ pi -e ./extensions/realtime-provider-cost.ts
65
+ ```
66
+
67
+ After changes in a running session: `/reload`.
68
+
69
+ Afterwards the extension is active immediately – **without further
70
+ configuration** the value appears in the footer next to the cost sum. If you use
71
+ the Powerline bar, the item should additionally be hooked in there (next
72
+ section).
73
+
74
+ ## Setup
75
+
76
+ ### Without pi-powerline-footer
77
+
78
+ Works standalone: `ctx.ui.setStatus` is a core API, the core footer shows the
79
+ value as its own line below the status line.
80
+
81
+ ### With pi-powerline-footer (recommended)
82
+
83
+ Add a custom item that reads the `realtime-provider-cost` status channel:
84
+
85
+ ```jsonc
86
+ {
87
+ "powerline": {
88
+ "preset": "default",
89
+ "customItems": [
90
+ {
91
+ "id": "provider-cost",
92
+ "statusKey": "realtime-provider-cost",
93
+ "position": "right",
94
+ "color": "warning",
95
+ "selfColorize": true,
96
+ "hideWhenMissing": true,
97
+ "excludeFromExtensionStatuses": true
98
+ }
99
+ ]
100
+ }
101
+ }
102
+ ```
103
+
104
+ Explicit positioning right next to `cost` via `powerline.layout`:
105
+
106
+ ```jsonc
107
+ {
108
+ "powerline": {
109
+ "layout": {
110
+ "left": ["model", "thinking", "shell_mode", "path", "git", "queue", "context_pct", "cache_read", "cost", "custom:provider-cost"]
111
+ },
112
+ "customItems": [
113
+ { "id": "provider-cost", "statusKey": "realtime-provider-cost", "selfColorize": true }
114
+ ]
115
+ }
116
+ }
117
+ ```
118
+
119
+ > **Important:** set `selfColorize: true`. Otherwise Powerline strips the item's
120
+ > ANSI colour codes and colours it itself – the dynamic white/gold switch on a
121
+ > provider change would be lost.
122
+
123
+ ## Colours
124
+
125
+ There are two layers:
126
+
127
+ 1. **Deviation colour coding.** The effective rate is compared with the model's
128
+ **catalogue price** (`models-store.json`); the colour shows the deviation. What
129
+ gets coloured are the **icons/arrows** (in and out separately); the numbers
130
+ themselves stay in the base colour so they remain readable on dark backgrounds.
131
+ The provider tag is also in the base colour.
132
+
133
+ Thresholds are configurable (setting `deviationThresholds`, command
134
+ `/provider-cost threshold …`); the percentages below are the defaults:
135
+
136
+ | Deviation from catalogue price | Colour |
137
+ | --- | --- |
138
+ | more than `green` % **cheaper** (< −10 %) | **green** |
139
+ | up to `yellow` % more expensive (0 % < x ≤ 10 %) | **yellow** |
140
+ | `yellow`–`orange` % more expensive (> 10 % and ≤ 20 %) | **orange** (256-colour 208) |
141
+ | more than `orange` % more expensive (> 20 %) | **red** |
142
+ | otherwise (0 %, up to `green` % cheaper, or no catalogue price known) | base colour |
143
+
144
+ > In and out are coloured **individually** (e.g. input arrow green, output
145
+ > arrow red). If there is no effective price or no catalogue price (e.g. `?`),
146
+ > the base colour stays.
147
+
148
+ Example: `↑`green `$2` / `↓`red `$12` (numbers white).
149
+
150
+ **Readability.** A terminal cannot draw an outline/stroke around glyphs (pure
151
+ font rendering). Therefore the **icon** carries the deviation colour and the
152
+ number stays neutral. `deviationStyle` controls the icon's SGR attributes:
153
+
154
+ | `deviationStyle` | Effect |
155
+ | --- | --- |
156
+ | `plain` (default) | plain foreground colour on the arrow |
157
+ | `bold` | arrow bolder/brighter (`SGR 1`) |
158
+ | `reverse` | arrow as a coloured block (`SGR 7`) |
159
+
160
+ Set via `/provider-cost style <plain|bold|reverse>` or setting
161
+ `deviationStyle`.
162
+
163
+ 2. **Base/switch colour** for everything else. **White** by default; when a
164
+ **provider switch** is detected the **whole** item is drawn **bold gold**
165
+ (`bold:#ffd700`) for one prompt and overrides the deviation colours. Set via
166
+ `/provider-cost color …` / `/provider-cost switchColor …` or the settings
167
+ `color` / `switchColor`.
168
+
169
+ Colour specs are freely choosable:
170
+
171
+ | Syntax | Example | Result |
172
+ | --- | --- | --- |
173
+ | Palette | `white`, `yellow`, `orange`, `red`, `green`, `cyan`, `magenta`, `blue`, `gray`, `none` | SGR 97/93/38;5;208/91/92/96/95/94/90 |
174
+ | Hex (truecolor) | `#ffd700`, `#fd0` | `38;2;r;g;b` |
175
+ | 256-colour | `226` (0-255) | `38;5;n` |
176
+ | Bold | `bold:yellow`, `bold:#ffd700`, `bold:226` | `1;<colour>` |
177
+ | Reverse | `reverse:red`, `reverse:orange` | `7;<colour>` (colour becomes the background) |
178
+ | Combined | `bold:reverse:red` | `1;7;<colour>` |
179
+
180
+ These are **not** CSS names and **not** theme names (`warning`, `error`, …) from
181
+ the Pi/Powerline theme world – the extension colours in ANSI itself so it can
182
+ switch dynamically (see `selfColorize` above).
183
+
184
+ Common alternatives for the switch highlight:
185
+
186
+ ```bash
187
+ /provider-cost switchColor bold:#ffd700 # default: bold gold (truecolor)
188
+ /provider-cost switchColor bold:220 # gold, 256-colour (available everywhere)
189
+ /provider-cost switchColor bold:226 # pure yellow, 256-colour
190
+ /provider-cost switchColor bold:yellow # bold bright yellow
191
+ /provider-cost color none # no colouring at all
192
+ ```
193
+
194
+ ## Commands
195
+
196
+ | Command | Effect |
197
+ | --- | --- |
198
+ | `/provider-cost` or `/provider-cost status` | State, currency, icons, colours, lookup+refresh interval, display, tag/source, cache age in prompts |
199
+ | `/provider-cost on` | Enable the display (persisted) |
200
+ | `/provider-cost off` | Disable the display (persisted) |
201
+ | `/provider-cost toggle` | Toggle (persisted) |
202
+ | `/provider-cost refresh` | Reload exchange rates **and** re-resolve provider/costs via the generation API (if possible) |
203
+ | `/provider-cost currency <CODE>` | Set the display currency (persisted) |
204
+ | `/provider-cost icons <auto\|nerd\|ascii>` | Set the icon mode (persisted) |
205
+ | `/provider-cost color <spec>` | Set the base colour, e.g. `white`, `#ffd700`, `226`, `bold:yellow` (persisted, see [Colours](#colours)) |
206
+ | `/provider-cost switchColor <spec>` | Set the switch colour (provider change) (persisted, see [Colours](#colours)) |
207
+ | `/provider-cost style <plain\|bold\|reverse>` | Attributes of the deviation colour on the in/out icon (persisted; default `plain`) |
208
+ | `/provider-cost threshold <green\|yellow\|orange> <pct>` | Set a deviation threshold in percent (persisted; defaults 10/10/20) |
209
+ | `/provider-cost lookup <on\|off\|refresh>` | Provider resolution on/off; `refresh` clears the provider **and** pricing cache and re-resolves |
210
+ | `/provider-cost notify <on\|off>` | Notification for every automatic generation-API request (persisted; default `off`) |
211
+
212
+ ## Configuration
213
+
214
+ In `~/.pi/agent/settings.json` under the root key `realtime-provider-cost`
215
+ (matching the extension name). Other keys are left untouched; every value can
216
+ also be set via a command.
217
+
218
+ ```jsonc
219
+ {
220
+ "realtime-provider-cost": {
221
+ "enabled": true,
222
+ "currency": "EUR",
223
+ "icons": "nerd",
224
+ "color": "white",
225
+ "switchColor": "bold:#ffd700",
226
+ "deviationStyle": "plain",
227
+ "deviationThresholds": { "green": 10, "yellow": 10, "orange": 20 },
228
+ "lookupUpstreamProvider": true,
229
+ "providerCacheRefreshPrompts": 10,
230
+ "notifyGenerationLookup": false
231
+ }
232
+ }
233
+ ```
234
+
235
+ | Field | Default | Description |
236
+ | --- | --- | --- |
237
+ | `enabled` | `true` | Display on/off |
238
+ | `currency` | `"USD"` | `USD`, `CNY`, `EUR`, `GBP`, `JPY`, `CAD`, `AUD`, `CHF`, `INR`, `KRW` |
239
+ | `icons` | `"auto"` | `auto` (terminal heuristic), `nerd`, `ascii` – `nerd`/`auto` use `↑`/`↓`, `ascii` uses `in:`/`out:` |
240
+ | `color` | `"white"` | Base colour: palette name, `#rrggbb` or `0-255`, optionally with `bold:` |
241
+ | `switchColor` | `"bold:#ffd700"` | Colour right after a detected provider change |
242
+ | `deviationStyle` | `"plain"` | SGR attributes of the deviation colour on the icon: `plain`, `bold`, `reverse` |
243
+ | `deviationThresholds` | `{green:10, yellow:10, orange:20}` | Percentage thresholds: below `-green` green, up to `yellow` yellow, up to `orange` orange, above red (all ≥ 0) |
244
+ | `lookupUpstreamProvider` | `true` | Provider/cost resolution active (routing constraint + cache + generation API) |
245
+ | `providerCacheRefreshPrompts` | `10` | After this many **prompts** (user turns) a `generation` cache entry is refreshed; `0` = always re-resolve |
246
+ | `notifyGenerationLookup` | `false` | Notify before every automatic generation-API request (with reason) |
247
+
248
+ ### Icons
249
+
250
+ 1. Env `PROVIDER_COST_NERD_FONTS=1` (nerd) / `=0` (ascii)
251
+ 2. Config `icons`
252
+ 3. `auto`: heuristic like Powerline (`GHOSTTY_RESOURCES_DIR` or
253
+ `TERM_PROGRAM`/`TERM` ∈ iterm, wezterm, kitty, ghostty, alacritty, kaku)
254
+
255
+ Many terminals only set `TERM=xterm-256color` → `auto` yields ASCII
256
+ (`in:`/`out:`); for icons use `icons: "nerd"` or `/provider-cost icons nerd`.
257
+
258
+ > The previously used Nerd Font arrows (`U+F090`/`U+F08B`) were replaced by
259
+ > `↑`/`↓` because private-use glyphs are rendered noticeably smaller.
260
+
261
+ ### Rounding
262
+
263
+ Rounded to **at most 4 decimal places**, trailing zeros removed (`$2`, `$12.5`,
264
+ `$0.2896`).
265
+
266
+ ## How it works
267
+
268
+ - **Real billing instead of catalogue price.** The source of the numbers is
269
+ OpenRouter: the **generation API** returns `total_cost` (actually charged) and
270
+ the token counts, and the **endpoint prices** provide the bucket ratio
271
+ (input/output/cache) of the provider that actually served the request:
272
+
273
+ ```
274
+ modelled = prompt*in + completion*out + cacheRead*cacheReadTokens (from endpoint prices)
275
+ factor = total_cost / modelled # discounts, peak overrides, price changes
276
+ in-rate = prompt * factor (USD per 1M tokens)
277
+ out-rate = completion * factor
278
+ ```
279
+
280
+ The `factor` makes the display match the invoice even when endpoint prices do
281
+ not (yet) exactly match the billed rate. As long as no API data is available,
282
+ the approximation from `usage.cost.*` (Pi catalogue) is shown.
283
+
284
+ - **Resolution.** Order:
285
+ 1. **Routing constraint** from `models.json`
286
+ (`providers.openrouter.modelOverrides.<model>.compat.openRouterRouting.only`)
287
+ – only counts as *certain* when `allow_fallbacks: false` is set. With
288
+ `allow_fallbacks: true` (OpenRouter default) another provider may serve even
289
+ if `only` names exactly one.
290
+ 2. **Persistent rate cache** (`~/.pi/agent/realtime-provider-cost/provider-cache.json`),
291
+ key = request model. Entries expire after `providerCacheRefreshPrompts`
292
+ **prompts** (not by time).
293
+ 3. **Generation API** `GET https://openrouter.ai/api/v1/generation?id=<responseId>`
294
+ – returns the provider **and** the real amount, at most 1 call per model at a
295
+ time. Data is only available a few seconds after the call → retry with
296
+ backoff (1s/2s/4s/8s). Meanwhile an **"update in progress" icon** (`⟳`) is
297
+ shown instead of the provider tag.
298
+
299
+ **When is the API called?**
300
+ - Provider *certain* (one `only` entry **and** `allow_fallbacks: false`) →
301
+ once on the first call, afterwards only every N prompts (rate cache).
302
+ - Provider *not certain* (`allow_fallbacks: true` or no `only`) →
303
+ **per response**, because only the actual provider counts.
304
+ - **Model switch** (different request model than the previous call) → forced
305
+ refresh of provider **and** costs, even if the cache would still be fresh. A
306
+ session restore with the same model is not a switch.
307
+
308
+ **Notification.** (Optional, default **off**: setting
309
+ `notifyGenerationLookup` or `/provider-cost notify on`.) Every automatically
310
+ triggered generation-API request is reported as a notify, including the reason
311
+ in parentheses, e.g.
312
+ `Generation API: resolving provider/costs for deepseek/… (cache miss).`
313
+ Reasons: `cache miss`, `cache expired (N prompts)`, `cache without rates`,
314
+ `stale cache`, `model switch`, `manual refresh` (`/provider-cost refresh`),
315
+ `cache cleared` (`/provider-cost lookup refresh`).
316
+
317
+ - **Preview on model switch.** When the model is switched (`model_select`), the
318
+ **catalogue prices** (`models-store.json`) of the new model are shown
319
+ immediately. The serving provider is not known at that point yet (the first
320
+ call of the new model is still running) → the tag shows `?`. As soon as the
321
+ first response arrives, the real value (including provider tag, or `⟳` while
322
+ resolving) replaces the preview. Tiered pricing is not applied in the preview –
323
+ without token counts only the base rates are known.
324
+ - **`/provider-cost refresh`** → reloads the exchange rates **and** forces a
325
+ generation-API call (provider + costs) when possible (OpenRouter,
326
+ `lookupUpstreamProvider` active, `responseId` present, no lookup already
327
+ running). Clear the cache first with `/provider-cost lookup refresh`.
328
+
329
+ The prompt counter is persisted in the cache and survives restarts. Example
330
+ with `providerCacheRefreshPrompts: 10`: resolution on the 1st prompt, then
331
+ again after the 10th further prompt.
332
+
333
+ - **Invalidation by call counter:** there is no signal that reveals a provider
334
+ switch per response (model slug, `system_fingerprint`, `native_finish_reason`,
335
+ `service_tier` are provider-independent). With a certain provider it is stable
336
+ in the short term, so resolving every N prompts suffices; otherwise it is
337
+ queried per response.
338
+ - **Two caches:** `provider-cache.json` (provider + rates per model) and
339
+ `endpoint-pricing.json` (provider price lists, 24 h). `/provider-cost lookup refresh`
340
+ clears both.
341
+ - **No batch endpoint:** OpenRouter offers neither multiple IDs nor a generations
342
+ list; `/api/v1/activity` requires a management key.
343
+ - **While streaming** the last known value stays; it is only updated on
344
+ `message_end`.
345
+ - **Failover:** if one side is not computable (e.g. `usage.input == 0`) or the
346
+ conversion rate is missing, `?` is shown per side.
347
+ - **Subscription providers** (OAuth or `kimi-coding`) → the item is hidden.
348
+ - **Free models** are shown as `$0/$0`.
349
+ - **Currency conversion** mirrors `pi-powerline-footer` (same source, 24h cache,
350
+ own file `…/realtime-provider-cost/currency-rates.json`); independent of
351
+ Powerline.
352
+ - **No interference with Pi:** the extension replaces neither the footer nor the
353
+ cost calculation; the session cost sum next to it remains Pi's catalogue value.
354
+
355
+ ## Background: why the generation API detour?
356
+
357
+ OpenRouter delivers the serving provider in the stream chunk as a `provider`
358
+ field, but Pi (`pi-ai`) discards it (it only reads `chunk.id` and `chunk.model`).
359
+ Pi likewise discards the actual cost amount (`usage.cost`) and computes costs from
360
+ the price table (`models-store.json`) – which reflects the **model base price**,
361
+ not the price of the routed provider (example `deepseek/deepseek-v4.1-flash` via
362
+ Fireworks: catalogue 0.15/0.60 vs. real 0.22/0.66). No provider header exists
363
+ (`X-Provider-Name` is only listed as "exposed" but is not sent). The generation
364
+ API is therefore the only reliable source for provider and billed amount.
365
+
366
+ ## Dependencies
367
+
368
+ `@earendil-works/pi-ai`, `@earendil-works/pi-coding-agent` and
369
+ `@earendil-works/pi-tui` are bundled by Pi and are therefore only declared as
370
+ optional `peerDependencies`. No runtime dependencies on other extensions.
371
+
372
+ ## License
373
+
374
+ [MIT](./LICENSE) – see [`LICENSE`](./LICENSE).