# @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 `refresh()` method fetches the account NOW and resolves with the resulting snapshot, for a user-initiated read that must not wait for the next scheduled tick; concurrent callers share one in-flight fetch, and a failed fetch resolves with the last-known snapshot rather than rejecting. 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. Every value describes only its own session's work. A forked child session's log opens with a verbatim copy of its parent's up to the header's `seedLength`, and the fold skips that inherited prefix (reading only its `request/context` route records, which carry no cost but attribute the child's first chunk-only step). A session total plus its subagents' totals is therefore a sum, never a double count. 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. A session folded while the table was empty (no key yet, or a failed first fetch) keeps its steps recorded as unpriced until a `stateVersion` bump refolds them. - **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.