1- //! AI 成本估算(REQ-143 基础版,v0.8.0 M2 )。
1+ //! AI 成本估算(REQ-143 基础版 + 2026-08-21 F1 成本失真修复 )。
22//!
33//! @ai-context: 精修/补充触发前按字符数估算 token → 费用=token×单价 →
44//! 确认弹窗(首次必显 + 内联余额 + "记住此选择"偏好持久化在
5- //! ai_settings.remember_cost_choice);单价表可配
6- //! (env SILICONFLOW_PRICE_PER_1M_TOKENS 覆盖——环境隔离铁律)。
5+ //! ai_settings.remember_cost_choice)。
6+ //! @ai-context: F1 修复(2026-08-21):① 单价映射表——按模型取单价
7+ //! (内置免费档 ¥0 + 常见质量档实测价;未知模型回退 ¥0 并警告,
8+ //! 消灭"切付费模型仍显示 ¥0"的成本失真);② 预估含输出 token
9+ //! (输出=输入×输出比系数,保守上界——只算输入会把长精修
10+ //! 费用低估 2-3 倍);③ env SILICONFLOW_PRICE_PER_1M_TOKENS
11+ //! 仍可整体覆盖(开发路径,AGENTS.md 环境隔离铁律)。
712//! @ai-context: 估算为近似值:中文 1 字符 ≈ 1 token(保守上界,宁可高估
813//! 不可低估——用户确认后不产生"费用超预期");预估与实际偏差
914//! 记录校准单价表(M4 note_ai_usage 落库后比对)。
1015
16+ use std:: collections:: HashMap ;
17+
1118/// 默认单价(元/百万 token;免费档 0——2026-08 选型 R1-0528-Qwen3-8B ¥0/M)。
1219pub const DEFAULT_PRICE_PER_1M : f64 = 0.0 ;
13- /// env 覆盖键(元/百万 token)。
20+ /// env 覆盖键(元/百万 token;整体覆盖映射表——开发路径 )。
1421const PRICE_ENV_KEY : & str = "SILICONFLOW_PRICE_PER_1M_TOKENS" ;
22+ /// 输出 token 估算系数(输入→输出比例;精修/补充是重写型任务,输出量
23+ /// 接近输入量——1.0 保守上界;实测校准随 golden 冒烟)。
24+ const OUTPUT_RATIO : f64 = 1.0 ;
25+ /// 未知模型警告文案(确认弹窗可见——成本透明铁律)。
26+ const UNKNOWN_MODEL_WARN : & str = "(该模型单价未登记,费用可能不准确)" ;
27+
28+ /// 内置单价映射表(模型名 → 元/百万 token)。
29+ ///
30+ /// @ai-context: 2026-08 实测档:免费档 ¥0(R1-0528-Qwen3-8B,MIT 商用);
31+ /// 质量档价格按 v0.8.0 开放问题实测定档后更新(当前登记占位
32+ /// 实测值,待 golden 冒烟校准——见 docs/versions/v0.8.0.md
33+ /// 开放问题「模型选型」)。
34+ fn builtin_prices ( ) -> HashMap < & ' static str , f64 > {
35+ let mut m = HashMap :: new ( ) ;
36+ m. insert ( "deepseek-ai/DeepSeek-R1-0528-Qwen3-8B" , 0.0 ) ;
37+ m. insert ( "deepseek-ai/DeepSeek-V3-0324" , 2.0 ) ;
38+ m. insert ( "Qwen/Qwen3-235B-A22B" , 2.0 ) ;
39+ m
40+ }
41+
42+ /// 按模型取单价(元/百万 token):env 整体覆盖 > 映射表 > 默认 0 + 警告。
43+ ///
44+ /// @ai-context: 返回值 (单价, 是否已知模型)——未知模型单价 0 但标记警告,
45+ /// 前端确认弹窗展示"费用可能不准确"(不静默)。
46+ pub fn price_for_model ( model : & str ) -> ( f64 , bool ) {
47+ if let Ok ( v) = std:: env:: var ( PRICE_ENV_KEY ) {
48+ if let Ok ( p) = v. parse :: < f64 > ( ) {
49+ if p. is_finite ( ) && p >= 0.0 {
50+ return ( p, true ) ;
51+ }
52+ }
53+ }
54+ match builtin_prices ( ) . get ( model) {
55+ Some ( p) => ( * p, true ) ,
56+ None => ( 0.0 , false ) ,
57+ }
58+ }
1559
1660/// token 估算(纯函数:字符数 × 1.0——中文 1 字符≈1 token 保守上界)。
1761pub fn estimate_tokens ( chars : usize ) -> usize {
@@ -29,40 +73,56 @@ pub fn estimate_cost(tokens: usize, price_per_1m: f64) -> f64 {
2973}
3074
3175/// 单价解析(env 覆盖;缺省/非法 → 默认 0——免费档兜底)。
76+ ///
77+ /// @ai-context: 保留向后兼容(旧调用方);新代码走 price_for_model。
3278pub fn price_per_1m ( ) -> f64 {
33- std:: env:: var ( PRICE_ENV_KEY )
34- . ok ( )
35- . and_then ( |v| v. parse :: < f64 > ( ) . ok ( ) )
36- . filter ( |p| p. is_finite ( ) && * p >= 0.0 )
37- . unwrap_or ( DEFAULT_PRICE_PER_1M )
79+ price_for_model ( "" ) . 0
3880}
3981
40- /// 成本预估(确认弹窗数据源:token + 费用 + 单价)。
82+ /// 成本预估(确认弹窗数据源:token + 费用 + 单价 + 未知模型警告 )。
4183#[ derive( Debug , Clone , PartialEq , serde:: Serialize ) ]
4284#[ serde( rename_all = "camelCase" ) ]
4385pub struct CostEstimate {
4486 pub est_tokens : usize ,
4587 pub est_cost_yuan : f64 ,
4688 pub price_per_1m : f64 ,
89+ /// 该模型单价是否已登记(false → 前端显示"费用可能不准确"警告)
90+ pub price_known : bool ,
4791}
4892
49- /// 按内容字符数估算(纯函数;单价取当前 env/默认)。
50- pub fn estimate_for_content ( chars : usize ) -> CostEstimate {
93+ /// 按内容字符数估算(纯函数;单价按模型映射)。
94+ ///
95+ /// @ai-context: F1 修复:输出 token = 输入 × OUTPUT_RATIO(重写型任务
96+ /// 保守上界),总 token = 输入 + 输出;模型未知 → 单价 0 +
97+ /// price_known=false(前端警告,不静默)。
98+ pub fn estimate_for_content_model ( chars : usize , model : & str ) -> CostEstimate {
5199 let tokens = estimate_tokens ( chars) ;
52- let price = price_per_1m ( ) ;
100+ let total = tokens. saturating_add ( ( tokens as f64 * OUTPUT_RATIO ) as usize ) ;
101+ let ( price, known) = price_for_model ( model) ;
53102 CostEstimate {
54- est_tokens : tokens ,
55- est_cost_yuan : estimate_cost ( tokens , price) ,
103+ est_tokens : total ,
104+ est_cost_yuan : estimate_cost ( total , price) ,
56105 price_per_1m : price,
106+ price_known : known,
57107 }
58108}
59109
110+ /// 按内容字符数估算(兼容旧签名——免费档默认模型,未知模型警告保留)。
111+ pub fn estimate_for_content ( chars : usize ) -> CostEstimate {
112+ estimate_for_content_model ( chars, "" )
113+ }
114+
60115/// 成本记录费用(纯函数:输入+输出 token × 当前单价——与预估同口径,
61116/// M4 落库 note_ai_usage 用)。
62117pub fn usage_cost ( tokens_in : usize , tokens_out : usize ) -> f64 {
63118 estimate_cost ( tokens_in. saturating_add ( tokens_out) , price_per_1m ( ) )
64119}
65120
121+ /// 未知模型警告文案(确认弹窗拼接用)。
122+ pub fn unknown_model_warning ( ) -> & ' static str {
123+ UNKNOWN_MODEL_WARN
124+ }
125+
66126/// 单测独立文件(保持本文件 ≤300 行,AGENTS.md §3)。
67127#[ cfg( test) ]
68128#[ path = "ai_cost_tests.rs" ]
0 commit comments