UNPKG

@lobehub/chat

Version:

Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.

900 lines (838 loc) 34.8 kB
import type { Stream } from '@anthropic-ai/sdk/streaming'; import { describe, expect, it, vi } from 'vitest'; import { AnthropicStream } from './anthropic'; describe('AnthropicStream', () => { it('should transform Anthropic stream to protocol stream', async () => { // @ts-ignore const mockAnthropicStream: Stream = { [Symbol.asyncIterator]() { let count = 0; return { next: async () => { switch (count) { case 0: count++; return { done: false, value: { type: 'message_start', message: { id: 'message_1', metadata: {} }, }, }; case 1: count++; return { done: false, value: { type: 'content_block_delta', delta: { type: 'text_delta', text: 'Hello' }, }, }; case 2: count++; return { done: false, value: { type: 'content_block_delta', delta: { type: 'text_delta', text: ' world!' }, }, }; case 3: count++; return { done: false, value: { type: 'message_delta', delta: { stop_reason: 'stop' }, }, }; default: return { done: true, value: undefined }; } }, }; }, }; const onStartMock = vi.fn(); const onTextMock = vi.fn(); const onCompletionMock = vi.fn(); const protocolStream = AnthropicStream(mockAnthropicStream, { callbacks: { onStart: onStartMock, onText: onTextMock, onCompletion: onCompletionMock, }, }); const decoder = new TextDecoder(); const chunks = []; // @ts-ignore for await (const chunk of protocolStream) { chunks.push(decoder.decode(chunk, { stream: true })); } expect(chunks).toEqual([ 'id: message_1\n', 'event: data\n', `data: {"id":"message_1","metadata":{}}\n\n`, 'id: message_1\n', 'event: text\n', `data: "Hello"\n\n`, 'id: message_1\n', 'event: text\n', `data: " world!"\n\n`, 'id: message_1\n', 'event: stop\n', `data: "stop"\n\n`, ]); expect(onStartMock).toHaveBeenCalledTimes(1); expect(onTextMock).toHaveBeenNthCalledWith(1, 'Hello'); expect(onTextMock).toHaveBeenNthCalledWith(2, ' world!'); expect(onCompletionMock).toHaveBeenCalledTimes(1); }); it('should handle tool use event and ReadableStream input', async () => { const streams = [ { type: 'message_start', message: { id: 'msg_017aTuY86wNxth5TE544yqJq', type: 'message', role: 'assistant', model: 'claude-3-sonnet-20240229', content: [], stop_reason: null, stop_sequence: null, usage: { input_tokens: 457, output_tokens: 1 }, }, }, { type: 'content_block_start', index: 0, content_block: { type: 'text', text: '' } }, { type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: '好' } }, { type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: '的:' } }, { type: 'content_block_stop', index: 0 }, { type: 'content_block_start', index: 1, content_block: { type: 'tool_use', id: 'toolu_01WdYWxYFQ8iu5iZq1Dy9Saf', name: 'realtime-weather____fetchCurrentWeather', input: {}, }, }, { type: 'content_block_delta', index: 1, delta: { type: 'input_json_delta', partial_json: '' }, }, { type: 'content_block_delta', index: 1, delta: { type: 'input_json_delta', partial_json: '{"city": "' }, }, { type: 'content_block_delta', index: 1, delta: { type: 'input_json_delta', partial_json: '杭' }, }, { type: 'content_block_delta', index: 1, delta: { type: 'input_json_delta', partial_json: '州"}' }, }, { type: 'content_block_stop', index: 1 }, { type: 'message_delta', delta: { stop_reason: 'tool_use', stop_sequence: null }, usage: { output_tokens: 83 }, }, ]; const mockReadableStream = new ReadableStream({ start(controller) { streams.forEach((chunk) => { controller.enqueue(chunk); }); controller.close(); }, }); const onToolCallMock = vi.fn(); const protocolStream = AnthropicStream(mockReadableStream, { callbacks: { onToolsCalling: onToolCallMock, }, }); const decoder = new TextDecoder(); const chunks = []; // @ts-ignore for await (const chunk of protocolStream) { chunks.push(decoder.decode(chunk, { stream: true })); } expect(chunks).toEqual( [ 'id: msg_017aTuY86wNxth5TE544yqJq', 'event: data', 'data: {"id":"msg_017aTuY86wNxth5TE544yqJq","type":"message","role":"assistant","model":"claude-3-sonnet-20240229","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":457,"output_tokens":1}}\n', 'id: msg_017aTuY86wNxth5TE544yqJq', 'event: data', 'data: ""\n', 'id: msg_017aTuY86wNxth5TE544yqJq', 'event: text', 'data: "好"\n', 'id: msg_017aTuY86wNxth5TE544yqJq', 'event: text', 'data: "的:"\n', 'id: msg_017aTuY86wNxth5TE544yqJq', 'event: data', 'data: {"type":"content_block_stop","index":0}\n', // Tool calls 'id: msg_017aTuY86wNxth5TE544yqJq', 'event: tool_calls', `data: [{"function":{"arguments":"","name":"realtime-weather____fetchCurrentWeather"},"id":"toolu_01WdYWxYFQ8iu5iZq1Dy9Saf","index":0,"type":"function"}]\n`, 'id: msg_017aTuY86wNxth5TE544yqJq', 'event: tool_calls', `data: [{"function":{"arguments":""},"index":0,"type":"function"}]\n`, 'id: msg_017aTuY86wNxth5TE544yqJq', 'event: tool_calls', `data: [{"function":{"arguments":"{\\"city\\": \\""},"index":0,"type":"function"}]\n`, 'id: msg_017aTuY86wNxth5TE544yqJq', 'event: tool_calls', `data: [{"function":{"arguments":""},"index":0,"type":"function"}]\n`, 'id: msg_017aTuY86wNxth5TE544yqJq', 'event: tool_calls', `data: [{"function":{"arguments":"州\\"}"},"index":0,"type":"function"}]\n`, 'id: msg_017aTuY86wNxth5TE544yqJq', 'event: data', 'data: {"type":"content_block_stop","index":1}\n', 'id: msg_017aTuY86wNxth5TE544yqJq', 'event: stop', 'data: "tool_use"\n', 'id: msg_017aTuY86wNxth5TE544yqJq', 'event: usage', 'data: {"inputCacheMissTokens":457,"totalInputTokens":457,"totalOutputTokens":84,"totalTokens":541}\n', ].map((item) => `${item}\n`), ); expect(onToolCallMock).toHaveBeenCalledTimes(5); }); it('should handle parallel tools use event and ReadableStream input', async () => { const streams = [ { type: 'message_start', message: { id: 'msg_0175ryA67RbGrnRrGBXFQEYK', type: 'message', role: 'assistant', model: 'claude-3-5-sonnet-20240620', content: [], stop_reason: null, stop_sequence: null, usage: { input_tokens: 485, output_tokens: 4 }, }, }, { type: 'content_block_start', index: 0, content_block: { type: 'text', text: '' } }, { type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: '好的,我会为您查询杭州和北京的天气情况。' }, }, { type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: '请稍等,我现在开始获取这两个城市的天气信息。' }, }, { type: 'content_block_stop', index: 0 }, { type: 'content_block_start', index: 1, content_block: { type: 'tool_use', id: 'toolu_011NuszmBcxskstLWe4z4z5B', name: 'realtime-weather____fetchCurrentWeather', input: {}, }, }, { type: 'content_block_delta', index: 1, delta: { type: 'input_json_delta', partial_json: '' }, }, { type: 'content_block_delta', index: 1, delta: { type: 'input_json_delta', partial_json: '{"city": "杭州"}' }, }, { type: 'content_block_stop', index: 1 }, { type: 'content_block_start', index: 2, content_block: { type: 'tool_use', id: 'toolu_01HojNiibMiKnYFvLrJyfX3B', name: 'realtime-weather____fetchCurrentWeather', input: {}, }, }, { type: 'content_block_delta', index: 2, delta: { type: 'input_json_delta', partial_json: '' }, }, { type: 'content_block_delta', index: 2, delta: { type: 'input_json_delta', partial_json: '{"city": "北京"}' }, }, { type: 'content_block_stop', index: 2 }, { type: 'message_delta', delta: { stop_reason: 'tool_use', stop_sequence: null }, usage: { output_tokens: 150 }, }, { type: 'message_stop' }, ]; const mockReadableStream = new ReadableStream({ start(controller) { streams.forEach((chunk) => { controller.enqueue(chunk); }); controller.close(); }, }); const onToolCallMock = vi.fn(); const protocolStream = AnthropicStream(mockReadableStream, { callbacks: { onToolsCalling: onToolCallMock, }, }); const decoder = new TextDecoder(); const chunks = []; // @ts-ignore for await (const chunk of protocolStream) { chunks.push(decoder.decode(chunk, { stream: true })); } expect(chunks).toEqual( [ 'id: msg_0175ryA67RbGrnRrGBXFQEYK', 'event: data', 'data: {"id":"msg_0175ryA67RbGrnRrGBXFQEYK","type":"message","role":"assistant","model":"claude-3-5-sonnet-20240620","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":485,"output_tokens":4}}\n', 'id: msg_0175ryA67RbGrnRrGBXFQEYK', 'event: data', 'data: ""\n', 'id: msg_0175ryA67RbGrnRrGBXFQEYK', 'event: text', 'data: "好的,我会为您查询杭州和北京的天气情况。"\n', 'id: msg_0175ryA67RbGrnRrGBXFQEYK', 'event: text', 'data: "请稍等,我现在开始获取这两个城市的天气信息。"\n', 'id: msg_0175ryA67RbGrnRrGBXFQEYK', 'event: data', 'data: {"type":"content_block_stop","index":0}\n', // Tool calls 'id: msg_0175ryA67RbGrnRrGBXFQEYK', 'event: tool_calls', `data: [{"function":{"arguments":"","name":"realtime-weather____fetchCurrentWeather"},"id":"toolu_011NuszmBcxskstLWe4z4z5B","index":0,"type":"function"}]\n`, 'id: msg_0175ryA67RbGrnRrGBXFQEYK', 'event: tool_calls', `data: [{"function":{"arguments":""},"index":0,"type":"function"}]\n`, 'id: msg_0175ryA67RbGrnRrGBXFQEYK', 'event: tool_calls', `data: [{"function":{"arguments":"{\\"city\\": \\"杭州\\"}"},"index":0,"type":"function"}]\n`, 'id: msg_0175ryA67RbGrnRrGBXFQEYK', 'event: data', `data: {"type":"content_block_stop","index":1}\n`, 'id: msg_0175ryA67RbGrnRrGBXFQEYK', 'event: tool_calls', `data: [{"function":{"arguments":"","name":"realtime-weather____fetchCurrentWeather"},"id":"toolu_01HojNiibMiKnYFvLrJyfX3B","index":1,"type":"function"}]\n`, 'id: msg_0175ryA67RbGrnRrGBXFQEYK', 'event: tool_calls', `data: [{"function":{"arguments":""},"index":1,"type":"function"}]\n`, 'id: msg_0175ryA67RbGrnRrGBXFQEYK', 'event: tool_calls', `data: [{"function":{"arguments":"{\\"city\\": \\"北京\\"}"},"index":1,"type":"function"}]\n`, 'id: msg_0175ryA67RbGrnRrGBXFQEYK', 'event: data', 'data: {"type":"content_block_stop","index":2}\n', 'id: msg_0175ryA67RbGrnRrGBXFQEYK', 'event: stop', 'data: "tool_use"\n', 'id: msg_0175ryA67RbGrnRrGBXFQEYK', 'event: usage', 'data: {"inputCacheMissTokens":485,"totalInputTokens":485,"totalOutputTokens":154,"totalTokens":639}\n', 'id: msg_0175ryA67RbGrnRrGBXFQEYK', 'event: stop', 'data: "message_stop"\n', ].map((item) => `${item}\n`), ); expect(onToolCallMock).toHaveBeenCalledTimes(6); }); it('should handle prompts context caching', async () => { const streams = [ { type: 'message_start', message: { id: 'msg_01Vxc4yQTEjkDSba3N3BMbH8', type: 'message', role: 'assistant', model: 'claude-3-7-sonnet-20250219', content: [], stop_reason: null, stop_sequence: null, usage: { input_tokens: 6, cache_creation_input_tokens: 457, cache_read_input_tokens: 17918, output_tokens: 2, }, }, }, { type: 'content_block_start', index: 0, content_block: { type: 'text', text: '' } }, { type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: '\n\n根' } }, { type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: '/\n[^20]: https://s' }, }, { type: 'content_block_stop', index: 0 }, { type: 'message_delta', delta: { stop_reason: 'end_turn', stop_sequence: null }, usage: { output_tokens: 3222 }, }, { type: 'message_stop' }, ]; const mockReadableStream = new ReadableStream({ start(controller) { streams.forEach((chunk) => { controller.enqueue(chunk); }); controller.close(); }, }); const protocolStream = AnthropicStream(mockReadableStream); const decoder = new TextDecoder(); const chunks = []; // @ts-ignore for await (const chunk of protocolStream) { chunks.push(decoder.decode(chunk, { stream: true })); } expect(chunks).toEqual( [ 'id: msg_01Vxc4yQTEjkDSba3N3BMbH8', 'event: data', 'data: {"id":"msg_01Vxc4yQTEjkDSba3N3BMbH8","type":"message","role":"assistant","model":"claude-3-7-sonnet-20250219","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":6,"cache_creation_input_tokens":457,"cache_read_input_tokens":17918,"output_tokens":2}}\n', 'id: msg_01Vxc4yQTEjkDSba3N3BMbH8', 'event: data', 'data: ""\n', 'id: msg_01Vxc4yQTEjkDSba3N3BMbH8', 'event: text', 'data: "\\n\\n根"\n', 'id: msg_01Vxc4yQTEjkDSba3N3BMbH8', 'event: text', 'data: "/\\n[^20]: https://s"\n', 'id: msg_01Vxc4yQTEjkDSba3N3BMbH8', 'event: data', 'data: {"type":"content_block_stop","index":0}\n', 'id: msg_01Vxc4yQTEjkDSba3N3BMbH8', 'event: stop', 'data: "end_turn"\n', 'id: msg_01Vxc4yQTEjkDSba3N3BMbH8', 'event: usage', 'data: {"inputCacheMissTokens":6,"inputCachedTokens":17918,"inputWriteCacheTokens":457,"totalInputTokens":18381,"totalOutputTokens":3224,"totalTokens":21605}\n', 'id: msg_01Vxc4yQTEjkDSba3N3BMbH8', 'event: stop', 'data: "message_stop"\n', ].map((item) => `${item}\n`), ); }); describe('thinking', () => { it('should handle normal thinking ', async () => { const streams = [ { type: 'message_start', message: { id: 'msg_01MNsLe7n1uVLtu6W8rCFujD', type: 'message', role: 'assistant', model: 'claude-3-7-sonnet-20250219', content: [], stop_reason: null, stop_sequence: null, usage: { input_tokens: 46, cache_creation_input_tokens: 0, cache_read_input_tokens: 0, output_tokens: 11, }, }, }, { type: 'content_block_start', index: 0, content_block: { type: 'thinking', thinking: '', signature: '' }, }, { type: 'content_block_delta', index: 0, delta: { type: 'thinking_delta', thinking: '我需要比较两个数字的' }, }, { type: 'content_block_delta', index: 0, delta: { type: 'thinking_delta', thinking: '大小:9.8和9' }, }, { type: 'content_block_delta', index: 0, delta: { type: 'thinking_delta', thinking: '11\n\n所以9.8比9.11大。' }, }, { type: 'content_block_delta', index: 0, delta: { type: 'signature_delta', signature: 'EuYBCkQYAiJAHnHRJG4nPBrdTlo6CmXoyE8WYoQeoPiLnXaeuaM8ExdiIEkVvxK1DYXOz5sCubs2s/G1NsST8A003Zb8XmuhYBIMwDGMZSZ3+gxOEBpVGgzdpOlDNBTxke31SngiMKUk6WcSiA11OSVBuInNukoAhnRd5jPAEg7e5mIoz/qJwnQHV8I+heKUreP77eJdFipQaM3FHn+avEHuLa/Z/fu0O9BftDi+caB1UWDwJakNeWX1yYTvK+N1v4gRpKbj4AhctfYHMjq8qX9XTnXme5AGzCYC6HgYw2/RfalWzwNxI6k=', }, }, { type: 'content_block_stop', index: 0 }, { type: 'content_block_start', index: 1, content_block: { type: 'text', text: '' } }, { type: 'content_block_delta', index: 1, delta: { type: 'text_delta', text: '9.8比9.11大。' }, }, { type: 'content_block_stop', index: 1 }, { type: 'message_delta', delta: { stop_reason: 'end_turn', stop_sequence: null }, usage: { output_tokens: 354 }, }, { type: 'message_stop' }, ]; const mockReadableStream = new ReadableStream({ start(controller) { streams.forEach((chunk) => { controller.enqueue(chunk); }); controller.close(); }, }); const protocolStream = AnthropicStream(mockReadableStream); const decoder = new TextDecoder(); const chunks = []; // @ts-ignore for await (const chunk of protocolStream) { chunks.push(decoder.decode(chunk, { stream: true })); } expect(chunks).toEqual( [ 'id: msg_01MNsLe7n1uVLtu6W8rCFujD', 'event: data', 'data: {"id":"msg_01MNsLe7n1uVLtu6W8rCFujD","type":"message","role":"assistant","model":"claude-3-7-sonnet-20250219","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":46,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":11}}\n', 'id: msg_01MNsLe7n1uVLtu6W8rCFujD', 'event: reasoning', 'data: ""\n', 'id: msg_01MNsLe7n1uVLtu6W8rCFujD', 'event: reasoning', 'data: "我需要比较两个数字的"\n', 'id: msg_01MNsLe7n1uVLtu6W8rCFujD', 'event: reasoning', 'data: "大小:9.8和9"\n', 'id: msg_01MNsLe7n1uVLtu6W8rCFujD', 'event: reasoning', 'data: "11\\n\\n所以9.8比9.11大。"\n', // Tool calls 'id: msg_01MNsLe7n1uVLtu6W8rCFujD', 'event: reasoning_signature', `data: "EuYBCkQYAiJAHnHRJG4nPBrdTlo6CmXoyE8WYoQeoPiLnXaeuaM8ExdiIEkVvxK1DYXOz5sCubs2s/G1NsST8A003Zb8XmuhYBIMwDGMZSZ3+gxOEBpVGgzdpOlDNBTxke31SngiMKUk6WcSiA11OSVBuInNukoAhnRd5jPAEg7e5mIoz/qJwnQHV8I+heKUreP77eJdFipQaM3FHn+avEHuLa/Z/fu0O9BftDi+caB1UWDwJakNeWX1yYTvK+N1v4gRpKbj4AhctfYHMjq8qX9XTnXme5AGzCYC6HgYw2/RfalWzwNxI6k="\n`, 'id: msg_01MNsLe7n1uVLtu6W8rCFujD', 'event: data', `data: {"type":"content_block_stop","index":0}\n`, 'id: msg_01MNsLe7n1uVLtu6W8rCFujD', 'event: data', `data: ""\n`, 'id: msg_01MNsLe7n1uVLtu6W8rCFujD', 'event: text', `data: "9.8比9.11大。"\n`, 'id: msg_01MNsLe7n1uVLtu6W8rCFujD', 'event: data', `data: {"type":"content_block_stop","index":1}\n`, 'id: msg_01MNsLe7n1uVLtu6W8rCFujD', 'event: stop', 'data: "end_turn"\n', 'id: msg_01MNsLe7n1uVLtu6W8rCFujD', 'event: usage', 'data: {"inputCacheMissTokens":46,"totalInputTokens":46,"totalOutputTokens":365,"totalTokens":411}\n', 'id: msg_01MNsLe7n1uVLtu6W8rCFujD', 'event: stop', 'data: "message_stop"\n', ].map((item) => `${item}\n`), ); }); it('should handle flagged thinking ', async () => { const streams = [ { type: 'message_start', message: { id: 'msg_019q32esPvu3TftzZnL6JPys', type: 'message', role: 'assistant', model: 'claude-3-7-sonnet-20250219', content: [], stop_reason: null, stop_sequence: null, usage: { input_tokens: 92, cache_creation_input_tokens: 0, cache_read_input_tokens: 0, output_tokens: 4, }, }, }, { type: 'content_block_start', index: 0, content_block: { type: 'redacted_thinking', data: 'EvYBCoYBGAIiQNzXoJZW+Ocan2YajVtfm4HE2B3NJdxl05x4M+qDZ2XDAv8uysmma7oaIwNsO/gaZDcaYphIPVvSR0da9BiU4fkqQOseUkmKX3f2PDTFQsTVPGJQdiAoojyYWydq912tQiaWOAnV8pEpsw5qzAhjTg7a/VhucOXRjSO6PrBGUJs4IGgSDEOrVeGKw+XJKwI32RoMXxGUrsCpnzifc238IjCiip27oNxaDKqsGVsa3l8CxznwldGK5o7NKoAWxBr6EjmUyWBfHSjCBSG58dLhH6AqHTHs1h7CpyC9q2PiGFKyI6Qpyq27LMf/IJrL4JzY', }, }, { type: 'ping' }, { type: 'content_block_stop', index: 0 }, { type: 'content_block_start', index: 1, content_block: { type: 'redacted_thinking', data: 'EqsCCoYBGAIiQFOzsK5wAM+th5SAo3iYCtupF+/ToOYMoKuQowEkQdMYSr+uTiZGV17Ezt1YopNShapyJHraaanqud0SpjNWb1EqQAIs1xKVmShDP/KzTnkeGj3sB1w9fjEcB8I4Q1oYXmAOvEeBRp+/0eszpC5KM4vfBXockGREIX3b9t0aVkKV5LQSDMMox34k4/t6jt5lwBoM8BCR+z8yvwr8RmRAIjCuZUKwzt5cpTSSKsMRF5w/NkH0KeVbDPkHJAHoyKbVThaz2tNP4DGn9Hje/eOhm14qUjEqjkE7ZBa4oXfutU09Ekn6S+Cn5SsYrFLeg+o4/8ewb8YHuspvYbMMN4IwbkqQp19hi2z6QxUWWbLrpMe40Fi2PNKct/dmGmw/SF692L/tyOU=', }, }, { type: 'content_block_stop', index: 1 }, { type: 'content_block_start', index: 2, content_block: { type: 'redacted_thinking', data: 'EowCCoYBGAIiQK1Px08f5EwkoGrjGov2SWq2eHJVrkwBhL9atUCuZegNB+yK0F2ENixvwLlFjZOeSDhfVZ3von76crqoGaEUOUgqQGnTe9FWXAOXYnreuT4sCpUCVSq6pyewyyYCJkAVHTc8YCgPQsGagW9qNmUJDNdCoFyMEtFzqRuHZk3nc/9KjJgSDK4yegsNIw6czWXdCxoMzzrg26MN6RjFUrRqIjAjjEWG7mPPMolxAVvscgcaETILV4WtO4xOXDxK0L2NLSb+GlR7LQraWOATBMBc0lAqM43SvsI2xLX6GvdtNIr98tAKXpadetuHoDta+uqVn9dRfJG6Nno0e1cdx9VzgrOM2I0l6w==', }, }, { type: 'content_block_stop', index: 2 }, { type: 'content_block_start', index: 3, content_block: { type: 'redacted_thinking', data: 'EvMGCoYBGAIiQKkoAFWygajHbTRK/q0hrakXULQBWfg0/EAiNRami4uuzOwDVEPBDu74aP47MMQG0zhLspVkvGpOlfNLkkeROYEqQHO9MLpvtKDkob22tAH2ctP7CxIhI+SRZ0flou71sDdaVtcsel2dIas8+soULHfW68glHJ1ormzeUKv9YHtvVxMSDIC31I3S0nvTPOB/GxoMos7jtbwUPmYvx4viIjD01EiiuBny4srom6xEm/c9VCJQaRKuglEehQ3BRxn2Qs28eGNs7EV63kF5DHV7QTIqmQUaw3A/XKIK+2dhPMzE9/n7VSeWvPl7lFLgTCZBW+q49KoLNuIw5tGMR2nXxTrykvQt9zhNDb9TYAsu8nubMASJt9hWwwMpXAPJhUOP5IL+/p6YDuN9Y5TbDkCiR+3Dgs4xh6VeBhD0cusWdC2LefHT92i1dz2mCFhTtPG8nr/jChOGv/KPPO24sJcSMUYu1T07ohiDCe6vjEckBP2aaSH46rcGEydFBaufPKGD2LsiQfrFDRx639AFlwdeSz30cRrjYCiXBu3l/it0LYt8m5Ixsn41P0xFiPDfecZAkGymvrV8JrS0uPnRbpF9n4CNj1YanoplbVgA9yegj962PnRBHwIoT/UMTLnBgxNE1J9LM6JuMbDQRXpYpZ7OaB9FXwxCKjcWgSiGmiPjdWwan8z7cILDes3Kz9sBaqF4s6uj9eJ31fFL9dHKS0jciCrOPMfKUOQSP/HRuAUsyeyUROquh4MIfXLUUPrFCXyyy42wBvrTXkdWOGZF/wMw6YQGC3iNbgldO4K6OBc8+6+AhRsZR51EuBp1iMl5na6KspyVJnCMx52lUYq3SXNTZkiika/z1jO3C1+cvrzQggo9Yf56bzjKBlVjdjqsIqaNOB8BQqU8EidE668/7cMLF3YJP2YwohEO1C7vOV1vliNkyxdCFz6qB9q8vzZ1hIlFz8LHVxZRmmlMMnAq/Q9nWOXmi/6lIXVRIP+4z6dyIWNINTR/D2ZsMjN34cnDgxgbzGuDoicikliSnJG+RB1smJSAmMrNf+U+JZSW2zpU+7zu1dZm5DMKlef+pmbIJMCxVS7v98vAxt/tO+99HlXwhktL4JuOdC2TcvrDm56e2IeGY0KR5TVA2sfCqxEyb+QAAbwDD7TDwq+r62GVBA==', }, }, { type: 'content_block_stop', index: 3 }, { type: 'content_block_start', index: 4, content_block: { type: 'text', text: '' } }, { type: 'content_block_delta', index: 4, delta: { type: 'text_delta', text: "I'm not able to respond to special commands or triggers", }, }, { type: 'content_block_delta', index: 4, delta: { type: 'text_delta', text: ' with information, answer questions, or assist with many other ways.', }, }, { type: 'content_block_stop', index: 4 }, { type: 'message_delta', delta: { stop_reason: 'end_turn', stop_sequence: null }, usage: { output_tokens: 259 }, }, { type: 'message_stop' }, ]; const mockReadableStream = new ReadableStream({ start(controller) { streams.forEach((chunk) => { controller.enqueue(chunk); }); controller.close(); }, }); const protocolStream = AnthropicStream(mockReadableStream); const decoder = new TextDecoder(); const chunks = []; // @ts-ignore for await (const chunk of protocolStream) { chunks.push(decoder.decode(chunk, { stream: true })); } expect(chunks).toEqual( [ 'id: msg_019q32esPvu3TftzZnL6JPys', 'event: data', 'data: {"id":"msg_019q32esPvu3TftzZnL6JPys","type":"message","role":"assistant","model":"claude-3-7-sonnet-20250219","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":92,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":4}}\n', 'id: msg_019q32esPvu3TftzZnL6JPys', 'event: flagged_reasoning_signature', 'data: "EvYBCoYBGAIiQNzXoJZW+Ocan2YajVtfm4HE2B3NJdxl05x4M+qDZ2XDAv8uysmma7oaIwNsO/gaZDcaYphIPVvSR0da9BiU4fkqQOseUkmKX3f2PDTFQsTVPGJQdiAoojyYWydq912tQiaWOAnV8pEpsw5qzAhjTg7a/VhucOXRjSO6PrBGUJs4IGgSDEOrVeGKw+XJKwI32RoMXxGUrsCpnzifc238IjCiip27oNxaDKqsGVsa3l8CxznwldGK5o7NKoAWxBr6EjmUyWBfHSjCBSG58dLhH6AqHTHs1h7CpyC9q2PiGFKyI6Qpyq27LMf/IJrL4JzY"\n', 'id: msg_019q32esPvu3TftzZnL6JPys', 'event: data', 'data: {"type":"ping"}\n', 'id: msg_019q32esPvu3TftzZnL6JPys', 'event: data', 'data: {"type":"content_block_stop","index":0}\n', 'id: msg_019q32esPvu3TftzZnL6JPys', 'event: flagged_reasoning_signature', 'data: "EqsCCoYBGAIiQFOzsK5wAM+th5SAo3iYCtupF+/ToOYMoKuQowEkQdMYSr+uTiZGV17Ezt1YopNShapyJHraaanqud0SpjNWb1EqQAIs1xKVmShDP/KzTnkeGj3sB1w9fjEcB8I4Q1oYXmAOvEeBRp+/0eszpC5KM4vfBXockGREIX3b9t0aVkKV5LQSDMMox34k4/t6jt5lwBoM8BCR+z8yvwr8RmRAIjCuZUKwzt5cpTSSKsMRF5w/NkH0KeVbDPkHJAHoyKbVThaz2tNP4DGn9Hje/eOhm14qUjEqjkE7ZBa4oXfutU09Ekn6S+Cn5SsYrFLeg+o4/8ewb8YHuspvYbMMN4IwbkqQp19hi2z6QxUWWbLrpMe40Fi2PNKct/dmGmw/SF692L/tyOU="\n', // Tool calls 'id: msg_019q32esPvu3TftzZnL6JPys', 'event: data', `data: {"type":"content_block_stop","index":1}\n`, 'id: msg_019q32esPvu3TftzZnL6JPys', 'event: flagged_reasoning_signature', `data: "EowCCoYBGAIiQK1Px08f5EwkoGrjGov2SWq2eHJVrkwBhL9atUCuZegNB+yK0F2ENixvwLlFjZOeSDhfVZ3von76crqoGaEUOUgqQGnTe9FWXAOXYnreuT4sCpUCVSq6pyewyyYCJkAVHTc8YCgPQsGagW9qNmUJDNdCoFyMEtFzqRuHZk3nc/9KjJgSDK4yegsNIw6czWXdCxoMzzrg26MN6RjFUrRqIjAjjEWG7mPPMolxAVvscgcaETILV4WtO4xOXDxK0L2NLSb+GlR7LQraWOATBMBc0lAqM43SvsI2xLX6GvdtNIr98tAKXpadetuHoDta+uqVn9dRfJG6Nno0e1cdx9VzgrOM2I0l6w=="\n`, 'id: msg_019q32esPvu3TftzZnL6JPys', 'event: data', `data: {"type":"content_block_stop","index":2}\n`, 'id: msg_019q32esPvu3TftzZnL6JPys', 'event: flagged_reasoning_signature', `data: "EvMGCoYBGAIiQKkoAFWygajHbTRK/q0hrakXULQBWfg0/EAiNRami4uuzOwDVEPBDu74aP47MMQG0zhLspVkvGpOlfNLkkeROYEqQHO9MLpvtKDkob22tAH2ctP7CxIhI+SRZ0flou71sDdaVtcsel2dIas8+soULHfW68glHJ1ormzeUKv9YHtvVxMSDIC31I3S0nvTPOB/GxoMos7jtbwUPmYvx4viIjD01EiiuBny4srom6xEm/c9VCJQaRKuglEehQ3BRxn2Qs28eGNs7EV63kF5DHV7QTIqmQUaw3A/XKIK+2dhPMzE9/n7VSeWvPl7lFLgTCZBW+q49KoLNuIw5tGMR2nXxTrykvQt9zhNDb9TYAsu8nubMASJt9hWwwMpXAPJhUOP5IL+/p6YDuN9Y5TbDkCiR+3Dgs4xh6VeBhD0cusWdC2LefHT92i1dz2mCFhTtPG8nr/jChOGv/KPPO24sJcSMUYu1T07ohiDCe6vjEckBP2aaSH46rcGEydFBaufPKGD2LsiQfrFDRx639AFlwdeSz30cRrjYCiXBu3l/it0LYt8m5Ixsn41P0xFiPDfecZAkGymvrV8JrS0uPnRbpF9n4CNj1YanoplbVgA9yegj962PnRBHwIoT/UMTLnBgxNE1J9LM6JuMbDQRXpYpZ7OaB9FXwxCKjcWgSiGmiPjdWwan8z7cILDes3Kz9sBaqF4s6uj9eJ31fFL9dHKS0jciCrOPMfKUOQSP/HRuAUsyeyUROquh4MIfXLUUPrFCXyyy42wBvrTXkdWOGZF/wMw6YQGC3iNbgldO4K6OBc8+6+AhRsZR51EuBp1iMl5na6KspyVJnCMx52lUYq3SXNTZkiika/z1jO3C1+cvrzQggo9Yf56bzjKBlVjdjqsIqaNOB8BQqU8EidE668/7cMLF3YJP2YwohEO1C7vOV1vliNkyxdCFz6qB9q8vzZ1hIlFz8LHVxZRmmlMMnAq/Q9nWOXmi/6lIXVRIP+4z6dyIWNINTR/D2ZsMjN34cnDgxgbzGuDoicikliSnJG+RB1smJSAmMrNf+U+JZSW2zpU+7zu1dZm5DMKlef+pmbIJMCxVS7v98vAxt/tO+99HlXwhktL4JuOdC2TcvrDm56e2IeGY0KR5TVA2sfCqxEyb+QAAbwDD7TDwq+r62GVBA=="\n`, 'id: msg_019q32esPvu3TftzZnL6JPys', 'event: data', `data: {"type":"content_block_stop","index":3}\n`, 'id: msg_019q32esPvu3TftzZnL6JPys', 'event: data', `data: ""\n`, 'id: msg_019q32esPvu3TftzZnL6JPys', 'event: text', `data: "I'm not able to respond to special commands or triggers"\n`, 'id: msg_019q32esPvu3TftzZnL6JPys', 'event: text', `data: " with information, answer questions, or assist with many other ways."\n`, 'id: msg_019q32esPvu3TftzZnL6JPys', 'event: data', `data: {"type":"content_block_stop","index":4}\n`, 'id: msg_019q32esPvu3TftzZnL6JPys', 'event: stop', 'data: "end_turn"\n', 'id: msg_019q32esPvu3TftzZnL6JPys', 'event: usage', 'data: {"inputCacheMissTokens":92,"totalInputTokens":92,"totalOutputTokens":263,"totalTokens":355}\n', 'id: msg_019q32esPvu3TftzZnL6JPys', 'event: stop', 'data: "message_stop"\n', ].map((item) => `${item}\n`), ); }); }); it('should handle ReadableStream input', async () => { const mockReadableStream = new ReadableStream({ start(controller) { controller.enqueue({ type: 'message_start', message: { id: 'message_1', metadata: {} }, }); controller.enqueue({ type: 'content_block_delta', delta: { type: 'text_delta', text: 'Hello' }, }); controller.enqueue({ type: 'message_stop', }); controller.close(); }, }); const protocolStream = AnthropicStream(mockReadableStream); const decoder = new TextDecoder(); const chunks = []; // @ts-ignore for await (const chunk of protocolStream) { chunks.push(decoder.decode(chunk, { stream: true })); } expect(chunks).toEqual([ 'id: message_1\n', 'event: data\n', `data: {"id":"message_1","metadata":{}}\n\n`, 'id: message_1\n', 'event: text\n', `data: "Hello"\n\n`, 'id: message_1\n', 'event: stop\n', `data: "message_stop"\n\n`, ]); }); it('should handle un-normal block type', async () => { const streams = [ { type: 'message_start', message: { id: 'msg_01MNsLe7n1uVLtu6W8rCFujD', type: 'message', role: 'assistant', model: 'claude-3-7-sonnet-20250219', content: [], stop_reason: null, stop_sequence: null, usage: { input_tokens: 46, cache_creation_input_tokens: 0, cache_read_input_tokens: 0, output_tokens: 11, }, }, }, { type: 'content_block_start', index: 0, content_block: { type: 'thinking', thinking: 'abc', signature: 'dddd' }, }, { type: 'content_block_start', index: 0, content_block: { type: 'thinking', thinking: null }, }, { type: 'content_block_start', index: 0, content_block: { type: 'abc', abc: '' }, }, { type: 'content_block_delta', index: 0, delta: { type: 'abc', abc: '123' }, }, ]; const mockReadableStream = new ReadableStream({ start(controller) { streams.forEach((chunk) => { controller.enqueue(chunk); }); controller.close(); }, }); const protocolStream = AnthropicStream(mockReadableStream); const decoder = new TextDecoder(); const chunks = []; // @ts-ignore for await (const chunk of protocolStream) { chunks.push(decoder.decode(chunk, { stream: true })); } expect(chunks).toEqual( [ 'id: msg_01MNsLe7n1uVLtu6W8rCFujD', 'event: data', 'data: {"id":"msg_01MNsLe7n1uVLtu6W8rCFujD","type":"message","role":"assistant","model":"claude-3-7-sonnet-20250219","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":46,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":11}}\n', 'id: msg_01MNsLe7n1uVLtu6W8rCFujD', 'event: reasoning', 'data: "abc"\n', 'id: msg_01MNsLe7n1uVLtu6W8rCFujD', 'event: reasoning_signature', 'data: "dddd"\n', 'id: msg_01MNsLe7n1uVLtu6W8rCFujD', 'event: data', 'data: {"type":"thinking","thinking":null}\n', 'id: msg_01MNsLe7n1uVLtu6W8rCFujD', 'event: data', 'data: {"type":"content_block_start","index":0,"content_block":{"type":"abc","abc":""}}\n', 'id: msg_01MNsLe7n1uVLtu6W8rCFujD', 'event: data', 'data: {"type":"content_block_delta","index":0,"delta":{"type":"abc","abc":"123"}}\n', ].map((item) => `${item}\n`), ); }); });