Skip to main content

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

1use crate::{
2    transform::{Report, TransformError},
3    wire::openai::{chat::response as c, responses::response as r},
4};
5
6/// Actual usage facts absent from an older Chat response. None never means zero.
7#[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}