@rayadesu/dsh-llm-billing 0.3.9 → 0.3.10

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/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # last confirmed-consistent state. Both languages carry equal authority; after
3
3
  # editing either side, bring the other along and re-record both hashes with:
4
4
  # git hash-object README.md README.zh.md
5
- README.md: 453fdb62c6081c6f5704bf878888c326b523d5e1
6
- README.zh.md: c147bff571b7e00abebb1fad188a11058354bbc5
5
+ README.md: d6b416ee95bf7cd53032e6685fa1cc8b03ce0fa6
6
+ README.zh.md: 7370b6c25af9aba7250402f1a22ad4a3870e9b58
package/README.md CHANGED
@@ -16,7 +16,7 @@ Add the plugin to a composition (a `cordis.yml` row) and give it a credential. I
16
16
  # baseURL: https://api.deepseek.com
17
17
  ```
18
18
 
19
- The plugin registers the `billing` Remote with six methods: `getBalance(force?)` (the parsed `/user/balance` snapshot; a snapshot younger than the 15-second host TTL is reused, `force` bypasses it, and each request aborts after 5 seconds), `getSessionSpend(sessionId)` (one session's billed cost), `getTodaySpend(force?)` (every session's billed cost on the current Beijing-time calendar day; `force` bypasses the host-side cache, for the badge's manual refresh), `getTodaySessionsSpend(force?)` (today's billed cost per session, sorted by cost descending, each row carrying the session's durable title), `getTurnSpend(sessionId, messageId)` (one completed turn's billed cost, located by its closing assistant message id), and `getSessionTurnSpends(sessionId)` (every completed turn's cost as a `messageId → total` map, folded in one pass — the transcript renders one row per message, so the client fetches this once per session instead of calling `getTurnSpend` per row). The spend prices each provider-reported usage sample — an `assistant/message`'s usage, or a failed/retried `assistant/attempt`'s stream usage, priced with the model of the latest `request/header` — at the official rate of the sample's own Beijing-time peak/off-peak classification. Peak windows apply weekdays (Monday–Friday) only, and weekends are always off-peak. A sample for the same `(turn, step)` replaces the earlier one and `llm/retry-started` makes the retried attempt add, matching DSH's own turn-usage disclosure; costs then sum per model. A turn is the `turn/start`..`turn/end` range holding the closing message; the ranking folds each session's title from its latest `session/title` event (last-wins, so a rename is reflected as soon as its event commits and the session is re-read).
19
+ The plugin registers the `billing` Remote with six methods: `getBalance(force?)` (the parsed `/user/balance` snapshot; a snapshot younger than the 15-second host TTL is reused, `force` bypasses it, and each request aborts after 5 seconds), `getSessionSpend(sessionId)` (one session's billed cost), `getTodaySpend(force?)` (every session's billed cost on the current Beijing-time calendar day; `force` bypasses the host-side cache, for the badge's manual refresh), `getTodaySessionsSpend(force?)` (today's billed cost per session, sorted by cost descending, each row carrying the session's durable title), `getTurnSpend(sessionId, messageId)` (one completed turn's billed cost, located by its closing assistant message id), and `getSessionTurnSpends(sessionId)` (every completed turn's cost as a `messageId → total` map, folded in one pass — the transcript renders one row per message, so the client fetches this once per session instead of calling `getTurnSpend` per row). The spend prices each provider-reported usage sample — an `assistant/message`'s usage, or a failed/retried `assistant/attempt`'s stream usage, priced with the model of the latest `request/header` — at the official rate revision in effect at the sample's own timestamp, classified peak/off-peak by its own Beijing-time hour. Peak windows apply weekdays (Monday–Friday) only, and weekends are always off-peak. A sample for the same `(turn, step)` replaces the earlier one and `llm/retry-started` makes the retried attempt add, matching DSH's own turn-usage disclosure; costs then sum per model. A turn is the `turn/start`..`turn/end` range holding the closing message; the ranking folds each session's title from its latest `session/title` event (last-wins, so a rename is reflected as soon as its event commits and the session is re-read).
20
20
 
21
21
  ### Today-spend read path (no full scans per message)
22
22
 
@@ -27,7 +27,7 @@ The plugin registers the `billing` Remote with six methods: `getBalance(force?)`
27
27
 
28
28
  After the first resolution per process, steady-state reads cost only the sessions whose logs actually changed. A session whose log cannot be read is skipped with a warning (and remembered) instead of failing the whole day's total.
29
29
 
30
- Note: the projection path prices a session's history once, at the rates in effect when its events were folded changing `billing.models` re-prices only events folded after the change (the events path re-prices the whole log).
30
+ Note: the projection path prices a session's history once, at the rates in effect when its events were folded. Published rate revisions travel inside the pricing closure and are resolved per sample timestamp, so a re-priced series bills its own history correctly however late a log is folded; only a configuration change (editing `billing.models`) re-prices just the events folded afterwards (the events path re-prices the whole log), and the unit's `stateVersion` is bumped whenever that resolution changes so checkpointed rows are refolded instead of kept.
31
31
 
32
32
  ## Forked sessions
33
33
 
@@ -47,11 +47,17 @@ The projection-cache reader likewise targets the current seam: `cachedSnapshot(h
47
47
  | --- | --- | --- |
48
48
  | `apiKeyEnv` | `DEEPSEEK_API_KEY` | Credential-reference (environment-variable) name resolved per call. |
49
49
  | `baseURL` | `$DEEPSEEK_BASE_URL` then `https://api.deepseek.com` | Endpoint base; `/user/balance` is appended. |
50
- | `models` | V4 Flash + V4.1 Flash + V4 Pro + V4 Flash Vision Exp + MiMo-V2.5 series | Advisory display rows, in presentation order. |
50
+ | `models` | V4.1 Flash (`deepseek-flash`) + V4 Flash + V4 Pro + V4 Flash Vision Exp + MiMo-V2.5 series | Advisory display rows, in presentation order; they mirror DSH's `llm-deepseek` catalog (plus the retired `deepseek-v4.1-flash-expires-on-0910` preview id, kept for readable historical labels). |
51
51
  | `billing.peakHours` | 09:00–12:00, 14:00–18:00 (Beijing, weekdays) | Peak-hour windows, applied weekdays (Mon–Fri) only; weekends and all other hours are off-peak. |
52
- | `billing.models` | Published V4 + MiMo rates | Per-model peak/off-peak price rows (`cacheHitInput`, `cacheMissInput`, `output`, in CNY per 1M tokens). |
52
+ | `billing.models` | Published V4 + MiMo rates | Per-model price rows (`cacheHitInput`, `cacheMissInput`, `output`, in CNY per 1M tokens) with an optional inclusive `effectiveFrom` (epoch ms). |
53
53
 
54
- Override one model without dropping the others by supplying a non-empty `billing.models` list; an empty or omitted list falls back to the published defaults.
54
+ Override one model without dropping the others by supplying a non-empty `billing.models` list; an empty or omitted list falls back to the published defaults. Several rows may share one model: each row is a rate revision, and a usage sample is priced at the peak/off-peak pair of the revision in effect at the sample's own timestamp (a row without `effectiveFrom` is that model's base revision and also covers every earlier instant).
55
+
56
+ The published table already ships DeepSeek's adjustments, so a session or a day spanning a change is priced exactly:
57
+
58
+ - **2026-09-10 12:00 Beijing** (`FLASH_SERIES_RATE_CHANGE_AT`): the whole flash series — the V4.1 Flash route `deepseek-flash` (released that day and now DSH's default), V4 Flash, V4 Flash Vision Exp, and the retired preview id — drops to off-peak 0.02 / 1.0 / 4.0 with peak at twice those prices. Earlier samples, including this route's own usage from before that instant, keep the superseded rates.
59
+ - **2026-09-14 12:00 Beijing** (`V4_PRO_ROUTE_SWITCH_AT`): the V4 Pro route is announced to be served by V4.1 Flash and billed at the V4.1 Flash rates; its row carries that second revision.
60
+ - MiMo-V2.5 series: untouched by either adjustment (flat rate).
55
61
 
56
62
  ## Model Experience
57
63
 
@@ -66,5 +72,5 @@ None; its only provider call is a credential-authenticated `/user/balance` read,
66
72
  - **Priced rows only** — the session and today spends only price models that have a `billing.models` row; a model without a rate row is omitted. An `assistant/attempt` is priced with the model of the latest `request/header`, so an attempt before any header contributes nothing.
67
73
  - **Up-to-60s staleness** — `getTodaySpend()` is served from the host-side cache for up to 60 seconds; only the manual refresh (`force`) recomputes immediately (still revision-gated, so an unchanged log costs nothing). The browser reads the live per-session value from the pushed projection instead, so the session line is never stale.
68
74
  - **Balance is TTL-cached** — one `/user/balance` snapshot is reused for up to 15 seconds and each request aborts after 5 seconds; `force` (the manual refresh) bypasses the TTL.
69
- - **Projection pricing is history-frozen** — when the projection path is active, a pricing-table change prices only events folded after the change; restart (or the events fallback) re-prices the full log.
75
+ - **Projection pricing follows the published revisions** — the projection fold resolves the rate revision per sample timestamp, so published re-pricing needs no refold; a hand-edited `billing.models` change prices only events folded after the change until the state version or the process is reset (the events fallback re-prices the full log).
70
76
  - **A cached cold row may trail its log** — a cold session whose cached row covers the queried day is re-read from the log for exactness; a row whose own day is not the queried day is trusted without a read, so a session that crashed between its last checkpoint and its last event can under-report that tail until it is next read.
package/README.zh.md CHANGED
@@ -16,7 +16,7 @@
16
16
  # baseURL: https://api.deepseek.com
17
17
  ```
18
18
 
19
- 插件注册 `billing` Remote,含六个方法:`getBalance(force?)`(解析后的 `/user/balance` 快照;15 秒宿主 TTL 内复用,`force` 绕过,单次请求 5 秒超时)、`getSessionSpend(sessionId)`(单个会话的计费花费)、`getTodaySpend(force?)`(当前北京时间自然日内所有会话的计费花费合计;`force` 绕过宿主侧缓存,供徽标手动刷新使用)、`getTodaySessionsSpend(force?)`(今日按会话的计费花费,按花费从高到低排序,每行带会话的持久标题)、`getTurnSpend(sessionId, messageId)`(单个已完成回合的计费花费,按收尾助手消息 id 定位)与 `getSessionTurnSpends(sessionId)`(该会话所有已完成回合的 `messageId → 金额` 映射,一趟折叠——对话每个消息行都要显示金额,客户端因此每会话只拉一次,而不是逐行调用 `getTurnSpend`)。计价的样本来源有两处:`assistant/message` 自身的 usage,以及失败/重试的 `assistant/attempt` 内嵌 stream 里的 usage(后者用最近一条 `request/header` 的模型),各按样本自身发生时刻(北京时间)所在的峰/谷单价计价——高峰窗口仅周一至周五适用,周末全天按低谷价。同一 `(turn, step)` 的后一份样本替换前一份,`llm/retry-started` 之后重试的那次累加,与 DSH 自己的回合用量口径一致;随后按模型汇总。一个回合即收尾消息所在的 `turn/start`..`turn/end` 区间;排行从每个会话日志里最后一条 `session/title` 事件折叠标题(last-wins,重命名事件一旦提交、会话被重新读取即反映新名字)。
19
+ 插件注册 `billing` Remote,含六个方法:`getBalance(force?)`(解析后的 `/user/balance` 快照;15 秒宿主 TTL 内复用,`force` 绕过,单次请求 5 秒超时)、`getSessionSpend(sessionId)`(单个会话的计费花费)、`getTodaySpend(force?)`(当前北京时间自然日内所有会话的计费花费合计;`force` 绕过宿主侧缓存,供徽标手动刷新使用)、`getTodaySessionsSpend(force?)`(今日按会话的计费花费,按花费从高到低排序,每行带会话的持久标题)、`getTurnSpend(sessionId, messageId)`(单个已完成回合的计费花费,按收尾助手消息 id 定位)与 `getSessionTurnSpends(sessionId)`(该会话所有已完成回合的 `messageId → 金额` 映射,一趟折叠——对话每个消息行都要显示金额,客户端因此每会话只拉一次,而不是逐行调用 `getTurnSpend`)。计价的样本来源有两处:`assistant/message` 自身的 usage,以及失败/重试的 `assistant/attempt` 内嵌 stream 里的 usage(后者用最近一条 `request/header` 的模型),各按样本自身发生时刻(北京时间)所在的峰/谷单价、以及该时刻生效的官方费率版本计价——高峰窗口仅周一至周五适用,周末全天按低谷价。同一 `(turn, step)` 的后一份样本替换前一份,`llm/retry-started` 之后重试的那次累加,与 DSH 自己的回合用量口径一致;随后按模型汇总。一个回合即收尾消息所在的 `turn/start`..`turn/end` 区间;排行从每个会话日志里最后一条 `session/title` 事件折叠标题(last-wins,重命名事件一旦提交、会话被重新读取即反映新名字)。
20
20
 
21
21
  ### 今日花费读取路径(消息触发不再全量扫描)
22
22
 
@@ -27,7 +27,7 @@
27
27
 
28
28
  进程内首次解析之后,稳态读取只花在日志确实变化过的会话上。日志无法读取的会话带警告跳过(并被记住),而不是让整日合计失败。
29
29
 
30
- 注意:投影路径对每个会话的历史只计价一次,按事件被折叠时的费率——修改 `billing.models` 只影响变更后折叠的事件(事件路径会重算整个日志)。
30
+ 注意:投影路径对每个会话的历史只计价一次,按事件被折叠时的费率。官方费率版本随定价闭包一起进入折叠,并按样本自身时刻解析,因此被调价的系列无论日志多晚折叠都能正确计价自身历史;只有**手工修改配置**(`billing.models`)才只影响变更后折叠的事件(事件路径会重算整个日志),而该解析口径变化时单元 `stateVersion` 会一并提升,使已落检查点被丢弃重折而不是沿用旧值。
31
31
 
32
32
  ## 分叉会话
33
33
 
@@ -47,11 +47,17 @@
47
47
  | --- | --- | --- |
48
48
  | `apiKeyEnv` | `DEEPSEEK_API_KEY` | 每次调用时解析的凭据引用(环境变量)名。 |
49
49
  | `baseURL` | `$DEEPSEEK_BASE_URL`,其次 `https://api.deepseek.com` | 端点基础地址;会追加 `/user/balance`。 |
50
- | `models` | V4 Flash + V4.1 Flash + V4 Pro + V4 Flash Vision Exp + MiMo-V2.5 系列 | 展示用的模型行,按展示顺序。 |
50
+ | `models` | V4.1 Flash(`deepseek-flash`)+ V4 Flash + V4 Pro + V4 Flash Vision Exp + MiMo-V2.5 系列 | 展示用的模型行,按展示顺序;与 DSH `llm-deepseek` 目录对齐(另保留已退役的 `deepseek-v4.1-flash-expires-on-0910` 预览 id,让历史日志仍有可读标签)。 |
51
51
  | `billing.peakHours` | 09:00–12:00、14:00–18:00(北京,仅工作日) | 高峰时段窗口,仅周一至周五适用;周末与其余时段均为低谷。 |
52
- | `billing.models` | 官方 V4 + MiMo 费率 | 每个模型的峰/谷单价行(`cacheHitInput`、`cacheMissInput`、`output`,单位:元/百万 token)。 |
52
+ | `billing.models` | 官方 V4 + MiMo 费率 | 每个模型的单价行(`cacheHitInput`、`cacheMissInput`、`output`,单位:元/百万 token),可带生效时刻 `effectiveFrom`(epoch 毫秒,含该时刻)。 |
53
53
 
54
- 只想覆盖某个模型而不丢其它,就提供一个非空的 `billing.models` 列表;空或省略则回退到官方默认费率。
54
+ 只想覆盖某个模型而不丢其它,就提供一个非空的 `billing.models` 列表;空或省略则回退到官方默认费率。同一个模型可以有多行:每行是一个费率版本,用量样本按**样本自身时刻**生效的那一版取峰/谷单价(不带 `effectiveFrom` 的行是该模型的基础版本,同时覆盖更早的一切时刻)。
55
+
56
+ 内置价目表已包含 DeepSeek 的两轮调整,跨越任一变更点的会话或自然日都能精确计价:
57
+
58
+ - **2026-09-10 12:00(北京时间)**(`FLASH_SERIES_RATE_CHANGE_AT`):整个 flash 系列——V4.1 Flash 路由 `deepseek-flash`(当日发布,现为 DSH 默认模型)、V4 Flash、V4 Flash Vision Exp 以及已退役的预览 id——降为谷时 0.02 / 1.0 / 4.0,峰时为其两倍;更早的样本(含该路由 12:00 之前的自身用量)沿用被取代的旧价。
59
+ - **2026-09-14 12:00(北京时间)**(`V4_PRO_ROUTE_SWITCH_AT`):V4 Pro 路由按公告改由 V4.1 Flash 服务并按 V4.1 Flash 计费,该行因此带有第二个费率版本。
60
+ - MiMo-V2.5 系列不受两轮调整影响(统一费率)。
55
61
 
56
62
  ## 模型体验
57
63
 
@@ -66,5 +72,5 @@
66
72
  - **有费率行才计价** —— 会话花费与今日花费只统计价目表(`billing.models`)里有的模型;没有费率行的模型不计入。`assistant/attempt` 用最近一条 `request/header` 的模型计价,因此首条 header 之前的 attempt 不计入。
67
73
  - **最多 60 秒延迟** —— `getTodaySpend()` 由宿主侧缓存服务最多 60 秒;只有手动刷新(`force`)立即重算(仍受 revision 门控,日志未变则零成本)。浏览器端「本会话花费」读的是推送的投影值,因此不会滞后。
68
74
  - **额度带 TTL 缓存** —— 一份 `/user/balance` 快照最多复用 15 秒,单次请求 5 秒超时;`force`(手动刷新)绕过 TTL。
69
- - **投影计价对历史冻结** —— 投影路径生效时,修改计价表只影响变更后折叠的事件;重启(或事件路径回退)才会重算整个日志。
75
+ - **投影计价跟随官方费率版本** —— 投影折叠按样本时刻解析费率版本,官方调价因此无需重折;手工改 `billing.models` 则只影响变更后折叠的事件,直到状态版本或进程重置(事件路径回退会重算整个日志)。
70
76
  - **冷缓存行可能滞后于日志** —— 冷会话若缓存行覆盖查询日,会重读日志以求精确;若缓存行自身的日期不是查询日则直接采信、不读日志,因此进程在最后一次检查点之后、最后一条事件之前崩溃的会话,其尾部可能暂时少算,直到该会话被重新读取。
package/lib/index.js CHANGED
@@ -322,33 +322,93 @@ const DEFAULT_PEAK_HOURS = [{
322
322
  start: 14,
323
323
  end: 18
324
324
  }];
325
- /** Official peak/off-peak rates (CNY per 1M tokens), effective 2026-08-17. */
325
+ /**
326
+ * Inclusive epoch ms of the published V4 Flash series re-pricing:
327
+ * 2026-09-10 12:00 Beijing time (UTC+8, no DST) = 04:00 UTC. Samples before
328
+ * this instant keep the base rates; samples at or after it bill at the second
329
+ * revision.
330
+ */
331
+ const FLASH_SERIES_RATE_CHANGE_AT = Date.UTC(2026, 8, 10, 4, 0, 0);
332
+ /**
333
+ * Inclusive epoch ms of the announced V4 Pro route switch: 2026-09-14 12:00
334
+ * Beijing time (UTC+8, no DST) = 04:00 UTC. From that instant the V4 Pro route
335
+ * is served by V4.1 Flash and billed at the V4.1 Flash rates.
336
+ */
337
+ const V4_PRO_ROUTE_SWITCH_AT = Date.UTC(2026, 8, 14, 4, 0, 0);
338
+ /** The V4 Flash series' base rates (effective 2026-08-17), CNY per 1M tokens. */
339
+ const FLASH_BASE_RATES = {
340
+ peak: {
341
+ cacheHitInput: .1,
342
+ cacheMissInput: 3,
343
+ output: 9
344
+ },
345
+ offPeak: {
346
+ cacheHitInput: .05,
347
+ cacheMissInput: 1.5,
348
+ output: 4.5
349
+ }
350
+ };
351
+ /**
352
+ * The V4 Flash series' second revision (effective
353
+ * {@link FLASH_SERIES_RATE_CHANGE_AT}): off-peak 0.02 / 1.0 / 4.0, peak at
354
+ * twice those prices.
355
+ */
356
+ const FLASH_REPRICED_RATES = {
357
+ effectiveFrom: FLASH_SERIES_RATE_CHANGE_AT,
358
+ peak: {
359
+ cacheHitInput: .04,
360
+ cacheMissInput: 2,
361
+ output: 8
362
+ },
363
+ offPeak: {
364
+ cacheHitInput: .02,
365
+ cacheMissInput: 1,
366
+ output: 4
367
+ }
368
+ };
369
+ /**
370
+ * The V4.1 Flash rates as they reach the retired V4 Pro route from
371
+ * {@link V4_PRO_ROUTE_SWITCH_AT}: the same price pair as the flash series'
372
+ * second revision, carried at its own effective instant.
373
+ */
374
+ const V4_PRO_SWITCHED_RATES = {
375
+ effectiveFrom: V4_PRO_ROUTE_SWITCH_AT,
376
+ peak: FLASH_REPRICED_RATES.peak,
377
+ offPeak: FLASH_REPRICED_RATES.offPeak
378
+ };
379
+ /**
380
+ * Official peak/off-peak rates (CNY per 1M tokens) per model, as dated
381
+ * revisions. Base rows are the schedule effective 2026-08-17; the V4 Flash
382
+ * series (V4.1 Flash, V4 Flash, V4 Flash Vision Exp) carries the second
383
+ * revision effective 2026-09-10 12:00 Beijing, and the V4 Pro row the V4.1
384
+ * Flash rates from its announced route switch (2026-09-14 12:00 Beijing) —
385
+ * the MiMo-V2.5 series is untouched by either adjustment. Rows sharing a model
386
+ * are that model's rate history.
387
+ */
326
388
  const DEFAULT_MODEL_PRICING = [
389
+ {
390
+ model: "deepseek-flash",
391
+ ...FLASH_BASE_RATES
392
+ },
393
+ {
394
+ model: "deepseek-flash",
395
+ ...FLASH_REPRICED_RATES
396
+ },
327
397
  {
328
398
  model: "deepseek-v4-flash",
329
- peak: {
330
- cacheHitInput: .1,
331
- cacheMissInput: 3,
332
- output: 9
333
- },
334
- offPeak: {
335
- cacheHitInput: .05,
336
- cacheMissInput: 1.5,
337
- output: 4.5
338
- }
399
+ ...FLASH_BASE_RATES
400
+ },
401
+ {
402
+ model: "deepseek-v4-flash",
403
+ ...FLASH_REPRICED_RATES
339
404
  },
340
405
  {
341
406
  model: "deepseek-v4.1-flash-expires-on-0910",
342
- peak: {
343
- cacheHitInput: .1,
344
- cacheMissInput: 3,
345
- output: 9
346
- },
347
- offPeak: {
348
- cacheHitInput: .05,
349
- cacheMissInput: 1.5,
350
- output: 4.5
351
- }
407
+ ...FLASH_BASE_RATES
408
+ },
409
+ {
410
+ model: "deepseek-v4.1-flash-expires-on-0910",
411
+ ...FLASH_REPRICED_RATES
352
412
  },
353
413
  {
354
414
  model: "deepseek-v4-pro",
@@ -363,18 +423,17 @@ const DEFAULT_MODEL_PRICING = [
363
423
  output: 13.5
364
424
  }
365
425
  },
426
+ {
427
+ model: "deepseek-v4-pro",
428
+ ...V4_PRO_SWITCHED_RATES
429
+ },
366
430
  {
367
431
  model: "deepseek-v4-flash-vision-exp",
368
- peak: {
369
- cacheHitInput: .1,
370
- cacheMissInput: 3,
371
- output: 9
372
- },
373
- offPeak: {
374
- cacheHitInput: .05,
375
- cacheMissInput: 1.5,
376
- output: 4.5
377
- }
432
+ ...FLASH_BASE_RATES
433
+ },
434
+ {
435
+ model: "deepseek-v4-flash-vision-exp",
436
+ ...FLASH_REPRICED_RATES
378
437
  },
379
438
  {
380
439
  model: "mimo-v2.5-pro",
@@ -409,22 +468,67 @@ const DEFAULT_MODEL_PRICING = [
409
468
  * `z.array` as `[]` rather than `undefined`, so emptiness — not just absence —
410
469
  * selects the defaults. Explicit non-empty rows override the same model; a
411
470
  * supplied non-empty `models` list is authoritative.
471
+ *
472
+ * Rows sharing a model are that model's rate revisions, kept in ascending
473
+ * `effectiveFrom` order (an undated base revision first). Two rows declaring
474
+ * the same effective instant are one revision and the later row wins — the
475
+ * historical override rule — so re-declaring a model can neither duplicate a
476
+ * revision nor install a second undated base.
412
477
  * @param config - optional raw billing configuration.
413
- * @returns the resolved table and peak-hour windows.
478
+ * @returns the resolved table (per model: its revisions plus the newest rates) and peak-hour windows.
414
479
  */
415
480
  function resolveBilling(config) {
416
481
  const peakHours = config?.peakHours !== void 0 && config.peakHours.length > 0 ? config.peakHours : DEFAULT_PEAK_HOURS;
417
482
  const rows = config?.models !== void 0 && config.models.length > 0 ? config.models : DEFAULT_MODEL_PRICING;
483
+ const schedules = /* @__PURE__ */ new Map();
484
+ for (const row of rows) {
485
+ const revision = row.effectiveFrom === void 0 ? {
486
+ peak: row.peak,
487
+ offPeak: row.offPeak
488
+ } : {
489
+ effectiveFrom: row.effectiveFrom,
490
+ peak: row.peak,
491
+ offPeak: row.offPeak
492
+ };
493
+ const revisions = schedules.get(row.model);
494
+ if (revisions === void 0) {
495
+ schedules.set(row.model, [revision]);
496
+ continue;
497
+ }
498
+ const duplicate = revisions.findIndex((candidate) => candidate.effectiveFrom === revision.effectiveFrom);
499
+ if (duplicate >= 0) revisions[duplicate] = revision;
500
+ else revisions.push(revision);
501
+ }
418
502
  const models = /* @__PURE__ */ new Map();
419
- for (const row of rows) models.set(row.model, {
420
- peak: row.peak,
421
- offPeak: row.offPeak
422
- });
503
+ for (const [model, revisions] of schedules) {
504
+ revisions.sort((left, right) => (left.effectiveFrom ?? Number.NEGATIVE_INFINITY) - (right.effectiveFrom ?? Number.NEGATIVE_INFINITY));
505
+ const newest = revisions[revisions.length - 1];
506
+ models.set(model, {
507
+ peak: newest.peak,
508
+ offPeak: newest.offPeak,
509
+ revisions
510
+ });
511
+ }
423
512
  return {
424
513
  peakHours,
425
514
  models
426
515
  };
427
516
  }
517
+ /**
518
+ * The rate revision in effect at one instant: the newest revision that took
519
+ * effect at or before it. Revisions are ascending, so the scan stops at the
520
+ * first future one. An instant before the earliest dated revision bills at that
521
+ * earliest revision — a model with only dated rows is never left unpriced.
522
+ */
523
+ function ratesAt(revisions, time) {
524
+ let chosen = revisions[0];
525
+ for (let index = 1; index < revisions.length; index += 1) {
526
+ const revision = revisions[index];
527
+ if (revision.effectiveFrom === void 0 || revision.effectiveFrom > time) break;
528
+ chosen = revision;
529
+ }
530
+ return chosen;
531
+ }
428
532
  /** Beijing is a fixed UTC+8 offset with no DST. */
429
533
  const BEIJING_OFFSET_MS = 288e5;
430
534
  /** Milliseconds in one day. */
@@ -472,6 +576,7 @@ function beijingPartsOf(time) {
472
576
  const msOfDay = shifted - epochDay * DAY_MS;
473
577
  const civil = civilDateOf(epochDay);
474
578
  return {
579
+ time,
475
580
  hour: Math.floor(msOfDay / 36e5),
476
581
  weekday: ((epochDay + 4) % 7 + 7) % 7,
477
582
  dayKey: `${civil.year}-${pad2(civil.month)}-${pad2(civil.day)}`
@@ -525,7 +630,8 @@ function isPeak(billing, now) {
525
630
  /**
526
631
  * Price one event at the official per-model rates, applying the peak/off-peak
527
632
  * table by its Beijing-time hour and weekday (peak windows apply Monday–Friday
528
- * only; weekends are off-peak). Each `assistant/message` event with usage
633
+ * only; weekends are off-peak) and the rate revision in effect at its own
634
+ * timestamp. Each `assistant/message` event with usage
529
635
  * contributes cache-hit input, cache-miss input (uncached input plus cache
530
636
  * writes), and output (reasoning included) tokens at the rate of its own
531
637
  * timestamp; a model with usage but no pricing row contributes nothing.
@@ -556,8 +662,10 @@ function priceEventAt(parts, event, billing, names) {
556
662
  }
557
663
  /**
558
664
  * Price one provider-reported usage sample for one model at the rates of the
559
- * sample's own Beijing-time hour and weekday. `undefined` when the model has
560
- * no pricing row.
665
+ * sample's own Beijing-time hour and weekday the peak or off-peak price of
666
+ * the rate revision in effect at the sample's own timestamp (a re-priced series
667
+ * bills its history at the rates that applied then). `undefined` when the model
668
+ * has no pricing row.
561
669
  * @param parts - the sample's Beijing-time view.
562
670
  * @param usage - the reported token buckets.
563
671
  * @param model - the wire model id the sample belongs to.
@@ -569,7 +677,8 @@ function priceUsage(parts, usage, model, billing, names) {
569
677
  const pricing = billing.models.get(model);
570
678
  if (pricing === void 0) return void 0;
571
679
  const peak = isPeakParts(billing, parts.hour, parts.weekday);
572
- const price = peak ? pricing.peak : pricing.offPeak;
680
+ const revision = ratesAt(pricing.revisions, parts.time);
681
+ const price = peak ? revision.peak : revision.offPeak;
573
682
  const hit = usage.cacheReadTokens ?? 0;
574
683
  const miss = usage.inputTokens + (usage.cacheWriteTokens ?? 0);
575
684
  const output = usage.outputTokens;
@@ -1119,13 +1228,15 @@ const billingUnitSchema = z$1.object({
1119
1228
  }).strict().nullable()
1120
1229
  }).strict();
1121
1230
  /**
1122
- * Build the `billingTodaySpend` unit for one resolved pricing table. The
1123
- * pricing closure is fixed at registration; a pricing-table change therefore
1124
- * prices only events folded after the change (historical spend keeps its
1125
- * historical rates), unlike the events-scan paths which re-price the whole
1126
- * log. Bump {@link ProjectionDefinition.stateVersion} whenever the state
1127
- * shape or fold semantics change, so persisted checkpoint rows are discarded
1128
- * instead of folded forward.
1231
+ * Build the `billingTodaySpend` unit for one resolved pricing table. Published
1232
+ * rate revisions travel inside the closure and are resolved per sample
1233
+ * timestamp, so a re-priced series bills its own history correctly however late
1234
+ * a log is folded; only a configuration change (editing `billing.models`) is
1235
+ * fixed at registration, and it re-prices just the events folded afterwards
1236
+ * (the events-scan paths re-price the whole log). Bump
1237
+ * {@link ProjectionDefinition.stateVersion} whenever the state shape or fold
1238
+ * semantics change, so persisted checkpoint rows are discarded instead of
1239
+ * folded forward.
1129
1240
  * @param billing - resolved pricing with peak-hour windows.
1130
1241
  * @param catalog - model display rows, in presentation order.
1131
1242
  * @returns the unit definition to register on `ctx.sessionProjections`.
@@ -1134,7 +1245,7 @@ function billingTodaySpendDefinition(billing, catalog) {
1134
1245
  const names = new Map(catalog.map((model) => [model.id, model.name]));
1135
1246
  return {
1136
1247
  key: BILLING_UNIT_KEY,
1137
- stateVersion: 3,
1248
+ stateVersion: 4,
1138
1249
  stateSchema: billingUnitSchema,
1139
1250
  init: (_header, inheritedEventCount) => emptyBillingFoldState(Number(inheritedEventCount ?? 0)),
1140
1251
  apply: (state, event) => applyBillingEvent(state, event, billing, names),
@@ -1698,7 +1809,18 @@ const DEFAULT_API_KEY_ENV = "DEEPSEEK_API_KEY";
1698
1809
  const BASE_URL_ENV = "DEEPSEEK_BASE_URL";
1699
1810
  /** Public API default; deployments may point elsewhere via $DEEPSEEK_BASE_URL. */
1700
1811
  const PUBLIC_BASE_URL = "https://api.deepseek.com";
1812
+ /**
1813
+ * Advisory display rows mirroring the DSH `llm-deepseek` catalog (V4.1 Flash
1814
+ * first, its current default route), plus the MiMo-V2.5 series. The retired
1815
+ * preview id `deepseek-v4.1-flash-expires-on-0910` stays so the logs that used
1816
+ * it keep a readable label; rows never restrict which models are priced — the
1817
+ * pricing table does.
1818
+ */
1701
1819
  const DEFAULT_MODELS = [
1820
+ {
1821
+ id: "deepseek-flash",
1822
+ name: "DeepSeek-V41-Flash"
1823
+ },
1702
1824
  {
1703
1825
  id: "deepseek-v4-flash",
1704
1826
  name: "DeepSeek-V4-Flash"
@@ -1733,16 +1855,18 @@ const tokenPrice = z.object({
1733
1855
  cacheMissInput: z.number().min(0),
1734
1856
  output: z.number().min(0)
1735
1857
  });
1858
+ const billingRateRow = z.object({
1859
+ model: z.string().required(),
1860
+ peak: tokenPrice,
1861
+ offPeak: tokenPrice,
1862
+ effectiveFrom: z.number().min(0)
1863
+ });
1736
1864
  const billingConfig = z.object({
1737
1865
  peakHours: z.array(z.object({
1738
1866
  start: z.number().step(1).min(0).max(23),
1739
1867
  end: z.number().step(1).min(0).max(24)
1740
1868
  })).default([...DEFAULT_PEAK_HOURS]),
1741
- models: z.array(z.object({
1742
- model: z.string().required(),
1743
- peak: tokenPrice,
1744
- offPeak: tokenPrice
1745
- })).default([...DEFAULT_MODEL_PRICING])
1869
+ models: z.array(billingRateRow).default([...DEFAULT_MODEL_PRICING])
1746
1870
  });
1747
1871
  const Config = z.object({
1748
1872
  apiKeyEnv: z.string().role("credential-ref").default(DEFAULT_API_KEY_ENV),
@@ -2017,4 +2141,4 @@ function apply(ctx, config) {
2017
2141
  });
2018
2142
  }
2019
2143
  //#endregion
2020
- export { BALANCE_CACHE_MS, BALANCE_TIMEOUT_MS, BILLING_UNIT_KEY, BillingFolder, Config, DEFAULT_MODEL_PRICING, DEFAULT_PEAK_HOURS, DeepSeekBalanceGateway, PUBLIC_BASE_URL, SESSION_SPEND_CACHE_LIMIT, SESSION_TURN_SPEND_CACHE_LIMIT, SessionTurnSpendFolder, SpendAccumulator, TODAY_SPEND_CACHE_MS, TODAY_SPEND_MAX_EVENTS, TodaySpendCache, TodaySpendScanner, addEventContribution, apply, applyBillingEvent, beijingDayKey, billingTodaySpendDefinition, computeSessionSpend, computeSessionTurnSpends, computeTodaySpend, computeTurnSpend, emptyBillingFoldState, emptyTodaySpend, fetchDeepSeekBalance, foldBillingUnit, foldOwnBilling, foldSessionTitle, forkBoundaryOf, isPeak, isSeededSession, liveSessionEvents, mergeTodaySpend, name, negateSpend, parseDeepSeekBalance, persistenceInspect, persistenceListSnapshots, priceEvent, priceUsage, resolveBilling, subtractSpend };
2144
+ export { BALANCE_CACHE_MS, BALANCE_TIMEOUT_MS, BILLING_UNIT_KEY, BillingFolder, Config, DEFAULT_MODEL_PRICING, DEFAULT_PEAK_HOURS, DeepSeekBalanceGateway, FLASH_SERIES_RATE_CHANGE_AT, PUBLIC_BASE_URL, SESSION_SPEND_CACHE_LIMIT, SESSION_TURN_SPEND_CACHE_LIMIT, SessionTurnSpendFolder, SpendAccumulator, TODAY_SPEND_CACHE_MS, TODAY_SPEND_MAX_EVENTS, TodaySpendCache, TodaySpendScanner, V4_PRO_ROUTE_SWITCH_AT, addEventContribution, apply, applyBillingEvent, beijingDayKey, billingTodaySpendDefinition, computeSessionSpend, computeSessionTurnSpends, computeTodaySpend, computeTurnSpend, emptyBillingFoldState, emptyTodaySpend, fetchDeepSeekBalance, foldBillingUnit, foldOwnBilling, foldSessionTitle, forkBoundaryOf, isPeak, isSeededSession, liveSessionEvents, mergeTodaySpend, name, negateSpend, parseDeepSeekBalance, persistenceInspect, persistenceListSnapshots, priceEvent, priceUsage, resolveBilling, subtractSpend };
@@ -24,21 +24,41 @@ export interface DeepSeekTokenPrice {
24
24
  /** 1M output tokens. */
25
25
  output: number;
26
26
  }
27
- /** Peak and off-peak price pair for one model. */
28
- export interface DeepSeekModelPricing {
27
+ /**
28
+ * One published peak/off-peak rate revision of a model: the price pair plus the
29
+ * instant it took effect. A provider re-prices a series without repricing its
30
+ * history, so the table keeps every revision and prices each sample at the
31
+ * rates of the sample's own timestamp.
32
+ */
33
+ export interface DeepSeekRateRevision {
29
34
  /** Price during peak hours. */
30
35
  peak: DeepSeekTokenPrice;
31
36
  /** Price during off-peak hours. */
32
37
  offPeak: DeepSeekTokenPrice;
38
+ /**
39
+ * Inclusive epoch ms this revision takes effect: it prices every sample at or
40
+ * after that instant. `undefined` on a model's base revision, which also
41
+ * covers every earlier instant.
42
+ */
43
+ effectiveFrom?: number;
44
+ }
45
+ /** Resolved pricing for one model: its published rate revisions, oldest first. */
46
+ export interface DeepSeekModelPricing {
47
+ /** Peak-hour price of the newest revision (the rates in effect now). */
48
+ peak: DeepSeekTokenPrice;
49
+ /** Off-peak price of the newest revision (the rates in effect now). */
50
+ offPeak: DeepSeekTokenPrice;
51
+ /**
52
+ * Every published revision of this model, ascending by `effectiveFrom` (an
53
+ * undated base revision first); always at least one. {@link priceUsage} picks
54
+ * the revision in effect at the priced sample's own timestamp.
55
+ */
56
+ revisions: readonly DeepSeekRateRevision[];
33
57
  }
34
58
  /** One model's pricing-table row in configuration form. */
35
- export interface BillingConfigModel {
59
+ export interface BillingConfigModel extends DeepSeekRateRevision {
36
60
  /** Wire model id. */
37
61
  model: string;
38
- /** Peak-hour price. */
39
- peak: DeepSeekTokenPrice;
40
- /** Off-peak price. */
41
- offPeak: DeepSeekTokenPrice;
42
62
  }
43
63
  /** One peak-hour window on a 24h Beijing-time clock, applied weekdays only. */
44
64
  export interface PeakHourWindow {
@@ -54,7 +74,11 @@ export interface BillingConfig {
54
74
  * only; weekends (Saturday and Sunday) are always off-peak.
55
75
  */
56
76
  peakHours?: PeakHourWindow[];
57
- /** Per-model pricing rows; omission uses the V4 Flash, V4.1 Flash, V4 Pro, and V4 Flash Vision defaults. */
77
+ /**
78
+ * Per-model pricing rows; omission uses the published V4 and MiMo rates.
79
+ * Several rows for one model declare that model's rate history, priced per
80
+ * sample by `effectiveFrom` (see {@link BillingConfigModel}).
81
+ */
58
82
  models?: BillingConfigModel[];
59
83
  }
60
84
  /**
@@ -63,7 +87,28 @@ export interface BillingConfig {
63
87
  * (effective 2026-08-23).
64
88
  */
65
89
  export declare const DEFAULT_PEAK_HOURS: readonly PeakHourWindow[];
66
- /** Official peak/off-peak rates (CNY per 1M tokens), effective 2026-08-17. */
90
+ /**
91
+ * Inclusive epoch ms of the published V4 Flash series re-pricing:
92
+ * 2026-09-10 12:00 Beijing time (UTC+8, no DST) = 04:00 UTC. Samples before
93
+ * this instant keep the base rates; samples at or after it bill at the second
94
+ * revision.
95
+ */
96
+ export declare const FLASH_SERIES_RATE_CHANGE_AT: number;
97
+ /**
98
+ * Inclusive epoch ms of the announced V4 Pro route switch: 2026-09-14 12:00
99
+ * Beijing time (UTC+8, no DST) = 04:00 UTC. From that instant the V4 Pro route
100
+ * is served by V4.1 Flash and billed at the V4.1 Flash rates.
101
+ */
102
+ export declare const V4_PRO_ROUTE_SWITCH_AT: number;
103
+ /**
104
+ * Official peak/off-peak rates (CNY per 1M tokens) per model, as dated
105
+ * revisions. Base rows are the schedule effective 2026-08-17; the V4 Flash
106
+ * series (V4.1 Flash, V4 Flash, V4 Flash Vision Exp) carries the second
107
+ * revision effective 2026-09-10 12:00 Beijing, and the V4 Pro row the V4.1
108
+ * Flash rates from its announced route switch (2026-09-14 12:00 Beijing) —
109
+ * the MiMo-V2.5 series is untouched by either adjustment. Rows sharing a model
110
+ * are that model's rate history.
111
+ */
67
112
  export declare const DEFAULT_MODEL_PRICING: readonly BillingConfigModel[];
68
113
  /** Resolved billing configuration: a pricing table plus peak-hour windows. */
69
114
  export interface ResolvedBilling {
@@ -79,12 +124,24 @@ export interface ResolvedBilling {
79
124
  * `z.array` as `[]` rather than `undefined`, so emptiness — not just absence —
80
125
  * selects the defaults. Explicit non-empty rows override the same model; a
81
126
  * supplied non-empty `models` list is authoritative.
127
+ *
128
+ * Rows sharing a model are that model's rate revisions, kept in ascending
129
+ * `effectiveFrom` order (an undated base revision first). Two rows declaring
130
+ * the same effective instant are one revision and the later row wins — the
131
+ * historical override rule — so re-declaring a model can neither duplicate a
132
+ * revision nor install a second undated base.
82
133
  * @param config - optional raw billing configuration.
83
- * @returns the resolved table and peak-hour windows.
134
+ * @returns the resolved table (per model: its revisions plus the newest rates) and peak-hour windows.
84
135
  */
85
136
  export declare function resolveBilling(config: BillingConfig | undefined): ResolvedBilling;
86
137
  /** One shifted-timestamp view of a Beijing (UTC+8, no DST) instant. */
87
138
  export interface BeijingParts {
139
+ /**
140
+ * The instant in epoch milliseconds. Pricing needs it back to resolve the
141
+ * rate revision in effect at the sample's own timestamp (see
142
+ * {@link ratesAt}), so the view carries it instead of a second parse.
143
+ */
144
+ time: number;
88
145
  /** Beijing hour, `0`–`23`. */
89
146
  hour: number;
90
147
  /** Beijing weekday as `getUTCDay()`: `0` is Sunday, `6` is Saturday. */
@@ -199,7 +256,8 @@ export interface BillingEventContribution {
199
256
  /**
200
257
  * Price one event at the official per-model rates, applying the peak/off-peak
201
258
  * table by its Beijing-time hour and weekday (peak windows apply Monday–Friday
202
- * only; weekends are off-peak). Each `assistant/message` event with usage
259
+ * only; weekends are off-peak) and the rate revision in effect at its own
260
+ * timestamp. Each `assistant/message` event with usage
203
261
  * contributes cache-hit input, cache-miss input (uncached input plus cache
204
262
  * writes), and output (reasoning included) tokens at the rate of its own
205
263
  * timestamp; a model with usage but no pricing row contributes nothing.
@@ -223,8 +281,10 @@ export declare function priceEvent(event: SessionEvent, billing: ResolvedBilling
223
281
  export declare function priceEventAt(parts: BeijingParts, event: SessionEvent, billing: ResolvedBilling, names: ReadonlyMap<string, string>): BillingEventContribution | undefined;
224
282
  /**
225
283
  * Price one provider-reported usage sample for one model at the rates of the
226
- * sample's own Beijing-time hour and weekday. `undefined` when the model has
227
- * no pricing row.
284
+ * sample's own Beijing-time hour and weekday the peak or off-peak price of
285
+ * the rate revision in effect at the sample's own timestamp (a re-priced series
286
+ * bills its history at the rates that applied then). `undefined` when the model
287
+ * has no pricing row.
228
288
  * @param parts - the sample's Beijing-time view.
229
289
  * @param usage - the reported token buckets.
230
290
  * @param model - the wire model id the sample belongs to.
@@ -21,33 +21,77 @@ export const DEFAULT_PEAK_HOURS = [
21
21
  { start: 9, end: 12 },
22
22
  { start: 14, end: 18 },
23
23
  ];
24
- /** Official peak/off-peak rates (CNY per 1M tokens), effective 2026-08-17. */
24
+ /**
25
+ * Inclusive epoch ms of the published V4 Flash series re-pricing:
26
+ * 2026-09-10 12:00 Beijing time (UTC+8, no DST) = 04:00 UTC. Samples before
27
+ * this instant keep the base rates; samples at or after it bill at the second
28
+ * revision.
29
+ */
30
+ export const FLASH_SERIES_RATE_CHANGE_AT = Date.UTC(2026, 8, 10, 4, 0, 0);
31
+ /**
32
+ * Inclusive epoch ms of the announced V4 Pro route switch: 2026-09-14 12:00
33
+ * Beijing time (UTC+8, no DST) = 04:00 UTC. From that instant the V4 Pro route
34
+ * is served by V4.1 Flash and billed at the V4.1 Flash rates.
35
+ */
36
+ export const V4_PRO_ROUTE_SWITCH_AT = Date.UTC(2026, 8, 14, 4, 0, 0);
37
+ /** The V4 Flash series' base rates (effective 2026-08-17), CNY per 1M tokens. */
38
+ const FLASH_BASE_RATES = {
39
+ peak: { cacheHitInput: 0.10, cacheMissInput: 3.0, output: 9.0 },
40
+ offPeak: { cacheHitInput: 0.05, cacheMissInput: 1.5, output: 4.5 },
41
+ };
42
+ /**
43
+ * The V4 Flash series' second revision (effective
44
+ * {@link FLASH_SERIES_RATE_CHANGE_AT}): off-peak 0.02 / 1.0 / 4.0, peak at
45
+ * twice those prices.
46
+ */
47
+ const FLASH_REPRICED_RATES = {
48
+ effectiveFrom: FLASH_SERIES_RATE_CHANGE_AT,
49
+ peak: { cacheHitInput: 0.04, cacheMissInput: 2.0, output: 8.0 },
50
+ offPeak: { cacheHitInput: 0.02, cacheMissInput: 1.0, output: 4.0 },
51
+ };
52
+ /**
53
+ * The V4.1 Flash rates as they reach the retired V4 Pro route from
54
+ * {@link V4_PRO_ROUTE_SWITCH_AT}: the same price pair as the flash series'
55
+ * second revision, carried at its own effective instant.
56
+ */
57
+ const V4_PRO_SWITCHED_RATES = {
58
+ effectiveFrom: V4_PRO_ROUTE_SWITCH_AT,
59
+ peak: FLASH_REPRICED_RATES.peak,
60
+ offPeak: FLASH_REPRICED_RATES.offPeak,
61
+ };
62
+ /**
63
+ * Official peak/off-peak rates (CNY per 1M tokens) per model, as dated
64
+ * revisions. Base rows are the schedule effective 2026-08-17; the V4 Flash
65
+ * series (V4.1 Flash, V4 Flash, V4 Flash Vision Exp) carries the second
66
+ * revision effective 2026-09-10 12:00 Beijing, and the V4 Pro row the V4.1
67
+ * Flash rates from its announced route switch (2026-09-14 12:00 Beijing) —
68
+ * the MiMo-V2.5 series is untouched by either adjustment. Rows sharing a model
69
+ * are that model's rate history.
70
+ */
25
71
  export const DEFAULT_MODEL_PRICING = [
26
- {
27
- model: 'deepseek-v4-flash',
28
- peak: { cacheHitInput: 0.10, cacheMissInput: 3.0, output: 9.0 },
29
- offPeak: { cacheHitInput: 0.05, cacheMissInput: 1.5, output: 4.5 },
30
- },
31
- // deepseek-v4.1-flash-expires-on-0910 bills at the same rates as
32
- // deepseek-v4-flash; image inputs are converted to tokens at the same
33
- // per-token price.
34
- {
35
- model: 'deepseek-v4.1-flash-expires-on-0910',
36
- peak: { cacheHitInput: 0.10, cacheMissInput: 3.0, output: 9.0 },
37
- offPeak: { cacheHitInput: 0.05, cacheMissInput: 1.5, output: 4.5 },
38
- },
72
+ // deepseek-flash is the V4.1 Flash route, DSH's default catalog entry; image
73
+ // inputs are converted to tokens at the same per-token price.
74
+ { model: 'deepseek-flash', ...FLASH_BASE_RATES },
75
+ { model: 'deepseek-flash', ...FLASH_REPRICED_RATES },
76
+ { model: 'deepseek-v4-flash', ...FLASH_BASE_RATES },
77
+ { model: 'deepseek-v4-flash', ...FLASH_REPRICED_RATES },
78
+ // deepseek-v4.1-flash-expires-on-0910 was the V4.1 Flash preview route,
79
+ // retired when the model was released on 2026-09-10; its rows stay so the
80
+ // logs that used it keep pricing.
81
+ { model: 'deepseek-v4.1-flash-expires-on-0910', ...FLASH_BASE_RATES },
82
+ { model: 'deepseek-v4.1-flash-expires-on-0910', ...FLASH_REPRICED_RATES },
39
83
  {
40
84
  model: 'deepseek-v4-pro',
41
85
  peak: { cacheHitInput: 0.30, cacheMissInput: 9.0, output: 27.0 },
42
86
  offPeak: { cacheHitInput: 0.15, cacheMissInput: 4.5, output: 13.5 },
43
87
  },
88
+ // From the announced route switch V4 Pro is served by V4.1 Flash and billed
89
+ // at the V4.1 Flash rates.
90
+ { model: 'deepseek-v4-pro', ...V4_PRO_SWITCHED_RATES },
44
91
  // deepseek-v4-flash-vision-exp bills at the same rates as deepseek-v4-flash;
45
92
  // images are converted to tokens at the same per-token price.
46
- {
47
- model: 'deepseek-v4-flash-vision-exp',
48
- peak: { cacheHitInput: 0.10, cacheMissInput: 3.0, output: 9.0 },
49
- offPeak: { cacheHitInput: 0.05, cacheMissInput: 1.5, output: 4.5 },
50
- },
93
+ { model: 'deepseek-v4-flash-vision-exp', ...FLASH_BASE_RATES },
94
+ { model: 'deepseek-v4-flash-vision-exp', ...FLASH_REPRICED_RATES },
51
95
  // MiMo-V2.5 series (Xiaomi): flat rate, no peak/off-peak distinction.
52
96
  {
53
97
  model: 'mimo-v2.5-pro',
@@ -66,8 +110,14 @@ export const DEFAULT_MODEL_PRICING = [
66
110
  * `z.array` as `[]` rather than `undefined`, so emptiness — not just absence —
67
111
  * selects the defaults. Explicit non-empty rows override the same model; a
68
112
  * supplied non-empty `models` list is authoritative.
113
+ *
114
+ * Rows sharing a model are that model's rate revisions, kept in ascending
115
+ * `effectiveFrom` order (an undated base revision first). Two rows declaring
116
+ * the same effective instant are one revision and the later row wins — the
117
+ * historical override rule — so re-declaring a model can neither duplicate a
118
+ * revision nor install a second undated base.
69
119
  * @param config - optional raw billing configuration.
70
- * @returns the resolved table and peak-hour windows.
120
+ * @returns the resolved table (per model: its revisions plus the newest rates) and peak-hour windows.
71
121
  */
72
122
  export function resolveBilling(config) {
73
123
  const peakHours = config?.peakHours !== undefined && config.peakHours.length > 0
@@ -76,11 +126,48 @@ export function resolveBilling(config) {
76
126
  const rows = config?.models !== undefined && config.models.length > 0
77
127
  ? config.models
78
128
  : DEFAULT_MODEL_PRICING;
129
+ const schedules = new Map();
130
+ for (const row of rows) {
131
+ // Spelled out per branch: `exactOptionalPropertyTypes` forbids handing an
132
+ // explicit `undefined` to an optional field.
133
+ const revision = row.effectiveFrom === undefined
134
+ ? { peak: row.peak, offPeak: row.offPeak }
135
+ : { effectiveFrom: row.effectiveFrom, peak: row.peak, offPeak: row.offPeak };
136
+ const revisions = schedules.get(row.model);
137
+ if (revisions === undefined) {
138
+ schedules.set(row.model, [revision]);
139
+ continue;
140
+ }
141
+ const duplicate = revisions.findIndex(candidate => candidate.effectiveFrom === revision.effectiveFrom);
142
+ if (duplicate >= 0)
143
+ revisions[duplicate] = revision;
144
+ else
145
+ revisions.push(revision);
146
+ }
79
147
  const models = new Map();
80
- for (const row of rows)
81
- models.set(row.model, { peak: row.peak, offPeak: row.offPeak });
148
+ for (const [model, revisions] of schedules) {
149
+ revisions.sort((left, right) => (left.effectiveFrom ?? Number.NEGATIVE_INFINITY) - (right.effectiveFrom ?? Number.NEGATIVE_INFINITY));
150
+ const newest = revisions[revisions.length - 1];
151
+ models.set(model, { peak: newest.peak, offPeak: newest.offPeak, revisions });
152
+ }
82
153
  return { peakHours, models };
83
154
  }
155
+ /**
156
+ * The rate revision in effect at one instant: the newest revision that took
157
+ * effect at or before it. Revisions are ascending, so the scan stops at the
158
+ * first future one. An instant before the earliest dated revision bills at that
159
+ * earliest revision — a model with only dated rows is never left unpriced.
160
+ */
161
+ function ratesAt(revisions, time) {
162
+ let chosen = revisions[0];
163
+ for (let index = 1; index < revisions.length; index += 1) {
164
+ const revision = revisions[index];
165
+ if (revision.effectiveFrom === undefined || revision.effectiveFrom > time)
166
+ break;
167
+ chosen = revision;
168
+ }
169
+ return chosen;
170
+ }
84
171
  /** Beijing is a fixed UTC+8 offset with no DST. */
85
172
  const BEIJING_OFFSET_MS = 8 * 3_600_000;
86
173
  /** Milliseconds in one day. */
@@ -130,6 +217,7 @@ export function beijingPartsOf(time) {
130
217
  const msOfDay = shifted - epochDay * DAY_MS;
131
218
  const civil = civilDateOf(epochDay);
132
219
  return {
220
+ time,
133
221
  hour: Math.floor(msOfDay / 3_600_000),
134
222
  // 1970-01-01 was a Thursday (4).
135
223
  weekday: ((epochDay + 4) % 7 + 7) % 7,
@@ -189,7 +277,8 @@ export function isPeak(billing, now) {
189
277
  /**
190
278
  * Price one event at the official per-model rates, applying the peak/off-peak
191
279
  * table by its Beijing-time hour and weekday (peak windows apply Monday–Friday
192
- * only; weekends are off-peak). Each `assistant/message` event with usage
280
+ * only; weekends are off-peak) and the rate revision in effect at its own
281
+ * timestamp. Each `assistant/message` event with usage
193
282
  * contributes cache-hit input, cache-miss input (uncached input plus cache
194
283
  * writes), and output (reasoning included) tokens at the rate of its own
195
284
  * timestamp; a model with usage but no pricing row contributes nothing.
@@ -222,8 +311,10 @@ export function priceEventAt(parts, event, billing, names) {
222
311
  }
223
312
  /**
224
313
  * Price one provider-reported usage sample for one model at the rates of the
225
- * sample's own Beijing-time hour and weekday. `undefined` when the model has
226
- * no pricing row.
314
+ * sample's own Beijing-time hour and weekday the peak or off-peak price of
315
+ * the rate revision in effect at the sample's own timestamp (a re-priced series
316
+ * bills its history at the rates that applied then). `undefined` when the model
317
+ * has no pricing row.
227
318
  * @param parts - the sample's Beijing-time view.
228
319
  * @param usage - the reported token buckets.
229
320
  * @param model - the wire model id the sample belongs to.
@@ -236,7 +327,8 @@ export function priceUsage(parts, usage, model, billing, names) {
236
327
  if (pricing === undefined)
237
328
  return undefined;
238
329
  const peak = isPeakParts(billing, parts.hour, parts.weekday);
239
- const price = peak ? pricing.peak : pricing.offPeak;
330
+ const revision = ratesAt(pricing.revisions, parts.time);
331
+ const price = peak ? revision.peak : revision.offPeak;
240
332
  const hit = usage.cacheReadTokens ?? 0;
241
333
  const miss = usage.inputTokens + (usage.cacheWriteTokens ?? 0);
242
334
  const output = usage.outputTokens;
@@ -24,8 +24,8 @@ import type { Context } from '@deepseek-ai/cordis';
24
24
  import z from '@deepseek-ai/schemastery';
25
25
  import type { BillingConfig } from './billing.ts';
26
26
  export { DeepSeekBalanceGateway, fetchDeepSeekBalance, parseDeepSeekBalance } from './balance.ts';
27
- export { addEventContribution, applyBillingEvent, beijingDayKey, BillingFolder, computeSessionSpend, computeSessionTurnSpends, computeTodaySpend, computeTurnSpend, DEFAULT_MODEL_PRICING, DEFAULT_PEAK_HOURS, emptyBillingFoldState, emptyTodaySpend, forkBoundaryOf, isPeak, isSeededSession, mergeTodaySpend, negateSpend, priceEvent, priceUsage, resolveBilling, SessionTurnSpendFolder, SpendAccumulator, subtractSpend, } from './billing.ts';
28
- export type { BillingConfig, BillingConfigModel, BillingEventContribution, BillingFoldSample, BillingFoldState, DeepSeekModelPricing, DeepSeekTokenPrice, PeakHourWindow, ResolvedBilling, } from './billing.ts';
27
+ export { addEventContribution, applyBillingEvent, beijingDayKey, BillingFolder, computeSessionSpend, computeSessionTurnSpends, computeTodaySpend, computeTurnSpend, DEFAULT_MODEL_PRICING, DEFAULT_PEAK_HOURS, emptyBillingFoldState, emptyTodaySpend, FLASH_SERIES_RATE_CHANGE_AT, forkBoundaryOf, isPeak, isSeededSession, mergeTodaySpend, negateSpend, priceEvent, priceUsage, resolveBilling, SessionTurnSpendFolder, SpendAccumulator, subtractSpend, V4_PRO_ROUTE_SWITCH_AT, } from './billing.ts';
28
+ export type { BillingConfig, BillingConfigModel, BillingEventContribution, BillingFoldSample, BillingFoldState, DeepSeekModelPricing, DeepSeekRateRevision, DeepSeekTokenPrice, PeakHourWindow, ResolvedBilling, } from './billing.ts';
29
29
  export type * from './types.ts';
30
30
  export { BILLING_UNIT_KEY, billingTodaySpendDefinition, foldBillingUnit, foldOwnBilling } from './projection.ts';
31
31
  export type { BillingUnitFold, BillingUnitState } from './projection.ts';
@@ -54,7 +54,12 @@ export interface Config {
54
54
  baseURL?: string;
55
55
  /** Advisory display rows, in presentation order; defaults to V4 Flash, V4.1 Flash, V4 Pro, and V4 Flash Vision Exp. */
56
56
  models?: BillingModel[];
57
- /** Pricing table and peak-hour windows; omission uses the published defaults. Peak windows apply weekdays (Monday–Friday) only; weekends are always off-peak. */
57
+ /**
58
+ * Pricing table and peak-hour windows; omission uses the published defaults
59
+ * (including the V4 Flash series re-pricing effective 2026-09-10 12:00
60
+ * Beijing). Peak windows apply weekdays (Monday–Friday) only; weekends are
61
+ * always off-peak.
62
+ */
58
63
  billing?: BillingConfig;
59
64
  }
60
65
  export declare const Config: z<Config>;
@@ -29,7 +29,7 @@ import { computeSessionSpend, computeTurnSpend, DEFAULT_MODEL_PRICING, DEFAULT_P
29
29
  import { billingTodaySpendDefinition } from "./projection.js";
30
30
  import { liveSessionEvents, persistenceInspect, TodaySpendCache, TodaySpendScanner } from "./today-spend.js";
31
31
  export { DeepSeekBalanceGateway, fetchDeepSeekBalance, parseDeepSeekBalance } from "./balance.js";
32
- export { addEventContribution, applyBillingEvent, beijingDayKey, BillingFolder, computeSessionSpend, computeSessionTurnSpends, computeTodaySpend, computeTurnSpend, DEFAULT_MODEL_PRICING, DEFAULT_PEAK_HOURS, emptyBillingFoldState, emptyTodaySpend, forkBoundaryOf, isPeak, isSeededSession, mergeTodaySpend, negateSpend, priceEvent, priceUsage, resolveBilling, SessionTurnSpendFolder, SpendAccumulator, subtractSpend, } from "./billing.js";
32
+ export { addEventContribution, applyBillingEvent, beijingDayKey, BillingFolder, computeSessionSpend, computeSessionTurnSpends, computeTodaySpend, computeTurnSpend, DEFAULT_MODEL_PRICING, DEFAULT_PEAK_HOURS, emptyBillingFoldState, emptyTodaySpend, FLASH_SERIES_RATE_CHANGE_AT, forkBoundaryOf, isPeak, isSeededSession, mergeTodaySpend, negateSpend, priceEvent, priceUsage, resolveBilling, SessionTurnSpendFolder, SpendAccumulator, subtractSpend, V4_PRO_ROUTE_SWITCH_AT, } from "./billing.js";
33
33
  export { BILLING_UNIT_KEY, billingTodaySpendDefinition, foldBillingUnit, foldOwnBilling } from "./projection.js";
34
34
  export { foldSessionTitle, liveSessionEvents, persistenceInspect, persistenceListSnapshots, TodaySpendCache, TodaySpendScanner } from "./today-spend.js";
35
35
  export const name = 'llm-billing';
@@ -37,7 +37,15 @@ const DEFAULT_API_KEY_ENV = 'DEEPSEEK_API_KEY';
37
37
  const BASE_URL_ENV = 'DEEPSEEK_BASE_URL';
38
38
  /** Public API default; deployments may point elsewhere via $DEEPSEEK_BASE_URL. */
39
39
  export const PUBLIC_BASE_URL = 'https://api.deepseek.com';
40
+ /**
41
+ * Advisory display rows mirroring the DSH `llm-deepseek` catalog (V4.1 Flash
42
+ * first, its current default route), plus the MiMo-V2.5 series. The retired
43
+ * preview id `deepseek-v4.1-flash-expires-on-0910` stays so the logs that used
44
+ * it keep a readable label; rows never restrict which models are priced — the
45
+ * pricing table does.
46
+ */
40
47
  const DEFAULT_MODELS = [
48
+ { id: 'deepseek-flash', name: 'DeepSeek-V41-Flash' },
41
49
  { id: 'deepseek-v4-flash', name: 'DeepSeek-V4-Flash' },
42
50
  { id: 'deepseek-v4.1-flash-expires-on-0910', name: 'DeepSeek-V4.1-Flash' },
43
51
  { id: 'deepseek-v4-pro', name: 'DeepSeek-V4-Pro' },
@@ -54,17 +62,22 @@ const tokenPrice = z.object({
54
62
  cacheMissInput: z.number().min(0),
55
63
  output: z.number().min(0),
56
64
  });
65
+ const billingRateRow = z.object({
66
+ model: z.string().required(),
67
+ peak: tokenPrice,
68
+ offPeak: tokenPrice,
69
+ // Several rows may share one model: each is a rate revision, and samples are
70
+ // priced by the revision in effect at their own timestamp (epoch ms,
71
+ // inclusive).
72
+ effectiveFrom: z.number().min(0),
73
+ });
57
74
  const billingConfig = z.object({
58
75
  // Copies of the readonly published tables, taken once at module load.
59
76
  peakHours: z.array(z.object({
60
77
  start: z.number().step(1).min(0).max(23),
61
78
  end: z.number().step(1).min(0).max(24),
62
79
  })).default([...DEFAULT_PEAK_HOURS]),
63
- models: z.array(z.object({
64
- model: z.string().required(),
65
- peak: tokenPrice,
66
- offPeak: tokenPrice,
67
- })).default([...DEFAULT_MODEL_PRICING]),
80
+ models: z.array(billingRateRow).default([...DEFAULT_MODEL_PRICING]),
68
81
  });
69
82
  export const Config = z.object({
70
83
  apiKeyEnv: z.string().role('credential-ref').default(DEFAULT_API_KEY_ENV),
@@ -48,13 +48,15 @@ export type BillingUnitDefinition = Omit<ProjectionDefinition<'billingTodaySpend
48
48
  wire: NonNullable<ProjectionDefinition<'billingTodaySpend', BillingUnitState>['wire']>;
49
49
  };
50
50
  /**
51
- * Build the `billingTodaySpend` unit for one resolved pricing table. The
52
- * pricing closure is fixed at registration; a pricing-table change therefore
53
- * prices only events folded after the change (historical spend keeps its
54
- * historical rates), unlike the events-scan paths which re-price the whole
55
- * log. Bump {@link ProjectionDefinition.stateVersion} whenever the state
56
- * shape or fold semantics change, so persisted checkpoint rows are discarded
57
- * instead of folded forward.
51
+ * Build the `billingTodaySpend` unit for one resolved pricing table. Published
52
+ * rate revisions travel inside the closure and are resolved per sample
53
+ * timestamp, so a re-priced series bills its own history correctly however late
54
+ * a log is folded; only a configuration change (editing `billing.models`) is
55
+ * fixed at registration, and it re-prices just the events folded afterwards
56
+ * (the events-scan paths re-price the whole log). Bump
57
+ * {@link ProjectionDefinition.stateVersion} whenever the state shape or fold
58
+ * semantics change, so persisted checkpoint rows are discarded instead of
59
+ * folded forward.
58
60
  * @param billing - resolved pricing with peak-hour windows.
59
61
  * @param catalog - model display rows, in presentation order.
60
62
  * @returns the unit definition to register on `ctx.sessionProjections`.
@@ -51,13 +51,15 @@ const billingUnitSchema = z.object({
51
51
  }).strict().nullable(),
52
52
  }).strict();
53
53
  /**
54
- * Build the `billingTodaySpend` unit for one resolved pricing table. The
55
- * pricing closure is fixed at registration; a pricing-table change therefore
56
- * prices only events folded after the change (historical spend keeps its
57
- * historical rates), unlike the events-scan paths which re-price the whole
58
- * log. Bump {@link ProjectionDefinition.stateVersion} whenever the state
59
- * shape or fold semantics change, so persisted checkpoint rows are discarded
60
- * instead of folded forward.
54
+ * Build the `billingTodaySpend` unit for one resolved pricing table. Published
55
+ * rate revisions travel inside the closure and are resolved per sample
56
+ * timestamp, so a re-priced series bills its own history correctly however late
57
+ * a log is folded; only a configuration change (editing `billing.models`) is
58
+ * fixed at registration, and it re-prices just the events folded afterwards
59
+ * (the events-scan paths re-price the whole log). Bump
60
+ * {@link ProjectionDefinition.stateVersion} whenever the state shape or fold
61
+ * semantics change, so persisted checkpoint rows are discarded instead of
62
+ * folded forward.
61
63
  * @param billing - resolved pricing with peak-hour windows.
62
64
  * @param catalog - model display rows, in presentation order.
63
65
  * @returns the unit definition to register on `ctx.sessionProjections`.
@@ -66,11 +68,13 @@ export function billingTodaySpendDefinition(billing, catalog) {
66
68
  const names = new Map(catalog.map(model => [model.id, model.name]));
67
69
  return {
68
70
  key: BILLING_UNIT_KEY,
69
- // v3: DSH-aligned attempt pricing (assistant/attempt samples, same-step
70
- // replacement, `llm/retry-started` closes the slot) on top of v2's
71
- // boundary-aware fold and whole-session total; older checkpoint rows are
72
- // discarded and refolded.
73
- stateVersion: 3,
71
+ // v4: rate revisions resolved per sample timestamp (the V4 Flash series is
72
+ // re-priced from 2026-09-10 12:00 Beijing), on top of v3's DSH-aligned
73
+ // attempt pricing, v2's boundary-aware fold, and the whole-session total.
74
+ // A row checkpointed by the previous version priced every sample at one
75
+ // flat pair of rates, so rows folded after the re-pricing instant would
76
+ // keep the superseded rates: bumping discards them and refolds.
77
+ stateVersion: 4,
74
78
  stateSchema: billingUnitSchema,
75
79
  init: (_header, inheritedEventCount) => emptyBillingFoldState(Number(inheritedEventCount ?? 0)),
76
80
  apply: (state, event) => applyBillingEvent(state, event, billing, names),
@@ -7,8 +7,12 @@
7
7
  * and weekday, with peak windows Monday–Friday 09:00–12:00 / 14:00–18:00 and
8
8
  * weekends always off-peak; cache-hit input, cache-miss input (including
9
9
  * cache writes), and output (including reasoning) are billed separately at
10
- * per-1M-token rates. The published table prices the DeepSeek V4 rows and
11
- * the MiMo-V2.5 series (flat rate, no peak/off-peak distinction).
10
+ * per-1M-token rates, taking the rate revision in effect at that same instant
11
+ * (the flash series was re-priced from 2026-09-10 12:00 Beijing, its earlier
12
+ * samples keeping the superseded rates, and V4 Pro follows the V4.1 Flash
13
+ * rates from its announced route switch on 2026-09-14 12:00 Beijing). The
14
+ * published table prices the DeepSeek V4 rows and the MiMo-V2.5 series (flat
15
+ * rate, no peak/off-peak distinction).
12
16
  * @module @rayadesu/dsh-llm-billing/types
13
17
  */
14
18
  import type { SessionId } from '@deepseek-ai/dsh-session';
@@ -7,8 +7,12 @@
7
7
  * and weekday, with peak windows Monday–Friday 09:00–12:00 / 14:00–18:00 and
8
8
  * weekends always off-peak; cache-hit input, cache-miss input (including
9
9
  * cache writes), and output (including reasoning) are billed separately at
10
- * per-1M-token rates. The published table prices the DeepSeek V4 rows and
11
- * the MiMo-V2.5 series (flat rate, no peak/off-peak distinction).
10
+ * per-1M-token rates, taking the rate revision in effect at that same instant
11
+ * (the flash series was re-priced from 2026-09-10 12:00 Beijing, its earlier
12
+ * samples keeping the superseded rates, and V4 Pro follows the V4.1 Flash
13
+ * rates from its announced route switch on 2026-09-14 12:00 Beijing). The
14
+ * published table prices the DeepSeek V4 rows and the MiMo-V2.5 series (flat
15
+ * rate, no peak/off-peak distinction).
12
16
  * @module @rayadesu/dsh-llm-billing/types
13
17
  */
14
18
  export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rayadesu/dsh-llm-billing",
3
3
  "description": "Standalone DeepSeek account-balance and session-spend provider exposed through the billing Remote",
4
- "version": "0.3.9",
4
+ "version": "0.3.10",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },