refactor(session): localize token delta detection

This commit is contained in:
imccyu
2026-08-23 23:29:38 +08:00
parent a050b3d4f1
commit e47c897f5f
3 changed files with 48 additions and 22 deletions
+1 -21
View File
@@ -3,7 +3,7 @@
import { randomUUID } from '@deepseek-ai/dsh-util-crypto'
import { MessageId, type CallId } from './brand.ts'
import { deepFreeze } from './call-config.ts'
import type { ContentBlock, StreamChunk, ToolResultBlock } from './types.ts'
import type { ContentBlock, ToolResultBlock } from './types.ts'
/** Provider/model identity and adapter-private replay data for an assistant message. */
export interface AssistantProvenance {
@@ -240,23 +240,3 @@ export function createToolResultMessage(input: ToolResultMessageInput): ToolResu
}],
})
}
/**
* Whether a stream chunk carries visible model output (the first-token
* boundary shared by client step timing and the whole-log sessionStats
* projection). Empty deltas (heartbeats, empty tool-call frames) do not count
* as a first token.
* @param chunk - the stream chunk to test.
* @returns true when the chunk contains a non-empty text/reasoning/tool delta.
*/
export function isTokenDelta(chunk: StreamChunk): boolean {
switch (chunk.type) {
case 'text-delta':
case 'reasoning-delta':
return chunk.text !== ''
case 'tool-call-delta':
return chunk.argumentsDelta !== '' || chunk.name !== undefined
default:
return false
}
}
@@ -24,9 +24,26 @@
*/
import { z } from 'zod'
import { isTokenDelta } from '@deepseek-ai/dsh-llm/message'
import type { StreamChunk } from '@deepseek-ai/dsh-llm/types'
import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection'
/* jscpd:ignore-start -- Session Stats owns its whole-log timing projection independently. */
/** Whether a stream chunk carries a non-empty first-token delta. */
function isTokenDelta(chunk: StreamChunk): boolean {
switch (chunk.type) {
case 'text-delta':
case 'reasoning-delta':
return chunk.text !== ''
case 'tool-call-delta':
return chunk.argumentsDelta !== '' || chunk.name !== undefined
default:
return false
}
}
/* jscpd:ignore-end */
/** Accumulated whole-log figures (the view is exactly these totals). */
interface SessionStatsTotals {
/** Distinct turns with at least one closed step so far. */
@@ -204,6 +204,35 @@ describe('sessionStats wall-time fold (controlled timestamps)', () => {
])).toEqual(totals({ turns: 1, steps: 1, llmMs: 1_000, ttftMs: 400, ttftSteps: 1 }))
})
it('uses non-empty Tool-call names or arguments as the first token', () => {
expect(fold([
at(1_000, 'step/start', { turn: 1, step: 1 }),
at(1_100, 'assistant/chunk', {
turn: 1,
step: 1,
chunk: { type: 'tool-call-delta', index: 0, id: 'call-1', argumentsDelta: '' },
}),
at(1_200, 'assistant/chunk', {
turn: 1,
step: 1,
chunk: { type: 'tool-call-delta', index: 0, id: 'call-1', name: 'read', argumentsDelta: '' },
}),
at(2_000, 'assistant/message', { turn: 1, step: 1, message }),
at(2_100, 'step/end', { turn: 1, step: 1 }),
])).toEqual(totals({ turns: 1, steps: 1, llmMs: 1_000, ttftMs: 200, ttftSteps: 1 }))
expect(fold([
at(1_000, 'step/start', { turn: 1, step: 1 }),
at(1_300, 'assistant/chunk', {
turn: 1,
step: 1,
chunk: { type: 'tool-call-delta', index: 0, id: 'call-1', argumentsDelta: '{' },
}),
at(2_000, 'assistant/message', { turn: 1, step: 1, message }),
at(2_100, 'step/end', { turn: 1, step: 1 }),
])).toEqual(totals({ turns: 1, steps: 1, llmMs: 1_000, ttftMs: 300, ttftSteps: 1 }))
})
it('leaves a cancelled step untimed: counted by step/end, no assembled message to accrue from', () => {
expect(fold([
at(1_000, 'step/start', { turn: 1, step: 1 }),