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            rest: Default::default(),
93        },
94        output_tokens: output,
95        output_tokens_details: r::ResponseOutputTokensDetails {
96            reasoning_tokens: thinking,
97            rest: Default::default(),
98        },
99        total_tokens: sum(total_input, output)?,
100        rest: Default::default(),
101    })
102}
103
104pub(crate) fn to_claude(input: r::ResponseUsage) -> Result<c::Usage, TransformError> {
105    let total_input = count(input.input_tokens, "usage.input_tokens")?;
106    let output = count(input.output_tokens, "usage.output_tokens")?;
107    let cached = count(
108        input.input_tokens_details.cached_tokens,
109        "usage.cached_tokens",
110    )?;
111    let written = count(
112        input.input_tokens_details.cache_write_tokens,
113        "usage.cache_write_tokens",
114    )?;
115    let thinking = count(
116        input.output_tokens_details.reasoning_tokens,
117        "usage.reasoning_tokens",
118    )?;
119
120    Ok(c::Usage {
121        input_tokens: total_input - cached - written,
122        output_tokens: output,
123        cache_creation_input_tokens: Some(Some(written)),
124        cache_read_input_tokens: Some(Some(cached)),
125        output_tokens_details: Some(Some(c::OutputTokensDetails {
126            thinking_tokens: thinking,
127            rest: Default::default(),
128        })),
129        cache_creation: None,
130        fallback_credit: None,
131        inference_geo: None,
132        iterations: None,
133        server_tool_use: None,
134        service_tier: None,
135        speed: None,
136        rest: Default::default(),
137    })
138}