Skip to main content

gproxy_protocol/transform/generate/claude_responses/
request.rs

1use 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/// The Claude upstream a Responses request is prepared for, when the caller
23/// knows it; a mismatched model or a missing origin is refused.
24#[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        &out.model,
216        &mut out.thinking,
217        &mut out.tool_choice,
218        &mut report,
219    );
220    Ok(Converted { value: out, report })
221}
222
223/// The thinking block a Responses reasoning item stands for. Only an item whose
224/// `encrypted_content` carries a Claude signature (`claude:`, see `signature`)
225/// has one: Anthropic accepts thinking only with the signature it issued, and
226/// the client's text is the text that signature covers. Any other reasoning
227/// (unsigned, OpenAI ciphertext, a Gemini signature) is left out.
228pub(super) fn thinking(
229    reasoning: r::ReasoningItem,
230    report: &mut Report,
231) -> Option<cc::ThinkingBlock> {
232    let encrypted = reasoning.encrypted_content.flatten();
233    let Some(Carried::Claude(signature)) = encrypted.as_deref().and_then(Carried::parse) else {
234        report.omitted(
235            "reasoning",
236            "Claude thinking requires a Claude signature; this reasoning carries none",
237        );
238        return None;
239    };
240    if signature.is_empty() {
241        report.omitted(
242            "reasoning",
243            "Claude thinking requires a non-empty signature",
244        );
245        return None;
246    }
247    let text = reasoning.content.map_or_else(
248        || {
249            reasoning
250                .summary
251                .into_iter()
252                .map(|v| v.text)
253                .collect::<Vec<_>>()
254                .join("")
255        },
256        |content| {
257            content
258                .into_iter()
259                .map(|v| v.text)
260                .collect::<Vec<_>>()
261                .join("")
262        },
263    );
264    Some(cc::ThinkingBlock::builder(cc::ThinkingBlockType::Tag, signature.to_owned(), text).build())
265}