ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Cherry Studio Provider Model 注册表系统:预设数据加载、规范化、种子写入与用户数据合并全解析

Cherry Studio Provider  Model 注册表系统:预设数据加载、规范化、种子写入与用户数据合并全解析 Cherry Studio Provider Model 注册表系统预设数据加载、规范化、种子写入与用户数据合并全解析【免费下载链接】cherry-studioAI productivity studio with smart chat, autonomous agents, and 300 assistants. Unified access to frontier LLMs项目地址: https://gitcode.com/GitHub_Trending/ch/cherry-studio导读Cherry Studio 将数百家 AI 提供方Provider与数千个模型的定义沉淀为三份随包发布的 JSON 注册表数据位于 packages/provider-registry/data并在运行时将其与 SQLite 中的用户配置按分层 Delta契约合并从而让上游目录更新能够零数据迁移地触达既有安装。本文基于 docs/references/provider-model/README.md 与其核心正文 docs/references/provider-model/provider-registry.md结合 packages/provider-registry 与主进程数据服务源码完整讲解三类注册表 JSON 的职责划分、RegistryLoader的缓存与 O(1) 索引机制、模型 ID 规范化normalizeModelId规则、预设 Provider 的 insert-only 种子写入以及模型/Provider 配置在注册表 → 用户行上的三层合并优先级。读完你既能理解 Cherry Studio 的 Provider/Model 数据架构也能掌握哪些字段该持久化、哪些字段该读时解析、何时需要数据回填的扩展设计方法。一、整体架构三份 JSON 主进程服务 两张数据库表从源码结构看整套 Provider/Model 体系由三层组成cherrystudio/provider-registry (包) ├── data/ │ ├── models.json 预设模型能力、定价、模态、推理支持…… │ ├── providers.json 预设提供方端点、apiFeatures、元数据 │ └── provider-models.json 提供方专属的模型级覆盖按提供方微调 ├── src/ │ ├── registry-loader.ts RegistryLoader加载、校验、缓存、索引、空闲 TTL │ ├── registry-utils.ts 纯函数lookupRegistryModel、buildPersistedEndpointConfigs │ ├── utils/normalize.ts normalizeModelId聚合前缀、变体后缀…… │ └── schemas/ Zod 校验 Schema │ src/main/data/ ├── db/seeding/ │ └── seeders/ │ └── presetProviderSeeder.ts ISeeder仅插入的 Provider 身份/认证脚手架 ├── services/ │ ├── ProviderRegistryService.ts 注册表查找与 Provider/Model 基线解析 │ ├── ModelService.ts Model CRUD 与用户 Delta 覆盖 │ └── ProviderService.ts Provider CRUD 与读取时 Provider 合并 └── api/handlers/ ├── models.ts Model CRUD、对账与注册表解析路由 └── providers.ts Provider CRUD 与预设投影路由三个 JSON 文件是目录事实的唯一来源source of truth架构文档刻意不重复目录条数因为其体积随上游数据独立变化。加载时三份文件分别经过ModelListSchema、ProviderListSchema、ProviderModelListSchema的 Zod 校验packages/provider-registry/src/schemas校验通过后才进入内存索引。1.1 三类数据的职责划分文件内容典型字段models.json全局模型目录跨 Provider 复用id、name、capabilities、contextWindow、maxOutputTokens、inputModalities/outputModalities、pricing、reasoning、parameterSupport、imageGeneration、ownedBy、openWeightsproviders.json预设提供方定义id、name、defaultChatEndpoint、endpointConfigsbaseUrl、adapterFamily、modelsApiUrls、reasoningFormat、apiFeatures、authMethods、authOptional、modelListSource、serverTools、metadata.website、availableInEditionsprovider-models.json提供方对特定模型的覆盖providerId、modelId引用models.json、apiModelId调用 API 时真实使用的 ID、capabilitiesadd/remove/force、limits、pricing、reasoningContracts、endpointTypes、inputModalities/outputModalities、disabled、replaceWith、name等独立字段以 providers.json 中的真实条目为例cherryin声明了四个端点anthropic-messages、google-generate-content、openai-chat-completions、openai-responses其中openai-chat-completions端点额外声明reasoningFormat: { type: openai-chat }并携带serverToolsweb-search、url-context与官网元数据。而provider-models.json中302ai对claude-opus-4-5等模型以apiModelId映射带日期快照的真实调用 ID如claude-opus-4-1-20250805同时覆盖其定价——这就是目录规范 ID与厂商实际 ID解耦的典型用法。值得注意的边界apiModelId的作用是保留提供方原始的 ID 格式OpenRouter 的anthropic/claude-3-5-sonnet、Vertex AI 的global.anthropic.claude-3-5-sonnet-v1:0等未设置时直接用modelId作为 API 调用 ID。若某模型在models.json中没有对应条目provider-models.json的name、description、family、ownedBy、imageGeneration等字段可以让它完全独立地存活在覆盖文件里——解析器会用synthesizePresetFromOverrideProviderRegistryService.ts从覆盖合成一个预设无需污染全局模型目录。二、数据流三条关键路径2.1 启动时预设 Provider 种子写入DbService.onInit() → SeedRunner.runAll(seeders) → PresetProviderSeeder.run(db) → RegistryLoader.loadProviders() // 读取 providers.json → SELECT 已有 provider IDuser_provider → 仅 INSERT 新增的 Provider 身份/认证行 → 绝不物化注册表拥有的连接配置SeedRunner会在providers.json版本变化时重跑该 Seeder但 Seeder 始终是insert-only已存在的 Provider 行被跳过。这一设计的正确性建立在注册表拥有的连接配置从不写入行之上——每次读取时都从当前注册表实时解析。行内只保存身份providerId、presetProviderId、用户拥有的显示名name以及必要的认证壳authConfig。从 presetProviderSeeder.ts 源码可见认证壳仅对三个复用他人端点协议的厂商生成vertexai生成{ type: iam-gcp, project: , location: }azure-openai生成{ type: iam-azure, apiVersion: }aws-bedrock生成{ type: iam-aws, region: }其余返回null按 v2 约定这类厂商的 URL 路由完全由authType驱动见ProviderSettings/utils/provider.ts的说明。toDbRow中presetProviderId取p.presetProviderId ?? p.id即无显式分组归属时预设就是自己。规范预设不可删除。多数预设满足providerId presetProviderId少数别名/分组预设如zai→zhipu、minimax-global→minimax通过注册表查找同样受到保护——isRegistryProvider只要发现providers.json中存在该 ID 即判定为预设行。而用户自建、从预设继承的 Provider 可以正常删除。2.2 按需触发模型创建POST /models [{ providerId: openai, modelId: gpt-4o }] → handler: 对每个条目调用 providerRegistryService.lookupModel(providerId, modelId) → RegistryLoader.findModel(gpt-4o) // O(1) 索引规范化兜底 → RegistryLoader.findOverride(openai, gpt-4o) // O(1) 索引 → 从注册表数据解析端点 profile // 仅主进程不持久化 → 返回 { presetModel, registryOverride, reasoningProfile } → handler: modelService.create(items) → mergePresetModel(preset, override, ...) → 将 DTO 显式字段与注册表基线比较 → 仅 INSERT 与基线不同的可空列到 user_model → list/get/mutation 响应 → 重建当前注册表基线 → 覆盖每个非空稀疏列lookupModelProviderRegistryService.ts是注册表 数据库感知的单模型查找先取 Provider 上下文含presetProviderId与默认端点再用loader.findOverride(presetProvider.id, modelId)找覆盖loader.findModel(...)找预设若命中覆盖但模型目录无条目则走synthesizePresetFromOverride合成。该服务不拥有任何数据库表、不直接访问数据库用户数据一律经ProviderService获取。2.3 解析 SDK 模型列表GET /providers/:providerId/models:resolve?idsgpt-4oidso3 → providerRegistryService.resolveModels(providerId, modelIds) → 对每个 modelId → RegistryLoader.findModel(modelId) // O(1)规范化兜底 → RegistryLoader.findOverride(providerId, modelId) // O(1) → mergePresetModel(preset, override, ...) 或 createCustomModel(...) → 返回合并后的 Model[]resolveModelsProviderRegistryService.ts是SDK 只提供 ID、其余全部来自注册表的关键路径SDK 数据不会覆盖精心维护的注册表数据。对注册表中未命中的 IDcreateCustomModel生成最小自定义模型能力为空数组、supportsStreaming: true、默认启用注册表合并失败被视为致命错误避免把不完整结果当成功同步持久化。解析时还会用deriveResolvedModelName生成可区分显示名——当多个 SKU 共享同一规范条目时如MiniMax/MiniMax-M2.1与裸MiniMax-M2.1规范化命中的条目会附加命名空间前缀/日期快照/变体等装饰后缀防止界面混淆。三、合并函数与优先级3.1 三个函数、三种场景函数适用场景合并层mergePresetModel注册表查询、resolveModelspreset → overrideapplyUserOverlay带显式用户 Delta 的 Model 读取合并后的注册表基线 → usercreateCustomModel注册表无匹配仅 modelId共享逻辑被抽取为applyPresetAndOverridepreset override 合并见 ProviderRegistryService.ts处理能力、模态、限制、定价、参数支持的逐字段合并与resolveReasoning推理配置解析。3.2 合并优先级非空稀疏列用户 Delta provider-models.json models.json 最高优先级 中间 最低对预设支撑的行preset-backed row每个可空的模型配置列都是独立的所有权标记null表示继承注册表任何非空值都是用户 Delta。自定义行则存储完整配置。这一约定让目录变更无需数据迁移即可触达既有行——空字符串与空数组作为显式覆盖依旧有效。applyUserOverlayModelService.ts的实现非常直白对name、description、capabilities、contextWindow、maxInputTokens/maxOutputTokens、pricing、parameterSupport、reasoning、supportsStreaming、endpointTypes、模态等字段逐一判断! null才覆盖undefined/null即未设置。配合createModelsSqliteHandlers中仅插入与基线不同的可空列共同保证 user_model 里永远只存真正的用户偏差。3.3 用户覆盖保护当用户修改某个可被注册表增强的字段例如name该值直接存入对应可空列。读取时从当前注册表出发、逐个应用非空列创建/PATCH 时把入参值与当前注册表基线比较——因此渲染层回显renderer echo不会冻结目录值把值恢复回基线时该列被清空重新变回null继承。3.4 未来新增字段的分支策略注册表拥有用户不可编辑加入注册表 Schema、运行时Model与mergePresetModel不要新增user_model列或持久化 Delta。既有预设行在下一次读取时自动获得该字段零 Schema 迁移、零数据回填。用户可编辑的预设字段为其增加一个可空 Delta 列纳入 create/PATCH 覆盖映射与预设 Delta 字段集。Schema 迁移仅新增列既有行无需回填——null即继承当前注册表值。自定义模型字段自定义行拥有完整配置新必需字段要么给运行时默认值、要么做自定义行回填这是预设继承规则的有意例外。一个列可同时被自定义行与预设行共享对自定义行必填、对预设行保持null典型如capabilities、reasoning。四、RegistryLoader缓存、索引与空闲 TTLregistry-loader.ts 是注册表 JSON 的读取与查询引擎其生命周期契约懒加载首次访问时才读盘启动时不加载预计算索引首次加载后一次性构建换取 O(1) 查找空闲 TTL默认 30 秒无访问即自动失效DEFAULT_IDLE_TTL_MS 30_000访问即触碰每次findModel/findOverride/loadModels都会重置计时器服务级缓存ProviderRegistryService共享一个 loaderProvider Seeder 自建独立 loader。RegistryLoader的构造函数接收三个文件路径RegistryPaths并允许自定义 TTLtouch()内部用setTimeout实现失效invalidate()清空全部数据与索引下次访问重新加载。4.1 索引一览索引键用途modelByIdmodel.id精确模型查找modelByNormIdnormalizeModelId(id)规范化兜底modelBySizedNorm保留参数规模的规范化 ID解析带参数规模标签的变体gpt-oss:20b→gpt-oss-20boverrideByKeyproviderId::modelId精确覆盖查找overrideByNormKeyproviderId::normalizeModelId(id)规范化兜底overrideByApiKeyproviderId::apiModelId按提供方侧调用 ID 精确查找overrideByNormApiKeyproviderId::normalizeModelId(apiModelId)规范化后的提供方 ID 兜底overridesByProviderproviderId某提供方的全部覆盖索引构建有几个值得注意的细节均有源码注释佐证modelBySizedNorm让gpt-oss-20b与gpt-oss-120b保持区分它们在大小写无关键上会坍缩为同一个gpt-ossoverrideByKey采用自变体优先规则——同一提供方可能用多个apiModelId服务同一个规范模型如 tokenhub 带日期的原厂直供变体共享deepseek-v4-flash规范键必须解析到无日期的自变体apiModelId modelId日期变体只能经apiModelId索引触达。4.2 查询 APIloader.findModel(modelId) // O(1)精确 → 规范化兜底 loader.findOverride(providerId, modelId) // O(1)精确 → 规范化兜底 loader.getOverridesForProvider(providerId) // O(1)按提供方分组 loader.invalidate() // 释放全部数据下次访问重载findModel对带冒号规模标签的 IDgpt-oss:20b会先用colonVariantTagToHyphen对齐为连字符拼写再走modelBySizedNorm宁可返回null也不做错误兄弟型号的规模无关猜测。findOverride同样严格精确的规范modelId与提供方apiModelId两种精确查找必须先于两种规范化兜底否则google.gemma-3-27b-it这类 ID 会被同族的gemma-3-12b-it抢先占用规范化键。另外 registry-loader.ts 还定义了REGISTRY_SCHEMA_VERSION 2与REGISTRY_MIN_APP_VERSION 2.0.13远程注册表更新器按v{版本}/路径拉取数据只有 Schema 结构变化才升版本新模态/能力/effort 等枚举词条的增长不再升级v2 客户端会丢弃不认识的字段但旧运行时无法执行的语义值新 adapter family、端点类型等由REGISTRY_MIN_APP_VERSION兜底门控。五、模型 ID 规范化normalizeModelId用户侧看到的模型 ID 往往与注册表规范 ID 不同规范化让两者对齐用户看到注册表有规范化动作aihubmix-gpt-4ogpt-4o剥掉聚合前缀gpt-4o:freegpt-4o剥掉变体后缀claude-3.5-sonnetclaude-3-5-sonnet规范化版本分隔符aihubmix-gpt-4o:freegpt-4o组合处理normalizeModelId()实现在 packages/provider-registry/src/utils/normalize.ts执行顺序为1. 剥离 Provider 前缀anthropic/claude-3 → claude-3取最后一个 / 段 2. 小写化 3. 剥离聚合前缀aihubmix-、zai-、siliconflow-、nvidia-、groq-…… 4. 展开已知缩写mm- → minimax- 5. 剥离变体后缀:free、-thinking、(beta)…… 6. 剥离参数规模-72b、-7b…… 7. 规范化版本分隔符3.5 → 3-5、3p5 → 3-5 8. 下划线折叠为连字符HF 风格 bce-embedding-base_v1 → bce-embedding-base-v1实现细节相当严谨COMMON_AGGREGATOR_PREFIXES中刻意不放mm-它是 MiniMax 缩写交给PREFIX_EXPANSIONS展开若先当聚合前缀剥掉会得到孤儿 IDm2-1-medium同样被排除在变体后缀之外它是真实型号层级mistral-medium剥变体时还保护non/no/pre/anti/post等复合前缀开头的词-no-think不被误剥。变体 → 量化 → 日期快照的剥离被组织为stripVariantQuantDateSuffixes的不动点循环一次遍历非幂等因为尾部日期会屏蔽内层变体。此外还有针对 Bedrock 跨厂商 ARN 的专门处理us.anthropic.claude-sonnet-4-5-v1:0会被折叠为claude-sonnet-4-5区域厂商点号前缀、厂商连字符前缀、-v1:0修订全部剥离。查找策略精确匹配优先、规范化兜底。这保证当gpt-4o与aihubmix-gpt-4o作为独立条目同时存在时精确匹配胜出规范化不会造成错误折叠。六、关键数据库表6.1 user_provider表定义见 src/main/data/db/schemas/userProvider.ts。核心原则是一个 Provider 实例 一个 apiHost1:1一个 apiHost 可挂多个 API Key1:N列用途providerId主键用户自定义唯一 IDpresetProviderId指向 providers.json 条目null 自定义 Provider。双重职责既是来源预设标识也是侧边栏分组键——少数注册表行zai→zhipu、minimax-global→minimax指向不同预设以归入该分组name用户拥有的显示名首次种子写入时由预设初始化endpointConfigsJSON Delta用户的baseUrl覆盖自定义 Provider 还可存adapterFamily路由提示defaultChatEndpoint可空用户覆盖null 继承注册表默认apiKeysJSON 数组的 API Key 条目apiFeaturesJSON Delta仅存与注册表/应用默认不同的标志null 继承全部默认authConfig统一认证配置如iam-gcp/iam-azure/iam-aws6.2 user_model列用途id确定性主键providerId::modelIdproviderIdmodelIdProvider 内的唯一模型身份presetModelId指向 models.json 条目null 自定义模型name/capabilities/supportsStreaming自定义行必填预设行是可空 DeltainputModalities/outputModalities自定义行完整配置或可空 DeltacontextWindow/maxOutputTokens自定义行完整配置或可空 Deltareasoning自定义模型的内在控制/令牌上限预设行从注册表解析pricing自定义行完整配置或可空 Deltaparameters自定义行完整配置或可空 DeltaorderKey提供方模型列表中的分数排序键notes用户备注七、Provider 配置合并行是 Delta不是快照Provider 连接配置遵循与模型相同的分层、读时合并。user_provider行是Delta只存用户显式设置的值键缺失即使用注册表值。合并发生在rowToRuntimeProviderProviderService经由ProviderRegistryService.mergeEndpointConfigs/getProviderDisplayMetadata完成user_provider (DB, delta) providers.json (registry) app 默认值字段所有权解析规则endpointConfigs[ep].baseUrl用户行 注册表endpointConfigs[ep].adapterFamily注册表注册表 行自定义 Provider 提示inferAdapterFamily(ep)endpointConfigs[ep].modelsApiUrls注册表仅注册表端点类型键集合注册表 ∪ 用户注册表键与行键的并集apiFeatures混合{...DEFAULT_API_FEATURES, ...registry, ...row}defaultChatEndpoint混合行 注册表mergeEndpointConfigsProviderRegistryService.ts的具体实现印证了这一点端点键取并集新增端点无需迁移adapterFamily优先注册表值、否则行值、最后inferAdapterFamily(ep)按端点协议推导anthropic-messages→anthropic、openai-responses→openai、兜底openai-compatible见 registry-utils.tsdialect逐键浅合并行只声明用户发现的偏差输出按字段重建历史遗留的仅注册表字段如reasoningFormatType绝不会跨入运行时状态。为什么零数据迁移对应 issue #17096注册表拥有的事实从不冻结进行。端点类型新增、adapterFamily 变化、baseUrl/功能开关/默认端点调整都会在 Delta 契约下自动到达既有行。写路径强制维持 DeltaEndpointConfigOverride是唯一可持久化的端点形状PATCH 规范化会丢弃与注册表基线相等的值。一个直观例子未被用户触碰的预设baseUrl不在行中若提供方在providers.json中修改该 URL下一次读取就会返回新 URL。用户自定义的代理 URL 则留在行里持续生效直到用户将其重置为当前注册表值。name是有意例外它是用户拥有的完整值种子初始化不是注册表 Delta后续注册表改名不会覆盖它。若产品语义要改为改名之前一直继承name必须先转换成显式 Delta 表示。7.1 何时需要回填注册表内容更新在存储所有权契约不变时不需要回填仅注册表字段读时直接解析baseUrl、apiFeatures、defaultChatEndpoint等混合字段在行 Delta 缺失时继承既有用户覆盖有意持续生效不是陈旧数据新注册表拥有字段应加入读时投影而非持久化。Schema 迁移可能仍需为新用户可编辑字段新增存储但预设行在null/缺失继承语义下无需数据回填。只有两种情形必须回填完整自定义行新增无运行时默认值的必需字段或既有字段的所有权从完整快照改为 Delta 且需保留旧库数据。新增注册表字段的分支注册表拥有只加进读时合并输出对端点配置字段不要加入EndpointConfigOverride——Zod 会自动从写 DTO 剥离未知键零迁移。用户可编辑端点字段混合所有权加入EndpointConfigOverrideSchema其keyof集合即权威所有权声明在合并中加一条row.x ?? registry.x规则写路径可选丢弃等于基线的值零迁移。用户可编辑 Provider 字段若属于既有 JSON Delta如apiFeatures扩展对应 Schema 与合并规则即零迁移否则需选择显式持久化位置。新增独立列属 Schema 变更但可空预设 Delta 列仍无需值回填。永远不要把注册表拥有值作为行快照持久化——这正是注册表更新变陈旧的确切成因。八、推理Reasoning配置的双边界设计推理配置被刻意拆成两个边界模型数据声明内在控制与令牌上限ReasoningSupportSchema的controlseffort离散档位 /budget数字预算 /toggle开关见 packages/provider-registry/src/schemas/model.ts。主进程注册表增强会将其投影为渲染层控件消费的运行时selectableEffortsderiveSelectableEfforts按端点 wire profile 过滤掉无法表达的选择如无off时剔除noneProvider 注册表数据声明封闭的reasoningFormatwire profilereasoningContracts、endpointConfigs[*].reasoningFormat仅在主进程解析与解释从不复制进 SQLite、DataApi 或渲染层状态。请求路径按精确 provider-model → 端点覆盖/默认 → 穷尽格式默认的顺序解析出一个 profileresolveReasoningProfileFromRegistrycontract.wire ?? format.wire ?? selectFormatWire(formatDefault, wireDialect)再与提交时的规范选择组合最终发射为原生 AI SDK Provider 选项或通用兼容参数。wireDialecteffort/budget解决同一协议两代参数形状互斥的问题Gemini 3thinkingLevelvs 2.xthinkingBudget、Claude 4.6adaptivevs ≤4.5enabledbudget_tokens且该事实随模型而非端点。更完整的 Schema、优先级与 UI→请求数据流见 packages/provider-registry/docs/reasoning-control.md。九、文件索引速查内容位置注册表 JSON 数据packages/provider-registry/dataZod Schemapackages/provider-registry/src/schemasRegistryLoader加载、索引、TTLpackages/provider-registry/src/registry-loader.ts纯查找/转换函数packages/provider-registry/src/registry-utils.ts规范化工具packages/provider-registry/src/utils/normalize.ts种子运行器src/main/data/db/seeding/SeedRunner.ts预设 Provider 种子src/main/data/db/seeding/seeders/presetProviderSeeder.ts注册表服务合并查询src/main/data/services/ProviderRegistryService.ts模型服务用户 Delta 覆盖src/main/data/services/ModelService.tsProvider 服务读时合并src/main/data/services/ProviderService.tsDB Schemasrc/main/data/db/schemas/userModel.ts、userProvider.ts合并行为测试src/main/data/services/tests/modelMerger.test.ts结语Cherry Studio 的 Provider/Model 注册表系统用三份 JSON 做事实源、SQLite 行做 Delta、读时三层合并的组合同时满足了目录数据的频繁更新与用户自定义的持久化需求。理解这套设计的关键在于三句话注册表拥有的值永不落库读时解析让上游更新零迁移生效每个可空列是独立的所有权标记null 继承、非空即用户覆盖models.json→provider-models.json→ 用户 Delta 的优先级是全局唯一规则。无论是排查为什么改了注册表 baseUrl 不生效、为目录贡献新模型还是设计自己的 Provider/Model 数据架构本文所述的索引策略、规范化步骤与 Delta 契约都是可直接复用的范式。【免费下载链接】cherry-studioAI productivity studio with smart chat, autonomous agents, and 300 assistants. Unified access to frontier LLMs项目地址: https://gitcode.com/GitHub_Trending/ch/cherry-studio创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进