gproxy_protocol/transform/generate/claude_responses/
request.rs1use crate::transform::generate::prompt_cache as cache;
2mod controls;
3pub(crate) mod history;
4pub(crate) mod mcp;
5mod media;
6pub(crate) mod messages;
7mod schema;
8pub(crate) mod tools;
9use crate::{
10 transform::{
11 Converted, Report, TransformError,
12 generate::signature::Carried,
13 identity::{IdentityFlow, TargetIdPolicy},
14 },
15 wire::{
16 DeclaredFields,
17 claude::{content as cc, generate_content as c},
18 openai::responses as r,
19 },
20};
21
22#[derive(Default)]
25pub struct ClaudeRequestContext {
26 pub target: Option<crate::transform::identity::IdentityTarget>,
27}
28
29pub fn claude_to_responses_request(
30 input: c::GenerateContentRequestBody,
31 target_model: impl Into<String>,
32 flow: &mut IdentityFlow,
33 policy: &TargetIdPolicy,
34) -> Result<Converted<r::GenerateContentRequestBody>, TransformError> {
35 let model = target_model.into();
36
37 let mut input = input.into_declared();
38 let mut ids = flow.clone();
39 let mut report = Report::default();
40 super::super::claude_controls::project(&mut input, &mut report);
41 let explicit_cache = cache::prepare_claude(&mut input, &mut report);
42 let mut out = r::GenerateContentRequestBody::builder().build();
43 controls::to_responses(&input, &mut out, &mut report)?;
44 out.model = Some(model.clone());
45 out.prompt_cache_options = explicit_cache.then(|| {
46 r::PromptCacheOptions::builder()
47 .mode(r::PromptCachingMode::Explicit)
48 .build()
49 });
50 out.tools = input
51 .tools
52 .map(|v| tools::to_responses(v, &mut report))
53 .map(crate::transform::optional)
54 .transpose()?
55 .flatten();
56 if let Some(choice) = input.tool_choice {
57 let (choice, parallel) = tools::choice_to_responses(choice);
58 out.tool_choice = Some(choice);
59 out.parallel_tool_calls = parallel.map(Some);
60 }
61 if let Some(servers) = input.mcp_servers {
62 for server in servers {
63 if let Some(tool) = mcp::to_responses(server)? {
64 out.tools.get_or_insert_with(Vec::new).push(tool);
65 }
66 }
67 }
68 let system = input.system.take();
69 out.input = Some(r::Input::Items(messages::to_responses(
70 input.messages,
71 &mut ids,
72 policy,
73 &mut report,
74 )?));
75 match system {
76 Some(crate::claude::count_tokens::SystemPrompt::Blocks(blocks))
77 if blocks.iter().any(|block| block.cache_control.is_some()) =>
78 {
79 if let Some(r::Input::Items(items)) = &mut out.input {
80 items.insert(
81 0,
82 r::InputItem::Easy(
83 r::EasyInputMessage::builder(
84 r::MessageContent::Parts(
85 blocks
86 .into_iter()
87 .map(|block| {
88 r::InputContent::Text(cache::responses_text(block))
89 })
90 .collect(),
91 ),
92 r::MessageRole::System,
93 )
94 .build(),
95 ),
96 );
97 }
98 }
99 Some(crate::claude::count_tokens::SystemPrompt::Blocks(blocks)) => {
100 out.instructions = Some(Some(
101 blocks
102 .into_iter()
103 .map(|block| block.text)
104 .collect::<Vec<_>>()
105 .join("\n"),
106 ));
107 }
108 Some(crate::claude::count_tokens::SystemPrompt::Text(text)) => {
109 out.instructions = Some(Some(text))
110 }
111 None => {}
112 }
113 crate::transform::instructions::responses(
114 &mut out.input,
115 &mut out.instructions,
116 &model,
117 &mut report,
118 );
119 *flow = ids;
120 crate::transform::generate::openai_controls::target_responses(&mut out, &mut report);
121 Ok(Converted { value: out, report })
122}
123
124pub fn responses_to_claude_request(
125 input: r::GenerateContentRequestBody,
126 target_model: impl Into<String>,
127 context: ClaudeRequestContext,
128) -> Result<Converted<c::GenerateContentRequestBody>, TransformError> {
129 let model = target_model.into();
130
131 if let Some(target) = &context.target
132 && (target.model != model
133 || target.dialect != crate::Dialect::Claude
134 || target.origin.as_ref().is_none_or(|v| v.trim().is_empty()))
135 {
136 return Err(TransformError::shape(
137 "reasoning.target",
138 "native Claude replay requires matching model and explicit upstream origin",
139 ));
140 }
141 let mut input = input.into_declared();
142 let max = input
143 .max_output_tokens
144 .flatten()
145 .ok_or_else(|| TransformError::missing_metadata("max_output_tokens"))?;
146 let mut out = c::GenerateContentRequestBody::builder(max, Vec::new(), model).build();
147 let mut report = Report::default();
148 cache::responses_breakpoint_defaults(&mut input);
149 crate::transform::generate::openai_controls::project_request(&mut input, &mut report)?;
150 super::super::client_tools::Bindings::for_target(&input, crate::Dialect::Claude)?
151 .lower(&mut input, &mut report)?;
152 controls::to_claude(&input, &mut out, &mut report)?;
153 out.cache_control = input
154 .prompt_cache_options
155 .as_ref()
156 .and_then(cache::responses_options);
157 if let Some(options) = &input.prompt_cache_options {
158 if options.prewarm == Some(true) {
159 return Err(TransformError::unsupported(
160 "prompt_cache_options.prewarm",
161 "Claude has no cache-only generation mode",
162 ));
163 }
164 if options.comparison_response_id.is_some() {
165 report.omitted(
166 "prompt_cache_options.comparison_response_id",
167 "Claude has no matching cache comparison control",
168 );
169 }
170 }
171 if let Some(input_tools) = input.tools {
172 let mut native = Vec::new();
173 let mut servers = Vec::new();
174 for tool in input_tools {
175 match tool {
176 r::tools::Tool::Mcp(tool) => servers.push(mcp::to_claude(tool, &mut report)?),
177 tool => native.push(tool),
178 }
179 }
180 if !native.is_empty() {
181 out.tools = Some(tools::to_claude(native)?);
182 }
183 if !servers.is_empty() {
184 out.mcp_servers = Some(servers);
185 }
186 }
187 let choice = input.tool_choice.or_else(|| {
188 input
189 .parallel_tool_calls
190 .flatten()
191 .map(|_| r::ToolChoice::Mode(r::ToolChoiceMode::Auto))
192 });
193 out.tool_choice = choice
194 .map(|v| {
195 crate::transform::optional(tools::choice_to_claude(
196 v,
197 input.parallel_tool_calls.flatten(),
198 ))
199 })
200 .transpose()?
201 .flatten();
202 let (mut messages, mut system) = history::to_claude(input.input, &mut report)?;
203 crate::transform::instructions::claude(&mut messages, &out.model, &mut report);
204 if let Some(Some(text)) = input.instructions {
205 system.insert(
206 0,
207 cc::TextBlock::builder(cc::TextBlockType::Tag, text).build(),
208 );
209 }
210 out.messages.append(&mut messages);
211 if !system.is_empty() {
212 out.system = Some(crate::claude::count_tokens::SystemPrompt::Blocks(system));
213 }
214 crate::transform::generate::claude_controls::target(
215 &mut out.messages,
216 &out.model,
217 &mut out.thinking,
218 &mut out.tool_choice,
219 &mut report,
220 );
221 Ok(Converted { value: out, report })
222}
223
224pub(super) fn thinking(
230 reasoning: r::ReasoningItem,
231 report: &mut Report,
232) -> Option<cc::ThinkingBlock> {
233 let encrypted = reasoning.encrypted_content.flatten();
234 let Some(Carried::Claude(signature)) = encrypted.as_deref().and_then(Carried::parse) else {
235 report.omitted(
236 "reasoning",
237 "Claude thinking requires a Claude signature; this reasoning carries none",
238 );
239 return None;
240 };
241 if signature.is_empty() {
242 report.omitted(
243 "reasoning",
244 "Claude thinking requires a non-empty signature",
245 );
246 return None;
247 }
248 let text = reasoning.content.map_or_else(
249 || {
250 reasoning
251 .summary
252 .into_iter()
253 .map(|v| v.text)
254 .collect::<Vec<_>>()
255 .join("")
256 },
257 |content| {
258 content
259 .into_iter()
260 .map(|v| v.text)
261 .collect::<Vec<_>>()
262 .join("")
263 },
264 );
265 Some(cc::ThinkingBlock::builder(cc::ThinkingBlockType::Tag, signature.to_owned(), text).build())
266}