dsh-session-cost-meter 0.0.0-stage → 0.1.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 Felix Li
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.md CHANGED
@@ -1,3 +1,285 @@
1
- # Temporary Holding Version
1
+ # dsh-session-cost-meter
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A DeepSeek Harness profile bundle that answers "what did **this session** cost?"
4
+ It shows a cost pill beside the composer, breaks the spend down by billing bucket,
5
+ tariff window and model, and prices every request with the rate that was in force
6
+ when that request was **dispatched** — so peak/off-peak (波峰/波谷) is handled per
7
+ request instead of being guessed from a session total.
8
+
9
+ ```
10
+ ┌ 本次会话成本 ¥2.3691 ┐
11
+ │ 缓存命中 24,734,848 tok ¥0.9894 │
12
+ │ 未缓存输入 207,128 tok ¥0.4143 │
13
+ │ 输出 120,684 tok ¥0.9655 │
14
+ │ 合计 25,062,660 tok ¥2.3691 │
15
+ ├───────────────────────────────────────────┤
16
+ │ 波峰 ¥2.3691 · 波谷 ¥0.0000 │
17
+ ├───────────────────────────────────────────┤
18
+ │ deepseek-flash 25.1M tok ¥2.3691 │
19
+ ├───────────────────────────────────────────┤
20
+ │ ● 当前时段:波峰 · 距切换 2h13m │
21
+ │ 高峰:周一至周五 09:00–12:00、14:00–18:00 │
22
+ │ 波谷价 = 波峰价 × 0.5 │
23
+ └───────────────────────────────────────────┘
24
+ ```
25
+
26
+ ## Where it appears
27
+
28
+ One pill joins the existing token chip on the composer's ambient row
29
+ (`conversation.composer.dock`, occupant id `session-cost`, order 5, so it sits
30
+ immediately after the shipped `stats` entry). Clicking it opens the breakdown
31
+ above. All text is localised through the Client locale service (`zh` + `en`), and
32
+ all styling uses only `--dsw-alias-*` theme tokens, so it follows the host theme
33
+ and light/dark switching.
34
+
35
+ ## Pricing model
36
+
37
+ Rates are per 1,000,000 tokens, quoted exactly as published — the CNY column is
38
+ authoritative, and the USD column is stored separately rather than derived,
39
+ because the page rounds it independently.
40
+
41
+ | model | 缓存命中 (hit) peak | hit off-peak | 未缓存输入 (miss) peak | miss off-peak | 输出 (output) peak | output off-peak |
42
+ |---|---|---|---|---|---|---|
43
+ | `deepseek-flash` (CNY) | 0.04 | 0.02 | 2.00 | 1.00 | 8.00 | 4.00 |
44
+ | `deepseek-flash` (USD) | 0.006 | 0.003 | 0.30 | 0.15 | 1.20 | 0.60 |
45
+ | `deepseek-v4-pro` (CNY) | 0.30 | 0.15 | 9.00 | 4.50 | 27.00 | 13.50 |
46
+ | `deepseek-v4-pro` (USD) | 0.044 | 0.022 | 1.32 | 0.66 | 3.96 | 1.98 |
47
+
48
+ Source: <https://api-docs.deepseek.com/zh-cn/quick_start/pricing/> (CNY) and
49
+ <https://api-docs.deepseek.com/quick_start/pricing/> (USD), effective
50
+ 2026-09-10 04:00 UTC. Off-peak is exactly half of peak for every model and every
51
+ bucket.
52
+
53
+ ### Peak (波峰) / off-peak (波谷)
54
+
55
+ The rule this implements, verbatim from DeepSeek:
56
+
57
+ > 闲时段价格为高峰时段价格的一半。北京时间周一至周五(不含中国法定节假日)9:00 - 12:00、14:00 - 18:00 为高峰时段;其余时段,包括周末及中国法定节假日全天均为空闲时段。
58
+
59
+ Peak is **Monday–Friday, 09:00–12:00 and 14:00–18:00 Beijing time**
60
+ (01:00–04:00 and 06:00–10:00 UTC), **excluding Chinese public holidays**.
61
+ Everything else — every evening, every night, all weekend, and all holiday time —
62
+ is off-peak at half price.
63
+
64
+ DeepSeek publishes the windows but **never the holiday calendar**, so the plugin
65
+ ships it: `PRC_HOLIDAYS_2026` in `lib/tariff.js` carries the 33 published 2026
66
+ dates from 《国务院办公厅关于2026年部分节假日安排的通知》(郭办发明电〔2025〕7 号), and
67
+ the bundle patch repeats the same list so an already-installed plugin is correct
68
+ without waiting for a restart (a test asserts the two lists are identical).
69
+
70
+ **Make-up working days stay off-peak.** The notice designates Saturday/Sunday
71
+ workdays in 2026 (01-04, 02-14, 02-28, 05-09, 09-20, 10-10). Those are
72
+ deliberately *not* holidays and *not* peak: the rule defines peak as
73
+ Monday–Friday and says every other period, "including weekends", is off-peak.
74
+ `peakWeekdays` already excludes them.
75
+
76
+ Holidays are off-peak for the **whole local day**, so on 2026-10-01 the
77
+ 09:00–12:00 and 14:00–18:00 windows bill at half price. Without the calendar,
78
+ holiday hours would be billed at peak — roughly **doubling** the reported cost —
79
+ so the panel warns in two cases: no calendar at all, or a calendar that no longer
80
+ covers the year the tariff clock is in (`holidayYears` is published for this).
81
+ A user's `config.holidays` replaces the shipped list entirely.
82
+
83
+ ### How each request is priced
84
+
85
+ * `cost = cacheMiss × missRate + cacheHit × hitRate + cacheWrite × writeRate + output × outputRate`
86
+ * The four buckets are the harness's **disjoint** counts. DeepSeek's API reports a
87
+ cache-inclusive `prompt_tokens`; the adapter already subtracts cache hits, so
88
+ `inputTokens` is the cache-miss count and nothing is charged twice.
89
+ * `reasoningTokens` is a subset of `outputTokens` and is deliberately **not**
90
+ charged again.
91
+ * `cacheWrite` is reported by the API but not billed, so its rate is 0.
92
+ * The tariff instant for a request is its **`step/start` time**, not the
93
+ `assistant/message` time. The settlement message is appended after the stream
94
+ finishes, so a request dispatched at 11:59 Beijing and settled at 12:01 would
95
+ otherwise be billed in the wrong window. Without a `step/start` the settlement
96
+ time is used.
97
+ * A model with no published rate keeps its tokens but adds **no** money; the panel
98
+ then warns how many tokens were excluded instead of inventing a price.
99
+ * Attempt samples are replaced, not accumulated: a retried request is billed once
100
+ per attempt, and a repeat usage sample for the same attempt does not
101
+ double count.
102
+
103
+ ## Configuration
104
+
105
+ Tunables live in the plugin row's `config`, which the profile's own patch layer
106
+ can override without touching this bundle:
107
+
108
+ ```yaml
109
+ # ~/.dsh/profiles/<profile>/cordis.patch.yml
110
+ - id: session-cost
111
+ config:
112
+ currency: USD # CNY (default) or USD
113
+ holidays: # replaces the shipped calendar entirely
114
+ - 2027-01-01
115
+ - 2027-01-02
116
+ rates:
117
+ offPeakMultiplier: 0.5 # off-peak multiplier
118
+ models:
119
+ deepseek-flash:
120
+ output: 8 # override one rate
121
+ my-model:
122
+ cacheHit: 0.01
123
+ cacheMiss: 0.5
124
+ cacheWrite: 0
125
+ output: 1
126
+ ```
127
+
128
+ `Config` is a Standard Schema validator, so a misspelled key, a negative rate, a
129
+ malformed date or an out-of-range window fails the plugin **loudly at startup**
130
+ instead of silently pricing the session with defaults. Because it is not a
131
+ schemastery schema, the Plugin Manager page shows these fields as unknown
132
+ configuration (`status: unsupported`) rather than rendering a form; the values
133
+ still work.
134
+
135
+ ⚠️ **The shipped calendar covers 2026 only.** The State Council publishes each
136
+ year's arrangement in the previous November, so around 2027-01-01 this list must
137
+ be extended (or replaced via `config.holidays`). Until then a 2027 statutory
138
+ holiday would be priced as a peak weekday, **doubling** the reported cost for
139
+ those days — and the panel says exactly that when the tariff clock leaves the
140
+ covered years.
141
+
142
+ ## Install
143
+
144
+ From the registry, by bundle name:
145
+
146
+ ```
147
+ plugin_manager install_bundle target: dsh-session-cost-meter
148
+ ```
149
+
150
+ or from a checkout / tarball, by absolute path:
151
+
152
+ ```
153
+ plugin_manager install_bundle target: <absolute path to this directory>
154
+ ```
155
+
156
+ Either form runs `pnpm add <spec>` in the active profile, appends the bundle to
157
+ `dsh.profile.bundles`, and applies the change live (`application: applied`). The
158
+ same is available from the CLI as `dsh plugin --profile <name> add <spec>`, and
159
+ from the Web UI's Plugins settings page, which lists the bundle once installed and
160
+ lets you disable it. Add `- id: session-cost disabled: true` to the profile patch
161
+ layer to turn it off without uninstalling.
162
+
163
+ ## Uninstall
164
+
165
+ ```
166
+ plugin_manager remove_bundle target: dsh-session-cost-meter
167
+ ```
168
+
169
+ ## Releasing
170
+
171
+ The package is a **bundle**: an npm package whose manifest declares
172
+ `dsh.bundle` and whose `cordis.patch.yml` inserts the loader row. Publishing is
173
+ therefore an ordinary npm publish, and `private` must stay absent. The `prepack`
174
+ script runs the whole suite, so a broken package cannot be uploaded.
175
+
176
+ ```sh
177
+ # authenticate once, against the registry that accepts new publishes
178
+ npm login --registry=https://registry.npmjs.org/
179
+
180
+ # then, from the repo root
181
+ pnpm pack # optional: inspect the tarball first
182
+ pnpm publish --registry=https://registry.npmjs.org/
183
+ ```
184
+
185
+ `pnpm publish --dry-run` performs the full flow without uploading and is the safe
186
+ way to check a change to the manifest. Pass `--no-git-checks` only when
187
+ publishing from a detached or dirty tree.
188
+
189
+ Use the Harness's bundled Node 24 (`load_workspace_dependencies`) rather than an
190
+ older shell Node: `npm@11.7` on Node 23.0.0 mangles paths inside `npm pack`
191
+ (`ib/client.js` ENOENT) and cannot produce a tarball at all. The configured
192
+ `~/.npmrc` here points at `registry.npmmirror.com`, a read-only mirror — always
193
+ name `--registry=https://registry.npmjs.org/` explicitly when publishing.
194
+
195
+ `tests/package.test.mjs` is the release gate. A package name appears in three
196
+ places — the manifest, the loader row in `cordis.patch.yml`, and the Client
197
+ bundle's `window.__ModuleLoader__.load({ id })` — and the suite fails unless all
198
+ three agree, so a rename cannot ship half-applied.
199
+
200
+ ## Verification status
201
+
202
+ * 50 unit tests (`node --test "tests/*.test.mjs"`) cover the published windows
203
+ against fixed instants, a minute-by-minute sweep of the schedule the Client
204
+ reads against the host rules, the host/Client pure-function copies against each
205
+ other, the per-bucket arithmetic, replacement/retry semantics, reference
206
+ stability, schema validation, config validation, the shipped 2026 holiday
207
+ calendar (count, format, no duplicates, make-up weekends excluded) and the
208
+ panel's covered/stale/missing holiday reporting.
209
+ * A real `deepseek-flash` session log (193 billing-relevant events, metadata
210
+ only — no message content) folds to the totals computed independently from the
211
+ same log: ¥1.16035704 over 10,816,537 tokens in 96 attempts.
212
+ * Live in the running profile: the host fiber is `active`, the Client occupant
213
+ `session-cost` is registered on `conversation.composer.dock` beside `stats`, and
214
+ the `sessionCost` row is checkpointed in the projection cache with values whose
215
+ arithmetic checks out exactly (¥2.36912192 = 0.98939392 + 0.414256 + 0.965472).
216
+ An independent fold of the durable log reproduced that row exactly (160
217
+ attempts, 28,216,576 / 209,990 / 131,665 tokens, ¥2.60196304).
218
+ * The holiday calendar was confirmed live after re-applying the row: the running
219
+ projection reports all **33** published 2026 dates, `2026-01-01` through
220
+ `2026-10-07`, so the National Day closure now bills off-peak.
221
+ * **Not verified visually**: no browser control is available in this environment,
222
+ so the panel's rendered appearance has not been observed; only its syntax, its
223
+ manifest, its live slot registration and its data are verified.
224
+ * Host-side code changes need an app restart to take effect (client bundles
225
+ hot-reload). The first install is live immediately; a row toggle re-applies
226
+ patch-level config without a restart.
227
+
228
+ ## Design notes
229
+
230
+ * The host half is a `sessionCost` **session projection**. It registers no
231
+ services, subscribes to nothing, and imports **no packages** — not even Cordis
232
+ or a schema library — so it cannot disagree with the host about a module
233
+ instance. `apply(state, event)` returns the same state reference for events it
234
+ ignores, and `wire.view` returns the same object when the view content is
235
+ unchanged, so an unrelated event never re-renders a client.
236
+ * The client half derives the current tier from a **schedule the host sends**
237
+ (the tier in force plus its flip instants) rather than re-implementing the
238
+ window rules; the only duplicated functions are a five-line parity lookup and a
239
+ bucket sum, and a test extracts both from the bundle and checks them directly.
240
+ * No Harness Client package is imported (`react` and `react-dom` from the browser
241
+ module table only), which is why the pill draws its own icon and copies the
242
+ shipped panel's spacing instead of importing primitives.
243
+ * **Text color convention**: `--dsw-alias-label-secondary` is the resting tone for
244
+ labels, section headings, notes and the pill; `--dsw-alias-label-primary` is
245
+ reserved for values and names. The `--dsw-alias-state-*` tokens are *indicator*
246
+ colors only (the tier dot, the unpriced warning) — using one for ordinary body
247
+ text renders a near-invisible gray, which is a mistake this plugin made in its
248
+ first revision.
249
+ * A per-model row's token total is summed from its buckets when the host did not
250
+ publish a `total`, so a running host that still holds its previous module
251
+ generation renders correctly.
252
+
253
+ ## Known limitations
254
+
255
+ * **Holiday calendar** — DeepSeek never publishes it; the plugin ships the 2026
256
+ list from the State Council notice and must be extended for each new year once
257
+ the arrangement is published (see above). `config.holidays` replaces it.
258
+ * **Make-up working days** — treated as off-peak, following the rule's wording
259
+ that peak is Monday–Friday and weekends are off-peak. If a future DeepSeek
260
+ notice bills makeup workdays at peak, that cannot be expressed by a weekly
261
+ weekday mask and would need a per-date override.
262
+ * **`deepseek-v4-pro` routing conflict** — DeepSeek's news post (2026-09-10) says
263
+ V4-Pro requests route to V4.1-Flash at Flash rates from 2026-09-14, while the
264
+ changelog from the same date says V4-Pro keeps serving at unchanged billing.
265
+ This plugin prices V4-Pro at its own published rates; override
266
+ `rates.models.deepseek-v4-pro` if your account bills it as Flash.
267
+ * **Retired ids** — `deepseek-chat` and `deepseek-reasoner` were discontinued
268
+ (2026-07-24) and have no current published rate, so they are reported as
269
+ unpriced rather than guessed. `deepseek-v4-flash` and
270
+ `deepseek-v4-flash-vision-exp` are aliased to `deepseek-flash`, which is what
271
+ the provider bills them at.
272
+ * **Rate drift** — DeepSeek can change prices; this table is a snapshot. Override
273
+ it in config rather than editing `lib/tariff.js`.
274
+ * **Schedule horizon** — the Client's tariff schedule covers 14 days, refreshed on
275
+ every session event. Past that horizon with no new events at all, the pill falls
276
+ back to the host's last computed tier.
277
+ * **Prompt-caching effects** — cost follows provider-reported cache hits, so a
278
+ change in cache behaviour changes the bill, not the estimate. No request is
279
+ predicted or pre-priced.
280
+
281
+ ## Prior art
282
+
283
+ `@goodandready/dsh-cost-meter` (MIT, third-party) is an independently published
284
+ DSH plugin with the same goal; this bundle was written from the running harness's
285
+ own APIs and does not depend on it.
@@ -0,0 +1,63 @@
1
+ # Bundle patch for dsh-session-cost-meter.
2
+ #
3
+ # The bundle owns exactly one loader row: the host plugin that registers the
4
+ # `sessionCost` session projection.
5
+ #
6
+ # `holidays` repeats the calendar lib/tariff.js ships as `PRC_HOLIDAYS_2026`.
7
+ # Config is read when the profile recomposes, while a running Host keeps its
8
+ # loaded JavaScript generation until restart, so carrying the calendar here makes
9
+ # an already-installed plugin correct without waiting for a restart. A test
10
+ # asserts the two lists are identical, and the user's profile patch layer can
11
+ # still override any of it, e.g.
12
+ #
13
+ # - id: session-cost
14
+ # config:
15
+ # currency: USD
16
+ # holidays: ['2027-01-01']
17
+ #
18
+ # Source: 《国务院办公厅关于2026年部分节假日安排的通知》(郭办发明电〔2025〕7 号).
19
+ - insert:
20
+ - id: session-cost
21
+ name: 'dsh-session-cost-meter'
22
+ config:
23
+ holidays:
24
+ # New Year's Day: January 1 (Thu) - January 3 (Sat).
25
+ - '2026-01-01'
26
+ - '2026-01-02'
27
+ - '2026-01-03'
28
+ # Spring Festival: February 15 (Sun) - February 23 (Mon).
29
+ - '2026-02-15'
30
+ - '2026-02-16'
31
+ - '2026-02-17'
32
+ - '2026-02-18'
33
+ - '2026-02-19'
34
+ - '2026-02-20'
35
+ - '2026-02-21'
36
+ - '2026-02-22'
37
+ - '2026-02-23'
38
+ # Qingming Festival: April 4 (Sat) - April 6 (Mon).
39
+ - '2026-04-04'
40
+ - '2026-04-05'
41
+ - '2026-04-06'
42
+ # Labor Day: May 1 (Fri) - May 5 (Tue).
43
+ - '2026-05-01'
44
+ - '2026-05-02'
45
+ - '2026-05-03'
46
+ - '2026-05-04'
47
+ - '2026-05-05'
48
+ # Dragon Boat Festival: June 19 (Fri) - June 21 (Sun).
49
+ - '2026-06-19'
50
+ - '2026-06-20'
51
+ - '2026-06-21'
52
+ # Mid-Autumn Festival: September 25 (Fri) - September 27 (Sun).
53
+ - '2026-09-25'
54
+ - '2026-09-26'
55
+ - '2026-09-27'
56
+ # National Day: October 1 (Thu) - October 7 (Wed).
57
+ - '2026-10-01'
58
+ - '2026-10-02'
59
+ - '2026-10-03'
60
+ - '2026-10-04'
61
+ - '2026-10-05'
62
+ - '2026-10-06'
63
+ - '2026-10-07'