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:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user