多 LLM Provider 的适配器模式 — 一套接口支持 10+ 模型
难度: ⭐⭐⭐ | 字数: ~2500 字核心代码:
src/chat/client.ts→createClient()+src/providers/manager.ts关键词: OpenAI SDK, 适配器模式, 多 Provider, API 认证头, Endpoint 切换
一、问题:每个 LLM 厂商的 API 都不太一样
| DeepSeek | https://api.deepseek.com/v1 | Authorization: Bearer sk-xxx | |
| MiMo (小米) | https://api.xiaomimimo.com/v1 | api-key: mk-xxx | |
| OpenAI | https://api.openai.com/v1 | Authorization: Bearer sk-xxx | |
| 自定义代理 | https://your-proxy.com |
虽然它们都声称"兼容 OpenAI API 格式",但在细节上总有差异。CodeHi 需要让用户无缝切换这些 Provider,而不需要关心底层差异。
二、核心抽象:createClient 工厂函数
exportfunctioncreateClient(config: ProviderConfig): OpenAI {constheaders: Record<string, string> = {};// ① URL 标准化:去掉用户可能多写的 /chat/completionslet baseURL = config.apiUrl; baseURL = baseURL.replace(/\/chat\/completions\/?$/, '');// ② 认证头类型分发switch (config.headerType) {case'api-key': headers['api-key'] = config.apiKey;break;case'none':// 不添加额外认证头(某些代理只需要 URL)break;// 'bearer':走默认逻辑,SDK 自动添加 Authorization: Bearer xxx }returnnewOpenAI({baseURL: baseURL,apiKey: config.apiKey || 'no-key',defaultHeaders: headers, });}设计要点:
1. URL 容错处理
用户可能从 OpenAI 文档复制完整的 chat completions URL:
https://api.deepseek.com/v1/chat/completions ← 多了 /chat/completionshttps://api.deepseek.com/v1 ← 正确格式OpenAI SDK 会在 baseURL 后面自动拼接 /chat/completions。如果用户已经写了,最终会变成:
https://api.deepseek.com/v1/chat/completions/chat/completions ← 404!所以 CodeHi 用一行正则预处理:
baseURL = baseURL.replace(/\/chat\/completions\/?$/, '');2. 三种认证头类型
OpenAI SDK 默认行为是 Authorization: Bearer <apiKey>,这对 DeepSeek 和 OpenAI 本身没问题。但 MiMo 使用自定义头 api-key,某些内网代理可能不需要任何认证头(如 Ollama 本地部署)。
switch (config.headerType) {case'api-key': headers['api-key'] = config.apiKey; break;case'none': /* 什么都不做 */break;// 默认:bearer → SDK 自动处理}三、多 Endpoint 支持:一个 Provider 可以有多个连接
现实场景:同一个 DeepSeek Provider,你可能同时有:
🏢 公司订阅的 API → https://company.deepseek-proxy.com (api-key: mk-xxx)🏠 个人 API Key → https://api.deepseek.com/v1 (bearer: sk-yyy)CodeHi 引入了 Endpoint 概念:每个 Provider 下面可以有多个端点,每个端点有独立的 URL、Key、认证类型。
exportinterfaceEndpoint {id: string; // 唯一标识,如 "company"、"personal"label: string; // 显示名称apiUrl: string;apiKey: string; headerType?: 'bearer' | 'api-key' | 'none';}exportinterfaceProviderConfig {name: string;displayName: string;apiKey: string; // 默认端点的 Keymodel: string; // 默认模型 models?: string[]; // 可选模型列表apiUrl: string; // 默认端点的 URLheaderType: 'bearer' | 'api-key' | 'none'; endpoints?: Endpoint[]; // 多端点列表}Endpoint 覆盖逻辑:
publicgetEndpointConfig(providerName: string, endpointId?: string) {const config = this.getProviderConfigByName(providerName);if (!config) returnnull;// 查找指定 endpointif (endpointId && config.endpoints) {const ep = config.endpoints.find(e => e.id === endpointId);if (ep) {return {apiUrl: ep.apiUrl,apiKey: ep.apiKey,headerType: ep.headerType || config.headerType// endpoint 级可覆盖 }; } }// 回退:使用第一个 endpoint 或 Provider 顶层配置if (config.endpoints && config.endpoints.length > 0) {const first = config.endpoints[0];return {apiUrl: first.apiUrl,apiKey: first.apiKey,headerType: first.headerType || config.headerType }; }return {apiUrl: config.apiUrl,apiKey: config.apiKey,headerType: config.headerType };}覆盖优先级(从高到低):
Endpoint.headerType > Provider.headerTypeEndpoint.apiUrl > Provider.apiUrlEndpoint.apiKey > Provider.apiKey四、连接测试:调用 /models 验证可用性
添加新 Provider 后如何验证连接?CodeHi 实现了端点测试:
publicasynctestEndpointConnection(apiUrl: string,apiKey: string,headerType: 'bearer' | 'api-key' | 'none'): Promise<{ success: boolean; message: string }> {try {const client = createClient({name: '_test_',displayName: '_test_', apiKey, apiUrl, headerType,model: '', } asProviderConfig);const resp = await client.models.list();const count = resp?.data?.length ?? 0;return { success: true, message: `✅ 连接成功,可用模型 ${count} 个` }; } catch (e: any) {const msg = e?.message || String(e);const short = msg.length > 120 ? msg.slice(0, 120) + '…' : msg;return { success: false, message: `❌ ${short}` }; }}这里用了一个巧妙的技巧——SDK 的 models.list() 接口,它只需要认证通过就能返回模型列表。对于未配置模型的 Provider,虽然 model: '' 是空字符串,但 SDK 不会在 models.list() 中使用它。
错误信息截断到 120 字符,避免大段 HTML 错误页面污染 UI。
五、内置 vs 自定义 Provider
CodeHi 将 Provider 分为两类:
// 内置 Provider:硬编码配置 + 用户配置覆盖constbuiltIn: ProviderInfo[] = [ {name: 'mimo',displayName: 'MiMo',models: ['mimo-v2.5', 'mimo-v2.5-pro'],defaultModel: this.config.get<string>('mimoModel') || 'mimo-v2.5-pro',endpoints: [ { id: 'official', label: '默认' }, ...(builtInExtraEndpoints['mimo'] || []) ] }, {name: 'deepseek',displayName: 'DeepSeek',models: ['deepseek-v4-pro', 'deepseek-v4-flash'],defaultModel: this.config.get<string>('deepseekModel') || 'deepseek-v4-pro',endpoints: [ { id: 'official', label: '默认' }, ...(builtInExtraEndpoints['deepseek'] || []) ] }];// 自定义 Provider:完全由用户配置constcustom: ProviderInfo[] = customProviders.map(p => ({name: p.name,displayName: p.name,models: p.models?.length ? p.models : [p.model],defaultModel: p.model,endpoints: p.endpoints?.map(ep => ({ id: ep.id, label: ep.label })) || []}));return [...builtIn, ...custom];内置 Provider 的特殊待遇:
模型列表由代码维护(硬编码),不会因用户配置错误而丢失 默认端点 official始终存在用户在 VS Code 设置中配置 API Key,自动映射到 official endpoint 用户可以额外添加自定义 endpoint(如公司代理)
六、自动切换 Provider 的 UX 优化
ProviderManager 的事件驱动机制(详见文章 #3)包含了几种智能切换场景:
case'endpoints-changed': {// 场景1: 当前 Provider 无任何 Key,但变更的 Provider 有 Key → 自动切换if (!(curCfg?.endpoints?.some(ep => !!ep.apiKey))) {if (changedCfg?.endpoints?.some(ep => !!ep.apiKey)) {awaitthis.globalState.update('CodeHiAgent_provider', e.providerName);// ... } }// 场景2: 当前 Provider 就是变更的那个,但当前 endpoint 无 Key// → 自动切到第一个有 Key 的 endpointif (currentProvider === e.providerName) {const currentEp = changedCfg?.endpoints?.find(ep => ep.id === currentEpId);if (!currentEp || !currentEp.apiKey) {const firstWithKey = changedCfg?.endpoints?.find(ep => !!ep.apiKey);if (firstWithKey) {awaitthis.globalState.update('CodeHiAgent_endpoint', firstWithKey.id); } } }}这避免了用户看到"请配置 API Key"错误后还要手动去切换 Provider/Endpoint 的复杂交互。
七、总结
CodeHi 的多 Provider 架构用 150 行配置管理代码 + 30 行工厂函数 实现了:
/chat/completions | |
/models 接口验证 | |
所有这些对外暴露的统一接口只有一个:OpenAI SDK 客户端。无论底层是什么 Provider、什么认证方式,上层 Agent Loop 和 stream 处理完全不需要关心。
下一篇预告:#12 如何给 AI 设计一套安全的文件操作工具
💡 CodeHi 正在 VS Code Marketplace 可安装 — 一个安全的多 Provider AI 编程助手,支持写入开关、Checkpoint 回滚、25+ 工具调用。
夜雨聆风