@mzzsfy/dsh-usage-dash 0.3.0 → 0.5.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 +55 -14
- package/package.json +2 -2
- package/src/archive-reader.js +138 -0
- package/src/client.js +539 -128
- package/src/collector.js +175 -29
- package/src/direct-log-reader.js +126 -0
- package/src/pricing.js +5 -6
- package/src/query.js +38 -14
- package/src/routes.js +14 -1
- package/src/store.js +86 -4
- package/test/archive-reader.test.mjs +365 -0
- package/test/client.test.mjs +210 -12
- package/test/collector.test.mjs +458 -19
- package/test/direct-log-reader.test.mjs +104 -0
- package/test/pricing-parity.test.mjs +38 -4
- package/test/pricing.test.mjs +50 -18
- package/test/query.test.mjs +50 -1
- package/test/routes.test.mjs +9 -2
- package/test/stats-line.test.mjs +6 -6
- package/test/store.test.mjs +116 -1
- package/test/stream-parity.test.mjs +75 -0
- package/test/turn-tail.test.mjs +161 -41
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// 官方同构 parity:collector 移植的助手流首 token 读取与官方 dsh-llm
|
|
2
|
+
// assistantStreamFirstTokenTime / isTokenDelta / runFirstTokenTime 语义逐向量对表。
|
|
3
|
+
// 期望值按官方源码语义推导(首个 token delta = 非空 text/reasoning 成员、
|
|
4
|
+
// 非空 tool-call arguments 成员或具名 tool-call run 起点)。
|
|
5
|
+
// 对表锚点:对表自 dsh 0.1.2-rc.1 官方源码(dsh-llm/lib/types/assistant-stream.js
|
|
6
|
+
// 与 dsh-session-stats/lib/types/projection.js);dsh 本体升级后须重对表,
|
|
7
|
+
// 此处静态向量不感知官方实现漂移。
|
|
8
|
+
|
|
9
|
+
import test from 'node:test'
|
|
10
|
+
import assert from 'node:assert/strict'
|
|
11
|
+
|
|
12
|
+
import { assistantStreamFirstTokenTime } from '../src/collector.js'
|
|
13
|
+
|
|
14
|
+
const textRun = (time0, texts, dt = []) => ({ type: 'text-chunks', time0, index: 0, dt, texts })
|
|
15
|
+
const reasoningRun = (time0, texts, dt = []) => ({ type: 'reasoning-chunks', time0, index: 0, dt, texts })
|
|
16
|
+
const toolRun = (time0, args, extra = {}) => ({ type: 'tool-call-chunks', time0, index: 0, dt: [], id: 'c1', args, ...extra })
|
|
17
|
+
const rawChunk = (time, chunk) => ({ type: 'chunk', time, chunk })
|
|
18
|
+
|
|
19
|
+
test('text run 首个非空成员时刻按 time0+dt 差分还原', () => {
|
|
20
|
+
assert.equal(assistantStreamFirstTokenTime([textRun(1000, ['', 'ab'], [100])]), 1100)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('全空 text run 无首 token', () => {
|
|
24
|
+
assert.equal(assistantStreamFirstTokenTime([textRun(1000, ['', ''])]), undefined)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('reasoning run 与 text run 同语义', () => {
|
|
28
|
+
assert.equal(assistantStreamFirstTokenTime([reasoningRun(500, ['', '思考'], [20])]), 520)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('具名 tool call run 整体起于 time0', () => {
|
|
32
|
+
assert.equal(assistantStreamFirstTokenTime([toolRun(700, ['', '{'], { name: 'fn' })]), 700)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('匿名 tool call run 取首个非空 arguments 成员', () => {
|
|
36
|
+
assert.equal(assistantStreamFirstTokenTime([toolRun(700, ['', '{'], { dt: [30] })]), 730)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('裸 chunk:text-delta 非空即 token,空文本跳过', () => {
|
|
40
|
+
assert.equal(assistantStreamFirstTokenTime([
|
|
41
|
+
rawChunk(100, { type: 'text-delta', index: 0, text: '' }),
|
|
42
|
+
rawChunk(200, { type: 'text-delta', index: 0, text: 'hi' }),
|
|
43
|
+
]), 200)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('裸 chunk:usage 与 finish 非 token', () => {
|
|
47
|
+
assert.equal(assistantStreamFirstTokenTime([
|
|
48
|
+
rawChunk(100, { type: 'usage' }),
|
|
49
|
+
rawChunk(200, { type: 'finish' }),
|
|
50
|
+
]), undefined)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('首个产出 token 的记录生效,后续记录不覆盖', () => {
|
|
54
|
+
assert.equal(assistantStreamFirstTokenTime([
|
|
55
|
+
rawChunk(100, { type: 'usage' }),
|
|
56
|
+
textRun(900, ['a']),
|
|
57
|
+
textRun(500, ['b']),
|
|
58
|
+
]), 900)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('空流与非数组输入无首 token', () => {
|
|
62
|
+
assert.equal(assistantStreamFirstTokenTime([]), undefined)
|
|
63
|
+
assert.equal(assistantStreamFirstTokenTime(undefined), undefined)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('未知记录形态安全跳过不崩溃', () => {
|
|
67
|
+
assert.equal(assistantStreamFirstTokenTime([
|
|
68
|
+
{ type: 'text-delta', text: 'x' },
|
|
69
|
+
textRun(300, ['晚到']),
|
|
70
|
+
]), 300)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('dt 差分短缺视为损坏,按无首 token 处理', () => {
|
|
74
|
+
assert.equal(assistantStreamFirstTokenTime([textRun(1000, ['', 'ab'], [])]), undefined)
|
|
75
|
+
})
|
package/test/turn-tail.test.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// 注入点B
|
|
1
|
+
// 注入点B 纯函数层测试:messageId 反查矩阵/模型键回退/芯片文本/title 口径/文案键/注册与 CSS 镜像
|
|
2
2
|
// client.js 为经典 script bundle(禁 import/export),整文件 IIFE 书挡;经 client-eval 剥壳后整源求值,按顶层声明名收集
|
|
3
3
|
import { test } from 'node:test'
|
|
4
4
|
import assert from 'node:assert/strict'
|
|
@@ -10,14 +10,15 @@ const {
|
|
|
10
10
|
MESSAGES_EN,
|
|
11
11
|
MESSAGES_ZH,
|
|
12
12
|
MODEL_UNROUTED,
|
|
13
|
-
|
|
14
|
-
TURN_TAIL_PRIORITY,
|
|
15
|
-
buildTurnCostLine,
|
|
13
|
+
buildTurnCostChipText,
|
|
16
14
|
createTranslator,
|
|
17
15
|
matchPrice,
|
|
18
|
-
|
|
16
|
+
turnCostAmountOf,
|
|
19
17
|
turnCostTitleText,
|
|
20
18
|
turnModelOf,
|
|
19
|
+
turnTokenUsageOfMessage,
|
|
20
|
+
turnUsageSourceOfMessage,
|
|
21
|
+
usageSourceBuckets,
|
|
21
22
|
} = core
|
|
22
23
|
|
|
23
24
|
const zhT = createTranslator(MESSAGES_ZH)
|
|
@@ -26,32 +27,134 @@ const enT = createTranslator(MESSAGES_EN)
|
|
|
26
27
|
// 本地时区固定时钟,与 client.test.mjs 同一刻
|
|
27
28
|
const NOW = new Date(2026, 2, 15, 14, 30, 45)
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
const tailNode = (messageId, tokenUsage) => ({
|
|
31
|
+
kind: 'turn-tail',
|
|
32
|
+
data: { turn: 1, closing: { finalNode: { seq: 9, messageId } }, tokenUsage },
|
|
32
33
|
})
|
|
33
34
|
|
|
34
|
-
test('
|
|
35
|
+
test('反查:matchId 命中返回 tokenUsage 本体', () => {
|
|
35
36
|
const tokenUsage = { uncachedInputTokens: 10, outputTokens: 5, totalTokens: 15 }
|
|
36
|
-
|
|
37
|
-
assert.equal(selectTurnTokenUsage(owner), tokenUsage)
|
|
37
|
+
assert.equal(turnTokenUsageOfMessage([tailNode('m-1', tokenUsage)], 'm-1'), tokenUsage)
|
|
38
38
|
})
|
|
39
39
|
|
|
40
|
-
test('
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
test('反查:多节点取命中者', () => {
|
|
41
|
+
const tokenUsage = { uncachedInputTokens: 1, outputTokens: 1, totalTokens: 2 }
|
|
42
|
+
const nodes = [tailNode('m-0', { uncachedInputTokens: 9, outputTokens: 9, totalTokens: 18 }), tailNode('m-1', tokenUsage)]
|
|
43
|
+
assert.equal(turnTokenUsageOfMessage(nodes, 'm-1'), tokenUsage)
|
|
43
44
|
})
|
|
44
45
|
|
|
45
|
-
test('
|
|
46
|
-
|
|
47
|
-
assert.equal(
|
|
48
|
-
assert.equal(
|
|
49
|
-
assert.equal(selectTurnTokenUsage(null), null)
|
|
50
|
-
assert.equal(selectTurnTokenUsage(undefined), null)
|
|
46
|
+
test('反查:非命中 kind 与 messageId 不误收', () => {
|
|
47
|
+
const assistant = { kind: 'assistant', messageId: 'm-1', data: { closing: { finalNode: { messageId: 'm-1' } }, tokenUsage: {} } }
|
|
48
|
+
assert.equal(turnTokenUsageOfMessage([assistant], 'm-1'), null)
|
|
49
|
+
assert.equal(turnTokenUsageOfMessage([tailNode('m-0', {})], 'm-1'), null)
|
|
51
50
|
})
|
|
52
51
|
|
|
53
|
-
test('
|
|
54
|
-
assert.equal(
|
|
52
|
+
test('反查:tokenUsage 缺失返回 null', () => {
|
|
53
|
+
assert.equal(turnTokenUsageOfMessage([{ kind: 'turn-tail', data: { closing: { finalNode: { messageId: 'm-1' } } } }], 'm-1'), null)
|
|
54
|
+
assert.equal(turnTokenUsageOfMessage([tailNode('m-1', null)], 'm-1'), null)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('反查:closing.usage 采样回退为聚合形态,可选桶带上', () => {
|
|
58
|
+
const sampled = { inputTokens: 100, outputTokens: 20, totalTokens: 130, cacheReadTokens: 10 }
|
|
59
|
+
const node = { kind: 'turn-tail', data: { closing: { finalNode: { seq: 1, messageId: 'm-1' }, usage: sampled } } }
|
|
60
|
+
assert.deepEqual(turnTokenUsageOfMessage([node], 'm-1'), { uncachedInputTokens: 100, outputTokens: 20, totalTokens: 130, cacheReadTokens: 10 })
|
|
61
|
+
const bare = { kind: 'turn-tail', data: { closing: { finalNode: { seq: 2, messageId: 'm-2' }, usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 } } } }
|
|
62
|
+
assert.deepEqual(turnTokenUsageOfMessage([bare], 'm-2'), { uncachedInputTokens: 1, outputTokens: 1, totalTokens: 2 })
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('反查:非数组与空列表返回 null', () => {
|
|
66
|
+
assert.equal(turnTokenUsageOfMessage(null, 'm-1'), null)
|
|
67
|
+
assert.equal(turnTokenUsageOfMessage(undefined, 'm-1'), null)
|
|
68
|
+
assert.equal(turnTokenUsageOfMessage([], 'm-1'), null)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('反查:chat 节点表 Map 形态与数组等价', () => {
|
|
72
|
+
const tokenUsage = { uncachedInputTokens: 10, outputTokens: 5, totalTokens: 15 }
|
|
73
|
+
const nodes = new Map([
|
|
74
|
+
['k1', tailNode('m-0', { uncachedInputTokens: 9, outputTokens: 9, totalTokens: 18 })],
|
|
75
|
+
['k2', tailNode('m-1', tokenUsage)],
|
|
76
|
+
])
|
|
77
|
+
assert.equal(turnTokenUsageOfMessage(nodes, 'm-1'), tokenUsage)
|
|
78
|
+
assert.equal(turnTokenUsageOfMessage(new Map(), 'm-1'), null)
|
|
79
|
+
assert.equal(turnTokenUsageOfMessage({ size: 0 }, 'm-1'), null)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test('反查:单节点形状残缺跳过不抛,后续命中不受阻', () => {
|
|
83
|
+
const hostile = { get kind() { throw new Error('boom') } }
|
|
84
|
+
const tokenUsage = { uncachedInputTokens: 1, outputTokens: 1, totalTokens: 2 }
|
|
85
|
+
assert.equal(turnTokenUsageOfMessage([hostile, tailNode('m-1', tokenUsage)], 'm-1'), tokenUsage)
|
|
86
|
+
assert.equal(turnTokenUsageOfMessage([{ kind: 'turn-tail', get data() { throw new Error('boom') } }], 'm-1'), null)
|
|
87
|
+
assert.equal(turnTokenUsageOfMessage([null, 'x', 42], 'm-1'), null)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('反查:同一容器重复调用命中索引,结果一致', () => {
|
|
91
|
+
const tokenUsage = { uncachedInputTokens: 10, outputTokens: 5, totalTokens: 15 }
|
|
92
|
+
const nodes = [tailNode('m-1', tokenUsage)]
|
|
93
|
+
// Given 同一容器连续两次反查(首次建索引,二次命中缓存)
|
|
94
|
+
const first = turnTokenUsageOfMessage(nodes, 'm-1')
|
|
95
|
+
const second = turnTokenUsageOfMessage(nodes, 'm-1')
|
|
96
|
+
// Then 结果一致且均为 tokenUsage 本体
|
|
97
|
+
assert.equal(first, tokenUsage)
|
|
98
|
+
assert.equal(second, tokenUsage)
|
|
99
|
+
// 未命中的 messageId 二次查询仍为 null(索引缓存不放大误命中)
|
|
100
|
+
assert.equal(turnTokenUsageOfMessage(nodes, 'm-2'), null)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test('反查源:命中返回仓库内原始引用,采样形态不归一(归一属渲染层)', () => {
|
|
104
|
+
const tokenUsage = { uncachedInputTokens: 10, outputTokens: 5, totalTokens: 15 }
|
|
105
|
+
assert.equal(turnUsageSourceOfMessage([tailNode('m-1', tokenUsage)], 'm-1'), tokenUsage)
|
|
106
|
+
// 视图节点直挂 tokenUsage 次选
|
|
107
|
+
const directNode = { kind: 'turn-tail', data: { closing: { finalNode: { seq: 1, messageId: 'm-2' } }, tokenUsage } }
|
|
108
|
+
assert.equal(turnUsageSourceOfMessage([directNode], 'm-2'), tokenUsage)
|
|
109
|
+
// 采样 usage 原样返回(引用稳定,useSyncExternalStore 快照可比对)
|
|
110
|
+
const sampled = { inputTokens: 100, outputTokens: 20, totalTokens: 130, cacheReadTokens: 10 }
|
|
111
|
+
const sampledNode = { kind: 'turn-tail', data: { closing: { finalNode: { seq: 2, messageId: 'm-3' }, usage: sampled } } }
|
|
112
|
+
assert.equal(turnUsageSourceOfMessage([sampledNode], 'm-3'), sampled)
|
|
113
|
+
// 三级全缺返回 null(稳定值)
|
|
114
|
+
assert.equal(turnUsageSourceOfMessage([{ kind: 'turn-tail', data: { closing: { finalNode: { seq: 3, messageId: 'm-4' } } } }], 'm-4'), null)
|
|
115
|
+
assert.equal(turnUsageSourceOfMessage([], 'm-1'), null)
|
|
116
|
+
assert.equal(turnUsageSourceOfMessage(null, 'm-1'), null)
|
|
117
|
+
// 仓库两态
|
|
118
|
+
assert.equal(turnUsageSourceOfMessage(new Map([['k', tailNode('m-1', tokenUsage)]]), 'm-1'), tokenUsage)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test('反查源:节点数据更新后同容器引用随之更替', () => {
|
|
122
|
+
// Given 命中节点无用量源 When 位置数据补上 tokenUsage(同一容器对象,仓库可变语义)
|
|
123
|
+
const node = { kind: 'turn-tail', data: { closing: { finalNode: { seq: 1, messageId: 'm-1' } } } }
|
|
124
|
+
const nodes = [node]
|
|
125
|
+
assert.equal(turnUsageSourceOfMessage(nodes, 'm-1'), null)
|
|
126
|
+
const tokenUsage = { uncachedInputTokens: 1, outputTokens: 1, totalTokens: 2 }
|
|
127
|
+
node.data.tokenUsage = tokenUsage
|
|
128
|
+
// Then 反查读到新引用 —— selector 快照由恒等(null)变为新引用,驱动重渲染
|
|
129
|
+
assert.equal(turnUsageSourceOfMessage(nodes, 'm-1'), tokenUsage)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
test('归一:采样形态转聚合桶,聚合形态原样直通', () => {
|
|
133
|
+
assert.deepEqual(
|
|
134
|
+
usageSourceBuckets({ inputTokens: 100, outputTokens: 20, totalTokens: 130, cacheReadTokens: 10 }),
|
|
135
|
+
{ uncachedInputTokens: 100, outputTokens: 20, totalTokens: 130, cacheReadTokens: 10 },
|
|
136
|
+
)
|
|
137
|
+
const aggregated = { uncachedInputTokens: 8, outputTokens: 2, totalTokens: 10, routes: [{ provider: 'p', model: 'm' }] }
|
|
138
|
+
assert.equal(usageSourceBuckets(aggregated), aggregated)
|
|
139
|
+
assert.equal(usageSourceBuckets(null), null)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
test('反查:容器换引用(快照更替)后新节点可见', () => {
|
|
143
|
+
const oldUsage = { uncachedInputTokens: 1, outputTokens: 1, totalTokens: 2 }
|
|
144
|
+
const oldNodes = [tailNode('m-1', oldUsage)]
|
|
145
|
+
assert.equal(turnTokenUsageOfMessage(oldNodes, 'm-1'), oldUsage)
|
|
146
|
+
// Given 节点表内容变化以新容器表达(官方 store 快照语义),新增回合节点
|
|
147
|
+
const newUsage = { uncachedInputTokens: 7, outputTokens: 7, totalTokens: 14 }
|
|
148
|
+
const newNodes = [...oldNodes, tailNode('m-2', newUsage)]
|
|
149
|
+
// Then 新容器首查即可见新节点,旧容器结果不受影响
|
|
150
|
+
assert.equal(turnTokenUsageOfMessage(newNodes, 'm-2'), newUsage)
|
|
151
|
+
assert.equal(turnTokenUsageOfMessage(oldNodes, 'm-1'), oldUsage)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
test('计价模型键:双全拼两段,仅 model 用裸名,双缺回退全通配键', () => {
|
|
155
|
+
assert.equal(turnModelOf({ routes: [{ provider: 'p', model: 'm' }] }), 'p/m')
|
|
156
|
+
assert.equal(turnModelOf({ routes: [{ model: 'm' }] }), 'm')
|
|
157
|
+
assert.equal(turnModelOf({ routes: [{ provider: 'p' }] }), MODEL_UNROUTED)
|
|
55
158
|
assert.equal(turnModelOf({ routes: [] }), MODEL_UNROUTED)
|
|
56
159
|
assert.equal(turnModelOf({}), MODEL_UNROUTED)
|
|
57
160
|
assert.equal(turnModelOf(null), MODEL_UNROUTED)
|
|
@@ -67,46 +170,63 @@ test('routes 缺失模型键命中全通配规则', () => {
|
|
|
67
170
|
assert.deepEqual(matchPrice(rules, turnModelOf(tokenUsage), NOW), { input: 1, output: 0, cacheRead: 0, cacheWrite: 0 })
|
|
68
171
|
})
|
|
69
172
|
|
|
70
|
-
test('
|
|
173
|
+
test('芯片文本四桶全带:prompt 三桶入费用分母按价计', () => {
|
|
71
174
|
const tokenUsage = {
|
|
72
175
|
uncachedInputTokens: 8000, outputTokens: 1000, totalTokens: 10000,
|
|
73
|
-
cacheReadTokens: 1000, cacheWriteTokens: 0, routes: [{ provider: 'p', model: '
|
|
176
|
+
cacheReadTokens: 1000, cacheWriteTokens: 0, routes: [{ provider: 'p', model: 'm' }],
|
|
74
177
|
}
|
|
75
178
|
const price = { input: 1, output: 2, cacheRead: 0, cacheWrite: 0 }
|
|
76
|
-
assert.equal(
|
|
179
|
+
assert.equal(buildTurnCostChipText(zhT, tokenUsage, price, '¥'), '¥0.01')
|
|
77
180
|
})
|
|
78
181
|
|
|
79
|
-
test('
|
|
182
|
+
test('芯片文本可选桶缺失按 0 计入费用', () => {
|
|
80
183
|
const tokenUsage = { uncachedInputTokens: 8000, outputTokens: 1000, totalTokens: 9000 }
|
|
81
184
|
const price = { input: 1, output: 2, cacheRead: 5, cacheWrite: 5 }
|
|
82
|
-
assert.equal(
|
|
185
|
+
assert.equal(buildTurnCostChipText(zhT, tokenUsage, price, '¥'), '¥0.01')
|
|
83
186
|
})
|
|
84
187
|
|
|
85
|
-
test('
|
|
188
|
+
test('计费额 0 元为 null,正额保留', () => {
|
|
86
189
|
const tokenUsage = { uncachedInputTokens: 8000, outputTokens: 1000, totalTokens: 9000 }
|
|
87
|
-
assert.equal(
|
|
88
|
-
assert.equal(
|
|
190
|
+
assert.equal(turnCostAmountOf(tokenUsage, { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }), null)
|
|
191
|
+
assert.equal(turnCostAmountOf(tokenUsage, null), null)
|
|
192
|
+
assert.equal(turnCostAmountOf(tokenUsage, { input: 1, output: 0, cacheRead: 0, cacheWrite: 0 }), 0.008)
|
|
89
193
|
})
|
|
90
194
|
|
|
91
|
-
test('
|
|
92
|
-
const
|
|
93
|
-
assert.equal(
|
|
195
|
+
test('芯片文本取不到价为占位符', () => {
|
|
196
|
+
const tokenUsage = { uncachedInputTokens: 8000, outputTokens: 1000, totalTokens: 9000 }
|
|
197
|
+
assert.equal(buildTurnCostChipText(zhT, tokenUsage, null, ''), '—')
|
|
198
|
+
assert.equal(buildTurnCostChipText(enT, tokenUsage, null, ''), '—')
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
test('title 含 token 摘要与估算口径,可选桶未上报追加标注', () => {
|
|
202
|
+
const full = { uncachedInputTokens: 8000, outputTokens: 1000, totalTokens: 9000, cacheReadTokens: 0, cacheWriteTokens: 0 }
|
|
203
|
+
assert.equal(turnCostTitleText(zhT, full), '输入 8K tok · 输出 1K tok,单轮用量按当前费率估算')
|
|
94
204
|
const missing = { uncachedInputTokens: 1, outputTokens: 1, totalTokens: 2 }
|
|
95
|
-
assert.equal(turnCostTitleText(zhT, missing), '
|
|
96
|
-
assert.equal(turnCostTitleText(enT, missing), 'Per-turn usage estimated at current rates,Not reported by this provider')
|
|
205
|
+
assert.equal(turnCostTitleText(zhT, missing), '输入 1 tok · 输出 1 tok,单轮用量按当前费率估算,该提供商未上报此桶')
|
|
206
|
+
assert.equal(turnCostTitleText(enT, missing), 'Input 1 tok · Output 1 tok,Per-turn usage estimated at current rates,Not reported by this provider')
|
|
97
207
|
})
|
|
98
208
|
|
|
99
|
-
test('注入点B 文案键 zh/en
|
|
100
|
-
assert.equal(MESSAGES_ZH['stats.
|
|
101
|
-
assert.equal(MESSAGES_EN['stats.
|
|
209
|
+
test('注入点B 文案键 zh/en 值锁定,独立行旧键已删', () => {
|
|
210
|
+
assert.equal(MESSAGES_ZH['stats.cost'], '费用 ≈ {cost}')
|
|
211
|
+
assert.equal(MESSAGES_EN['stats.cost'], 'Cost ≈ {cost}')
|
|
212
|
+
assert.equal(MESSAGES_ZH['turnCostChip'], '{cost}')
|
|
213
|
+
assert.equal(MESSAGES_EN['turnCostChip'], '{cost}')
|
|
214
|
+
assert.equal(MESSAGES_ZH['stats.turnCost'], undefined)
|
|
215
|
+
assert.equal(MESSAGES_EN['stats.turnCost'], undefined)
|
|
102
216
|
assert.equal(MESSAGES_ZH.turnCostTitle, '单轮用量按当前费率估算')
|
|
103
217
|
assert.equal(MESSAGES_EN.turnCostTitle, 'Per-turn usage estimated at current rates')
|
|
104
218
|
assert.equal(MESSAGES_ZH.turnTokensUnreported, '该提供商未上报此桶')
|
|
105
219
|
assert.equal(MESSAGES_EN.turnTokensUnreported, 'Not reported by this provider')
|
|
220
|
+
assert.equal(MESSAGES_ZH.costDisplayDesc, '在信息栏、趋势悬浮与回合费用芯片中显示按当前费率估算的费用。')
|
|
221
|
+
assert.equal(MESSAGES_EN.costDisplayDesc, 'Show costs estimated at current rates in the stats line, trend tooltips and the turn cost chip.')
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
test('注册指向官方动作行槽,turnTail 旧路径移除', () => {
|
|
225
|
+
assert.ok(CLIENT_SOURCE.includes("ctx.slots.inject('conversation.chat.assistant-actions'"))
|
|
226
|
+
assert.ok(!CLIENT_SOURCE.includes('conversation.chat.turnTail'))
|
|
106
227
|
})
|
|
107
228
|
|
|
108
|
-
test('
|
|
229
|
+
test('芯片样式:显隐随官方动作行父级,不自绘重复规则', () => {
|
|
109
230
|
assert.ok(CLIENT_SOURCE.includes('.ud-turn-cost{'))
|
|
110
|
-
assert.ok(CLIENT_SOURCE.includes('
|
|
111
|
-
assert.ok(CLIENT_SOURCE.includes('[data-actions-reveal=hover]:hover .ud-turn-cost,[data-actions-reveal=hover]:focus-within .ud-turn-cost{opacity:1}'))
|
|
231
|
+
assert.ok(!CLIENT_SOURCE.includes('[data-actions-reveal=hover] .ud-turn-cost'))
|
|
112
232
|
})
|