fix(litellm): parse nested usage payloads (#8081)

Signed-off-by: James Totah <135163520+jamestotah@users.noreply.github.com>
Signed-off-by: Douwe Osinga <douwe@squareup.com>
Co-authored-by: Douwe Osinga <douwe@squareup.com>
This commit is contained in:
jamestotah
2026-03-25 06:05:40 -07:00
committed by GitHub
parent 03f546bd14
commit a47add0a83
2 changed files with 90 additions and 1 deletions
+36
View File
@@ -372,6 +372,8 @@ pub struct Usage {
pub input_tokens: Option<i32>,
pub output_tokens: Option<i32>,
pub total_tokens: Option<i32>,
pub cache_read_input_tokens: Option<i32>,
pub cache_write_input_tokens: Option<i32>,
}
fn sum_optionals<T>(a: Option<T>, b: Option<T>) -> Option<T>
@@ -395,6 +397,13 @@ impl Add for Usage {
sum_optionals(self.output_tokens, other.output_tokens),
sum_optionals(self.total_tokens, other.total_tokens),
)
.with_cache_tokens(
sum_optionals(self.cache_read_input_tokens, other.cache_read_input_tokens),
sum_optionals(
self.cache_write_input_tokens,
other.cache_write_input_tokens,
),
)
}
}
@@ -425,8 +434,20 @@ impl Usage {
input_tokens,
output_tokens,
total_tokens: calculated_total,
cache_read_input_tokens: None,
cache_write_input_tokens: None,
}
}
pub fn with_cache_tokens(
mut self,
cache_read_input_tokens: Option<i32>,
cache_write_input_tokens: Option<i32>,
) -> Self {
self.cache_read_input_tokens = cache_read_input_tokens;
self.cache_write_input_tokens = cache_write_input_tokens;
self
}
}
pub trait ProviderDef: Send + Sync {
@@ -1086,4 +1107,19 @@ mod tests {
assert_eq!(info.output_token_cost, Some(0.00001));
assert_eq!(info.currency, Some("$".to_string()));
}
#[test]
fn test_usage_addition_includes_cached_tokens() {
let usage_a =
Usage::new(Some(100), Some(20), Some(120)).with_cache_tokens(Some(10), Some(5));
let usage_b = Usage::new(Some(50), Some(8), Some(58)).with_cache_tokens(Some(4), Some(1));
let combined = usage_a + usage_b;
assert_eq!(combined.input_tokens, Some(150));
assert_eq!(combined.output_tokens, Some(28));
assert_eq!(combined.total_tokens, Some(178));
assert_eq!(combined.cache_read_input_tokens, Some(14));
assert_eq!(combined.cache_write_input_tokens, Some(6));
}
}
+54 -1
View File
@@ -490,6 +490,11 @@ pub fn response_to_message(response: &Value) -> anyhow::Result<Message> {
}
pub fn get_usage(usage: &Value) -> Usage {
let usage = usage
.get("usage")
.filter(|nested| nested.is_object())
.unwrap_or(usage);
let input_tokens = usage
.get("prompt_tokens")
.and_then(|v| v.as_i64())
@@ -500,16 +505,27 @@ pub fn get_usage(usage: &Value) -> Usage {
.and_then(|v| v.as_i64())
.map(|v| v as i32);
let cache_read_input_tokens = usage
.get("cache_read_input_tokens")
.and_then(|v| v.as_i64())
.map(|v| v as i32);
let cache_write_input_tokens = usage
.get("cache_creation_input_tokens")
.and_then(|v| v.as_i64())
.map(|v| v as i32);
let total_tokens = usage
.get("total_tokens")
.and_then(|v| v.as_i64())
.map(|v| v as i32)
.or_else(|| match (input_tokens, output_tokens) {
(Some(input), Some(output)) => Some(input + output),
(Some(input), Some(output)) => Some(input.saturating_add(output)),
_ => None,
});
Usage::new(input_tokens, output_tokens, total_tokens)
.with_cache_tokens(cache_read_input_tokens, cache_write_input_tokens)
}
fn extract_usage_with_output_tokens(chunk: &StreamingChunk) -> Option<ProviderUsage> {
@@ -1730,6 +1746,43 @@ mod tests {
assert_eq!(usage.usage.total_tokens, Some(expected_total));
}
#[test]
fn test_get_usage_preserves_provider_totals_with_cache_fields() {
let usage = get_usage(&json!({
"prompt_tokens": 120,
"completion_tokens": 30,
"total_tokens": 150,
"cache_read_input_tokens": 80,
"cache_creation_input_tokens": 20
}));
assert_eq!(usage.input_tokens, Some(120));
assert_eq!(usage.output_tokens, Some(30));
assert_eq!(usage.total_tokens, Some(150));
assert_eq!(usage.cache_read_input_tokens, Some(80));
assert_eq!(usage.cache_write_input_tokens, Some(20));
}
#[test]
fn test_get_usage_reads_nested_usage_object() {
let usage = get_usage(&json!({
"id": "chatcmpl_test",
"usage": {
"prompt_tokens": 84,
"completion_tokens": 21,
"total_tokens": 105,
"cache_read_input_tokens": 60,
"cache_creation_input_tokens": 10
}
}));
assert_eq!(usage.input_tokens, Some(84));
assert_eq!(usage.output_tokens, Some(21));
assert_eq!(usage.total_tokens, Some(105));
assert_eq!(usage.cache_read_input_tokens, Some(60));
assert_eq!(usage.cache_write_input_tokens, Some(10));
}
#[tokio::test]
async fn test_streamed_multi_tool_response_to_messages() -> anyhow::Result<()> {
let response_lines = r#"