gproxy_protocol/transform/generate/chat_responses/response/
usage.rs1use crate::{
2 transform::{Report, TransformError},
3 wire::openai::{chat::response as c, responses::response as r},
4};
5
6#[derive(Debug, Clone, Copy, Default)]
8pub struct ChatUsageSupplement {
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 fact(source: Option<i64>, extra: Option<i64>, field: &str) -> Result<i64, TransformError> {
19 count(
20 source
21 .or(extra)
22 .ok_or_else(|| TransformError::missing_metadata(field))?,
23 field,
24 )
25}
26
27pub(crate) fn to_responses(
28 source: c::Usage,
29 extra: ChatUsageSupplement,
30 report: &mut Report,
31) -> Result<r::ResponseUsage, TransformError> {
32 let prompt = source.prompt_tokens_details;
33 let completion = source.completion_tokens_details;
34 let cached = fact(
35 prompt.as_ref().and_then(|d| d.cached_tokens),
36 extra.cached_tokens,
37 "usage.cached_tokens",
38 )?;
39 let written = fact(
40 prompt.as_ref().and_then(|d| d.cache_write_tokens),
41 extra.cache_write_tokens,
42 "usage.cache_write_tokens",
43 )?;
44 let reasoning = fact(
45 completion.as_ref().and_then(|d| d.reasoning_tokens),
46 extra.reasoning_tokens,
47 "usage.reasoning_tokens",
48 )?;
49
50 for (present, field) in [
51 (
52 prompt.as_ref().is_some_and(|d| d.text_tokens.is_some()),
53 "usage.prompt_tokens_details.text_tokens",
54 ),
55 (
56 prompt.as_ref().is_some_and(|d| d.image_tokens.is_some()),
57 "usage.prompt_tokens_details.image_tokens",
58 ),
59 (
60 completion.as_ref().is_some_and(|d| d.text_tokens.is_some()),
61 "usage.completion_tokens_details.text_tokens",
62 ),
63 (
64 prompt.as_ref().is_some_and(|d| d.audio_tokens.is_some()),
65 "usage.prompt_tokens_details.audio_tokens",
66 ),
67 (
68 completion
69 .as_ref()
70 .is_some_and(|d| d.audio_tokens.is_some()),
71 "usage.completion_tokens_details.audio_tokens",
72 ),
73 (
74 completion
75 .as_ref()
76 .is_some_and(|d| d.accepted_prediction_tokens.is_some()),
77 "usage.completion_tokens_details.accepted_prediction_tokens",
78 ),
79 (
80 completion
81 .as_ref()
82 .is_some_and(|d| d.rejected_prediction_tokens.is_some()),
83 "usage.completion_tokens_details.rejected_prediction_tokens",
84 ),
85 ] {
86 if present {
87 report.omitted(field, "Responses usage has no matching detail field");
88 }
89 }
90 Ok(r::ResponseUsage {
91 input_tokens: source.prompt_tokens,
92 output_tokens: source.completion_tokens,
93 total_tokens: source.total_tokens,
94 input_tokens_details: r::ResponseInputTokensDetails {
95 cached_tokens: cached,
96 cache_write_tokens: written,
97 cache_write_tokens_details: None,
98 rest: Default::default(),
99 },
100 output_tokens_details: r::ResponseOutputTokensDetails {
101 reasoning_tokens: reasoning,
102 rest: Default::default(),
103 },
104 rest: Default::default(),
105 })
106}
107
108pub(crate) fn to_chat(source: r::ResponseUsage) -> Result<c::Usage, TransformError> {
109 Ok(c::Usage {
110 prompt_tokens: source.input_tokens,
111 completion_tokens: source.output_tokens,
112 total_tokens: source.total_tokens,
113 prompt_tokens_details: Some(c::PromptTokensDetails {
114 text_tokens: None,
115 image_tokens: None,
116 cache_write_tokens: Some(source.input_tokens_details.cache_write_tokens),
117 cached_tokens: Some(source.input_tokens_details.cached_tokens),
118 audio_tokens: None,
119 rest: Default::default(),
120 }),
121 completion_tokens_details: Some(c::CompletionTokensDetails {
122 text_tokens: None,
123 reasoning_tokens: Some(source.output_tokens_details.reasoning_tokens),
124 audio_tokens: None,
125 accepted_prediction_tokens: None,
126 rejected_prediction_tokens: None,
127 rest: Default::default(),
128 }),
129 rest: Default::default(),
130 })
131}