dsh-switch-cost
DeepSeek Harness (dsh) plugin to compare LLM API cost across models and providers: prices the current session on the model that ran it, then reprices the same token counts against 15 routes on file. DeepSeek peak/off-peak resolved per UTC hour, cache read and write priced separately, every rate with its source and check date.
- Stars
- 0
- Language
- JavaScript
- Created
- Aug 24, 2026
- Updated
- Aug 24, 2026
Introduction
dsh-switch-cost
English | 中文
Prices the current session twice: once on the route that actually ran it, and once on every other route in the shipped price table. It answers "what did this cost" like the other usage plugins do, and then the question they don't — "what would the same traffic have cost somewhere else."
The session's usage is folded into token buckets keyed by route and by UTC hour, so a provider that bills different rates at different hours is priced hour by hour rather than at whichever rate happened to be in force when you asked. The fold records when; the price table decides what that hour costs.
Install
dsh plugin add dsh-switch-cost
Composes into any assembly that provides sessionProjections and tools. Both registrations sit behind ctx.inject(['sessionProjections'], …) — an assembly that composes no projection registry gets neither the projection nor the tool, rather than a tool that always answers zero.
Config
switch-cost:
alternatives: 8 # how many other routes to report, cheapest first
Service surface
This plugin registers no service. It registers one projection unit and one tool.
Projection: switchCost
Host-only (no wire), stateVersion: 1. State:
{
routes: { [`${provider}/${model}`]: { byHour: { [`${utcDay}-${utcHour}`]: Buckets } } },
route: string | null,
last: { turn, step, routeKey, hourKey, buckets } | null,
}
Buckets is { input, cacheRead, cacheWrite, output }. Hour keys are sparse; utcDay is 0–6 with Sunday first.
A step reports its usage twice — once as an assistant/chunk of type usage, once on the assembled assistant/message. The fold keeps last-wins per (turn, step): the second sample replaces the first instead of adding to it, and the withdrawal is made from the hour the first sample was originally filed under, not the hour the replacement arrived in. A step that straddles an hour boundary therefore leaves no phantom bucket. Events the fold does not care about return the same state reference, as the projection drive requires.
Usage that arrives before any request/context is filed under unknown/unknown rather than dropped.
Tool: switch_cost
No parameters. Reads the projection state for the calling agent's session and returns:
{
tokens, // whole-session bucket totals
actual: [{ route, tokens, cost, costText, ratesApplied, pricedAs, source, checkedAt, layer }],
actualTotal, actualTotalText,
alternatives: [{ route, cost, costText, versusActual, source, checkedAt, layer }],
unpriced?: [{ route, tokens }],
caveats: string[],
}
A call with no owning agent session is rejected. A route with no price on file is reported under unpriced and left out of the total — it is never silently priced as zero.
Sample rendered result:
Ran on deepseek-official/deepseek-v4-flash: $0.0416 (peak and off-peak hours both billed)
Same tokens, other routes:
deepseek-official/deepseek-v4-flash-vision-exp $0.0416 +0%
openai/gpt-5.6-luna $0.0513 +23%
deepseek-official/deepseek-v4-pro $0.1260 +203%
zhipuai/glm-4.7 $0.1446 +247%
anthropic/claude-haiku-4-5 $0.2452 +489%
moonshotai/kimi-k2.6 $0.2707 +550%
The price table
dsh-switch-cost/prices exports the table. Every row carries source, checkedAt and layer.
Two layers, vendor first:
layer: 'vendor'— read off the provider's own pricing page oncheckedAt.layer: 'models.dev'— taken from the community catalog.
Vendor wins because the catalog lags and, more importantly, because it has no representation of time-of-use pricing at all: off_peak, peak and time_of_use appear nowhere in its schema, and its tiers field carries context-size tiers only. A provider that bills two rates a day cannot be expressed there, so a plugin that mirrored the catalog would price those sessions at a single rate that is neither of the two.
Time-of-use
A row may carry a tariff instead of a single flat block. DeepSeek's is the one shipped:
{ peakWindowsUtc: [[1, 4], [6, 10]], peakWeekdaysUtc: [1, 2, 3, 4, 5] }
Those are the published Beijing windows 09:00–12:00 and 14:00–18:00, weekdays, with weekends fully off-peak. The rule is resolved in UTC, and for these particular windows that is exact rather than approximate: every hour they cover satisfies hour + 8 < 24, so adding the Beijing offset never rolls the date over, and the UTC weekday equals the Beijing weekday for every hour that can be peak. A test asserts that property directly and fails if a future window ever reaches past 16:00 UTC — at which point resolving in UTC would stop being safe and the rule would need a real timezone.
Model Experience
What the model sees
One tool with an empty parameter schema, described as reporting session cost and the cross-route comparison. The result text names each route, its cost, whether both tariffs were billed, and the alternatives cheapest-first with a signed percentage against what was actually spent. The closing line restates that the comparison is of published rates at equal token counts.
Token effect
One small fixed schema. The result grows with the number of routes that ran plus alternatives rows — roughly 200–400 tokens at the default of 8.
KV Cache effect
None. The plugin prices usage that has already been logged; it never assembles or sends a provider request, and it contributes nothing to the system prompt.
What this does not claim
These ship inside every result, not just here, because a result read by a model will otherwise be presented as a bill.
- Token counts are the ones the model that actually ran produced. A different model tokenizes differently and would not produce these counts. This compares published rates at equal token counts; it does not predict the other model's invoice.
- The cache read/write split is carried over unchanged. Prompt caching differs by provider in what it caches, how long it holds it, and what it charges to write. A route with a very different caching model is the least reliable row in the table.
- Every rate is pay-as-you-go list price. Subscription plans, prepaid credit packages and free tiers are not modelled.
- Prices are a dated snapshot. Each row reports its own source and date; check them before acting on a number.
Where a provider publishes no separate cache-write price, those tokens are billed at the input rate — cache-write tokens are cache-miss tokens — and the result sets cacheWriteBilledAtInputRate so the imputation is visible rather than assumed.
Development
pnpm install
pnpm test
License
MIT