Skip to main content

gproxy_protocol/adapt/generate/
chat_claude.rs

1use super::{Endpoint, GenerationIdentity, GenerationOutcome, GenerationProgress, transport};
2use crate::transform::generate::claude_chat as p;
3use crate::wire::claude::generate_content as c;
4use crate::wire::openai::chat as h;
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 Claude endpoint.
14#[derive(Debug)]
15pub struct ChatViaClaude {
16    original_request: h::GenerateContentRequestBody,
17    target_request: c::GenerateContentRequestBody,
18    selected_model: String,
19    endpoint: Endpoint,
20    identities: GenerationIdentity,
21    report: Report,
22}
23
24impl ChatViaClaude {
25    pub fn prepare(
26        input: h::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.flatten() == Some(true) {
37            return Err(TransformError::unsupported(
38                "stream",
39                "use the incremental invocation entrypoint",
40            ));
41        }
42        let mut converted = p::openai_to_claude(&original_request, &selected_model)?;
43        super::claude_chat_ids::claude_request(
44            &mut converted.value,
45            &mut identities,
46            Dialect::OpenAiChat,
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: h::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::chat(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: h::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.chat(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) -> &h::GenerateContentRequestBody {
92        &self.original_request
93    }
94    pub fn target_request(&self) -> &c::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: c::GenerateContentResponseBody,
109        facts: p::ResponseSupplement,
110    ) -> Result<Converted<h::GenerateContentResponseBody>, TransformError> {
111        let native = native.into_declared();
112        let mut converted = p::claude_response_to_openai(&native, &self.selected_model, &facts)?;
113        super::claude_chat_ids::chat_response(&mut converted.value, &mut self.identities)?;
114        Ok(converted)
115    }
116    /// Performs one POST. The factual supplement is obtained from the actual decoded response.
117    /// Conversion errors keep native bytes/results in caller-owned progress.
118    pub async fn invoke<U: Upstream, S: crate::capability::StateStore>(
119        &mut self,
120        upstream: &U,
121        target: &U::Target,
122        limits: CodecLimits,
123        state: &super::GenerationStateAccess<'_, S>,
124        progress: &mut GenerationProgress<c::GenerateContentResponseBody>,
125        facts: impl FnOnce(
126            &c::GenerateContentResponseBody,
127        ) -> Result<p::ResponseSupplement, TransformError>,
128    ) -> Result<GenerationOutcome<h::GenerateContentResponseBody>, TransformError> {
129        transport::bind(
130            (&self.endpoint, &self.selected_model),
131            &self.identities,
132            (&self.original_request, &self.target_request),
133            state,
134            limits,
135            progress,
136            false,
137        )?;
138        if !transport::send(
139            upstream,
140            target,
141            self.endpoint.clone(),
142            self.target_request.clone(),
143            limits,
144            progress,
145        )
146        .await?
147        {
148            return Ok(transport::rejected(progress));
149        }
150        self.recover(limits, state, progress, facts).await
151    }
152    /// Complete response conversion and pending state writes using the retained result; sends no request.
153    pub async fn recover<S: crate::capability::StateStore>(
154        &mut self,
155        limits: CodecLimits,
156        state: &super::GenerationStateAccess<'_, S>,
157        progress: &mut GenerationProgress<c::GenerateContentResponseBody>,
158        facts: impl FnOnce(
159            &c::GenerateContentResponseBody,
160        ) -> Result<p::ResponseSupplement, TransformError>,
161    ) -> Result<GenerationOutcome<h::GenerateContentResponseBody>, TransformError> {
162        transport::bind(
163            (&self.endpoint, &self.selected_model),
164            &self.identities,
165            (&self.original_request, &self.target_request),
166            state,
167            limits,
168            progress,
169            true,
170        )?;
171        if progress
172            .raw_response
173            .as_ref()
174            .is_none_or(|r| !r.status.is_success())
175        {
176            return Err(TransformError::invalid_result(
177                "generation.recovery",
178                "successful native response required",
179            ));
180        }
181        let native = transport::recover_native(progress, limits)?;
182        let facts = facts(&native)?;
183        let converted = self.convert_response(native, facts)?;
184        transport::finish(progress, converted, self.report.clone(), limits)
185    }
186}
187
188/// A prepared Claude client request executed by the selected Chat Completions endpoint.
189#[derive(Debug)]
190pub struct ClaudeViaChat {
191    original_request: c::GenerateContentRequestBody,
192    target_request: h::GenerateContentRequestBody,
193    selected_model: String,
194    endpoint: Endpoint,
195    identities: GenerationIdentity,
196    report: Report,
197}
198
199impl ClaudeViaChat {
200    pub fn prepare(
201        input: c::GenerateContentRequestBody,
202        selected_model: impl Into<String>,
203        endpoint: Endpoint,
204        identities: GenerationIdentity,
205    ) -> Result<Self, TransformError> {
206        Self::prepare_with_replay(
207            input,
208            selected_model,
209            endpoint,
210            identities,
211            &Default::default(),
212        )
213    }
214    fn prepare_with_replay(
215        input: c::GenerateContentRequestBody,
216        selected_model: impl Into<String>,
217        endpoint: Endpoint,
218        mut identities: GenerationIdentity,
219        replay: &super::GenerationToolReplay,
220    ) -> Result<Self, TransformError> {
221        let selected_model = selected_model.into();
222        if selected_model.trim().is_empty() {
223            return Err(TransformError::missing_metadata("selected_model"));
224        }
225        let original_request = input.into_declared();
226        if original_request.stream == Some(true) {
227            return Err(TransformError::unsupported(
228                "stream",
229                "use the incremental invocation entrypoint",
230            ));
231        }
232        let mut converted = p::claude_to_openai(&original_request, &selected_model)?;
233        super::legacy_chat::restore(&mut converted.value, replay, &mut converted.report)?;
234        super::claude_chat_ids::chat_request(
235            &mut converted.value,
236            &mut identities,
237            Dialect::Claude,
238        )?;
239        Ok(Self {
240            original_request,
241            target_request: converted.value.into_declared(),
242            selected_model,
243            endpoint,
244            identities,
245            report: converted.report,
246        })
247    }
248    /// Restore exact tool aliases and names from declared history/scoped state before mapping.
249    pub async fn prepare_with_state<S: crate::capability::StateStore>(
250        input: c::GenerateContentRequestBody,
251        endpoint: Endpoint,
252        identities: GenerationIdentity,
253        state: &super::GenerationStateAccess<'_, S>,
254    ) -> Result<Self, TransformError> {
255        let selected_model = state.target.model.clone();
256        let original = input.into_declared();
257        let (restored, names) = super::history::claude(original.clone(), state).await?;
258        let mut prepared =
259            Self::prepare_with_replay(restored, selected_model, endpoint, identities, &names)?;
260        prepared.original_request = original;
261        Ok(prepared)
262    }
263    /// Materialize foreign resources and restore scoped history before preparing the selected endpoint.
264    pub async fn prepare_with_capabilities<
265        S: crate::capability::StateStore,
266        R: crate::capability::ResourceAccess,
267    >(
268        input: c::GenerateContentRequestBody,
269        endpoint: Endpoint,
270        identities: GenerationIdentity,
271        state: &super::GenerationStateAccess<'_, S>,
272        resources: &super::GenerationResources<'_, R>,
273    ) -> Result<Self, TransformError> {
274        let original = input.into_declared();
275        let materialized = resources.claude(original.clone()).await?;
276        let mut prepared =
277            Self::prepare_with_state(materialized, endpoint, identities, state).await?;
278        prepared.original_request = original;
279        Ok(prepared)
280    }
281
282    pub fn original_request(&self) -> &c::GenerateContentRequestBody {
283        &self.original_request
284    }
285    pub fn target_request(&self) -> &h::GenerateContentRequestBody {
286        &self.target_request
287    }
288    pub fn identities(&self) -> &GenerationIdentity {
289        &self.identities
290    }
291    pub fn report(&self) -> &Report {
292        &self.report
293    }
294    pub fn selected_model(&self) -> &str {
295        &self.selected_model
296    }
297    pub fn convert_response(
298        &mut self,
299        native: h::GenerateContentResponseBody,
300        facts: p::ResponseSupplement,
301    ) -> Result<Converted<c::GenerateContentResponseBody>, TransformError> {
302        let native = native.into_declared();
303        let mut converted = p::openai_response_to_claude(&native, &self.selected_model, &facts)?;
304        super::claude_chat_ids::claude_response(&mut converted.value, &mut self.identities)?;
305        Ok(converted)
306    }
307    /// Performs one POST. The factual supplement is obtained from the actual decoded response.
308    /// Conversion errors keep native bytes/results in caller-owned progress.
309    pub async fn invoke<U: Upstream, S: crate::capability::StateStore>(
310        &mut self,
311        upstream: &U,
312        target: &U::Target,
313        limits: CodecLimits,
314        state: &super::GenerationStateAccess<'_, S>,
315        progress: &mut GenerationProgress<h::GenerateContentResponseBody>,
316        facts: impl FnOnce(
317            &h::GenerateContentResponseBody,
318        ) -> Result<p::ResponseSupplement, TransformError>,
319    ) -> Result<GenerationOutcome<c::GenerateContentResponseBody>, TransformError> {
320        transport::bind(
321            (&self.endpoint, &self.selected_model),
322            &self.identities,
323            (&self.original_request, &self.target_request),
324            state,
325            limits,
326            progress,
327            false,
328        )?;
329        if !transport::send(
330            upstream,
331            target,
332            self.endpoint.clone(),
333            self.target_request.clone(),
334            limits,
335            progress,
336        )
337        .await?
338        {
339            return Ok(transport::rejected(progress));
340        }
341        self.recover(limits, state, progress, facts).await
342    }
343    /// Complete response conversion and pending state writes using the retained result; sends no request.
344    pub async fn recover<S: crate::capability::StateStore>(
345        &mut self,
346        limits: CodecLimits,
347        state: &super::GenerationStateAccess<'_, S>,
348        progress: &mut GenerationProgress<h::GenerateContentResponseBody>,
349        facts: impl FnOnce(
350            &h::GenerateContentResponseBody,
351        ) -> Result<p::ResponseSupplement, TransformError>,
352    ) -> Result<GenerationOutcome<c::GenerateContentResponseBody>, TransformError> {
353        transport::bind(
354            (&self.endpoint, &self.selected_model),
355            &self.identities,
356            (&self.original_request, &self.target_request),
357            state,
358            limits,
359            progress,
360            true,
361        )?;
362        if progress
363            .raw_response
364            .as_ref()
365            .is_none_or(|r| !r.status.is_success())
366        {
367            return Err(TransformError::invalid_result(
368                "generation.recovery",
369                "successful native response required",
370            ));
371        }
372        let native = transport::recover_native(progress, limits)?;
373        let facts = facts(&native)?;
374        let converted = self.convert_response(native, facts)?;
375        transport::finish(progress, converted, self.report.clone(), limits)
376    }
377}
378
379super::prepared::prepared_generation!(ChatViaClaude, h::GenerateContentRequestBody => c::GenerateContentRequestBody);
380super::prepared::stream_synthesis!(ChatViaClaude, h::GenerateContentRequestBody);
381super::prepared::prepared_generation!(ClaudeViaChat, c::GenerateContentRequestBody => h::GenerateContentRequestBody);
382super::prepared::stream_synthesis!(ClaudeViaChat, c::GenerateContentRequestBody);