dsh-ui-cost-meter 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 yuu1111
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.ja.md ADDED
@@ -0,0 +1,124 @@
1
+ # dsh-ui-cost-meter
2
+
3
+ [English](README.md) | 日本語
4
+
5
+ DeepSeek Harness の Web GUI に いまのセッションがいくら使ったかを出すピルを1つ足す
6
+ プラグインです シェルが持つターン数とトークンのピルと同じ行に並びます
7
+
8
+ ```
9
+ ⏱ 1 turns 71 steps · 285 tok/s 🗄 7.3M tok · Cache hit 99% $ 0.42
10
+ ```
11
+
12
+ 金額はセッション投影の上に載っているため ログ全体の集計になり ページングや圧縮で
13
+ 変わりません 再読み込みでも消えず 値付けはハーネスがすでに耐久ログへ記録している
14
+ provider usage だけから行います ピルはシェルの統計ピルと同じ形で 押すと内訳の
15
+ パネルが開きます(入力・出力・キャッシュ・合計と 値付けに使った route)
16
+
17
+ ## 導入
18
+
19
+ ```sh
20
+ dsh plugin --profile web add dsh-ui-cost-meter
21
+ ```
22
+
23
+ ソースから使う場合は `add link:<path>` でも同じです プラグインが自分の bundle patch を
24
+ 持っているため profile の `cordis.patch.yml` を書き換えなくても行が入ります
25
+
26
+ ## 設定
27
+
28
+ 設定はすべてプラグインの行に置きます 単価はすべて **100万トークンあたり** で 通貨は
29
+ `symbol` が示すものに揃えてください 下の例は DeepSeek V4.1 Flash(`deepseek-flash`
30
+ OpenCode Go 側の id は `deepseek-v4.1-flash`)と DeepSeek V4 Pro を DeepSeek の
31
+ 公示単価(オフピーク)で値付けします 公式 API と OpenCode Go は同じ数字を請求します
32
+ 実際に払う額に合わせて書き換えてください
33
+
34
+ ```yaml
35
+ - id: cost-meter
36
+ name: dsh-ui-cost-meter
37
+ config:
38
+ symbol: '$'
39
+ rates:
40
+ opencode-go/deepseek-flash:
41
+ input: 0.15
42
+ output: 0.6
43
+ cacheRead: 0.003
44
+ cacheWrite: 0
45
+ opencode-go/deepseek-v4-pro:
46
+ input: 0.66
47
+ output: 1.98
48
+ cacheRead: 0.022
49
+ cacheWrite: 0
50
+ deepseek-official/deepseek-flash:
51
+ input: 0.15
52
+ output: 0.6
53
+ cacheRead: 0.003
54
+ cacheWrite: 0
55
+ deepseek-official/deepseek-v4-pro:
56
+ input: 0.66
57
+ output: 1.98
58
+ cacheRead: 0.022
59
+ cacheWrite: 0
60
+ ```
61
+
62
+ route ごとに1つしか置けないためここではオフピークを示します ピークはどの欄もちょうど
63
+ 倍で 月曜から金曜の 01:00-04:00 UTC と 06:00-10:00 UTC にあたります DeepSeek は
64
+ キャッシュ書きを課金しないため `cacheWrite` は `0` のままです 廃止された
65
+ `deepseek-v4-flash` と `deepseek-v4-flash-vision-exp` の id は現在 V4.1 Flash へ
66
+ 回され Flash の単価で課金されます OpenCode Go は月10ドルの定額プランで この単価は
67
+ トークンの請求額ではなく プランの使用上限に対する計量に使われます
68
+
69
+ 出典: [DeepSeek Models & Pricing](https://api-docs.deepseek.com/quick_start/pricing)
70
+ [OpenCode Go](https://opencode.ai/docs/go/)
71
+
72
+ | フィールド | 意味 |
73
+ |---|---|
74
+ | `symbol` | 金額の前に付ける記号 既定は `$` |
75
+ | `rates` | 単価表 キーは `"<provider>/<model>"` 次に `"<model>"` で route のキーが優先 |
76
+ | `fallback` | どのキーにも一致しない route の単価 省略すると既定では値付けしない |
77
+
78
+ 単価の無い route は推測せず **未設定** として数えます 合計には入らず ピルの金額に
79
+ 末尾の `+` が付きます 内訳のパネルには未設定のトークン数が出るため 単価の抜けが
80
+ 黙って間違った金額になることはありません
81
+
82
+ ## 数字の出どころ
83
+
84
+ ホスト側は `costMeter` セッション投影を登録します `request/header` で route を決め
85
+ 確定した `assistant/message`(表面にメッセージを残さなかった課金済みの試行は
86
+ `assistant/attempt`)が stream に埋め込んだ usage を畳みます 同じターンとステップの
87
+ 再報告は前の標本を差し替え `llm/retry-started` はその枠を閉じるため 再試行は
88
+ 足し込まれます 入力・出力・キャッシュ読み・キャッシュ書きのトークンを route の
89
+ 単価で値付けします
90
+
91
+ ブラウザ側は `conversation.composer.dock` 枠へ1つ登録し React の portal で
92
+ シェルの統計ピルの行(`[data-composer-stats]`)へ差し込みます そのため字・間隔・
93
+ 高さがその行のまま揃い 別の行を始めません 行がまだ無いセッション(ステップも
94
+ トークンも無い状態)では自前の行として中央に描きます
95
+
96
+ ピルとそのパネルはシェル側の宣言(`--dsw-*` のトークン・寸法・角丸・2列の行の
97
+ 組み方)をそのまま持ちます 外部のバンドルはシェルが配るプラットフォームモジュール
98
+ だけを読め その CSS Modules には届かないためです
99
+
100
+ ## 注意
101
+
102
+ - **請求書ではありません** ハーネスのトークン計数に設定の単価を掛けたものです
103
+ 正しいのは provider の請求で 単価を間違えればピルも間違えます
104
+ - **route ごとに1つの単価です** セッション途中で価格が変わる provider や
105
+ リクエストの大きさで単価が変わる route は 実際に払う額に合わせてください
106
+ - **通貨の換算はしません** `symbol` が示す通貨でそのまま出します
107
+
108
+ ## 開発
109
+
110
+ ```sh
111
+ bun install
112
+ bun run build # lib/index.js(ホスト側)と lib/client.js(ブラウザ側)
113
+ bun test # 値付け・整形・ビルド済みバンドルの検証
114
+ bun run check # 型チェック
115
+ ```
116
+
117
+ `lib/` は生成物で git からは除外しています `build.ts` はホスト側を ESM として
118
+ (`zod` と `@deepseek-ai/schemastery` は external) ブラウザ側を CJS へ束ねて
119
+ client modules が配信する `window.__ModuleLoader__.load({ id, factory })` 形式へ
120
+ 包みます
121
+
122
+ ## ライセンス
123
+
124
+ MIT
package/README.md ADDED
@@ -0,0 +1,137 @@
1
+ # dsh-ui-cost-meter
2
+
3
+ English | [日本語](README.ja.md)
4
+
5
+ Real-time spend for the DeepSeek Harness Web GUI: one pill under the composer,
6
+ beside the shipped turn/token pills, that says how much the session has cost so
7
+ far.
8
+
9
+ ```
10
+ ⏱ 1 turns 71 steps · 285 tok/s 🗄 7.3M tok · Cache hit 99% $ 0.42
11
+ ```
12
+
13
+ The amount rides the session projection seam, so it is whole-log (paging and
14
+ compaction cannot change it), it survives reloads, and it is priced from the
15
+ durable provider usage the harness already logs. The pill is shaped like the
16
+ shipped stats pills: clicking it opens a breakdown panel that repeats their
17
+ dialog's title, rule, and label/value rows, naming each cache bucket, the
18
+ unpriced tokens, and the route that priced them.
19
+
20
+ ## Install
21
+
22
+ ```sh
23
+ dsh plugin --profile web add dsh-ui-cost-meter
24
+ ```
25
+
26
+ Under a source checkout, `add link:<path>` works the same way. The plugin
27
+ declares its own bundle patch, so the row appears in the profile without
28
+ editing `cordis.patch.yml`.
29
+
30
+ ## Configure
31
+
32
+ Every field lives on the plugin row, and every price is per **one million
33
+ tokens** in whatever currency `symbol` names. The example below prices DeepSeek
34
+ V4.1 Flash (`deepseek-flash`; OpenCode Go's own id is `deepseek-v4.1-flash`)
35
+ and DeepSeek V4 Pro at DeepSeek's published off-peak rates, which the official
36
+ API and OpenCode Go both charge. Replace them with the rates you actually pay.
37
+
38
+ ```yaml
39
+ - id: cost-meter
40
+ name: dsh-ui-cost-meter
41
+ config:
42
+ symbol: '$'
43
+ rates:
44
+ opencode-go/deepseek-flash:
45
+ input: 0.15
46
+ output: 0.6
47
+ cacheRead: 0.003
48
+ cacheWrite: 0
49
+ opencode-go/deepseek-v4-pro:
50
+ input: 0.66
51
+ output: 1.98
52
+ cacheRead: 0.022
53
+ cacheWrite: 0
54
+ deepseek-official/deepseek-flash:
55
+ input: 0.15
56
+ output: 0.6
57
+ cacheRead: 0.003
58
+ cacheWrite: 0
59
+ deepseek-official/deepseek-v4-pro:
60
+ input: 0.66
61
+ output: 1.98
62
+ cacheRead: 0.022
63
+ cacheWrite: 0
64
+ ```
65
+
66
+ One rate per route can only carry the off-peak number, so that is what the
67
+ example shows: peak is exactly double in every field and covers 01:00-04:00 and
68
+ 06:00-10:00 UTC, Monday through Friday. DeepSeek bills no cache writes, so
69
+ `cacheWrite` stays `0`, and the retired `deepseek-v4-flash` and
70
+ `deepseek-v4-flash-vision-exp` ids now route to V4.1 Flash at the Flash price.
71
+ OpenCode Go is a $10/month plan, so its rates meter usage against the plan's
72
+ limits rather than pricing a per-token invoice.
73
+
74
+ Sources: [DeepSeek Models & Pricing](https://api-docs.deepseek.com/quick_start/pricing),
75
+ [OpenCode Go](https://opencode.ai/docs/go/).
76
+
77
+ | Field | Meaning |
78
+ |---|---|
79
+ | `symbol` | Prefixed to every amount. Default `$`. |
80
+ | `rates` | Unit prices keyed by `"<provider>/<model>"`, then `"<model>"`. The route key wins. |
81
+ | `fallback` | Unit prices for every route no key matches. Omit it to price nothing by default. |
82
+
83
+ A route with no matching rate is not guessed at: its tokens are counted as
84
+ **unpriced**, contribute nothing to the total, and make the pill mark its amount
85
+ with a trailing `+`. The panel then names how many tokens were left unpriced, so
86
+ a missing rate is visible instead of silently wrong.
87
+
88
+ ## Where the numbers come from
89
+
90
+ The host half registers the `costMeter` session projection. It folds the
91
+ durable log: a `request/header` sets the route, and each settled
92
+ `assistant/message` (or `assistant/attempt`, which preserves a billed attempt
93
+ that left no surface message) contributes the provider usage embedded in its
94
+ stream. A re-reported sample for the same turn and step replaces the earlier
95
+ one; `llm/retry-started` closes that slot so a retried attempt adds instead.
96
+ Input, output, cache-read, and cache-write tokens are then priced by the route's
97
+ rates.
98
+
99
+ The client half registers one entry on the `conversation.composer.dock` slot
100
+ and renders it into the shipped stats row (`[data-composer-stats]`) with a React
101
+ portal, so the cost pill shares that row's font, spacing, and vertical rhythm
102
+ instead of starting a row of its own. Without a stats row to join (a session
103
+ that has produced no steps or tokens yet) it falls back to drawing its own
104
+ centered row.
105
+
106
+ The pill and its panel carry the shell's own declarations — the same `--dsw-*`
107
+ tokens, sizes, radius, and two-column row grid as the shipped pills and their
108
+ dialog — because a third-party bundle can only reach the platform modules the
109
+ shell seeds, not its CSS modules.
110
+
111
+ ## Caveats
112
+
113
+ - **Not a billing record.** The figures are the harness's own token accounting
114
+ times the rates you configured. The provider's invoice is the authority; a
115
+ wrong rate in `rates` is a wrong pill.
116
+ - **One rate per route.** A provider that changes its price mid-session, or a
117
+ route whose price depends on the request size, needs a rate that reflects what
118
+ you actually pay.
119
+ - **Currency is yours to name.** Nothing here converts between currencies.
120
+
121
+ ## Development
122
+
123
+ ```sh
124
+ bun install
125
+ bun run build # lib/index.js (host half) and lib/client.js (browser half)
126
+ bun test # pricing fold, formatting, and the built bundle
127
+ bun run check # type check
128
+ ```
129
+
130
+ `lib/` is generated and ignored by git. `build.ts` bundles the host half as ESM
131
+ (with `zod` and `@deepseek-ai/schemastery` left external) and the browser half
132
+ as CJS wrapped in the `window.__ModuleLoader__.load({ id, factory })` loader
133
+ format the client module system serves.
134
+
135
+ ## License
136
+
137
+ MIT
@@ -0,0 +1,49 @@
1
+ # dsh-ui-cost-meter layer.
2
+ #
3
+ # Adds one plugin row. The host half registers the `costMeter` session
4
+ # projection (whole-log spend folded from durable provider usage) and the
5
+ # browser half adds one pill to the composer's stats dock.
6
+ #
7
+ # Configuration (row `config`):
8
+ # symbol: display symbol prefixed to every amount. Default '$'.
9
+ # rates: per-1M-token unit prices, keyed by "<provider>/<model>" or by
10
+ # "<model>". The route key wins over the bare model, which wins
11
+ # over `fallback`. Tokens whose route matches nothing are counted
12
+ # as unpriced and the pill marks its total with a trailing '+'.
13
+ # fallback: unit prices for every route no key matches. Omit it to price
14
+ # nothing by default, which keeps a wrong rate from looking right.
15
+ #
16
+ # The values below are DeepSeek's published off-peak rates in USD per 1M
17
+ # tokens, which the official API and OpenCode Go both charge. Off-peak is half
18
+ # of peak, and peak runs 01:00-04:00 and 06:00-10:00 UTC, Monday through Friday.
19
+ # Checked 2026-09-14 against:
20
+ # https://api-docs.deepseek.com/quick_start/pricing
21
+ # https://opencode.ai/docs/go/
22
+ # OpenCode Go is a $10/month plan that meters these rates against usage limits
23
+ # rather than billing per token. Replace them with the rates you actually pay.
24
+ - insert:
25
+ - id: cost-meter
26
+ name: dsh-ui-cost-meter
27
+ config:
28
+ symbol: '$'
29
+ rates:
30
+ opencode-go/deepseek-flash:
31
+ input: 0.15
32
+ output: 0.6
33
+ cacheRead: 0.003
34
+ cacheWrite: 0
35
+ opencode-go/deepseek-v4-pro:
36
+ input: 0.66
37
+ output: 1.98
38
+ cacheRead: 0.022
39
+ cacheWrite: 0
40
+ deepseek-official/deepseek-flash:
41
+ input: 0.15
42
+ output: 0.6
43
+ cacheRead: 0.003
44
+ cacheWrite: 0
45
+ deepseek-official/deepseek-v4-pro:
46
+ input: 0.66
47
+ output: 1.98
48
+ cacheRead: 0.022
49
+ cacheWrite: 0
package/lib/client.js ADDED
@@ -0,0 +1,314 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-ui-cost-meter",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ var __defProp = Object.defineProperty;
7
+ var __getOwnPropNames = Object.getOwnPropertyNames;
8
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
9
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
10
+ function __accessProp(key) {
11
+ return this[key];
12
+ }
13
+ var __toCommonJS = (from) => {
14
+ var entry = (__moduleCache ??= new WeakMap).get(from), desc;
15
+ if (entry)
16
+ return entry;
17
+ entry = __defProp({}, "__esModule", { value: true });
18
+ if (from && typeof from === "object" || typeof from === "function") {
19
+ for (var key of __getOwnPropNames(from))
20
+ if (!__hasOwnProp.call(entry, key))
21
+ __defProp(entry, key, {
22
+ get: __accessProp.bind(from, key),
23
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
24
+ });
25
+ }
26
+ __moduleCache.set(from, entry);
27
+ return entry;
28
+ };
29
+ var __moduleCache;
30
+ var __returnValue = (v) => v;
31
+ function __exportSetter(name, newValue) {
32
+ this[name] = __returnValue.bind(null, newValue);
33
+ }
34
+ var __export = (target, all) => {
35
+ for (var name in all)
36
+ __defProp(target, name, {
37
+ get: all[name],
38
+ enumerable: true,
39
+ configurable: true,
40
+ set: __exportSetter.bind(all, name)
41
+ });
42
+ };
43
+
44
+ // src/client.tsx
45
+ var exports_client = {};
46
+ __export(exports_client, {
47
+ apply: () => apply,
48
+ inject: () => inject
49
+ });
50
+ module.exports = __toCommonJS(exports_client);
51
+ var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
52
+ var import_react = require("react");
53
+ var import_react_dom = require("react-dom");
54
+
55
+ // src/shared.ts
56
+ var PROJECTION_KEY = "costMeter";
57
+ function amountDecimals(value) {
58
+ const safe = Number.isFinite(value) && value > 0 ? value : 0;
59
+ if (safe >= 1)
60
+ return 2;
61
+ if (safe >= 0.01)
62
+ return 3;
63
+ return 4;
64
+ }
65
+ function formatAmount(value) {
66
+ const safe = Number.isFinite(value) && value > 0 ? value : 0;
67
+ return safe.toFixed(amountDecimals(safe));
68
+ }
69
+ function formatTokens(value) {
70
+ if (!Number.isFinite(value) || value <= 0)
71
+ return "0";
72
+ if (value >= 1e6)
73
+ return `${trimZero(value / 1e6)}M`;
74
+ if (value >= 1000)
75
+ return `${trimZero(value / 1000)}k`;
76
+ return String(Math.round(value));
77
+ }
78
+ function trimZero(value) {
79
+ const fixed = value >= 100 ? value.toFixed(0) : value.toFixed(1);
80
+ return fixed.endsWith(".0") ? fixed.slice(0, -2) : fixed;
81
+ }
82
+ function breakdownRows(view) {
83
+ const decimals = amountDecimals(view.total);
84
+ const money = (value) => `${view.symbol}${(Number.isFinite(value) && value > 0 ? value : 0).toFixed(decimals)}`;
85
+ const rows = [
86
+ { label: "Uncached input", value: money(view.input) },
87
+ { label: "Cached input", value: money(view.cacheRead) }
88
+ ];
89
+ if (view.cacheWrite > 0)
90
+ rows.push({ label: "Cache write", value: money(view.cacheWrite) });
91
+ rows.push({ label: "Output", value: money(view.output) });
92
+ if (view.unpricedTokens > 0)
93
+ rows.push({
94
+ label: "Unpriced",
95
+ value: `${formatTokens(view.unpricedTokens)} tok`
96
+ });
97
+ const route = view.provider === null ? view.model : `${view.provider}/${view.model}`;
98
+ if (route !== null)
99
+ rows.push({ label: "Provider / model", value: route, route: true });
100
+ return rows;
101
+ }
102
+
103
+ // src/client.tsx
104
+ var jsx_runtime = require("react/jsx-runtime");
105
+ var DOCK = "conversation.composer.dock";
106
+ var STATS_ROW_SELECTOR = "[data-composer-stats]";
107
+ var PILL_ID = "cost";
108
+ var PANEL_TITLE = "Cost";
109
+ var PANEL_GAP = 8;
110
+ var PANEL_MARGIN = 12;
111
+ var MEASURE_STYLE = {
112
+ visibility: "hidden",
113
+ left: 0,
114
+ top: 0
115
+ };
116
+ var STYLE = `
117
+ .dsh-ui-cost-meter{max-width:var(--dsh-chat-content-width);box-sizing:border-box;width:100%;padding:4px calc(var(--dsh-composer-side-clearance) + 16px) 0px;font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(20px + var(--dsh-content-font-delta-secondary,0px));justify-content:center;gap:12px;margin:0 auto;display:flex}
118
+ .dsh-ui-cost-meter-anchor{min-width:0;display:inline-flex}
119
+ .dsh-ui-cost-meter-pill{box-sizing:border-box;max-width:100%;color:var(--dsw-alias-label-tertiary);font:inherit;font-variant-numeric:tabular-nums;line-height:inherit;white-space:nowrap;background:0 0;border:none;border-radius:24px;align-items:center;gap:6px;padding:1px 8px;display:inline-flex}
120
+ .dsh-ui-cost-meter-symbol{flex:none;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:12px;font-weight:600}
121
+ button.dsh-ui-cost-meter-pill{cursor:pointer}
122
+ button.dsh-ui-cost-meter-pill:hover,button.dsh-ui-cost-meter-pill[aria-expanded=true]{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}
123
+ .dsh-ui-cost-meter-label{text-overflow:ellipsis;min-width:0;overflow:hidden}
124
+ .dsh-ui-cost-meter-unpriced{margin-left:1px;font-size:11px}
125
+ .dsh-ui-cost-meter-panel{z-index:1100;box-sizing:border-box;background:var(--dsw-specific-menu);--dsw-elevation-stroke-color:var(--dsw-alias-border-l1);width:max-content;min-width:min(300px,100vw - 24px);max-width:min(440px,100vw - 24px);box-shadow:var(--dsw-elevation-prominent);color:var(--dsw-alias-label-secondary);cursor:default;border:0;border-radius:12px;padding:16px;font-size:12px;line-height:18px;position:fixed}
126
+ .dsh-ui-cost-meter-title{color:var(--dsw-alias-label-primary);justify-content:space-between;gap:16px;margin-bottom:8px;font-weight:500;display:flex}
127
+ .dsh-ui-cost-meter-titleRule{border-top:.5px solid var(--dsw-alias-border-l2);margin-bottom:10px}
128
+ .dsh-ui-cost-meter-titleValue{font-variant-numeric:tabular-nums}
129
+ .dsh-ui-cost-meter-titleLabel{align-items:center;gap:6px;min-width:0;display:inline-flex}
130
+ .dsh-ui-cost-meter-details{color:var(--dsw-alias-label-tertiary);grid-template-columns:minmax(76px,auto) minmax(0,1fr);gap:6px 16px;margin:0;display:grid}
131
+ .dsh-ui-cost-meter-details dt,.dsh-ui-cost-meter-details dd{min-width:0;margin:0}
132
+ .dsh-ui-cost-meter-details dd{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;text-align:right}
133
+ .dsh-ui-cost-meter-route{overflow-wrap:anywhere}
134
+ `;
135
+ var STYLE_TAG_ID = "dsh-ui-cost-meter/style.css";
136
+ var inject = ["slots"];
137
+ function useStatsRow() {
138
+ const anchorRef = import_react.useRef(null);
139
+ const [row, setRow] = import_react.useState(null);
140
+ import_react.useEffect(() => {
141
+ const container = anchorRef.current?.parentElement;
142
+ if (container === undefined || container === null)
143
+ return;
144
+ const sync = () => {
145
+ setRow((current) => {
146
+ const next = container.querySelector(STATS_ROW_SELECTOR);
147
+ return current === next ? current : next;
148
+ });
149
+ };
150
+ sync();
151
+ const observer = new MutationObserver(sync);
152
+ observer.observe(container, { childList: true, subtree: true });
153
+ return () => {
154
+ observer.disconnect();
155
+ };
156
+ }, []);
157
+ return { anchorRef, row };
158
+ }
159
+ function useCostDialog() {
160
+ const [open, setOpen] = import_react.useState(false);
161
+ const rootRef = import_react.useRef(null);
162
+ const panelRef = import_react.useRef(null);
163
+ const pos = import_dsh_client_ui_primitives.useAnchoredPosition({
164
+ open,
165
+ anchorRef: rootRef,
166
+ panelRef,
167
+ side: "top",
168
+ gap: PANEL_GAP,
169
+ margin: PANEL_MARGIN
170
+ });
171
+ import_dsh_client_ui_primitives.useDismissOnOutsidePointer(rootRef, open, setOpen, panelRef);
172
+ import_react.useEffect(() => {
173
+ if (!open)
174
+ return;
175
+ const onKeyDown = (event) => {
176
+ if (event.key === "Escape")
177
+ setOpen(false);
178
+ };
179
+ document.addEventListener("keydown", onKeyDown);
180
+ return () => {
181
+ document.removeEventListener("keydown", onKeyDown);
182
+ };
183
+ }, [open]);
184
+ return { open, setOpen, rootRef, panelRef, pos };
185
+ }
186
+ function CostPillBody({ view }) {
187
+ const { open, setOpen, rootRef, panelRef, pos } = useCostDialog();
188
+ const amount = `${view.symbol}${formatAmount(view.total)}`;
189
+ return /* @__PURE__ */ jsx_runtime.jsxs("span", {
190
+ ref: rootRef,
191
+ className: "dsh-ui-cost-meter-anchor",
192
+ children: [
193
+ /* @__PURE__ */ jsx_runtime.jsxs("button", {
194
+ type: "button",
195
+ className: "dsh-ui-cost-meter-pill",
196
+ "aria-haspopup": "dialog",
197
+ "aria-expanded": open,
198
+ "aria-label": `${PANEL_TITLE} ${amount}${view.unpricedTokens > 0 ? "+" : ""}`,
199
+ onClick: () => {
200
+ setOpen(!open);
201
+ },
202
+ children: [
203
+ /* @__PURE__ */ jsx_runtime.jsx("span", {
204
+ className: "dsh-ui-cost-meter-symbol",
205
+ "aria-hidden": true,
206
+ children: view.symbol
207
+ }),
208
+ /* @__PURE__ */ jsx_runtime.jsx("span", {
209
+ className: "dsh-ui-cost-meter-label",
210
+ children: formatAmount(view.total)
211
+ }),
212
+ view.unpricedTokens > 0 && /* @__PURE__ */ jsx_runtime.jsx("span", {
213
+ className: "dsh-ui-cost-meter-unpriced",
214
+ "aria-hidden": true,
215
+ children: "+"
216
+ })
217
+ ]
218
+ }),
219
+ open && import_react_dom.createPortal(/* @__PURE__ */ jsx_runtime.jsxs("div", {
220
+ ref: panelRef,
221
+ className: "dsh-ui-cost-meter-panel",
222
+ role: "dialog",
223
+ "aria-label": PANEL_TITLE,
224
+ style: pos ?? MEASURE_STYLE,
225
+ children: [
226
+ /* @__PURE__ */ jsx_runtime.jsxs("div", {
227
+ className: "dsh-ui-cost-meter-title",
228
+ children: [
229
+ /* @__PURE__ */ jsx_runtime.jsxs("span", {
230
+ className: "dsh-ui-cost-meter-titleLabel",
231
+ children: [
232
+ /* @__PURE__ */ jsx_runtime.jsx("span", {
233
+ className: "dsh-ui-cost-meter-symbol",
234
+ "aria-hidden": true,
235
+ children: view.symbol
236
+ }),
237
+ PANEL_TITLE
238
+ ]
239
+ }),
240
+ /* @__PURE__ */ jsx_runtime.jsx("span", {
241
+ className: "dsh-ui-cost-meter-titleValue",
242
+ children: amount
243
+ })
244
+ ]
245
+ }),
246
+ /* @__PURE__ */ jsx_runtime.jsx("div", {
247
+ className: "dsh-ui-cost-meter-titleRule",
248
+ "aria-hidden": true
249
+ }),
250
+ /* @__PURE__ */ jsx_runtime.jsx("dl", {
251
+ className: "dsh-ui-cost-meter-details",
252
+ "data-cost-meter-details": true,
253
+ children: breakdownRows(view).map((row) => /* @__PURE__ */ jsx_runtime.jsxs(import_react.Fragment, {
254
+ children: [
255
+ /* @__PURE__ */ jsx_runtime.jsx("dt", {
256
+ children: row.label
257
+ }),
258
+ /* @__PURE__ */ jsx_runtime.jsx("dd", {
259
+ className: row.route === true ? "dsh-ui-cost-meter-route" : undefined,
260
+ children: row.value
261
+ })
262
+ ]
263
+ }, row.label))
264
+ })
265
+ ]
266
+ }), document.body)
267
+ ]
268
+ });
269
+ }
270
+ function CostPill({ useProjection }) {
271
+ const view = useProjection(PROJECTION_KEY);
272
+ const { anchorRef, row } = useStatsRow();
273
+ const body = view === undefined || view.tokens === 0 ? null : /* @__PURE__ */ jsx_runtime.jsx(CostPillBody, {
274
+ view
275
+ });
276
+ return /* @__PURE__ */ jsx_runtime.jsxs(jsx_runtime.Fragment, {
277
+ children: [
278
+ /* @__PURE__ */ jsx_runtime.jsx("span", {
279
+ ref: anchorRef,
280
+ hidden: true
281
+ }),
282
+ body !== null && (row === null ? /* @__PURE__ */ jsx_runtime.jsx("div", {
283
+ className: "dsh-ui-cost-meter",
284
+ children: body
285
+ }) : import_react_dom.createPortal(body, row))
286
+ ]
287
+ });
288
+ }
289
+ function installStyle() {
290
+ const existing = document.getElementById(STYLE_TAG_ID);
291
+ if (existing !== null)
292
+ return () => {
293
+ return;
294
+ };
295
+ const style = document.createElement("style");
296
+ style.id = STYLE_TAG_ID;
297
+ style.textContent = STYLE;
298
+ document.head.append(style);
299
+ return () => {
300
+ style.remove();
301
+ };
302
+ }
303
+ function apply(ctx) {
304
+ ctx.effect(installStyle, "cost-meter: pill style");
305
+ ctx.slots.inject(DOCK, () => ctx.slots.register({
306
+ name: DOCK,
307
+ id: PILL_ID,
308
+ order: 10
309
+ }, CostPill));
310
+ }
311
+
312
+ return module.exports;
313
+ }
314
+ });
package/lib/index.js ADDED
@@ -0,0 +1,274 @@
1
+ // src/index.ts
2
+ import z from "@deepseek-ai/schemastery";
3
+ import { z as zod } from "zod";
4
+
5
+ // src/shared.ts
6
+ var PROJECTION_KEY = "costMeter";
7
+ var ZERO_PARTS = {
8
+ input: 0,
9
+ output: 0,
10
+ cacheRead: 0,
11
+ cacheWrite: 0
12
+ };
13
+ function readCount(value) {
14
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0)
15
+ return;
16
+ return value;
17
+ }
18
+ function readField(value, key) {
19
+ for (const [name, field] of Object.entries(value)) {
20
+ if (name === key)
21
+ return field;
22
+ }
23
+ return;
24
+ }
25
+ function bucketsFromUsage(usage) {
26
+ if (typeof usage !== "object" || usage === null)
27
+ return;
28
+ const uncachedInput = readCount(readField(usage, "inputTokens"));
29
+ const output = readCount(readField(usage, "outputTokens"));
30
+ if (uncachedInput === undefined || output === undefined)
31
+ return;
32
+ return {
33
+ uncachedInput,
34
+ output,
35
+ cacheRead: readCount(readField(usage, "cacheReadTokens")) ?? 0,
36
+ cacheWrite: readCount(readField(usage, "cacheWriteTokens")) ?? 0
37
+ };
38
+ }
39
+ function bucketsEqual(left, right) {
40
+ return left.uncachedInput === right.uncachedInput && left.output === right.output && left.cacheRead === right.cacheRead && left.cacheWrite === right.cacheWrite;
41
+ }
42
+ function tokenTotal(buckets) {
43
+ return buckets.uncachedInput + buckets.output + buckets.cacheRead + buckets.cacheWrite;
44
+ }
45
+ function resolveRates(settings, provider, model) {
46
+ const rates = settings.rates;
47
+ if (model !== null) {
48
+ if (provider !== null) {
49
+ const routed = rates[`${provider}/${model}`];
50
+ if (routed !== undefined)
51
+ return routed;
52
+ }
53
+ const bare = rates[model];
54
+ if (bare !== undefined)
55
+ return bare;
56
+ }
57
+ return settings.fallback;
58
+ }
59
+ function priceBuckets(buckets, rates) {
60
+ if (rates === undefined)
61
+ return;
62
+ return {
63
+ input: perMillion(buckets.uncachedInput, rates.input),
64
+ output: perMillion(buckets.output, rates.output),
65
+ cacheRead: perMillion(buckets.cacheRead, rates.cacheRead),
66
+ cacheWrite: perMillion(buckets.cacheWrite, rates.cacheWrite)
67
+ };
68
+ }
69
+ function perMillion(tokens, rate) {
70
+ return tokens / 1e6 * rate;
71
+ }
72
+ function addParts(left, right) {
73
+ return {
74
+ input: left.input + right.input,
75
+ output: left.output + right.output,
76
+ cacheRead: left.cacheRead + right.cacheRead,
77
+ cacheWrite: left.cacheWrite + right.cacheWrite
78
+ };
79
+ }
80
+ function subtractParts(left, right) {
81
+ return {
82
+ input: left.input - right.input,
83
+ output: left.output - right.output,
84
+ cacheRead: left.cacheRead - right.cacheRead,
85
+ cacheWrite: left.cacheWrite - right.cacheWrite
86
+ };
87
+ }
88
+ function totalOf(parts) {
89
+ return parts.input + parts.output + parts.cacheRead + parts.cacheWrite;
90
+ }
91
+
92
+ // src/index.ts
93
+ var name = "cost-meter";
94
+ var inject = ["sessionProjections"];
95
+ var RatesSchema = z.object({
96
+ input: z.number().min(0).default(0),
97
+ output: z.number().min(0).default(0),
98
+ cacheRead: z.number().min(0).default(0),
99
+ cacheWrite: z.number().min(0).default(0)
100
+ });
101
+ var Config = z.object({
102
+ symbol: z.string().default("$"),
103
+ rates: z.dict(RatesSchema).default({}),
104
+ fallback: RatesSchema
105
+ });
106
+ var partsSchema = zod.strictObject({
107
+ input: zod.number(),
108
+ output: zod.number(),
109
+ cacheRead: zod.number(),
110
+ cacheWrite: zod.number()
111
+ });
112
+ var bucketsSchema = zod.strictObject({
113
+ uncachedInput: zod.number(),
114
+ output: zod.number(),
115
+ cacheRead: zod.number(),
116
+ cacheWrite: zod.number()
117
+ });
118
+ var viewSchema = zod.strictObject({
119
+ symbol: zod.string(),
120
+ total: zod.number(),
121
+ input: zod.number(),
122
+ output: zod.number(),
123
+ cacheRead: zod.number(),
124
+ cacheWrite: zod.number(),
125
+ tokens: zod.number(),
126
+ unpricedTokens: zod.number(),
127
+ provider: zod.string().nullable(),
128
+ model: zod.string().nullable()
129
+ });
130
+ var stateSchema = zod.strictObject({
131
+ parts: partsSchema,
132
+ tokens: zod.number(),
133
+ unpricedTokens: zod.number(),
134
+ route: zod.strictObject({ provider: zod.string(), model: zod.string() }).nullable(),
135
+ last: zod.strictObject({
136
+ turn: zod.number(),
137
+ step: zod.number(),
138
+ buckets: bucketsSchema,
139
+ cost: partsSchema,
140
+ unpriced: zod.number()
141
+ }).nullable(),
142
+ wire: viewSchema
143
+ });
144
+ function usageOf(event) {
145
+ if (event.type === "assistant/message" && event.data.usage !== undefined)
146
+ return event.data.usage;
147
+ if (event.type !== "assistant/message" && event.type !== "assistant/attempt")
148
+ return;
149
+ return lastStreamUsage(event.data.stream);
150
+ }
151
+ function lastStreamUsage(stream) {
152
+ if (!Array.isArray(stream))
153
+ return;
154
+ for (let index = stream.length - 1;index >= 0; index -= 1) {
155
+ const record = stream[index];
156
+ if (record?.type === "chunk" && record.chunk?.type === "usage")
157
+ return record.chunk.usage;
158
+ }
159
+ return;
160
+ }
161
+ function buildView(parts, tokens, unpricedTokens, route, symbol) {
162
+ return {
163
+ symbol,
164
+ total: totalOf(parts),
165
+ input: parts.input,
166
+ output: parts.output,
167
+ cacheRead: parts.cacheRead,
168
+ cacheWrite: parts.cacheWrite,
169
+ tokens,
170
+ unpricedTokens,
171
+ provider: route?.provider ?? null,
172
+ model: route?.model ?? null
173
+ };
174
+ }
175
+ function withSample(state, previous, sample, route, symbol) {
176
+ const tokens = state.tokens - (previous === null ? 0 : tokenTotal(previous.buckets)) + tokenTotal(sample.buckets);
177
+ const unpricedTokens = state.unpricedTokens - (previous?.unpriced ?? 0) + sample.unpriced;
178
+ const parts = addParts(subtractParts(state.parts, previous?.cost ?? ZERO_PARTS), sample.cost);
179
+ return {
180
+ ...state,
181
+ parts,
182
+ tokens,
183
+ unpricedTokens,
184
+ last: sample,
185
+ wire: buildView(parts, tokens, unpricedTokens, route, symbol)
186
+ };
187
+ }
188
+ function withRoute(state, config, settings) {
189
+ if (state.route?.provider === config.provider && state.route.model === config.model)
190
+ return state;
191
+ const route = { provider: config.provider, model: config.model };
192
+ return {
193
+ ...state,
194
+ route,
195
+ wire: buildView(state.parts, state.tokens, state.unpricedTokens, route, settings.symbol)
196
+ };
197
+ }
198
+ function withoutRetriedSample(state, turn, step) {
199
+ return state.last !== null && state.last.turn === turn && state.last.step === step ? { ...state, last: null } : state;
200
+ }
201
+ function readSample(state, event, settings) {
202
+ if (event.type !== "assistant/message" && event.type !== "assistant/attempt")
203
+ return null;
204
+ const usage = usageOf(event);
205
+ if (usage === undefined)
206
+ return null;
207
+ const buckets = bucketsFromUsage(usage);
208
+ if (buckets === undefined)
209
+ return null;
210
+ const turn = event.data.turn;
211
+ const step = event.data.step;
212
+ const previous = state.last !== null && state.last.turn === turn && state.last.step === step ? state.last : null;
213
+ const rates = resolveRates(settings, state.route?.provider ?? null, state.route?.model ?? null);
214
+ const parts = priceBuckets(buckets, rates) ?? ZERO_PARTS;
215
+ return {
216
+ previous,
217
+ sample: {
218
+ turn,
219
+ step,
220
+ buckets,
221
+ cost: parts,
222
+ unpriced: rates === undefined ? tokenTotal(buckets) : 0
223
+ }
224
+ };
225
+ }
226
+ function costMeterProjection(settings) {
227
+ return {
228
+ key: PROJECTION_KEY,
229
+ stateVersion: 1,
230
+ stateSchema,
231
+ init: () => ({
232
+ parts: ZERO_PARTS,
233
+ tokens: 0,
234
+ unpricedTokens: 0,
235
+ route: null,
236
+ last: null,
237
+ wire: buildView(ZERO_PARTS, 0, 0, null, settings.symbol)
238
+ }),
239
+ apply: (state, event) => {
240
+ if (event.type === "request/header") {
241
+ return withRoute(state, event.data.header.config, settings);
242
+ }
243
+ if (event.type === "llm/retry-started") {
244
+ return withoutRetriedSample(state, event.data.turn, event.data.step);
245
+ }
246
+ const read = readSample(state, event, settings);
247
+ if (read === null)
248
+ return state;
249
+ if (read.previous !== null && bucketsEqual(read.previous.buckets, read.sample.buckets)) {
250
+ return state;
251
+ }
252
+ return withSample(state, read.previous, read.sample, state.route, settings.symbol);
253
+ },
254
+ wire: {
255
+ viewSchema,
256
+ view: (state) => state.wire
257
+ }
258
+ };
259
+ }
260
+ function apply(ctx, config) {
261
+ const settings = {
262
+ symbol: config?.symbol ?? "$",
263
+ rates: config?.rates ?? {},
264
+ ...config?.fallback === undefined ? {} : { fallback: config.fallback }
265
+ };
266
+ ctx.sessionProjections.register(costMeterProjection(settings));
267
+ }
268
+ export {
269
+ Config,
270
+ apply,
271
+ costMeterProjection,
272
+ inject,
273
+ name
274
+ };
package/package.json ADDED
@@ -0,0 +1,105 @@
1
+ {
2
+ "name": "dsh-ui-cost-meter",
3
+ "version": "0.0.0",
4
+ "description": "DeepSeek Harness Web GUI plugin: real-time spend for the current session, priced from per-model token rates and shown as a pill under the composer",
5
+ "homepage": "https://github.com/yuu1111/dsh-ui-cost-meter#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/yuu1111/dsh-ui-cost-meter/issues"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/yuu1111/dsh-ui-cost-meter.git"
12
+ },
13
+ "keywords": [
14
+ "cordis",
15
+ "deepseek-harness",
16
+ "dsh",
17
+ "dsh-plugin",
18
+ "cost",
19
+ "spend",
20
+ "price",
21
+ "billing",
22
+ "token",
23
+ "web-ui"
24
+ ],
25
+ "license": "MIT",
26
+ "publishConfig": {
27
+ "access": "public",
28
+ "registry": "https://registry.npmjs.org/"
29
+ },
30
+ "author": {
31
+ "name": "yuu1111"
32
+ },
33
+ "type": "module",
34
+ "main": "lib/index.js",
35
+ "exports": {
36
+ ".": "./lib/index.js",
37
+ "./client": "./lib/client.js"
38
+ },
39
+ "files": [
40
+ "lib",
41
+ "cordis.patch.yml",
42
+ "README.md",
43
+ "README.ja.md",
44
+ "LICENSE"
45
+ ],
46
+ "engines": {
47
+ "node": ">=20"
48
+ },
49
+ "packageManager": "bun@1.4.2",
50
+ "scripts": {
51
+ "build": "bun build.ts",
52
+ "check": "tsc -p tsconfig.json",
53
+ "test": "bun run build && bun test",
54
+ "lint": "biome check .",
55
+ "format": "biome check --write --unsafe ."
56
+ },
57
+ "dsh": {
58
+ "bundle": {
59
+ "patch": "./cordis.patch.yml"
60
+ },
61
+ "client": {
62
+ "platform": "web",
63
+ "inject": [
64
+ "@deepseek-ai/dsh-client-ui-conversation"
65
+ ]
66
+ }
67
+ },
68
+ "peerDependencies": {
69
+ "@deepseek-ai/cordis": "^4.0.2",
70
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.1.5-rc.2",
71
+ "@deepseek-ai/dsh-session-projection": "^0.1.5-rc.2",
72
+ "@deepseek-ai/schemastery": "^3.18.2",
73
+ "zod": "^4.6.2"
74
+ },
75
+ "peerDependenciesMeta": {
76
+ "@deepseek-ai/dsh-client-ui-conversation": {
77
+ "optional": true
78
+ }
79
+ },
80
+ "devDependencies": {
81
+ "@deepseek-ai/cordis": "^4.0.2",
82
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.1.5-rc.2",
83
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.1.5-rc.2",
84
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.5-rc.2",
85
+ "@deepseek-ai/dsh-llm": "^0.1.5-rc.2",
86
+ "@deepseek-ai/dsh-session": "^0.1.5-rc.2",
87
+ "@deepseek-ai/dsh-session-projection": "^0.1.5-rc.2",
88
+ "@deepseek-ai/schemastery": "^3.18.2",
89
+ "@types/bun": "^1.4.2",
90
+ "@types/react": "~19.3.0",
91
+ "react": "^19.3.0",
92
+ "typescript": "^7.0.2",
93
+ "zod": "^4.6.2",
94
+ "@deepseek-ai/dsh-client-ui-session": "^0.1.5-rc.2",
95
+ "@deepseek-ai/dsh-client-ui-renderer": "^0.1.5-rc.2",
96
+ "@deepseek-ai/dsh-llm-retry": "^0.1.5-rc.2",
97
+ "react-dom": "^19.3.0",
98
+ "@types/react-dom": "~19.3.0",
99
+ "@biomejs/biome": "^2.5.13",
100
+ "@yuu1111/biome-config": "^2.2.0",
101
+ "@yuu1111/tsconfig": "^1.0.7",
102
+ "@yuu1111/knip-config": "^1.0.7",
103
+ "knip": "^6.35.1"
104
+ }
105
+ }