Skip to main content

gproxy_protocol/transform/generate/claude_responses/
request.rs

1mod 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/// The Claude upstream a Responses request is prepared for, when the caller
22/// knows it; a mismatched model or a missing origin is refused.
23#[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
168/// The thinking block a Responses reasoning item stands for. Only an item whose
169/// `encrypted_content` carries a Claude signature (`claude:`, see `signature`)
170/// has one: Anthropic accepts thinking only with the signature it issued, and
171/// the client's text is the text that signature covers. Any other reasoning
172/// (unsigned, OpenAI ciphertext, a Gemini signature) is left out.
173pub(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}