feat(provider): 添加独立的 Provider 模块管理模型提供商
实现可扩展的 Provider 系统,支持动态注册自定义提供商: Core 模块 (packages/core/src/provider/): - types.ts: Provider 相关类型定义 - builtin/: 内置提供商 (Anthropic, OpenAI, DeepSeek) - registry.ts: ProviderRegistry 单例类 - config.ts: 配置持久化 (~/.ai-terminal-assistant/providers.json) - utils.ts: 连接测试等工具函数 Server API (packages/server/src/routes/providers.ts): - GET/POST/PUT/DELETE /providers 提供商管理 - POST /providers/:id/test 连接测试 - 自定义模型管理接口 Frontend (packages/ui/): - ProvidersPanel 组件用于管理提供商 - API client 函数和类型定义 主要功能: - 支持动态注册 OpenAI 兼容服务 (Ollama, vLLM 等) - 每个提供商独立的 API Key 配置 - 预设模型列表 + 自定义模型输入 - 连接测试验证
This commit is contained in:
@@ -35,10 +35,12 @@ vi.mock('../../../src/agent/permission-merger.js', () => ({
|
||||
checkBashPermission: (...args: unknown[]) => mockCheckBashPermission(...args),
|
||||
}));
|
||||
|
||||
// Mock providers
|
||||
// Mock provider registry
|
||||
const mockGetModelFactory = vi.fn();
|
||||
vi.mock('../../../src/core/providers.js', () => ({
|
||||
getModelFactory: (...args: unknown[]) => mockGetModelFactory(...args),
|
||||
vi.mock('../../../src/provider/index.js', () => ({
|
||||
getProviderRegistry: () => ({
|
||||
getModelFactory: (...args: unknown[]) => mockGetModelFactory(...args),
|
||||
}),
|
||||
}));
|
||||
|
||||
import { AgentExecutor } from '../../../src/agent/executor.js';
|
||||
|
||||
@@ -116,7 +116,7 @@ describe('AgentExecutor - Agent 执行器', () => {
|
||||
|
||||
it('不支持的 provider 抛出错误', () => {
|
||||
const config = { ...mockBaseConfig, provider: 'unknown' as any };
|
||||
expect(() => new AgentExecutor(mockAgentInfo, config, mockToolRegistry)).toThrow('不支持的 provider');
|
||||
expect(() => new AgentExecutor(mockAgentInfo, config, mockToolRegistry)).toThrow('Provider not found: unknown');
|
||||
});
|
||||
|
||||
it('使用 Agent 指定的 provider', () => {
|
||||
|
||||
@@ -1,264 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { getModelFactory, providers } from '../../../src/core/providers.js';
|
||||
|
||||
// Mock AI SDK providers
|
||||
vi.mock('@ai-sdk/anthropic', () => ({
|
||||
createAnthropic: vi.fn(() => {
|
||||
const modelFn = (model: string) => ({ modelId: `anthropic:${model}` });
|
||||
return modelFn;
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@ai-sdk/deepseek', () => ({
|
||||
createDeepSeek: vi.fn(() => {
|
||||
const modelFn = (model: string) => ({ modelId: `deepseek:${model}` });
|
||||
return modelFn;
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@ai-sdk/openai', () => ({
|
||||
createOpenAI: vi.fn(() => {
|
||||
const modelFn = (model: string) => ({ modelId: `openai:${model}` });
|
||||
return modelFn;
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('qwen-ai-provider-v5', () => ({
|
||||
createQwen: vi.fn(() => {
|
||||
const modelFn = (model: string) => ({ modelId: `qwen:${model}` });
|
||||
return modelFn;
|
||||
}),
|
||||
}));
|
||||
|
||||
import { createAnthropic } from '@ai-sdk/anthropic';
|
||||
import { createDeepSeek } from '@ai-sdk/deepseek';
|
||||
import { createOpenAI } from '@ai-sdk/openai';
|
||||
import { createQwen } from 'qwen-ai-provider-v5';
|
||||
|
||||
describe('providers', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('providers 注册表', () => {
|
||||
it('包含所有支持的 provider 类型', () => {
|
||||
expect(providers).toHaveProperty('anthropic');
|
||||
expect(providers).toHaveProperty('deepseek');
|
||||
expect(providers).toHaveProperty('openai');
|
||||
});
|
||||
|
||||
it('每个 provider 是一个工厂函数', () => {
|
||||
expect(typeof providers.anthropic).toBe('function');
|
||||
expect(typeof providers.deepseek).toBe('function');
|
||||
expect(typeof providers.openai).toBe('function');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Anthropic provider', () => {
|
||||
it('创建 Anthropic 客户端', () => {
|
||||
const factory = providers.anthropic({
|
||||
apiKey: 'test-api-key',
|
||||
});
|
||||
|
||||
expect(createAnthropic).toHaveBeenCalledWith({
|
||||
apiKey: 'test-api-key',
|
||||
baseURL: undefined,
|
||||
});
|
||||
expect(typeof factory).toBe('function');
|
||||
});
|
||||
|
||||
it('支持自定义 baseUrl', () => {
|
||||
providers.anthropic({
|
||||
apiKey: 'test-api-key',
|
||||
baseUrl: 'https://custom.anthropic.com',
|
||||
});
|
||||
|
||||
expect(createAnthropic).toHaveBeenCalledWith({
|
||||
apiKey: 'test-api-key',
|
||||
baseURL: 'https://custom.anthropic.com',
|
||||
});
|
||||
});
|
||||
|
||||
it('返回的工厂函数可以创建模型', () => {
|
||||
const factory = providers.anthropic({
|
||||
apiKey: 'test-api-key',
|
||||
});
|
||||
|
||||
const model = factory('claude-3-opus');
|
||||
expect(model).toEqual({ modelId: 'anthropic:claude-3-opus' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('DeepSeek provider', () => {
|
||||
it('创建 DeepSeek 客户端', () => {
|
||||
const factory = providers.deepseek({
|
||||
apiKey: 'test-deepseek-key',
|
||||
});
|
||||
|
||||
expect(createDeepSeek).toHaveBeenCalledWith({
|
||||
apiKey: 'test-deepseek-key',
|
||||
baseURL: undefined,
|
||||
});
|
||||
expect(typeof factory).toBe('function');
|
||||
});
|
||||
|
||||
it('支持自定义 baseUrl', () => {
|
||||
providers.deepseek({
|
||||
apiKey: 'test-deepseek-key',
|
||||
baseUrl: 'https://custom.deepseek.com',
|
||||
});
|
||||
|
||||
expect(createDeepSeek).toHaveBeenCalledWith({
|
||||
apiKey: 'test-deepseek-key',
|
||||
baseURL: 'https://custom.deepseek.com',
|
||||
});
|
||||
});
|
||||
|
||||
it('返回的工厂函数可以创建模型', () => {
|
||||
const factory = providers.deepseek({
|
||||
apiKey: 'test-deepseek-key',
|
||||
});
|
||||
|
||||
const model = factory('deepseek-chat');
|
||||
expect(model).toEqual({ modelId: 'deepseek:deepseek-chat' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('OpenAI provider', () => {
|
||||
it('创建 OpenAI 客户端(标准 URL)', () => {
|
||||
const factory = providers.openai({
|
||||
apiKey: 'test-openai-key',
|
||||
});
|
||||
|
||||
expect(createOpenAI).toHaveBeenCalledWith({
|
||||
apiKey: 'test-openai-key',
|
||||
baseURL: undefined,
|
||||
});
|
||||
expect(createQwen).not.toHaveBeenCalled();
|
||||
expect(typeof factory).toBe('function');
|
||||
});
|
||||
|
||||
it('支持自定义 baseUrl(非 DashScope)', () => {
|
||||
providers.openai({
|
||||
apiKey: 'test-openai-key',
|
||||
baseUrl: 'https://custom.openai.com/v1',
|
||||
});
|
||||
|
||||
expect(createOpenAI).toHaveBeenCalledWith({
|
||||
apiKey: 'test-openai-key',
|
||||
baseURL: 'https://custom.openai.com/v1',
|
||||
});
|
||||
expect(createQwen).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('返回的工厂函数可以创建模型', () => {
|
||||
const factory = providers.openai({
|
||||
apiKey: 'test-openai-key',
|
||||
});
|
||||
|
||||
const model = factory('gpt-4');
|
||||
expect(model).toEqual({ modelId: 'openai:gpt-4' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('DashScope (Qwen) 检测', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('检测 dashscope URL 并使用 Qwen provider', () => {
|
||||
providers.openai({
|
||||
apiKey: 'test-qwen-key',
|
||||
baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
||||
});
|
||||
|
||||
expect(createQwen).toHaveBeenCalledWith({
|
||||
apiKey: 'test-qwen-key',
|
||||
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
||||
});
|
||||
expect(createOpenAI).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('检测包含 dashscope 的任意 URL', () => {
|
||||
providers.openai({
|
||||
apiKey: 'test-qwen-key',
|
||||
baseUrl: 'https://api.dashscope.example.com/v1',
|
||||
});
|
||||
|
||||
expect(createQwen).toHaveBeenCalled();
|
||||
expect(createOpenAI).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Qwen 工厂函数可以创建模型', () => {
|
||||
const factory = providers.openai({
|
||||
apiKey: 'test-qwen-key',
|
||||
baseUrl: 'https://dashscope.aliyuncs.com/v1',
|
||||
});
|
||||
|
||||
const model = factory('qwen-turbo');
|
||||
expect(model).toEqual({ modelId: 'qwen:qwen-turbo' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getModelFactory', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('获取 Anthropic 模型工厂', () => {
|
||||
const factory = getModelFactory('anthropic', {
|
||||
apiKey: 'test-key',
|
||||
});
|
||||
|
||||
expect(typeof factory).toBe('function');
|
||||
expect(createAnthropic).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('获取 DeepSeek 模型工厂', () => {
|
||||
const factory = getModelFactory('deepseek', {
|
||||
apiKey: 'test-key',
|
||||
});
|
||||
|
||||
expect(typeof factory).toBe('function');
|
||||
expect(createDeepSeek).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('获取 OpenAI 模型工厂', () => {
|
||||
const factory = getModelFactory('openai', {
|
||||
apiKey: 'test-key',
|
||||
});
|
||||
|
||||
expect(typeof factory).toBe('function');
|
||||
expect(createOpenAI).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('传递正确的选项给 provider', () => {
|
||||
getModelFactory('anthropic', {
|
||||
apiKey: 'my-api-key',
|
||||
baseUrl: 'https://my-proxy.com',
|
||||
});
|
||||
|
||||
expect(createAnthropic).toHaveBeenCalledWith({
|
||||
apiKey: 'my-api-key',
|
||||
baseURL: 'https://my-proxy.com',
|
||||
});
|
||||
});
|
||||
|
||||
it('不支持的 provider 抛出错误', () => {
|
||||
expect(() => {
|
||||
getModelFactory('unsupported' as any, {
|
||||
apiKey: 'test-key',
|
||||
});
|
||||
}).toThrow('不支持的 provider: unsupported');
|
||||
});
|
||||
|
||||
it('返回的工厂函数可以创建模型实例', () => {
|
||||
const factory = getModelFactory('anthropic', {
|
||||
apiKey: 'test-key',
|
||||
});
|
||||
|
||||
const model = factory('claude-3-sonnet');
|
||||
expect(model).toEqual({ modelId: 'anthropic:claude-3-sonnet' });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user