模型定价表的三层来源与本地覆盖
CC Switch 会把用量明细里的 token 数换算成花费。要换算就得有单价,单价存在哪、谁说了算、你改了之后应用更新会不会把你改的冲掉——这三个问题的答案分别落在三个不同的地方。
以下全部基于我们本地 clone 的 cc-switch 仓库快照 c39c903(提交日期 2026-08-10),仓库内版本号 3.19.2。我们只读源码文本,没有安装也没有运行过这个桌面应用,所以本文不涉及任何界面与操作描述。
落地的只有一张表,来源有三层
价格最终只住在一个地方:SQLite 里的 model_pricing 表。建表语句在 src-tauri/src/database/schema.rs:235-244,六列 —— model_id 作主键,display_name,然后是四种单价:input_cost_per_million、output_cost_per_million、cache_read_cost_per_million、cache_creation_cost_per_million。
有一处值得先记下来:这四列的价格是以 TEXT 存的,不是数值类型。
往这张表里写数据的路径有三条,这就是标题里的「三层」:
| 层 | 位置 | 我们数出来的条目数 | 写入方式 |
|---|---|---|---|
| 内置种子 | src-tauri/src/database/schema.rs:1529 起的 seed_model_pricing | 189 条 | INSERT OR IGNORE |
| 内置修价 | src-tauri/src/database/schema.rs:2484 起的 repair_current_model_pricing | 34 条 | UPDATE ... SET 带守卫 |
| 本地覆盖 | 应用配置目录下的 model-pricing.json | 只存你改过的 | 启动时同步进库 |
189 与 34 这两个数是我们自己数出来的:先按行剥掉 // 注释,再在 let pricing_data = [ ... ] 与 let pricing_fixes = [ ... ] 数组内按括号深度扫描第一层元组并计数。种子那 189 条里 model_id 无重复。
第一层为什么必须是 INSERT OR IGNORE
seed_model_pricing 用的是 INSERT OR IGNORE(src-tauri/src/database/schema.rs:2462-2470),意思很直白:这个 model_id 已经在表里了,就当没这回事,一行都不改。
这条语义决定了后面所有事情的走向。你第一次跑起来时,189 条种子价格进库;从此之后,无论应用怎么升级、种子数组里那一行的数值改成什么,你库里那一行都不会被种子覆盖。因为主键已存在,OR IGNORE 直接跳过。
对老用户来说这是保护 —— 你手动改过的价格不会在某次更新后被静默改回去。但它同时也带来一个后果:上游降价了,光改种子数组是没用的,只有从没建过库的新用户才吃得到新价。
第二层:那张 34 条的修价表就是为了解决这个
repair_current_model_pricing 的存在,就是给上面那个后果打的补丁。它不走 INSERT,走的是 UPDATE ... SET,而且每条都带守卫条件(src-tauri/src/database/schema.rs:2484 起)。
这张表最有意思的地方在于它的注释是日期化的,按改价批次成组标注了日期与缘由。举两条:src-tauri/src/database/schema.rs:2486-2492 那条注释写的是「2026-07-30 OpenAI GPT-5.6 降价:luna -80%、terra -20%(sol 不变)」;:2537 那条写的是「2026-07-31 models.dev 审计核价:DeepSeek V4 发布后 chat/reasoner 降为 V4 Flash」。
所以这 34 条不是「另一份价格表」,而是一串带时间戳的定向修正记录。种子表是「一个模型第一次进库时按什么价算」,修价表是「某个具体日子因为某个具体事件,把已经进库的某几行改成什么」。两张表的职责完全不同,混着看就乱了。
想知道这个仓库改过哪些模型的价、又是为什么改,直接读这 34 条的注释比读 CHANGELOG 快——我们摘到的两条注释日期分别是 2026-07-30 与 2026-07-31。
第三层:本地覆盖文件刻意不存全量
这是本篇最反直觉的一处,也是最容易踩的一处。
用户自定义的价格走一个本地文件 model-pricing.json,放在应用配置目录下,文件版本常量 MODEL_PRICING_FILE_VERSION = 1(src-tauri/src/services/model_pricing.rs:13-14、:102-104)。文件结构四块:version、modelsDevSync、models(覆盖项)、deletedModelIds(删除墓碑)。
按直觉,你会预期这个文件是「你库里全部价格的一份可编辑镜像」——打开就能看到 189 条,想改哪条改哪条。它不是。 代码注释把这件事写得很死(src-tauri/src/services/model_pricing.rs:70-80、:229-233、:291-294):本地文件只存用户与 models.dev 的覆盖项,内置价归数据库所有,这样应用更新才能修正内置价。
把这句话和前两层拼起来,逻辑就闭合了:一旦把完整的种子表导出到本地文件,那 189 条内置价就全都变成了「用户覆盖项」;代码注释给的理由是:内置价必须归数据库所有,应用更新才能修正它。把完整种子表导出到本地文件之后,内置价变成了覆盖项,第二层 repair_current_model_pricing 做的定向修正会在启动时被回滚——这就是它坚持只存差异的原因。
对你的直接影响是两条:第一,这个文件里条目少甚至为空是正常的,不代表价格表没生成;第二,别拿它当价格全集去做备份或迁移,你搬走的只是覆盖层,不是价格本身。
写这个文件用的是 atomic_write,并有进程级互斥锁 MODEL_PRICING_FILE_LOCK(src-tauri/src/services/model_pricing.rs:16-20、:219-225)。删除记的是墓碑而不是直接抹掉,也是同一个思路:如果只是把行删了,下次种子或同步一来它又回来了,得留个标记说明「这是我主动删的」。
models.dev 同步:默认是关的,而且在前端
第三层里还挂着一条外部来源。同步配置结构 ModelsDevSyncConfig 有六个字段(src-tauri/src/services/model_pricing.rs:41-68):autoSyncEnabled、includeCommonModels、selectedModelKeys、excludedCommonModelKeys、lastSyncAt、lastSyncError。
两个默认值要看清:autoSyncEnabled 默认 false,includeCommonModels 默认 true。也就是说自动同步不开箱即用,得你自己打开。
抓取动作发生在前端而不是 Rust 后端:MODELS_DEV_API_URL = "https://models.dev/api.json",超时 15000ms(src/lib/modelsDevPricing.ts:3-4);启动同步有节流常量 MODELS_DEV_STARTUP_SYNC_INTERVAL_MS = 6 * 60 * 60 * 1000,即六小时(src/lib/modelsDevAutoSync.ts:20)。同步结果算覆盖项,和用户手改的价格住在同一个文件的 models 这一块里。
配套的 Tauri 命令有七个,都在 src-tauri/src/commands/usage.rs:122-243:get_model_pricing、update_model_pricing、update_model_pricing_batch、delete_model_pricing、get_models_dev_sync_config、save_models_dev_sync_config、record_models_dev_sync_result。
一条请求怎么找到自己的价
价格表齐了,还得能命中。查找函数 find_model_pricing_row 的策略是先按候选列表精确匹配,匹配不上再考虑前缀匹配。前缀匹配不是无条件放行,should_try_pricing_prefix_match 按模型名里短横线的个数设了门槛(src-tauri/src/services/usage_stats.rs:2044-2054、:2319-2362):
claude-前缀,至少要 3 个短横线o1/o3/o4/o5开头,至少 1 个gpt-、gemini-、deepseek-、qwen-、glm-、kimi-、minimax-这七个族,至少 2 个
门槛的作用是防止过短的名字胡乱命中一整族的价格。这也意味着,如果你自己加一条价格但 model_id 写得比仓库里的短横线更少,它可能既精确匹配不上、也够不着前缀匹配的门槛。
命中之后用的是哪条价,会被留痕:明细表 proxy_request_logs 有一列 pricing_model,注释写明它是写入时实际用于计价的模型名,NULL 表示 v11 之前的历史行、'' 表示未计价的错误行(src-tauri/src/database/schema.rs:194-200)。日聚合表 usage_daily_rollups 也把 pricing_model 放进了主键 6 元组里,建表注释说这是为了「否则明细被 prune 后接管计费不可审计」(src-tauri/src/database/schema.rs:268-296)。换句话说,你事后想核某笔花费是按哪个模型的价算的,看这一列,不要靠 model 列去猜。
用户手册那张预设价格表,和种子表对不上
用户手册 4.4「预设价格」也列了一张价格表,它和数据库种子表的取值不一致,有三处可核实的差异:
币种口径不同。 手册中国厂商表以人民币列价,例如 deepseek-v4-flash 记为 ¥1.00/¥2.00/¥0.20、glm-4.7 记为 ¥2.00/¥8.00/¥0.40、kimi-k2-turbo 记为 ¥8.00/¥58.00/¥1.00(docs/user-manual/zh/4-proxy/4.4-usage.md:296-315);种子表里同名条目存的是另一组数值:deepseek-v4-flash 0.14/0.28/0.0028、glm-4.7 0.6/2.2/0.11、kimi-k2-turbo 1.11/8.06/0.14。
有一条手册标「免费」而种子表有价。 手册写「mimo-v2-flash|免费|免费|-」(4.4-usage.md:315),种子表该条目是 0.09 / 0.29 / 0.009 / 0。
模型代次不同步。 手册 Claude 表最新一档是「Claude 4.8 系列 / claude-opus-4-8」(4.4-usage.md:250-255),种子表里已经有 claude-fable-5、claude-mythos-5、claude-opus-5、claude-sonnet-5、claude-sonnet-4-6 等更新条目(schema.rs:1530-1560 附近)。手册那张表没有声明自己是全量列表,原文写的是「预设了常用模型的官方价格」。
这三处差异我们只陈述,不推断哪一处「才是对的」、也不拿它评价这个项目。你要算钱,以你库里 model_pricing 表的实际值为准。
你自己怎么核一遍
三个动作,都不需要安装这个应用,clone 下来读源码就行:
grep -n "CREATE TABLE IF NOT EXISTS model_pricing" src-tauri/src/database/schema.rs
grep -n "fn seed_model_pricing\|fn repair_current_model_pricing" src-tauri/src/database/schema.rs
grep -n "MODEL_PRICING_FILE_VERSION" src-tauri/src/services/model_pricing.rs
第一条确认表结构六列与 TEXT 存储;第二条把两张内置表的起始行拉出来,往下翻就能看到 INSERT OR IGNORE 与 UPDATE ... SET 的区别;第三条落到本地覆盖文件上,从这一行往下翻到 :70-80 的结构定义,就是前面说的那四块。
要核单条模型的价,别用肉眼在 schema.rs:1529 到 :2462 之间翻。我们用的办法是 Python 正则 \(\s*"<model_id>"\s*,([^)]*)\),在去掉注释后的扁平化文本上抽取——不去注释会把注释里出现的同名字符串一起抓进来。数条目数同理,先剥 // 注释,再按括号深度只数第一层元组。以上命令按仓库中的文本结构组合,我们没有在本机运行验证过,以你自己 clone 到的仓库内容为准。
最后一句分寸话:本文出现的所有价格数值都是源码里的默认配置与文档里的表格取值,不是任何一家上游的现行报价,更不能拿来推算你的账单。CC Switch 会在本机读写 CLI 配置并保存 API Key,涉及本机敏感数据,改动定价文件之前先想清楚你在改的是覆盖层还是全集。
本文依据 CC Switch 官方仓库(github.com/farion1231/cc-switch)的 README、docs/ 下的用户手册与发布说明、
src/config/ 的预设定义与 src-tauri/src/ 的后端源码整理,核对日 2026-08-10,对应仓库快照 c39c903。
本文内容为仓库源码与文档口径,我们没有安装或运行过这个桌面应用,
因此不涉及界面外观、操作手感与切换速度的任何描述。
文中出现的阈值与默认值均为源码中的默认配置,不构成对实际运行结果的保证。
该项目仍在快速迭代,版本与默认值随时可能变动,请以仓库最新内容为准。
安全相关做法请结合自身环境评估,本文不构成安全方案建议。