mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
Assistant footers and the stats line gain TTFT/tok-per-second readings folded from step timings; context occupancy moves off the stats line onto a composer ring whose panel shows a heuristic system/tools/messages breakdown from the new token-meter contextBreakdown session projection.
155 lines
6.3 KiB
TypeScript
155 lines
6.3 KiB
TypeScript
// Per-turn latency/throughput fold and the footer figure formatters.
|
|
|
|
import { describe, expect, it } from 'vitest'
|
|
import type { AssistantMessageNode, ConversationNode, UserMessageNode } from '@deepseek-ai/dsh-client-runtime/client'
|
|
import { assistantStepReading, deriveTurnMetrics } from '../src/client/chat/turn-metrics.ts'
|
|
import { formatLatencySeconds, formatTokensPerSecond } from '../src/client/chat/message-chrome.ts'
|
|
|
|
interface StepSpec {
|
|
seq: number
|
|
turn: number
|
|
step: number
|
|
timing?: AssistantMessageNode['timing']
|
|
usage?: unknown
|
|
}
|
|
|
|
const assistant = ({ seq, turn, step, timing, usage }: StepSpec): AssistantMessageNode => ({
|
|
kind: 'assistant', seq, time: seq * 1_000, turn, step, blocks: [{ kind: 'text', text: `t${seq}` }],
|
|
...(timing === undefined ? {} : { timing }),
|
|
...(usage === undefined ? {} : { usage }),
|
|
})
|
|
|
|
const user = (seq: number): UserMessageNode => ({
|
|
kind: 'user', seq, time: seq * 1_000, content: [{ type: 'text', text: 'hi' }] as never, source: null,
|
|
})
|
|
|
|
describe('assistantStepReading', () => {
|
|
it('derives ttft, decode time, and output tokens from a fully recorded step', () => {
|
|
const reading = assistantStepReading(assistant({
|
|
seq: 2, turn: 1, step: 1,
|
|
timing: { stepStartTime: 1_000, firstTokenTime: 1_800, completedTime: 6_800 },
|
|
usage: { outputTokens: 200 },
|
|
}))
|
|
expect(reading).toEqual({ ttftMs: 800, decodeMs: 5_000, outputTokens: 200 })
|
|
})
|
|
|
|
it('returns nulls when timing is absent', () => {
|
|
const reading = assistantStepReading(assistant({ seq: 2, turn: 1, step: 1, usage: { outputTokens: 5 } }))
|
|
expect(reading).toEqual({ ttftMs: null, decodeMs: null, outputTokens: 5 })
|
|
})
|
|
|
|
it('needs both boundaries for ttft and clamps negative spans to zero', () => {
|
|
expect(assistantStepReading(assistant({
|
|
seq: 2, turn: 1, step: 1,
|
|
timing: { stepStartTime: null, firstTokenTime: 1_800, completedTime: 6_800 },
|
|
}))).toEqual({ ttftMs: null, decodeMs: 5_000, outputTokens: null })
|
|
expect(assistantStepReading(assistant({
|
|
seq: 2, turn: 1, step: 1,
|
|
timing: { stepStartTime: 1_000, firstTokenTime: null, completedTime: 6_800 },
|
|
}))).toEqual({ ttftMs: null, decodeMs: null, outputTokens: null })
|
|
expect(assistantStepReading(assistant({
|
|
seq: 2, turn: 1, step: 1,
|
|
timing: { stepStartTime: 2_000, firstTokenTime: 1_500, completedTime: 1_200 },
|
|
}))).toEqual({ ttftMs: 0, decodeMs: 0, outputTokens: null })
|
|
})
|
|
|
|
it('rejects non-object, missing, and non-finite usage token counts', () => {
|
|
const timing = { stepStartTime: 1_000, firstTokenTime: 1_500, completedTime: 2_000 }
|
|
expect(assistantStepReading(assistant({ seq: 2, turn: 1, step: 1, timing, usage: 'weird' })).outputTokens).toBeNull()
|
|
expect(assistantStepReading(assistant({ seq: 2, turn: 1, step: 1, timing, usage: {} })).outputTokens).toBeNull()
|
|
const nan = assistant({ seq: 2, turn: 1, step: 1, timing, usage: { outputTokens: Number.NaN } })
|
|
expect(assistantStepReading(nan).outputTokens).toBeNull()
|
|
expect(assistantStepReading(assistant({ seq: 2, turn: 1, step: 1, timing, usage: { outputTokens: -3 } })).outputTokens).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('deriveTurnMetrics', () => {
|
|
it('takes ttft from the lowest step and throughput over all sampled steps', () => {
|
|
const nodes: ConversationNode[] = [
|
|
user(1),
|
|
// Out of step order on purpose: the lowest step owns the ttft slot.
|
|
assistant({
|
|
seq: 4, turn: 1, step: 2,
|
|
timing: { stepStartTime: 10_000, firstTokenTime: 10_200, completedTime: 12_200 },
|
|
usage: { outputTokens: 60 },
|
|
}),
|
|
assistant({
|
|
seq: 2, turn: 1, step: 1,
|
|
timing: { stepStartTime: 1_000, firstTokenTime: 2_200, completedTime: 5_200 },
|
|
usage: { outputTokens: 40 },
|
|
}),
|
|
]
|
|
// 100 tokens over 5s of decode.
|
|
expect(deriveTurnMetrics(nodes).get(1)).toEqual({ ttftMs: 1_200, tokensPerSecond: 20 })
|
|
})
|
|
|
|
it('emits ttft without throughput when no step carries usage', () => {
|
|
const nodes = [assistant({
|
|
seq: 2, turn: 1, step: 1,
|
|
timing: { stepStartTime: 1_000, firstTokenTime: 1_900, completedTime: 3_000 },
|
|
})]
|
|
expect(deriveTurnMetrics(nodes).get(1)).toEqual({ ttftMs: 900 })
|
|
})
|
|
|
|
it('emits throughput without ttft when only a later step is recorded', () => {
|
|
const nodes = [
|
|
assistant({ seq: 2, turn: 1, step: 1 }),
|
|
assistant({
|
|
seq: 4, turn: 1, step: 2,
|
|
timing: { stepStartTime: 10_000, firstTokenTime: 10_500, completedTime: 12_500 },
|
|
usage: { outputTokens: 30 },
|
|
}),
|
|
]
|
|
expect(deriveTurnMetrics(nodes).get(1)).toEqual({ tokensPerSecond: 15 })
|
|
})
|
|
|
|
it('omits turns with no readings and zero-decode throughput', () => {
|
|
const nodes = [
|
|
assistant({ seq: 2, turn: 1, step: 1 }),
|
|
assistant({
|
|
seq: 4, turn: 2, step: 1,
|
|
timing: { stepStartTime: null, firstTokenTime: 5_000, completedTime: 5_000 },
|
|
usage: { outputTokens: 10 },
|
|
}),
|
|
]
|
|
expect(deriveTurnMetrics(nodes).size).toBe(0)
|
|
})
|
|
|
|
it('keeps turns independent and ignores non-assistant nodes', () => {
|
|
const nodes: ConversationNode[] = [
|
|
user(1),
|
|
assistant({
|
|
seq: 2, turn: 1, step: 1,
|
|
timing: { stepStartTime: 1_000, firstTokenTime: 1_400, completedTime: 2_400 },
|
|
usage: { outputTokens: 10 },
|
|
}),
|
|
user(3),
|
|
assistant({
|
|
seq: 4, turn: 2, step: 1,
|
|
timing: { stepStartTime: 4_000, firstTokenTime: 4_100, completedTime: 6_100 },
|
|
usage: { outputTokens: 100 },
|
|
}),
|
|
]
|
|
const metrics = deriveTurnMetrics(nodes)
|
|
expect(metrics.get(1)).toEqual({ ttftMs: 400, tokensPerSecond: 10 })
|
|
expect(metrics.get(2)).toEqual({ ttftMs: 100, tokensPerSecond: 50 })
|
|
})
|
|
})
|
|
|
|
describe('footer figure formatters', () => {
|
|
it('formats latency with one decimal under ten seconds and whole seconds beyond', () => {
|
|
expect(formatLatencySeconds(840)).toBe('0.8')
|
|
expect(formatLatencySeconds(1_000)).toBe('1')
|
|
expect(formatLatencySeconds(9_949)).toBe('9.9')
|
|
expect(formatLatencySeconds(12_400)).toBe('12')
|
|
expect(formatLatencySeconds(-5)).toBe('0')
|
|
})
|
|
|
|
it('formats throughput with whole tokens from ten up and one decimal below', () => {
|
|
expect(formatTokensPerSecond(34.4)).toBe('34')
|
|
expect(formatTokensPerSecond(9.96)).toBe('10')
|
|
expect(formatTokensPerSecond(3.14)).toBe('3.1')
|
|
expect(formatTokensPerSecond(-1)).toBe('0')
|
|
})
|
|
})
|