gproxy_protocol/transform/generate/claude_responses/response/
usage.rs1use crate::{
2 transform::{Report, TransformError},
3 wire::{claude::generate_content as c, openai::responses as r},
4};
5
6#[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}