dsh-plugin-billing 0.0.0-stage → 0.1.1

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.
@@ -0,0 +1,161 @@
1
+ /**
2
+ * 计费单价与计价 —— 会被编译进浏览器 bundle(见 `scripts/build-client.mjs`)。
3
+ *
4
+ * 本文件是**纯声明**(没有 import/export),因为 bundle 是外壳直接执行的 classic
5
+ * script,只能通过 `require` 拿统一基座里的共享模块。构建脚本把它与
6
+ * `client-body.js` 一起拼进 factory 闭包。
7
+ *
8
+ * ## 单价来源
9
+ *
10
+ * DeepSeek 官方「模型 & 价格」页(元 / 百万 tokens,**高峰时段**):
11
+ * https://api-docs.deepseek.com/zh-cn/quick_start/pricing
12
+ *
13
+ * 官方口径:空闲时段价格是高峰时段的一半;高峰期(北京时间)为周一至周五
14
+ * 09:00–12:00 与 14:00–18:00,其余为空闲时段。因此这里只登记高峰价,
15
+ * 空闲价由 `rateFactor()` 折算,避免两套数字各自漂移。
16
+ *
17
+ * ## 计费口径
18
+ *
19
+ * 扣费 = token 消耗量 × 单价。四个桶对应提供方的三类计费项:
20
+ *
21
+ * | 桶 | 来源 | 计费方式 |
22
+ * |---|---|---|
23
+ * | 缓存未命中输入 | `usage.inputTokens` | 缓存未命中单价 |
24
+ * | 缓存命中输入 | `usage.cacheReadTokens` | 缓存命中单价 |
25
+ * | 缓存写入 | `usage.cacheWriteTokens` | 并入缓存未命中(DeepSeek 不单列此项) |
26
+ * | 输出 | `usage.outputTokens` | 输出单价 |
27
+ *
28
+ * `usage.reasoningTokens` 是输出的一部分(官方与 harness 的 token-meter 同口径),
29
+ * **不重复计入**。
30
+ */
31
+
32
+ /** 计费币种。与余额接口返回的 `currency` 对齐。 */
33
+ const CURRENCY = 'CNY';
34
+
35
+ /**
36
+ * 高峰时段单价(元 / 百万 tokens),构建期由 `scripts/build-client.mjs` 从仓库根的
37
+ * `pricing.json` 内联进来。key 必须与 harness 路由里的模型 id 完全一致
38
+ * (来自 `request/context` 的 `model`)。
39
+ *
40
+ * 这只是**兜底**:Host 侧每次请求都读同一份 `pricing.json`(并支持
41
+ * `$DSH_HOME/billing-prices.json` 覆盖),通过路由的 `prices` 字段下发。因此改价只需编辑
42
+ * 那个 JSON —— 不用重新构建 bundle,也不用重启服务。
43
+ */
44
+ const DEFAULT_PRICES = {/*__PRICES__*/};
45
+
46
+ /** 定价页来源标注的兜底值(正常由路由下发 `priceSource`)。 */
47
+ const PRICE_SOURCE = 'DeepSeek 官方定价页';
48
+
49
+ /**
50
+ * 选用哪张价格表:**路由下发的优先**,缺失时退回构建期内联的兜底表。
51
+ * @param data - 路由响应体(可能为 null)。
52
+ * @returns 高峰价表。
53
+ */
54
+ function priceTable(data) {
55
+ if (data !== null && typeof data === 'object' && data.prices !== null && typeof data.prices === 'object') {
56
+ const keys = Object.keys(data.prices);
57
+ if (keys.length > 0) return data.prices;
58
+ }
59
+ return DEFAULT_PRICES;
60
+ }
61
+
62
+ /** 高峰时段(北京时间当天分钟数区间,左闭右开)。 */
63
+ const PEAK_WINDOWS = [
64
+ [9 * 60, 12 * 60],
65
+ [14 * 60, 18 * 60],
66
+ ];
67
+
68
+ /** 由 UTC 毫秒换到北京时间的固定偏移;不依赖运行机器的本地时区。 */
69
+ const BEIJING_OFFSET_MS = 8 * 60 * 60 * 1000;
70
+
71
+ /**
72
+ * 判断某一时刻是否落在高峰时段。
73
+ * 用 UTC getter 读取“加上固定偏移后的墙上时间”,因此主机时区无关。
74
+ * @param atMs - epoch 毫秒。
75
+ * @returns 高峰时段为 true;周末恒为 false。
76
+ */
77
+ function isPeak(atMs) {
78
+ const shifted = new Date(atMs + BEIJING_OFFSET_MS);
79
+ const weekday = shifted.getUTCDay();
80
+ if (weekday === 0 || weekday === 6) return false;
81
+ const minutes = shifted.getUTCHours() * 60 + shifted.getUTCMinutes();
82
+ return PEAK_WINDOWS.some(function (window) {
83
+ return minutes >= window[0] && minutes < window[1];
84
+ });
85
+ }
86
+
87
+ /**
88
+ * 高峰倍率:高峰 1,空闲 0.5(官方“空闲为高峰一半”)。
89
+ * @param atMs - epoch 毫秒。
90
+ * @returns 倍率。
91
+ */
92
+ function rateFactor(atMs) {
93
+ return isPeak(atMs) ? 1 : 0.5;
94
+ }
95
+
96
+ /**
97
+ * 取某模型在某时刻的三档单价(元 / 百万 tokens)。
98
+ * @param model - 模型 id。
99
+ * @param atMs - epoch 毫秒。
100
+ * @param table - 可选的价格表;缺省用构建期内联的兜底表。
101
+ * @returns `{ cacheHit, cacheMiss, output, peak, factor }`;模型未登记时为 `undefined`。
102
+ */
103
+ function ratesFor(model, atMs, table) {
104
+ const peak = (table ?? DEFAULT_PRICES)[model];
105
+ if (peak === undefined) return undefined;
106
+ const factor = rateFactor(atMs);
107
+ return {
108
+ cacheHit: peak.cacheHit * factor,
109
+ cacheMiss: peak.cacheMiss * factor,
110
+ output: peak.output * factor,
111
+ peak: factor === 1,
112
+ factor: factor,
113
+ };
114
+ }
115
+
116
+ /**
117
+ * 从一个 `TokenUsage` 取出四个计费桶。缺字段按 0 计。
118
+ * @param usage - 提供方或 harness 记录的用量对象。
119
+ * @returns `{ cacheMiss, cacheRead, cacheWrite, output }`。
120
+ */
121
+ function bucketsOf(usage) {
122
+ const read = function (key) {
123
+ const value = usage === null || typeof usage !== 'object' ? undefined : usage[key];
124
+ return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : 0;
125
+ };
126
+ return {
127
+ cacheMiss: read('inputTokens'),
128
+ cacheRead: read('cacheReadTokens'),
129
+ cacheWrite: read('cacheWriteTokens'),
130
+ output: read('outputTokens'),
131
+ };
132
+ }
133
+
134
+ /** 四个桶逐项相加。 */
135
+ function addBuckets(left, right) {
136
+ return {
137
+ cacheMiss: left.cacheMiss + right.cacheMiss,
138
+ cacheRead: left.cacheRead + right.cacheRead,
139
+ cacheWrite: left.cacheWrite + right.cacheWrite,
140
+ output: left.output + right.output,
141
+ };
142
+ }
143
+
144
+ /** 桶的总 token 数。 */
145
+ function bucketsTotal(buckets) {
146
+ return buckets.cacheMiss + buckets.cacheRead + buckets.cacheWrite + buckets.output;
147
+ }
148
+
149
+ /**
150
+ * 按单价把桶折成金额。
151
+ * 缓存写入并入“缓存未命中”档,与 DeepSeek 的计费口径一致。
152
+ * @param buckets - 四个计费桶。
153
+ * @param rates - `ratesFor()` 的返回值。
154
+ * @returns 金额(`CURRENCY` 单位)。
155
+ */
156
+ function costOf(buckets, rates) {
157
+ const billedMiss = buckets.cacheMiss + buckets.cacheWrite;
158
+ return (
159
+ (billedMiss * rates.cacheMiss + buckets.cacheRead * rates.cacheHit + buckets.output * rates.output) / 1e6
160
+ );
161
+ }