Vercel AI SDK v5 多 Provider 架构
Cherry Studio 基于 Vercel AI SDK v5 的多 Provider 架构
学习目标
- 理解 Cherry Studio
packages/aiCore/的整体架构与三层分离设计 - 掌握 ProviderExtension、ExtensionRegistry、RuntimeExecutor 的协同工作流
- 了解 Cherry Studio 特有的设计决策:自定义 OpenAI fork、工具能力解析、变体模式
- 能够在 Cherry Studio 的插件体系中编写自定义插件
前置知识
- Vercel AI SDK v5 HubProvider 多 Provider 聚合模式 — HubProvider 模式的通用概念、ProviderExtension 与 PluginEngine 的基础原理
- 模型提供商抽象层 — 多模型路由与故障转移的基本概念
本章不重复通用知识,仅聚焦 Cherry Studio 的具体实现方案。请先阅读上方的 common 教程。
项目实践
1. 整体架构:三层分离
Cherry Studio 的 AI SDK 封装层位于 packages/aiCore/,采用三层分离架构,与通用 HubProvider 模式相比,职责划分更为精细:
三层职责明确:
| 层级 | 目录 | 核心类 | 职责 |
|---|---|---|---|
| Provider 注册层 | core/providers/ | ExtensionRegistry、ProviderExtension | 管理 Provider 的注册、缓存、实例化、变体、工具工厂 |
| Runtime 运行时层 | core/runtime/ | RuntimeExecutor、PluginEngine | 处理请求管线、插件执行、模型解析、API 调用 |
| 模型层 | core/models/ | isV3Model、isV2Model | 模型版本检测与兼容层判断 |
公共 API 通过 src/index.ts 统一导出,内部 core/index.ts 则面向 renderer/src/aiCore/ 等模块提供底层能力。
2. Provider 注册与实例化流程
ExtensionRegistry 是全局单例,管理所有 ProviderExtension 的注册表。每个 ProviderExtension 负责单个 Provider 的完整生命周期:
Cherry Studio 的具体实现要点:
- LRUCache(max=10):按 settings 的稳定 hash 缓存 Provider 实例,
updateAgeOnGet=true确保热 Provider 常驻 - In-flight 并发安全:
pendingCreationsMap 存储Promise<TProvider>,防止并发请求重复创建 - 两种注册方式:
create模式 — 直接传入 Provider 创建函数import模式 — 动态import()+creatorFunctionName定位导出函数
- 变体机制:同一 Provider 的不同模式通过
variantSuffix区分(如openai-chat、azure-responses),变体可通过transform函数对基础 Provider 进行转换 - 别名支持:
aliases数组允许同一 Provider 多名称访问,注册时校验别名不冲突
伪代码 — 创建并缓存 Provider 实例:
类 ProviderExtension: 属性 instances: LRUCache<hash, Provider> // max=10 属性 pendingCreations: Map<hash, Promise<Provider>>
方法 createProvider(settings, variantSuffix): hash = computeHash(settings, variantSuffix) if hash in instances: return instances[hash] if hash in pendingCreations: return pendingCreations[hash] // 返回同一 promise
promise = _doCreateProvider(settings, variantSuffix) pendingCreations[hash] = promise try: result = await promise return result finally: pendingCreations.delete(hash)
方法 _doCreateProvider(settings, variantSuffix): if config.create: baseProvider = await config.create(settings) elif config.import: module = await config.import() baseProvider = await module[config.creatorFunctionName](settings)
if variantSuffix: variant = getVariant(variantSuffix) if variant.transform: baseProvider = await variant.transform(baseProvider, settings)
instances.set(hash, baseProvider) return baseProvider3. RuntimeExecutor:每个 Provider 一个执行器实例
与通用 HubProvider 不同,Cherry Studio 为每个 Provider 创建一个独立的 RuntimeExecutor,而非所有 Provider 共享单一 Hub。这样做的好处是:
- 每个 Provider 可以拥有独立的插件集合
modelResolver可以针对 Provider 定制- 类型安全:
RuntimeConfig<TSettingsMap, T>在编译期约束 providerId 与 settings 的对应关系
RuntimeExecutor 的关键行为:
- 构造函数中创建
createProviderRegistry({ [providerId]: provider }),注册当前 Provider streamText/generateText根据model参数类型动态注入resolveModel内部插件resolveModel方法优先使用config.modelResolver,否则通过registry.languageModel('providerId:modelId')解析embedMany不经过插件管线,直接通过registry.embeddingModel()解析- 构造函数中自动修补
provider.embeddingModel:如果 Provider 只有textEmbeddingModel而无embeddingModel,自动桥接(兼容@openrouter/ai-sdk-provider等库)
伪代码 — RuntimeExecutor 的流式文本调用:
类 RuntimeExecutor: 属性 pluginEngine: PluginEngine 属性 registry: ProviderRegistry
方法 streamText(params): if typeof params.model == "string": pluginEngine.use([ createResolveModelPlugin(), // 负责 modelId → LanguageModel createConfigureContextPlugin() // 占位钩子 ]) else: pluginEngine.use([ createConfigureContextPlugin() ])
return pluginEngine.executeStreamWithPlugins( "streamText", params, (model, transformedParams, streamTransforms) => ai.streamText({ ...transformedParams, model: model, experimental_transform: mergedStreamTransforms }) )
方法 resolveModel(modelOrId): if typeof modelOrId == "string": if config.modelResolver: return config.modelResolver(modelOrId) return registry.languageModel("providerId:modelOrId") return modelOrId4. PluginEngine 七阶段管线
PluginEngine 是 Cherry Studio 自有的插件执行引擎,与 AI SDK 原生中间件协同工作。完整的执行管线:
Cherry Studio 的具体实现细节:
| 钩子 | 执行策略 | PluginManager 方法 | Cherry Studio 特点 |
|---|---|---|---|
configureContext | 串行 | executeConfigureContext | 插件可写入 context.middlewares 数组 |
onRequestStart | 并行 | executeParallel | 使用 Promise.all(非 allSettled),一个失败全部失败 |
resolveModel | First | executeFirst | 返回第一个非 null 结果 |
transformParams | 链式 | executeTransformParams | 每个插件返回 Partial<TParams>,逐步合并 |
transformResult | 链式 | executeTransformResult | 每个插件接收并返回完整 TResult |
onRequestEnd | 并行 | executeParallel | 接收转换后的最终结果 |
onError | 并行 | executeParallel | 错误重新抛出,不吞异常 |
transformStream | 收集 | collectStreamTransforms | 返回 TransformStream 数组,交给 AI SDK 的 experimental_transform |
流式调用有额外步骤:在 transformParams 之后、executor 之前,通过 collectStreamTransforms 收集所有流转换器,合并到 experimental_transform 参数中。
5. 内置插件体系
Cherry Studio 在 core/plugins/built-in/ 中提供了内置插件:
- providerToolPlugin — 处理 Provider 级别的工具调用,包括 prompt 工具使用、工具执行、流事件管理
- webSearchPlugin — 集成网络搜索能力,通过
toolFactories声明 Provider 对 webSearch 的支持
6. 自定义错误类型
Cherry Studio 定义了结构化的错误类型族,全部继承自 AiCoreError:
AiCoreError 提供 code(错误码)、context(结构化上下文)、cause(原始错误链)三个核心属性,并实现 toJSON() 用于序列化。
7. ExtensionRegistry 的高级能力
ExtensionRegistry 除了基本的注册/查找功能外,还提供了多种高级能力:
Provider ID 解析:
parseProviderId('openai-chat')→{ baseId: 'openai', mode: 'chat', isVariant: true }parseProviderId('oai')→{ baseId: 'openai', isVariant: false }(别名解析)resolveProviderIdWithMode('openai', 'chat')→'openai-chat'
工具能力解析:
resolveToolCapability(providerId, capability, modelProvider) 采用两阶段策略:
- Direct — 直接从 Provider 自身的
toolFactories获取 - Aggregator fallback — 从
modelProvider字符串分段回退查找(如"aihubmix.google"→ 从google段查找)
伪代码 — 工具能力解析:
方法 resolveToolCapability(providerId, capability, modelProvider): // 阶段1: 直接从 provider 的 toolFactories 查找 factory = getToolFactory(providerId, capability) if factory: provider = getToolProvider(providerId) return { factory, provider }
// 阶段2: 从 modelProvider 分段回退 if modelProvider is string: segments = modelProvider.split(".") for segment in reversed(segments): factory = getToolFactory(segment, capability) if factory: provider = getToolProvider(segment) return { factory, provider }
return undefined工具 Provider 提取:
getToolProvider(providerId) 用于提取 Provider 的 .tools 属性。优先使用缓存的 Provider 实例,无缓存时使用 { apiKey: '_tool_descriptor' } 创建轻量占位实例。
8. 依赖与版本
Cherry Studio 的 aiCore 层依赖:
| 依赖 | 版本 | 用途 |
|---|---|---|
ai | ^6.0.143 | Vercel AI SDK 核心(v5 接口) |
@ai-sdk/provider | — | Provider V3 类型定义 |
@cherrystudio/openai | 6.15.0 | 自定义 OpenAI fork,替代原生 openai 包 |
20+ @ai-sdk/* | 各版本 | 各 LLM 提供商 SDK |
lru-cache | — | Provider 实例缓存 |
@cherrystudio/openai 是 Cherry Studio 对原生 openai 包的自定义 fork,用于支持特定的 API 端点、协议模式和参数行为。
9. 公共 API 导出
packages/aiCore/src/index.ts 导出的公共 API:
| 导出 | 类型 | 用途 |
|---|---|---|
createExecutor | 函数 | 创建 RuntimeExecutor |
streamText | 函数 | 流式文本生成 |
generateText | 函数 | 非流式文本生成 |
generateImage | 函数 | 图像生成 |
embedMany | 函数 | 批量文本嵌入 |
isV3Model | 函数 | 检查模型是否为 V3 版本 |
isV2Model | 函数 | 检查模型是否为 V2 版本 |
definePlugin | 函数 | 创建 AiPlugin 的工厂函数 |
PluginEngine | 类 | 插件引擎实例,支持独立操作插件 |
| 错误类型族 | 类 | AiCoreError、ModelResolutionError 等 |
问题与规避
1. Provider 实例缓存与配置变更失效
问题:ProviderExtension 使用 settings 的稳定 hash 作为缓存 key。hash 函数对对象 key 排序后序列化,如果用户修改了配置但 hash 值不变(如字段重排),可能命中旧实例。
规避:
ProviderExtension.clearCache()在 API Key 等关键配置变更时手动调用- hash 函数使用
stableStringify(key 排序 + 递归序列化),确保相同配置产生相同 hash - 提供
getCachedProvider()和getCacheStats()用于调试缓存状态
2. 插件执行顺序导致参数覆盖
问题:多个插件注册了 transformParams 钩子,后执行的插件可能覆盖前者的修改。
规避:
- 使用
enforce: 'pre' | 'post'控制插件在管线中的位置 —pre插件最先执行,post插件最后执行 - PluginManager 的
sortPlugins方法自动排序:pre → normal → post - 在插件文档中声明依赖关系和期望的执行位置
3. 中间件链性能开销
问题:每个中间件通过 wrapLanguageModel 增加一层函数调用,中间件过多时延迟显著。
规避:
- Cherry Studio 使用
context.middlewares数组统一管理,在模型解析后一次性调用wrapLanguageModel包装所有中间件 - 避免在每次请求中重复
wrapLanguageModel - 中间件由各插件在
configureContext阶段写入,按需加载
4. 递归调用无限循环
问题:插件通过 context.recursiveCall() 发起递归调用(如 tool use 后的 follow-up),可能导致无限循环。
规避:
maxRecursiveDepth默认限制为 10 层- 使用
try...finally确保recursiveDepth计数器在异常时也能恢复 - 超过深度限制时抛出
RecursiveDepthError,携带requestId、currentDepth、maxDepth信息
5. 类型安全与 any 转换
问题:PluginEngine 内部使用 AiPlugin<any, any>[] 存储插件,依赖逆变/协变规则保证安全。
规避:
- 只通过
definePlugin工厂函数创建插件 PluginManager在每次执行时创建类型化的临时实例:new PluginManager<TParams, TResult>(this.basePlugins as AiPlugin<TParams, TResult>[])- 不手动构造
AiPlugin对象,不直接操作插件数组
6. 图像生成错误包装
问题:图像生成调用中的错误可能来自底层 SDK,缺乏上下文信息。
规避:
RuntimeExecutor.generateImage使用try/catch包装,将底层错误转换为ImageGenerationError,携带providerId、modelId和原始错误- 图像模型解析失败时抛出
ImageModelResolutionError
设计取舍
Cherry Studio 执行器模式 vs 单一 HubProvider
通用 HubProvider 模式使用一个 Hub 聚合所有 Provider。Cherry Studio 选择为每个 Provider 创建独立的 RuntimeExecutor:
| 维度 | Cherry Studio(每 Provider 一个 Executor) | 单一 HubProvider |
|---|---|---|
| 插件隔离 | 每个 Provider 独立插件集合 | 所有 Provider 共享同一管线 |
| 类型安全 | 编译期约束 providerId ↔ settings | 需要运行时校验 |
| 模型解析 | 支持 modelResolver 自定义 | 统一 Registry 查找 |
| 内存占用 | 每个 Executor 持有独立 Registry | 单一 Registry 共享 |
| 扩展性 | 新增 Provider 只需注册 Extension | 需要修改 Hub 内部逻辑 |
取舍理由:Cherry Studio 需要为不同 Provider 挂载不同的内置插件(如 providerToolPlugin 需要访问 Provider 特定的 .tools 属性),且需要支持自定义 modelResolver(如 xAI responses 模式)。独立 Executor 模式在隔离性和类型安全上更有优势。
自定义 OpenAI Fork vs 原生 openai 包
Cherry Studio 使用 @cherrystudio/openai (6.15.0) 替代原生 openai 包:
| 维度 | 自定义 Fork | 原生 openai |
|---|---|---|
| API 端点 | 支持自定义 base_url 和特殊端点 | 固定端点 |
| 协议模式 | 支持 OpenAI chat 和 responses 双模式 | 原生模式 |
| 参数行为 | Cherry Studio 特有的参数处理 | 标准参数 |
| 维护成本 | 需要同步上游更新 | 自动更新 |
取舍理由:Cherry Studio 需要支持多种 OpenAI 兼容服务(包括聚合代理服务),原生 openai 包无法满足所有场景。自定义 fork 确保了 API 兼容性的同时,保持了与 AI SDK Provider 接口的对接。
插件系统 vs 纯中间件
Cherry Studio 同时支持 AI SDK 原生中间件(LanguageModelMiddleware)和自有的插件系统(AiPlugin):
| 维度 | 中间件(AI SDK 原生) | 插件(Cherry Studio 自有) |
|---|---|---|
| 作用范围 | 单个 LanguageModel | 整个请求管线 |
| 生命周期 | wrap 函数,前/后处理 | 7 阶段钩子 |
| 数量 | 通过 wrapLanguageModel 嵌套 | 可注册多个 |
| 流处理 | 原生支持 | 通过 transformStream |
| 错误处理 | 需自行处理 | onError 并行钩子 |
取舍理由:中间件适合模型级别的处理(如缓存、日志),插件适合跨 Provider 的全局处理(如工具注入、参数标准化)。两者通过 context.middlewares 桥接 — 插件在 configureContext 阶段写入中间件,PluginEngine 在模型解析后统一应用。
参考来源
- Cherry Studio 源码: https://github.com/CherryHQ/cherry-studio
packages/aiCore/源码: https://github.com/CherryHQ/cherry-studio/tree/d32cbecfa870af5c2b56a377c97d49e554284233/packages/aiCore/- Vercel AI SDK v5 文档: https://sdk.vercel.ai/docs
- AI SDK Provider Registry API: https://sdk.vercel.ai/docs/reference/ai/provider-registry
- AI SDK wrapLanguageModel: https://sdk.vercel.ai/docs/reference/ai/wrap-language-model
- 通用知识:Vercel AI SDK v5 HubProvider 多 Provider 聚合模式