@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
text/typescript
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`),
);
});
});