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:
bowenliang123 committed 2026-10-06 21:21:57 +08:00
1 parent 33f61453d7
commit ef72a7bbb2
6 files changed
+67 -25

No files matched your search

+5 -5
View File
@@ -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
+12 -1
View File
@@ -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
View File
@@ -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',
+7 -5
View File
@@ -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. */
+12 -12
View File
@@ -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()
}
})
})