use serde_json::{Value, json};
use crate::ir::IrStreamEvent;
use super::u32_field;
pub(crate) fn from_chat(usage: &Value) -> IrStreamEvent {
let cache_read = nested_u32(usage, "prompt_tokens_details", "cached_tokens")
.filter(|&n| n > 0)
.or_else(|| u32_field(usage, "cached_tokens").filter(|&n| n > 0))
.or_else(|| u32_field(usage, "prompt_cache_hit_tokens"))
.unwrap_or(0);
let cache_write = nested_u32(usage, "prompt_tokens_details", "cache_write_tokens").unwrap_or(0);
let reasoning = nested_u32(usage, "completion_tokens_details", "reasoning_tokens").unwrap_or(0);
IrStreamEvent::Usage {
prompt_tokens: u32_field(usage, "prompt_tokens")
.unwrap_or(0)
.saturating_sub(cache_read),
completion_tokens: u32_field(usage, "completion_tokens")
.unwrap_or(0)
.saturating_sub(reasoning),
cache_read_tokens: cache_read,
cache_write_tokens: cache_write,
reasoning_tokens: reasoning,
}
}
pub(super) fn from_anthropic(usage: &Value) -> IrStreamEvent {
let reasoning = nested_u32(usage, "output_tokens_details", "thinking_tokens").unwrap_or(0);
IrStreamEvent::Usage {
prompt_tokens: u32_field(usage, "input_tokens").unwrap_or(0),
completion_tokens: u32_field(usage, "output_tokens")
.unwrap_or(0)
.saturating_sub(reasoning),
cache_read_tokens: u32_field(usage, "cache_read_input_tokens").unwrap_or(0),
cache_write_tokens: u32_field(usage, "cache_creation_input_tokens").unwrap_or(0),
reasoning_tokens: reasoning,
}
}
pub(super) fn from_responses(usage: &Value) -> IrStreamEvent {
let cache_read = nested_u32(usage, "input_tokens_details", "cached_tokens").unwrap_or(0);
let reasoning = nested_u32(usage, "output_tokens_details", "reasoning_tokens").unwrap_or(0);
IrStreamEvent::Usage {
prompt_tokens: u32_field(usage, "input_tokens")
.unwrap_or(0)
.saturating_sub(cache_read),
completion_tokens: u32_field(usage, "output_tokens")
.unwrap_or(0)
.saturating_sub(reasoning),
cache_read_tokens: cache_read,
cache_write_tokens: 0,
reasoning_tokens: reasoning,
}
}
pub(super) fn from_gemini(usage: &Value) -> IrStreamEvent {
let cache_read = u32_field(usage, "cachedContentTokenCount")
.or_else(|| u32_field(usage, "cached_content_token_count"))
.unwrap_or(0);
let reasoning = u32_field(usage, "thoughtsTokenCount")
.or_else(|| u32_field(usage, "thoughts_token_count"))
.unwrap_or(0);
let prompt = u32_field(usage, "promptTokenCount")
.or_else(|| u32_field(usage, "prompt_token_count"))
.unwrap_or(0);
let completion = u32_field(usage, "candidatesTokenCount")
.or_else(|| u32_field(usage, "candidates_token_count"))
.unwrap_or(0);
IrStreamEvent::Usage {
prompt_tokens: prompt.saturating_sub(cache_read),
completion_tokens: completion,
cache_read_tokens: cache_read,
cache_write_tokens: 0,
reasoning_tokens: reasoning,
}
}
pub(super) fn encode_chat(
prompt_tokens: u32,
completion_tokens: u32,
cache_read_tokens: u32,
cache_write_tokens: u32,
reasoning_tokens: u32,
) -> Value {
let prompt_wire = prompt_tokens.saturating_add(cache_read_tokens);
let completion_wire = completion_tokens.saturating_add(reasoning_tokens);
let mut usage = json!({
"prompt_tokens": prompt_wire,
"completion_tokens": completion_wire,
"total_tokens": prompt_wire.saturating_add(completion_wire),
});
if cache_read_tokens > 0 || cache_write_tokens > 0 {
let mut details = json!({});
if cache_read_tokens > 0 {
details["cached_tokens"] = json!(cache_read_tokens);
}
if cache_write_tokens > 0 {
details["cache_write_tokens"] = json!(cache_write_tokens);
}
usage["prompt_tokens_details"] = details;
}
if reasoning_tokens > 0 {
usage["completion_tokens_details"] = json!({ "reasoning_tokens": reasoning_tokens });
}
json!({ "choices": [], "usage": usage })
}
pub(super) fn encode_anthropic(
prompt_tokens: u32,
completion_tokens: u32,
cache_read_tokens: u32,
cache_write_tokens: u32,
reasoning_tokens: u32,
) -> Value {
let mut usage = json!({
"input_tokens": prompt_tokens,
"output_tokens": completion_tokens.saturating_add(reasoning_tokens),
});
if cache_read_tokens > 0 {
usage["cache_read_input_tokens"] = json!(cache_read_tokens);
}
if cache_write_tokens > 0 {
usage["cache_creation_input_tokens"] = json!(cache_write_tokens);
}
if reasoning_tokens > 0 {
usage["output_tokens_details"] = json!({ "thinking_tokens": reasoning_tokens });
}
json!({
"type": "message_delta",
"delta": {},
"usage": usage,
})
}
pub(super) fn encode_responses(
prompt_tokens: u32,
completion_tokens: u32,
cache_read_tokens: u32,
reasoning_tokens: u32,
) -> Value {
let mut usage = json!({
"input_tokens": prompt_tokens.saturating_add(cache_read_tokens),
"output_tokens": completion_tokens.saturating_add(reasoning_tokens),
});
if cache_read_tokens > 0 {
usage["input_tokens_details"] = json!({ "cached_tokens": cache_read_tokens });
}
if reasoning_tokens > 0 {
usage["output_tokens_details"] = json!({ "reasoning_tokens": reasoning_tokens });
}
json!({
"type": "response.completed",
"response": { "status": "completed", "usage": usage },
})
}
pub(super) fn encode_gemini(
prompt_tokens: u32,
completion_tokens: u32,
cache_read_tokens: u32,
reasoning_tokens: u32,
) -> Value {
let mut usage = json!({
"promptTokenCount": prompt_tokens.saturating_add(cache_read_tokens),
"candidatesTokenCount": completion_tokens,
});
if cache_read_tokens > 0 {
usage["cachedContentTokenCount"] = json!(cache_read_tokens);
}
if reasoning_tokens > 0 {
usage["thoughtsTokenCount"] = json!(reasoning_tokens);
}
json!({ "usageMetadata": usage })
}
fn nested_u32(value: &Value, object: &str, key: &str) -> Option<u32> {
u32_field(value.get(object)?, key)
}