跳转到内容

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/ExtensionRegistryProviderExtension管理 Provider 的注册、缓存、实例化、变体、工具工厂
Runtime 运行时层core/runtime/RuntimeExecutorPluginEngine处理请求管线、插件执行、模型解析、API 调用
模型层core/models/isV3ModelisV2Model模型版本检测与兼容层判断

公共 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-chatazure-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/providerProvider V3 类型定义
@cherrystudio/openai6.15.0自定义 OpenAI fork,替代原生 openai 包
20+ @ai-sdk/*各版本各 LLM 提供商 SDK
lru-cacheProvider 实例缓存

@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插件引擎实例,支持独立操作插件
错误类型族AiCoreErrorModelResolutionError

问题与规避

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,携带 requestIdcurrentDepthmaxDepth 信息

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,携带 providerIdmodelId 和原始错误
  • 图像模型解析失败时抛出 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 在模型解析后统一应用。

参考来源