Skip to content
dsh.fish
Bundle

dsh-switch-cost

Prices a dsh session against other models: what it cost on the route that ran it, and what the same token counts would have cost elsewhere.

Source
xyzs996
License
MIT
Updated
Updated 6 days ago

Readme

# dsh-switch-cost

English | [中文](README.zh.md)

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

```bash
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

```yaml
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:

```ts
{
  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:

```ts
{
  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 on `checkedAt`.
- `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:

```js
{ 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

```bash
pnpm install
pnpm test
```

## License

MIT

Install

dsh plugin --profile web add github:xyzs996/dsh-switch-cost

Profile: web

  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source