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:
@@ -9,7 +9,7 @@ import { cors } from 'hono/cors';
|
||||
import { logger } from 'hono/logger';
|
||||
import { createBunWebSocket } from 'hono/bun';
|
||||
|
||||
import { sessionsRouter, toolsRouter, configRouter, filesRouter, commandsRouter, mcpRouter, hooksRouter, agentsRouter, checkpointsRouter } from './routes/index.js';
|
||||
import { sessionsRouter, toolsRouter, configRouter, filesRouter, commandsRouter, mcpRouter, hooksRouter, agentsRouter, checkpointsRouter, providersRouter } from './routes/index.js';
|
||||
import {
|
||||
handleWebSocket,
|
||||
handleWebSocketMessage,
|
||||
@@ -87,6 +87,7 @@ api.route('/mcp', mcpRouter);
|
||||
api.route('/hooks', hooksRouter);
|
||||
api.route('/agents', agentsRouter);
|
||||
api.route('/checkpoints', checkpointsRouter);
|
||||
api.route('/providers', providersRouter);
|
||||
|
||||
// SSE 事件流
|
||||
api.get('/sessions/:id/events', handleSSE);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user