gproxy_protocol/transform/generate/claude_responses/stream/
context.rs1use 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#[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}