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