@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.
412 lines (339 loc) • 12.5 kB
text/typescript
// @vitest-environment node
import { ModelProvider } from 'model-bank';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { testProvider } from '../../providerTestUtils';
import { LobeOllamaCloudAI, params } from './index';
// Basic provider tests
testProvider({
Runtime: LobeOllamaCloudAI,
bizErrorType: 'ProviderBizError',
chatDebugEnv: 'DEBUG_OLLAMA_CLOUD_CHAT_COMPLETION',
chatModel: 'llama3.2',
defaultBaseURL: 'https://ollama.com/v1',
invalidErrorType: 'InvalidProviderAPIKey',
provider: ModelProvider.OllamaCloud,
test: {
skipAPICall: true,
skipErrorHandle: true,
},
});
// Custom feature tests
describe('LobeOllamaCloudAI - custom features', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('params export', () => {
it('should export params object', () => {
expect(params).toBeDefined();
expect(params.provider).toBe(ModelProvider.OllamaCloud);
expect(params.baseURL).toBe('https://ollama.com/v1');
});
it('should have correct structure', () => {
expect(params).toHaveProperty('chatCompletion');
expect(params).toHaveProperty('debug');
expect(params).toHaveProperty('models');
expect(params.chatCompletion).toHaveProperty('handlePayload');
});
});
describe('debug configuration', () => {
it('should disable debug by default', () => {
delete process.env.DEBUG_OLLAMA_CLOUD_CHAT_COMPLETION;
const result = params.debug.chatCompletion();
expect(result).toBe(false);
});
it('should enable debug when env is set to 1', () => {
process.env.DEBUG_OLLAMA_CLOUD_CHAT_COMPLETION = '1';
const result = params.debug.chatCompletion();
expect(result).toBe(true);
});
it('should disable debug when env is set to other values', () => {
process.env.DEBUG_OLLAMA_CLOUD_CHAT_COMPLETION = '0';
const result = params.debug.chatCompletion();
expect(result).toBe(false);
});
it('should disable debug when env is empty string', () => {
process.env.DEBUG_OLLAMA_CLOUD_CHAT_COMPLETION = '';
const result = params.debug.chatCompletion();
expect(result).toBe(false);
});
});
describe('handlePayload', () => {
it('should preserve all payload properties', () => {
const payload = {
max_tokens: 100,
messages: [{ content: 'Hello', role: 'user' }],
model: 'llama3.2',
stream: true,
temperature: 0.7,
};
const result = params.chatCompletion.handlePayload(payload as any);
expect(result.model).toBe('llama3.2');
expect(result.messages).toEqual(payload.messages);
expect(result.temperature).toBe(0.7);
expect(result.max_tokens).toBe(100);
expect(result.stream).toBe(true);
});
it('should handle minimal payload', () => {
const payload = {
messages: [{ content: 'Hello', role: 'user' }],
model: 'llama3.2',
};
const result = params.chatCompletion.handlePayload(payload as any);
expect(result.model).toBe('llama3.2');
expect(result.messages).toEqual(payload.messages);
});
it('should handle payload with additional parameters', () => {
const payload = {
frequency_penalty: 0.5,
messages: [{ content: 'Hello', role: 'user' }],
model: 'llama3.2',
presence_penalty: 0.3,
stop: ['END'],
top_p: 0.9,
};
const result = params.chatCompletion.handlePayload(payload as any);
expect(result.model).toBe('llama3.2');
expect(result.messages).toEqual(payload.messages);
expect(result.top_p).toBe(0.9);
expect(result.frequency_penalty).toBe(0.5);
expect(result.presence_penalty).toBe(0.3);
expect(result.stop).toEqual(['END']);
});
it('should handle different model names', () => {
const payload1 = {
messages: [{ content: 'Hello', role: 'user' }],
model: 'llama3.2',
};
const payload2 = {
messages: [{ content: 'Hello', role: 'user' }],
model: 'codellama',
};
const result1 = params.chatCompletion.handlePayload(payload1 as any);
const result2 = params.chatCompletion.handlePayload(payload2 as any);
expect(result1.model).toBe('llama3.2');
expect(result2.model).toBe('codellama');
});
});
describe('models function', () => {
it('should fetch and process models when response has data array', async () => {
const mockClient = {
apiKey: 'test',
baseURL: 'https://ollama.com/v1',
models: {
list: vi.fn().mockResolvedValue({
data: [
{ id: 'llama3.2', object: 'model', owned_by: 'ollama' },
{ id: 'codellama', object: 'model', owned_by: 'ollama' },
],
}),
},
};
const result = await params.models({ client: mockClient as any });
expect(mockClient.models.list).toHaveBeenCalledTimes(1);
expect(result).toBeDefined();
expect(Array.isArray(result)).toBe(true);
});
it('should handle response as direct array', async () => {
const mockClient = {
apiKey: 'test',
baseURL: 'https://ollama.com/v1',
models: {
list: vi.fn().mockResolvedValue([
{ id: 'llama3.2', object: 'model', owned_by: 'ollama' },
{ id: 'codellama', object: 'model', owned_by: 'ollama' },
]),
},
};
const result = await params.models({ client: mockClient as any });
expect(mockClient.models.list).toHaveBeenCalledTimes(1);
expect(result).toBeDefined();
expect(Array.isArray(result)).toBe(true);
});
it('should handle empty data array', async () => {
const mockClient = {
apiKey: 'test',
baseURL: 'https://ollama.com/v1',
models: {
list: vi.fn().mockResolvedValue({
data: [],
}),
},
};
const result = await params.models({ client: mockClient as any });
expect(mockClient.models.list).toHaveBeenCalledTimes(1);
expect(result).toBeDefined();
expect(Array.isArray(result)).toBe(true);
expect(result).toHaveLength(0);
});
it('should handle empty direct array', async () => {
const mockClient = {
apiKey: 'test',
baseURL: 'https://ollama.com/v1',
models: {
list: vi.fn().mockResolvedValue([]),
},
};
const result = await params.models({ client: mockClient as any });
expect(mockClient.models.list).toHaveBeenCalledTimes(1);
expect(result).toBeDefined();
expect(Array.isArray(result)).toBe(true);
expect(result).toHaveLength(0);
});
it('should handle API error gracefully', async () => {
const mockClient = {
apiKey: 'test',
baseURL: 'https://ollama.com/v1',
models: {
list: vi.fn().mockRejectedValue(new Error('API Error')),
},
};
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const result = await params.models({ client: mockClient as any });
expect(mockClient.models.list).toHaveBeenCalledTimes(1);
expect(result).toEqual([]);
expect(consoleWarnSpy).toHaveBeenCalledWith(
'Failed to fetch Ollama Cloud models. Please ensure your Ollama Cloud API key is valid:',
expect.any(Error),
);
consoleWarnSpy.mockRestore();
});
it('should handle network error', async () => {
const mockClient = {
apiKey: 'test',
baseURL: 'https://ollama.com/v1',
models: {
list: vi.fn().mockRejectedValue(new Error('Network Error')),
},
};
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const result = await params.models({ client: mockClient as any });
expect(result).toEqual([]);
expect(consoleWarnSpy).toHaveBeenCalled();
consoleWarnSpy.mockRestore();
});
it('should handle invalid API key error', async () => {
const mockClient = {
apiKey: 'invalid',
baseURL: 'https://ollama.com/v1',
models: {
list: vi.fn().mockRejectedValue(new Error('Invalid API Key')),
},
};
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const result = await params.models({ client: mockClient as any });
expect(result).toEqual([]);
expect(consoleWarnSpy).toHaveBeenCalledWith(
'Failed to fetch Ollama Cloud models. Please ensure your Ollama Cloud API key is valid:',
expect.any(Error),
);
consoleWarnSpy.mockRestore();
});
it('should handle null response', async () => {
const mockClient = {
apiKey: 'test',
baseURL: 'https://ollama.com/v1',
models: {
list: vi.fn().mockResolvedValue(null),
},
};
const result = await params.models({ client: mockClient as any });
expect(result).toBeDefined();
expect(Array.isArray(result)).toBe(true);
expect(result).toHaveLength(0);
});
it('should handle undefined response', async () => {
const mockClient = {
apiKey: 'test',
baseURL: 'https://ollama.com/v1',
models: {
list: vi.fn().mockResolvedValue(undefined),
},
};
const result = await params.models({ client: mockClient as any });
expect(result).toBeDefined();
expect(Array.isArray(result)).toBe(true);
expect(result).toHaveLength(0);
});
it('should handle response with non-array data', async () => {
const mockClient = {
apiKey: 'test',
baseURL: 'https://ollama.com/v1',
models: {
list: vi.fn().mockResolvedValue({
data: 'not-an-array',
}),
},
};
const result = await params.models({ client: mockClient as any });
expect(result).toBeDefined();
expect(Array.isArray(result)).toBe(true);
expect(result).toHaveLength(0);
});
it('should handle response as non-array object without data property', async () => {
const mockClient = {
apiKey: 'test',
baseURL: 'https://ollama.com/v1',
models: {
list: vi.fn().mockResolvedValue({
models: [{ id: 'llama3.2' }],
}),
},
};
const result = await params.models({ client: mockClient as any });
expect(result).toBeDefined();
expect(Array.isArray(result)).toBe(true);
expect(result).toHaveLength(0);
});
it('should handle response with data property but null value', async () => {
const mockClient = {
apiKey: 'test',
baseURL: 'https://ollama.com/v1',
models: {
list: vi.fn().mockResolvedValue({
data: null,
}),
},
};
const result = await params.models({ client: mockClient as any });
expect(result).toBeDefined();
expect(Array.isArray(result)).toBe(true);
expect(result).toHaveLength(0);
});
it('should handle timeout error', async () => {
const mockClient = {
apiKey: 'test',
baseURL: 'https://ollama.com/v1',
models: {
list: vi.fn().mockRejectedValue(new Error('Request timeout')),
},
};
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const result = await params.models({ client: mockClient as any });
expect(result).toEqual([]);
expect(consoleWarnSpy).toHaveBeenCalled();
consoleWarnSpy.mockRestore();
});
});
describe('runtime instantiation', () => {
it('should create instance with api key', () => {
const runtime = new LobeOllamaCloudAI({ apiKey: 'test_api_key' });
expect(runtime).toBeDefined();
expect(runtime).toBeInstanceOf(LobeOllamaCloudAI);
});
it('should create instance with custom baseURL', () => {
const runtime = new LobeOllamaCloudAI({
apiKey: 'test_api_key',
baseURL: 'https://custom.ollama.com/v1',
});
expect(runtime).toBeDefined();
expect(runtime).toBeInstanceOf(LobeOllamaCloudAI);
});
it('should create instance with additional options', () => {
const runtime = new LobeOllamaCloudAI({
apiKey: 'test_api_key',
baseURL: 'https://ollama.com/v1',
});
expect(runtime).toBeDefined();
});
});
});