Skip to content
dsh.fish
Bundle

dsh-deepseek-cost-watch

DeepSeek off-peak alert, account balance and live Session cost estimate for the DeepSeek Harness Web UI

Source
Webificio
License
MIT
Updated
Updated 7 hours ago

Readme

# dsh-deepseek-cost-watch

A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) profile bundle that puts the
DeepSeek **off-peak schedule**, the **remaining account balance**, and a **live cost estimate**
into the Web UI.

It replaces two throw-away dynamic plugins with one permanent, installable package: no bundler,
no build step, no dependencies, two small JavaScript files.

---

## What it does

### 1. Off-peak badge (frame-wide overlay)

A floating card in the bottom-right corner of the app:

```
┌─────────────────────────────────────┐
│ ● DeepSeek off-peak              –  │
│ [−50%]  Discount active             │
│ Ends in 1h 42m 07s                  │
│ Balance                     $8.28   │
│ ─────────────────────────────────── │
│ Peak UTC: Mon–Fri 01:00–04:00 ·     │
│           06:00–10:00               │
│ Local time (UTC+02:00):             │
│           03:00–06:00 · 08:00–12:00 │
│ Outside those windows every model   │
│ costs half.                         │
└─────────────────────────────────────┘
```

* **Live state** — green (`−50%`) during off-peak, amber (`PEAK`) during peak hours, refreshed
  once a second.
* **Countdown** to the next peak/off-peak flip.
* **Balance** — the credit left on the DeepSeek account, straight from the official
  `GET /user/balance` endpoint. Hover for the granted / topped-up split and the read time; click
  the row to refresh it on the spot.
* **Schedule in both zones** — the fixed UTC windows plus the same windows shifted into your
  browser's timezone, including the `(+1d)` marker when a window crosses midnight locally.
* **Collapsible** — press `–` to shrink it into a `PEAK · 18m 04s` / `OFF-PEAK −50%` pill; click
  the pill to expand it again.

### 2. Cost estimate (under the composer)

Beside the shipped token counter, above the composer:

* **Collapsed:** a coloured dot plus the estimated spend for the open Session — `● ≈ $0.1414`.
  Green means the request you are about to make is billed off-peak, amber means peak.
* **Expanded (click):** the full breakdown, in normal page flow, so nothing overlaps the
  composer or the badge:

```
Estimated total                     $0.1414
Balance                              $8.28
Peak (01-04, 06-10 UTC, Mon-Fri)   $0.1414 · 5.95M tok
Off-peak                            $0.00 · 0 tok
Uncached input                     114.46k tok
Cache read (hit)                   5.77M tok
Output                             60.35k tok
Model                    DeepSeek-V4.1-Flash · $0.1414
Right now              peak · changes at 10:00 UTC
Estimate from the official DeepSeek rates (2026-09-15): cache hit /
cache miss / output, with off-peak at half the peak rate. Not a billed amount.
```

The estimate is **history-aware**: every billed request in the Session log is priced with the card
that was in force *when that request happened*, so a Session that spans a tier change is split
correctly between peak and off-peak instead of being priced entirely at today's rate.

### 3. Balance

The same figure the DeepSeek platform shows on its top-up page, without leaving the app:

* Read by the **Host**, never by the browser: the API key stays in the Host process and is sent
  only to `api.deepseek.com`.
* Resolved exactly like the model route does — the stored credential (`DEEPSEEK_API_KEY`, the
  ref the Models page writes) and, failing that, the launching environment.
* Refreshed once on plugin activation, every **5 minutes**, and on demand when you click the row.
  The Host caches an answer for 60 seconds, so several browser tabs never multiply the calls.
* Degrades instead of breaking: no credential renders `no API key`, an unreachable API renders
  `unavailable`, and both explain themselves in the tooltip. Low credit (`is_available: false`)
  turns the amount amber.

---

## Requirements

* DeepSeek Harness with the `web` profile (a version that ships `dsh plugin` and `dsh.bundle`
  profile bundles — `0.1.5-rc` or later).
* `pnpm` on `PATH` (`dsh plugin` forwards to it).
* `git` on `PATH` when installing from GitHub.

## Install

From GitHub:

```sh
dsh plugin --profile web add github:Webificio/dsh-deepseek-cost-watch
```

From a local checkout (after replacing `owner/repo` with your own fork, the same command works
against your fork):

```sh
dsh plugin --profile web add /absolute/path/to/dsh-deepseek-cost-watch
```

Either command installs the package into the `web` profile and appends it to
`dsh.profile.bundles` automatically (the package declares `dsh.bundle.patch`, so `dsh plugin`
recognises it as a profile layer).

Then **restart the profile** — a bundle is composed at profile boot, so stop the running
`dsh web` and start it again:

```sh
dsh web
```

Reload the browser page. You should see the off-peak card in the bottom-right corner, and the
cost line appear in the composer dock as soon as the open Session has billed at least one request.

### Verify the install

```sh
cat "${DSH_HOME:-$HOME/.dsh}/profiles/web/package.json"
```

`dsh-deepseek-cost-watch` must appear in `dsh.profile.bundles`. If it does not, the plugin was
installed as a plain dependency; re-run the `add` command and read pnpm's output.

### Uninstall

```sh
dsh plugin --profile web remove dsh-deepseek-cost-watch
```

Restart the profile afterwards. Both occupants and both Host routes disappear with the package.

---

## How it works

One profile row, two halves of the same package:

| Half | Entry | What it contributes |
| --- | --- | --- |
| Host | `lib/host.js` (`main`) | Two read-only Fetch routes on the Connection carrier: `GET /api/deepseek-cost-watch/report?sessionId=…` (folds the live Session log into a cost estimate) and `GET /api/deepseek-cost-watch/balance` (reads the account credit with the stored key). |
| Browser | `lib/client.js` (`exports["./client"]`) | A Cordis client plugin claiming two slots: `shell.overlay` (the badge with the balance row) and `conversation.composer.dock` (the cost line). |

```
browser (client.js)                    host (host.js)
────────────────────                   ──────────────
shell.overlay       ─┐
  off-peak badge      │  in-process: local peak/off-peak math (no round trip)
  balance row         │
conversation.        ─┤
  composer.dock       │
  cost line  ─────────┤ fetch ─────────▶ GET /api/deepseek-cost-watch/report
                                        │   ctx.sessions.get(id).snapshotEvents()
                                        │   → foldUsage() → JSON
                      └ fetch ─────────▶ GET /api/deepseek-cost-watch/balance
                                            ctx.credentials.resolve('DEEPSEEK_API_KEY')
                                            → api.deepseek.com/user/balance → JSON
```

The fold lives on the Host on purpose: only the Session log records *when* each request was
billed, while the browser's `tokenUsage` projection carries aggregate token totals with no
timestamps. The balance is read on the Host for a different reason: the API key must never reach
the browser. The overlay's live clock needs no Host round trip at all, so it recomputes locally
every second from the published schedule.

Everything the plugin registers — both Fetch routes, the stylesheet, the balance poll, and both
Slot occupants — is owned by the plugin's fiber, so stopping, updating, or removing the package
withdraws all of it.

### Files

```
dsh-deepseek-cost-watch/
├── package.json        # name, exports, dsh.bundle.patch, dsh.client.platform
├── cordis.patch.yml    # the one profile row this bundle inserts
├── lib/host.js         # Host half: rates, schedule, session-log fold, balance, both routes
├── lib/client.js       # Browser half: hand-written module-loader bundle
├── test/smoke.mjs      # dependency-free checks for both halves
├── README.md
└── LICENSE
```

---

## Balance, credentials and endpoints

| Setting | Default | Where |
| --- | --- | --- |
| Credential ref | `DEEPSEEK_API_KEY` | stored credential (Models page) or the launching environment |
| API base URL | `https://api.deepseek.com` | `DEEPSEEK_BASE_URL`, the same variable the model adapter reads |
| Balance endpoint | `GET /user/balance` | — |
| Host cache | 60 s | `BALANCE_TTL_MS` in `lib/host.js` |
| Browser poll | 5 min, plus click-to-refresh | `BALANCE_REFRESH_MS` in `lib/client.js` |

The key is used for exactly one request: `Authorization: Bearer <key>` against the configured base
URL. It is never logged, never returned by the route, and never sent to the browser.

---

## Prices, and how to update them

The rate tables live at the top of [`lib/host.js`](lib/host.js) as USD per 1,000,000 tokens,
written `[off-peak, peak]` per bucket (cache hit, cache miss, output):

```js
export const RATE_TABLES = [
  {
    id: 'deepseek-flash',
    name: 'DeepSeek-V4.1-Flash',
    models: ['deepseek-flash', 'deepseek-v4-flash'],
    cacheHit: [0.003, 0.006],
    cacheMiss: [0.15, 0.3],
    output: [0.6, 1.2],
  },
]
```

* `RATE_AS_OF` and `RATE_SOURCE` are printed in the expanded panel, so a stale table is visible
  in the UI rather than silent.
* Update the numbers when DeepSeek publishes new pricing, then restart the profile.
* Only `deepseek-official` routes are priced. Anything else is counted and reported as
  *unpriced* rather than guessed at.
* The schedule (`PEAK_WINDOWS`) is UTC: Monday–Friday 01:00–04:00 and 06:00–10:00, with off-peak
  at half price at every other hour.
* The number shown is an **estimate**, not an invoice: it uses the list price of the configured
  model, while credits, discounts, or a provider-side price change are invisible to it.

## Development

There is no build step. Edit `lib/host.js` or `lib/client.js` and restart the profile.

```sh
npm test        # node test/smoke.mjs — no dependencies
```

The smoke test prices a synthetic Session log across both rate cards, exercises the report route
(200 / 400 / 404), drives the balance route against a stubbed DeepSeek API (success, cache,
`?refresh`, HTTP error, unreachable, missing credential), then loads the browser bundle with a
stub module loader and asserts that it registers the module id, claims both Slot occupants,
injects the stylesheet, polls the balance, and renders the badge with the remaining credit.

`lib/client.js` is a hand-written bundle in the Harness's client module-loader format
(`window.__ModuleLoader__.load({ id, factory })`), which is why the package needs no bundler.
Two details are load-bearing:

* the registered module `id` **must equal the package name** in `package.json`, and
* the bundle must stay self-contained — `React` comes from the runtime's module table
  (`require('react')`), and nothing else is imported.

### Limitations

* The cost line appears only for Sessions the Host still holds in memory, i.e. the conversation
  you have open. Cold or archived Sessions are not re-read from disk.
* The estimate covers billed assistant requests in the Session log; subagent Sessions are priced
  in their own conversation.
* The browser half targets the Web profile (`dsh.client.platform: "web"`); TUI and headless
  profiles load the Host routes only.
* The balance needs outbound network access to the DeepSeek API from the Host process; behind a
  proxy, set `DEEPSEEK_BASE_URL` to the reachable origin. When it cannot be read, the row says so
  and the rest of the plugin keeps working.
* The balance is the account's credit, not a per-Session figure, and it is only as fresh as its
  last read (5 minutes, or one click).

## License

MIT — see [LICENSE](LICENSE).

Install

dsh plugin --profile web add github:Webificio/dsh-deepseek-cost-watch#ec9be9518263434a1daef03595358af7dc316bf7

Profile: web

Source