跳转到内容

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 的插件体系中编写自定义插件

前置知识

本章不重复通用知识,仅聚焦 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 并发安全:pendingCreations Map 存储 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 baseProvider

3. 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 modelOrId

4. PluginEngine 七阶段管线

PluginEngine 是 Cherry Studio 自有的插件执行引擎,与 AI SDK 原生中间件协同工作。完整的执行管线:

Cherry Studio 的具体实现细节:

钩子执行策略PluginManager 方法Cherry Studio 特点
configureContext串行executeConfigureContext插件可写入 context.middlewares 数组
onRequestStart并行executeParallel使用 Promise.all(非 allSettled),一个失败全部失败
resolveModelFirstexecuteFirst返回第一个非 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) 采用两阶段策略:

  1. Direct — 直接从 Provider 自身的 toolFactories 获取
  2. 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.143Vercel AI SDK 核心(v5 接口)
@ai-sdk/provider—Provider V3 类型定义
@cherrystudio/openai6.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 在模型解析后统一应用。

参考来源