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:
2025-12-13 01:50:27 +08:00
parent 1d69fd876d
commit 6ec6fe2f9f
24 changed files with 2609 additions and 342 deletions
+1
View File
@@ -13,3 +13,4 @@ export { mcpRouter } from './mcp.js';
export { hooksRouter } from './hooks.js';
export { agentsRouter } from './agents.js';
export { checkpointsRouter } from './checkpoints.js';
export { providersRouter } from './providers.js';
+425
View File
@@ -0,0 +1,425 @@
/**
* Providers API Routes
*
* 模型提供商管理相关的 REST API
*/
import { Hono } from 'hono';
// Types from core - dynamically import to avoid build dependency
interface ProviderListItem {
id: string;
name: string;
description?: string;
builtin: boolean;
enabled: boolean;
hasApiKey: boolean;
modelCount: number;
}
interface ModelInfo {
id: string;
name: string;
capabilities?: {
vision?: boolean;
functionCalling?: boolean;
streaming?: boolean;
};
contextWindow?: number;
maxOutput?: number;
}
interface ProviderDetail {
id: string;
name: string;
description?: string;
builtin: boolean;
baseUrl?: string;
apiKeyEnvVar?: string;
models: ModelInfo[];
allowCustomModels: boolean;
config: {
enabled: boolean;
hasApiKey: boolean;
baseUrl?: string;
customModels: ModelInfo[];
};
}
interface CustomProviderDefinition {
id: string;
name: string;
description?: string;
baseUrl: string;
apiKeyEnvVar?: string;
models?: ModelInfo[];
allowCustomModels?: boolean;
}
interface ProviderConfig {
id?: string;
apiKey?: string;
apiKeyEnvVar?: string;
baseUrl?: string;
enabled?: boolean;
customModels?: ModelInfo[];
}
interface ConnectionTestResult {
success: boolean;
latency?: number;
error?: string;
}
export const providersRouter = new Hono();
// Core module reference
let coreModule: any = null;
/**
* Load core module dynamically
*/
async function getCoreModule() {
if (!coreModule) {
try {
const corePath = '@ai-assistant/core';
coreModule = await import(corePath);
} catch {
return null;
}
}
return coreModule;
}
/**
* GET /providers - List all providers
*/
providersRouter.get('/', async (c) => {
const core = await getCoreModule();
if (!core) {
return c.json({ success: false, error: 'Core module not available' }, 503);
}
try {
const registry = core.getProviderRegistry();
const providers: ProviderListItem[] = registry.listForApi();
return c.json({
success: true,
data: providers,
});
} catch (error) {
return c.json(
{
success: false,
error: error instanceof Error ? error.message : 'Failed to list providers',
},
500
);
}
});
/**
* GET /providers/:id - Get provider detail
*/
providersRouter.get('/:id', async (c) => {
const core = await getCoreModule();
if (!core) {
return c.json({ success: false, error: 'Core module not available' }, 503);
}
const id = c.req.param('id');
try {
const registry = core.getProviderRegistry();
const detail: ProviderDetail | undefined = registry.getDetail(id);
if (!detail) {
return c.json({ success: false, error: `Provider not found: ${id}` }, 404);
}
return c.json({
success: true,
data: detail,
});
} catch (error) {
return c.json(
{
success: false,
error: error instanceof Error ? error.message : 'Failed to get provider',
},
500
);
}
});
/**
* GET /providers/:id/models - Get provider's model list
*/
providersRouter.get('/:id/models', async (c) => {
const core = await getCoreModule();
if (!core) {
return c.json({ success: false, error: 'Core module not available' }, 503);
}
const id = c.req.param('id');
try {
const registry = core.getProviderRegistry();
if (!registry.has(id)) {
return c.json({ success: false, error: `Provider not found: ${id}` }, 404);
}
const models: ModelInfo[] = registry.getModels(id);
return c.json({
success: true,
data: models,
});
} catch (error) {
return c.json(
{
success: false,
error: error instanceof Error ? error.message : 'Failed to get models',
},
500
);
}
});
/**
* POST /providers/:id/test - Test provider connection
*/
providersRouter.post('/:id/test', async (c) => {
const core = await getCoreModule();
if (!core) {
return c.json({ success: false, error: 'Core module not available' }, 503);
}
const id = c.req.param('id');
try {
const body = await c.req.json().catch(() => ({}));
const apiKey = body.apiKey as string | undefined;
const registry = core.getProviderRegistry();
const result: ConnectionTestResult = await registry.testConnection(id, apiKey);
return c.json({
success: true,
data: result,
});
} catch (error) {
return c.json(
{
success: false,
error: error instanceof Error ? error.message : 'Connection test failed',
},
500
);
}
});
/**
* POST /providers - Register custom provider
*/
providersRouter.post('/', async (c) => {
const core = await getCoreModule();
if (!core) {
return c.json({ success: false, error: 'Core module not available' }, 503);
}
try {
const body: CustomProviderDefinition = await c.req.json();
// Validate required fields
if (!body.id || !body.name || !body.baseUrl) {
return c.json(
{
success: false,
error: 'Missing required fields: id, name, baseUrl',
},
400
);
}
const registry = core.getProviderRegistry();
registry.registerCustom(body);
await registry.saveConfig();
return c.json({
success: true,
message: `Provider ${body.id} registered`,
});
} catch (error) {
return c.json(
{
success: false,
error: error instanceof Error ? error.message : 'Failed to register provider',
},
400
);
}
});
/**
* PUT /providers/:id - Update provider config
*/
providersRouter.put('/:id', async (c) => {
const core = await getCoreModule();
if (!core) {
return c.json({ success: false, error: 'Core module not available' }, 503);
}
const id = c.req.param('id');
try {
const body: ProviderConfig = await c.req.json();
const registry = core.getProviderRegistry();
if (!registry.has(id)) {
return c.json({ success: false, error: `Provider not found: ${id}` }, 404);
}
registry.setConfig(id, { ...body, id });
await registry.saveConfig();
return c.json({
success: true,
message: `Provider ${id} config updated`,
});
} catch (error) {
return c.json(
{
success: false,
error: error instanceof Error ? error.message : 'Failed to update provider config',
},
400
);
}
});
/**
* DELETE /providers/:id - Delete custom provider
*/
providersRouter.delete('/:id', async (c) => {
const core = await getCoreModule();
if (!core) {
return c.json({ success: false, error: 'Core module not available' }, 503);
}
const id = c.req.param('id');
try {
const registry = core.getProviderRegistry();
const removed = registry.removeCustom(id);
if (!removed) {
return c.json({ success: false, error: `Provider not found: ${id}` }, 404);
}
await registry.saveConfig();
return c.json({
success: true,
message: `Provider ${id} removed`,
});
} catch (error) {
return c.json(
{
success: false,
error: error instanceof Error ? error.message : 'Failed to remove provider',
},
400
);
}
});
/**
* POST /providers/:id/models - Add custom model
*/
providersRouter.post('/:id/models', async (c) => {
const core = await getCoreModule();
if (!core) {
return c.json({ success: false, error: 'Core module not available' }, 503);
}
const providerId = c.req.param('id');
try {
const body: ModelInfo = await c.req.json();
// Validate required fields
if (!body.id || !body.name) {
return c.json(
{
success: false,
error: 'Missing required fields: id, name',
},
400
);
}
const registry = core.getProviderRegistry();
registry.addCustomModel(providerId, body);
await registry.saveConfig();
return c.json({
success: true,
message: `Model ${body.id} added to ${providerId}`,
});
} catch (error) {
return c.json(
{
success: false,
error: error instanceof Error ? error.message : 'Failed to add model',
},
400
);
}
});
/**
* DELETE /providers/:id/models/:modelId - Delete custom model
*/
providersRouter.delete('/:id/models/:modelId', async (c) => {
const core = await getCoreModule();
if (!core) {
return c.json({ success: false, error: 'Core module not available' }, 503);
}
const providerId = c.req.param('id');
const modelId = c.req.param('modelId');
try {
const registry = core.getProviderRegistry();
const removed = registry.removeCustomModel(providerId, modelId);
if (!removed) {
return c.json(
{
success: false,
error: `Model ${modelId} not found in ${providerId}`,
},
404
);
}
await registry.saveConfig();
return c.json({
success: true,
message: `Model ${modelId} removed from ${providerId}`,
});
} catch (error) {
return c.json(
{
success: false,
error: error instanceof Error ? error.message : 'Failed to remove model',
},
400
);
}
});