Skip to main content

gproxy_protocol/adapt/generate/
claude_responses.rs

1use super::{Endpoint, GenerationIdentity, GenerationOutcome, GenerationProgress, transport};
2use crate::transform::generate::claude_responses as p;
3use crate::wire::claude::generate_content as c;
4use crate::wire::openai::responses as r;
5use crate::{
6    Dialect,
7    capability::Upstream,
8    codec::CodecLimits,
9    transform::{Converted, Report, TransformError},
10    wire::DeclaredFields,
11};
12
13/// A prepared Claude client request executed by the selected Responses endpoint.
14#[derive(Debug)]
15pub struct ClaudeViaResponses {
16    original_request: c::GenerateContentRequestBody,
17    target_request: r::GenerateContentRequestBody,
18    selected_model: String,
19    endpoint: Endpoint,
20    identities: GenerationIdentity,
21    report: Report,
22}
23
24impl ClaudeViaResponses {
25    pub fn prepare(
26        input: c::GenerateContentRequestBody,
27        selected_model: impl Into<String>,
28        endpoint: Endpoint,
29        mut identities: GenerationIdentity,
30    ) -> Result<Self, TransformError> {
31        let selected_model = selected_model.into();
32        if selected_model.trim().is_empty() {
33            return Err(TransformError::missing_metadata("selected_model"));
34        }
35        let original_request = input.into_declared();
36        if original_request.stream == Some(true) {
37            return Err(TransformError::unsupported(
38                "stream",
39                "use the incremental invocation entrypoint",
40            ));
41        }
42        let converted = p::claude_to_responses_request(
43            original_request.clone(),
44            &selected_model,
45            &mut identities.request,
46            &identities.request_policy,
47        )?;
48        Ok(Self {
49            original_request,
50            target_request: converted.value.into_declared(),
51            selected_model,
52            endpoint,
53            identities,
54            report: converted.report,
55        })
56    }
57    /// Restore exact tool aliases and names from declared history/scoped state before mapping.
58    pub async fn prepare_with_state<S: crate::capability::StateStore>(
59        input: c::GenerateContentRequestBody,
60        endpoint: Endpoint,
61        identities: GenerationIdentity,
62        state: &super::GenerationStateAccess<'_, S>,
63    ) -> Result<Self, TransformError> {
64        let selected_model = state.target.model.clone();
65        let original = input.into_declared();
66        let (restored, names) = super::history::claude(original.clone(), state).await?;
67        let _ = names;
68        let mut prepared = Self::prepare(restored, selected_model, endpoint, identities)?;
69        prepared.original_request = original;
70        Ok(prepared)
71    }
72    /// Materialize foreign resources and restore scoped history before preparing the selected endpoint.
73    pub async fn prepare_with_capabilities<
74        S: crate::capability::StateStore,
75        R: crate::capability::ResourceAccess,
76    >(
77        input: c::GenerateContentRequestBody,
78        endpoint: Endpoint,
79        identities: GenerationIdentity,
80        state: &super::GenerationStateAccess<'_, S>,
81        resources: &super::GenerationResources<'_, R>,
82    ) -> Result<Self, TransformError> {
83        let original = input.into_declared();
84        let materialized = resources.claude(original.clone()).await?;
85        let mut prepared =
86            Self::prepare_with_state(materialized, endpoint, identities, state).await?;
87        prepared.original_request = original;
88        Ok(prepared)
89    }
90
91    pub fn original_request(&self) -> &c::GenerateContentRequestBody {
92        &self.original_request
93    }
94    pub fn target_request(&self) -> &r::GenerateContentRequestBody {
95        &self.target_request
96    }
97    pub fn identities(&self) -> &GenerationIdentity {
98        &self.identities
99    }
100    pub fn report(&self) -> &Report {
101        &self.report
102    }
103    pub fn selected_model(&self) -> &str {
104        &self.selected_model
105    }
106    pub fn convert_response(
107        &mut self,
108        native: r::GenerateContentResponseBody,
109        _facts: p::ClaudeRequestContext,
110    ) -> Result<Converted<c::GenerateContentResponseBody>, TransformError> {
111        let native = native.into_declared();
112        p::responses_to_claude_response(
113            native,
114            &mut self.identities.response,
115            &self.identities.response_policy,
116        )
117    }
118    /// Performs one POST. The factual supplement is obtained from the actual decoded response.
119    /// Conversion errors keep native bytes/results in caller-owned progress.
120    pub async fn invoke<U: Upstream, S: crate::capability::StateStore>(
121        &mut self,
122        upstream: &U,
123        target: &U::Target,
124        limits: CodecLimits,
125        state: &super::GenerationStateAccess<'_, S>,
126        progress: &mut GenerationProgress<r::GenerateContentResponseBody>,
127        facts: impl FnOnce(
128            &r::GenerateContentResponseBody,
129        ) -> Result<p::ClaudeRequestContext, TransformError>,
130    ) -> Result<GenerationOutcome<c::GenerateContentResponseBody>, TransformError> {
131        transport::bind(
132            (&self.endpoint, &self.selected_model),
133            &self.identities,
134            (&self.original_request, &self.target_request),
135            state,
136            limits,
137            progress,
138            false,
139        )?;
140        if !transport::send(
141            upstream,
142            target,
143            self.endpoint.clone(),
144            self.target_request.clone(),
145            limits,
146            progress,
147        )
148        .await?
149        {
150            return Ok(transport::rejected(progress));
151        }
152        self.recover(limits, state, progress, facts).await
153    }
154    /// Complete response conversion and pending state writes using the retained result; sends no request.
155    pub async fn recover<S: crate::capability::StateStore>(
156        &mut self,
157        limits: CodecLimits,
158        state: &super::GenerationStateAccess<'_, S>,
159        progress: &mut GenerationProgress<r::GenerateContentResponseBody>,
160        facts: impl FnOnce(
161            &r::GenerateContentResponseBody,
162        ) -> Result<p::ClaudeRequestContext, TransformError>,
163    ) -> Result<GenerationOutcome<c::GenerateContentResponseBody>, TransformError> {
164        transport::bind(
165            (&self.endpoint, &self.selected_model),
166            &self.identities,
167            (&self.original_request, &self.target_request),
168            state,
169            limits,
170            progress,
171            true,
172        )?;
173        if progress
174            .raw_response
175            .as_ref()
176            .is_none_or(|r| !r.status.is_success())
177        {
178            return Err(TransformError::invalid_result(
179                "generation.recovery",
180                "successful native response required",
181            ));
182        }
183        let native = transport::recover_native(progress, limits)?;
184        let facts = facts(&native)?;
185        let converted = self.convert_response(native, facts)?;
186        transport::finish(progress, converted, self.report.clone(), limits)
187    }
188}
189
190/// Effective settings and measured facts absent from the upstream wire response.
191#[derive(Debug)]
192pub struct ClaudeReturnFacts {
193    pub parallel_tool_calls: bool,
194    pub tool_choice: r::input::ToolChoice,
195    pub prompt_cache_options: Option<r::response::ResponsePromptCacheOptions>,
196    pub usage: p::ResponsesUsageFacts,
197    pub created_at: i64,
198}
199
200/// A prepared Responses client request executed by the selected Claude endpoint.
201#[derive(Debug)]
202pub struct ResponsesViaClaude {
203    original_request: r::GenerateContentRequestBody,
204    target_request: c::GenerateContentRequestBody,
205    selected_model: String,
206    endpoint: Endpoint,
207    identities: GenerationIdentity,
208    report: Report,
209}
210
211impl ResponsesViaClaude {
212    pub fn prepare(
213        input: r::GenerateContentRequestBody,
214        selected_model: impl Into<String>,
215        endpoint: Endpoint,
216        mut identities: GenerationIdentity,
217        context: p::ClaudeRequestContext,
218    ) -> Result<Self, TransformError> {
219        let selected_model = selected_model.into();
220        if selected_model.trim().is_empty() {
221            return Err(TransformError::missing_metadata("selected_model"));
222        }
223        let original_request = input.into_declared();
224        if original_request.stream.flatten() == Some(true) {
225            return Err(TransformError::unsupported(
226                "stream",
227                "use the incremental invocation entrypoint",
228            ));
229        }
230        let mut converted =
231            p::responses_to_claude_request(original_request.clone(), &selected_model, context)?;
232        super::claude_chat_ids::claude_request(
233            &mut converted.value,
234            &mut identities,
235            Dialect::OpenAi,
236        )?;
237        Ok(Self {
238            original_request,
239            target_request: converted.value.into_declared(),
240            selected_model,
241            endpoint,
242            identities,
243            report: converted.report,
244        })
245    }
246    /// Restore exact tool aliases and names from declared history/scoped state before mapping.
247    pub async fn prepare_with_state<S: crate::capability::StateStore>(
248        input: r::GenerateContentRequestBody,
249        endpoint: Endpoint,
250        identities: GenerationIdentity,
251        state: &super::GenerationStateAccess<'_, S>,
252        context: p::ClaudeRequestContext,
253    ) -> Result<Self, TransformError> {
254        let selected_model = state.target.model.clone();
255        let original = input.into_declared();
256        let mut lowered = original.clone();
257        let mut tool_report = Report::default();
258        crate::transform::generate::client_tools::Bindings::for_target(&original, Dialect::Claude)?
259            .lower(&mut lowered, &mut tool_report)?;
260        let (restored, _) = super::history::responses(lowered, state).await?;
261        if context
262            .target
263            .as_ref()
264            .is_some_and(|target| target != &state.target)
265        {
266            return Err(TransformError::shape(
267                "signature.context",
268                "caller replay binding conflicts with selected state",
269            ));
270        }
271        let mut prepared = Self::prepare(restored, selected_model, endpoint, identities, context)?;
272        prepared.original_request = original;
273        prepared.report.diagnostics.extend(tool_report.diagnostics);
274        Ok(prepared)
275    }
276    /// Materialize foreign resources and restore scoped history before preparing the selected endpoint.
277    pub async fn prepare_with_capabilities<
278        S: crate::capability::StateStore,
279        R: crate::capability::ResourceAccess,
280    >(
281        input: r::GenerateContentRequestBody,
282        endpoint: Endpoint,
283        identities: GenerationIdentity,
284        state: &super::GenerationStateAccess<'_, S>,
285        resources: &super::GenerationResources<'_, R>,
286        context: p::ClaudeRequestContext,
287    ) -> Result<Self, TransformError> {
288        let original = input.into_declared();
289        let materialized = resources.responses(original.clone()).await?;
290        let mut prepared =
291            Self::prepare_with_state(materialized, endpoint, identities, state, context).await?;
292        prepared.original_request = original;
293        Ok(prepared)
294    }
295
296    pub fn original_request(&self) -> &r::GenerateContentRequestBody {
297        &self.original_request
298    }
299    pub fn target_request(&self) -> &c::GenerateContentRequestBody {
300        &self.target_request
301    }
302    pub fn identities(&self) -> &GenerationIdentity {
303        &self.identities
304    }
305    pub fn report(&self) -> &Report {
306        &self.report
307    }
308    pub fn selected_model(&self) -> &str {
309        &self.selected_model
310    }
311    pub fn convert_response(
312        &mut self,
313        native: c::GenerateContentResponseBody,
314        facts: ClaudeReturnFacts,
315    ) -> Result<Converted<r::GenerateContentResponseBody>, TransformError> {
316        let native = native.into_declared();
317        p::claude_to_responses_response(
318            native,
319            p::ClaudeResponseContext {
320                request: self.original_request.clone(),
321                effective_parallel_tool_calls: facts.parallel_tool_calls,
322                effective_tool_choice: facts.tool_choice,
323                usage: facts.usage,
324                created_at: facts.created_at,
325                effective_prompt_cache_options: facts.prompt_cache_options,
326            },
327            &mut self.identities.response,
328            &self.identities.response_policy,
329        )
330    }
331    /// Performs one POST. The factual supplement is obtained from the actual decoded response.
332    /// Conversion errors keep native bytes/results in caller-owned progress.
333    pub async fn invoke<U: Upstream, S: crate::capability::StateStore>(
334        &mut self,
335        upstream: &U,
336        target: &U::Target,
337        limits: CodecLimits,
338        state: &super::GenerationStateAccess<'_, S>,
339        progress: &mut GenerationProgress<c::GenerateContentResponseBody>,
340        facts: impl FnOnce(&c::GenerateContentResponseBody) -> Result<ClaudeReturnFacts, TransformError>,
341    ) -> Result<GenerationOutcome<r::GenerateContentResponseBody>, TransformError> {
342        transport::bind(
343            (&self.endpoint, &self.selected_model),
344            &self.identities,
345            (&self.original_request, &self.target_request),
346            state,
347            limits,
348            progress,
349            false,
350        )?;
351        if !transport::send(
352            upstream,
353            target,
354            self.endpoint.clone(),
355            self.target_request.clone(),
356            limits,
357            progress,
358        )
359        .await?
360        {
361            return Ok(transport::rejected(progress));
362        }
363        self.recover(limits, state, progress, facts).await
364    }
365    /// Complete response conversion and pending state writes using the retained result; sends no request.
366    pub async fn recover<S: crate::capability::StateStore>(
367        &mut self,
368        limits: CodecLimits,
369        state: &super::GenerationStateAccess<'_, S>,
370        progress: &mut GenerationProgress<c::GenerateContentResponseBody>,
371        facts: impl FnOnce(&c::GenerateContentResponseBody) -> Result<ClaudeReturnFacts, TransformError>,
372    ) -> Result<GenerationOutcome<r::GenerateContentResponseBody>, TransformError> {
373        transport::bind(
374            (&self.endpoint, &self.selected_model),
375            &self.identities,
376            (&self.original_request, &self.target_request),
377            state,
378            limits,
379            progress,
380            true,
381        )?;
382        if progress
383            .raw_response
384            .as_ref()
385            .is_none_or(|r| !r.status.is_success())
386        {
387            return Err(TransformError::invalid_result(
388                "generation.recovery",
389                "successful native response required",
390            ));
391        }
392        let native = transport::recover_native(progress, limits)?;
393        let facts = facts(&native)?;
394        let converted = self.convert_response(native, facts)?;
395        transport::finish(progress, converted, self.report.clone(), limits)
396    }
397}
398
399super::prepared::prepared_generation!(ClaudeViaResponses, c::GenerateContentRequestBody => r::GenerateContentRequestBody);
400super::prepared::stream_synthesis!(ClaudeViaResponses, c::GenerateContentRequestBody);
401super::prepared::prepared_generation!(ResponsesViaClaude, r::GenerateContentRequestBody => c::GenerateContentRequestBody);
402super::prepared::stream_synthesis!(ResponsesViaClaude, r::GenerateContentRequestBody, context: p::ClaudeRequestContext);