跳到主要内容

02 · 供应商注册表

这章讲:30 多家供应商的差异写在哪、怎么注册、以及上层一句 createModel(settings) 是如何被翻译成“具体哪个类、什么 host、什么 key”的。

2.1 它要解决的小问题

第1章的基类要求每个供应商提供 getChatModel()。但“有哪些供应商、各自默认 host 是什么、有哪些模型、模型能力如何”这些元数据 + 工厂函数得有地方放,而且要能:

  • 让上层只凭一个字符串 id('claude''ollama')就拿到正确的实现;
  • 让 UI 能列出“系统内置供应商”给用户挑;
  • 让用户自定义一个没内置的供应商(填个 OpenAI 兼容地址即可)。

2.2 思路:一个全局 Map + import 副作用

注册表本体异常简单——就是一个 Map<string, ProviderDefinition>(src/shared/providers/registry.ts:4):

const providerRegistry = new Map<string, ProviderDefinition>()
export function defineProvider(definition) {
providerRegistry.set(definition.id, definition) // 注册即入 Map
return definition
}

关键在注册时机:每个供应商定义文件在模块顶层就调用 defineProvider(...),所以只要 import 那个文件,注册就发生了src/shared/providers/index.ts:7-31 用一串纯副作用 import 把所有内置供应商一次性灌进去:

import './definitions/chatboxai' // ChatboxAI 故意第一个 → 列表置顶
import './definitions/openai'
import './definitions/claude'
// … 共 24 个内置供应商

注释明说:import 顺序决定 UI 显示顺序(index.ts:6-7)。这是“用 import 副作用做注册”的常见模式,代价是顺序变敏感、tree-shaking 不能动它们。

2.3 一个供应商定义长什么样

以 Claude 为例(src/shared/providers/definitions/claude.ts:6)。一个定义 = 元数据 + 一个 createModel 工厂:

export const claudeProvider = defineProvider({
id: ModelProviderEnum.Claude,
name: 'Claude',
defaultSettings: {
apiHost: 'https://api.anthropic.com/v1',
models: [
{ modelId: 'claude-opus-4-8', contextWindow: 1_000_000, maxOutput: 32_000,
capabilities: ['vision','reasoning','tool_use'] },
// …
],
},
createModel: (config) => new Claude({ /* host/key/headers… */ }, config.dependencies),
})

两点值得注意:

  • capabilities 在这里声明,它最终决定第4章工具门控的结果(模型不标 tool_use 就拿不到工具)。
  • OAuth 分支:Claude 支持用 OAuth token(sk-ant-oat-*)而非 API key,此时要加 beta header claude-code-20250219,oauth-2025-04-20 并改用 Bearer 自定义 fetch(claude.ts:57-86)。这说明定义文件也是“认证差异”的归宿。

2.4 主线:从设置到实例

上层(orchestration)调的是 getModel(settings, globalSettings, config, deps)(src/shared/providers/index.ts:129)。它的翻译流程:

怎么读:从左到右是“拿 id → 查定义 → 凑齐参数 → 造实例”。

settings.provider ('claude')


getProviderDefinition(id) ── 命中? ──► 用定义里的 createModel 工厂
│ 未命中 ▲
▼ │ 传入: host/key/model/能力
providerBaseInfo.isCustom ? ──是──► createCustomProviderModel (OpenAI 兼容)
│ 否

抛错: 找不到该 provider

关键步骤:

  • getProviderSettings() 把会话设置 + 全局设置合并,算出最终 host(OAuth 激活时强制用默认 host,因为 token 是发给特定端点的)(index.ts:65-93)。
  • getModelConfig() 逐级回退找该 modelId 的元数据,最后 enrichModelFromRegistry() 补上能力/上下文窗口(index.ts:98-120)。
  • 命中注册定义 → 调 providerDefinition.createModel(createConfig)(index.ts:164)。

2.5 精华:自定义供应商 = OpenAI 兼容回退

用户自己加的供应商不在注册表里。Chatbox 的处理是:只要它 isCustom,就用 createCustomProviderModel()OpenAI 兼容实现(index.ts:171-174)。

这背后是一个事实判断:“OpenAI 的 /chat/completions 协议已是事实标准”——Ollama、LM Studio、SiliconFlow、众多第三方网关都兼容它。所以一个 OpenAICompatible 子类(src/shared/models/openai-compatible.ts)就能覆盖长尾。该子类还重写了 isSupportToolUse(scope)(openai-compatible.ts:42),因为兼容端点对工具的支持参差不齐,需要更细的门控。

2.6 边界

  • 注册表是纯内存的,没有持久化;每次启动靠 import 重建。
  • 自定义供应商的能力(vision/tool)由用户在 UI 勾选,不像内置那样有权威清单,容易填错导致工具门控误判。

2.7 代码地图

主题文件符号
注册表本体src/shared/providers/registry.tsdefineProvider / getProviderDefinition / getSystemProviders
副作用注册 + 顺序src/shared/providers/index.ts(顶部 import 串)
设置→实例主线src/shared/providers/index.tsgetModel / getProviderSettings / getModelConfig
自定义供应商回退src/shared/providers/utils.tscreateCustomProviderModel
供应商定义样例src/shared/providers/definitions/claude.tsclaudeProvider
OpenAI 兼容子类src/shared/models/openai-compatible.tsisSupportToolUse
能力富化src/shared/model-registry/enrich.tsenrichModelFromRegistry