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 +21 -0
- package/README.ja.md +124 -0
- package/README.md +137 -0
- package/cordis.patch.yml +49 -0
- package/lib/client.js +314 -0
- package/lib/index.js +274 -0
- package/package.json +105 -0
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
|
package/cordis.patch.yml
ADDED
|
@@ -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
|
+
}
|