4.4 KiB
@deepseek-ai/dsh-openrouter-usage
English | 中文
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
- 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.
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 discardsusage.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.