@rayadesu/dsh-llm-billing 0.3.8 → 0.3.9
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 +2 -2
- package/README.md +15 -11
- package/README.zh.md +15 -11
- package/lib/index.js +890 -382
- package/lib/typert.host.js +49 -5
- package/lib/typert.remote-client.d.ts +5 -3
- package/lib/typert.remote-client.js +49 -5
- package/lib/types/balance.d.ts +33 -15
- package/lib/types/balance.js +28 -13
- package/lib/types/billing.d.ts +217 -21
- package/lib/types/billing.js +434 -71
- package/lib/types/index.d.ts +14 -4
- package/lib/types/index.js +211 -70
- package/lib/types/projection.d.ts +24 -25
- package/lib/types/projection.js +30 -31
- package/lib/types/today-spend.d.ts +138 -62
- package/lib/types/today-spend.js +209 -261
- package/lib/types/types.d.ts +30 -5
- package/lib/types/types.js +8 -0
- package/package.json +5 -1
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:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: 453fdb62c6081c6f5704bf878888c326b523d5e1
|
|
6
|
+
README.zh.md: c147bff571b7e00abebb1fad188a11058354bbc5
|
package/README.md
CHANGED
|
@@ -16,28 +16,30 @@ 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
|
|
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).
|
|
20
20
|
|
|
21
21
|
### Today-spend read path (no full scans per message)
|
|
22
22
|
|
|
23
|
-
`getTodaySpend()` never scans every session log per request. A 60-second Beijing-day cache with in-flight coalescing serves message-triggered reads; only the manual refresh (`force`) bypasses the time window. Behind a miss,
|
|
23
|
+
`getTodaySpend()` never scans every session log per request. A 60-second Beijing-day cache with in-flight coalescing serves message-triggered reads; only the manual refresh (`force`) bypasses the time window. Behind a miss, one scan produces BOTH the aggregate and the ranking (the aggregate is the sum of the rows):
|
|
24
24
|
|
|
25
|
-
- **Projection path** (used when `@deepseek-ai/dsh-session-projection` is composed): the plugin registers the `billingTodaySpend` projection unit,
|
|
26
|
-
- **Events path** (fallback without the registry):
|
|
25
|
+
- **Projection path** (used when `@deepseek-ai/dsh-session-projection` is composed): the plugin registers the client-visible `billingTodaySpend` projection unit — eagerly, as soon as the registry exists, so DSH's write-behind checkpoints a row for every session at each `turn/end`. Live sessions are read from their eager cells with zero log I/O; a cold session is answered from the projection cache's zero-I/O `cachedSnapshot` row whenever that row's own latest priced day is not the queried day, and only otherwise (or when no usable row exists) inspected and folded locally. Only sessions whose persisted revision changed since the last resolution are touched, and a failed resolution is remembered by revision instead of being retried on every scan.
|
|
26
|
+
- **Events path** (fallback without the registry): folds each session's log with the same pricing fold (Beijing-day filter during collection, 200 000-event cap), skipping sessions whose persisted revision is unchanged.
|
|
27
27
|
|
|
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 instead of failing the whole day's total.
|
|
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
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).
|
|
31
31
|
|
|
32
32
|
## Forked sessions
|
|
33
33
|
|
|
34
|
-
A forked session (DSH's "fork" of a conversation) opens its log with a verbatim copy of its source session's events. Without special handling, the same model outputs would be billed once per copy: the child's session spend would include the inherited prefix, and today's spend would count it a second time alongside the parent's. The plugin prices only a session's OWN events — the fork boundary is the session's persisted state (`header.seedLength` on the ≤ 0.1.1-rc.2 runtime, `Session.inheritedEventCount` / `inspect().inheritedEventCount` on 0.1.2-alpha.4+, both read structurally)
|
|
34
|
+
A forked session (DSH's "fork" of a conversation) opens its log with a verbatim copy of its source session's events. Without special handling, the same model outputs would be billed once per copy: the child's session spend would include the inherited prefix, and today's spend would count it a second time alongside the parent's. The plugin prices only a session's OWN events — the fork boundary is the session's persisted state (`header.seedLength` on the ≤ 0.1.1-rc.2 runtime, `Session.inheritedEventCount` / `inspect().inheritedEventCount` on 0.1.2-alpha.4+, both read structurally). The `billingTodaySpend` unit is boundary-aware (its state carries the cut and `apply` skips events below it), so the eager cell is correct for a fork child; the cold path skips the projection cache for a seeded session and folds its own events with the durable cut. Fork children are therefore billed from their first new exchange onward (a freshly forked session prices to zero), today's spend counts each model output exactly once, and the same lineage-safe rule covers multi-generation forks and subagent forks (spawned with `context: 'fork'`). The boundary is the persisted value, so a resumed fork child keeps its original boundary, while a session created without a seed — ordinary sessions and cold resumes included — carries no boundary and is billed in full.
|
|
35
35
|
|
|
36
36
|
## Runtime compatibility
|
|
37
37
|
|
|
38
38
|
Since 0.1.2-alpha.4, DSH replaced the live `Session` log surface `Session.events` with `Session.snapshotEvents()` (no args = the full current log) and `Session.ownEvents()`, and moved `SessionHeader.seedLength` to `Session.inheritedEventCount` (the persistence `inspect()` result carries the value beside `meta`; `listSnapshots()` headers keep only the boolean `isSeeded`). Every log read in the plugin goes through the structural adapters `liveSessionEvents` / `forkBoundaryOf` / `isSeededSession`, which accept both the ≤ 0.1.1-rc.2 and the 0.1.2-alpha.4+ shapes — the npm release baseline (`^0.1.2-alpha.5`) and the ahead-of-npm monorepo runtime both work without modification. On an unknown surface that has neither shape the plugin fails loudly rather than silently pricing an empty log.
|
|
39
39
|
|
|
40
|
-
The persistence service itself changed surface too: 0.1.1-rc.2 exposes `inspect(id)` / `listSnapshots()`, while the
|
|
40
|
+
The persistence service itself changed surface too: 0.1.1-rc.2 exposes `inspect(id)` / `listSnapshots()`, while the handle-based seam exposes `open(id, 'read')` + `SessionHandle.read()` / `list()`. The scanner reads both families through `persistenceInspect` / `persistenceListSnapshots` (the handle is always closed, including after a failed read), so the same plugin serves the published alpha line and the refactored checkout. `SessionHandle.read()` itself has two generations: it first returned the bare event array, and since DSH `9b78f99dec` (in the 0.1.5-alpha.1 checkout) it returns `{ eventState, events }`; `handleReadEvents` unwraps both, so a cold read keeps working across the change.
|
|
41
|
+
|
|
42
|
+
The projection-cache reader likewise targets the current seam: `cachedSnapshot(header, inheritedEventCount, keys)` (zero I/O, wire rows only). Its predecessor, an async `coldSnapshot(id)` that read the log itself, no longer exists, so the plugin never depends on it.
|
|
41
43
|
|
|
42
44
|
## Configuration
|
|
43
45
|
|
|
@@ -45,9 +47,9 @@ The persistence service itself changed surface too: 0.1.1-rc.2 exposes `inspect(
|
|
|
45
47
|
| --- | --- | --- |
|
|
46
48
|
| `apiKeyEnv` | `DEEPSEEK_API_KEY` | Credential-reference (environment-variable) name resolved per call. |
|
|
47
49
|
| `baseURL` | `$DEEPSEEK_BASE_URL` then `https://api.deepseek.com` | Endpoint base; `/user/balance` is appended. |
|
|
48
|
-
| `models` | V4 Flash + V4 Pro + V4 Flash Vision Exp | Advisory display rows, in presentation order. |
|
|
50
|
+
| `models` | V4 Flash + V4.1 Flash + V4 Pro + V4 Flash Vision Exp + MiMo-V2.5 series | Advisory display rows, in presentation order. |
|
|
49
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. |
|
|
50
|
-
| `billing.models` | Published V4 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 peak/off-peak price rows (`cacheHitInput`, `cacheMissInput`, `output`, in CNY per 1M tokens). |
|
|
51
53
|
|
|
52
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.
|
|
53
55
|
|
|
@@ -61,6 +63,8 @@ None; its only provider call is a credential-authenticated `/user/balance` read,
|
|
|
61
63
|
|
|
62
64
|
## Known Limitations and Deferred Work
|
|
63
65
|
|
|
64
|
-
- **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.
|
|
65
|
-
- **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).
|
|
66
|
+
- **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
|
+
- **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
|
+
- **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.
|
|
66
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.
|
|
70
|
+
- **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,28 +16,30 @@
|
|
|
16
16
|
# baseURL: https://api.deepseek.com
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
插件注册 `billing` Remote
|
|
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
|
|
|
23
|
-
`getTodaySpend()` 每次请求都不会全量扫描所有会话日志。一个 60 秒的北京日缓存带 in-flight 合并,服务于消息触发的读取;只有手动刷新(`force
|
|
23
|
+
`getTodaySpend()` 每次请求都不会全量扫描所有会话日志。一个 60 秒的北京日缓存带 in-flight 合并,服务于消息触发的读取;只有手动刷新(`force`)绕过时间窗口。缓存未命中时,**一次扫描同时产出聚合与排行**(聚合即各行之和):
|
|
24
24
|
|
|
25
|
-
- **投影路径**(当组合中装配了 `@deepseek-ai/dsh-session-projection`
|
|
26
|
-
-
|
|
25
|
+
- **投影路径**(当组合中装配了 `@deepseek-ai/dsh-session-projection` 时启用):插件注册客户端可见的 `billingTodaySpend` 投影单元——**注册表一出现就提前注册**,因此 DSH 的 write-behind 会在每个 `turn/end` 为每个会话落一行检查点。live 会话零日志 I/O 直读其 eager 单元;冷会话若投影缓存的 `cachedSnapshot` 行(零 I/O)自身的最新计价日不是查询日,就直接作答,否则(或没有可用行时)才读取日志本地折叠。只有持久化 revision 在上次解析后变化过的会话才会被读取;读取失败的会话按 revision 记住,不再每轮重试。
|
|
26
|
+
- **事件路径**(无注册表时的回退):对每个会话用同一套计价折叠(收集时按北京日过滤,20 万事件上限),跳过持久化 revision 未变的会话。
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
进程内首次解析之后,稳态读取只花在日志确实变化过的会话上。日志无法读取的会话带警告跳过(并被记住),而不是让整日合计失败。
|
|
29
29
|
|
|
30
30
|
注意:投影路径对每个会话的历史只计价一次,按事件被折叠时的费率——修改 `billing.models` 只影响变更后折叠的事件(事件路径会重算整个日志)。
|
|
31
31
|
|
|
32
32
|
## 分叉会话
|
|
33
33
|
|
|
34
|
-
分叉会话(DSH 的「分叉会话」)的日志以来源会话事件的逐字节副本开头。若不特殊处理,同一批模型输出会按副本数重复计费:子会话的会话花费会包含继承前缀,今日花费也会在父会话之外再计一次。插件只对会话的**自有事件**计费——分叉边界取自已持久化的会话状态(≤ 0.1.1-rc.2 运行时为 `header.seedLength`;0.1.2-alpha.4+ 运行时为 `Session.inheritedEventCount` / `inspect().inheritedEventCount
|
|
34
|
+
分叉会话(DSH 的「分叉会话」)的日志以来源会话事件的逐字节副本开头。若不特殊处理,同一批模型输出会按副本数重复计费:子会话的会话花费会包含继承前缀,今日花费也会在父会话之外再计一次。插件只对会话的**自有事件**计费——分叉边界取自已持久化的会话状态(≤ 0.1.1-rc.2 运行时为 `header.seedLength`;0.1.2-alpha.4+ 运行时为 `Session.inheritedEventCount` / `inspect().inheritedEventCount`,两者都以结构方式读取)。`billingTodaySpend` 单元本身**带边界**(状态里存着切割点,`apply` 跳过其下事件),因此分叉子会话的 eager 单元直接可用;冷路径对 seeded 会话跳过投影缓存,用持久边界折叠自有事件。因此分叉子会话从分叉后的第一次新交流开始计费(刚分叉的会话花费为零),今日花费对每个模型输出只计一次,同一血缘规则同样覆盖多代分叉与 subagent 分叉(`context: 'fork'` 生成)。边界取自已持久化的值,所以恢复后的分叉子会话保持原边界;而创建时没有 seed 的会话——包括普通会话与冷恢复——不带边界,正常全额计费。
|
|
35
35
|
|
|
36
36
|
## 运行时兼容性
|
|
37
37
|
|
|
38
38
|
0.1.2-alpha.4 起,DSH 把 live `Session` 的日志读取表面从 `Session.events` 改为 `Session.snapshotEvents()`(无参 = 当前全量日志)与 `Session.ownEvents()`,并把 `SessionHeader.seedLength` 移至 `Session.inheritedEventCount`(持久化侧 `inspect()` 的结果在 `meta` 之外携带该值,`listSnapshots()` 的 header 只剩布尔 `isSeeded`)。插件的所有日志读取都走结构适配器 `liveSessionEvents` / `forkBoundaryOf` / `isSeededSession`,同时接受 ≤ 0.1.1-rc.2 与 0.1.2-alpha.4+ 两种形状——npm 发布基线(`^0.1.2-alpha.5`)与超前于它的 monorepo 运行时代码均无需改动即可工作。遇到两种形状都没有的未知运行时表面时,插件会显式失败而不是静默按零花费计价。
|
|
39
39
|
|
|
40
|
-
持久化服务的表面同样换代:0.1.1-rc.2 提供 `inspect(id)` / `listSnapshots()`,而 handle
|
|
40
|
+
持久化服务的表面同样换代:0.1.1-rc.2 提供 `inspect(id)` / `listSnapshots()`,而 handle 化 seam 提供 `open(id, 'read')` + `SessionHandle.read()` / `list()`。扫描器通过 `persistenceInspect` / `persistenceListSnapshots` 同时读取两代表面(handle 总会关闭,读取失败时也一样),因此同一套插件既能服务已发布的 alpha 线,也能服务重构后的 checkout。`SessionHandle.read()` 自身也有两代:最初返回裸事件数组,DSH `9b78f99dec`(0.1.5-alpha.1 checkout 中)起返回 `{ eventState, events }`;`handleReadEvents` 同时接受两种形状,冷读因此不受该变更影响。
|
|
41
|
+
|
|
42
|
+
投影缓存的读取同样对准当前 seam:`cachedSnapshot(header, inheritedEventCount, keys)`(零 I/O,只读 wire 行)。它的前身——自己读日志的异步 `coldSnapshot(id)`——已不存在,插件不再依赖它。
|
|
41
43
|
|
|
42
44
|
## 配置
|
|
43
45
|
|
|
@@ -45,9 +47,9 @@
|
|
|
45
47
|
| --- | --- | --- |
|
|
46
48
|
| `apiKeyEnv` | `DEEPSEEK_API_KEY` | 每次调用时解析的凭据引用(环境变量)名。 |
|
|
47
49
|
| `baseURL` | `$DEEPSEEK_BASE_URL`,其次 `https://api.deepseek.com` | 端点基础地址;会追加 `/user/balance`。 |
|
|
48
|
-
| `models` | V4 Flash + V4 Pro + V4 Flash Vision Exp | 展示用的模型行,按展示顺序。 |
|
|
50
|
+
| `models` | V4 Flash + V4.1 Flash + V4 Pro + V4 Flash Vision Exp + MiMo-V2.5 系列 | 展示用的模型行,按展示顺序。 |
|
|
49
51
|
| `billing.peakHours` | 09:00–12:00、14:00–18:00(北京,仅工作日) | 高峰时段窗口,仅周一至周五适用;周末与其余时段均为低谷。 |
|
|
50
|
-
| `billing.models` | 官方 V4 费率 | 每个模型的峰/谷单价行(`cacheHitInput`、`cacheMissInput`、`output`,单位:元/百万 token)。 |
|
|
52
|
+
| `billing.models` | 官方 V4 + MiMo 费率 | 每个模型的峰/谷单价行(`cacheHitInput`、`cacheMissInput`、`output`,单位:元/百万 token)。 |
|
|
51
53
|
|
|
52
54
|
只想覆盖某个模型而不丢其它,就提供一个非空的 `billing.models` 列表;空或省略则回退到官方默认费率。
|
|
53
55
|
|
|
@@ -61,6 +63,8 @@
|
|
|
61
63
|
|
|
62
64
|
## 已知限制与暂缓事项
|
|
63
65
|
|
|
64
|
-
- **有费率行才计价** —— 会话花费与今日花费只统计价目表(`billing.models
|
|
65
|
-
- **最多 60 秒延迟** —— `getTodaySpend()` 由宿主侧缓存服务最多 60 秒;只有手动刷新(`force`)立即重算(仍受 revision
|
|
66
|
+
- **有费率行才计价** —— 会话花费与今日花费只统计价目表(`billing.models`)里有的模型;没有费率行的模型不计入。`assistant/attempt` 用最近一条 `request/header` 的模型计价,因此首条 header 之前的 attempt 不计入。
|
|
67
|
+
- **最多 60 秒延迟** —— `getTodaySpend()` 由宿主侧缓存服务最多 60 秒;只有手动刷新(`force`)立即重算(仍受 revision 门控,日志未变则零成本)。浏览器端「本会话花费」读的是推送的投影值,因此不会滞后。
|
|
68
|
+
- **额度带 TTL 缓存** —— 一份 `/user/balance` 快照最多复用 15 秒,单次请求 5 秒超时;`force`(手动刷新)绕过 TTL。
|
|
66
69
|
- **投影计价对历史冻结** —— 投影路径生效时,修改计价表只影响变更后折叠的事件;重启(或事件路径回退)才会重算整个日志。
|
|
70
|
+
- **冷缓存行可能滞后于日志** —— 冷会话若缓存行覆盖查询日,会重读日志以求精确;若缓存行自身的日期不是查询日则直接采信、不读日志,因此进程在最后一次检查点之后、最后一条事件之前崩溃的会话,其尾部可能暂时少算,直到该会话被重新读取。
|