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
+442
View File
@@ -0,0 +1,442 @@
/**
* Provider Registry
*
* 管理所有注册的提供商(内置 + 自定义)
*/
import type { LanguageModel } from 'ai';
import type {
ProviderInfo,
ProviderConfig,
CustomProviderDefinition,
RegisteredProvider,
ConnectionTestResult,
ProviderFactory,
ModelInfo,
ProviderListItem,
ProviderDetail,
} from './types.js';
import { builtinProviders, isBuiltinProvider } from './builtin/index.js';
import {
loadProvidersConfig,
saveProvidersConfig,
resolveApiKey,
} from './config.js';
import {
testOpenAICompatibleConnection,
createOpenAICompatibleFactory,
isValidProviderId,
isValidUrl,
} from './utils.js';
/**
* Provider Registry
* 管理所有提供商的单例类
*/
export class ProviderRegistry {
/** 已注册的提供商 */
private providers: Map<string, RegisteredProvider> = new Map();
/** 提供商配置 */
private configs: Map<string, ProviderConfig> = new Map();
/** 自定义提供商定义 */
private customDefinitions: Map<string, CustomProviderDefinition> = new Map();
/** 是否已完全初始化(包括用户配置) */
private fullyInitialized = false;
/** 工作目录 */
private workdir?: string;
constructor() {
// 同步初始化内置提供商(不需要异步加载)
this.initBuiltinProviders();
}
/**
* 同步初始化内置提供商
*/
private initBuiltinProviders(): void {
for (const [id, builtin] of Object.entries(builtinProviders)) {
this.providers.set(id, {
info: builtin.info,
factory: builtin.factory,
});
}
}
/**
* 完整初始化 Registry
* 加载用户配置(自定义提供商和配置)
*/
async init(workdir?: string): Promise<void> {
if (this.fullyInitialized) return;
this.workdir = workdir;
// 加载用户配置
const config = await loadProvidersConfig(workdir);
// 加载自定义提供商
if (config.providers) {
for (const [id, definition] of Object.entries(config.providers)) {
this.registerCustomInternal(definition);
}
}
// 加载提供商配置
if (config.configs) {
for (const [id, providerConfig] of Object.entries(config.configs)) {
this.configs.set(id, providerConfig);
}
}
this.fullyInitialized = true;
}
/**
* 检查是否已完全初始化
*/
isInitialized(): boolean {
return this.fullyInitialized;
}
/**
* 确保内置提供商已加载(总是true,因为构造函数中已初始化)
* 保留此方法以便未来可能需要的检查
*/
private ensureInitialized(): void {
// 内置提供商在构造函数中同步初始化,总是可用
// 用户配置需要调用 init() 异步加载
}
/**
* 列出所有提供商
*/
list(): ProviderInfo[] {
this.ensureInitialized();
return Array.from(this.providers.values()).map((p) => p.info);
}
/**
* 列出所有提供商(API 响应格式)
*/
listForApi(): ProviderListItem[] {
this.ensureInitialized();
return Array.from(this.providers.entries()).map(([id, provider]) => {
const config = this.configs.get(id);
const apiKey = resolveApiKey(config, provider.info.apiKeyEnvVar);
const customModels = config?.customModels ?? [];
return {
id,
name: provider.info.name,
description: provider.info.description,
builtin: provider.info.builtin,
enabled: config?.enabled ?? true,
hasApiKey: !!apiKey,
modelCount: provider.info.models.length + customModels.length,
};
});
}
/**
* 获取提供商
*/
get(id: string): RegisteredProvider | undefined {
this.ensureInitialized();
return this.providers.get(id);
}
/**
* 获取提供商信息
*/
getInfo(id: string): ProviderInfo | undefined {
return this.get(id)?.info;
}
/**
* 获取提供商详情(API 响应格式)
*/
getDetail(id: string): ProviderDetail | undefined {
this.ensureInitialized();
const provider = this.providers.get(id);
if (!provider) return undefined;
const config = this.configs.get(id);
const apiKey = resolveApiKey(config, provider.info.apiKeyEnvVar);
return {
id,
name: provider.info.name,
description: provider.info.description,
builtin: provider.info.builtin,
baseUrl: config?.baseUrl ?? provider.info.baseUrl,
apiKeyEnvVar: provider.info.apiKeyEnvVar,
models: provider.info.models,
allowCustomModels: provider.info.allowCustomModels ?? false,
config: {
enabled: config?.enabled ?? true,
hasApiKey: !!apiKey,
baseUrl: config?.baseUrl,
customModels: config?.customModels ?? [],
},
};
}
/**
* 检查提供商是否存在
*/
has(id: string): boolean {
this.ensureInitialized();
return this.providers.has(id);
}
/**
* 内部注册自定义提供商
*/
private registerCustomInternal(definition: CustomProviderDefinition): void {
const factory = createOpenAICompatibleFactory(definition.baseUrl);
const info: ProviderInfo = {
id: definition.id,
name: definition.name,
description: definition.description,
builtin: false,
baseUrl: definition.baseUrl,
apiKeyEnvVar: definition.apiKeyEnvVar,
models: definition.models ?? [],
allowCustomModels: definition.allowCustomModels ?? true,
};
this.providers.set(definition.id, { info, factory });
this.customDefinitions.set(definition.id, definition);
}
/**
* 注册自定义提供商
*/
registerCustom(definition: CustomProviderDefinition): void {
this.ensureInitialized();
// 验证 ID
if (!isValidProviderId(definition.id)) {
throw new Error(`Invalid provider ID: ${definition.id}`);
}
// 不能覆盖内置提供商
if (isBuiltinProvider(definition.id)) {
throw new Error(`Cannot override builtin provider: ${definition.id}`);
}
// 验证 URL
if (!isValidUrl(definition.baseUrl)) {
throw new Error(`Invalid base URL: ${definition.baseUrl}`);
}
this.registerCustomInternal(definition);
}
/**
* 移除自定义提供商
*/
removeCustom(id: string): boolean {
this.ensureInitialized();
// 不能删除内置提供商
if (isBuiltinProvider(id)) {
throw new Error(`Cannot remove builtin provider: ${id}`);
}
const removed = this.providers.delete(id);
this.customDefinitions.delete(id);
this.configs.delete(id);
return removed;
}
/**
* 设置提供商配置
*/
setConfig(id: string, config: ProviderConfig): void {
this.ensureInitialized();
if (!this.providers.has(id)) {
throw new Error(`Provider not found: ${id}`);
}
this.configs.set(id, { ...config, id });
}
/**
* 获取提供商配置
*/
getConfig(id: string): ProviderConfig | undefined {
this.ensureInitialized();
return this.configs.get(id);
}
/**
* 获取所有配置
*/
getAllConfigs(): Record<string, ProviderConfig> {
this.ensureInitialized();
return Object.fromEntries(this.configs);
}
/**
* 获取提供商的模型列表
*/
getModels(providerId: string): ModelInfo[] {
this.ensureInitialized();
const provider = this.providers.get(providerId);
if (!provider) return [];
const config = this.configs.get(providerId);
const customModels = config?.customModels ?? [];
return [...provider.info.models, ...customModels];
}
/**
* 添加自定义模型
*/
addCustomModel(providerId: string, model: ModelInfo): void {
this.ensureInitialized();
if (!this.providers.has(providerId)) {
throw new Error(`Provider not found: ${providerId}`);
}
const config = this.configs.get(providerId) ?? { id: providerId };
const customModels = config.customModels ?? [];
// 检查是否已存在
if (customModels.some((m) => m.id === model.id)) {
throw new Error(`Model already exists: ${model.id}`);
}
customModels.push(model);
this.configs.set(providerId, { ...config, customModels });
}
/**
* 移除自定义模型
*/
removeCustomModel(providerId: string, modelId: string): boolean {
this.ensureInitialized();
const config = this.configs.get(providerId);
if (!config?.customModels) return false;
const index = config.customModels.findIndex((m) => m.id === modelId);
if (index === -1) return false;
config.customModels.splice(index, 1);
this.configs.set(providerId, config);
return true;
}
/**
* 测试提供商连接
*/
async testConnection(
providerId: string,
apiKey?: string
): Promise<ConnectionTestResult> {
this.ensureInitialized();
const provider = this.providers.get(providerId);
if (!provider) {
return { success: false, error: `Provider not found: ${providerId}` };
}
const config = this.configs.get(providerId);
const resolvedApiKey = apiKey ?? resolveApiKey(config, provider.info.apiKeyEnvVar);
if (!resolvedApiKey && !provider.info.baseUrl?.includes('localhost')) {
return { success: false, error: 'API key not configured' };
}
const baseUrl = config?.baseUrl ?? provider.info.baseUrl;
// 对于自定义提供商,使用 OpenAI 兼容测试
if (!provider.info.builtin && baseUrl) {
return testOpenAICompatibleConnection(resolvedApiKey ?? '', baseUrl);
}
// 对于内置提供商,简单验证 API key 存在
return {
success: !!resolvedApiKey,
error: resolvedApiKey ? undefined : 'API key not configured',
};
}
/**
* 获取模型工厂函数
*/
getModelFactory(
providerId: string,
options?: { apiKey?: string; baseUrl?: string }
): (model: string) => LanguageModel {
this.ensureInitialized();
const provider = this.providers.get(providerId);
if (!provider) {
throw new Error(`Provider not found: ${providerId}`);
}
const config = this.configs.get(providerId);
const apiKey = options?.apiKey ?? resolveApiKey(config, provider.info.apiKeyEnvVar);
const baseUrl = options?.baseUrl ?? config?.baseUrl ?? provider.info.baseUrl;
if (!apiKey) {
throw new Error(`API key not configured for provider: ${providerId}`);
}
return provider.factory({ apiKey, baseUrl });
}
/**
* 保存配置到文件
*/
async saveConfig(workdir?: string): Promise<void> {
this.ensureInitialized();
const config = {
providers: Object.fromEntries(this.customDefinitions),
configs: Object.fromEntries(this.configs),
};
await saveProvidersConfig(config, workdir ?? this.workdir);
}
/**
* 重新加载配置
*/
async reloadConfig(workdir?: string): Promise<void> {
this.fullyInitialized = false;
this.configs.clear();
this.customDefinitions.clear();
// 清除自定义提供商,保留内置提供商
for (const [id, provider] of this.providers.entries()) {
if (!provider.info.builtin) {
this.providers.delete(id);
}
}
await this.init(workdir ?? this.workdir);
}
}
/** 单例实例 */
export const providerRegistry = new ProviderRegistry();
/** 获取 ProviderRegistry 实例 */
export function getProviderRegistry(): ProviderRegistry {
return providerRegistry;
}