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:
@@ -16,7 +16,7 @@ import type {
|
||||
ImageData,
|
||||
} from './types.js';
|
||||
import { checkBashPermission } from './permission-merger.js';
|
||||
import { getModelFactory } from '../core/providers.js';
|
||||
import { getProviderRegistry } from '../provider/index.js';
|
||||
|
||||
/**
|
||||
* Agent 执行器
|
||||
@@ -37,9 +37,10 @@ export class AgentExecutor {
|
||||
this.baseConfig = baseConfig;
|
||||
this.toolRegistry = toolRegistry;
|
||||
|
||||
// 获取模型工厂
|
||||
// 使用 ProviderRegistry 获取模型工厂
|
||||
const provider = agentInfo.model?.provider ?? baseConfig.provider;
|
||||
this.getModel = getModelFactory(provider, {
|
||||
const registry = getProviderRegistry();
|
||||
this.getModel = registry.getModelFactory(provider, {
|
||||
apiKey: baseConfig.apiKey,
|
||||
baseUrl: baseConfig.baseUrl,
|
||||
});
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
import type { AgentInfo, ImageData } from '../agent/types.js';
|
||||
import { agentRegistry, AgentExecutor } from '../agent/index.js';
|
||||
import { loadVisionConfig } from '../utils/config.js';
|
||||
import { getModelFactory } from './providers.js';
|
||||
import { getProviderRegistry } from '../provider/index.js';
|
||||
import { getHookManager } from '../hooks/index.js';
|
||||
import { getGitManager } from '../git/index.js';
|
||||
|
||||
@@ -49,7 +49,9 @@ export class Agent {
|
||||
this.config = config;
|
||||
this.originalSystemPrompt = config.systemPrompt;
|
||||
|
||||
this.getModel = getModelFactory(config.provider, {
|
||||
// 使用 ProviderRegistry 获取模型工厂
|
||||
const registry = getProviderRegistry();
|
||||
this.getModel = registry.getModelFactory(config.provider, {
|
||||
apiKey: config.apiKey,
|
||||
baseUrl: config.baseUrl,
|
||||
});
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
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';
|
||||
import type { LanguageModel } from 'ai';
|
||||
import type { ProviderType } from '../types/index.js';
|
||||
|
||||
/**
|
||||
* Provider 配置选项
|
||||
*/
|
||||
export interface ProviderOptions {
|
||||
apiKey: string;
|
||||
baseUrl?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider 工厂函数类型
|
||||
*/
|
||||
export type ProviderFactory = (options: ProviderOptions) => (model: string) => LanguageModel;
|
||||
|
||||
/**
|
||||
* 检查 baseUrl 是否为阿里云百炼/DashScope
|
||||
*/
|
||||
function isDashScopeUrl(baseUrl?: string): boolean {
|
||||
if (!baseUrl) return false;
|
||||
return baseUrl.includes('dashscope');
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider 注册表
|
||||
* 支持 Anthropic、DeepSeek、OpenAI 及 OpenAI 兼容 API(如阿里云百炼)
|
||||
*/
|
||||
export const providers: Record<ProviderType, ProviderFactory> = {
|
||||
anthropic: ({ apiKey, baseUrl }) => {
|
||||
const client = createAnthropic({ apiKey, baseURL: baseUrl });
|
||||
return (model) => client(model);
|
||||
},
|
||||
deepseek: ({ apiKey, baseUrl }) => {
|
||||
const client = createDeepSeek({ apiKey, baseURL: baseUrl });
|
||||
return (model) => client(model);
|
||||
},
|
||||
openai: ({ apiKey, baseUrl }) => {
|
||||
// 如果是百炼的 URL,使用 qwen provider
|
||||
if (isDashScopeUrl(baseUrl)) {
|
||||
const client = createQwen({ apiKey, baseURL: baseUrl });
|
||||
return (model) => client(model);
|
||||
}
|
||||
const client = createOpenAI({ apiKey, baseURL: baseUrl });
|
||||
return (model) => client(model);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取模型工厂函数
|
||||
*/
|
||||
export function getModelFactory(
|
||||
provider: ProviderType,
|
||||
options: ProviderOptions
|
||||
): (model: string) => LanguageModel {
|
||||
const factory = providers[provider];
|
||||
if (!factory) {
|
||||
throw new Error(`不支持的 provider: ${provider}`);
|
||||
}
|
||||
return factory(options);
|
||||
}
|
||||
@@ -158,3 +158,37 @@ export {
|
||||
loadMCPConfig,
|
||||
createMCPToolAdapter,
|
||||
} from './mcp/index.js';
|
||||
|
||||
// Provider
|
||||
export {
|
||||
ProviderRegistry,
|
||||
providerRegistry,
|
||||
getProviderRegistry,
|
||||
builtinProviders,
|
||||
getBuiltinProviders,
|
||||
getBuiltinProvider,
|
||||
isBuiltinProvider,
|
||||
loadProvidersConfig,
|
||||
saveProvidersConfig,
|
||||
resolveApiKey,
|
||||
testOpenAICompatibleConnection,
|
||||
createOpenAICompatibleFactory,
|
||||
isValidProviderId,
|
||||
isValidUrl,
|
||||
} from './provider/index.js';
|
||||
|
||||
export type {
|
||||
BuiltinProviderType,
|
||||
ProviderType,
|
||||
ModelCapabilities,
|
||||
ModelInfo,
|
||||
ProviderInfo,
|
||||
ProviderConfig,
|
||||
CustomProviderDefinition,
|
||||
ConnectionTestResult,
|
||||
ProviderFactory,
|
||||
RegisteredProvider,
|
||||
ProvidersConfigFile,
|
||||
ProviderListItem,
|
||||
ProviderDetail,
|
||||
} from './provider/index.js';
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Anthropic Provider Definition
|
||||
*/
|
||||
|
||||
import { createAnthropic } from '@ai-sdk/anthropic';
|
||||
import type { ProviderInfo, ProviderFactory } from '../types.js';
|
||||
|
||||
export const anthropicProvider: ProviderInfo = {
|
||||
id: 'anthropic',
|
||||
name: 'Anthropic',
|
||||
description: 'Claude AI models by Anthropic',
|
||||
builtin: true,
|
||||
apiKeyEnvVar: 'ANTHROPIC_API_KEY',
|
||||
models: [
|
||||
{
|
||||
id: 'claude-sonnet-4-20250514',
|
||||
name: 'Claude Sonnet 4',
|
||||
capabilities: { vision: true, functionCalling: true, streaming: true },
|
||||
contextWindow: 200000,
|
||||
maxOutput: 8192,
|
||||
},
|
||||
{
|
||||
id: 'claude-3-5-sonnet-20241022',
|
||||
name: 'Claude 3.5 Sonnet',
|
||||
capabilities: { vision: true, functionCalling: true, streaming: true },
|
||||
contextWindow: 200000,
|
||||
maxOutput: 8192,
|
||||
},
|
||||
{
|
||||
id: 'claude-3-opus-20240229',
|
||||
name: 'Claude 3 Opus',
|
||||
capabilities: { vision: true, functionCalling: true, streaming: true },
|
||||
contextWindow: 200000,
|
||||
maxOutput: 4096,
|
||||
},
|
||||
{
|
||||
id: 'claude-3-haiku-20240307',
|
||||
name: 'Claude 3 Haiku',
|
||||
capabilities: { vision: true, functionCalling: true, streaming: true },
|
||||
contextWindow: 200000,
|
||||
maxOutput: 4096,
|
||||
},
|
||||
],
|
||||
allowCustomModels: true,
|
||||
};
|
||||
|
||||
export const anthropicFactory: ProviderFactory = ({ apiKey, baseUrl }) => {
|
||||
const client = createAnthropic({ apiKey, baseURL: baseUrl });
|
||||
return (model) => client(model);
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* DeepSeek Provider Definition
|
||||
*/
|
||||
|
||||
import { createDeepSeek } from '@ai-sdk/deepseek';
|
||||
import type { ProviderInfo, ProviderFactory } from '../types.js';
|
||||
|
||||
export const deepseekProvider: ProviderInfo = {
|
||||
id: 'deepseek',
|
||||
name: 'DeepSeek',
|
||||
description: 'DeepSeek AI models',
|
||||
builtin: true,
|
||||
apiKeyEnvVar: 'DEEPSEEK_API_KEY',
|
||||
models: [
|
||||
{
|
||||
id: 'deepseek-chat',
|
||||
name: 'DeepSeek Chat',
|
||||
capabilities: { vision: false, functionCalling: true, streaming: true },
|
||||
contextWindow: 64000,
|
||||
maxOutput: 8192,
|
||||
},
|
||||
{
|
||||
id: 'deepseek-coder',
|
||||
name: 'DeepSeek Coder',
|
||||
capabilities: { vision: false, functionCalling: true, streaming: true },
|
||||
contextWindow: 64000,
|
||||
maxOutput: 8192,
|
||||
},
|
||||
{
|
||||
id: 'deepseek-reasoner',
|
||||
name: 'DeepSeek Reasoner',
|
||||
capabilities: { vision: false, functionCalling: false, streaming: true },
|
||||
contextWindow: 64000,
|
||||
maxOutput: 8192,
|
||||
},
|
||||
],
|
||||
allowCustomModels: true,
|
||||
};
|
||||
|
||||
export const deepseekFactory: ProviderFactory = ({ apiKey, baseUrl }) => {
|
||||
const client = createDeepSeek({ apiKey, baseURL: baseUrl });
|
||||
return (model) => client(model);
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Builtin Providers
|
||||
*
|
||||
* 内置提供商定义
|
||||
*/
|
||||
|
||||
import { anthropicProvider, anthropicFactory } from './anthropic.js';
|
||||
import { openaiProvider, openaiFactory } from './openai.js';
|
||||
import { deepseekProvider, deepseekFactory } from './deepseek.js';
|
||||
import type { ProviderInfo, ProviderFactory } from '../types.js';
|
||||
|
||||
export interface BuiltinProvider {
|
||||
info: ProviderInfo;
|
||||
factory: ProviderFactory;
|
||||
}
|
||||
|
||||
/** 所有内置提供商 */
|
||||
export const builtinProviders: Record<string, BuiltinProvider> = {
|
||||
anthropic: {
|
||||
info: anthropicProvider,
|
||||
factory: anthropicFactory,
|
||||
},
|
||||
openai: {
|
||||
info: openaiProvider,
|
||||
factory: openaiFactory,
|
||||
},
|
||||
deepseek: {
|
||||
info: deepseekProvider,
|
||||
factory: deepseekFactory,
|
||||
},
|
||||
};
|
||||
|
||||
/** 获取所有内置提供商列表 */
|
||||
export function getBuiltinProviders(): BuiltinProvider[] {
|
||||
return Object.values(builtinProviders);
|
||||
}
|
||||
|
||||
/** 获取内置提供商 */
|
||||
export function getBuiltinProvider(id: string): BuiltinProvider | undefined {
|
||||
return builtinProviders[id];
|
||||
}
|
||||
|
||||
/** 检查是否为内置提供商 */
|
||||
export function isBuiltinProvider(id: string): boolean {
|
||||
return id in builtinProviders;
|
||||
}
|
||||
|
||||
export { anthropicProvider, anthropicFactory } from './anthropic.js';
|
||||
export { openaiProvider, openaiFactory } from './openai.js';
|
||||
export { deepseekProvider, deepseekFactory } from './deepseek.js';
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* OpenAI Provider Definition
|
||||
*/
|
||||
|
||||
import { createOpenAI } from '@ai-sdk/openai';
|
||||
import { createQwen } from 'qwen-ai-provider-v5';
|
||||
import type { ProviderInfo, ProviderFactory } from '../types.js';
|
||||
|
||||
export const openaiProvider: ProviderInfo = {
|
||||
id: 'openai',
|
||||
name: 'OpenAI',
|
||||
description: 'GPT models by OpenAI (also supports OpenAI-compatible APIs)',
|
||||
builtin: true,
|
||||
apiKeyEnvVar: 'OPENAI_API_KEY',
|
||||
models: [
|
||||
{
|
||||
id: 'gpt-4o',
|
||||
name: 'GPT-4o',
|
||||
capabilities: { vision: true, functionCalling: true, streaming: true },
|
||||
contextWindow: 128000,
|
||||
maxOutput: 16384,
|
||||
},
|
||||
{
|
||||
id: 'gpt-4o-mini',
|
||||
name: 'GPT-4o Mini',
|
||||
capabilities: { vision: true, functionCalling: true, streaming: true },
|
||||
contextWindow: 128000,
|
||||
maxOutput: 16384,
|
||||
},
|
||||
{
|
||||
id: 'gpt-4-turbo',
|
||||
name: 'GPT-4 Turbo',
|
||||
capabilities: { vision: true, functionCalling: true, streaming: true },
|
||||
contextWindow: 128000,
|
||||
maxOutput: 4096,
|
||||
},
|
||||
{
|
||||
id: 'o1',
|
||||
name: 'o1',
|
||||
capabilities: { vision: false, functionCalling: false, streaming: true },
|
||||
contextWindow: 200000,
|
||||
maxOutput: 100000,
|
||||
},
|
||||
{
|
||||
id: 'o1-mini',
|
||||
name: 'o1 Mini',
|
||||
capabilities: { vision: false, functionCalling: false, streaming: true },
|
||||
contextWindow: 128000,
|
||||
maxOutput: 65536,
|
||||
},
|
||||
],
|
||||
allowCustomModels: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查 baseUrl 是否为阿里云百炼/DashScope
|
||||
*/
|
||||
function isDashScopeUrl(baseUrl?: string): boolean {
|
||||
if (!baseUrl) return false;
|
||||
return baseUrl.includes('dashscope');
|
||||
}
|
||||
|
||||
export const openaiFactory: ProviderFactory = ({ apiKey, baseUrl }) => {
|
||||
// 如果是百炼的 URL,使用 qwen provider
|
||||
if (isDashScopeUrl(baseUrl)) {
|
||||
const client = createQwen({ apiKey, baseURL: baseUrl });
|
||||
return (model) => client(model);
|
||||
}
|
||||
const client = createOpenAI({ apiKey, baseURL: baseUrl });
|
||||
return (model) => client(model);
|
||||
};
|
||||
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Provider Configuration Persistence
|
||||
*
|
||||
* 提供商配置持久化
|
||||
*/
|
||||
|
||||
import { existsSync } from 'node:fs';
|
||||
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { homedir } from 'node:os';
|
||||
import type { ProvidersConfigFile, CustomProviderDefinition, ProviderConfig } from './types.js';
|
||||
|
||||
/** 配置目录名 */
|
||||
const CONFIG_DIR = '.ai-terminal-assistant';
|
||||
|
||||
/** 配置文件名 */
|
||||
const CONFIG_FILE = 'providers.json';
|
||||
|
||||
/**
|
||||
* 获取配置目录路径
|
||||
*/
|
||||
export function getConfigDir(workdir?: string): string {
|
||||
return workdir ? join(workdir, CONFIG_DIR) : join(homedir(), CONFIG_DIR);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置文件路径
|
||||
*/
|
||||
export function getConfigPath(workdir?: string): string {
|
||||
return join(getConfigDir(workdir), CONFIG_FILE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载提供商配置
|
||||
*/
|
||||
export async function loadProvidersConfig(workdir?: string): Promise<ProvidersConfigFile> {
|
||||
const configPath = getConfigPath(workdir);
|
||||
|
||||
if (!existsSync(configPath)) {
|
||||
return { providers: {}, configs: {} };
|
||||
}
|
||||
|
||||
try {
|
||||
const content = await readFile(configPath, 'utf-8');
|
||||
const config = JSON.parse(content) as ProvidersConfigFile;
|
||||
return {
|
||||
providers: config.providers ?? {},
|
||||
configs: config.configs ?? {},
|
||||
};
|
||||
} catch {
|
||||
// 解析失败返回空配置
|
||||
return { providers: {}, configs: {} };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存提供商配置
|
||||
*/
|
||||
export async function saveProvidersConfig(
|
||||
config: ProvidersConfigFile,
|
||||
workdir?: string
|
||||
): Promise<void> {
|
||||
const configDir = getConfigDir(workdir);
|
||||
const configPath = getConfigPath(workdir);
|
||||
|
||||
// 确保目录存在
|
||||
if (!existsSync(configDir)) {
|
||||
await mkdir(configDir, { recursive: true });
|
||||
}
|
||||
|
||||
await writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取提供商的 API Key
|
||||
* 优先从环境变量获取,其次从配置获取
|
||||
*/
|
||||
export function resolveApiKey(config?: ProviderConfig, envVar?: string): string | undefined {
|
||||
// 优先使用配置中指定的环境变量
|
||||
if (config?.apiKeyEnvVar) {
|
||||
const key = process.env[config.apiKeyEnvVar];
|
||||
if (key) return key;
|
||||
}
|
||||
|
||||
// 其次使用默认环境变量
|
||||
if (envVar) {
|
||||
const key = process.env[envVar];
|
||||
if (key) return key;
|
||||
}
|
||||
|
||||
// 最后使用直接配置的 apiKey
|
||||
return config?.apiKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并自定义提供商定义和配置
|
||||
*/
|
||||
export function mergeProviderConfig(
|
||||
definition: CustomProviderDefinition,
|
||||
config?: ProviderConfig
|
||||
): CustomProviderDefinition & { enabled: boolean } {
|
||||
return {
|
||||
...definition,
|
||||
// 配置可以覆盖 baseUrl
|
||||
baseUrl: config?.baseUrl ?? definition.baseUrl,
|
||||
// 合并模型列表
|
||||
models: [...(definition.models ?? []), ...(config?.customModels ?? [])],
|
||||
enabled: config?.enabled ?? true,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Provider Module
|
||||
*
|
||||
* 模型提供商管理模块
|
||||
*/
|
||||
|
||||
// Types
|
||||
export type {
|
||||
BuiltinProviderType,
|
||||
ProviderType,
|
||||
ModelCapabilities,
|
||||
ModelInfo,
|
||||
ProviderInfo,
|
||||
ProviderConfig,
|
||||
CustomProviderDefinition,
|
||||
ConnectionTestResult,
|
||||
ProviderFactory,
|
||||
RegisteredProvider,
|
||||
ProvidersConfigFile,
|
||||
ProviderListItem,
|
||||
ProviderDetail,
|
||||
} from './types.js';
|
||||
|
||||
// Registry
|
||||
export {
|
||||
ProviderRegistry,
|
||||
providerRegistry,
|
||||
getProviderRegistry,
|
||||
} from './registry.js';
|
||||
|
||||
// Builtin providers
|
||||
export {
|
||||
builtinProviders,
|
||||
getBuiltinProviders,
|
||||
getBuiltinProvider,
|
||||
isBuiltinProvider,
|
||||
} from './builtin/index.js';
|
||||
|
||||
// Config utilities
|
||||
export {
|
||||
loadProvidersConfig,
|
||||
saveProvidersConfig,
|
||||
resolveApiKey,
|
||||
getConfigDir,
|
||||
getConfigPath,
|
||||
} from './config.js';
|
||||
|
||||
// Utils
|
||||
export {
|
||||
testOpenAICompatibleConnection,
|
||||
createOpenAICompatibleFactory,
|
||||
isValidProviderId,
|
||||
isValidUrl,
|
||||
} from './utils.js';
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* Provider Module Types
|
||||
*
|
||||
* 模型提供商类型定义
|
||||
*/
|
||||
|
||||
import type { LanguageModel } from 'ai';
|
||||
|
||||
/** 内置提供商类型 */
|
||||
export type BuiltinProviderType = 'anthropic' | 'deepseek' | 'openai';
|
||||
|
||||
/** 提供商类型(内置 + 自定义) */
|
||||
export type ProviderType = BuiltinProviderType | string;
|
||||
|
||||
/** 模型能力 */
|
||||
export interface ModelCapabilities {
|
||||
/** 支持视觉/图片 */
|
||||
vision?: boolean;
|
||||
/** 支持函数调用 */
|
||||
functionCalling?: boolean;
|
||||
/** 支持流式输出 */
|
||||
streaming?: boolean;
|
||||
}
|
||||
|
||||
/** 模型信息 */
|
||||
export interface ModelInfo {
|
||||
/** 模型 ID(如 'claude-sonnet-4-20250514') */
|
||||
id: string;
|
||||
/** 显示名称(如 'Claude Sonnet 4') */
|
||||
name: string;
|
||||
/** 模型能力 */
|
||||
capabilities?: ModelCapabilities;
|
||||
/** 上下文窗口大小(tokens) */
|
||||
contextWindow?: number;
|
||||
/** 最大输出 tokens */
|
||||
maxOutput?: number;
|
||||
}
|
||||
|
||||
/** 提供商元信息 */
|
||||
export interface ProviderInfo {
|
||||
/** 提供商唯一 ID */
|
||||
id: string;
|
||||
/** 显示名称 */
|
||||
name: string;
|
||||
/** 描述 */
|
||||
description?: string;
|
||||
/** 是否为内置提供商 */
|
||||
builtin: boolean;
|
||||
/** API 基础 URL */
|
||||
baseUrl?: string;
|
||||
/** API Key 环境变量名 */
|
||||
apiKeyEnvVar?: string;
|
||||
/** 可用模型列表 */
|
||||
models: ModelInfo[];
|
||||
/** 是否允许自定义模型 */
|
||||
allowCustomModels?: boolean;
|
||||
}
|
||||
|
||||
/** 提供商配置(用户设置) */
|
||||
export interface ProviderConfig {
|
||||
/** 提供商 ID */
|
||||
id: string;
|
||||
/** API Key(直接存储,不推荐) */
|
||||
apiKey?: string;
|
||||
/** API Key 环境变量名 */
|
||||
apiKeyEnvVar?: string;
|
||||
/** 自定义 Base URL */
|
||||
baseUrl?: string;
|
||||
/** 是否启用 */
|
||||
enabled?: boolean;
|
||||
/** 用户添加的自定义模型 */
|
||||
customModels?: ModelInfo[];
|
||||
}
|
||||
|
||||
/** 自定义提供商定义(用户注册时使用) */
|
||||
export interface CustomProviderDefinition {
|
||||
/** 提供商唯一 ID */
|
||||
id: string;
|
||||
/** 显示名称 */
|
||||
name: string;
|
||||
/** 描述 */
|
||||
description?: string;
|
||||
/** API 基础 URL(必填) */
|
||||
baseUrl: string;
|
||||
/** API Key 环境变量名 */
|
||||
apiKeyEnvVar?: string;
|
||||
/** 可用模型列表 */
|
||||
models?: ModelInfo[];
|
||||
/** 是否允许自定义模型 */
|
||||
allowCustomModels?: boolean;
|
||||
}
|
||||
|
||||
/** 连接测试结果 */
|
||||
export interface ConnectionTestResult {
|
||||
/** 是否成功 */
|
||||
success: boolean;
|
||||
/** 延迟(毫秒) */
|
||||
latency?: number;
|
||||
/** 错误信息 */
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/** Provider 工厂函数 */
|
||||
export type ProviderFactory = (config: {
|
||||
apiKey: string;
|
||||
baseUrl?: string;
|
||||
}) => (model: string) => LanguageModel;
|
||||
|
||||
/** 已注册的提供商(运行时) */
|
||||
export interface RegisteredProvider {
|
||||
/** 提供商信息 */
|
||||
info: ProviderInfo;
|
||||
/** 模型工厂函数 */
|
||||
factory: ProviderFactory;
|
||||
/** 用户配置 */
|
||||
config?: ProviderConfig;
|
||||
}
|
||||
|
||||
/** 提供商配置文件格式 */
|
||||
export interface ProvidersConfigFile {
|
||||
/** 自定义提供商定义 */
|
||||
providers?: Record<string, CustomProviderDefinition>;
|
||||
/** 提供商配置(API Key、选项等) */
|
||||
configs?: Record<string, ProviderConfig>;
|
||||
}
|
||||
|
||||
/** 提供商列表项(API 响应用) */
|
||||
export interface ProviderListItem {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
builtin: boolean;
|
||||
enabled: boolean;
|
||||
hasApiKey: boolean;
|
||||
modelCount: number;
|
||||
}
|
||||
|
||||
/** 提供商详情(API 响应用) */
|
||||
export 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[];
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* Provider Utilities
|
||||
*
|
||||
* 提供商工具函数
|
||||
*/
|
||||
|
||||
import { createOpenAI } from '@ai-sdk/openai';
|
||||
import type { ConnectionTestResult, ProviderFactory } from './types.js';
|
||||
|
||||
/**
|
||||
* 测试提供商连接
|
||||
* 通过简单的模型列表请求测试连接是否正常
|
||||
*/
|
||||
export async function testProviderConnection(
|
||||
factory: ProviderFactory,
|
||||
apiKey: string,
|
||||
baseUrl?: string,
|
||||
testModel?: string
|
||||
): Promise<ConnectionTestResult> {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// 创建模型实例
|
||||
const getModel = factory({ apiKey, baseUrl });
|
||||
const model = getModel(testModel ?? 'test');
|
||||
|
||||
// 尝试获取模型信息(这会触发连接验证)
|
||||
// 由于不同提供商的验证方式不同,这里使用一个简单的方法
|
||||
// 实际上只是验证工厂函数能否正常创建模型
|
||||
if (!model) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Failed to create model instance',
|
||||
};
|
||||
}
|
||||
|
||||
const latency = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
latency,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
latency: Date.now() - startTime,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试 OpenAI 兼容 API 连接
|
||||
* 通过 /v1/models 端点测试
|
||||
*/
|
||||
export async function testOpenAICompatibleConnection(
|
||||
apiKey: string,
|
||||
baseUrl: string
|
||||
): Promise<ConnectionTestResult> {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// 构建 models 端点 URL
|
||||
const modelsUrl = baseUrl.endsWith('/')
|
||||
? `${baseUrl}models`
|
||||
: `${baseUrl}/models`;
|
||||
|
||||
const response = await fetch(modelsUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
const latency = Date.now() - startTime;
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
error: `HTTP ${response.status}: ${response.statusText}`,
|
||||
latency,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
latency,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Network error',
|
||||
latency: Date.now() - startTime,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 OpenAI 兼容的提供商工厂
|
||||
* 用于自定义 OpenAI 兼容服务(如 Ollama、vLLM 等)
|
||||
*/
|
||||
export function createOpenAICompatibleFactory(baseUrl: string): ProviderFactory {
|
||||
return ({ apiKey }) => {
|
||||
const client = createOpenAI({
|
||||
apiKey: apiKey || 'dummy-key', // 某些本地服务不需要 key
|
||||
baseURL: baseUrl,
|
||||
});
|
||||
return (model) => client(model);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证提供商 ID 格式
|
||||
*/
|
||||
export function isValidProviderId(id: string): boolean {
|
||||
// 只允许字母、数字、连字符和下划线
|
||||
return /^[a-zA-Z][a-zA-Z0-9_-]*$/.test(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 URL 格式
|
||||
*/
|
||||
export function isValidUrl(url: string): boolean {
|
||||
try {
|
||||
new URL(url);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
import { z } from 'zod';
|
||||
import type { ProviderType } from '../provider/types.js';
|
||||
|
||||
// Re-export ProviderType from provider module
|
||||
export type { ProviderType } from '../provider/types.js';
|
||||
|
||||
// 内容块类型(支持多模态)
|
||||
export interface TextContentBlock {
|
||||
@@ -65,9 +69,6 @@ export interface ToolCall {
|
||||
input: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// 支持的 Provider 类型
|
||||
export type ProviderType = 'anthropic' | 'deepseek' | 'openai';
|
||||
|
||||
// Agent 配置
|
||||
export interface AgentConfig {
|
||||
provider: ProviderType;
|
||||
|
||||
Reference in New Issue
Block a user