@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.
200 lines (175 loc) • 5.33 kB
text/typescript
import { beforeAll, describe, expect, it, vi } from 'vitest';
import { SparkAIStream, transformSparkResponseToStream } from './spark';
import type OpenAI from 'openai';
describe('SparkAIStream', () => {
beforeAll(() => {});
it('should transform non-streaming response to stream', async () => {
const mockResponse = {
id: "cha000ceba6@dx193d200b580b8f3532",
object: "chat.completion",
created: 1734395014,
model: "max-32k",
choices: [
{
message: {
role: "assistant",
content: "",
refusal: null,
tool_calls: {
type: "function",
function: {
arguments: '{"city":"Shanghai"}',
name: "realtime-weather____fetchCurrentWeather"
},
id: "call_1"
}
},
index: 0,
logprobs: null,
finish_reason: "tool_calls"
}
],
usage: {
prompt_tokens: 8,
completion_tokens: 0,
total_tokens: 8
}
} as unknown as OpenAI.ChatCompletion;
const stream = transformSparkResponseToStream(mockResponse);
const decoder = new TextDecoder();
const chunks = [];
// @ts-ignore
for await (const chunk of stream) {
chunks.push(chunk);
}
expect(chunks).toHaveLength(2);
expect(chunks[0].choices[0].delta.tool_calls).toEqual([{
function: {
arguments: '{"city":"Shanghai"}',
name: "realtime-weather____fetchCurrentWeather"
},
id: "call_1",
index: 0,
type: "function"
}]);
expect(chunks[1].choices[0].finish_reason).toBeDefined();
});
it('should transform streaming response with tool calls', async () => {
const mockStream = new ReadableStream({
start(controller) {
controller.enqueue({
id: "cha000b0bf9@dx193d1ffa61cb894532",
object: "chat.completion.chunk",
created: 1734395014,
model: "max-32k",
choices: [
{
delta: {
role: "assistant",
content: "",
tool_calls: {
type: "function",
function: {
arguments: '{"city":"Shanghai"}',
name: "realtime-weather____fetchCurrentWeather"
},
id: "call_1"
}
},
index: 0
}
]
} as unknown as OpenAI.ChatCompletionChunk);
controller.close();
}
});
const onToolCallMock = vi.fn();
const protocolStream = SparkAIStream(mockStream, {
onToolCall: 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: cha000b0bf9@dx193d1ffa61cb894532\n',
'event: tool_calls\n',
`data: [{\"function\":{\"arguments\":\"{\\\"city\\\":\\\"Shanghai\\\"}\",\"name\":\"realtime-weather____fetchCurrentWeather\"},\"id\":\"call_1\",\"index\":0,\"type\":\"function\"}]\n\n`
]);
expect(onToolCallMock).toHaveBeenCalledTimes(1);
});
it('should handle text content in stream', async () => {
const mockStream = new ReadableStream({
start(controller) {
controller.enqueue({
id: "test-id",
object: "chat.completion.chunk",
created: 1734395014,
model: "max-32k",
choices: [
{
delta: {
content: "Hello",
role: "assistant"
},
index: 0
}
]
} as OpenAI.ChatCompletionChunk);
controller.enqueue({
id: "test-id",
object: "chat.completion.chunk",
created: 1734395014,
model: "max-32k",
choices: [
{
delta: {
content: " World",
role: "assistant"
},
index: 0
}
]
} as OpenAI.ChatCompletionChunk);
controller.close();
}
});
const onTextMock = vi.fn();
const protocolStream = SparkAIStream(mockStream, {
onText: onTextMock
});
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: test-id\n',
'event: text\n',
'data: "Hello"\n\n',
'id: test-id\n',
'event: text\n',
'data: " World"\n\n'
]);
expect(onTextMock).toHaveBeenNthCalledWith(1, '"Hello"');
expect(onTextMock).toHaveBeenNthCalledWith(2, '" World"');
});
it('should handle empty stream', async () => {
const mockStream = new ReadableStream({
start(controller) {
controller.close();
}
});
const protocolStream = SparkAIStream(mockStream);
const decoder = new TextDecoder();
const chunks = [];
// @ts-ignore
for await (const chunk of protocolStream) {
chunks.push(decoder.decode(chunk, { stream: true }));
}
expect(chunks).toEqual([]);
});
});