| import { describe, it, expect } from 'vitest'; |
| import { parseStreamingResponse } from '../streaming-parser'; |
|
|
| function makeSSEStream(chunks: string[]): Response { |
| const encoder = new TextEncoder(); |
| const stream = new ReadableStream({ |
| start(controller) { |
| for (const chunk of chunks) controller.enqueue(encoder.encode(chunk)); |
| controller.close(); |
| }, |
| }); |
| return new Response(stream); |
| } |
|
|
| const sse = (data: object) => `data: ${JSON.stringify(data)}\n\n`; |
| const BASH_ONLY = new Set(['bash']); |
|
|
| |
| const HUGE_ARGS = JSON.stringify({ command: 'cat ' + 'x'.repeat(5000) }); |
|
|
| describe('streaming-parser early tool-name abort', () => { |
| it('aborts early on a non-bash tool name (OpenAI format) before reading arguments', async () => { |
| const chunks = [ |
| sse({ choices: [{ delta: { tool_calls: [{ index: 0, id: 'tc1', type: 'function', function: { name: 'cat', arguments: '' } }] }, index: 0 }] }), |
| |
| sse({ choices: [{ delta: { tool_calls: [{ index: 0, function: { arguments: HUGE_ARGS } }] }, index: 0 }] }), |
| sse({ choices: [{ delta: {}, index: 0, finish_reason: 'tool_calls' }] }), |
| 'data: [DONE]\n\n', |
| ]; |
| const result = await parseStreamingResponse(makeSSEStream(chunks), { |
| provider: 'openrouter', model: 'test', allowedToolNames: BASH_ONLY, |
| }); |
| expect(result.invalidToolName).toBe('cat'); |
| |
| const cat = (result.toolCalls ?? []).find(t => t.function?.name === 'cat'); |
| expect(cat?.function?.arguments ?? '').not.toContain('xxxx'); |
| }); |
|
|
| it('does not abort a valid bash tool call', async () => { |
| const chunks = [ |
| sse({ choices: [{ delta: { tool_calls: [{ index: 0, id: 'tc1', type: 'function', function: { name: 'bash', arguments: '' } }] }, index: 0 }] }), |
| sse({ choices: [{ delta: { tool_calls: [{ index: 0, function: { arguments: JSON.stringify({ command: 'ls /' }) } }] }, index: 0 }] }), |
| sse({ choices: [{ delta: {}, index: 0, finish_reason: 'tool_calls' }] }), |
| 'data: [DONE]\n\n', |
| ]; |
| const result = await parseStreamingResponse(makeSSEStream(chunks), { |
| provider: 'openrouter', model: 'test', allowedToolNames: BASH_ONLY, |
| }); |
| expect(result.invalidToolName).toBeUndefined(); |
| expect(result.toolCalls?.[0]?.function?.name).toBe('bash'); |
| expect(result.toolCalls?.[0]?.function?.arguments).toContain('ls /'); |
| }); |
|
|
| it('does not abort when no allowedToolNames is provided (backward compatible)', async () => { |
| const chunks = [ |
| sse({ choices: [{ delta: { tool_calls: [{ index: 0, id: 'tc1', type: 'function', function: { name: 'cat', arguments: JSON.stringify({ path: '/x' }) } }] }, index: 0 }] }), |
| sse({ choices: [{ delta: {}, index: 0, finish_reason: 'tool_calls' }] }), |
| 'data: [DONE]\n\n', |
| ]; |
| const result = await parseStreamingResponse(makeSSEStream(chunks), { |
| provider: 'openrouter', model: 'test', |
| }); |
| expect(result.invalidToolName).toBeUndefined(); |
| expect(result.toolCalls?.[0]?.function?.name).toBe('cat'); |
| }); |
|
|
| it('aborts early on a non-bash tool name (Anthropic format)', async () => { |
| const chunks = [ |
| sse({ type: 'content_block_start', index: 0, content_block: { type: 'tool_use', id: 'tc1', name: 'cat' } }), |
| sse({ type: 'content_block_delta', index: 0, delta: { type: 'input_json_delta', partial_json: HUGE_ARGS } }), |
| sse({ type: 'content_block_stop', index: 0 }), |
| ]; |
| const result = await parseStreamingResponse(makeSSEStream(chunks), { |
| provider: 'anthropic', model: 'test', allowedToolNames: BASH_ONLY, |
| }); |
| expect(result.invalidToolName).toBe('cat'); |
| }); |
| }); |
|
|