Skip to main content

lc_core/token_counter/
tracker.rs

1//! Token 追踪 LLM 包装器与成本估算
2
3use std::sync::Arc;
4
5use crate::language_models::LLMResult;
6use crate::{BaseChatModel, RunnableConfig};
7use lc_schema::Message;
8use tokio::sync::Mutex;
9
10use super::counter::{TokenCounter, TrackerTokenUsage};
11use super::tiktoken::TiktokenCounter;
12
13/// 带 Token 统计的 LLM 包装器
14///
15/// 包装任意 `BaseChatModel`,自动累计 prompt / completion token 用量,
16/// 优先使用 LLM 返回的真实 usage,无则用 tiktoken 估算。
17pub struct TokenTrackingLLM<L: BaseChatModel> {
18    llm: L,
19    counter: Arc<dyn TokenCounter>,
20    usage: Arc<Mutex<TrackerTokenUsage>>,
21}
22
23impl<L: BaseChatModel> TokenTrackingLLM<L> {
24    pub fn new(llm: L, counter: Arc<dyn TokenCounter>) -> Self {
25        Self {
26            llm,
27            counter,
28            usage: Arc::new(Mutex::new(TrackerTokenUsage::new())),
29        }
30    }
31
32    /// 用 Tiktoken(cl100k_base)计数器包装
33    pub fn for_openai(llm: L) -> Result<Self, String> {
34        let counter = TiktokenCounter::new()?;
35        Ok(Self::new(llm, Arc::new(counter)))
36    }
37
38    /// 调用 LLM 并统计 token
39    pub async fn chat(
40        &self,
41        messages: Vec<Message>,
42        config: Option<RunnableConfig>,
43    ) -> Result<LLMResult, L::Error> {
44        let estimated_prompt = self.counter.count_messages(&messages);
45        let result = self.llm.chat(messages, config).await?;
46
47        // 优先用 LLM 返回的真实 usage,否则用估算。
48        // `TrackerTokenUsage` 与 `language_models::TokenUsage` 同为 usize,
49        // 无需精度损失转换(Q6)。
50        let (prompt, completion) = result
51            .token_usage
52            .as_ref()
53            .map(|u| (u.prompt_tokens, u.completion_tokens))
54            .unwrap_or((
55                estimated_prompt as usize,
56                self.counter.count_tokens(&result.content) as usize,
57            ));
58
59        self.usage.lock().await.add(prompt, completion);
60        Ok(result)
61    }
62
63    /// 获取累计用量
64    pub async fn get_usage(&self) -> TrackerTokenUsage {
65        self.usage.lock().await.clone()
66    }
67
68    /// 重置统计
69    pub async fn reset(&self) {
70        self.usage.lock().await.reset();
71    }
72
73    /// 估算成本(美元)
74    pub async fn estimate_cost(&self, pricing: &ModelPricing) -> f64 {
75        let usage = self.get_usage().await;
76        pricing.calculate(usage.prompt_tokens, usage.completion_tokens)
77    }
78}
79
80/// 模型定价(每 1K token 价格,美元)
81pub struct ModelPricing {
82    pub prompt_price_per_1k: f64,
83    pub completion_price_per_1k: f64,
84}
85
86impl ModelPricing {
87    pub fn new(prompt: f64, completion: f64) -> Self {
88        Self {
89            prompt_price_per_1k: prompt,
90            completion_price_per_1k: completion,
91        }
92    }
93
94    /// gpt-4o-mini 定价(美元/1K token)
95    pub fn gpt4o_mini() -> Self {
96        Self::new(0.15, 0.60)
97    }
98
99    /// gpt-4o 定价(美元/1K token)
100    pub fn gpt4o() -> Self {
101        Self::new(2.50, 10.00)
102    }
103
104    /// 计算成本
105    pub fn calculate(&self, prompt: usize, completion: usize) -> f64 {
106        (prompt as f64 / 1000.0) * self.prompt_price_per_1k
107            + (completion as f64 / 1000.0) * self.completion_price_per_1k
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn test_model_pricing_gpt4o_mini() {
117        let p = ModelPricing::gpt4o_mini();
118        // 1000 prompt * 0.15/1k + 1000 completion * 0.60/1k = 0.75
119        let cost = p.calculate(1000, 1000);
120        assert!((cost - 0.75).abs() < 0.001);
121    }
122
123    #[test]
124    fn test_model_pricing_zero() {
125        let p = ModelPricing::gpt4o_mini();
126        assert_eq!(p.calculate(0, 0), 0.0);
127    }
128
129    #[test]
130    fn test_model_pricing_custom() {
131        let p = ModelPricing::new(1.0, 2.0);
132        // 500 * 1.0/1k + 250 * 2.0/1k = 0.5 + 0.5 = 1.0
133        let cost = p.calculate(500, 250);
134        assert!((cost - 1.0).abs() < 0.001);
135    }
136
137    // NOTE: Tests that require OpenAIChat live in the lc-providers crate
138    // because lc-core cannot depend on lc-providers (circular dependency).
139    // The TokenTrackingLLM integration is tested there instead.
140}