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.

442 lines (363 loc) 13.6 kB
import OpenAI from 'openai'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { imageUrlToBase64 } from '@/utils/imageToBase64'; import { convertImageUrlToFile, convertMessageContent, convertOpenAIMessages, convertOpenAIResponseInputs, } from './openaiHelpers'; import { parseDataUri } from './uriParser'; // 模拟依赖 vi.mock('@/utils/imageToBase64'); vi.mock('./uriParser'); describe('convertMessageContent', () => { beforeEach(() => { vi.resetAllMocks(); }); afterEach(() => { vi.restoreAllMocks(); }); it('should return the same content if not image_url type', async () => { const content = { type: 'text', text: 'Hello' } as OpenAI.ChatCompletionContentPart; const result = await convertMessageContent(content); expect(result).toEqual(content); }); it('should convert image URL to base64 when necessary', async () => { // 设置环境变量 process.env.LLM_VISION_IMAGE_USE_BASE64 = '1'; const content = { type: 'image_url', image_url: { url: 'https://example.com/image.jpg' }, } as OpenAI.ChatCompletionContentPart; vi.mocked(parseDataUri).mockReturnValue({ type: 'url', base64: null, mimeType: null }); vi.mocked(imageUrlToBase64).mockResolvedValue({ base64: 'base64String', mimeType: 'image/jpeg', }); const result = await convertMessageContent(content); expect(result).toEqual({ type: 'image_url', image_url: { url: 'data:image/jpeg;base64,base64String' }, }); expect(parseDataUri).toHaveBeenCalledWith('https://example.com/image.jpg'); expect(imageUrlToBase64).toHaveBeenCalledWith('https://example.com/image.jpg'); }); it('should not convert image URL when not necessary', async () => { process.env.LLM_VISION_IMAGE_USE_BASE64 = undefined; const content = { type: 'image_url', image_url: { url: 'https://example.com/image.jpg' }, } as OpenAI.ChatCompletionContentPart; vi.mocked(parseDataUri).mockReturnValue({ type: 'url', base64: null, mimeType: null }); const result = await convertMessageContent(content); expect(result).toEqual(content); expect(imageUrlToBase64).not.toHaveBeenCalled(); }); }); describe('convertOpenAIMessages', () => { it('should convert string content messages', async () => { const messages = [ { role: 'user', content: 'Hello' }, { role: 'assistant', content: 'Hi there' }, ] as OpenAI.ChatCompletionMessageParam[]; const result = await convertOpenAIMessages(messages); expect(result).toEqual(messages); }); it('should convert array content messages', async () => { const messages = [ { role: 'user', content: [ { type: 'text', text: 'Hello' }, { type: 'image_url', image_url: { url: 'https://example.com/image.jpg' } }, ], }, ] as OpenAI.ChatCompletionMessageParam[]; vi.spyOn(Promise, 'all'); vi.mocked(parseDataUri).mockReturnValue({ type: 'url', base64: null, mimeType: null }); vi.mocked(imageUrlToBase64).mockResolvedValue({ base64: 'base64String', mimeType: 'image/jpeg', }); process.env.LLM_VISION_IMAGE_USE_BASE64 = '1'; const result = await convertOpenAIMessages(messages); expect(result).toEqual([ { role: 'user', content: [ { type: 'text', text: 'Hello' }, { type: 'image_url', image_url: { url: 'data:image/jpeg;base64,base64String' }, }, ], }, ]); expect(Promise.all).toHaveBeenCalledTimes(2); // 一次用于消息数组,一次用于内容数组 process.env.LLM_VISION_IMAGE_USE_BASE64 = undefined; }); it('should convert array content messages', async () => { const messages = [ { role: 'user', content: [ { type: 'text', text: 'Hello' }, { type: 'image_url', image_url: { url: 'https://example.com/image.jpg' } }, ], }, ] as OpenAI.ChatCompletionMessageParam[]; vi.spyOn(Promise, 'all'); vi.mocked(parseDataUri).mockReturnValue({ type: 'url', base64: null, mimeType: null }); vi.mocked(imageUrlToBase64).mockResolvedValue({ base64: 'base64String', mimeType: 'image/jpeg', }); const result = await convertOpenAIMessages(messages); expect(result).toEqual(messages); expect(Promise.all).toHaveBeenCalledTimes(2); // 一次用于消息数组,一次用于内容数组 }); }); describe('convertOpenAIResponseInputs', () => { it('应该正确转换普通文本消息', async () => { const messages: OpenAI.ChatCompletionMessageParam[] = [ { role: 'user', content: 'Hello' }, { role: 'assistant', content: 'Hi there!' }, ]; const result = await convertOpenAIResponseInputs(messages); expect(result).toEqual([ { role: 'user', content: 'Hello' }, { role: 'assistant', content: 'Hi there!' }, ]); }); it('应该正确转换带有工具调用的消息', async () => { const messages: OpenAI.ChatCompletionMessageParam[] = [ { role: 'assistant', content: '', tool_calls: [ { id: 'call_123', type: 'function', function: { name: 'test_function', arguments: '{"key": "value"}', }, }, ], }, ]; const result = await convertOpenAIResponseInputs(messages); expect(result).toEqual([ { arguments: 'test_function', call_id: 'call_123', name: 'test_function', type: 'function_call', }, ]); }); it('应该正确转换工具响应消息', async () => { const messages: OpenAI.ChatCompletionMessageParam[] = [ { role: 'tool', content: 'Function result', tool_call_id: 'call_123', }, ]; const result = await convertOpenAIResponseInputs(messages); expect(result).toEqual([ { call_id: 'call_123', output: 'Function result', type: 'function_call_output', }, ]); }); it('应该正确转换包含图片的消息', async () => { const messages: OpenAI.ChatCompletionMessageParam[] = [ { role: 'user', content: [ { type: 'text', text: 'Here is an image' }, { type: 'image_url', image_url: { url: 'data:image/jpeg;base64,test123', }, }, ], }, ]; const result = await convertOpenAIResponseInputs(messages); expect(result).toEqual([ { role: 'user', content: [ { type: 'input_text', text: 'Here is an image' }, { type: 'input_image', image_url: 'data:image/jpeg;base64,test123', }, ], }, ]); }); it('应该正确处理混合类型的消息序列', async () => { const messages: OpenAI.ChatCompletionMessageParam[] = [ { role: 'user', content: 'I need help with a function' }, { role: 'assistant', content: '', tool_calls: [ { id: 'call_456', type: 'function', function: { name: 'get_data', arguments: '{}', }, }, ], }, { role: 'tool', content: '{"result": "success"}', tool_call_id: 'call_456', }, ]; const result = await convertOpenAIResponseInputs(messages); expect(result).toEqual([ { role: 'user', content: 'I need help with a function' }, { arguments: 'get_data', call_id: 'call_456', name: 'get_data', type: 'function_call', }, { call_id: 'call_456', output: '{"result": "success"}', type: 'function_call_output', }, ]); }); }); describe('convertImageUrlToFile', () => { beforeEach(() => { vi.resetAllMocks(); }); afterEach(() => { vi.restoreAllMocks(); }); describe('Data URL handling', () => { it('should convert PNG data URL to File object correctly', async () => { const base64Data = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='; const dataUrl = `data:image/png;base64,${base64Data}`; const result = await convertImageUrlToFile(dataUrl); expect(result).toBeDefined(); expect(result).toHaveProperty('name', 'image.png'); expect(result).toHaveProperty('type', 'image/png'); expect(result).toHaveProperty('size'); expect(result.size).toBeGreaterThan(0); }); it('should convert JPEG data URL to File object correctly', async () => { const base64Data = '/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwA9BQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=='; const dataUrl = `data:image/jpeg;base64,${base64Data}`; const result = await convertImageUrlToFile(dataUrl); expect(result).toBeDefined(); expect(result).toHaveProperty('name', 'image.jpeg'); expect(result).toHaveProperty('type', 'image/jpeg'); expect(result).toHaveProperty('size'); expect(result.size).toBeGreaterThan(0); }); it('should convert WebP data URL to File object correctly', async () => { const base64Data = 'UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoBAAEAAAAAJaQAA6g='; const dataUrl = `data:image/webp;base64,${base64Data}`; const result = await convertImageUrlToFile(dataUrl); expect(result).toBeDefined(); expect(result).toHaveProperty('name', 'image.webp'); expect(result).toHaveProperty('type', 'image/webp'); expect(result).toHaveProperty('size'); expect(result.size).toBeGreaterThan(0); }); }); describe('HTTP URL handling', () => { const mockFetch = vi.fn(); beforeEach(() => { // Mock global fetch using vi.stubGlobal for better isolation vi.stubGlobal('fetch', mockFetch); }); afterEach(() => { vi.unstubAllGlobals(); vi.clearAllMocks(); }); it('should convert HTTP URL to File object correctly', async () => { const mockArrayBuffer = new ArrayBuffer(8); const mockHeaders = new Headers(); mockHeaders.set('content-type', 'image/jpeg'); mockFetch.mockResolvedValue({ ok: true, arrayBuffer: () => Promise.resolve(mockArrayBuffer), headers: mockHeaders, } satisfies Partial<Response>); const result = await convertImageUrlToFile('https://example.com/image.jpg'); expect(mockFetch).toHaveBeenCalledWith('https://example.com/image.jpg'); expect(result).toBeDefined(); expect(result).toHaveProperty('name', 'image.jpeg'); expect(result).toHaveProperty('type', 'image/jpeg'); expect(result).toHaveProperty('size'); expect(result.size).toEqual(8); }); it('should handle different content types from HTTP response headers', async () => { const testCases = [ { contentType: 'image/jpeg', expectedExtension: 'jpeg' }, { contentType: 'image/png', expectedExtension: 'png' }, { contentType: 'image/webp', expectedExtension: 'webp' }, { contentType: null, expectedExtension: 'png' }, // default fallback ]; for (const testCase of testCases) { const mockArrayBuffer = new ArrayBuffer(8); const mockHeaders = new Headers(); if (testCase.contentType) { mockHeaders.set('content-type', testCase.contentType); } mockFetch.mockResolvedValue({ ok: true, arrayBuffer: () => Promise.resolve(mockArrayBuffer), headers: mockHeaders, } satisfies Partial<Response>); const result = await convertImageUrlToFile('https://example.com/image.jpg'); expect(result).toHaveProperty('name', `image.${testCase.expectedExtension}`); expect(result).toHaveProperty('type', testCase.contentType || 'image/png'); vi.clearAllMocks(); } }); it('should throw error when HTTP request fails', async () => { mockFetch.mockResolvedValue({ ok: false, statusText: 'Not Found', } satisfies Partial<Response>); await expect(convertImageUrlToFile('https://example.com/nonexistent.jpg')).rejects.toThrow( 'Failed to fetch image from https://example.com/nonexistent.jpg: Not Found', ); expect(mockFetch).toHaveBeenCalledWith('https://example.com/nonexistent.jpg'); }); it('should throw error when network request fails', async () => { mockFetch.mockRejectedValue(new Error('Network error')); await expect(convertImageUrlToFile('https://example.com/image.jpg')).rejects.toThrow( 'Network error', ); expect(mockFetch).toHaveBeenCalledWith('https://example.com/image.jpg'); }); }); describe('Edge cases', () => { it('should handle malformed data URL gracefully', async () => { const malformedDataUrl = 'data:invalid-format'; // 这个测试可能会抛出错误,我们需要适当处理 await expect(convertImageUrlToFile(malformedDataUrl)).rejects.toThrow(); }); }); });