@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.
310 lines (257 loc) • 9.58 kB
text/typescript
// @vitest-environment node
import { ModelProvider } from 'model-bank';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { testProvider } from '../../providerTestUtils';
import { LobeQiniuAI, params } from './index';
const provider = ModelProvider.Qiniu;
const defaultBaseURL = 'https://openai.qiniu.com/v1';
testProvider({
Runtime: LobeQiniuAI,
provider,
defaultBaseURL,
chatDebugEnv: 'DEBUG_QINIU_CHAT_COMPLETION',
chatModel: 'deepseek-r1',
invalidErrorType: 'InvalidProviderAPIKey',
bizErrorType: 'ProviderBizError',
test: {
skipAPICall: true,
skipErrorHandle: true,
},
});
describe('LobeQiniuAI - custom features', () => {
let instance: InstanceType<typeof LobeQiniuAI>;
beforeEach(() => {
instance = new LobeQiniuAI({ apiKey: 'test_api_key' });
vi.spyOn(instance['client'].chat.completions, 'create').mockResolvedValue(
new ReadableStream() as any,
);
});
describe('params.debug', () => {
it('should disable debug mode by default', () => {
delete process.env.DEBUG_QINIU_CHAT_COMPLETION;
const result = params.debug.chatCompletion();
expect(result).toBe(false);
});
it('should enable debug mode when DEBUG_QINIU_CHAT_COMPLETION is set to 1', () => {
process.env.DEBUG_QINIU_CHAT_COMPLETION = '1';
const result = params.debug.chatCompletion();
expect(result).toBe(true);
delete process.env.DEBUG_QINIU_CHAT_COMPLETION;
});
it('should disable debug mode when DEBUG_QINIU_CHAT_COMPLETION is not 1', () => {
process.env.DEBUG_QINIU_CHAT_COMPLETION = '0';
const result = params.debug.chatCompletion();
expect(result).toBe(false);
delete process.env.DEBUG_QINIU_CHAT_COMPLETION;
});
});
describe('params.models', () => {
it('should fetch and process models successfully', async () => {
const mockClient = {
models: {
list: vi.fn().mockResolvedValue({
data: [{ id: 'gpt-4' }, { id: 'claude-3-opus' }, { id: 'gemini-pro' }],
}),
},
};
const models = await params.models!({ client: mockClient as any });
expect(mockClient.models.list).toHaveBeenCalled();
expect(Array.isArray(models)).toBe(true);
});
it('should use processMultiProviderModelList to parse models', async () => {
const mockClient = {
models: {
list: vi.fn().mockResolvedValue({
data: [{ id: 'gpt-4' }, { id: 'claude-3-opus' }],
}),
},
};
const models = await params.models!({ client: mockClient as any });
// processMultiProviderModelList should return valid model cards
expect(models.length).toBeGreaterThan(0);
models.forEach((model) => {
expect(model).toHaveProperty('id');
});
});
it('should handle empty model list', async () => {
const mockClient = {
models: {
list: vi.fn().mockResolvedValue({
data: [],
}),
},
};
const models = await params.models!({ client: mockClient as any });
expect(models).toEqual([]);
});
it('should handle API errors gracefully', async () => {
const mockClient = {
models: {
list: vi.fn().mockRejectedValue(new Error('API Error')),
},
};
await expect(params.models!({ client: mockClient as any })).rejects.toThrow('API Error');
});
it('should pass qiniu as the provider name to processMultiProviderModelList', async () => {
const mockClient = {
models: {
list: vi.fn().mockResolvedValue({
data: [{ id: 'test-model' }],
}),
},
};
// The function should internally call processMultiProviderModelList with 'qiniu' as second param
const models = await params.models!({ client: mockClient as any });
// Verify that the models are processed (non-empty if valid models exist)
expect(Array.isArray(models)).toBe(true);
});
it('should handle models with OpenAI provider', async () => {
const mockClient = {
models: {
list: vi.fn().mockResolvedValue({
data: [{ id: 'gpt-4' }, { id: 'gpt-3.5-turbo' }],
}),
},
};
const models = await params.models!({ client: mockClient as any });
expect(models.length).toBeGreaterThan(0);
// Should detect OpenAI models and include them
const gpt4 = models.find((m) => m.id === 'gpt-4');
expect(gpt4).toBeDefined();
});
it('should handle models with Anthropic provider', async () => {
const mockClient = {
models: {
list: vi.fn().mockResolvedValue({
data: [{ id: 'claude-3-opus' }, { id: 'claude-3-sonnet' }],
}),
},
};
const models = await params.models!({ client: mockClient as any });
expect(models.length).toBeGreaterThan(0);
// Should detect Anthropic models and include them
const claude = models.find((m) => m.id === 'claude-3-opus');
expect(claude).toBeDefined();
});
it('should handle models with Google provider', async () => {
const mockClient = {
models: {
list: vi.fn().mockResolvedValue({
data: [{ id: 'gemini-pro' }, { id: 'gemini-1.5-pro' }],
}),
},
};
const models = await params.models!({ client: mockClient as any });
expect(models.length).toBeGreaterThan(0);
// Should detect Google models and include them
const gemini = models.find((m) => m.id === 'gemini-pro');
expect(gemini).toBeDefined();
});
it('should handle mixed provider models', async () => {
const mockClient = {
models: {
list: vi.fn().mockResolvedValue({
data: [
{ id: 'gpt-4' },
{ id: 'claude-3-opus' },
{ id: 'gemini-pro' },
{ id: 'deepseek-chat' },
],
}),
},
};
const models = await params.models!({ client: mockClient as any });
expect(models.length).toBeGreaterThan(0);
// Should detect and include models from multiple providers
expect(models.some((m) => m.id === 'gpt-4')).toBe(true);
expect(models.some((m) => m.id === 'claude-3-opus')).toBe(true);
expect(models.some((m) => m.id === 'gemini-pro')).toBe(true);
});
it('should handle models with only id property', async () => {
const mockClient = {
models: {
list: vi.fn().mockResolvedValue({
data: [{ id: 'model-1' }, { id: 'model-2' }],
}),
},
};
const models = await params.models!({ client: mockClient as any });
expect(Array.isArray(models)).toBe(true);
});
it('should handle network timeout errors', async () => {
const mockClient = {
models: {
list: vi.fn().mockRejectedValue(new Error('Network timeout')),
},
};
await expect(params.models!({ client: mockClient as any })).rejects.toThrow(
'Network timeout',
);
});
it('should handle invalid API response format', async () => {
const mockClient = {
models: {
list: vi.fn().mockResolvedValue({
data: null,
}),
},
};
// Should throw or handle gracefully when data is null
await expect(async () => {
await params.models!({ client: mockClient as any });
}).rejects.toThrow();
});
it('should handle missing data property in response', async () => {
const mockClient = {
models: {
list: vi.fn().mockResolvedValue({}),
},
};
// Should throw or handle gracefully when data property is missing
await expect(async () => {
await params.models!({ client: mockClient as any });
}).rejects.toThrow();
});
it('should handle models with various DeepSeek variants', async () => {
const mockClient = {
models: {
list: vi.fn().mockResolvedValue({
data: [{ id: 'deepseek-chat' }, { id: 'deepseek-coder' }, { id: 'deepseek-r1' }],
}),
},
};
const models = await params.models!({ client: mockClient as any });
expect(models.length).toBeGreaterThan(0);
});
});
describe('exports', () => {
it('should export params object', () => {
expect(params).toBeDefined();
expect(params.provider).toBe(ModelProvider.Qiniu);
expect(params.baseURL).toBe('https://openai.qiniu.com/v1');
});
it('should export LobeQiniuAI class', () => {
expect(LobeQiniuAI).toBeDefined();
expect(typeof LobeQiniuAI).toBe('function');
});
it('should export params with all required properties', () => {
expect(params).toHaveProperty('provider');
expect(params).toHaveProperty('baseURL');
expect(params).toHaveProperty('apiKey');
expect(params).toHaveProperty('debug');
expect(params).toHaveProperty('models');
});
it('should have debug.chatCompletion function', () => {
expect(params.debug).toBeDefined();
expect(params.debug.chatCompletion).toBeDefined();
expect(typeof params.debug.chatCompletion).toBe('function');
});
it('should have models function', () => {
expect(params.models).toBeDefined();
expect(typeof params.models).toBe('function');
});
it('should have correct apiKey placeholder', () => {
expect(params.apiKey).toBe('placeholder-to-avoid-error');
});
});
});