Skip to main content

gproxy_protocol/adapt/generate/
chat_responses.rs

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