- 人工智能
- 大模型
- AI 应用
- 交互助手
- 本地部署
【免费下载链接】cherry-studio
🍒 Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端
导读
Cherry Studio 的 Provider 与 Model 数据采用"预设注册表 + 用户增量"的双层架构:内置的@cherrystudio/provider-registry包维护一套权威的预设目录(能力、价格、模态、端点等),启动时仅播种身份与认证骨架,运行时将所有连接配置与模型元数据实时合并用户增量。本文以 docs/references/provider-model/provider-registry.md 为骨架,结合 packages/provider-registry 包与 ProviderRegistryService.ts 等源码,系统讲解注册表的三份 JSON 数据、RegistryLoader的索引与 TTL 缓存、模型 ID 归一化管线、三层合并函数、数据库表结构与"零数据迁移"的增量设计,帮助读者理解预设数据如何在不冻结快照的前提下持续演进,以及如何安全地为注册表新增字段。
架构总览:一份注册表包,两处消费入口
整个体系由两部分组成:
@cherrystudio/provider-registry包(packages/provider-registry):纯数据与纯函数层,负责注册表 JSON 的定义、校验(Zod)、加载(RegistryLoader)与查询(索引化的纯函数)。它不依赖 Node.js 的fs,registry-utils.ts明确注释"Safe to import from browser/renderer contexts"。- 主进程数据服务(src/main/data):负责把注册表数据播种进 SQLite、处理用户 CRUD,并在每次读取时把"注册表基线 + 用户增量"合并成运行时对象。
注册表包内部结构
packages/provider-registry/ ├── data/ │ ├── models.json 预设模型(能力、价格、模态、参数支持...) │ ├── providers.json 预设提供商(端点、apiFeatures、元数据) │ └── provider-models.json 按提供商定制的模型覆盖(per-provider tweaks) ├── src/ │ ├── registry-loader.ts RegistryLoader:加载、校验、缓存、建索引、空闲 TTL │ ├── registry-utils.ts 纯函数:lookupRegistryModel、buildPersistedEndpointConfigs、inferAdapterFamily │ ├── utils/normalize.ts normalizeModelId 及各类辅助(聚合商前缀、变体后缀、参数规模...) │ └── schemas/ Zod 校验 schema(model/provider/provider-models/forwardCompat/enums...) └── docs/ └── reasoning-control.md 推理格式的 schema、优先级与 UI→请求数据流主进程消费端
src/main/data/ ├── db/seeding/ │ └── seeders/presetProviderSeeder.ts ISeeder:仅插入的提供商身份/认证骨架播种 ├── services/ │ ├── ProviderRegistryService.ts 注册表查询与提供商/模型基线解析 │ ├── ModelService.ts 模型 CRUD 与用户增量叠加 │ └── ProviderService.ts 提供商 CRUD 与读取期合并 └── api/handlers/ ├── models.ts 模型 CRUD、对账、注册表解析路由 └── providers.ts 提供商 CRUD 与预设投影路由值得强调的是:原文档刻意不在架构文档里重复目录条目数量,因为 JSON 文件本身才是唯一事实来源(source of truth),其规模随发布独立变化。
数据流三条主线
1. 启动:预设提供商播种(仅插入)
数据库初始化时,DbService.onInit()触发SeedRunner.runAll(seeders),其中PresetProviderSeeder只做一件事——把注册表里还不存在的提供商身份行插入user_provider:
DbService.onInit() → SeedRunner.runAll(seeders) → PresetProviderSeeder.run(db) → RegistryLoader.loadProviders() // 读取 providers.json → SELECT 已有 provider ID(来自 user_provider) → 仅 INSERT 新提供商的 identity/auth 行 → 绝不物化注册表拥有的连接配置关键设计(见 presetProviderSeeder.ts 的头部注释与实现):
- 种子行是增量行(DELTA row):
endpointConfigs、defaultChatEndpoint这类注册表拥有的连接配置不会被持久化,而是在每次读取时从注册表实时解析(源码注释引用了 issue #17096)。因此注册表更新无需对账即可到达既有安装。 - 只播种用户可编辑的脚手架:身份(
providerId)、预设归属(presetProviderId)、显示名(name)以及必需的认证外壳(authConfig)。toDbRow中presetProviderId ?? p.id处理了别名/分组预设(如zai→zhipu)。 SeedRunner在providers.json版本变化时重跑该 seeder,但由于"已有行跳过",它保持幂等与仅插入语义。- 特殊认证外壳:
getSeedAuthConfig只为三个复用他人端点协议的提供商注入认证类型——vertexai用iam-gcp、azure-openai用iam-azure、aws-bedrock用iam-aws,其余返回null。这是因为"Azure/Vertex/Bedrock 复用其他厂商的端点协议,authType是唯一可靠的判别器",厂商 URL 路由由authType驱动(iam-azure→ AI SDKcreateAzure,iam-gcp→ Vertex SDK)。 - 保护语义:规范预设提供商不可被用户删除。多数行满足
providerId === presetProviderId;别名/分组预设也通过注册表查询获得保护。继承自预设的用户自建提供商则可以删除。
2. 按需:模型创建(POST /models)
当用户添加模型时(例如POST /models [{ providerId: 'openai', modelId: 'gpt-4o' }]),处理流程把"注册表解析"与"用户增量入库"分开:
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) 索引查询 → 从注册表数据解析端点配置档(仅主进程,不持久化) → 返回 { presetModel, registryOverride, reasoningProfile } → handler:modelService.create(items) → mergePresetModel(preset, override, ...) → 将显式 DTO 字段与注册表基线比较 → 仅 INSERT 与基线不同的可空列到 user_model → list/get/mutation 响应 → 重建当前注册表基线 → 叠加每个非空稀疏列注意最后两步的精妙之处:Create 阶段比较"传入值与当前注册表基线",只有不同才入库,因此渲染层回显(renderer echo)不会把目录值"冻结"进数据库;而读取阶段总是"从当前注册表基线出发叠加非空列"。两者结合保证了目录演进能自动到达既有行。
3. 解析 SDK 模型列表(resolveModels)
GET /providers/:providerId/models:resolve?ids=gpt-4o&ids=o3 → providerRegistryService.resolveModels(providerId, modelIds) → 对每个 modelId: → RegistryLoader.findModel(modelId) // O(1),归一化回退 → RegistryLoader.findOverride(providerId, modelId) // O(1) → mergePresetModel(...) 或 createCustomModel(...) → 返回合并后的 Model[]该路由服务于"SDK 只提供模型 ID"的场景——能力、价格、模态等其余全部数据来自注册表,SDK 数据不会覆盖精选的注册表数据。
三个合并函数与优先级
源码在 ProviderRegistryService.ts 中为三种不同场景提供了三个独立函数(见 L407-L519 附近):
| 函数 | 使用场景 | 合并层 |
|---|---|---|
mergePresetModel | 注册表查询、resolveModels | preset → override(两层,不涉及用户数据) |
applyUserOverlay | 带显式用户增量的模型读取 | 合并后的注册表基线 → 用户 |
createCustomModel | 注册表无匹配项 | 仅 modelId(最小化自定义模型) |
公共逻辑被抽取为:
applyPresetAndOverride(presetModel, catalogOverride):对除推理外的所有字段做 preset → override 两层合并。从源码看,它依次处理 capabilities(applyCapabilityOverride支持force/add语义)、modalities、endpointTypes、name、contextWindow、maxOutputTokens/maxInputTokens、pricing(浅展开叠加)、parameterSupport 与replaceWith(模型替换标记,会生成providerId::replaceWith的唯一 ID)。resolveReasoning(reasoningSupport, profile):把模型的推理声明(如 effort 枚举、thinking token 预算、toggle)结合端点推理格式 profile,解析出运行时推理配置。
优先级规则
非空稀疏列(用户增量) > provider-models.json(按提供商覆盖) > models.json(全局预设) 最高 中间 最低对于预设支撑的行,每个可空的模型配置列都是独立的归属标记:null表示继承注册表;任何非空值都是用户增量。自定义行则存储完整配置。这一设计使得目录变化无需数据迁移即可到达既有行——显式的空字符串与空数组仍然作为合法覆盖生效(即"空值覆盖"不会被误判为"继承")。
用户覆盖保护
当用户修改某个可被注册表增强的字段(例如name),该值被直接存入对应的可空列;读取时从当前注册表出发叠加所有非空列。Create/PATCH 会把传入值与当前注册表基线比较,因此:
- 渲染层回显不会冻结目录值;
- 把值恢复为基线会清空该列(重新回到"继承"语义)。
源码中的EndpointConfigOverrideSchema与 PATCH 归一化进一步保证写路径的增量性——等于基线的值在写入时被丢弃。
未来新增字段的三种路径(原文档的核心方法论)
| 场景 | 做法 | 是否迁移 |
|---|---|---|
| 注册表拥有、用户不可编辑的字段 | 加入注册表 schema、运行时Model类型与mergePresetModel;不加user_model列 | 零迁移,既有预设行下次读取即获得 |
| 用户可编辑的预设字段 | 新增可空增量列,纳入 create/PATCH 叠加映射与预设增量字段集 | 需 schema 迁移加列,但既有行无需数据回填(null=继承) |
| 自定义模型字段 | 自定义行拥有完整配置;新增必填字段需运行时默认值或自定义行回填 | 这是预设继承规则的刻意例外 |
一个有趣的事实佐证:一个被自定义行与预设行共享的列,可以对自定义行是必填、对预设行保持 null(例如capabilities与reasoning)——userModel.ts 中的检查约束presetModelId IS NOT NULL OR (name IS NOT NULL AND capabilities IS NOT NULL AND supportsStreaming IS NOT NULL)正是这一语义的数据库级落地。
RegistryLoader:索引化缓存与 30 秒空闲 TTL
registry-loader.ts 实现了"缓存 + 索引 + 空闲自动过期"的注册表访问器。其生命周期设计:
- 懒加载(Lazy load):数据在首次访问时才从磁盘读取,不在启动时加载(
loadModels/loadProviders/loadProviderModels内部先touch()再检查缓存)。 - 预计算索引:首次加载后一次性构建全部索引,使查询达到 O(1)。
- 空闲 TTL:默认 30 秒(
DEFAULT_IDLE_TTL_MS = 30_000,构造函数可注入覆盖)。每次touch()重置定时器;定时器触发invalidate()释放全部数据与索引,下次访问重新加载。 - 作用域隔离:
ProviderRegistryService在查询间共享同一个 loader;PresetProviderSeeder则创建自己的 loader(见 presetProviderSeeder.ts 的getLoader())。
索引矩阵
| 索引 | 键 | 用途 |
|---|---|---|
modelById | model.id | 精确模型查询 |
modelByNormId | normalizeModelId(id) | 归一化回退 |
modelBySizedNorm | 保留参数规模的归一化 ID | 解析带参数规模标签的变体(如gpt-oss:20b) |
overrideByKey | providerId::modelId | 精确覆盖查询 |
overrideByNormKey | providerId::normalizeModelId(id) | 归一化回退 |
overrideByApiKey | providerId::apiModelId | 精确的提供商侧模型 ID 查询 |
overrideByNormApiKey | providerId::normalizeModelId(apiModelId) | 归一化的提供商侧回退 |
overridesByProvider | providerId | 某提供商的全部覆盖 |
查询 API
loader.findModel(modelId) // O(1):精确 → 归一化回退 loader.findOverride(providerId, modelId) // O(1):精确 → 归一化回退 loader.getOverridesForProvider(providerId) // O(1):按提供商分组 loader.invalidate() // 释放全部数据,下次访问重新加载源码中findModel的查询顺序非常讲究(这也是索引多达 8 张的原因):
- 先查
modelById(精确匹配); - 若 ID 带冒号变体标签(如
gpt-oss:20b),用colonVariantTagToHyphen重排为连字符拼写后,走保留规模的modelBySizedNorm——确保:20b命中gpt-oss-20b而不是同家族的gpt-oss-120b兄弟行; - 无标签时优先 size-preserving 键,若目录里根本没有该规模(如
qwen2.5:7b目录只有qwen2-5-*-instruct),返回 null 而非瞎猜——错误兄弟行的价格、限制与presetModelId比没有元数据更糟; - 最后才回退到无规模的
modelByNormId。
findOverride同样有严格的顺序纪律(见源码 L316-L335 的注释):两个精确查询(canonical modelId、provider apiModelId)必须先于两个归一化回退。因为归一化会剥掉规模/日期后缀,多个不同行会塌缩到同一个归一化键(如google.gemma-3-27b-it与gemma-3-12b-it都 →gemma-3-it);若归一化回退先于 apiModelId 精确查询,一个精确的 SDK ID 就会解析到先被索引的同族行。此外,overrideByKey建立时采用了"self variant 优先"规则:当同一 canonical 键出现多个行(如 tokenhub 带日期的原厂直供变体共享deepseek-v4-flash)时,apiModelId === modelId的自身体变体优先占用槽位。
远程目录与 schema 版本
registry-loader.ts还导出与远程目录更新相关的契约常量:
REGISTRY_SCHEMA_VERSION = 2:远程更新器从v{version}/路径拉取,同步 CI 发布到匹配目录,保证应用只会收到其捆绑 schema 能解析的数据。v2 起 schema 采用"丢弃未知键"的向前兼容策略(schemas/forwardCompat.ts),枚举词表增长不再升级版本,只有结构性变更(字段改名/改型/必填移除)才升级。REGISTRY_MIN_APP_VERSION = '2.0.13':能执行当前目录语义值的最老应用版本;新 adapter family、端点类型或线行为需要升级它。REMOTE_REGISTRY_FILES = ['models.json', 'provider-models.json']:可被未签名远端数据覆盖的文件;providers.json(提供商路由)始终捆绑。
模型 ID 归一化:一份 ID 到目录规范 ID 的七步管线
不同提供商暴露给用户的模型 ID 往往与注册表规范 ID 不同,normalizeModelId()(packages/provider-registry/src/utils/normalize.ts)是"单一事实来源":
| 用户看到 | 注册表拥有 | 归一化 |
|---|---|---|
aihubmix-gpt-4o | gpt-4o | 剥离聚合商前缀 |
gpt-4o:free | gpt-4o | 剥离变体后缀 |
claude-3.5-sonnet | claude-3-5-sonnet | 归一化版本分隔符 |
aihubmix-gpt-4o:free | gpt-4o | 组合处理 |
处理管线:
1. 剥离提供商前缀(如 "anthropic/claude-3" → "claude-3") 2. 转小写 3. 剥离聚合商前缀(aihubmix-、zai-、siliconflow-、...) 4. 展开已知缩写(mm- → minimax-) 5. 剥离变体后缀(:free、-thinking、(beta)、...) 6. 剥离参数规模(-72b、-7b、...) 7. 归一化版本分隔符(3.5 → 3-5、3p5 → 3-5)源码揭示了大量精心维护的细节常量:
- 聚合商前缀(
COMMON_AGGREGATOR_PREFIXES):AIHubMix 系(aihubmix-/aihub-/ahm-)、云厂商路由(alicloud-/azure-/baidu-/cbs-...)、平台聚合商(deepinfra-/groq-/nvidia-/sophnet-)、下划线前缀(dmxapi_/aistudio_)。注意mm-故意不在其中——它是 MiniMax 简写,由PREFIX_EXPANSIONS展开为minimax-,若先作为聚合商前缀剥离会导致m2-1孤儿 ID。 - 变体后缀:冒号系(
:free/:nitro/:extended/:beta/:preview/:thinking/:exacto/:latest/:cloud)、连字符系(-free/-search/-online/-think/-reasoning/-classic/-low/-high/-minimal/-thinking/-aliyun...)与括号系((free)/(beta)/(preview)/(thinking))。-medium同样故意不在连字符后缀列表——它是真实模型档位名(mistral-medium、devstral-medium)。 - 保护复合前缀:
non/no/pre/anti/post——剥离-no-think前会检查前缀是否为独立词元,避免误伤inferno-search这类本应剥离的 ID(注释举例:...-no-think保留,volcano-free剥离)。 - 量化后缀:
-fp8/-fp16/-bf16/-awq/-int4/-int8/-gguf/-gptq——同一逻辑模型的不同精度拼写被折叠。 - 日期快照(
DATE_SNAPSHOT_PATTERN):剥离claude-sonnet-4-5-20250929、gpt-4o-2024-08-06、kimi-k2-250905等发布日戳;要求合法月份(01-12)与日期(01-31),确保glm-4-9b这类规模/版本永不被误伤。该模式与构建期规范化器(generate-catalog.ts)共享同一定义。 - Bedrock 跨厂商 ARN:剥离前导的
[region.]vendor.点号段与vendor-连字符段(us.anthropic.claude-sonnet-4-5-v1:0→claude-sonnet-4-5),并剥离尾部修订号:0/-v1:0。 - 归一化到不动点:变体 → 量化 → 日期剥离需要迭代至稳定(
stripVariantQuantDateSuffixes循环),因为"尾部日期会屏蔽内部变体"——...-thinking-2507只有在日期剥离后才暴露-thinking。 keepParameterSize模式:保留规模的关键字,供modelBySizedNorm索引使用——先通过colonVariantTagToHyphen把:20b重排为-20b(gpt-oss:20b→gpt-oss-20b),使大小成为 ID 的连字符词元而不被stripParameterSize剥离。
查找策略:先精确匹配、后归一化回退。这保证若gpt-4o与aihubmix-gpt-4o同时作为独立条目存在,精确匹配优先。从源码看,findModel还追加了"保留规模的归一化优先于无规模家族键"以及"无对应规模的目录条目时返回 null"两条规则,避免跨规模兄弟行的错误元数据污染。
关键数据库表:增量存储的两张表
user_provider(src/main/data/db/schemas/userProvider.ts)
| 列 | 用途 |
|---|---|
providerId | 主键,用户自定义的唯一 ID |
presetProviderId | 关联 providers.json 条目(null=自定义提供商)。双重职责:标识来源预设并作为侧栏分组键——少数注册表行(如zai→zhipu、minimax-global→minimax)指向不同预设,从而折叠到该分组下 |
name | 用户拥有的显示名,首次播种时从预设初始化 |
endpointConfigs | JSON 增量:用户的baseUrl覆盖;自定义提供商还可存adapterFamily路由提示 |
defaultChatEndpoint | 可空用户覆盖;null 继承注册表默认值 |
apiKeys | API 密钥条目 JSON 数组 |
apiFeatures | JSON 增量:仅存与注册表/应用默认值不同的标志;null 继承全部默认值 |
user_model(src/main/data/db/schemas/userModel.ts)
| 列 | 用途 |
|---|---|
id | 确定性主键:providerId::modelId |
providerId+modelId | 提供商内模型唯一身份(有唯一约束user_model_provider_model_unique) |
presetModelId | 关联 models.json 条目(null=自定义模型),兼作追溯标记 |
name/capabilities/supportsStreaming | 自定义行必填;预设行可空增量 |
inputModalities/outputModalities | 自定义行完整配置或预设行可空增量 |
contextWindow/maxOutputTokens | 同上 |
reasoning | 自定义行的内禀控制/token 限制;预设行从注册表解析 |
pricing | 自定义行完整配置或预设行可空增量 |
parameters | 同上 |
orderKey | 提供商模型列表中的分数排序键 |
notes | 用户备注 |
从 userModel.ts 源码看,表还带presetModelId索引、(providerId, isEnabled)索引、按providerId作用域的orderKey索引,以及前文提到的检查约束(preset 行可空、custom 行必填)。providerId通过外键级联删除(ON DELETE CASCADE)。
提供商配置合并:与模型相同的增量契约
提供商连接配置采用与模型相同的分层、读取期合并。user_provider行本身是一个增量(delta):只存用户显式设置的内容,键缺失即"使用注册表值"。合并发生在rowToRuntimeProvider(ProviderService)中,经由ProviderRegistryService.mergeEndpointConfigs/getProviderDisplayMetadata:
user_provider (DB, delta) > providers.json (registry) > app defaults| 字段 | 归属 | 解析 |
|---|---|---|
endpointConfigs[ep].baseUrl | 用户 | row > registry |
endpointConfigs[ep].adapterFamily | 注册表 | registry > row(自定义提供商提示)>inferAdapterFamily(ep) |
endpointConfigs[ep].modelsApiUrls | 注册表 | 仅注册表 |
| 端点类型键集 | 注册表 ∪ 用户 | 注册表与行的键取并集 |
apiFeatures | 混合 | {...DEFAULT_API_FEATURES, ...registry, ...row} |
defaultChatEndpoint | 混合 | row > registry |
inferAdapterFamily(registry-utils.ts)是 seeder / migrator / UI 创建路径的单一事实来源:目录adapterFamily优先(编码厂商特定中继路由,如 AiHubMix 上 anthropic-messages 的aihubmix)→ 按端点类型默认值(anthropic-messages→anthropic、google-generate-content→google、ollama-chat/ollama-generate→ollama、jina-rerank→jina-rerank、openai-responses→openai)→ 最终回退openai-compatible。
另一个推导细节:endpointImpliedCapability从"能力专属端点"推导模型能力——jina-rerank只能重排、openai-embeddings只能嵌入、图像/音频/视频专用端点只能服务对应媒体任务。这是"目录没有该条目(如不透明网关/NewAPI 模型 ID)时从端点推导能力"的单一事实来源。
为什么注册表更新"零数据迁移"
因为注册表拥有的值从不被冻结进行内,注册表更新(新端点类型、adapter family 变更、baseUrl 变更、特性开关、默认端点变化)以零数据迁移到达增量契约下创建的行(issue #17096)。写路径强制增量:
EndpointConfigOverride是唯一可持久化的端点形状;- PATCH 归一化丢弃等于注册表基线的值。
例如:一个未被触碰的预设baseUrl不出现在行中。若提供商在providers.json中更改该 URL,下次读取即返回新 URL;用户自定义的代理 URL 保留在行中并持续生效,直到用户把它重置回当前注册表值。
provider.name是刻意的例外:它是用户拥有的完整值(播种时初始化),不是注册表增量——之后注册表改名不会替换它。若产品语义要改为"改名之前继承",必须先把name转换为显式增量表示。
何时需要回填(Backfill)
注册表内容更新在存储归属契约不变时不需要回填:
- 注册表专属字段在读取时直接解析;
- 混合字段(
baseUrl、apiFeatures、defaultChatEndpoint)在其行增量缺失时继承; - 既有用户覆盖刻意保持优先,它们不是陈旧数据;
- 新增的注册表专属字段应加入读取期投影,而非持久化。
schema 迁移仍可能需要——当新增用户可编辑字段需要存储时;但预设行在"null/缺失=继承"语义下无需数据回填。只有两种情况必须回填:完整自定义行新增无运行时默认值的必填字段;或既有字段从完整快照改为增量、且需保留旧数据库。
新增注册表字段的操作指南(原文档的落地要点):
- 注册表拥有、用户不可编辑:只加入读取期合并输出。端点配置字段不要加入
EndpointConfigOverride——Zod 会自动从写 DTO 中剥掉它(因为keyof集合是权威归属声明)。零迁移。 - 用户可编辑的端点字段(混合归属):加入
EndpointConfigOverrideSchema(keyof集合即权威归属声明),在合并中加row.x ?? registry.x规则,可选地在写入时丢弃等于基线的值。零迁移——缺失键回退注册表。 - 用户可编辑的提供商字段:若属于既有 JSON 增量(如
apiFeatures),扩展该 schema 与合并规则即零迁移;否则选择显式持久化覆盖位置。新独立列属于 schema 变更,但可空的预设增量列仍无需值回填。该设计不让任意顶层字段免迁移。 - 永不把注册表拥有的值作为行快照持久化——那正是注册表更新变陈旧的确切原因。
推理配置:模型数据与提供商线协议的边界划分
推理(reasoning)配置被刻意拆分到两个边界(详见 packages/provider-registry/docs/reasoning-control.md):
- 模型数据声明内禀控制与 token 限制。主进程注册表增强把这些投影为仅运行时的
selectableEfforts,供渲染层控件消费。模型侧 schema(schemas/model.ts)支持三种控制种类:effort(离散档位,values是模型内禀词表,'none'出现 ⇔ 推理可被禁用)、budget(数值型思考 token 预算,min/max/default,且min<=default<=max有 superRefine 校验)、toggle(仅开/关)。 - 提供商注册表数据声明一个封闭的
reasoningFormat线协议 profile,仅在主进程解析与解释,从不复制进 SQLite、DataApi 或渲染层状态。
请求路径从"精确的提供商-模型 → 端点覆盖/默认 → 穷尽格式默认"解析出一个 profile,与提交时的规范选择组合,最终发射为原生 AI SDK 提供商选项或通用兼容参数。推理的引入还伴随能力注入:mergePresetModel中,当传入reasoningSupport时,MODEL_CAPABILITY.REASONING会被并入 capabilities(见 ProviderRegistryService.ts)。
值得一提的createCustomModel细节:无注册表匹配的模型仍会通过inferCustomModelReasoning(modelId, profile)在摄取期推断推理描述——只要 ID 可辨识为推理 SKU,自定义行也像目录行一样拥有推理描述符(issue #16598)。synthesizePresetFromOverride则允许provider-models.json完全独立承载厂商独占模型(ModelScope 的Tongyi-MAI/Z-Image-Turbo、PPIO 定制端点等),无需全局目录条目。
文件位置速查
| 内容 | 位置 |
|---|---|
| 注册表 JSON 数据 | packages/provider-registry/data/ |
| Zod schemas | packages/provider-registry/src/schemas/ |
| RegistryLoader(加载、索引、TTL) | packages/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 |
| 预设提供商播种 | src/main/data/db/seeding/seeders/presetProviderSeeder.ts |
| 服务(合并查询) | src/main/data/services/ProviderRegistryService.ts |
| 模型服务(用户增量叠加) | src/main/data/services/ModelService.ts |
| 提供商服务 | src/main/data/services/ProviderService.ts |
| DB schemas | src/main/data/db/schemas/userModel.ts、src/main/data/db/schemas/userProvider.ts |
总结:增量契约如何让目录持续演进
Cherry Studio 的 Provider/Model 注册表系统本质上是三句话的架构:
- 目录即事实:
models.json、providers.json、provider-models.json是唯一事实来源,RegistryLoader以 O(1) 索引 + 30 秒空闲 TTL 提供高速只读访问; - 行即增量:
user_provider与user_model只存用户显式差异,null/键缺失 = 继承注册表,任何注册表更新零迁移到达既有行; - 读取时合并:
mergePresetModel(preset→override)、applyUserOverlay(基线→用户)、createCustomModel(纯自定义)三个合并函数按场景分工,配合七步 ID 归一化管线,让 SDK 的任意 ID 拼写都能落回目录规范行。
理解这套契约后,无论是排查"为什么我的模型价格变了"(目录更新了,你的行没有覆盖)、还是规划"如何给注册表加一个新字段"(按上文三种路径选择),都有了清晰的决策依据。
- 人工智能
- 大模型
- AI 应用
- 交互助手
- 本地部署
【免费下载链接】cherry-studio
🍒 Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端
相关推荐
Cherry Studio Provider & Model Registry 系统解析:预设数据如何加载、归一化、播种并与用户数据合并
Cherry Studio Provider & Model Registry 系统解析:预设数据如何加载、归一化、播种并与用户数据合并 本篇技术指南围绕 Ch
AI 应用大模型桌面应用本地部署RAGCherry Studio Provider & Model 注册表系统:预设数据加载、规范化、种子写入与用户数据合并全解析
Cherry Studio Provider & Model 注册表系统:预设数据加载、规范化、种子写入与用户数据合并全解析 导读 Cherry Studio
AI 应用大模型桌面应用本地部署RAGCherry Studio 工具注册表(Tool Registry)深度解析:统一 AI SDK ToolEntry、MCP 同步与延迟暴露机制
Cherry Studio 工具注册表(Tool Registry)深度解析:统一 AI SDK ToolEntry、MCP 同步与延迟暴露机制 导读 本文围绕
AI 应用大模型桌面应用本地部署RAG
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考