Extend the openRouterCost projection with a per-step priced-cost map keyed by turn:step so surfaces can render spend without re-pricing, and have the Trajectory view read it through the framework useProjection seat: each priced assistant step shows its USD cost on the request boundary chip and in the inspector summary, while unpriced steps stay blank. Also fix a pre-existing exactOptionalPropertyTypes error in extractCacheRates that the host typecheck surfaced.
105 lines
4.6 KiB
Markdown
105 lines
4.6 KiB
Markdown
# @deepseek-ai/dsh-openrouter-usage
|
||
|
||
English | [中文](README.zh.md)
|
||
|
||
OpenRouter spend tracking: a Typert Remote gateway exposing the account
|
||
balance plus the `openRouterCost` session projection, which prices logged
|
||
token usage against OpenRouter's model pricing table.
|
||
|
||
The package is inert without a resolvable `OPENROUTER_API_KEY` (the same
|
||
credential OpenRouter LLM routing already uses): no key means no fetch, an
|
||
empty pricing table (so every step stays unpriced), and an empty balance.
|
||
|
||
## Config
|
||
|
||
```yaml
|
||
- id: openrouter-usage
|
||
name: '@deepseek-ai/dsh-openrouter-usage'
|
||
config:
|
||
apiKeyEnv: OPENROUTER_API_KEY
|
||
baseURL: https://openrouter.ai/api/v1
|
||
syncEnabled: true
|
||
pricingRefreshMs: 21600000
|
||
balanceRefreshMs: 60000
|
||
```
|
||
|
||
| Key | Default | Meaning |
|
||
| --- | --- | --- |
|
||
| `apiKeyEnv` | `OPENROUTER_API_KEY` | Credential reference resolved per refresh. |
|
||
| `baseURL` | `https://openrouter.ai/api/v1` | OpenRouter API root; `/credits`, `/models`, and `/auth/key` are appended. |
|
||
| `syncEnabled` | `true` | Whether periodic pricing/balance refresh runs. |
|
||
| `pricingRefreshMs` | `21600000` (6h) | Pricing-table refresh interval in ms. |
|
||
| `balanceRefreshMs` | `60000` (60s) | Balance refresh interval in ms. |
|
||
|
||
The key resolves through the credentials seam (`ctx.credentials`), with the
|
||
launch environment as fallback, exactly like `dsh-web-search-deepseek`.
|
||
|
||
## Service contract
|
||
|
||
`ctx.openRouterUsage` is a Typert Remote gateway. The `snapshot()` method
|
||
returns a detached copy of the last successful balance snapshot: `balanceUsd`
|
||
is the available balance from `GET /credits` (`total_credits` minus the spent
|
||
`total_usage`, the figure OpenRouter's dashboard surfaces), plus `label` and
|
||
the monthly `usageTokens`/`limitTokens` budget from `GET /auth/key`,
|
||
`isFreeTier`, and an `updatedAt` epoch. Before any successful fetch it serves
|
||
an all-`null`
|
||
record; a failed refresh keeps the last-known snapshot and logs. The same key
|
||
also refreshes the model pricing table from `GET /models`
|
||
(`pricing.prompt`/`completion` USD per token, plus a flat `request` fee and
|
||
optional `input_cache_read`/`input_cache_write` when disclosed).
|
||
|
||
The `openRouterCost` projection folds each session's logged token usage
|
||
(`assistant/chunk` usage and `assistant/message` usage, deduplicated per
|
||
step) against the pricing table. Attribution prefers the assembled message's
|
||
own `provider`/`model`; a chunk-only (failed) step prices from the newest
|
||
`request/context` route. A step on a non-`openrouter` provider is outside the
|
||
domain and changes nothing; an OpenRouter step whose model has no pricing
|
||
entry counts as an unknown (unpriced) step. The projection's `steps` view
|
||
field maps each priced step to its cost in USD under `${turn}:${step}` keys,
|
||
so per-step surfaces can render spend without re-pricing; unpriced steps are
|
||
absent from the map.
|
||
|
||
## Extension points
|
||
|
||
Web surfaces read the `openRouterCost` projection and call the balance Remote
|
||
through the client assembly (`ctx.remote.openRouterUsage.snapshot()`); the
|
||
component stack ships in `dsh-client-ui-openrouter-usage`. The gateway
|
||
requires no session or agent wiring — everything rides the durable whole-log
|
||
projection and one cached fetch.
|
||
|
||
## Model Experience
|
||
|
||
### OpenRouter cost and balance readouts
|
||
|
||
#### What the model sees
|
||
|
||
Nothing. Cost and balance are client-facing read models only: neither enters
|
||
a model request, a tool schema, or any prompt. The agent loop is unchanged.
|
||
|
||
#### Token effect
|
||
|
||
No model tokens. Priced figures derive from usage already logged by the
|
||
existing `assistant/message` and `assistant/chunk` events.
|
||
|
||
#### KV Cache effect
|
||
|
||
None; the KV cache is unaffected because cost is a projection of existing
|
||
usage events, never a new model-visible input.
|
||
|
||
## Known Limitations and Deferred Work
|
||
|
||
- **Estimate, not itemized billing** — cost is `tokens × model pricing`,
|
||
not OpenRouter's itemized per-generation billing. Generation ids are not
|
||
durably logged, and the pi-ai adapter discards `usage.cost`, so the
|
||
projection reconstructs spend from token counts. Free-tier and
|
||
promotional pricing may differ from the model table.
|
||
- **Pricing as of the fold** — the projection prices a cell with the model
|
||
table current when that cell folds. Refreshing pricing only affects cells
|
||
folded afterward; already-folded history keeps its prior figures.
|
||
- **Per-token approximation** — cache-read/cache-write fall back to the
|
||
prompt rate when the API does not disclose separate cache rates, and the
|
||
flat request fee is charged once per step. Bills may differ by fractions
|
||
of a cent.
|
||
- **No settings card** — the plugin exposes config only through cordis.yml;
|
||
a settings-section UI is deferred.
|