乐于分享
好东西不私藏

8. 从零构建 VS Code AI 编程agent助手-多 LLM Provider 的适配器模式 — 一套接口支持 10+ 模型

8. 从零构建 VS Code AI 编程agent助手-多 LLM Provider 的适配器模式 — 一套接口支持 10+ 模型

多 LLM Provider 的适配器模式 — 一套接口支持 10+ 模型

难度: ⭐⭐⭐ | 字数: ~2500 字核心代码src/chat/client.ts → createClient() + src/providers/manager.ts关键词: OpenAI SDK, 适配器模式, 多 Provider, API 认证头, Endpoint 切换


一、问题:每个 LLM 厂商的 API 都不太一样

厂商
API URL
认证方式
Header
DeepSeekhttps://api.deepseek.com/v1
Bearer Token
Authorization: Bearer sk-xxx
MiMo (小米)https://api.xiaomimimo.com/v1
API Key
api-key: mk-xxx
OpenAIhttps://api.openai.com/v1
Bearer Token
Authorization: Bearer sk-xxx
自定义代理https://your-proxy.com
可能无认证

虽然它们都声称"兼容 OpenAI API 格式",但在细节上总有差异。CodeHi 需要让用户无缝切换这些 Provider,而不需要关心底层差异。


二、核心抽象:createClient 工厂函数

exportfunctioncreateClient(config: ProviderConfig): OpenAI {constheadersRecord<stringstring> = {};// ① 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.apiKeybreak;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 {idstring;       // 唯一标识,如 "company"、"personal"labelstring;    // 显示名称apiUrlstring;apiKeystring;  headerType?: 'bearer' | 'api-key' | 'none';}exportinterfaceProviderConfig {namestring;displayNamestring;apiKeystring;         // 默认端点的 Keymodelstring;          // 默认模型  models?: string[];      // 可选模型列表apiUrlstring;         // 默认端点的 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(apiUrlstring,apiKeystring,headerType'bearer' | 'api-key' | 'none'): Promise<{ successbooleanmessagestring }> {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 { successtruemessage`✅ 连接成功,可用模型 ${count} 个` };  } catch (eany) {const msg = e?.message || String(e);const short = msg.length > 120 ? msg.slice(0120) + '…' : msg;return { successfalsemessage`❌ ${short}` };  }}

这里用了一个巧妙的技巧——SDK 的 models.list() 接口,它只需要认证通过就能返回模型列表。对于未配置模型的 Provider,虽然 model: '' 是空字符串,但 SDK 不会在 models.list() 中使用它。

错误信息截断到 120 字符,避免大段 HTML 错误页面污染 UI。


五、内置 vs 自定义 Provider

CodeHi 将 Provider 分为两类:

// 内置 Provider:硬编码配置 + 用户配置覆盖constbuiltInProviderInfo[] = [  {name'mimo',displayName'MiMo',models: ['mimo-v2.5''mimo-v2.5-pro'],defaultModelthis.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'],defaultModelthis.config.get<string>('deepseekModel') || 'deepseek-v4-pro',endpoints: [      { id'official'label'默认' },      ...(builtInExtraEndpoints['deepseek'] || [])    ]  }];// 自定义 Provider:完全由用户配置constcustomProviderInfo[] = 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.idlabel: 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 行工厂函数 实现了:

能力
实现
内置 Provider(Mimo/DeepSeek)
硬编码模型列表 + 用户 Key 覆盖
自定义 Provider
完整 CRUD + 持久化到 VS Code 配置
多 Endpoint
每个 Provider 下支持多个连接配置
三种认证方式
Bearer / API-Key / None
URL 容错
自动去掉多余的 /chat/completions
连接测试
通过 /models 接口验证
智能切换
配好 Key 后自动切到可用 Provider

所有这些对外暴露的统一接口只有一个:OpenAI SDK 客户端。无论底层是什么 Provider、什么认证方式,上层 Agent Loop 和 stream 处理完全不需要关心。


下一篇预告:#12 如何给 AI 设计一套安全的文件操作工具

💡 CodeHi 正在 VS Code Marketplace 可安装 — 一个安全的多 Provider AI 编程助手,支持写入开关、Checkpoint 回滚、25+ 工具调用。