feat(stats): session cache-hit on the token card, plain rate figure on the timing card
The token card's title row gains the session's cache-hit share in its right corner — the harness chat line's own reads-over-billed formula, reusing the stats board's label and tooltip — hidden until something was billed. The timing card's throughput chip drops its pill chrome and bold weight, now reading 输出速度 N tok/s (zh) / TPS N tok/s (en) with the TPS expansion on the tooltip; both corners share a new plain-text .lc-card-rate style and keep their native-title tooltips.
This commit is contained in:
1 parent
33f61453d7
commit
ef72a7bbb2
6 files changed
+67
-25
No files matched your search
@@ -70,10 +70,10 @@ export function makeStatsTiming(kit: ViewKit, Donut: (props: DonutProps) => Reac
|
||||
const [hoverKey, setHoverKey] = useState<string | null>(null)
|
||||
const timing = props.timing
|
||||
const wall = timing !== null && Number.isFinite(timing.wallMs) && timing.wallMs > 0 ? timing.wallMs : 0
|
||||
// The throughput chip: the host paired provider output tokens with decode
|
||||
// windows exactly as the harness session-stats fold does, so the two
|
||||
// figures can never disagree. An unreadable or absent pairing (an older
|
||||
// cached row) renders no chip.
|
||||
// The throughput figure: the host paired provider output tokens with
|
||||
// decode windows exactly as the harness session-stats fold does, so the
|
||||
// two figures can never disagree. An unreadable or absent pairing (an
|
||||
// older cached row) renders no figure.
|
||||
const tps = timing !== null
|
||||
&& typeof timing.speedMs === 'number' && Number.isFinite(timing.speedMs) && timing.speedMs > 0
|
||||
&& typeof timing.speedTokens === 'number' && Number.isFinite(timing.speedTokens) && timing.speedTokens >= 0
|
||||
@@ -187,7 +187,7 @@ export function makeStatsTiming(kit: ViewKit, Donut: (props: DonutProps) => Reac
|
||||
<div className="lc-card-title">
|
||||
<span className="lc-card-title-text">{t('timing.title')}</span>
|
||||
{tps !== null && (
|
||||
<span className="lc-timing-tps" title={t('timing.tpsTip')}>{t('timing.tps', { tps })}</span>
|
||||
<span className="lc-card-rate" title={t('timing.tpsTip')}>{t('timing.tps', { tps })}</span>
|
||||
)}
|
||||
</div>
|
||||
{rows.length === 0
|
||||
|
||||
@@ -10,12 +10,15 @@
|
||||
* construction. The estimated category counts carry the ≈ marker (the
|
||||
* composition card's convention); the center total and the output count are
|
||||
* exact. Zero parts stay hidden once any usage is reported; the empty state
|
||||
* (no provider report yet) keeps all seven rows behind a dash center.
|
||||
* (no provider report yet) keeps all seven rows behind a dash center. The
|
||||
* title row's right corner carries the session's cache-hit share — the harness
|
||||
* chat line's own reads-over-billed formula — hidden until something was billed.
|
||||
*/
|
||||
|
||||
import { useState, type ReactElement } from 'react'
|
||||
import type { ContextBreakdown, Snapshot, TokenUsage } from '../../shared/types'
|
||||
import { billedParts } from '../categories'
|
||||
import { cacheHitPercent } from '../format'
|
||||
import { numOf } from '../services'
|
||||
import type { ViewKit } from '../viewkit'
|
||||
|
||||
@@ -44,6 +47,11 @@ export function makeStatsTokens(kit: ViewKit, Donut: (props: DonutProps) => Reac
|
||||
: 0
|
||||
const output = props.usage !== null ? numOf(props.usage.outputTokens) : 0
|
||||
const total = input + output
|
||||
// The title row's corner figure: cache reads over billed prompt-side input,
|
||||
// the harness chat line's own formula (statsContext's cell shares it).
|
||||
const hit = props.usage === null
|
||||
? null
|
||||
: cacheHitPercent(numOf(props.usage.cacheReadTokens), input)
|
||||
// Null usage (no provider report — the chat line shows no pill either)
|
||||
// takes the same zeroed split billedParts produces for a zero report.
|
||||
const parts = billedParts(props.current, props.breakdown, props.usage ?? NO_USAGE)
|
||||
@@ -61,6 +69,9 @@ export function makeStatsTokens(kit: ViewKit, Donut: (props: DonutProps) => Reac
|
||||
<div className="lc-card lc-col-stats lc-col-donut flex-1 min-w-[min(360px,100%)]">
|
||||
<div className="lc-card-title">
|
||||
<span className="lc-card-title-text">{t('tokens.title')}</span>
|
||||
{hit !== null && (
|
||||
<span className="lc-card-rate" title={t('stats.cacheHitTip')}>{t('stats.cacheHit')} {hit}%</span>
|
||||
)}
|
||||
</div>
|
||||
{/* donut + legend row: the gap folds at a 320px card, below 240px the row wraps
|
||||
and the ring centers over the full-width legend (all keyed to the lc-card container). */}
|
||||
|
||||
+2
-2
@@ -55,7 +55,7 @@ export const DICT_ZH: Record<string, string> = {
|
||||
'timing.toolTimes': '{n}次',
|
||||
'timing.blockTimes': '{n}次',
|
||||
'timing.empty': '暂无耗时数据 · 对话开始后这里会显示时长分布',
|
||||
'timing.tps': '{tps} tok/s',
|
||||
'timing.tps': '输出速度 {tps} tok/s',
|
||||
'timing.tpsTip': '输出速度(TPS):累计解码输出 tokens ÷ 累计解码时长,与 DeepSeek Harness 会话统计同口径。',
|
||||
'timing.strip': '耗时时间轴:各段按发生次序紧凑铺排,宽度为耗时占比,刻度为零、四分位与全程',
|
||||
'tokens.title': 'Token 统计',
|
||||
@@ -458,7 +458,7 @@ export const DICT_EN: Record<string, string> = {
|
||||
'timing.toolTimes': '{n} runs',
|
||||
'timing.blockTimes': '{n} blocks',
|
||||
'timing.empty': 'No timing data yet — durations appear as the conversation runs',
|
||||
'timing.tps': '{tps} tok/s',
|
||||
'timing.tps': 'TPS {tps} tok/s',
|
||||
'timing.tpsTip': 'Output speed (TPS): summed decoded output tokens over summed decode time — the same pairing as the harness session stats.',
|
||||
'timing.strip': 'Session-time strip: slices packed in occurrence order, width as share of the active time; ticks at zero, the quartiles, and the full span',
|
||||
'tokens.title': 'Token Stats',
|
||||
|
||||
@@ -24,11 +24,13 @@ a.lc-stat:hover { border-color: var(--dsw-alias-label-secondary); }
|
||||
.lc-stat-tip { top: calc(100% + 6px); left: 0; max-width: 280px; line-height: 1.5; font-weight: 400; }
|
||||
.lc-stat-tip-prices { display: block; margin-top: 5px; border-top: 1px dashed var(--dsw-alias-border-l1); padding-top: 5px; }
|
||||
.lc-stat-tip-head { display: block; font-weight: 600; }
|
||||
/* The timing card's throughput chip (statsTiming.tsx): a quiet figure pinned
|
||||
to the title row's right end — the same harness session-stats pairing, so
|
||||
the two figures never disagree. `margin-left: auto` parks it at the row's
|
||||
end; the title row's own wrap carries it below the title under pressure. */
|
||||
.lc-timing-tps { flex: none; margin-left: auto; padding: 1px 8px; border-radius: 999px; background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l2); font-size: 12px; line-height: 18px; font-variant-numeric: tabular-nums; cursor: default; }
|
||||
/* The donut cards' right-corner rate figure (statsTiming.tsx's decode TPS,
|
||||
statsTokens.tsx's session cache hit): plain text pinned to the title row's
|
||||
right end, regular weight against the 600 title — no pill chrome; the
|
||||
native `title` tooltip explains it on hover. `margin-left: auto` parks it
|
||||
at the row's end; the title row's own wrap carries it below the title
|
||||
under pressure. */
|
||||
.lc-card-rate { flex: none; margin-left: auto; font-weight: 400; font-size: 12px; line-height: 18px; font-variant-numeric: tabular-nums; cursor: default; }
|
||||
.lc-stat-tip-row { display: block; margin-top: 2px; }
|
||||
/* One billed model's block: the four bucket rates as label/price rows, the
|
||||
listing line beneath naming the registry face the rates resolved from. */
|
||||
|
||||
@@ -215,32 +215,32 @@ describe('StatsTiming — the generation split', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('StatsTiming — the throughput chip', () => {
|
||||
describe('StatsTiming — the throughput figure', () => {
|
||||
const paired: TimingTotals = { ...TIMING, speedTokens: 12_345, speedMs: 42_720 }
|
||||
|
||||
test('the paired seat renders the harness-formatted figure in the title row', async () => {
|
||||
test('the paired seat renders the output-speed figure in the title row', async () => {
|
||||
const m = await mount(h(StatsTiming, { timing: paired }))
|
||||
const chip = query(m.container, '.lc-timing-tps')
|
||||
assert.equal(chip?.textContent, '289 tok/s', '12345 tokens over 42.72s')
|
||||
assert.ok(chip?.getAttribute('title')?.includes('Output speed (TPS)'))
|
||||
const rate = query(m.container, '.lc-card-rate')
|
||||
assert.equal(rate?.textContent, 'TPS 289 tok/s', '12345 tokens over 42.72s')
|
||||
assert.ok(rate?.getAttribute('title')?.includes('Output speed (TPS)'))
|
||||
await m.unmount()
|
||||
})
|
||||
|
||||
test('the chip localizes its tooltip and stays in the title row', async () => {
|
||||
test('the figure localizes its label and tooltip in the title row', async () => {
|
||||
const m = await mount(h(StatsTimingZh, { timing: paired }))
|
||||
const chip = query(m.container, '.lc-timing-tps')
|
||||
assert.equal(chip?.textContent, '289 tok/s')
|
||||
assert.ok(chip?.getAttribute('title')?.includes('输出速度(TPS)'))
|
||||
const rate = query(m.container, '.lc-card-rate')
|
||||
assert.equal(rate?.textContent, '输出速度 289 tok/s')
|
||||
assert.ok(rate?.getAttribute('title')?.includes('输出速度(TPS)'))
|
||||
await m.unmount()
|
||||
})
|
||||
|
||||
test('a sub-10 figure keeps one decimal', async () => {
|
||||
const m = await mount(h(StatsTiming, { timing: { ...TIMING, speedTokens: 26, speedMs: 10_000 } }))
|
||||
assert.equal(query(m.container, '.lc-timing-tps')?.textContent, '2.6 tok/s')
|
||||
assert.equal(query(m.container, '.lc-card-rate')?.textContent, 'TPS 2.6 tok/s')
|
||||
await m.unmount()
|
||||
})
|
||||
|
||||
test('an unpaired, zero-window, or hostile seat renders no chip', async () => {
|
||||
test('an unpaired, zero-window, or hostile seat renders no figure', async () => {
|
||||
for (const timing of [
|
||||
TIMING,
|
||||
{ ...TIMING, speedTokens: 500, speedMs: 0 },
|
||||
@@ -248,7 +248,7 @@ describe('StatsTiming — the throughput chip', () => {
|
||||
{ ...TIMING, speedTokens: -5, speedMs: 1_000 },
|
||||
] as TimingTotals[]) {
|
||||
const m = await mount(h(StatsTiming, { timing }))
|
||||
assert.equal(queryAll(m.container, '.lc-timing-tps').length, 0, JSON.stringify(timing.speedTokens))
|
||||
assert.equal(queryAll(m.container, '.lc-card-rate').length, 0, JSON.stringify(timing.speedTokens))
|
||||
await m.unmount()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -112,3 +112,32 @@ describe('StatsTokens', () => {
|
||||
await m.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
describe('StatsTokens — the cache-hit corner figure', () => {
|
||||
test('a billed session shows the chat line\'s cache-hit share in the title row', async () => {
|
||||
const m = await mount(h(StatsTokens, { usage: USAGE, current: CURRENT, breakdown: null }))
|
||||
const rate = query(m.container, '.lc-card-rate')
|
||||
assert.equal(rate?.textContent, 'Cache Hit 25.00%', '100 reads over the 400 billed')
|
||||
assert.ok(rate?.getAttribute('title')?.includes('Cumulative cache-read share'))
|
||||
await m.unmount()
|
||||
})
|
||||
|
||||
test('the figure localizes its label and tooltip', async () => {
|
||||
const kitZh = makeKit('zh')
|
||||
const StatsTokensZh = makeStatsTokens(kitZh, makeDonut(kitZh))
|
||||
const m = await mount(h(StatsTokensZh, { usage: USAGE, current: CURRENT, breakdown: null }))
|
||||
const rate = query(m.container, '.lc-card-rate')
|
||||
assert.equal(rate?.textContent, '缓存命中 25.00%')
|
||||
assert.ok(rate?.getAttribute('title')?.includes('整个会话累计的缓存读取'))
|
||||
await m.unmount()
|
||||
})
|
||||
|
||||
test('an unbilled or unreported session renders no figure', async () => {
|
||||
const zero: TokenUsage = { uncachedInputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 }
|
||||
for (const usage of [null, zero] as (TokenUsage | null)[]) {
|
||||
const m = await mount(h(StatsTokens, { usage, current: CURRENT, breakdown: null }))
|
||||
assert.equal(queryAll(m.container, '.lc-card-rate').length, 0, JSON.stringify(usage))
|
||||
await m.unmount()
|
||||
}
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user