mpinaev/dsh-context-governor ↗★ 1
dsh-context-governor
监控 DSH 会话上下文大小与 Token 费用 适合需要实时监控 DeepSeek 余额、提示词大小和单步成本的用户。
安裝
npx -p @deepseek-ai/dsh dsh plugin --profile web add github:mpinaev/dsh-context-governor說明文件
閱讀完整 README ↗dsh-context-governor
A DeepSeek Harness plugin: a session context indicator — prompt size, step cost, cache-hit, bands and the compaction threshold, DeepSeek balance and the peak/off-peak tariff, plus a handoff button.
It never calls a model and spends no tokens at all — details below.
What it looks like
The chip in the session header opens the panel below. The same panel is available in English, Chinese and Russian:
| English | 中文 | Русский |
|---|---|---|
![]() | ![]() | ![]() |
The handoff button sits next to the model selector:

No tokens, no model calls
Everything the plugin shows is measurement and arithmetic. It reads tokens and the window from harness projections, computes the cost with the configured rates and fetches the balance over HTTP.
- No generation requests. Not for review, not for summarization, not in the background: the plugin never asks a model to compute anything.
- Zero tokens. The plugin sends no provider requests and spends none of your
budget. Its only touch of the LLM layer is reading the model catalog
(
resolveModelInfo) for the window and limits; that is not generation and not billable tokens. - Nothing is modified. It does not touch the history, the system prompt or the prefix cache, so it cannot affect prompt caching.
- The network is used for the balance only. The harness account service (when
signed in) and
api.deepseek.com/user/balanceare not a model. Plusgit statuson a handoff click, locally.
Install
As a package:
dsh plugin --profile web add dsh-context-governor
From source:
dsh plugin --profile web add github:mpinaev/dsh-context-governor
Locally, without installing: drop the directory into
~/.dsh/profiles/web/plugins/ and reference the file from the profile's
cordis.patch.yml:
- insert:
- id: context-governor
name: /absolute/path/to/dsh-context-governor/index.js
Installed as a package, the host half is read at startup, so restart dsh web
and reload the page — the client half is served as a revision snapshot.
Check it with npm test: the smoke test runs the host half against a stub
harness, with no network and no model calls, and pins the compaction threshold,
the cache buckets, the step cost and the balance provider gate.
Supported DSH versions
| DSH version | State |
|---|---|
0.1.7-rc.2 … `` overrides the default | |
freshRate / cacheReadRate / cacheWriteRate / outputRate, so a second | |
provider can carry its own numbers (and its own peakMultiplier); |
- the peak/off-peak tariff (⚡/🌙, the
×Nmultiplier, Chinese holidays) is DeepSeek's, so it is shown only for a seasonal provider: by default an id starting withdeepseek. A foreign provider has a flat rate; declareseasonal: truein itsproviderRatesentry if it really follows DeepSeek's schedule. A flat provider gets neither the time marker nor the tariff rows.
Signals
- a server log entry when a session enters a band, on a cold prefill and on an expensive step;
- a client chip in the session header, coloured by band, with a details panel;
- on the chip: the balance and, for a provider that follows DeepSeek's schedule, the tariff marker (peak / off-peak with countdown).
Peak/off-peak tariff and balance
Tariff. The DeepSeek rule is fixed: peak is Beijing time (UTC+8), on weekdays, 09:00–12:00 and 14:00–18:00; everything else, weekends included, is off-peak and costs half. The local timezone plays no part in the decision, only in the display. The configured rates (freshRate, cacheReadRate, cacheWriteRate, outputRate) are off-peak; at peak they are multiplied by peakMultiplier (2 by default), so the step cost and the warnings follow the tariff of the moment. The countdown runs to the next real switch: boundaries inside a weekend are skipped, so after Friday 18:00 it counts to Monday 09:00, not to Saturday. On a provider with that schedule, at peak the tariff marker, the Beijing time and the balance are drawn in red, in both the chip and the panel, so the expensive hours are visible at a glance.
Chinese public holidays. The official rule has an easy-to-miss caveat: peak is
weekdays excluding Chinese public holidays, and on those holidays DeepSeek
stays off-peak around the clock. The plugin takes the year's calendar from a
maintained source (holidayUrl, by default the data of the chinese-days
package), caches it on disk once (holidayCacheDir, default
/cache/context-governor) and refreshes itself — no code to edit every
year. The network is used in the background only: if the calendar has not arrived,
the tariff falls back to the weekday rule and the panel honestly says the holiday
calendar is not loaded. Your own dates can be set with the holidays config — an
array of 'YYYY-MM-DD' or 'YYYY-MM-DD..YYYY-MM-DD' ranges; they apply
immediately, take priority over the source and work without a network. On a
holiday the tariff row reads "off-peak (Chinese holiday)" and the countdown skips
the whole holiday to the next working peak. The network can be turned off
entirely: holidayFetch: false.
Balance. Sources in order:
- The official platform account — the harness service
ctx.deepseekAccount(getBalance). It works when the account is signed in and the client has sent its build version; wallets arrive as strings, the currency may be CNY or USD, and bonus wallets come as a separate list. - The API key —
GET https://api.deepseek.com/user/balance, with the key from the credentials seam (DEEPSEEK_API_KEY) or from the environment.
The source field says where the numbers came from: account or api-key.
The fallback to the second source is deliberate: another build may have no platform
sign-in, and the balance must not disappear because of that.
The balance is shown only for DeepSeek providers. On cline, clinebot and any
other provider a foreign total on the chip is plain misinformation: no balance is
displayed there, and the panel shows a DeepSeek-only note instead of a number. The
provider list is configurable through balanceProviders (default
['deepseek-official']), and any id starting with deepseek is accepted. If a
session has no route of its own yet, the provider is not guessed and the balance
stays hidden.
The key never leaves the host: only numbers reach the client (total, bonus, top-up, currency, availability). Concurrent reads collapse, and failures degrade into a state (no-credential, error, disabled) rather than an exception. The refresh button clears the cache and re-reads the balance and the tariff.
The currency is not converted. The step cost and the configured rates
(freshRate, cacheReadRate, cacheWriteRate, outputRate — all $/1M) are in
US dollars, while the balance is printed in whatever currency the source reports
(USD or CNY for DeepSeek), to the cent. The plugin never converts between them, so
a ¥ balance and a $ step cost in the same chip are different units — compare
them only after converting yourself.
Route access. /context-governor/api/* are closed by a guard header and an
Origin check: a request must carry x-dsh-context-governor: 1 (only the plugin
client sets it) and must not be cross-origin. Without the header — 403, with a
foreign Origin — 403. A plain loopback GET without Origin is allowed, which
keeps curl diagnostics convenient.
The practical point: at 99% cache-hit an expensive step is not a large context but fresh input or a cache miss, and the tariff doubles that difference.
Handoff button
One click: the host assembles a session summary (task, state, cwd, touched files,
child sessions), the client opens a new session in the same workspace through
ctx.uiWorkspace.startSession() and puts the summary into its draft. There are
no branches or fallbacks: no Alt+click, no insertion into the current session, no
clipboard.
Two declared services keep it reliable:
- the client declares
inject = ['slots', 'uiWorkspace']: without the declaration Cordis does not hand over the service, and the button used to slip into the fallback paths instead of opening a session; - the host declares
inject = [... 'sessionQuery']: without it the summary came out with no task, state or paths (placeholders only).
The summary reaches the new session lazily: the click remembers the text,
startSession() opens the session, its input slot renders, and the same
component puts the text into the draft through inputActions.setDraft.
UI languages
Languages: en (default), zh, ru. The switch is a small button next to
the refresh control in the chip header: it shows the current code (EN / Chinese /
RU) and cycles the language. The choice is remembered in the browser
(localStorage: dsh-context-governor.lang) and immediately re-renders both the
chip and the handoff button.
The language covers all interface text (panel labels, warnings, tooltips) and the
language of the document the handoff button inserts into the new session: the
client passes lang to /api/handoff. The host returns data without text
(band, tariff and warnings are codes), so switching the language needs no data
refetch.
Thresholds
The plugin never hardcodes the model window — it reads it from the harness:
- The main source is the
contextPressureprojection (contextWindow,pressureTokens) ofctx.sessionProjections: the authoritative window of the live session. - The output reserve is the request
maxTokensfrom therequest/headerevent; until thenrequest/context(contextWindow) serves as a hint. - The model catalog (
ctx.llm.resolveModelInfoforagentDefaultModel.currentSelection()) remains a hint until the first request.
Window and reserve are kept per session.
The compaction threshold and the bands are derived from the window:
reserve = request maxTokens
compaction threshold = min(thresholdRatio * window, window - reserve - headroomTokens)
band i = bandRatios[i] * compaction threshold
With a 1,000,000 window and a 256,000 reserve the threshold is 678,464 and the
bands are 237k / 407k / 577k. With an 800,000 window the threshold is 478,464 and
the bands 167k / 287k / 407k. The formula matches dsh-compaction-basic
(window - reserve - headroom); with constant bands, critical would


