Skip to main content

gproxy_protocol/transform/generate/claude_responses/stream/
context.rs

1use super::super::ClaudeResponseContext;
2use super::common::{StreamLimits, declared, invalid, measure};
3use crate::{
4    transform::{Report, TransformError},
5    wire::{claude::generate_content as c, openai::responses as r},
6};
7use std::collections::BTreeMap;
8
9/// Exact source citation occurrence and measured target-native annotation facts.
10/// Coordinates and resource IDs are supplied by the host, never guessed.
11#[derive(Debug, Clone, serde::Serialize)]
12pub struct BoundResponseAnnotation {
13    pub source_block_index: usize,
14    pub source_citation_index: usize,
15    pub source: c::ResponseTextCitation,
16    pub target: r::OutputAnnotation,
17}
18
19pub struct ClaudeToResponsesContext {
20    pub response: ClaudeResponseContext,
21    pub annotations: Vec<BoundResponseAnnotation>,
22}
23
24impl From<ClaudeResponseContext> for ClaudeToResponsesContext {
25    fn from(response: ClaudeResponseContext) -> Self {
26        Self {
27            response,
28            annotations: Vec::new(),
29        }
30    }
31}
32
33pub(super) fn clean_context(
34    mut ctx: ClaudeToResponsesContext,
35    limits: StreamLimits,
36) -> Result<ClaudeToResponsesContext, TransformError> {
37    ctx.response.request = declared(ctx.response.request);
38    ctx.response.effective_tool_choice = declared(ctx.response.effective_tool_choice);
39    ctx.response.effective_prompt_cache_options =
40        declared(ctx.response.effective_prompt_cache_options);
41    if ctx.response.created_at < 0 {
42        return Err(invalid("negative factual creation timestamp"));
43    }
44    for count in [
45        ctx.response.usage.cached_tokens,
46        ctx.response.usage.cache_write_tokens,
47        ctx.response.usage.reasoning_tokens,
48    ]
49    .into_iter()
50    .flatten()
51    {
52        if count < 0 {
53            return Err(invalid("negative factual usage counter"));
54        }
55    }
56    let request = &ctx.response.request;
57    if request
58        .parallel_tool_calls
59        .flatten()
60        .is_some_and(|v| v != ctx.response.effective_parallel_tool_calls)
61        || request
62            .tool_choice
63            .as_ref()
64            .is_some_and(|v| v != &ctx.response.effective_tool_choice)
65    {
66        return Err(invalid("effective controls contradict original request"));
67    }
68    if let Some(requested) = &request.prompt_cache_options {
69        let actual = ctx
70            .response
71            .effective_prompt_cache_options
72            .as_ref()
73            .ok_or_else(|| TransformError::missing_metadata("prompt_cache_options"))?;
74        if requested.mode.is_some_and(|v| v != actual.mode)
75            || requested.ttl.is_some_and(|v| v != actual.ttl)
76        {
77            return Err(invalid(
78                "effective cache settings contradict original request",
79            ));
80        }
81    }
82    ctx.annotations = ctx
83        .annotations
84        .into_iter()
85        .map(|v| BoundResponseAnnotation {
86            source: declared(v.source),
87            target: declared(v.target),
88            ..v
89        })
90        .collect();
91    measure(
92        &(
93            &ctx.response.request,
94            &ctx.response.effective_tool_choice,
95            &ctx.response.effective_prompt_cache_options,
96            &ctx.annotations,
97        ),
98        limits.max_bytes,
99    )?;
100    Ok(ctx)
101}
102
103pub(super) fn clone_response_context(ctx: &ClaudeResponseContext) -> ClaudeResponseContext {
104    ClaudeResponseContext {
105        request: ctx.request.clone(),
106        effective_parallel_tool_calls: ctx.effective_parallel_tool_calls,
107        effective_tool_choice: ctx.effective_tool_choice.clone(),
108        usage: ctx.usage,
109        created_at: ctx.created_at,
110        effective_prompt_cache_options: ctx.effective_prompt_cache_options.clone(),
111    }
112}
113
114pub(super) fn annotations(
115    facts: Vec<BoundResponseAnnotation>,
116    content: &[c::ResponseContentBlock],
117    report: &mut Report,
118) -> Result<BTreeMap<usize, Vec<r::OutputAnnotation>>, TransformError> {
119    let mut supplied = BTreeMap::new();
120    for fact in facts {
121        let key = (fact.source_block_index, fact.source_citation_index);
122        if supplied.insert(key, fact).is_some() {
123            return Err(invalid("duplicate citation binding"));
124        }
125    }
126    let mut output = BTreeMap::new();
127    for (block_index, block) in content.iter().enumerate() {
128        if let c::ResponseContentBlock::Text(text) = block {
129            for (citation_index, citation) in text
130                .citations
131                .as_ref()
132                .and_then(Option::as_ref)
133                .into_iter()
134                .flatten()
135                .enumerate()
136            {
137                if let Some(fact) = supplied.remove(&(block_index, citation_index)) {
138                    if &fact.source != citation {
139                        return Err(invalid(
140                            "citation facts belong to a different source occurrence",
141                        ));
142                    }
143                    let (start, end) = match &fact.target {
144                        r::OutputAnnotation::Url(v) => (v.start_index, v.end_index),
145                        r::OutputAnnotation::ContainerFile(v) => (v.start_index, v.end_index),
146                        r::OutputAnnotation::File(v) => (v.index, v.index),
147                        r::OutputAnnotation::Path(v) => (v.index, v.index),
148                    };
149                    if start < 0
150                        || end < start
151                        || usize::try_from(end)
152                            .ok()
153                            .is_none_or(|n| n > text.text.len())
154                    {
155                        return Err(invalid("target citation coordinates exceed source text"));
156                    }
157                    output
158                        .entry(block_index)
159                        .or_insert_with(Vec::new)
160                        .push(fact.target);
161                } else {
162                    report.omitted(
163                        format!("content[{block_index}].citations[{citation_index}]"),
164                        "actual target output coordinates/resource bindings are unavailable",
165                    );
166                }
167            }
168        }
169    }
170    if !supplied.is_empty() {
171        return Err(invalid("citation binding has no matching source citation"));
172    }
173    Ok(output)
174}