Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 63 additions & 13 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,18 @@ src/client/panel-copy.ts English copy for the panel (deliberately NOT a locale
src/client/panel-slots.ts SlotMap merge for `main` + `sidebar.footer.action`.
src/client/panel-styles.ts The panel stylesheet + its `data-plugin-css` id (the
idempotence key `injectPanelCss` selects on).
src/client/prices.ts One-shot price-table controller over `commandcode/prices`.
src/client/prices.ts Price-table controller over `commandcode/prices` (cached,
bounded transient retries, manual retry, rebind reload).
src/client/session-cost.ts Session-cost calculation + copy (React-free).
src/client/session-cost-view.tsx The dock entry that feeds the injected cost.
src/client/session-cost-display.ts DOM injection into the harness's token-usage
pill and usage dialog (browser only).
src/client/session-cost-slots.ts SlotMap merge for `conversation.composer.dock`.
src/cost-facts.ts JSON-only billing facts shared by the Host projection and
the browser readout (groups, peak/hour rule, pricing key).
src/cost-projection.ts Durable `commandCodeCost` session projection: folds each
request's model, attempt time and prompt band so the
readout prices history instead of cumulative totals.
src/client/version.ts Plugin version for the settings-page footer (package.json import, inlined at build).
src/client/update.ts Update hint: throttled npm-registry `latest` check +
tolerant semver compare (React-free, storage/fetch/time
Expand Down Expand Up @@ -127,8 +133,11 @@ tests/session-cost-display.test.ts DOM-injection tests for the pill and the
usage dialog, driven through the real class and its
`doc`/`observe` seams against a fake DOM (confirmation,
self-heal, hide/restore, disposal).
tests/prices-client.test.ts price-table controller tests (one-shot cache, a
Host without the endpoint).
tests/cost-projection.test.ts durable cost-fact fold tests against the real
projection registry (v1/v2 settlements, retries, history
restore, tier boundaries, free/unpriced subtotals).
tests/prices-client.test.ts price-table controller tests (cache, bounded
transient retries, a Host without the endpoint).
tests/package.test.ts package-metadata contract (Harness peers start at rc.1,
no dsh-client-runtime).
tests/config-schema.test.ts Config credential contract: literal apiKey fields
Expand Down Expand Up @@ -219,16 +228,17 @@ tsdown.config.ts Build config (tsdown -> lib/, ESM, .d.ts + client.js).
- **Errors**: throw `LlmError` with stable codes. 401 → `INVALID_CREDENTIAL`; 429 → `RATE_LIMIT`; other HTTP → `PROVIDER_HTTP_ERROR` (403 body's `error.code`, e.g. `MODEL_NOT_IN_PLAN`, is parsed into the message). Unsupported options (`stop`) and image input throw `UNSUPPORTED_OPTION` / `UNSUPPORTED_CONTENT` rather than silently dropping.
- **Adapter is cordis-free** by design: `src/adapter.ts` takes a per-request `options()` thunk + `resolveApiKey()` from the plugin entry, so settings changes reach the next request without re-registration. It also accepts an injectable `fetchImpl` for tests.
- **Plans & quota panel + composer session cost (ported from PR #36)**: two
client-only surfaces on top of the EXISTING Host facts. (1) The sidebar footer
client surfaces using Host usage and durable request-cost facts. (1) The sidebar footer
card (`sidebar.footer.action`, order 1 — directly above Settings) and the
dashboard it opens in the layout's keyed `main` slot both render one
projection, `buildPanelView()` in `src/client/panel.ts`: plan, the 5-hour and
weekly windows with their own spend/limits, monthly credits derived the CLI's
way (`limit - remaining`), and the purchased/free balances. The projections
fetch nothing themselves: both read the usage controller's snapshot, and
`startPanelAutoRefresh()` is a refcounted 2-minute tick that calls
`usage.refresh()` while a credential is configured (it is the panel's only
fetch, and it skips the call — never the timer — without one).
`usage.refresh()` while mounted. The Host report determines configuration,
including composition literals and CLI-auth fallback; browser credential
references must never gate this read. Unreported credit fields stay dashes.
(2) The composer readout (`conversation.composer.dock`, id
`commandcode-session-cost` — never the shipped `stats` id, which would REPLACE
the harness's token/cache-hit/throughput cell) renders NO surface of its own:
Expand Down Expand Up @@ -265,6 +275,42 @@ tsdown.config.ts Build config (tsdown -> lib/, ESM, .d.ts + client.js).
The three slot declarations are re-stated locally
(`panel-slots.ts`, `session-cost-slots.ts`) and must stay structurally
identical to upstream's, exactly like the Models-card merge in `card.tsx`.
- **Durable session cost facts** (`src/cost-projection.ts`): optional reflective
`sessionProjections` registration folds `request/header` model selection and
`step/start` / `llm/retry-started` timestamps alongside v1 usage chunks and v2
assistant message/attempt stream settlements. Samples replace within an
attempt; retries add. Rate-equivalent requests aggregate into bounded groups,
using all prompt token buckets for each request's context tier. The pricing
fingerprint versions checkpoints and guards the client table; schema/fold
changes must bump the fingerprint seed. Restoring/forking uses the Host log,
not browser memory. Match all buckets against `tokenUsage` before decorating.
Never price cumulative usage with `modelSelection.lastUsed` or current time.
Missing projection means hidden cost; missing rates/other-provider usage
produce a labeled subtotal. Published-rate estimates are not provider invoices.
**KNOWN LIMITATION — one fold rule cannot serve both log generations.** The
replacement semantics here are the 0.1.5 token-meter's (`llm/retry-started`
closes the replacement slot, so a retried attempt ADDS). The 0.1.2/0.1.3-era
fold instead replaced on `(turn, step)` alone and never handled that event
(`dsh-client-connection`'s fixture projection is exactly that rule). On those
engines the two folds therefore disagree by the retried attempt's tokens, and
because the client requires per-bucket EQUALITY against `tokenUsage`
(`src/client/session-cost.ts`), a session that retried there loses the readout
entirely rather than showing a wrong figure. Retries are routine on this route
(429 plus the near-unbounded retry policy) and 0.1.2-rc.1/0.1.3 are declared
compatible in `package.json`, so this is a real gap, not a theoretical one.
Closing it means recording which fold rule wrote a group (or versioning the
projection per engine generation). Until then, do NOT "fix" the equality gate
by loosening it: that gate is what keeps a mismatched fold from being priced.
- **Settings usage card availability** (`usageCardState()` in
`src/client/usage.ts`): the card's auto-fetch, refresh button and "no key"
hint derive from the HOST report's `entry.configured`, never from a browser
credential reference. A composition literal (`Config.apiKey`) is a stripped
secret and the official CLI auth file is not in the credentials store, so both
are invisible to the browser while the Host happily serves requests with them
— `state.anyAccountConfigured` must not gate this card or the post-save
refresh trigger (`src/client/index.ts`). `shouldRefresh` is true only in
`status === 'idle'`, so a failed fetch never becomes an automatic request
loop, and the button stays enabled for a manual retry.
- **Price table Remote (`commandcode/prices`)**: `src/model-prices.ts` vendors
the official per-token rates and serves them Host-side over the SAME
`commandcodeUsage` service and one combined contribution (report + catalog +
Expand All @@ -277,7 +323,7 @@ tsdown.config.ts Build config (tsdown -> lib/, ESM, .d.ts + client.js).
instead of restating it; the model-independent half of that rule is
`isPeakPricingHour()` in `capabilities.ts`, which `peakPricingState()` now
delegates to. `CommandCodePricesController` (`src/client/prices.ts`) is a
one-shot cache, and both the namespace member and `UsageRemote.prices` are
cache with three bounded transient retries (1/2/4 seconds), and both the namespace member and `UsageRemote.prices` are
OPTIONAL because the Host and bundle can be a cross-version pair — a Host
without the endpoint lands in a permanent "no prices" state instead of
throwing. `tests/model-prices.test.ts` fails whenever a catalog model has no
Expand All @@ -286,9 +332,8 @@ tsdown.config.ts Build config (tsdown -> lib/, ESM, .d.ts + client.js).
**Syncing the table is a script, not a hand edit**: `node
scripts/sync-model-prices.mjs` re-reads the page's embedded model JSON, asserts
it still duplicates its base rates into `offPeak` and that peak ≥ off-peak,
prints a warning for every model carrying `contextTiers` (the row stores the
BASE band only, so those figures are a FLOOR for a session that crosses the
band), CROSS-CHECKS each rewritten row against the page's own rendered table,
carries every `contextTiers` band including its inclusive input-token bound,
CROSS-CHECKS each rewritten row against the page's own rendered table,
and rewrites only the `MODEL_PRICE_ROWS` literal. `--check` reports drift
without writing (exit 1 on drift, exit 2 when the page could not be read, so a
network failure never reads as "up to date"). The cross-check is the point: a
Expand All @@ -297,9 +342,14 @@ tsdown.config.ts Build config (tsdown -> lib/, ESM, .d.ts + client.js).
inconsistencies to leave alone rather than "fix" in the table: the four GPT
rows publish a literal `cacheWriteCost: 0` in the JSON while the rendered
column shows `—` (the table stays faithful to the machine-readable source),
and the browser restates the Mon–Fri rule that `isPeakPricingHour()` owns
(only the windows travel with the table), so a weekday-rule change is a
two-place edit.
and the Mon–Fri rule is restated in the browser because only the WINDOWS
travel with the table. The live copies are `isPeakPricingHour()` in
`capabilities.ts` (Host picker labels) and `peakHour()` in `cost-facts.ts`
(the readout's path, imported by the client bundle) — a weekday-rule change
is a two-place edit across those two. `isPeakHour()` in
`src/client/session-cost.ts` is a leftover third copy that nothing calls:
only `tests/session-cost.test.ts` references it, and it is dead-code-eliminated
out of `lib/client.js`. Delete it (and that one test) rather than re-wiring it.

- **Usage Remote (`commandcode/report`)**: the settings page's account card
fetches the usage report Host-side through the Typert Gateway — the browser
Expand Down
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
### Added
- **A plans & quota card in the sidebar and a dashboard behind it.** The bottom of the sidebar now carries a Command Code card — directly above Settings — showing the serving account's plan and both quota windows (5-hour and weekly) with their spend and limits. Clicking it opens a dashboard in the center column with the plan badge and billing period, the two windows as progress bars with reset times, monthly credit consumption, and the purchased/free credit balances. Both surfaces are driven by the same Host-side usage report the settings page's account card already uses, so no key ever reaches the browser, and the panel refreshes itself in the background on a 2-minute tick while it is on screen. The card is English by construction rather than following the harness language. It needs a harness whose layout can select a center panel — dsh 0.1.5 (rc.1) or newer: on an older engine the card is not registered at all, because a button that opens nothing is worse than no button, and every other screen is unaffected. Ported from [#36](https://github.com/Mars-Sea/dsh-commandcode-provider/pull/36) by [@xer-on](https://github.com/xer-on).

- **The composer now shows what the current session has cost, in dollars.** Under the input, next to the harness's own token counter, a session served by Command Code gains an amount (for example `1.2M tokens · Cache hit 87% · $0.02`), and the token-usage dialog the counter opens gains a price per row (uncached input, completion, cache read). The figures are computed from the session's own token buckets against Command Code's published per-token rates, including the peak/off-peak halves of the hourly-priced DeepSeek models. The readout decorates the harness's counter instead of replacing it, so the existing token/cache-hit/throughput figures and their translations stay exactly as they are. Three rules keep it honest: only Command Code sessions are priced, a model with no published cache-write rate says so rather than guessing a multiplier, and a session with nothing to price (no usage yet, an unknown model) renders no figure at all instead of a misleading `$0.00`. Ported from [#36](https://github.com/Mars-Sea/dsh-commandcode-provider/pull/36) by [@xer-on](https://github.com/xer-on).
- **The composer now shows a published-rate estimate for the current session.** Under the input, next to the harness's own token counter, a session served by Command Code gains an amount (for example `1.2M tokens · Cache hit 87% · ≈$0.02`), and the token-usage dialog the counter opens gains a price per row (uncached input, completion, cache read). The figures are computed from the session's own token buckets against Command Code's published per-token rates, including the peak/off-peak halves of the hourly-priced DeepSeek models. The readout decorates the harness's counter instead of replacing it, so the existing token/cache-hit/throughput figures and their translations stay exactly as they are. Three rules keep it honest: only Command Code sessions are priced, a model with no published cache-write rate says so rather than guessing a multiplier, and a session with nothing to price (no usage yet, an unknown model) renders no figure at all instead of a misleading `$0.00`. Ported from [#36](https://github.com/Mars-Sea/dsh-commandcode-provider/pull/36) by [@xer-on](https://github.com/xer-on).

### Changed
- **The model price table is served to the browser over a new `commandcode/prices` Remote.** The official per-token rates (input, output, cache-read, cache-write where published, plus the peak-hour override for the hourly-priced models) are vendored Host-side and reach the composer through the existing `commandcodeUsage` service, so the browser bundle carries no copy that could drift and a price row reaches an open page without a rebuild. A price row whose catalog name differs from the pricing page's slug (a vendor prefix, a dated suffix) is matched automatically, and a model the page does not price renders no cost rather than a guess. The table is pinned against the catalog by `tests/model-prices.test.ts`, which fails when a catalog model has no price — the visible decision point when upstream adds a model. It is refreshed with `node scripts/sync-model-prices.mjs`, which re-reads the page, cross-checks the embedded rates against the page's own rendered table and rewrites only the rows (`--check` reports drift without writing).

### Fixed
- **Free usage mixed with unpriced usage no longer displays a zero subtotal.** Purely free sessions retain `Free`, while partial sessions need positive priced spend before a figure is shown.
- **The settings usage card also resolves availability from the Host.** Literal and CLI-auth keys can fetch and refresh usage without a browser credential reference. The slash command omits its balance ratio when either input is unreported or the denominator is zero, and displays a single percent sign for known ratios.

- **Session estimates preserve each request's model and time across reloads and model switches.** An optional Host projection replays durable usage, including failed attempts and retry replacement rules; all eight context-tiered price rows retain their bands. Missing history hides the estimate, and unpriced or other-provider usage is labeled as a subtotal. The result uses the installed published-rate snapshot, not the provider invoice.
- **Quota refresh resolves credentials on the Host**, including literal config keys and CLI-auth fallback. Omitted purchased/free balances remain unknown instead of appearing as zero.
- **Transient price-table failures retry automatically** three times with bounded backoff. Host rebinds invalidate stale results; manual refresh remains available after retries are exhausted.

- **A session cost on a model with no published cache-write rate no longer shows `$0.00`.** If every billed token was one the price table has no rate for, the total was zero for lack of a rate rather than for lack of spending, and the readout announced a confident `$0.00`. It now renders nothing, exactly as it already did for an unknown model. Relatedly, a cache-write row whose rate IS published is no longer hidden from the usage dialog: its cost was already part of the total above it, so dropping the row left the visible figures unable to explain the amount.

- **The plans panel no longer reports a failed billing call as a fully consumed quota.** The credits endpoint is fetched in parallel with the plan and can fail on its own; the monthly balance then arrived as an absent number, which the panel read as `0` and turned into "100% used, quota exhausted" — with purchased/free tiles showing balances that were never fetched. Unreported figures now render as a dash, and the quota bar is simply not drawn until both halves of the ratio are known. Only the quota windows the endpoint actually reported get a row, so an unlimited plan no longer shows two fabricated 0%-of-limit meters.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Unofficial [DeepSeek Harness](https://deepseek-harness.github.io/deepseek-harnes
- **Reasoning-effort support** — models with selectable reasoning effort levels expose them in the picker.
- **Image input** — Vision-capable models accept images.
- **Plans & quota panel** — a Command Code card at the bottom of the sidebar (directly above Settings) shows the serving account's plan and its 5-hour and weekly windows; clicking it opens a dashboard with the billing period, both windows as progress bars with reset times, monthly credit consumption, and the purchased/free balances. Needs dsh 0.1.5 (rc.1) or newer, and reads in English on every harness language.
- **Session cost readout** — a session served by Command Code shows what it has cost, in dollars, next to the harness's own token counter under the composer, and the token-usage dialog gains a price per row. Rates come from Command Code's published price list, including the peak/off-peak halves of the hourly-priced DeepSeek models. The harness's own token/cache-hit/throughput figures are left exactly as they are, and no figure is shown when there is nothing honest to price (no usage yet, an unknown model, or a rate the price list does not publish). Also English on every harness language.
- **Session cost estimate** — published-rate estimates (`≈`) beside the composer token counter and in its usage dialog, using each request's model, request time and context tier from durable session history. Model switches and viewing the session later do not reprice earlier requests. Mixed-provider or unpriced usage shows a labeled subtotal (`≥`); missing history or wholly unpriceable usage stays hidden. These are estimates from the installed price snapshot, not provider invoices. Also English on every harness language.
- **Web search** — the dsh `web_search` tool is backed by the Command Code Provider API (`/alpha/web-search`) with the same key/endpoint as chat, so no separate search key or base URL is needed. See [Web search](#web-search).

See [Screenshots](#screenshots) below for what the UI looks like.
Expand Down
Loading
Loading