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
@@ -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);
};
+110
View File
@@ -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,
};
}
+54
View File
@@ -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';
+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;
}
+154
View File
@@ -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[];
};
}
+131
View File
@@ -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;
}
}