request-inspection.spec.ts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. import { describe, expect, it } from 'vitest'
  2. import type { HistoryEntry } from '@deepseek-ai/dsh-client-connection/client'
  3. import { createAssistantMessage, createUserMessage } from '@deepseek-ai/dsh-llm'
  4. import type { SessionEvent } from '@deepseek-ai/dsh-session/types'
  5. import { inspectRequests } from '../src/client/sessions/request-inspection.ts'
  6. const at = (seq: number, type: string, data: unknown): SessionEvent =>
  7. ({ seq, time: 1_700_000_000_000 + seq, type, data }) as SessionEvent
  8. const entriesOf = (events: readonly SessionEvent[]): HistoryEntry[] =>
  9. events.map(event => ({ event }))
  10. describe('inspectRequests', () => {
  11. it('projects ordinary and compaction calls into one chronological request stream', () => {
  12. const events = [
  13. at(0, 'step/start', { turn: 1, step: 1 }),
  14. at(1, 'request/header', {
  15. reason: 'initial',
  16. header: {
  17. config: { provider: 'fake', model: 'model' },
  18. system: 'system',
  19. tools: [{
  20. name: 'read',
  21. description: 'Read a file.',
  22. parameters: { type: 'object' },
  23. }],
  24. },
  25. }),
  26. at(2, 'tool/call', {
  27. turn: 1,
  28. step: 1,
  29. callId: 'call-1',
  30. name: 'read',
  31. arguments: '{}',
  32. }),
  33. at(3, 'assistant/message', {
  34. turn: 1,
  35. step: 1,
  36. message: createAssistantMessage({
  37. content: [{ type: 'text', text: 'done' }],
  38. source: { provider: 'fake', model: 'model' },
  39. }),
  40. usage: { inputTokens: 5, outputTokens: 2 },
  41. }),
  42. at(4, 'step/end', { turn: 1, step: 1 }),
  43. at(5, 'compact/start', { turn: 1 }),
  44. at(6, 'compact/summary', {
  45. summary: [{ type: 'text', text: 'summary' }],
  46. rawOutput: [
  47. { type: 'reasoning', text: 'thought' },
  48. { type: 'text', text: 'summary' },
  49. ],
  50. provider: 'fake',
  51. model: 'compact-model',
  52. usage: { inputTokens: 8, outputTokens: 3 },
  53. }),
  54. at(7, 'user/message', createUserMessage({
  55. content: [{ type: 'text', text: 'checkpoint' }],
  56. source: { kind: 'plugin', plugin: 'compact' },
  57. })),
  58. at(8, 'compact/end', { turn: 1 }),
  59. ]
  60. const snapshot = inspectRequests(entriesOf(events))
  61. expect(snapshot.requests).toMatchObject([
  62. {
  63. purpose: 'assistant',
  64. startSeq: 0,
  65. resultSeq: 3,
  66. status: 'complete',
  67. prompt: {
  68. config: { provider: 'fake', model: 'model' },
  69. system: 'system',
  70. },
  71. promptChange: { seq: 1, kind: 'initial' },
  72. },
  73. {
  74. purpose: 'compaction',
  75. startSeq: 5,
  76. resultSeq: 6,
  77. replacementSeq: 7,
  78. status: 'complete',
  79. summary: [{ type: 'text', text: 'summary' }],
  80. },
  81. ])
  82. expect(snapshot.callSchemas.get('call-1')?.name).toBe('read')
  83. })
  84. it('captures schemas for nested tool dispatches from the active request header', () => {
  85. const snapshot = inspectRequests(entriesOf([
  86. at(0, 'request/header', {
  87. reason: 'initial',
  88. header: {
  89. config: { provider: 'fake', model: 'model' },
  90. tools: [{
  91. name: 'read',
  92. description: 'Read a file.',
  93. parameters: { type: 'object' },
  94. }],
  95. },
  96. }),
  97. at(1, 'tool/code-dispatch-start', {
  98. parentCallId: 'parent',
  99. subCallId: 'nested',
  100. name: 'read',
  101. arguments: {},
  102. }),
  103. ]))
  104. expect(snapshot.callSchemas.get('nested')?.name).toBe('read')
  105. })
  106. it('keeps chunk-reported usage through request failure and prefers it to message fallback', () => {
  107. const chunkUsage = { inputTokens: 21, outputTokens: 3 }
  108. const retryUsage = {
  109. inputTokens: 5,
  110. outputTokens: 2,
  111. cacheReadTokens: 8,
  112. reasoningTokens: 1,
  113. }
  114. const snapshot = inspectRequests(entriesOf([
  115. at(0, 'step/start', { turn: 1, step: 1 }),
  116. at(1, 'assistant/chunk', {
  117. turn: 1,
  118. step: 1,
  119. chunk: { type: 'usage', usage: chunkUsage },
  120. }),
  121. at(2, 'llm/retry', {
  122. turn: 1,
  123. step: 1,
  124. retry: 1,
  125. maxRetries: 2,
  126. delayMs: 100,
  127. failure: { message: 'rate limited' },
  128. }),
  129. at(3, 'assistant/chunk', {
  130. turn: 1,
  131. step: 1,
  132. chunk: { type: 'usage', usage: retryUsage },
  133. }),
  134. at(4, 'assistant/message', {
  135. turn: 1,
  136. step: 1,
  137. message: createAssistantMessage({
  138. content: [{ type: 'text', text: 'recovered' }],
  139. source: { provider: 'fake', model: 'model' },
  140. }),
  141. usage: { inputTokens: 1, outputTokens: 1 },
  142. }),
  143. ]))
  144. expect(snapshot.requests[0]).toMatchObject({
  145. status: 'complete',
  146. usage: {
  147. inputTokens: 26,
  148. outputTokens: 5,
  149. cacheReadTokens: 8,
  150. reasoningTokens: 1,
  151. },
  152. })
  153. })
  154. it('treats a scrubbed durable-fixture tool catalog as unavailable', () => {
  155. const snapshot = inspectRequests(entriesOf([
  156. at(0, 'step/start', { turn: 1, step: 1 }),
  157. at(1, 'request/header', {
  158. reason: 'initial',
  159. header: {
  160. config: { provider: 'fake', model: 'model' },
  161. tools: '{{tools}}',
  162. },
  163. }),
  164. at(2, 'tool/call', {
  165. turn: 1,
  166. step: 1,
  167. callId: 'call-1',
  168. name: 'read',
  169. arguments: '{}',
  170. }),
  171. ]))
  172. expect(snapshot.callSchemas).toEqual(new Map())
  173. expect(snapshot.requests[0]?.prompt?.tools).toEqual([])
  174. })
  175. })