lean_ctx/proxy/
usage_accounting.rs1use axum::http::HeaderMap;
22use serde_json::Value;
23
24const LITELLM_COST_HEADER: &str = "x-litellm-response-cost";
26
27pub(super) fn upstream_is_openrouter(base_url: &str) -> bool {
30 let rest = base_url
31 .strip_prefix("https://")
32 .or_else(|| base_url.strip_prefix("http://"))
33 .unwrap_or(base_url);
34 let host_port = rest.split(['/', '?']).next().unwrap_or(rest);
35 let host = host_port.split(':').next().unwrap_or(host_port);
36 host.eq_ignore_ascii_case("openrouter.ai")
37 || host.to_ascii_lowercase().ends_with(".openrouter.ai")
38}
39
40pub(super) fn cost_from_headers(headers: &HeaderMap, extra_header: Option<&str>) -> Option<f64> {
45 let mut names = vec![LITELLM_COST_HEADER];
46 if let Some(h) = extra_header {
47 names.push(h);
48 }
49 for name in names {
50 let cost = headers
51 .get(name)
52 .and_then(|v| v.to_str().ok())
53 .and_then(|s| s.trim().parse::<f64>().ok())
54 .filter(|c| c.is_finite() && *c >= 0.0);
55 if cost.is_some() {
56 return cost;
57 }
58 }
59 None
60}
61
62pub(super) fn inject_usage_include(doc: &mut Value) {
68 let Some(obj) = doc.as_object_mut() else {
69 return;
70 };
71 let usage = obj
72 .entry("usage")
73 .or_insert_with(|| Value::Object(serde_json::Map::new()));
74 if let Some(usage_obj) = usage.as_object_mut() {
75 usage_obj.entry("include").or_insert(Value::Bool(true));
76 }
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82
83 #[test]
84 fn openrouter_hosts_are_recognized() {
85 for url in [
86 "https://openrouter.ai/api",
87 "https://openrouter.ai",
88 "http://openrouter.ai:443/api",
89 "https://gateway.openrouter.ai/api",
90 "https://OPENROUTER.AI/api",
91 ] {
92 assert!(
93 upstream_is_openrouter(url),
94 "{url} must count as OpenRouter"
95 );
96 }
97 }
98
99 #[test]
100 fn other_upstreams_are_not_openrouter() {
101 for url in [
102 "https://api.openai.com",
103 "https://my-resource.services.ai.azure.com",
104 "https://api.groq.com/openai",
105 "http://127.0.0.1:11434",
106 "https://evil-openrouter.ai.example.com",
107 "https://notopenrouter.ai",
108 ] {
109 assert!(
110 !upstream_is_openrouter(url),
111 "{url} must NOT count as OpenRouter"
112 );
113 }
114 }
115
116 #[test]
117 fn litellm_cost_header_is_measured() {
118 let mut h = HeaderMap::new();
119 h.insert("x-litellm-response-cost", "0.00042".parse().unwrap());
120 assert_eq!(cost_from_headers(&h, None), Some(0.00042));
121 }
122
123 #[test]
124 fn operator_header_is_recognized_and_junk_ignored() {
125 let mut h = HeaderMap::new();
126 h.insert("x-corp-billed-usd", "1.25".parse().unwrap());
127 assert_eq!(
128 cost_from_headers(&h, Some("x-corp-billed-usd")),
129 Some(1.25),
130 "configured gateway header must be read"
131 );
132 assert_eq!(
133 cost_from_headers(&h, None),
134 None,
135 "unconfigured extra header is not consulted"
136 );
137
138 let mut junk = HeaderMap::new();
139 junk.insert("x-litellm-response-cost", "not-a-number".parse().unwrap());
140 junk.insert("x-corp-billed-usd", "-4".parse().unwrap());
141 assert_eq!(
142 cost_from_headers(&junk, Some("x-corp-billed-usd")),
143 None,
144 "unparseable and negative figures never enter the ledger"
145 );
146 }
147
148 #[test]
149 fn litellm_header_beats_extra_header_order() {
150 let mut h = HeaderMap::new();
151 h.insert("x-litellm-response-cost", "0.10".parse().unwrap());
152 h.insert("x-corp-billed-usd", "9.99".parse().unwrap());
153 assert_eq!(
154 cost_from_headers(&h, Some("x-corp-billed-usd")),
155 Some(0.10),
156 "the standard header wins when both are present"
157 );
158 }
159
160 #[test]
161 fn injects_usage_include_when_absent() {
162 let mut doc = serde_json::json!({"model": "deepseek/deepseek-v4-flash", "messages": []});
163 inject_usage_include(&mut doc);
164 assert_eq!(doc["usage"]["include"], Value::Bool(true));
165 }
166
167 #[test]
168 fn existing_opt_out_is_respected() {
169 let mut doc = serde_json::json!({"model": "m", "usage": {"include": false}});
170 inject_usage_include(&mut doc);
171 assert_eq!(doc["usage"]["include"], Value::Bool(false));
172 }
173
174 #[test]
175 fn non_object_usage_is_left_untouched() {
176 let mut doc = serde_json::json!({"model": "m", "usage": true});
177 inject_usage_include(&mut doc);
178 assert_eq!(doc["usage"], Value::Bool(true));
179 }
180}