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        &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
224/// The thinking block a Responses reasoning item stands for. Only an item whose
225/// `encrypted_content` carries a Claude signature (`claude:`, see `signature`)
226/// has one: Anthropic accepts thinking only with the signature it issued, and
227/// the client's text is the text that signature covers. Any other reasoning
228/// (unsigned, OpenAI ciphertext, a Gemini signature) is left out.
229pub(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}