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.

450 lines (384 loc) • 13.7 kB
// @vitest-environment node import { ModelProvider } from 'model-bank'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { testProvider } from '../../providerTestUtils'; import { LobeCometAPIAI, params } from './index'; // Basic provider tests testProvider({ Runtime: LobeCometAPIAI, chatDebugEnv: 'DEBUG_COMETAPI_COMPLETION', chatModel: 'gpt-3.5-turbo', defaultBaseURL: 'https://api.cometapi.com/v1', provider: ModelProvider.CometAPI, test: { skipAPICall: true, }, }); // Custom feature tests describe('LobeCometAPIAI - custom features', () => { let instance: InstanceType<typeof LobeCometAPIAI>; beforeEach(() => { instance = new LobeCometAPIAI({ apiKey: 'test_api_key' }); vi.spyOn(instance['client'].chat.completions, 'create').mockResolvedValue( new ReadableStream() as any, ); }); describe('params object', () => { it('should export params with correct baseURL', () => { expect(params.baseURL).toBe('https://api.cometapi.com/v1'); }); it('should have correct provider', () => { expect(params.provider).toBe(ModelProvider.CometAPI); }); }); describe('debug configuration', () => { it('should disable debug by default', () => { delete process.env.DEBUG_COMETAPI_COMPLETION; const result = params.debug.chatCompletion(); expect(result).toBe(false); }); it('should enable debug when env is set', () => { process.env.DEBUG_COMETAPI_COMPLETION = '1'; const result = params.debug.chatCompletion(); expect(result).toBe(true); delete process.env.DEBUG_COMETAPI_COMPLETION; }); }); describe('handlePayload', () => { it('should force streaming to true', () => { const payload = { messages: [{ content: 'Hello', role: 'user' as const }], model: 'gpt-3.5-turbo', stream: false, }; const result = params.chatCompletion.handlePayload!(payload); expect(result.stream).toBe(true); }); it('should preserve streaming when already true', () => { const payload = { messages: [{ content: 'Hello', role: 'user' as const }], model: 'gpt-3.5-turbo', stream: true, }; const result = params.chatCompletion.handlePayload!(payload); expect(result.stream).toBe(true); }); it('should preserve model parameter', () => { const payload = { messages: [{ content: 'Hello', role: 'user' as const }], model: 'gpt-4', stream: false, }; const result = params.chatCompletion.handlePayload!(payload); expect(result.model).toBe('gpt-4'); }); it('should preserve messages parameter', () => { const messages = [ { content: 'You are a helpful assistant', role: 'system' as const }, { content: 'Hello', role: 'user' as const }, ]; const payload = { messages, model: 'gpt-3.5-turbo', stream: false, }; const result = params.chatCompletion.handlePayload!(payload); expect(result.messages).toEqual(messages); }); it('should preserve other payload properties', () => { const payload = { max_tokens: 100, messages: [{ content: 'Hello', role: 'user' as const }], model: 'gpt-3.5-turbo', stream: false, temperature: 0.7, top_p: 0.9, }; const result = params.chatCompletion.handlePayload!(payload); expect(result.model).toBe('gpt-3.5-turbo'); expect(result.messages).toEqual(payload.messages); expect(result.temperature).toBe(0.7); expect(result.max_tokens).toBe(100); expect(result.top_p).toBe(0.9); }); it('should handle payload with tools', () => { const payload = { messages: [{ content: 'Hello', role: 'user' as const }], model: 'gpt-4', stream: false, tools: [ { function: { description: 'test function', name: 'test', }, type: 'function' as const, }, ], }; const result = params.chatCompletion.handlePayload!(payload); expect(result.stream).toBe(true); expect(result.tools).toEqual(payload.tools); }); it('should handle empty tools array', () => { const payload = { messages: [{ content: 'Hello', role: 'user' as const }], model: 'gpt-3.5-turbo', stream: false, tools: [], }; const result = params.chatCompletion.handlePayload!(payload); expect(result.stream).toBe(true); expect(result.tools).toEqual([]); }); it('should preserve frequency_penalty parameter', () => { const payload = { frequency_penalty: 0.5, messages: [{ content: 'Hello', role: 'user' as const }], model: 'gpt-3.5-turbo', stream: false, }; const result = params.chatCompletion.handlePayload!(payload); expect(result.frequency_penalty).toBe(0.5); }); it('should preserve presence_penalty parameter', () => { const payload = { messages: [{ content: 'Hello', role: 'user' as const }], model: 'gpt-3.5-turbo', presence_penalty: 0.3, stream: false, }; const result = params.chatCompletion.handlePayload!(payload); expect(result.presence_penalty).toBe(0.3); }); }); describe('models', () => { it('should fetch and process models', async () => { const mockClient = { apiKey: 'test', baseURL: 'https://api.cometapi.com/v1', models: { list: vi.fn().mockResolvedValue({ data: [ { id: 'gpt-3.5-turbo', object: 'model', owned_by: 'openai' }, { id: 'gpt-4', object: 'model', owned_by: 'openai' }, { id: 'claude-3', object: 'model', owned_by: 'anthropic' }, ], }), }, }; const models = await params.models!({ client: mockClient as any }); expect(models).toBeDefined(); expect(models.length).toBe(3); expect(mockClient.models.list).toHaveBeenCalled(); }); it('should map model fields correctly', async () => { const mockClient = { apiKey: 'test', baseURL: 'https://api.cometapi.com/v1', models: { list: vi.fn().mockResolvedValue({ data: [{ id: 'gpt-3.5-turbo', object: 'model', owned_by: 'openai' }], }), }, }; const models = await params.models!({ client: mockClient as any }); const model = models[0]; expect(model.id).toBe('gpt-3.5-turbo'); }); it('should filter unnecessary fields from raw model data', async () => { const mockClient = { apiKey: 'test', baseURL: 'https://api.cometapi.com/v1', models: { list: vi.fn().mockResolvedValue({ data: [ { created: 1_677_649_963, id: 'gpt-3.5-turbo', object: 'model', owned_by: 'openai', parent: null, permission: [], root: 'gpt-3.5-turbo', }, ], }), }, }; await params.models!({ client: mockClient as any }); // Should only include id, object, owned_by in the mapped list }); it('should handle empty model list', async () => { const mockClient = { apiKey: 'test', baseURL: 'https://api.cometapi.com/v1', models: { list: vi.fn().mockResolvedValue({ data: [], }), }, }; const models = await params.models!({ client: mockClient as any }); expect(models).toEqual([]); }); it('should handle missing data field', async () => { const mockClient = { apiKey: 'test', baseURL: 'https://api.cometapi.com/v1', models: { list: vi.fn().mockResolvedValue({}), }, }; const models = await params.models!({ client: mockClient as any }); expect(models).toEqual([]); }); it('should handle null data field', async () => { const mockClient = { apiKey: 'test', baseURL: 'https://api.cometapi.com/v1', models: { list: vi.fn().mockResolvedValue({ data: null, }), }, }; const models = await params.models!({ client: mockClient as any }); expect(models).toEqual([]); }); it('should handle API error gracefully', async () => { const mockClient = { apiKey: 'test', baseURL: 'https://api.cometapi.com/v1', models: { list: vi.fn().mockRejectedValue(new Error('API Error')), }, }; const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); const models = await params.models!({ client: mockClient as any }); expect(models).toEqual([]); expect(consoleWarnSpy).toHaveBeenCalledWith( 'Failed to fetch CometAPI models. Please ensure your CometAPI API key is valid:', expect.any(Error), ); consoleWarnSpy.mockRestore(); }); it('should handle network error', async () => { const mockClient = { apiKey: 'test', baseURL: 'https://api.cometapi.com/v1', models: { list: vi.fn().mockRejectedValue(new Error('Network Error')), }, }; const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); const models = await params.models!({ client: mockClient as any }); expect(models).toEqual([]); consoleWarnSpy.mockRestore(); }); it('should handle unauthorized error', async () => { const mockClient = { apiKey: 'test', baseURL: 'https://api.cometapi.com/v1', models: { list: vi.fn().mockRejectedValue(new Error('Unauthorized')), }, }; const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); const models = await params.models!({ client: mockClient as any }); expect(models).toEqual([]); consoleWarnSpy.mockRestore(); }); it('should process multi-provider model list', async () => { const mockClient = { apiKey: 'test', baseURL: 'https://api.cometapi.com/v1', models: { list: vi.fn().mockResolvedValue({ data: [ { id: 'gpt-4', object: 'model', owned_by: 'openai' }, { id: 'claude-3-opus', object: 'model', owned_by: 'anthropic' }, { id: 'gemini-pro', object: 'model', owned_by: 'google' }, ], }), }, }; const models = await params.models!({ client: mockClient as any }); expect(models.length).toBe(3); // processMultiProviderModelList should handle different providers }); it('should merge with known model data from model-bank', async () => { const mockClient = { apiKey: 'test', baseURL: 'https://api.cometapi.com/v1', models: { list: vi.fn().mockResolvedValue({ data: [{ id: 'gpt-3.5-turbo', object: 'model', owned_by: 'openai' }], }), }, }; const models = await params.models!({ client: mockClient as any }); const model = models[0]; // Should have properties from both API and model-bank expect(model.id).toBe('gpt-3.5-turbo'); }); it('should handle models not in model-bank', async () => { const mockClient = { apiKey: 'test', baseURL: 'https://api.cometapi.com/v1', models: { list: vi.fn().mockResolvedValue({ data: [{ id: 'custom-model', object: 'model', owned_by: 'custom' }], }), }, }; const models = await params.models!({ client: mockClient as any }); const model = models[0]; expect(model.id).toBe('custom-model'); }); it('should handle models with different owned_by values', async () => { const mockClient = { apiKey: 'test', baseURL: 'https://api.cometapi.com/v1', models: { list: vi.fn().mockResolvedValue({ data: [ { id: 'model-1', object: 'model', owned_by: 'openai' }, { id: 'model-2', object: 'model', owned_by: 'anthropic' }, { id: 'model-3', object: 'model', owned_by: 'google' }, { id: 'model-4', object: 'model', owned_by: 'custom' }, ], }), }, }; const models = await params.models!({ client: mockClient as any }); expect(models.length).toBe(4); }); it('should preserve object field in processed models', async () => { const mockClient = { apiKey: 'test', baseURL: 'https://api.cometapi.com/v1', models: { list: vi.fn().mockResolvedValue({ data: [{ id: 'gpt-3.5-turbo', object: 'model', owned_by: 'openai' }], }), }, }; await params.models!({ client: mockClient as any }); // The mapped model list should preserve the object field }); it('should call processMultiProviderModelList with correct arguments', async () => { const mockClient = { apiKey: 'test', baseURL: 'https://api.cometapi.com/v1', models: { list: vi.fn().mockResolvedValue({ data: [{ id: 'gpt-3.5-turbo', object: 'model', owned_by: 'openai' }], }), }, }; await params.models!({ client: mockClient as any }); // processMultiProviderModelList should be called with modelList and 'cometapi' }); }); });