opi_coding_agent/
pricing.rs1use opi_ai::stream::Pricing;
14
15pub fn lookup_pricing(model_spec: &str) -> Option<Pricing> {
19 let (provider, model) = model_spec.split_once(':')?;
20 match provider {
21 "anthropic" => anthropic_pricing(model),
22 "openai" | "openai-responses" => openai_pricing(model),
23 "openrouter" => openrouter_pricing(model),
24 "gemini" => gemini_pricing(model),
25 "mistral" => mistral_pricing(model),
26 _ => None,
27 }
28}
29
30fn anthropic_pricing(model: &str) -> Option<Pricing> {
31 if model.contains("opus") {
32 Some(Pricing {
33 input_cost_per_mtok: 15.0,
34 output_cost_per_mtok: 75.0,
35 cache_read_cost_per_mtok: 1.5,
36 cache_write_cost_per_mtok: 18.75,
37 })
38 } else if model.contains("sonnet") {
39 Some(Pricing {
40 input_cost_per_mtok: 3.0,
41 output_cost_per_mtok: 15.0,
42 cache_read_cost_per_mtok: 0.3,
43 cache_write_cost_per_mtok: 3.75,
44 })
45 } else if model.contains("haiku") {
46 Some(Pricing {
47 input_cost_per_mtok: 0.8,
48 output_cost_per_mtok: 4.0,
49 cache_read_cost_per_mtok: 0.08,
50 cache_write_cost_per_mtok: 1.0,
51 })
52 } else {
53 None
54 }
55}
56
57fn openai_pricing(model: &str) -> Option<Pricing> {
58 if model.starts_with("gpt-4o-mini") {
59 Some(Pricing {
60 input_cost_per_mtok: 0.15,
61 output_cost_per_mtok: 0.60,
62 cache_read_cost_per_mtok: 0.075,
63 cache_write_cost_per_mtok: 0.0,
64 })
65 } else if model.starts_with("gpt-4o") {
66 Some(Pricing {
67 input_cost_per_mtok: 2.50,
68 output_cost_per_mtok: 10.0,
69 cache_read_cost_per_mtok: 1.25,
70 cache_write_cost_per_mtok: 0.0,
71 })
72 } else if model.starts_with("gpt-4-turbo") {
73 Some(Pricing {
74 input_cost_per_mtok: 10.0,
75 output_cost_per_mtok: 30.0,
76 cache_read_cost_per_mtok: 0.0,
77 cache_write_cost_per_mtok: 0.0,
78 })
79 } else if model.starts_with("gpt-3.5") {
80 Some(Pricing {
81 input_cost_per_mtok: 0.50,
82 output_cost_per_mtok: 1.50,
83 cache_read_cost_per_mtok: 0.0,
84 cache_write_cost_per_mtok: 0.0,
85 })
86 } else {
87 None
88 }
89}
90
91fn openrouter_pricing(model: &str) -> Option<Pricing> {
92 if let Some(stripped) = model.strip_prefix("anthropic/") {
94 return anthropic_pricing(stripped);
95 }
96 if let Some(stripped) = model.strip_prefix("openai/") {
97 return openai_pricing(stripped);
98 }
99 if let Some(stripped) = model.strip_prefix("google/") {
100 return gemini_pricing(stripped);
101 }
102 if let Some(stripped) = model.strip_prefix("mistralai/") {
103 return mistral_pricing(stripped);
104 }
105 None
106}
107
108fn gemini_pricing(model: &str) -> Option<Pricing> {
109 if model.contains("flash") {
110 Some(Pricing {
111 input_cost_per_mtok: 0.075,
112 output_cost_per_mtok: 0.30,
113 cache_read_cost_per_mtok: 0.01875,
114 cache_write_cost_per_mtok: 0.0,
115 })
116 } else if model.contains("pro") {
117 Some(Pricing {
118 input_cost_per_mtok: 1.25,
119 output_cost_per_mtok: 5.0,
120 cache_read_cost_per_mtok: 0.3125,
121 cache_write_cost_per_mtok: 0.0,
122 })
123 } else {
124 None
125 }
126}
127
128fn mistral_pricing(model: &str) -> Option<Pricing> {
129 if model.contains("large") {
130 Some(Pricing {
131 input_cost_per_mtok: 2.0,
132 output_cost_per_mtok: 6.0,
133 cache_read_cost_per_mtok: 0.0,
134 cache_write_cost_per_mtok: 0.0,
135 })
136 } else if model.contains("medium") {
137 Some(Pricing {
138 input_cost_per_mtok: 2.7,
139 output_cost_per_mtok: 8.1,
140 cache_read_cost_per_mtok: 0.0,
141 cache_write_cost_per_mtok: 0.0,
142 })
143 } else if model.contains("small") {
144 Some(Pricing {
145 input_cost_per_mtok: 0.20,
146 output_cost_per_mtok: 0.60,
147 cache_read_cost_per_mtok: 0.0,
148 cache_write_cost_per_mtok: 0.0,
149 })
150 } else {
151 None
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 #[test]
160 fn anthropic_sonnet_resolves() {
161 let p = lookup_pricing("anthropic:claude-sonnet-4").unwrap();
162 assert_eq!(p.input_cost_per_mtok, 3.0);
163 assert_eq!(p.output_cost_per_mtok, 15.0);
164 }
165
166 #[test]
167 fn openai_gpt4o_mini_resolves() {
168 let p = lookup_pricing("openai:gpt-4o-mini").unwrap();
169 assert_eq!(p.input_cost_per_mtok, 0.15);
170 }
171
172 #[test]
173 fn gemini_flash_resolves() {
174 let p = lookup_pricing("gemini:gemini-1.5-flash").unwrap();
175 assert_eq!(p.input_cost_per_mtok, 0.075);
176 }
177
178 #[test]
179 fn mistral_large_resolves() {
180 let p = lookup_pricing("mistral:mistral-large-latest").unwrap();
181 assert_eq!(p.input_cost_per_mtok, 2.0);
182 }
183
184 #[test]
185 fn openrouter_forwards_to_underlying() {
186 let p = lookup_pricing("openrouter:anthropic/claude-sonnet-4").unwrap();
187 assert_eq!(p.input_cost_per_mtok, 3.0);
188 }
189
190 #[test]
191 fn unknown_model_returns_none() {
192 assert!(lookup_pricing("anthropic:not-a-real-model").is_none());
193 assert!(lookup_pricing("malformed").is_none());
194 assert!(lookup_pricing("future-provider:foo").is_none());
195 }
196}