Skip to main content

adk_eval/
pricing.rs

1//! Per-model pricing configuration for cost estimation.
2//!
3//! This module provides pricing tables used by the [`CostTracker`](crate::CostTracker)
4//! to compute estimated dollar costs from token usage. Pricing is specified as
5//! cost per 1,000 tokens for both input and output.
6//!
7//! # Example
8//!
9//! ```rust
10//! use adk_eval::pricing::{ModelPricing, default_pricing};
11//!
12//! // Use built-in pricing for common models
13//! let pricing = default_pricing();
14//! assert!(!pricing.is_empty());
15//!
16//! // Create custom pricing for a specific model
17//! let custom = ModelPricing {
18//!     model_name: "my-custom-model".to_string(),
19//!     input_cost_per_1k: 0.001,
20//!     output_cost_per_1k: 0.002,
21//! };
22//! ```
23
24use serde::{Deserialize, Serialize};
25
26/// Per-model pricing configuration.
27///
28/// Defines the cost per 1,000 input and output tokens for a specific model.
29/// Used by [`CostTracker`](crate::CostTracker) to compute estimated dollar
30/// costs from token counts extracted during evaluation.
31#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
32pub struct ModelPricing {
33    /// Model identifier (e.g., "gemini-2.5-flash", "gpt-4o")
34    pub model_name: String,
35    /// Cost per 1,000 input tokens in USD
36    pub input_cost_per_1k: f64,
37    /// Cost per 1,000 output tokens in USD
38    pub output_cost_per_1k: f64,
39}
40
41impl ModelPricing {
42    /// Create a new `ModelPricing` entry.
43    ///
44    /// # Arguments
45    ///
46    /// * `model_name` - Model identifier string
47    /// * `input_cost_per_1k` - Cost per 1K input tokens (USD)
48    /// * `output_cost_per_1k` - Cost per 1K output tokens (USD)
49    ///
50    /// # Example
51    ///
52    /// ```rust
53    /// use adk_eval::pricing::ModelPricing;
54    ///
55    /// let pricing = ModelPricing::new("gpt-4o", 0.0025, 0.01);
56    /// assert_eq!(pricing.model_name, "gpt-4o");
57    /// ```
58    pub fn new(
59        model_name: impl Into<String>,
60        input_cost_per_1k: f64,
61        output_cost_per_1k: f64,
62    ) -> Self {
63        Self { model_name: model_name.into(), input_cost_per_1k, output_cost_per_1k }
64    }
65}
66
67/// Returns default pricing tables for common LLM models.
68///
69/// Includes approximate pricing for Google Gemini, OpenAI GPT, and Anthropic
70/// Claude model families. Prices are approximations and may not reflect the
71/// latest published rates.
72///
73/// # Example
74///
75/// ```rust
76/// use adk_eval::pricing::default_pricing;
77///
78/// let pricing = default_pricing();
79/// let gemini_flash = pricing.iter().find(|p| p.model_name == "gemini-2.5-flash");
80/// assert!(gemini_flash.is_some());
81/// ```
82pub fn default_pricing() -> Vec<ModelPricing> {
83    vec![
84        // Google Gemini models
85        ModelPricing::new("gemini-2.5-flash", 0.00015, 0.0006),
86        ModelPricing::new("gemini-2.5-pro", 0.00125, 0.005),
87        ModelPricing::new("gemini-2.0-flash", 0.0001, 0.0004),
88        ModelPricing::new("gemini-2.0-flash-lite", 0.000075, 0.0003),
89        // OpenAI models
90        ModelPricing::new("gpt-4o", 0.0025, 0.01),
91        ModelPricing::new("gpt-4o-mini", 0.00015, 0.0006),
92        ModelPricing::new("gpt-4-turbo", 0.01, 0.03),
93        ModelPricing::new("gpt-4", 0.03, 0.06),
94        ModelPricing::new("gpt-3.5-turbo", 0.0005, 0.0015),
95        ModelPricing::new("o1", 0.015, 0.06),
96        ModelPricing::new("o1-mini", 0.003, 0.012),
97        ModelPricing::new("o3-mini", 0.0011, 0.0044),
98        // Anthropic Claude models
99        ModelPricing::new("claude-sonnet-4-20250514", 0.003, 0.015),
100        ModelPricing::new("claude-3.5-haiku", 0.0008, 0.004),
101        ModelPricing::new("claude-3-opus", 0.015, 0.075),
102        ModelPricing::new("claude-3-haiku", 0.00025, 0.00125),
103        // DeepSeek models
104        ModelPricing::new("deepseek-chat", 0.00014, 0.00028),
105        ModelPricing::new("deepseek-reasoner", 0.00055, 0.0022),
106    ]
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn test_model_pricing_new() {
115        let pricing = ModelPricing::new("test-model", 0.001, 0.002);
116        assert_eq!(pricing.model_name, "test-model");
117        assert_eq!(pricing.input_cost_per_1k, 0.001);
118        assert_eq!(pricing.output_cost_per_1k, 0.002);
119    }
120
121    #[test]
122    fn test_default_pricing_not_empty() {
123        let pricing = default_pricing();
124        assert!(!pricing.is_empty());
125    }
126
127    #[test]
128    fn test_default_pricing_includes_gemini() {
129        let pricing = default_pricing();
130        let gemini = pricing.iter().find(|p| p.model_name == "gemini-2.5-flash");
131        assert!(gemini.is_some());
132        let gemini = gemini.unwrap();
133        assert!(gemini.input_cost_per_1k > 0.0);
134        assert!(gemini.output_cost_per_1k > 0.0);
135    }
136
137    #[test]
138    fn test_default_pricing_includes_openai() {
139        let pricing = default_pricing();
140        let gpt4o = pricing.iter().find(|p| p.model_name == "gpt-4o");
141        assert!(gpt4o.is_some());
142        let gpt4o = gpt4o.unwrap();
143        assert!(gpt4o.input_cost_per_1k > 0.0);
144        assert!(gpt4o.output_cost_per_1k > 0.0);
145    }
146
147    #[test]
148    fn test_default_pricing_includes_anthropic() {
149        let pricing = default_pricing();
150        let claude = pricing.iter().find(|p| p.model_name == "claude-sonnet-4-20250514");
151        assert!(claude.is_some());
152        let claude = claude.unwrap();
153        assert!(claude.input_cost_per_1k > 0.0);
154        assert!(claude.output_cost_per_1k > 0.0);
155    }
156
157    #[test]
158    fn test_default_pricing_all_positive_costs() {
159        let pricing = default_pricing();
160        for model in &pricing {
161            assert!(
162                model.input_cost_per_1k >= 0.0,
163                "Model {} has negative input cost",
164                model.model_name
165            );
166            assert!(
167                model.output_cost_per_1k >= 0.0,
168                "Model {} has negative output cost",
169                model.model_name
170            );
171        }
172    }
173
174    #[test]
175    fn test_model_pricing_serialization_roundtrip() {
176        let pricing = ModelPricing::new("test-model", 0.001, 0.002);
177        let json = serde_json::to_string(&pricing).unwrap();
178        let deserialized: ModelPricing = serde_json::from_str(&json).unwrap();
179        assert_eq!(pricing, deserialized);
180    }
181
182    #[test]
183    fn test_default_pricing_unique_model_names() {
184        let pricing = default_pricing();
185        let mut names: Vec<&str> = pricing.iter().map(|p| p.model_name.as_str()).collect();
186        let original_len = names.len();
187        names.sort();
188        names.dedup();
189        assert_eq!(names.len(), original_len, "Default pricing contains duplicate model names");
190    }
191}