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.

827 lines (813 loc) 31.6 kB
import { describe, expect, it, vi } from 'vitest'; import { AgentRuntimeErrorType } from '@/libs/model-runtime'; import { FIRST_CHUNK_ERROR_KEY } from '../protocol'; import { createReadableStream, readStreamChunk } from '../utils'; import { OpenAIResponsesStream } from './responsesStream'; describe('OpenAIResponsesStream', () => { it('should transform OpenAI stream to protocol stream', async () => { const mockOpenAIStream = createReadableStream([ { type: 'response.created', response: { id: 'resp_683e7b8ca3308190b6837f20d2c015cd0cf93af363cdcf58', object: 'response', created_at: 1748925324, status: 'in_progress', error: null, incomplete_details: null, instructions: null, max_output_tokens: null, model: 'o4-mini', output: [], parallel_tool_calls: true, previous_response_id: null, reasoning: { effort: 'medium', summary: null }, service_tier: 'auto', store: false, temperature: 1, text: { format: { type: 'text' } }, tool_choice: 'auto', tools: [ { type: 'function', description: 'a search service. Useful for when you need to answer questions about current events. Input should be a search query. Output is a JSON array of the query results', name: 'lobe-web-browsing____search____builtin', parameters: { properties: { query: { description: 'The search query', type: 'string' }, searchCategories: { description: 'The search categories you can set:', items: { enum: ['general', 'images', 'news', 'science', 'videos'], type: 'string', }, type: 'array', }, searchEngines: { description: 'The search engines you can use:', items: { enum: [ 'google', 'bilibili', 'bing', 'duckduckgo', 'npm', 'pypi', 'github', 'arxiv', 'google scholar', 'z-library', 'reddit', 'imdb', 'brave', 'wikipedia', 'pinterest', 'unsplash', 'vimeo', 'youtube', ], type: 'string', }, type: 'array', }, searchTimeRange: { description: 'The time range you can set:', enum: ['anytime', 'day', 'week', 'month', 'year'], type: 'string', }, }, required: ['query'], type: 'object', }, strict: true, }, { type: 'function', description: 'A crawler can visit page content. Output is a JSON object of title, content, url and website', name: 'lobe-web-browsing____crawlSinglePage____builtin', parameters: { properties: { url: { description: 'The url need to be crawled', type: 'string' } }, required: ['url'], type: 'object', }, strict: true, }, { type: 'function', description: 'A crawler can visit multi pages. If need to visit multi website, use this one. Output is an array of JSON object of title, content, url and website', name: 'lobe-web-browsing____crawlMultiPages____builtin', parameters: { properties: { urls: { items: { description: 'The urls need to be crawled', type: 'string' }, type: 'array', }, }, required: ['urls'], type: 'object', }, strict: true, }, ], top_p: 1, truncation: 'disabled', usage: null, user: null, metadata: {}, }, }, { type: 'response.in_progress', response: { id: 'resp_683e7b8ca3308190b6837f20d2c015cd0cf93af363cdcf58', object: 'response', created_at: 1748925324, status: 'in_progress', error: null, incomplete_details: null, instructions: null, max_output_tokens: null, model: 'o4-mini', output: [], parallel_tool_calls: true, previous_response_id: null, reasoning: { effort: 'medium', summary: null }, service_tier: 'auto', store: false, temperature: 1, text: { format: { type: 'text' } }, tool_choice: 'auto', tools: [ { type: 'function', description: 'a search service. Useful for when you need to answer questions about current events. Input should be a search query. Output is a JSON array of the query results', name: 'lobe-web-browsing____search____builtin', parameters: { properties: { query: { description: 'The search query', type: 'string' }, searchCategories: { description: 'The search categories you can set:', items: { enum: ['general', 'images', 'news', 'science', 'videos'], type: 'string', }, type: 'array', }, searchEngines: { description: 'The search engines you can use:', items: { enum: [ 'google', 'bilibili', 'bing', 'duckduckgo', 'npm', 'pypi', 'github', 'arxiv', 'google scholar', 'z-library', 'reddit', 'imdb', 'brave', 'wikipedia', 'pinterest', 'unsplash', 'vimeo', 'youtube', ], type: 'string', }, type: 'array', }, searchTimeRange: { description: 'The time range you can set:', enum: ['anytime', 'day', 'week', 'month', 'year'], type: 'string', }, }, required: ['query'], type: 'object', }, strict: true, }, { type: 'function', description: 'A crawler can visit page content. Output is a JSON object of title, content, url and website', name: 'lobe-web-browsing____crawlSinglePage____builtin', parameters: { properties: { url: { description: 'The url need to be crawled', type: 'string' } }, required: ['url'], type: 'object', }, strict: true, }, { type: 'function', description: 'A crawler can visit multi pages. If need to visit multi website, use this one. Output is an array of JSON object of title, content, url and website', name: 'lobe-web-browsing____crawlMultiPages____builtin', parameters: { properties: { urls: { items: { description: 'The urls need to be crawled', type: 'string' }, type: 'array', }, }, required: ['urls'], type: 'object', }, strict: true, }, ], top_p: 1, truncation: 'disabled', usage: null, user: null, metadata: {}, }, }, { type: 'response.output_item.added', output_index: 0, item: { id: 'rs_683e7bc80a9c81908f6e3d61ad63cc1e0cf93af363cdcf58', type: 'reasoning', summary: [], }, }, { type: 'response.output_item.added', output_index: 1, item: { id: 'msg_683e7bde8b0c8190970ab8c719c0fc1c0cf93af363cdcf58', type: 'message', status: 'in_progress', content: [], role: 'assistant', }, }, { type: 'response.content_part.added', item_id: 'msg_683e7bde8b0c8190970ab8c719c0fc1c0cf93af363cdcf58', output_index: 1, content_index: 0, part: { type: 'output_text', annotations: [], text: 'Hello' }, }, { type: 'response.content_part.added', item_id: 'msg_683e7bde8b0c8190970ab8c719c0fc1c0cf93af363cdcf58', output_index: 1, content_index: 0, part: { type: 'output_text', annotations: [], text: ' world' }, }, ]); const onStartMock = vi.fn(); const onTextMock = vi.fn(); const onCompletionMock = vi.fn(); const protocolStream = OpenAIResponsesStream(mockOpenAIStream, { callbacks: { onStart: onStartMock, onText: onTextMock, onCompletion: onCompletionMock, }, }); const chunks = await readStreamChunk(protocolStream); expect(chunks).toMatchSnapshot(); expect(onStartMock).toHaveBeenCalledTimes(1); expect(onCompletionMock).toHaveBeenCalledTimes(1); }); describe('Reasoning', () => { it('summary', async () => { const mockOpenAIStream = createReadableStream([ { type: 'response.created', response: { id: 'resp_684313b89200819087f27686e0c822260b502bf083132d0d', object: 'response', created_at: 1749226424, status: 'in_progress', error: null, incomplete_details: null, instructions: null, max_output_tokens: null, model: 'o4-mini', output: [], parallel_tool_calls: true, previous_response_id: null, reasoning: { effort: 'medium', summary: 'detailed' }, service_tier: 'auto', store: false, temperature: 1, text: { format: { type: 'text' } }, tool_choice: 'auto', tools: [ { type: 'function', description: 'a search service. Useful for when you need to answer questions about current events. Input should be a search query. Output is a JSON array of the query results', name: 'lobe-web-browsing____search____builtin', parameters: { properties: { query: { description: 'The search query', type: 'string' }, searchCategories: { description: 'The search categories you can set:', items: { enum: ['general', 'images', 'news', 'science', 'videos'], type: 'string', }, type: 'array', }, searchEngines: { description: 'The search engines you can use:', items: { enum: [ 'google', 'bilibili', 'bing', 'duckduckgo', 'npm', 'pypi', 'github', 'arxiv', 'google scholar', 'z-library', 'reddit', 'imdb', 'brave', 'wikipedia', 'pinterest', 'unsplash', 'vimeo', 'youtube', ], type: 'string', }, type: 'array', }, searchTimeRange: { description: 'The time range you can set:', enum: ['anytime', 'day', 'week', 'month', 'year'], type: 'string', }, }, required: ['query'], type: 'object', }, strict: true, }, { type: 'function', description: 'A crawler can visit page content. Output is a JSON object of title, content, url and website', name: 'lobe-web-browsing____crawlSinglePage____builtin', parameters: { properties: { url: { description: 'The url need to be crawled', type: 'string' }, }, required: ['url'], type: 'object', }, strict: true, }, { type: 'function', description: 'A crawler can visit multi pages. If need to visit multi website, use this one. Output is an array of JSON object of title, content, url and website', name: 'lobe-web-browsing____crawlMultiPages____builtin', parameters: { properties: { urls: { items: { description: 'The urls need to be crawled', type: 'string' }, type: 'array', }, }, required: ['urls'], type: 'object', }, strict: true, }, ], top_p: 1, truncation: 'disabled', usage: null, user: null, metadata: {}, }, }, { type: 'response.in_progress', response: { id: 'resp_684313b89200819087f27686e0c822260b502bf083132d0d', object: 'response', created_at: 1749226424, status: 'in_progress', error: null, incomplete_details: null, instructions: null, max_output_tokens: null, model: 'o4-mini', output: [], parallel_tool_calls: true, previous_response_id: null, reasoning: { effort: 'medium', summary: 'detailed' }, service_tier: 'auto', store: false, temperature: 1, text: { format: { type: 'text' } }, tool_choice: 'auto', tools: [ { type: 'function', description: 'a search service. Useful for when you need to answer questions about current events. Input should be a search query. Output is a JSON array of the query results', name: 'lobe-web-browsing____search____builtin', parameters: { properties: { query: { description: 'The search query', type: 'string' }, searchCategories: { description: 'The search categories you can set:', items: { enum: ['general', 'images', 'news', 'science', 'videos'], type: 'string', }, type: 'array', }, searchEngines: { description: 'The search engines you can use:', items: { enum: [ 'google', 'bilibili', 'bing', 'duckduckgo', 'npm', 'pypi', 'github', 'arxiv', 'google scholar', 'z-library', 'reddit', 'imdb', 'brave', 'wikipedia', 'pinterest', 'unsplash', 'vimeo', 'youtube', ], type: 'string', }, type: 'array', }, searchTimeRange: { description: 'The time range you can set:', enum: ['anytime', 'day', 'week', 'month', 'year'], type: 'string', }, }, required: ['query'], type: 'object', }, strict: true, }, { type: 'function', description: 'A crawler can visit page content. Output is a JSON object of title, content, url and website', name: 'lobe-web-browsing____crawlSinglePage____builtin', parameters: { properties: { url: { description: 'The url need to be crawled', type: 'string' }, }, required: ['url'], type: 'object', }, strict: true, }, { type: 'function', description: 'A crawler can visit multi pages. If need to visit multi website, use this one. Output is an array of JSON object of title, content, url and website', name: 'lobe-web-browsing____crawlMultiPages____builtin', parameters: { properties: { urls: { items: { description: 'The urls need to be crawled', type: 'string' }, type: 'array', }, }, required: ['urls'], type: 'object', }, strict: true, }, ], top_p: 1, truncation: 'disabled', usage: null, user: null, metadata: {}, }, }, { type: 'response.output_item.added', output_index: 0, item: { id: 'rs_684313b9774481908ee856625f82fb8c0b502bf083132d0d', type: 'reasoning', summary: [], }, }, { type: 'response.reasoning_summary_part.added', item_id: 'rs_684313b9774481908ee856625f82fb8c0b502bf083132d0d', output_index: 0, summary_index: 0, part: { type: 'summary_text', text: '' }, }, { type: 'response.reasoning_summary_text.delta', item_id: 'rs_684313b9774481908ee856625f82fb8c0b502bf083132d0d', output_index: 0, summary_index: 0, delta: '**Answering a', }, { type: 'response.reasoning_summary_text.delta', item_id: 'rs_684313b9774481908ee856625f82fb8c0b502bf083132d0d', output_index: 0, summary_index: 0, delta: ' numeric or 9.92', }, { type: 'response.reasoning_summary_text.delta', item_id: 'rs_684313b9774481908ee856625f82fb8c0b502bf083132d0d', output_index: 0, summary_index: 0, delta: '.', }, { type: 'response.reasoning_summary_text.done', item_id: 'rs_684313b9774481908ee856625f82fb8c0b502bf083132d0d', output_index: 0, summary_index: 0, text: '**Answering a numeric comparison**\n\nThe user is asking in Chinese which number is larger: 9.1 or 9.92. This is straightforward since 9.92 is clearly larger, as it\'s greater than 9.1. We can respond with "9.92大于9.1" without needing to search for more information. It\'s a simple comparison, but Iould also add a little explanation, noting that 9.92 is indeed 0.82 more than 9.1. However, keeping it simple with "9.92 > 9.1" is perfectly fine!', }, { type: 'response.reasoning_summary_part.done', item_id: 'rs_684313b9774481908ee856625f82fb8c0b502bf083132d0d', output_index: 0, summary_index: 0, part: { type: 'summary_text', text: '**Answering a numeric comparison**\n\nThe user is asking in Chinese which number is larger: 9.1 or 9.92. This is straightforward since 9.92 is clearly larger, as it\'s greater than 9.1. We can respond with "9.92大于9.1" without needing to search for more information. Is a simple comparison, but I could also add a little explanation, noting that 9.92 is indeed 0.82 more than 9.1. However, keeping it simple with "9.92 > 9.1" is perfectly fine!', }, }, { type: 'response.reasoning_summary_part.added', item_id: 'rs_6843fe13e73c8190a49d9372ef8cd46f08c019075e7c8955', output_index: 0, summary_index: 1, part: { type: 'summary_text', text: '' }, }, { type: 'response.reasoning_summary_text.delta', item_id: 'rs_6843fe13e73c8190a49d9372ef8cd46f08c019075e7c8955', output_index: 0, summary_index: 1, delta: '**Exploring a mathematical sequence**', }, { type: 'response.reasoning_summary_text.delta', item_id: 'rs_6843fe13e73c8190a49d9372ef8cd46f08c019075e7c8955', output_index: 0, summary_index: 1, delta: ' analyzing', }, { type: 'response.output_item.done', output_index: 0, item: { id: 'rs_684313b9774481908ee856625f82fb8c0b502bf083132d0d', type: 'reasoning', summary: [ { type: 'summary_text', text: '**Answering a numeric comparison**\n\nThe user is asking in Chinese which number is larger: 9.1 or 9.92. This is straightforward since 9.92 is clearly larger, as it\'s greater than 9.1. We can respond with "9.92大于9.1" without needing to search for more information. It\'s simple comparison, but I could also add a little explanation, noting that 9.92 is indeed 0.82 more than 9.1. However, keeping it simple with "9.92 > 9.1" is perfectly fine!', }, ], }, }, { type: 'response.output_item.added', output_index: 1, item: { id: 'msg_684313bee2c88190b0f4b09621ad7dc60b502bf083132d0d', type: 'message', status: 'in_progress', content: [], role: 'assistant', }, }, { type: 'response.content_part.added', item_id: 'msg_684313bee2c88190b0f4b09621ad7dc60b502bf083132d0d', output_index: 1, content_index: 0, part: { type: 'output_text', annotations: [], text: '' }, }, { type: 'response.output_text.delta', item_id: 'msg_684313bee2c88190b0f4b09621ad7dc60b502bf083132d0d', output_index: 1, content_index: 0, delta: '9.92 比 9.1 大。', }, { type: 'response.output_text.done', item_id: 'msg_684313bee2c88190b0f4b09621ad7dc60b502bf083132d0d', output_index: 1, content_index: 0, text: '9.92 比 9.1 大。', }, { type: 'response.content_part.done', item_id: 'msg_684313bee2c88190b0f4b09621ad7dc60b502bf083132d0d', output_index: 1, content_index: 0, part: { type: 'output_text', annotations: [], text: '9.92 比 9.1 大。' }, }, { type: 'response.output_item.done', output_index: 1, item: { id: 'msg_684313bee2c88190b0f4b09621ad7dc60b502bf083132d0d', type: 'message', status: 'completed', content: [{ type: 'output_text', annotations: [], text: '9.92 比 9. 大。' }], role: 'assistant', }, }, { type: 'response.completed', response: { id: 'resp_684313b89200819087f27686e0c822260b502bf083132d0d', object: 'response', created_at: 1749226424, status: 'completed', error: null, incomplete_details: null, instructions: null, max_output_tokens: null, model: 'o4-mini', output: [ { id: 'rs_684313b9774481908ee856625f82fb8c0b502bf083132d0d', type: 'reasoning', summary: [ { type: 'summary_text', text: '**Answering a numeric comparison**\n\nThe user is asking in Chinese which number is larger: 9.1 or 9.92. This is straightforward since 9.92 is clearly larger, as it\'s greater than 9.1. We can respond with "9.92大于9.1" without needing to search for more information. It\'s a simplcomparison, but I could also add a little explanation, noting that 9.92 is indeed 0.82 more than 9.1. However, keeping it simple with "9.92 > 9.1" is perfectly fine!', }, ], }, { id: 'msg_684313bee2c88190b0f4b09621ad7dc60b502bf083132d0d', type: 'message', status: 'completed', content: [{ type: 'output_text', annotations: [], text: '9.92 比 9.1 大。' }], role: 'assistant', }, ], parallel_tool_calls: true, previous_response_id: null, reasoning: { effort: 'medium', summary: 'detailed' }, service_tier: 'default', store: false, temperature: 1, text: { format: { type: 'text' } }, tool_choice: 'auto', tools: [ { type: 'function', description: 'a search service. Useful for when you need to answer questions about current events. Input should be a search query. Output is a JSON array of the query results', name: 'lobe-web-browsing____search____builtin', parameters: { properties: { query: { description: 'The search query', type: 'string' }, searchCategories: { description: 'The search categories you can set:', items: { enum: ['general', 'images', 'news', 'science', 'videos'], type: 'string', }, type: 'array', }, searchEngines: { description: 'The search engines you can use:', items: { enum: [ 'google', 'bilibili', 'bing', 'duckduckgo', 'npm', 'pypi', 'github', 'arxiv', 'google scholar', 'z-library', 'reddit', 'imdb', 'brave', 'wikipedia', 'pinterest', 'unsplash', 'vimeo', 'youtube', ], type: 'string', }, type: 'array', }, searchTimeRange: { description: 'The time range you can set:', enum: ['anytime', 'day', 'week', 'month', 'year'], type: 'string', }, }, required: ['query'], type: 'object', }, strict: true, }, { type: 'function', description: 'A crawler can visit page content. Output is a JSON object of title, content, url and website', name: 'lobe-web-browsing____crawlSinglePage____builtin', parameters: { properties: { url: { description: 'The url need to be crawled', type: 'string' }, }, required: ['url'], type: 'object', }, strict: true, }, { type: 'function', description: 'A crawler can visit multi pages. If need to visit multi website, use this one. Output is an array of JSON object of title, content, url and website', name: 'lobe-web-browsing____crawlMultiPages____builtin', parameters: { properties: { urls: { items: { description: 'The urls need to be crawled', type: 'string' }, type: 'array', }, }, required: ['urls'], type: 'object', }, strict: true, }, ], top_p: 1, truncation: 'disabled', usage: { input_tokens: 2391, input_tokens_details: { cached_tokens: 2298 }, output_tokens: 144, output_tokens_details: { reasoning_tokens: 128 }, total_tokens: 2535, }, user: null, metadata: {}, }, }, ]); const onStartMock = vi.fn(); const onTextMock = vi.fn(); const onCompletionMock = vi.fn(); const protocolStream = OpenAIResponsesStream(mockOpenAIStream, { callbacks: { onStart: onStartMock, onText: onTextMock, onCompletion: onCompletionMock, }, }); const chunks = await readStreamChunk(protocolStream); expect(chunks).toMatchSnapshot(); expect(onStartMock).toHaveBeenCalledTimes(1); expect(onCompletionMock).toHaveBeenCalledTimes(1); }); }); });