@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.
- package/README.md +13 -11
- package/package.json +1 -1
- package/src/client.js +588 -160
- package/src/collector.js +46 -12
- package/src/index.js +2 -2
- package/src/pricing.js +47 -9
- package/src/query.js +35 -3
- package/src/routes.js +3 -2
- package/src/store.js +123 -43
- package/test/client-eval.mjs +23 -0
- package/test/client-scope.test.mjs +60 -0
- package/test/client.test.mjs +253 -43
- package/test/collector.test.mjs +93 -0
- package/test/pricing-parity.test.mjs +99 -36
- package/test/pricing.test.mjs +112 -10
- package/test/query.test.mjs +53 -3
- package/test/routes.test.mjs +11 -7
- package/test/stats-line.test.mjs +8 -16
- package/test/store.test.mjs +145 -22
- package/test/turn-tail.test.mjs +12 -20
|
@@ -1,34 +1,28 @@
|
|
|
1
1
|
// 双实现同源 parity 测试:client.js 顶层定价镜像函数与宿主 src/pricing.js 同输入同输出
|
|
2
2
|
// 向量表驱动:同一批向量喂双侧,deepEqual 双侧结果并对期望值断言
|
|
3
|
-
// client.js
|
|
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 {
|
|
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
|
|
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: '
|
|
91
|
-
rules: [ruleOf('
|
|
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('
|
|
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('
|
|
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)
|
package/test/pricing.test.mjs
CHANGED
|
@@ -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 精确规则与 '
|
|
57
|
+
// Given 精确规则与 '*/*' 规则并存 When 匹配 Then 取精确规则 price
|
|
57
58
|
const exact = makeRule({ model: 'a/b', price: { input: 5 } })
|
|
58
|
-
const wildcard = makeRule({ model: '
|
|
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 精确规则条件不满足、'
|
|
71
|
+
// Given 精确规则条件不满足、'*/*' 规则无条件 When 匹配 Then 回落 '*/*' 而非 null
|
|
71
72
|
const exact = makeRule({ model: 'a/b', conditions: [{ kind: 'weekdays', days: [1] }] })
|
|
72
|
-
const wildcard = makeRule({ model: '
|
|
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 首条 '
|
|
78
|
-
const first = makeRule({ model: '
|
|
79
|
-
const second = makeRule({ model: '
|
|
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 仅 '
|
|
85
|
-
const wildcard = makeRule({ model: '
|
|
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] }] })]
|
package/test/query.test.mjs
CHANGED
|
@@ -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
|
]
|
package/test/routes.test.mjs
CHANGED
|
@@ -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
|
-
|
|
342
|
-
|
|
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 -
|
|
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 -
|
|
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] }] }] },
|
package/test/stats-line.test.mjs
CHANGED
|
@@ -1,19 +1,11 @@
|
|
|
1
1
|
// 底部信息栏增强纯函数层测试:官方 StatsLine 口径(dsh-client-ui-chat 同构)+ usp 双开关状态
|
|
2
2
|
// Given/When/Then 场景内嵌于用例描述
|
|
3
|
-
// client.js
|
|
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 {
|
|
7
|
-
|
|
8
|
-
|
|
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: '
|
|
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 '
|
|
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
|
})
|