Procházet zdrojové kódy

refactor(session): localize token delta detection

imccyu před 1 měsícem
rodič
revize
e47c897f5f

+ 1 - 21
packages/llm/llm/src/message.ts

@@ -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
-  }
-}

+ 18 - 1
packages/session/session-stats/src/projection.ts

@@ -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. */

+ 29 - 0
packages/session/session-stats/tests/projection.spec.ts

@@ -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 }),