拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Cherry Studio Adapter Family:AI SDK 路由的单一事实来源设计解析

Cherry Studio Adapter FamilyAI SDK 路由的单一事实来源设计解析【免费下载链接】cherry-studio Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端项目地址: https://gitcode.com/CherryHQ/cherry-studio导读Cherry Studio 将每一次 AI 请求路由到约 26 个ai-sdk/*适配器adapter之一适配器决定了请求格式、流式编解码与各家厂商的专属工具如 OpenAI 的webSearch_20250305、Google 的googleSearch、OpenAI Responses API 等能否正确工作。本文以仓库中的设计文档 adapter-family.md 为核心骨架结合源码深入解析adapterFamily 机制它如何用一个写入期推导、运行时只读的字段替代旧的启发式解析器解决 MiniMax、Silicon、AiHubMix 这类多端点中转服务同一provider.id下同时暴露 OpenAI 与 Anthropic 两种协议的 URL的适配器选型问题。读完本文你将掌握该机制的完整设计脉络、三条写入路径、运行时解析链路以及对应的测试验证体系。背景为什么要引入 adapterFamilyCherry Studio 的每一次 LLM 请求都会被路由到某个 AI SDK 适配器。适配器是ai-sdk/*系列包ai-sdk/anthropic、ai-sdk/openai、ai-sdk/google-vertex等每个包都知道自己对应厂商的请求格式、流式编解码、能力矩阵与厂商专属工具。选错适配器的后果是把 OpenAI 形状的 JSON 发给 Anthropic 协议的端点然后得到一堆乱码。旧版v1解析器通过推断provider.id、provider.type与apiHost字符串来挑选适配器。在一个 provider 只有一个 API 端点的假设下这没问题但遇到多端点中转服务就失效了MiniMax、Silicon、AiHubMix 等中转服务在同一个provider.id下既暴露 openai-chat-completions URL也暴露 anthropic-messages URL同一个 provider 取决于请求发往哪个 URL需要用到两个不同的适配器最直观的故障症状是翻译输出里泄漏出think标签——中转服务的 anthropic-messages 端点收到了 OpenAI 格式的请求无法识别reasoning字段就把它当普通文本原样回显。这个 bug 的根源在于适配器的正确答案其实早就存在于目录数据catalog里却被旧 schema 丢弃运行时只能用一堆启发式规则重新猜测。核心设计adapterFamily 按端点配置设计文档给出的方案非常克制每个端点配置endpoint config携带一个adapterFamily: string字段运行时解析器只读取它不做任何推断。实际实现位于 endpoint.tsexport function resolveAiSdkProviderId(provider: Provider, endpointType: EndpointType | undefined): AppProviderId { const adapterFamily endpointType ? provider.endpointConfigs?.[endpointType]?.adapterFamily : undefined if (adapterFamily adapterFamily in appProviderIds) { return resolveProviderVariant(appProviderIds[adapterFamily], endpointType) } if (endpointType ENDPOINT_TYPE.OPENAI_RESPONSES) { return appProviderIds[open-responses] } return appProviderIds[openai-compatible] }解析器的核心逻辑就是从provider.endpointConfigs[endpointType].adapterFamily读出适配器族 → 在appProviderIds中查表 → 必要时追加变体后缀读不到就走openai-compatible兜底实际代码对OPENAI_RESPONSES还保留了open-responses的一行回退。零启发式、无字符串嗅探一行信号决定一切。三层身份栈层级示例角色provider.idminimax、silicon、my-relay面向用户的身份标识、UI 标签、路由键endpointTypeopenai-chat-completions、anthropic-messagesURL 路径模板 协议族adapterFamilyopenai-compatible、anthropic、azure-responses哪个ai-sdk/*包实现了该协议以 MiniMax 类中转服务为例provider.idminimax但它的两个端点分别携带adapterFamilyopenai-compatible与adapterFamilyanthropic——同一个身份每个端点配不同的适配器。变体variant机制appProviderIds由核心扩展与应用扩展合并而来变体自动自映射。在 merged.ts 中每个扩展的variants会被注册为${name}-${variant.suffix}例如azure-responses自映射为自身而 resolveProviderVariant 负责把基础 id 映射到变体 id当端点为openai-chat-completions或ollama-chat时尝试openai → openai-chat当端点为openai-responses时尝试openai → openai-responses变体不存在时原样返回基础 id幂等已经是变体的azure-responses不会被二次加工。这样adapterFamilyopenai的端点既能正确路由到 Chat Completions 变体也能在 Responses API 下切换到openai-responses变体而 catalog 里声明的azure-responses等已带变体的值则保持不变。providerOptions 命名空间的单一推导解析出适配器 id 之后还需要推导它在providerOptions中的命名空间如openai、anthropic、vertex、bedrock。resolveEndpointProviderOptionsKey 将两步合成一个调用保证 gateway 消费者、reasoning 选项等所有写providerOptions的地方永远不会对命名空间产生分歧export function resolveEndpointProviderOptionsKey(provider: Provider, resolvedEndpoint: ResolvedEndpoint): string { return resolveProviderOptionsKey(resolveAiSdkProviderId(provider, resolvedEndpoint.endpointType), { actualProviderId: provider.id, endpointType: resolvedEndpoint.endpointType, gatewayProviderOptionsKey: resolvedEndpoint.providerOptionsKey }) }其中 resolveProviderOptionsKey 对cherryin、newapi、aihubmix、dmxapi、gateway这类中转身份还做了端点级细分端点为ANTHROPIC_MESSAGES时命名空间归anthropic为GOOGLE_GENERATE_CONTENT时归google为OPENAI_RESPONSES时归openai否则用 provider 自身 id。adapterFamily 从哪来写入期推导运行时只读adapterFamily是一个写入期write-time推导值。共有三条写入路径但只共享一个推导函数inferAdapterFamilyexport function inferAdapterFamily( endpointType: EndpointType, catalogConfig?: PickRegistryEndpointConfig, adapterFamily | PickPersistedEndpointConfig, adapterFamily | null ): string { if (catalogConfig?.adapterFamily) return catalogConfig.adapterFamily return ENDPOINT_TYPE_TO_DEFAULT_ADAPTER_FAMILY[endpointType] ?? openai-compatible }优先级是catalog 显式声明 端点类型协议默认 openai-compatible兜底。其中端点类型默认值纯粹由协议推导——任何 anthropic-messages 端点都必须用 anthropic 适配器没有例外endpoint type默认适配器anthropic-messagesanthropicgoogle-generate-contentgoogleollama-chat/ollama-generateollamajina-rerankjina-rerankopenai-responsesopenaiopenai-chat-completions及其他openai-compatible最终兜底Path 1 —— Catalog新装用户目录数据 providers.json 为每个 provider 的每个端点声明adapterFamily。seeder 通过 buildPersistedEndpointConfigs 把它原样透传持久化新装用户在写库时就拿到正确值{ id: silicon, endpointConfigs: { openai-chat-completions: { baseUrl: ..., adapterFamily: openai-compatible }, anthropic-messages: { baseUrl: ..., adapterFamily: anthropic } } }在 schema 层面RegistryEndpointConfigSchema.adapterFamily 是z.string().optional()注释明确要求AI SDK 适配器族与appProviderIds中注册的 id 对齐解析器应优先使用它而不是启发式 id/baseUrl 推断。 该目录覆盖仓库内全部 provider 条目审计结论catalog 中 100% 的端点上都有adapterFamily。Path 2 —— v1 → v2 迁移存量用户迁移器 ProviderModelMappings.ts 的buildEndpointConfigs按优先级回填adapterFamily先查 catalog按legacy.id匹配没有 catalog 匹配时回退到legacy.type最后落到端点类型默认值const fromCatalog catalogEndpoints?.[key]?.adapterFamily const legacyHint key ENDPOINT_TYPE.ANTHROPIC_MESSAGES ? undefined : legacyTypeFamily const adapterFamily fromCatalog ?? legacyHint ?? inferAdapterFamily(key)两个值得注意的设计细节ANTHROPIC_MESSAGES跳过 legacy 提示v1 的自定义 anthropic 中转服务即使端点讲的是 anthropic 协议legacy.type也常常是openai中转协议类型。如果此时用 legacy 提示覆盖就会重蹈旧 bug 覆辙所以端点的协议必须获胜。LEGACY_TYPE_TO_ADAPTER_FAMILY迁移器本地为无法命中 catalog 的自定义 provider 提供更精确的信号例如legacy.typenew-api→newapi适配器比通用的openai-compatible默认值更准确。该映射位于 ProviderModelMappings.ts覆盖openai、openai-response、anthropic、gemini、new-api、gateway、ollama等类型。Path 3 —— UI 自定义 provider 创建未来当未来的添加自定义 providerUI 允许用户输入 baseUrl 时表单提交会调用inferAdapterFamily(userPickedEndpoint, catalogConfigIfAny)并把结果与 baseUrl 一起写入。用户永远不会直接选择adapterFamily——它始终是从(endpointType, 可选的 catalog 预设)推导出来的值。推导函数是共享的同一个 importUI 接线只需一行。运行时数据落点持久化侧endpoint_configs是 SQLite 的 JSON 文本列userProvider.ts其类型 StoredEndpointConfigOverride 在基类之上扩展了adapterFamily?: string源码注释明确其定位adapterFamily是自定义 v1 中转服务的 legacy 迁移来源。运行时类型定义位于 src/shared/data/types/provider.ts。代码位置总览文件职责providers.jsonCatalog每个 provider 每个端点的adapterFamilyschemas/provider.tsRegistryEndpointConfigSchema.adapterFamilyzod schemaregistry-utils.tsinferAdapterFamily单一事实来源buildPersistedEndpointConfigs透传字段registry-loader.tsfindProvider(id)查找供迁移器使用src/shared/data/types/provider.ts运行时EndpointConfigOverride.adapterFamilypresetProviderSeeder.ts新装写入路径ProviderModelMappings.tsv1 → v2 回填buildEndpointConfigsendpoint.ts运行时解析器读取adapterFamily应用变体后缀备选方案对比为什么拒绝四条路设计文档逐一记录了被否决的替代方案每一条都揭示了 adapterFamily 设计背后的权衡A. 保留启发式解析链 —— 否决原 v2 解析器有约 40 行回退逻辑按provider.id/presetProviderId做 Azure 检测、grok 特例、provider.id ∈ appProviderIds、presetProviderId ∈ appProviderIds、api.openai.combaseUrl 嗅探、ANTHROPIC_MESSAGES → anthropic最终护栏。它对已知类型有效但MiniMax 式中转会选错正是踩过的 bug每来一个新厂商就要加一个新分支而正确答案早已在 catalog 数据里只是被 schema 丢弃了。否决理由catalog 已经按端点编码了答案直接读取它比重新推导严格更准确。B. 运行时查 catalog —— 否决让解析器每次请求都调RegistryLoader.findProvider(provider.id)从 catalog 查adapterFamily。否决理由给每次 LLM 调用的热路径增加 registry 依赖无法处理自定义无 catalog 匹配provider仍需要回退链该值在行生命周期内是稳定的写入期算一次严格更便宜。C. 运行时仅从 endpointType 推断 —— 否决当provider.endpointConfigs[ep].adapterFamily缺失时解析器直接回退到inferAdapterFamily(endpointType)。否决理由丢失 catalog 编码的厂商特定路由例如aihubmix的 anthropic 端点用的是adapterFamilyaihubmix而不是通用anthropic默认值——该中转服务在内部自己做多厂商路由让解析器承担本属于写入期的推断逻辑把哪个适配器正确的决策拆到两个文件里。写入期推断的核心收益catalog 更新例如新条目补上adapterFamily对新装与迁移立即生效无需改动任何运行时解析器代码。D. 在 UI 中暴露 adapterFamily 下拉框 —— 否决用户创建自定义 provider 时直接选适配器族。否决理由该概念是实现细节导入哪个ai-sdk/*包用户关心的是我的 URL 讲的是哪种 API 协议而endpointType已经捕捉了这个问题——用户从下拉框选anthropic-messages或openai-chat-completions就隐含了适配器两个下拉框表达同一个概念选择只会加倍用户的困惑。UI 暴露端点类型系统从中推导适配器族。验证与测试adapterFamily 的每个环节都有针对性测试覆盖目标验证方式inferAdapterFamily5 个用例registry-utils.test.ts——catalog 优先、端点默认值、openai-compatible 兜底、双 schema 兼容迁移器回填9 个用例ProviderModelMappings.test.ts——catalog 命中、legacy.type 回退、ANTHROPIC 默认、catalog legacy.type 优先级、多端点中转运行时解析器54 个用例endpoint.test.ts——catalog adapterFamily 路由、变体后缀应用openai→openai-chat、已是变体的azure-responses幂等、MiniMax 式中转回归原始 bug、未知族降级buildPersistedEndpointConfigs9 个用例registry-utils.test.ts——adapterFamily 透传、保留规则回归基线方面src/main/ai下共 317 个用例通过 / 7 个失败且失败的 3 个文件AiStreamManager、WebSearchTool、toolSearch与本次改动无关属于改动前就存在的既有失败。数据库迁移无需任何 DDL本次设计不需要数据库迁移。endpoint_configs是 JSON 文本列新字段只是 JSON 形状内部的一个键不改变列类型与存储方式。仓库约定见 CLAUDE.md 中关于 schema 与 drizzle SQL 的说明也允许开发中期数据库漂移因此这一改动对存量用户完全透明。附测试夹具收敛adapterFamily 重构本不要求测试夹具改动但解析器测试套件变大后重复的本地工厂函数成了维护负担。于是五个测试文件统一迁移到集中式工厂 src/main/ai/tests/fixtures/makeProvider、makeModel、makeAssistant此前每个文件各有一份几乎相同的本地工厂。夹具放在消费者旁src/main/ai/而非 schema 旁src/shared/data/types/遵循现在时原则目前还没有非src/main/ai/的消费者将来出现时再上提也容易。小结adapterFamily 机制的精髓可以概括为三句话catalog 是适配器选型的事实来源写入期推导一次运行时只读协议endpointType永远优先于身份provider.id/type的启发式猜测。它用几行代码和一个字符串字段消灭了约 40 行回退逻辑让 MiniMax 式多端点中转服务从按 provider 猜适配器变成按端点读适配器并确保 catalog 的每一次更新都能零运行时改动地惠及新装与迁移用户。对于需要接入新厂商或新端点协议的开发者来说只需要在 catalog 中声明adapterFamily解析器无需任何改动即可正确路由。【免费下载链接】cherry-studio Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端项目地址: https://gitcode.com/CherryHQ/cherry-studio创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门