dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
Session cost and remaining provider balance below the DeepSeek Harness Web composer.
A DeepSeek Harness plugin that shows one ambient line below the Web composer: the estimated model cost of the current session and of every subagent session it started, plus the remaining balance of the provider route in use.
[ Composer card ]
Cost $0.1306 · Balance $12.50
Hovering a pill shows its breakdown: token buckets per model route, how much of the total came from subagent sessions, per-request fees, and topped-up versus granted credit for the balance.
subagent/catalog) but never their usage; the plugin reads each child's own
projected cost instead — from the live Session while the app still holds it,
from the child's durable checkpoint once it has settled, and by re-folding that
child's own log where no checkpoint can serve it (see below).If a child session cannot be read at all — no live Session and no checkpoint
whose lifecycle matches — the plugin does not pretend it cost nothing. It marks
the total as a lower bound (at least $0.1306) and says how many sessions were
unreadable, rather than showing a number that looks exact.
Why a total can still differ from https://platform.deepseek.com's usage log:
that page bills an account, so it also covers sessions this machine never ran and
any other client using the same key. Auxiliary model calls whose usage is never
recorded (session titles, and the DeepSeek-side search calls a search request can
fan out to, up to maxUses per request) are billed by DeepSeek but are invisible
to any log-based reader. The plugin therefore estimates from what the logs
record, and labels that estimate as one.
dsh web). The balance read needs the
connection and credentials services, which the shipped Web composition
provides.DEEPSEEK_API_KEY credential reference. Cost needs nothing: prices ship in the
row config.# 1. install the package into a profile
dsh plugin --profile web add github:dsh-plugin-db/dsh-cost-meter
# …or from a local checkout
dsh plugin --profile web add link:/path/to/dsh-cost-meter
install_bundle.)The package ships a bundle patch, so enabling it inserts the row with built-in defaults. Cost works immediately; the balance pill appears once a wallet source answers. The credential reference is resolved on the Host, so the secret never reaches the browser.
Every value lives in the row config, so it survives DSH upgrades in the
profile's own cordis.patch.yml (highest-priority layer):
- id: dsh-cost-meter
name: 'dsh-cost-meter'
config:
prices:
- provider: '*' # '*' matches any route; a literal provider wins over '*'
model: deepseek-flash
currency: USD
input: 0.3 # per 1M cache-miss input tokens
output: 1.2 # per 1M output tokens
cacheRead: 0.006 # per 1M cache-hit input tokens
cacheWrite: 0.3 # optional; defaults to `input`
offPeak: # optional; used outside `peakWindows`
input: 0.15
output: 0.6
cacheRead: 0.003
cacheWrite: 0.15
peakWindows: # optional; defaults to DeepSeek's published windows
- { startHourUtc: 1, endHourUtc: 4, weekdaysOnly: true }
- { startHourUtc: 6, endHourUtc: 10, weekdaysOnly: true }
balances: # optional; provider route → wallet endpoint
- provider: deepseek-official
url: https://api.deepseek.com/user/balance
credential: DEEPSEEK_API_KEY # credential reference resolved in the Host
auth: bearer # or x-api-key
timeoutMs: 10000
fees: # optional; Session events that are billed per request
- event: web/deepseek-search-llm-request
label: Search
per1kRequests: 0.75 # DeepSeek Search/Fetch list price, USD
currency: USD
Lookup specificity is provider+model → provider+* → *+model → *+*. A
route with no matching row still shows its token count, and the cost pill is
prefixed with "at least" while unpriced calls exist, so an estimate never
pretends to be complete.
A fees row counts the Session events of that exact type and bills them per
1,000 requests, outside token pricing, which is how DeepSeek bills its Search and
Fetch calls. The default row covers web/deepseek-search-llm-request, the event
the Web app's DeepSeek search feature records for each search request. Set
fees: [] to price tokens only.
Shipped defaults are DeepSeek's published list prices for deepseek-flash and
deepseek-v4-pro (per 1M tokens, USD) with their peak/off-peak windows, and the
Search/Fetch per-request fee. Prices change; edit them in the profile patch.
The Client reads the balance in two steps, and reports what it finds:
ctx.remote.account.getBalance, for
routes whose provider starts with deepseek.balances (an exact provider wins
over *), resolves the named credential through ctx.credentials, calls the
endpoint, and answers with the wallet amounts only.A route the Host prices no wallet for (unsupported-provider) shows nothing, so
the pill never appears for a provider that simply has no balances row. Every
other outcome is visible: the amount when the wallets are read, and Balance —
with the reason in its tooltip when the credential is missing, the endpoint
fails, or the route does not answer at all. That reason carries the HTTP status
and the body, which separates the two silent failures: a browser sees
404 not found when this build's route is not mounted, and 401 unauthorized
when the request never reached it.
The key never reaches the browser, and the endpoint is ordinary configuration:
another provider whose API returns a wallet needs a new balances row plus a
parser kind in index.js. Amounts are re-read once a minute; each pill carries
its own currency, so a USD cost and a CNY balance never get conflated.
| Half | File | Role |
|---|---|---|
| Host | index.js | Two sessionProjections units — costMeter (prices provider-reported usage and per-request fees) and costMeterFamily (this session plus every session it delegated to) — plus one balance route on Connection's /api channel |
| Client | client.js | One conversation.composer.dock entry that renders the family wire value, falling back to the session value, asking the Host for a family read while the projection is still incomplete, and reading the balance |
The Host folds the durable Session log and publishes two client-visible
projections; the Client renders them through the useProjection slot prop. Both
halves were written against documented seams only:
ctx.sessionProjections.register({key, stateVersion, stateSchema, init, apply, wire}) —
projection units; the registry calls only schema.parse, so the schemas here
are small hand-written normalizers with no schema-library dependency.SessionEvent envelope and payloads: request/header (route),
assistant/message / assistant/attempt (usage, from data.usage or the
last usage chunk of the compact stream), llm/retry-started (retry
replacement slot), compaction/summary (its own route and usage),
subagent/catalog (which child Sessions this one started, with the creation
time a checkpoint identity needs) and web/*-llm-request (per-request fees).
Unrecognized events return the same state reference, which the registry's
Object.is gate turns into zero downstream work.ctx.slots.inject(key, () => ctx.slots.register(options, Component)) and the
slot's standard useProjection prop.ctx.locale.register(ns, dicts) with locale: ns on the slot registration.Config is a Standard Schema object (~standard.validate), which
is exactly what Cordis resolveConfig reads, so no schemastery import is
needed. (The Config inspector reports such a schema as unsupported, so the
Plugins page draws no generated form; edit the values in YAML instead.)/api channel:
/api/dsh-cost-meter.balance and /api/dsh-cost-meter.family. The second one
exists because a projection view is synchronous and a settled subagent Session
can only be re-folded asynchronously; the Client asks for it while the
projection still reports unresolved children, so an exact total appears within
a second of the page loading rather than at that Session's next event. The read
fills the cache the projection view reads, so the pushed value is complete from
then on.ctx.remote.account.getBalance(metadata) for a signed-in account, and this
plugin's own exact Fetch route on Connection's /api channel
(/api/dsh-cost-meter.balance) for an API-key route. The route registers on a
separate optional child fiber that injects connection and credentials, and
the Client reaches it with the page's plain fetch — the same pattern shipped
UI plugins use (e.g. /api/present.open in dsh-client-ui-deliverables,
/api/session.export in dsh-session-log-export), which needs no Client
service to be resolved either. Two details are pinned because getting them
wrong is silent:
/api
(assertFetchRoute → endpointFromPath('/api', path)), which is why the
path is dotted rather than nested.connection.rpc.handle is not used: it mounts its route on the caller's
context (owner.webServer.register(route)), so it dies unless that fiber
also declares webServer, whereas connection.fetch.register only writes
into Connection's own route table. verify.mjs pins the dependency list and
the route shape.Both read paths are optional at runtime (ctx.get()), so the plugin still
activates and still shows cost in a composition without them. No Harness Client
package is imported, no DOM outside the component is written, no session event
is appended, and no credential ever leaves the Host process: the balance route
answers with wallet amounts only.
A subagent is a separate Session, so the family unit has to read another session's projection. Three constraints shape how it does that, and each one is load-bearing:
wire.view is invoked on every read, including the durable
checkpoint path (viewCheckpoint) where no live Session exists, and the
registry has no reentrancy guard. The family view therefore computes its own
spend from its own state (the same fold, run twice) and never reads its own
session's cells — only its children's.sessionQuery.observeSession
— the only read that re-folds a cold Session's complete log. So a child is read
live first, then from the durable checkpoint the cache wrote when it went cold
(the same live-then-cached shape dsh-session-reference uses), and only then
from an asynchronously filled cache. cachedSnapshot takes a header as its
identity witness and matches version, createdAt, cwd and isSeeded;
the parent's own header supplies all but createdAt, which subagent/catalog
records as childCreatedAt.stateVersion is not the registered one, so a settled Session that
finished under an older build answers no checkpoint read — for any key. The
plugin therefore folds such a child through sessionQuery.observeSession
(asynchronous, so it cannot run inside the view), caches the result, and asks
for that fold when a view misses. A settled Session's cost cannot change, so
one fold per child is enough; a child that runs again is read live again.unresolved,
never as zero, and the pill renders at least …. A child's own
costMeterFamily value already contains its descendants, which keeps this walk
one level deep and counts a grandchild exactly once.node verify.mjs runs 50 dependency-free checks with no browser and no running
profile: the Config schema, the balance route's dependency list and shape, the
fold's pricing and replacement rules, per-request fee counting, the family fold
and its live/durable/cached child resolution (including an unreadable child and a
throwing lookup), the family route's two-phase warm-up over settled children and
grandchildren, the projection's self-healing miss, every balance-route outcome, and a fake-React render of the
Client half that proves the dock entry emits the cost pill and the balance pill
together, shows the family total with its subagent and fee breakdown, degrades to
the session value when no family value exists, marks a subagent no source can
read as a lower bound, prefers the route only while it sees more than the
projection, reports Balance — with the upstream reason when the wallet cannot
be read, reports an unmounted route with its HTTP status, and stays silent for a
route with no configured wallet. It stubs the framework
rather than importing it, so it fails only when this plugin's own use of a seam
changes.
The fold was cross-checked against the shipped tokenUsage projection over a
real session (identical bucket totals) and the peak/off-peak selection is
unit-checked. The upstream wallet was called once with a stored key and answered
HTTP 200 with the expected wallet payload.
The plugin's own route cannot be exercised from a shell: it sits behind
Connection's Host/Origin fence and browser-cookie authentication, which answer
401 for a registered and an unregistered /api path alike. What a shell can
prove is that a running Host loaded this build at all — see below.
A Host process imports its plugin modules once, at boot, and never re-reads
them: editing index.js while the harness runs changes nothing, and a "restart"
that does not actually replace the process (a second dsh web that cannot bind
the port, or a page refresh) keeps the old build serving. The Client half is the
exception — it is served per request under an mtime-derived revision, so a page
refresh always picks up client.js.
STATE_VERSION therefore doubles as the build generation. The session
projection checkpoint ($DSH_HOME/storages/session_projcache/sessions/<id>.json)
records that version per row, so ver is the one way to tell from outside which
build a running process actually loaded:
python3 -c "import json,sys; print(json.load(open(sys.argv[1]))['record']['rows']['costMeter']['ver'])" \
"$DSH_HOME/storages/session_projcache/sessions/<session-id>.json"
Generation 2 is the first whose balance read is the exact Fetch route; generation
3 adds per-request fee accumulators. The family unit is registered beside the
spend unit at the same version, so a checkpoint written by an older build has no
costMeterFamily row and the family view simply falls back to the live read or
reports the child as unresolved until that child runs again.