mock-adapter.ts 2.8 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374
  1. import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
  2. import { LlmAdapter } from '@deepseek-ai/dsh-llm'
  3. /** Helpers to write scripted responses tersely. */
  4. export function textResponse(text: string): StreamChunk[] {
  5. return [
  6. { type: 'block-start', index: 0, blockType: 'text' },
  7. ...Array.from(text, (char): StreamChunk => ({ type: 'text-delta', index: 0, text: char })),
  8. { type: 'block-end', index: 0, block: { type: 'text', text } },
  9. { type: 'usage', usage: { inputTokens: 10, outputTokens: text.length } },
  10. { type: 'finish', reason: { kind: 'stop' } },
  11. ]
  12. }
  13. export function toolCallResponse(callId: string, name: string, args: object, text?: string): StreamChunk[] {
  14. const argumentsJson = JSON.stringify(args)
  15. const chunks: StreamChunk[] = []
  16. let index = 0
  17. if (text) {
  18. chunks.push(
  19. { type: 'block-start', index, blockType: 'text' },
  20. { type: 'text-delta', index, text },
  21. { type: 'block-end', index, block: { type: 'text', text } },
  22. )
  23. index += 1
  24. }
  25. chunks.push(
  26. { type: 'block-start', index, blockType: 'tool-call' },
  27. { type: 'tool-call-delta', index, id: callId, name, argumentsDelta: argumentsJson.slice(0, 5) },
  28. { type: 'tool-call-delta', index, id: callId, argumentsDelta: argumentsJson.slice(5) },
  29. {
  30. type: 'block-end',
  31. index,
  32. block: { type: 'tool-call', id: callId, name, arguments: argumentsJson },
  33. },
  34. { type: 'usage', usage: { inputTokens: 10, outputTokens: 5 } },
  35. { type: 'finish', reason: { kind: 'tool-calls' } },
  36. )
  37. return chunks
  38. }
  39. /**
  40. * Mock adapter driven by a script: each model call consumes the next entry.
  41. * Records every request it receives for assertions. An entry may be a
  42. * function to compute chunks from the request, or a 'hang' marker that
  43. * streams one chunk then waits until aborted.
  44. */
  45. export class MockAdapter extends LlmAdapter {
  46. requests: GenerateOptions[] = []
  47. constructor(private script: (StreamChunk[] | ((options: GenerateOptions) => StreamChunk[]) | 'hang')[]) {
  48. super()
  49. }
  50. async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
  51. this.requests.push(options)
  52. const entry = this.script.shift()
  53. if (!entry) throw new Error('MockAdapter: script exhausted')
  54. if (entry === 'hang') {
  55. yield { type: 'block-start', index: 0, blockType: 'text' }
  56. yield { type: 'text-delta', index: 0, text: 'partial' }
  57. await new Promise<void>((_resolve, reject) => {
  58. if (options.signal?.aborted) { reject(new Error('aborted')); return }
  59. options.signal?.addEventListener('abort', () => { reject(new Error('aborted')) }, { once: true })
  60. })
  61. return
  62. }
  63. const chunks = typeof entry === 'function' ? entry(options) : entry
  64. for (const chunk of chunks) {
  65. if (options.signal?.aborted) throw new Error('aborted')
  66. yield chunk
  67. }
  68. }
  69. }