use super::super::ClaudeGeminiUsageFacts as Facts;
use super::common::invalid;
use crate::{
transform::{Report, TransformError},
wire::{
claude::{generate_content as c, stream as s},
gemini as g,
},
};
fn count(n: i64) -> Result<i64, TransformError> {
Ok(n)
}
fn add(a: i64, b: i64) -> Result<i64, TransformError> {
count(a)?
.checked_add(count(b)?)
.ok_or_else(|| invalid("usage", "token count overflow"))
}
fn agree(a: Option<i64>, b: Option<i64>, _field: &str) -> Result<Option<i64>, TransformError> {
a.or(b).map(count).transpose()
}
fn written(usage: &c::Usage) -> Result<Option<i64>, TransformError> {
let detail = usage
.cache_creation
.as_ref()
.and_then(Option::as_ref)
.map(|v| add(v.ephemeral_1h_input_tokens, v.ephemeral_5m_input_tokens))
.map(crate::transform::optional)
.transpose()?
.flatten();
agree(
usage.cache_creation_input_tokens.flatten(),
detail,
"usage.cache_creation",
)
}
pub(super) fn prepare_initial(usage: &mut c::Usage, facts: Facts) -> Result<(), TransformError> {
count(usage.input_tokens)?;
count(usage.output_tokens)?;
let write = agree(
written(usage)?,
facts.cache_creation_input_tokens,
"usage.cache_creation",
)?;
let read = agree(
usage.cache_read_input_tokens.flatten(),
facts.cache_read_input_tokens,
"usage.cache_read",
)?;
if write.is_some() {
usage.cache_creation_input_tokens = write.map(Some);
}
if read.is_some() {
usage.cache_read_input_tokens = read.map(Some);
}
Ok(())
}
#[derive(Default)]
pub(super) struct ClaudeUsageProgress {
last_output: i64,
highest_thinking: Option<i64>,
thinking_at_output: Option<i64>,
}
impl ClaudeUsageProgress {
pub fn start(&mut self, usage: &c::Usage) -> Result<(), TransformError> {
self.observe(
usage.output_tokens,
usage
.output_tokens_details
.as_ref()
.and_then(Option::as_ref)
.map(|v| v.thinking_tokens),
usage.output_tokens_details.is_some(),
)
}
pub fn delta(&mut self, usage: &s::MessageDeltaUsage) -> Result<(), TransformError> {
self.observe(
usage.output_tokens,
usage
.output_tokens_details
.as_ref()
.and_then(Option::as_ref)
.map(|v| v.thinking_tokens),
usage.output_tokens_details.is_some(),
)
}
fn observe(
&mut self,
output: i64,
thinking: Option<i64>,
present: bool,
) -> Result<(), TransformError> {
self.last_output = output;
if let Some(n) = thinking {
self.highest_thinking = Some(n);
self.thinking_at_output = Some(output);
} else if present {
self.thinking_at_output = None;
}
Ok(())
}
pub fn finalize(&self, usage: &mut c::Usage, _facts: Facts) -> Result<(), TransformError> {
if self.thinking_at_output != Some(usage.output_tokens) {
usage.output_tokens_details = None;
}
Ok(())
}
}
pub(super) struct GeminiUsageProgress {
usage: g::UsageMetadata,
candidate_total: Option<i64>,
thinking_total: Option<i64>,
}
impl Default for GeminiUsageProgress {
fn default() -> Self {
Self {
usage: g::UsageMetadata::builder().build(),
candidate_total: None,
thinking_total: None,
}
}
}
impl GeminiUsageProgress {
pub fn observe(&mut self, new: &g::UsageMetadata) {
macro_rules! field {
($field:ident) => {
if new.$field.is_some() {
self.usage.$field = new.$field;
}
};
}
field!(prompt_token_count);
field!(cached_content_token_count);
field!(candidates_token_count);
field!(tool_use_prompt_token_count);
field!(thoughts_token_count);
field!(total_token_count);
if new.candidates_token_count.is_some() {
self.candidate_total = self.usage.total_token_count;
}
if new.thoughts_token_count.is_some() {
self.thinking_total = self.usage.total_token_count;
}
}
pub fn effective(
&self,
usage: &mut g::UsageMetadata,
final_thinking: Option<i64>,
) -> Result<bool, TransformError> {
let (Some(total), Some(prompt)) = (usage.total_token_count, usage.prompt_token_count)
else {
return Ok(false);
};
let output = count(total)?
.checked_sub(add(prompt, usage.tool_use_prompt_token_count.unwrap_or(0))?)
.ok_or_else(|| invalid("usage", "total below actual prompt"))?;
let old_c = usage.candidates_token_count;
let old_t = usage.thoughts_token_count;
let mut candidates = old_c.filter(|_| self.candidate_total == Some(total));
let native_thinking = old_t.filter(|_| self.thinking_total == Some(total));
let mut thinking = agree(native_thinking, final_thinking, "usage.thinking_tokens")?;
if candidates.is_none()
&& thinking.is_none()
&& let (Some(c), Some(t)) = (old_c, old_t)
&& add(c, t)? == output
{
candidates = Some(c);
thinking = Some(t);
}
if let (None, Some(thinking)) = (candidates, thinking) {
candidates = Some(output - thinking);
}
usage.candidates_token_count = candidates;
usage.thoughts_token_count = native_thinking;
Ok(usage.candidates_token_count != old_c || usage.thoughts_token_count != old_t)
}
pub fn usage(&self) -> &g::UsageMetadata {
&self.usage
}
}
pub(super) fn gemini_usage(
input: g::UsageMetadata,
mut facts: Facts,
initial: Option<&c::Usage>,
report: &mut Report,
) -> Result<(c::Usage, Facts), TransformError> {
if let Some(initial) = initial {
facts.cache_creation_input_tokens = agree(
facts.cache_creation_input_tokens,
written(initial)?,
"usage.cache_creation",
)?;
facts.cache_read_input_tokens = agree(
facts.cache_read_input_tokens,
initial.cache_read_input_tokens.flatten(),
"usage.cache_read",
)?;
if facts.cache_creation_input_tokens.is_none() {
if let (Some(prompt), Some(tool), Some(cached)) = (
input.prompt_token_count,
input.tool_use_prompt_token_count,
input
.cached_content_token_count
.or(facts.cache_read_input_tokens),
) {
let remaining = add(prompt, tool)?
.checked_sub(count(cached)?)
.and_then(|v| v.checked_sub(initial.input_tokens))
.ok_or_else(|| {
invalid(
"usage.cache_creation",
"known input exceeds prompt/cache totals",
)
})?;
facts.cache_creation_input_tokens = Some(remaining);
}
}
}
let mut usage = super::super::usage::to_claude(input, facts, report)?;
if let Some(initial) = initial {
usage.cache_creation = initial.cache_creation.clone();
usage.fallback_credit = initial.fallback_credit.clone();
usage.inference_geo = initial.inference_geo.clone();
usage.iterations = initial.iterations.clone();
usage.server_tool_use = initial.server_tool_use.clone();
usage.service_tier = initial.service_tier;
usage.speed = initial.speed;
}
Ok((usage, facts))
}