forked from wangziqi/gongxue-base
69 lines
2.3 KiB
TypeScript
69 lines
2.3 KiB
TypeScript
import { AiModelStreamService } from './ai-model-stream.service';
|
|
import type { AiRuntimeConfig } from '../ai-config/dto/ai-config.dto';
|
|
|
|
const config: AiRuntimeConfig = {
|
|
provider: 'DEEPSEEK' as AiRuntimeConfig['provider'],
|
|
baseUrl: 'https://example.test/v1',
|
|
apiKey: 'secret',
|
|
defaultModel: 'deepseek-reasoner',
|
|
timeoutMs: 1000,
|
|
enabled: true,
|
|
};
|
|
|
|
describe('AiModelStreamService', () => {
|
|
afterEach(() => jest.restoreAllMocks());
|
|
|
|
it('分离思考、正文并拼接分片工具调用,且处理无尾随空行的最后事件', async () => {
|
|
const chunks = [
|
|
'data: {"choices":[{"delta":{"reasoning_content":"思考"}}]}\n\n',
|
|
'data: {"choices":[{"delta":{"content":"答案","tool_calls":[{"index":0,"id":"call_","function":{"name":"search_","arguments":"{\\"q\\":"}}]}}]}\n\n',
|
|
'data: {"choices":[{"delta":{"tool_calls":[{"index":0,"id":"1","function":{"name":"students","arguments":"\\"张三\\"}"}}]}}]}',
|
|
];
|
|
async function* body() {
|
|
for (const chunk of chunks) yield Buffer.from(chunk);
|
|
}
|
|
const service = new AiModelStreamService();
|
|
jest.spyOn(service as never, 'pinnedPost' as never).mockResolvedValue({
|
|
status: 200,
|
|
contentType: 'text/event-stream',
|
|
body: body(),
|
|
} as never);
|
|
|
|
const events = [];
|
|
for await (const event of service.stream(
|
|
config,
|
|
[{ role: 'user', content: '查询' }],
|
|
[],
|
|
new AbortController().signal,
|
|
)) events.push(event);
|
|
|
|
expect(events).toEqual([
|
|
{ type: 'reasoning', delta: '思考' },
|
|
{ type: 'content', delta: '答案' },
|
|
{
|
|
type: 'complete',
|
|
toolCalls: [{ id: 'call_1', name: 'search_students', arguments: '{"q":"张三"}' }],
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('不向调用方暴露上游非 JSON 错误正文', async () => {
|
|
async function* body() { yield Buffer.from('proxy internal detail'); }
|
|
const service = new AiModelStreamService();
|
|
jest.spyOn(service as never, 'pinnedPost' as never).mockResolvedValue({
|
|
status: 502,
|
|
contentType: 'text/plain',
|
|
body: body(),
|
|
} as never);
|
|
const consume = async () => {
|
|
for await (const _ of service.stream(
|
|
config,
|
|
[{ role: 'user', content: '查询' }],
|
|
[],
|
|
new AbortController().signal,
|
|
)) void _;
|
|
};
|
|
await expect(consume()).rejects.toThrow('AI 服务暂时不可用');
|
|
});
|
|
});
|