@mzzsfy/dsh-usage-dash 0.1.0 → 0.3.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.
@@ -1,34 +1,28 @@
1
1
  // 双实现同源 parity 测试:client.js 顶层定价镜像函数与宿主 src/pricing.js 同输入同输出
2
2
  // 向量表驱动:同一批向量喂双侧,deepEqual 双侧结果并对期望值断言
3
- // client.js 为非模块 script(bundle 求值形态,禁 import/export),整源求值后按顶层声明名收集
3
+ // client.js 为经典 script bundle( import/export),整文件 IIFE 书挡;经 client-eval 剥壳后整源求值,按顶层声明名收集
4
4
  import { test } from 'node:test'
5
5
  import assert from 'node:assert/strict'
6
- import { readFileSync } from 'node:fs'
7
- import { fileURLToPath } from 'node:url'
8
- import { dirname, join } from 'node:path'
6
+ import { CLIENT_BODY, DECLARATION_NAMES } from './client-eval.mjs'
9
7
 
10
8
  import {
11
9
  matchPrice as hostMatchPrice,
12
10
  costOf as hostCostOf,
13
11
  conditionMatches as hostConditionMatches,
12
+ isTwoSegmentModel as hostIsTwoSegmentModel,
14
13
  UNIT_PER_MILLION as hostUnitPerMillion,
15
14
  CURRENCIES as hostCurrencies,
16
15
  CONDITION_KINDS as hostConditionKinds,
17
16
  TOKENS_PER_MILLION as hostTokensPerMillion,
18
17
  } from '../src/pricing.js'
19
18
 
20
- const CLIENT_SOURCE = readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'src', 'client.js'), 'utf8')
21
- const DECLARATION_NAMES = [
22
- ...new Set(
23
- [...CLIENT_SOURCE.matchAll(/^(?:const|function|let) ([A-Za-z_$][\w$]*)/gm)].map((match) => match[1])
24
- ),
25
- ]
26
- const core = new Function(`${CLIENT_SOURCE}\nreturn { ${DECLARATION_NAMES.join(', ')} }`)()
19
+ const core = new Function(`${CLIENT_BODY}\nreturn { ${DECLARATION_NAMES.join(', ')} }`)()
27
20
 
28
21
  const {
29
22
  matchPrice: clientMatchPrice,
30
23
  costOf: clientCostOf,
31
24
  conditionMatches: clientConditionMatches,
25
+ isTwoSegmentModel: clientIsTwoSegmentModel,
32
26
  UNIT_PER_MILLION: clientUnitPerMillion,
33
27
  CURRENCIES: clientCurrencies,
34
28
  CONDITION_KINDS: clientConditionKinds,
@@ -83,67 +77,112 @@ const MATCH_SUNDAY_1000 = [2026, 2, 15, 10, 0]
83
77
 
84
78
  const MATCH_VECTORS = [
85
79
  {
86
- name: '精确命中按数组序取首个',
87
- rules: [ruleOf('a', 1), ruleOf('b', 2)], model: 'b', date: MATCH_SUNDAY_1000, expected: priceOf(2),
80
+ name: '全名精确命中按数组序取首个',
81
+ rules: [ruleOf('x/a', 1), ruleOf('x/b', 2)], model: 'x/b', date: MATCH_SUNDAY_1000, expected: priceOf(2),
82
+ },
83
+ {
84
+ name: '同全名多条取数组序第一条',
85
+ rules: [ruleOf('x/a', 1), ruleOf('x/a', 3)], model: 'x/a', date: MATCH_SUNDAY_1000, expected: priceOf(1),
86
+ },
87
+ {
88
+ name: '模型名通配跨供应商命中',
89
+ rules: [ruleOf('*/a', 5)], model: 'zzz/a', date: MATCH_SUNDAY_1000, expected: priceOf(5),
90
+ },
91
+ {
92
+ name: '供应商通配命中同供应商模型',
93
+ rules: [ruleOf('x/*', 5)], model: 'x/zzz', date: MATCH_SUNDAY_1000, expected: priceOf(5),
94
+ },
95
+ {
96
+ name: '无精确回落全通配',
97
+ rules: [ruleOf('*/*', 5)], model: 'zzz/yyy', date: MATCH_SUNDAY_1000, expected: priceOf(5),
98
+ },
99
+ {
100
+ name: '全名精确优先于全通配',
101
+ rules: [ruleOf('*/*', 5), ruleOf('x/a', 1)], model: 'x/a', date: MATCH_SUNDAY_1000, expected: priceOf(1),
102
+ },
103
+ {
104
+ name: '匹配链:模型名通配优先于供应商通配',
105
+ rules: [ruleOf('x/*', 3), ruleOf('*/a', 2)], model: 'x/a', date: MATCH_SUNDAY_1000, expected: priceOf(2),
106
+ },
107
+ {
108
+ name: '匹配链:供应商通配优先于全通配',
109
+ rules: [ruleOf('*/*', 4), ruleOf('x/*', 3)], model: 'x/a', date: MATCH_SUNDAY_1000, expected: priceOf(3),
88
110
  },
89
111
  {
90
- name: '同 model 多条取数组序第一条',
91
- rules: [ruleOf('a', 1), ruleOf('a', 3)], model: 'a', date: MATCH_SUNDAY_1000, expected: priceOf(1),
112
+ name: '模型名段允许含斜杠',
113
+ rules: [ruleOf('x/*', 1)], model: 'x/deepseek/deepseek-chat', date: MATCH_SUNDAY_1000, expected: priceOf(1),
92
114
  },
93
115
  {
94
- name: '无精确回落通配',
95
- rules: [ruleOf('*', 5)], model: 'zzz', date: MATCH_SUNDAY_1000, expected: priceOf(5),
116
+ name: '请求裸名归 default 供应商',
117
+ rules: [ruleOf('default/a', 1), ruleOf('*/a', 2)], model: 'a', date: MATCH_SUNDAY_1000, expected: priceOf(1),
96
118
  },
97
119
  {
98
- name: '精确优先于通配',
99
- rules: [ruleOf('*', 5), ruleOf('a', 1)], model: 'a', date: MATCH_SUNDAY_1000, expected: priceOf(1),
120
+ name: '请求首位斜杠不拆段归 default 供应商',
121
+ rules: [ruleOf('default//x', 1), ruleOf('*/x', 2)], model: '/x', date: MATCH_SUNDAY_1000, expected: priceOf(1),
122
+ },
123
+ {
124
+ name: '单段旧形态规则失效',
125
+ rules: [ruleOf('*', 5), ruleOf('a', 1)], model: 'x/a', date: MATCH_SUNDAY_1000, expected: null,
126
+ },
127
+ {
128
+ name: '供应商段不同不命中',
129
+ rules: [ruleOf('x/*', 1)], model: 'y/a', date: MATCH_SUNDAY_1000, expected: null,
130
+ },
131
+ {
132
+ name: '模型段不同不命中',
133
+ rules: [ruleOf('*/a', 1)], model: 'x/b', date: MATCH_SUNDAY_1000, expected: null,
100
134
  },
101
135
  {
102
136
  name: '多条件 AND 全过命中',
103
- rules: [ruleOf('a', 1, [{ kind: 'weekdays', days: [0] }, { kind: 'dailyWindow', from: '00:00', to: '23:59' }])],
104
- model: 'a', date: MATCH_SUNDAY_1000, expected: priceOf(1),
137
+ rules: [ruleOf('x/a', 1, [{ kind: 'weekdays', days: [0] }, { kind: 'dailyWindow', from: '00:00', to: '23:59' }])],
138
+ model: 'x/a', date: MATCH_SUNDAY_1000, expected: priceOf(1),
105
139
  },
106
140
  {
107
141
  name: '一条件不过跳至下一条规则',
108
- rules: [ruleOf('a', 1, [{ kind: 'weekdays', days: [1] }]), ruleOf('a', 2, [])],
109
- model: 'a', date: MATCH_SUNDAY_1000, expected: priceOf(2),
142
+ rules: [ruleOf('x/a', 1, [{ kind: 'weekdays', days: [1] }]), ruleOf('x/a', 2, [])],
143
+ model: 'x/a', date: MATCH_SUNDAY_1000, expected: priceOf(2),
144
+ },
145
+ {
146
+ name: '高档条件不过逐层落低档',
147
+ rules: [ruleOf('x/a', 1, [{ kind: 'weekdays', days: [1] }]), ruleOf('*/a', 5)],
148
+ model: 'x/a', date: MATCH_SUNDAY_1000, expected: priceOf(5),
110
149
  },
111
150
  {
112
151
  name: '缺 conditions 形状残缺跳过',
113
- rules: [{ model: 'a', currency: '¥', price: priceOf(1) }],
114
- model: 'a', date: MATCH_SUNDAY_1000, expected: null,
152
+ rules: [{ model: 'x/a', currency: '¥', price: priceOf(1) }],
153
+ model: 'x/a', date: MATCH_SUNDAY_1000, expected: null,
115
154
  },
116
155
  {
117
156
  name: '缺 price 形状残缺跳过',
118
- rules: [{ model: 'a', currency: '¥', conditions: [] }],
119
- model: 'a', date: MATCH_SUNDAY_1000, expected: null,
157
+ rules: [{ model: 'x/a', currency: '¥', conditions: [] }],
158
+ model: 'x/a', date: MATCH_SUNDAY_1000, expected: null,
120
159
  },
121
160
  {
122
161
  name: 'unit 非 perMillion 跳过',
123
- rules: [{ ...ruleOf('a', 1), unit: 'perThousand' }],
124
- model: 'a', date: MATCH_SUNDAY_1000, expected: null,
162
+ rules: [{ ...ruleOf('x/a', 1), unit: 'perThousand' }],
163
+ model: 'x/a', date: MATCH_SUNDAY_1000, expected: null,
125
164
  },
126
165
  {
127
166
  name: 'unit 缺省视为 perMillion 命中',
128
- rules: [ruleOf('a', 1)],
129
- model: 'a', date: MATCH_SUNDAY_1000, expected: priceOf(1),
167
+ rules: [ruleOf('x/a', 1)],
168
+ model: 'x/a', date: MATCH_SUNDAY_1000, expected: priceOf(1),
130
169
  },
131
170
  {
132
171
  name: 'rules 非数组为 null',
133
- rules: 'nope', model: 'a', date: MATCH_SUNDAY_1000, expected: null,
172
+ rules: 'nope', model: 'x/a', date: MATCH_SUNDAY_1000, expected: null,
134
173
  },
135
174
  {
136
175
  name: 'model 非字符串为 null',
137
- rules: [ruleOf('a', 1)], model: 7, date: MATCH_SUNDAY_1000, expected: null,
176
+ rules: [ruleOf('x/a', 1)], model: 7, date: MATCH_SUNDAY_1000, expected: null,
138
177
  },
139
178
  {
140
179
  name: '非法时间戳为 null',
141
- rules: [ruleOf('a', 1)], model: 'a', date: null, expected: null,
180
+ rules: [ruleOf('x/a', 1)], model: 'x/a', date: null, expected: null,
142
181
  },
143
182
  {
144
183
  name: '条件不满足且无通配为 null',
145
- rules: [ruleOf('a', 1, [{ kind: 'weekdays', days: [1] }])],
146
- model: 'a', date: MATCH_SUNDAY_1000, expected: null,
184
+ rules: [ruleOf('x/a', 1, [{ kind: 'weekdays', days: [1] }])],
185
+ model: 'x/a', date: MATCH_SUNDAY_1000, expected: null,
147
186
  },
148
187
  ]
149
188
 
@@ -221,6 +260,30 @@ test(`matchPrice 双侧一致(${MATCH_VECTORS.length} 向量)`, () => {
221
260
  }
222
261
  })
223
262
 
263
+ const SEGMENT_VECTORS = [
264
+ { name: '两段合法', model: 'a/b', expected: true },
265
+ { name: '通配段合法', model: '*/*', expected: true },
266
+ { name: '多级模型名合法', model: 'a/b/c', expected: true },
267
+ { name: '单段非法', model: 'a', expected: false },
268
+ { name: '通配单段非法', model: '*', expected: false },
269
+ { name: '首位斜杠非法', model: '/b', expected: false },
270
+ { name: '空串非法', model: '', expected: false },
271
+ { name: '模型段空非法', model: 'a/', expected: false },
272
+ { name: '段含前导空白非法', model: ' /b', expected: false },
273
+ { name: '段含尾随空白非法', model: 'a/ ', expected: false },
274
+ { name: '段内含空白非法', model: 'a /b', expected: false },
275
+ { name: '通配段含空白非法', model: '* /b', expected: false },
276
+ ]
277
+
278
+ test(`isTwoSegmentModel 双侧一致(${SEGMENT_VECTORS.length} 向量)`, () => {
279
+ for (const vector of SEGMENT_VECTORS) {
280
+ const host = hostIsTwoSegmentModel(vector.model)
281
+ const client = clientIsTwoSegmentModel(vector.model)
282
+ assert.deepEqual(client, host, vector.name)
283
+ assert.equal(host, vector.expected, vector.name)
284
+ }
285
+ })
286
+
224
287
  test(`costOf 双侧一致(${COST_VECTORS.length} 向量)`, () => {
225
288
  for (const vector of COST_VECTORS) {
226
289
  const host = hostCostOf(vector.price, vector.buckets)
@@ -8,6 +8,7 @@ import {
8
8
  UNIT_PER_MILLION,
9
9
  conditionMatches,
10
10
  costOf,
11
+ isTwoSegmentModel,
11
12
  matchPrice,
12
13
  } from '../src/pricing.js'
13
14
 
@@ -19,7 +20,7 @@ const SATURDAY = new Date(2026, 8, 12)
19
20
  const localTime = (day, hour, minute = 0) => new Date(2026, 8, day, hour, minute)
20
21
 
21
22
  const makeRule = (overrides = {}) => ({
22
- model: '*',
23
+ model: '*/*',
23
24
  unit: UNIT_PER_MILLION,
24
25
  currency: '¥',
25
26
  price: { input: 1, output: 2, cacheRead: 0.5, cacheWrite: 1 },
@@ -53,9 +54,9 @@ test('无条件规则恒命中', () => {
53
54
  })
54
55
 
55
56
  test('精确模型优先于通配规则', () => {
56
- // Given 精确规则与 '*' 规则并存 When 匹配 Then 取精确规则 price
57
+ // Given 精确规则与 '*/*' 规则并存 When 匹配 Then 取精确规则 price
57
58
  const exact = makeRule({ model: 'a/b', price: { input: 5 } })
58
- const wildcard = makeRule({ model: '*', price: { input: 1 } })
59
+ const wildcard = makeRule({ model: '*/*', price: { input: 1 } })
59
60
  assert.deepEqual(matchPrice([wildcard, exact], 'a/b', SUNDAY), exact.price)
60
61
  })
61
62
 
@@ -67,25 +68,126 @@ test('精确子集按数组序取首个命中', () => {
67
68
  })
68
69
 
69
70
  test('精确子集全不命中回落通配子集', () => {
70
- // Given 精确规则条件不满足、'*' 规则无条件 When 匹配 Then 回落 '*' 而非 null
71
+ // Given 精确规则条件不满足、'*/*' 规则无条件 When 匹配 Then 回落 '*/*' 而非 null
71
72
  const exact = makeRule({ model: 'a/b', conditions: [{ kind: 'weekdays', days: [1] }] })
72
- const wildcard = makeRule({ model: '*', price: { input: 3 } })
73
+ const wildcard = makeRule({ model: '*/*', price: { input: 3 } })
73
74
  assert.deepEqual(matchPrice([exact, wildcard], 'a/b', SUNDAY), wildcard.price)
74
75
  })
75
76
 
76
77
  test('通配子集按数组序取首个命中', () => {
77
- // Given 首条 '*' 条件满足、次条无条件 When 匹配 Then 取首条
78
- const first = makeRule({ model: '*', conditions: [{ kind: 'weekdays', days: [0] }], price: { input: 1 } })
79
- const second = makeRule({ model: '*', price: { input: 2 } })
78
+ // Given 首条 '*/*' 条件满足、次条无条件 When 匹配 Then 取首条
79
+ const first = makeRule({ model: '*/*', conditions: [{ kind: 'weekdays', days: [0] }], price: { input: 1 } })
80
+ const second = makeRule({ model: '*/*', price: { input: 2 } })
80
81
  assert.deepEqual(matchPrice([first, second], 'a/b', SUNDAY), first.price)
81
82
  })
82
83
 
83
84
  test('无精确子集直接查通配子集', () => {
84
- // Given 仅 '*' 规则 When 匹配未列出模型 Then 命中通配
85
- const wildcard = makeRule({ model: '*', price: { input: 4 } })
85
+ // Given 仅 '*/*' 规则 When 匹配未列出模型 Then 命中通配
86
+ const wildcard = makeRule({ model: '*/*', price: { input: 4 } })
86
87
  assert.deepEqual(matchPrice([wildcard], 'other/c', SUNDAY), wildcard.price)
87
88
  })
88
89
 
90
+ test('匹配链:全名优先于模型名、供应商、全通配', () => {
91
+ // Given 四档规则并存 When 匹配 Then 依次全名 > 模型名 > 供应商 > 全通配
92
+ const rules = [
93
+ makeRule({ model: 'x/*', price: { input: 3 } }),
94
+ makeRule({ model: '*/*', price: { input: 4 } }),
95
+ makeRule({ model: '*/a', price: { input: 2 } }),
96
+ makeRule({ model: 'x/a', price: { input: 1 } }),
97
+ ]
98
+ assert.deepEqual(matchPrice(rules, 'x/a', SUNDAY), { input: 1 })
99
+ })
100
+
101
+ test('匹配链:模型名通配优先于供应商通配', () => {
102
+ // Given '*/a' 与 'x/*' 并存 When 匹配 x/a Then 模型名精确档胜
103
+ const rules = [
104
+ makeRule({ model: 'x/*', price: { input: 3 } }),
105
+ makeRule({ model: '*/a', price: { input: 2 } }),
106
+ ]
107
+ assert.deepEqual(matchPrice(rules, 'x/a', SUNDAY), { input: 2 })
108
+ })
109
+
110
+ test('匹配链:供应商通配优先于全通配', () => {
111
+ // Given 'x/*' 与 '*/*' 并存 When 匹配 x/a Then 供应商精确档胜
112
+ const rules = [
113
+ makeRule({ model: '*/*', price: { input: 4 } }),
114
+ makeRule({ model: 'x/*', price: { input: 3 } }),
115
+ ]
116
+ assert.deepEqual(matchPrice(rules, 'x/a', SUNDAY), { input: 3 })
117
+ })
118
+
119
+ test('模型名通配跨供应商命中同模型名', () => {
120
+ // Given '*/gpt-4o' When 匹配不同供应商同模型名 Then 命中;模型名不同不命中
121
+ const rule = makeRule({ model: '*/gpt-4o', price: { input: 7 } })
122
+ assert.deepEqual(matchPrice([rule], 'openai/gpt-4o', SUNDAY), { input: 7 })
123
+ assert.deepEqual(matchPrice([rule], 'azure/gpt-4o', SUNDAY), { input: 7 })
124
+ assert.equal(matchPrice([rule], 'openai/gpt-4o-mini', SUNDAY), null)
125
+ })
126
+
127
+ test('供应商通配命中同供应商全部模型', () => {
128
+ // Given 'openai/*' When 匹配同供应商多模型 Then 命中;异供应商不命中
129
+ const rule = makeRule({ model: 'openai/*', price: { input: 6 } })
130
+ assert.deepEqual(matchPrice([rule], 'openai/gpt-4o', SUNDAY), { input: 6 })
131
+ assert.deepEqual(matchPrice([rule], 'openai/o3', SUNDAY), { input: 6 })
132
+ assert.equal(matchPrice([rule], 'azure/gpt-4o', SUNDAY), null)
133
+ })
134
+
135
+ test('模型名段允许含斜杠按首个斜杠分段', () => {
136
+ // Given vendor 段通配或精确 When 匹配模型名含斜杠的多级模型 Then 按 vendor/余段比对
137
+ const vendorWide = makeRule({ model: 'openrouter/*', price: { input: 8 } })
138
+ assert.deepEqual(matchPrice([vendorWide], 'openrouter/deepseek/deepseek-chat', SUNDAY), { input: 8 })
139
+ const named = makeRule({ model: '*/deepseek/deepseek-chat', price: { input: 9 } })
140
+ assert.deepEqual(matchPrice([named], 'openrouter/deepseek/deepseek-chat', SUNDAY), { input: 9 })
141
+ assert.equal(matchPrice([named], 'openrouter/deepseek/chat', SUNDAY), null)
142
+ })
143
+
144
+ test('单段规则属旧形态失效不命中', () => {
145
+ // Given 旧写法 '*' 与裸名规则 When 匹配 Then 一律 null
146
+ const legacy = makeRule({ model: '*', price: { input: 1 } })
147
+ const bare = makeRule({ model: 'a', price: { input: 2 } })
148
+ assert.equal(matchPrice([legacy, bare], 'a/b', SUNDAY), null)
149
+ assert.equal(matchPrice([legacy, bare], 'a', SUNDAY), null)
150
+ })
151
+
152
+ test('请求裸名模型归 default 供应商', () => {
153
+ // Given 无斜杠请求 When 匹配 'default/名' 与 '*/名' Then 全名档优先
154
+ const exact = makeRule({ model: 'default/abc', price: { input: 1 } })
155
+ const named = makeRule({ model: '*/abc', price: { input: 2 } })
156
+ assert.deepEqual(matchPrice([named, exact], 'abc', SUNDAY), { input: 1 })
157
+ assert.deepEqual(matchPrice([named], 'abc', SUNDAY), { input: 2 })
158
+ })
159
+
160
+ test('档位回落:高档条件不满足逐层落低档', () => {
161
+ // Given 全名与模型名档条件仅周一 When 周日匹配 Then 落到无条件的供应商档
162
+ const exact = makeRule({ model: 'x/a', conditions: [{ kind: 'weekdays', days: [1] }], price: { input: 1 } })
163
+ const modelName = makeRule({ model: '*/a', conditions: [{ kind: 'weekdays', days: [1] }], price: { input: 2 } })
164
+ const vendor = makeRule({ model: 'x/*', price: { input: 3 } })
165
+ assert.deepEqual(matchPrice([exact, modelName, vendor], 'x/a', SUNDAY), { input: 3 })
166
+ })
167
+
168
+ test('同档规则按数组序取首个条件满足者', () => {
169
+ // Given 同为 '*/a' 两条,首条条件周日 When 周日匹配 Then 取首条
170
+ const first = makeRule({ model: '*/a', price: { input: 1 }, conditions: [{ kind: 'weekdays', days: [0] }] })
171
+ const second = makeRule({ model: '*/a', price: { input: 2 } })
172
+ assert.deepEqual(matchPrice([first, second], 'x/a', SUNDAY), { input: 1 })
173
+ })
174
+
175
+ test('isTwoSegmentModel 判定提交形态两段式', () => {
176
+ // Given 两段式与单段/空白段串 When 判定 Then 首斜杠在首位之后且两段无空白才成立
177
+ assert.equal(isTwoSegmentModel('a/b'), true)
178
+ assert.equal(isTwoSegmentModel('*/*'), true)
179
+ assert.equal(isTwoSegmentModel('a/b/c'), true)
180
+ assert.equal(isTwoSegmentModel('a'), false)
181
+ assert.equal(isTwoSegmentModel('*'), false)
182
+ assert.equal(isTwoSegmentModel('/b'), false)
183
+ assert.equal(isTwoSegmentModel(''), false)
184
+ assert.equal(isTwoSegmentModel('a/'), false)
185
+ assert.equal(isTwoSegmentModel(' /b'), false)
186
+ assert.equal(isTwoSegmentModel('a/ '), false)
187
+ assert.equal(isTwoSegmentModel('a /b'), false)
188
+ assert.equal(isTwoSegmentModel('* /b'), false)
189
+ })
190
+
89
191
  test('两子集全不命中返回 null 计 unpriced', () => {
90
192
  // Given 仅一条精确规则且条件不满足 When 匹配本模型与陌生模型 Then 均为 null
91
193
  const rules = [makeRule({ model: 'a/b', conditions: [{ kind: 'weekdays', days: [1] }] })]
@@ -257,7 +257,7 @@ test('H 粒度槽数恰为 MAX_SLOTS 时不出现 truncated', () => {
257
257
  // ---- attachCosts 聚合计价:计价唯一以 H 桶起点匹配,费用精度=小时级 ----
258
258
 
259
259
  const ruleOf = (overrides = {}) => ({
260
- model: '*',
260
+ model: '*/*',
261
261
  currency: '¥',
262
262
  price: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
263
263
  conditions: [],
@@ -266,12 +266,62 @@ const ruleOf = (overrides = {}) => ({
266
266
 
267
267
  const inputPrice = (input) => ({ input, output: 0, cacheRead: 0, cacheWrite: 0 })
268
268
 
269
+ test('speed 配对聚合:仅 durationMs>0 行计入分子分母,老行只进 tokens', () => {
270
+ const rows = [
271
+ makeRow({ bucket: '2020-01-01', model: 'm1', provider: 'p1', inputTokens: 5, outputTokens: 20, durationMs: 6500 }),
272
+ makeRow({ bucket: '2020-01-02', model: 'm1', provider: 'p1', outputTokens: 30, durationMs: 2500 }),
273
+ // 存量旧格式行:无 durationMs,token 计入总量但速度分母不含
274
+ makeRow({ bucket: '2020-01-03', model: 'm1', provider: 'p1', outputTokens: 100 }),
275
+ ]
276
+ const out = aggregateRange(rows, 'D', '2020-01-01', '2020-01-03')
277
+ const entry = out.models.find((item) => item.model === 'm1')
278
+ assert.equal(entry.tokens, 25 + 30 + 100)
279
+ assert.equal(entry.speed, (20 + 30) / ((6500 + 2500) / 1000))
280
+ })
281
+
282
+ test('无 durationMs 数据时 models 条目无 speed 字段', () => {
283
+ const rows = [
284
+ makeRow({ bucket: '2020-01-01', model: 'm1', provider: 'p1', outputTokens: 30 }),
285
+ ]
286
+ const out = aggregateRange(rows, 'D', '2020-01-01', '2020-01-01')
287
+ assert.equal(out.models.length, 1)
288
+ assert.equal('speed' in out.models[0], false)
289
+ })
290
+
291
+ test('纯缓存行带时长:输出 0 计入分母,speed 为 0', () => {
292
+ const rows = [
293
+ makeRow({ bucket: '2020-01-01', model: 'm1', provider: 'p1', cacheReadTokens: 10, outputTokens: 0, durationMs: 4000 }),
294
+ ]
295
+ const out = aggregateRange(rows, 'D', '2020-01-01', '2020-01-01')
296
+ assert.equal(out.models[0].speed, 0)
297
+ })
298
+
299
+ test('槽级 speed 配对聚合:同槽带时长行合计,无时长槽无 speed 字段', () => {
300
+ const rows = [
301
+ makeRow({ bucket: '2020-01-01', model: 'm1', provider: 'p1', outputTokens: 20, durationMs: 6500 }),
302
+ makeRow({ bucket: '2020-01-01', model: 'm2', provider: 'p2', outputTokens: 30, durationMs: 2500 }),
303
+ makeRow({ bucket: '2020-01-02', model: 'm1', provider: 'p1', outputTokens: 100 }),
304
+ ]
305
+ const out = aggregateRange(rows, 'D', '2020-01-01', '2020-01-02')
306
+ assert.equal(out.daily[0].speed, (20 + 30) / ((6500 + 2500) / 1000))
307
+ assert.equal('speed' in out.daily[1], false)
308
+ })
309
+
310
+ test('attachCosts 保留槽级 speed 字段供曲线消费', () => {
311
+ const rows = [
312
+ makeRow({ bucket: '2020-01-01T01', model: 'm1', provider: 'p1', outputTokens: 20, durationMs: 4000 }),
313
+ ]
314
+ const result = aggregateRange(rows, 'H', '2020-01-01T00', '2020-01-01T02')
315
+ const out = attachCosts(result, rows, 'H', [ruleOf()])
316
+ assert.equal(out.daily[1].speed, 5)
317
+ })
318
+
269
319
  test('attachCosts H 槽按桶起点计价并归集 totals 与 models', () => {
270
320
  const rows = [
271
321
  makeRow({ bucket: '2020-01-01T01', model: 'm1', provider: 'p1', inputTokens: 1000000, outputTokens: 500000 }),
272
322
  ]
273
323
  const result = aggregateRange(rows, 'H', '2020-01-01T00', '2020-01-01T02')
274
- const out = attachCosts(result, rows, 'H', [ruleOf({ model: 'm1', price: { input: 1, output: 2, cacheRead: 0, cacheWrite: 0 } })])
324
+ const out = attachCosts(result, rows, 'H', [ruleOf({ model: 'default/m1', price: { input: 1, output: 2, cacheRead: 0, cacheWrite: 0 } })])
275
325
  assert.equal(out.daily[1].cost, 2)
276
326
  assert.equal(out.cost, 2)
277
327
  assert.equal(out.unpriced, 0)
@@ -289,7 +339,7 @@ test('attachCosts 多行同桶多规则累加且残缺规则跳过后回落通
289
339
  makeRow({ bucket: '2020-01-01T01', model: 'm2', inputTokens: 1000000 }),
290
340
  ]
291
341
  const rules = [
292
- ruleOf({ model: 'm1', price: inputPrice(1) }),
342
+ ruleOf({ model: 'default/m1', price: inputPrice(1) }),
293
343
  { model: 'm2' },
294
344
  ruleOf({ price: inputPrice(3) }),
295
345
  ]
@@ -6,7 +6,7 @@ import assert from 'node:assert/strict'
6
6
  import { Readable } from 'node:stream'
7
7
 
8
8
  import { MAX_RANGE_SPAN_DAYS, registerUsageRoutes } from '../src/routes.js'
9
- import { DEFAULT_MINUTE_RETENTION_DAYS, minuteKey } from '../src/store.js'
9
+ import { DEFAULT_MINUTE_RETENTION_DAYS, hourKey, minuteKey } from '../src/store.js'
10
10
 
11
11
  const DAY_MS = 24 * 60 * 60 * 1000
12
12
  const MINUTE_MS = 60 * 1000
@@ -338,8 +338,9 @@ test('minutes 禁用保留时标注空 covered', async () => {
338
338
 
339
339
  test('minutes from 未对齐 10 分钟桶边界拒绝 400', async () => {
340
340
  const { routes } = mount()
341
- const d = new Date(FIXED_NOW)
342
- const unaligned = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}T12:07`
341
+ // from hourKey 本地渲染保证与 to 同一时区基准,前一小时的 :07 早于 to 且未对齐;
342
+ // 前提是 FIXED_NOW 不落入 DST 回拨小时(当前 2 月常量全球无转变日)
343
+ const unaligned = `${hourKey(FIXED_NOW - 60 * MINUTE_MS)}:07`
343
344
  const res = await invokeJson(routes, PATH_MINUTES, { payload: { from: unaligned, to: minuteKey(FIXED_NOW) } })
344
345
  const parsed = JSON.parse(res.body)
345
346
  assert.equal(res.statusCode, 400)
@@ -349,12 +350,12 @@ test('minutes from 未对齐 10 分钟桶边界拒绝 400', async () => {
349
350
 
350
351
  test('minutes 保留值超上限时 covered 窗口按 clamp 计算', async () => {
351
352
  const { routes } = mount({ retentionDays: () => 30 })
352
- const from = minuteKey(FIXED_NOW - 3 * DAY_MS)
353
+ const from = minuteKey(FIXED_NOW - 8 * DAY_MS)
353
354
  const to = minuteKey(FIXED_NOW)
354
355
  const res = await invokeJson(routes, PATH_MINUTES, { payload: { from, to } })
355
356
  const parsed = JSON.parse(res.body)
356
357
  assert.equal(res.statusCode, 200)
357
- assert.equal(parsed.value.coveredFrom, minuteKey(FIXED_NOW - 2 * DAY_MS))
358
+ assert.equal(parsed.value.coveredFrom, minuteKey(FIXED_NOW - 7 * DAY_MS))
358
359
  assert.equal(parsed.value.coveredTo, to)
359
360
  })
360
361
 
@@ -382,14 +383,14 @@ test('内部错误 500 不回显内部文本', async () => {
382
383
  // ---- S13 pricing 端点与 cost 集成 ----
383
384
 
384
385
  const PRICING_RULE = {
385
- model: 'deepseek-chat',
386
+ model: 'deepseek/deepseek-chat',
386
387
  currency: '¥',
387
388
  price: { input: 1, output: 2, cacheRead: 0, cacheWrite: 0 },
388
389
  conditions: [{ kind: 'dailyWindow', from: '09:00', to: '18:00' }],
389
390
  }
390
391
 
391
392
  const ruleWithInputPrice = (input) => ({
392
- model: 'deepseek-chat',
393
+ model: 'default/deepseek-chat',
393
394
  currency: '¥',
394
395
  price: { input, output: 0, cacheRead: 0, cacheWrite: 0 },
395
396
  conditions: [],
@@ -438,6 +439,9 @@ test('pricing POST 非法规则拒绝 400 且不写入', async () => {
438
439
  { rules: 'x' },
439
440
  {},
440
441
  { rules: [{ model: 1, currency: '¥', price: PRICING_RULE.price, conditions: [] }] },
442
+ { rules: [{ ...PRICING_RULE, model: 'deepseek-chat' }] },
443
+ { rules: [{ ...PRICING_RULE, model: '*' }] },
444
+ { rules: [{ ...PRICING_RULE, model: '/deepseek-chat' }] },
441
445
  { rules: [{ ...PRICING_RULE, price: { input: -1, output: 0, cacheRead: 0, cacheWrite: 0 } }] },
442
446
  { rules: [{ ...PRICING_RULE, conditions: [{ kind: 'unknown' }] }] },
443
447
  { rules: [{ ...PRICING_RULE, conditions: [{ kind: 'weekdays', days: [7] }] }] },
@@ -1,19 +1,11 @@
1
1
  // 底部信息栏增强纯函数层测试:官方 StatsLine 口径(dsh-client-ui-chat 同构)+ usp 双开关状态
2
2
  // Given/When/Then 场景内嵌于用例描述
3
- // client.js 为非模块 script(bundle 求值形态,禁 import/export),整源求值后按顶层声明名收集
3
+ // client.js 为经典 script bundle( import/export),整文件 IIFE 书挡;经 client-eval 剥壳后整源求值,按顶层声明名收集
4
4
  import { test } from 'node:test'
5
5
  import assert from 'node:assert/strict'
6
- import { readFileSync } from 'node:fs'
7
- import { fileURLToPath } from 'node:url'
8
- import { dirname, join } from 'node:path'
9
-
10
- const CLIENT_SOURCE = readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'src', 'client.js'), 'utf8')
11
- const DECLARATION_NAMES = [
12
- ...new Set(
13
- [...CLIENT_SOURCE.matchAll(/^(?:const|function|let) ([A-Za-z_$][\w$]*)/gm)].map((match) => match[1])
14
- ),
15
- ]
16
- const core = new Function(`${CLIENT_SOURCE}\nreturn { ${DECLARATION_NAMES.join(', ')} }`)()
6
+ import { CLIENT_BODY, DECLARATION_NAMES } from './client-eval.mjs'
7
+
8
+ const core = new Function(`${CLIENT_BODY}\nreturn { ${DECLARATION_NAMES.join(', ')} }`)()
17
9
 
18
10
  const {
19
11
  STATS_LINE_STORAGE_KEY,
@@ -43,11 +35,11 @@ const PREFS_COST_ON = { cachePrecision: false, tokenDetail: false, costDisplay:
43
35
  const zhT = createTranslator(MESSAGES_ZH)
44
36
  const enT = createTranslator(MESSAGES_EN)
45
37
 
46
- // 费用组装配输入:精确规则在前、通配规则兜底,通配仅全天时段生效
38
+ // 费用组装配输入:精确规则在前、全通配规则兜底,通配仅全天时段生效
47
39
  const COST_RULES = [
48
40
  { model: 'p/m', currency: '¥', price: { input: 2, output: 0, cacheRead: 0, cacheWrite: 0 }, conditions: [] },
49
41
  {
50
- model: '*', currency: '', price: { input: 1, output: 0, cacheRead: 0, cacheWrite: 0 },
42
+ model: '*/*', currency: '', price: { input: 1, output: 0, cacheRead: 0, cacheWrite: 0 },
51
43
  conditions: [{ kind: 'dailyWindow', from: '00:00', to: '00:00' }],
52
44
  },
53
45
  ]
@@ -353,8 +345,8 @@ test('buildCostItem 开关关恒为 null,组数组不受 rules 影响', () => {
353
345
  ])
354
346
  })
355
347
 
356
- test('buildCostItem routes 缺席按通配规则匹配,符号取全局口径(首个非空货币)', () => {
357
- // 无 routes → model '*',500000 × 1 / 每百万 = 0.5;命中通配(货币空),符号取表内首个非空 ¥
348
+ test('buildCostItem routes 缺席按全通配规则匹配,符号取全局口径(首个非空货币)', () => {
349
+ // 无 routes → model '*/*',500000 × 1 / 每百万 = 0.5;命中全通配(货币空),符号取表内首个非空 ¥
358
350
  assert.equal(buildCostItem(COST_USAGE, COST_RULES, PREFS_COST_ON, zhT, COST_NOW), '费用 ≈ ¥0.50')
359
351
  assert.equal(buildCostItem(COST_USAGE, COST_RULES, PREFS_COST_ON, enT, COST_NOW), 'Cost ≈ ¥0.50')
360
352
  })