Skip to main content

gproxy_protocol/transform/generate/claude_responses/response/
usage.rs

1use crate::{
2    transform::{Report, TransformError},
3    wire::{claude::generate_content as c, openai::responses as r},
4};
5
6/// Real usage facts only for counters absent from the Claude response.
7#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
8pub struct ResponsesUsageFacts {
9    pub cache_write_tokens: Option<i64>,
10    pub cached_tokens: Option<i64>,
11    pub reasoning_tokens: Option<i64>,
12}
13
14fn count(value: i64, _field: &str) -> Result<i64, TransformError> {
15    Ok(value)
16}
17
18fn sum(a: i64, b: i64) -> Result<i64, TransformError> {
19    a.checked_add(b)
20        .ok_or_else(|| TransformError::invalid_result("usage", "token count overflow"))
21}
22
23fn actual(source: Option<i64>, supplied: Option<i64>, field: &str) -> Result<i64, TransformError> {
24    count(
25        source
26            .or(supplied)
27            .ok_or_else(|| TransformError::missing_metadata(field))?,
28        field,
29    )
30}
31
32pub(crate) fn to_responses(
33    input: c::Usage,
34    facts: ResponsesUsageFacts,
35    report: &mut Report,
36) -> Result<r::ResponseUsage, TransformError> {
37    let uncached = count(input.input_tokens, "usage.input_tokens")?;
38    let output = count(input.output_tokens, "usage.output_tokens")?;
39    let breakdown = input
40        .cache_creation
41        .flatten()
42        .map(|v| {
43            sum(
44                count(v.ephemeral_1h_input_tokens, "cache_creation.1h")?,
45                count(v.ephemeral_5m_input_tokens, "cache_creation.5m")?,
46            )
47        })
48        .map(crate::transform::optional)
49        .transpose()?
50        .flatten();
51    let written = actual(
52        input.cache_creation_input_tokens.flatten().or(breakdown),
53        facts.cache_write_tokens,
54        "usage.cache_creation_input_tokens",
55    )?;
56
57    let cached = actual(
58        input.cache_read_input_tokens.flatten(),
59        facts.cached_tokens,
60        "usage.cache_read_input_tokens",
61    )?;
62    let thinking = actual(
63        input
64            .output_tokens_details
65            .flatten()
66            .map(|v| v.thinking_tokens),
67        facts.reasoning_tokens,
68        "usage.output_tokens_details.thinking_tokens",
69    )?;
70
71    let total_input = sum(sum(uncached, written)?, cached)?;
72    for (present, name) in [
73        (breakdown.is_some(), "cache_creation"),
74        (input.fallback_credit.is_some(), "fallback_credit"),
75        (input.inference_geo.is_some(), "inference_geo"),
76        (input.iterations.is_some(), "iterations"),
77        (input.server_tool_use.is_some(), "server_tool_use"),
78        (input.speed.is_some(), "speed"),
79    ] {
80        if present {
81            report.omitted(
82                format!("usage.{name}"),
83                "Responses has no equivalent usage detail",
84            );
85        }
86    }
87    Ok(r::ResponseUsage {
88        input_tokens: total_input,
89        input_tokens_details: r::ResponseInputTokensDetails {
90            cache_write_tokens: written,
91            cached_tokens: cached,
92            cache_write_tokens_details: None,
93            rest: Default::default(),
94        },
95        output_tokens: output,
96        output_tokens_details: r::ResponseOutputTokensDetails {
97            reasoning_tokens: thinking,
98            rest: Default::default(),
99        },
100        total_tokens: sum(total_input, output)?,
101        rest: Default::default(),
102    })
103}
104
105pub(crate) fn to_claude(input: r::ResponseUsage) -> Result<c::Usage, TransformError> {
106    let total_input = count(input.input_tokens, "usage.input_tokens")?;
107    let output = count(input.output_tokens, "usage.output_tokens")?;
108    let cached = count(
109        input.input_tokens_details.cached_tokens,
110        "usage.cached_tokens",
111    )?;
112    let written = count(
113        input.input_tokens_details.cache_write_tokens,
114        "usage.cache_write_tokens",
115    )?;
116    let thinking = count(
117        input.output_tokens_details.reasoning_tokens,
118        "usage.reasoning_tokens",
119    )?;
120
121    Ok(c::Usage {
122        input_tokens: total_input - cached - written,
123        output_tokens: output,
124        cache_creation_input_tokens: Some(Some(written)),
125        cache_read_input_tokens: Some(Some(cached)),
126        output_tokens_details: Some(Some(c::OutputTokensDetails {
127            thinking_tokens: thinking,
128            rest: Default::default(),
129        })),
130        cache_creation: None,
131        fallback_credit: None,
132        inference_geo: None,
133        iterations: None,
134        server_tool_use: None,
135        service_tier: None,
136        speed: None,
137        rest: Default::default(),
138    })
139}