kcode_intelligence_router/
pricing.rs1use serde::{Deserialize, Serialize};
2
3use crate::{Metering, TokenUsage};
4
5pub const PRICING_VERSION: &str = "kennedy-provider-pricing-2026-07-30";
7
8#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
10#[serde(rename_all = "snake_case")]
11pub enum CostAccuracy {
12 Exact,
14 Estimated,
16 Conservative,
18}
19
20#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
22#[serde(rename_all = "camelCase")]
23pub struct CostEstimate {
24 pub usd_nanos: u64,
25 pub accuracy: CostAccuracy,
26 pub pricing_version: String,
27}
28
29impl CostEstimate {
30 pub(crate) fn versioned(mut self) -> Self {
31 self.pricing_version = PRICING_VERSION.into();
32 self
33 }
34
35 pub(crate) fn with_surcharge(mut self, usd_nanos: u64, accuracy: CostAccuracy) -> Self {
36 self.usd_nanos = self.usd_nanos.saturating_add(usd_nanos);
37 if accuracy == CostAccuracy::Conservative
38 || accuracy == CostAccuracy::Estimated && self.accuracy == CostAccuracy::Exact
39 {
40 self.accuracy = accuracy;
41 }
42 self
43 }
44}
45
46#[derive(Clone, Copy)]
47struct TokenRates {
48 input: u64,
49 cached_input: u64,
50 output: u64,
51}
52
53pub fn estimate_cost(model: &str, metering: &Metering) -> Option<CostEstimate> {
58 match metering {
59 Metering::Tokens(usage) => estimate_token_cost(model, *usage),
60 Metering::DurationSeconds { seconds }
61 if model == kcode_openai_api::GPT_4O_TRANSCRIBE
62 && seconds.is_finite()
63 && *seconds >= 0.0 =>
64 {
65 Some(
67 CostEstimate {
68 usd_nanos: (*seconds * 100_000.0).round() as u64,
69 accuracy: CostAccuracy::Estimated,
70 pricing_version: String::new(),
71 }
72 .versioned(),
73 )
74 }
75 _ => None,
76 }
77}
78
79pub fn estimate_token_cost(model: &str, usage: TokenUsage) -> Option<CostEstimate> {
81 let total_input = usage.input_tokens.saturating_add(usage.cached_input_tokens);
82 let rates = match model {
83 "gpt-5.6" | "gpt-5.6-sol" if total_input <= 272_000 => TokenRates {
85 input: 5_000,
86 cached_input: 500,
87 output: 30_000,
88 },
89 "gpt-5.6" | "gpt-5.6-sol" => TokenRates {
90 input: 10_000,
91 cached_input: 1_000,
92 output: 45_000,
93 },
94 "gpt-5.6-terra" if total_input <= 272_000 => TokenRates {
95 input: 2_000,
96 cached_input: 200,
97 output: 12_000,
98 },
99 "gpt-5.6-terra" => TokenRates {
100 input: 4_000,
101 cached_input: 400,
102 output: 18_000,
103 },
104 "gpt-5.6-luna" if total_input <= 272_000 => TokenRates {
105 input: 200,
106 cached_input: 20,
107 output: 1_200,
108 },
109 "gpt-5.6-luna" => TokenRates {
110 input: 400,
111 cached_input: 40,
112 output: 1_800,
113 },
114 "gpt-4o-transcribe" => TokenRates {
115 input: 2_500,
116 cached_input: 2_500,
117 output: 10_000,
118 },
119 "gemini-2.5-flash" => TokenRates {
120 input: 300,
121 cached_input: 30,
122 output: 2_500,
123 },
124 "gemini-3.1-flash-lite" => TokenRates {
125 input: 250,
126 cached_input: 25,
127 output: 1_500,
128 },
129 "gemini-3.1-pro-preview" if total_input <= 200_000 => TokenRates {
130 input: 2_000,
131 cached_input: 200,
132 output: 12_000,
133 },
134 "gemini-3.1-pro-preview" => TokenRates {
135 input: 4_000,
136 cached_input: 400,
137 output: 18_000,
138 },
139 "gemini-3-pro-image" => TokenRates {
141 input: 2_000,
142 cached_input: 2_000,
143 output: 120_000,
144 },
145 "gpt-image-2" => TokenRates {
146 input: 8_000,
147 cached_input: 2_000,
148 output: 30_000,
149 },
150 _ => return None,
151 };
152 Some(
153 CostEstimate {
154 usd_nanos: usage
155 .input_tokens
156 .saturating_mul(rates.input)
157 .saturating_add(usage.cached_input_tokens.saturating_mul(rates.cached_input))
158 .saturating_add(
159 usage
160 .thinking_tokens
161 .saturating_add(usage.output_tokens)
162 .saturating_mul(rates.output),
163 ),
164 accuracy: if matches!(model, "gpt-image-2" | "gemini-3-pro-image") {
165 CostAccuracy::Conservative
166 } else {
167 CostAccuracy::Exact
168 },
169 pricing_version: String::new(),
170 }
171 .versioned(),
172 )
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 #[test]
180 fn openai_text_uses_distinct_cached_and_output_rates() {
181 let cost = estimate_token_cost(
182 "gpt-5.6-sol",
183 TokenUsage {
184 input_tokens: 1_000_000,
185 cached_input_tokens: 1_000_000,
186 thinking_tokens: 500_000,
187 output_tokens: 500_000,
188 },
189 )
190 .unwrap();
191 assert_eq!(cost.usd_nanos, 56_000_000_000);
193 }
194
195 #[test]
196 fn thinking_is_billed_at_output_rate() {
197 let cost = estimate_token_cost(
198 "gemini-3.1-flash-lite",
199 TokenUsage {
200 thinking_tokens: 10,
201 output_tokens: 5,
202 ..TokenUsage::default()
203 },
204 )
205 .unwrap();
206 assert_eq!(cost.usd_nanos, 22_500);
207 }
208
209 #[test]
210 fn unknown_models_are_not_silently_free() {
211 assert_eq!(
212 estimate_token_cost("future-model", TokenUsage::default()),
213 None
214 );
215 }
216}