Skip to main content

gproxy_protocol/transform/
dispatch.rs

1//! Runtime-keyed transform dispatcher.
2//!
3//! The typed transforms in `crate::transform::{claude,gemini,openai}::*` are
4//! statically dispatched via `TryFrom` bounds. This module exposes the same
5//! conversions as a dynamic API keyed on `(OperationFamily, ProtocolKind)`
6//! pairs, which is what an LLM gateway needs at request time when the source
7//! and destination protocols are only known at runtime.
8//!
9//! Moved here from `gproxy-provider::transform_dispatch` in the SDK layer
10//! refactor (spec: docs/superpowers/specs/2026-04-13-sdk-layer-refactor-design.md).
11
12use std::marker::PhantomData;
13use std::sync::Arc;
14
15use crate::kinds::{OperationFamily, ProtocolKind};
16use http::StatusCode;
17use serde::{Serialize, de::DeserializeOwned};
18
19use crate::transform::utils::TransformError;
20
21/// Codec trait for response wrapper enums in `gproxy_protocol`.
22///
23/// Every response enum has the shape
24/// `Success { stats_code, headers, body } | Error { stats_code, headers, body }`
25/// where the outer envelope metadata is internal bookkeeping and the actual
26/// HTTP body JSON is just the `body` field. `serde_json::from_slice::<Wrapper>`
27/// cannot parse a raw upstream response because raw JSON has no `stats_code` /
28/// `headers` top-level fields — it has the body fields directly.
29///
30/// This trait lets [`transform_json`] round-trip through a wrapper enum by
31/// deserializing only the inner body JSON and wrapping it in the `Success`
32/// variant (or falling back to `Error` if the body matches the error schema).
33///
34/// See the `impl_body_envelope!` macro below for implementations.
35trait BodyEnvelope: Sized {
36    /// Parse a raw HTTP response body into this wrapper's `Success` (or
37    /// `Error`) variant with placeholder `stats_code` / `headers`.
38    fn from_body_bytes(body: &[u8]) -> Result<Self, TransformError>;
39
40    /// Serialize just the inner `body` field of this wrapper to JSON bytes
41    /// for the client-facing HTTP response.
42    fn into_body_bytes(self) -> Result<Vec<u8>, TransformError>;
43}
44
45/// Generate a [`BodyEnvelope`] impl for a protocol response wrapper enum.
46///
47/// The macro covers the uniform `Success { stats_code, headers, body } |
48/// Error { stats_code, headers, body }` shape shared by every wrapper in
49/// `crate::{claude, gemini, openai}::*::response`. The two `body`
50/// field types differ per protocol, so the caller passes them in.
51macro_rules! impl_body_envelope {
52    (
53        $wrapper:ty,
54        success_body = $success_body:ty,
55        error_body = $error_body:ty,
56        headers = $headers:ty,
57    ) => {
58        impl BodyEnvelope for $wrapper {
59            fn from_body_bytes(bytes: &[u8]) -> Result<Self, TransformError> {
60                let success_err = match serde_json::from_slice::<$success_body>(bytes) {
61                    Ok(body) => {
62                        return Ok(Self::Success {
63                            stats_code: StatusCode::OK,
64                            headers: <$headers>::default(),
65                            body,
66                        });
67                    }
68                    Err(e) => e,
69                };
70                let error_err = match serde_json::from_slice::<$error_body>(bytes) {
71                    Ok(body) => {
72                        return Ok(Self::Error {
73                            stats_code: StatusCode::BAD_REQUEST,
74                            headers: <$headers>::default(),
75                            body,
76                        });
77                    }
78                    Err(e) => e,
79                };
80                let preview: String = String::from_utf8_lossy(
81                    &bytes[..std::cmp::min(bytes.len(), 600)],
82                )
83                .into_owned();
84                tracing::warn!(
85                    wrapper = stringify!($wrapper),
86                    success_error = %success_err,
87                    error_variant_error = %error_err,
88                    body_len = bytes.len(),
89                    body_preview = %preview,
90                    "response body did not match either variant of wrapper enum"
91                );
92                Err(TransformError::new(format!(
93                    "deserialize: body does not match success or error variant of {} \
94                     (success_err: {}; error_err: {})",
95                    stringify!($wrapper),
96                    success_err,
97                    error_err
98                )))
99            }
100
101            fn into_body_bytes(self) -> Result<Vec<u8>, TransformError> {
102                match self {
103                    Self::Success { body, .. } => serde_json::to_vec(&body)
104                        .map_err(|e| TransformError::new(format!("serialize: {e}"))),
105                    Self::Error { body, .. } => serde_json::to_vec(&body)
106                        .map_err(|e| TransformError::new(format!("serialize: {e}"))),
107                }
108            }
109        }
110    };
111}
112
113impl_body_envelope!(
114    crate::gemini::generate_content::response::GeminiGenerateContentResponse,
115    success_body = crate::gemini::generate_content::response::ResponseBody,
116    error_body = crate::gemini::types::GeminiApiErrorResponse,
117    headers = crate::gemini::types::GeminiResponseHeaders,
118);
119
120impl_body_envelope!(
121    crate::claude::create_message::response::ClaudeCreateMessageResponse,
122    success_body = crate::claude::create_message::response::ResponseBody,
123    error_body = crate::claude::types::BetaErrorResponse,
124    headers = crate::claude::types::ClaudeResponseHeaders,
125);
126
127impl_body_envelope!(
128    crate::openai::create_chat_completions::response::OpenAiChatCompletionsResponse,
129    success_body = crate::openai::create_chat_completions::response::ResponseBody,
130    error_body = crate::openai::types::OpenAiApiErrorResponse,
131    headers = crate::openai::types::OpenAiResponseHeaders,
132);
133
134impl_body_envelope!(
135    crate::openai::create_response::response::OpenAiCreateResponseResponse,
136    success_body = crate::openai::create_response::response::ResponseBody,
137    error_body = crate::openai::types::OpenAiApiErrorResponse,
138    headers = crate::openai::types::OpenAiResponseHeaders,
139);
140
141impl_body_envelope!(
142    crate::gemini::count_tokens::response::GeminiCountTokensResponse,
143    success_body = crate::gemini::count_tokens::response::ResponseBody,
144    error_body = crate::gemini::types::GeminiApiErrorResponse,
145    headers = crate::gemini::types::GeminiResponseHeaders,
146);
147
148impl_body_envelope!(
149    crate::openai::count_tokens::response::OpenAiCountTokensResponse,
150    success_body = crate::openai::count_tokens::response::ResponseBody,
151    error_body = crate::openai::types::OpenAiApiErrorResponse,
152    headers = crate::openai::types::OpenAiResponseHeaders,
153);
154
155impl_body_envelope!(
156    crate::claude::count_tokens::response::ClaudeCountTokensResponse,
157    success_body = crate::claude::count_tokens::response::ResponseBody,
158    error_body = crate::claude::types::BetaErrorResponse,
159    headers = crate::claude::types::ClaudeResponseHeaders,
160);
161
162impl_body_envelope!(
163    crate::gemini::model_get::response::GeminiModelGetResponse,
164    success_body = crate::gemini::model_get::response::ResponseBody,
165    error_body = crate::gemini::types::GeminiApiErrorResponse,
166    headers = crate::gemini::types::GeminiResponseHeaders,
167);
168
169impl_body_envelope!(
170    crate::openai::model_get::response::OpenAiModelGetResponse,
171    success_body = crate::openai::model_get::response::ResponseBody,
172    error_body = crate::openai::types::OpenAiApiErrorResponse,
173    headers = crate::openai::types::OpenAiResponseHeaders,
174);
175
176impl_body_envelope!(
177    crate::claude::model_get::response::ClaudeModelGetResponse,
178    success_body = crate::claude::model_get::response::ResponseBody,
179    error_body = crate::claude::types::BetaErrorResponse,
180    headers = crate::claude::types::ClaudeResponseHeaders,
181);
182
183impl_body_envelope!(
184    crate::gemini::model_list::response::GeminiModelListResponse,
185    success_body = crate::gemini::model_list::response::ResponseBody,
186    error_body = crate::gemini::types::GeminiApiErrorResponse,
187    headers = crate::gemini::types::GeminiResponseHeaders,
188);
189
190impl_body_envelope!(
191    crate::openai::model_list::response::OpenAiModelListResponse,
192    success_body = crate::openai::model_list::response::ResponseBody,
193    error_body = crate::openai::types::OpenAiApiErrorResponse,
194    headers = crate::openai::types::OpenAiResponseHeaders,
195);
196
197impl_body_envelope!(
198    crate::claude::model_list::response::ClaudeModelListResponse,
199    success_body = crate::claude::model_list::response::ResponseBody,
200    error_body = crate::claude::types::BetaErrorResponse,
201    headers = crate::claude::types::ClaudeResponseHeaders,
202);
203
204impl_body_envelope!(
205    crate::gemini::embeddings::response::GeminiEmbedContentResponse,
206    success_body = crate::gemini::embeddings::response::ResponseBody,
207    error_body = crate::gemini::types::GeminiApiErrorResponse,
208    headers = crate::gemini::types::GeminiResponseHeaders,
209);
210
211impl_body_envelope!(
212    crate::openai::embeddings::response::OpenAiEmbeddingsResponse,
213    success_body = crate::openai::embeddings::response::ResponseBody,
214    error_body = crate::openai::types::OpenAiApiErrorResponse,
215    headers = crate::openai::types::OpenAiResponseHeaders,
216);
217
218impl_body_envelope!(
219    crate::openai::create_image::response::OpenAiCreateImageResponse,
220    success_body = crate::openai::create_image::response::ResponseBody,
221    error_body = crate::openai::types::OpenAiApiErrorResponse,
222    headers = crate::openai::types::OpenAiResponseHeaders,
223);
224
225impl_body_envelope!(
226    crate::openai::create_image_edit::response::OpenAiCreateImageEditResponse,
227    success_body = crate::openai::create_image_edit::response::ResponseBody,
228    error_body = crate::openai::types::OpenAiApiErrorResponse,
229    headers = crate::openai::types::OpenAiResponseHeaders,
230);
231
232impl_body_envelope!(
233    crate::openai::compact_response::response::OpenAiCompactResponse,
234    success_body = crate::openai::compact_response::response::ResponseBody,
235    error_body = crate::openai::types::OpenAiApiErrorResponse,
236    headers = crate::openai::types::OpenAiResponseHeaders,
237);
238
239trait RequestDescriptor: Sized {
240    type Body: DeserializeOwned + Serialize;
241
242    fn from_body(body: Self::Body) -> Self;
243    fn into_body(self) -> Self::Body;
244
245    /// Inject a model identifier into the request descriptor's path parameters.
246    ///
247    /// The HTTP body alone does not always carry the model — Gemini requests,
248    /// for example, keep it in the URL (`/v1beta/models/{model}:generateContent`).
249    /// `transform_request` strips the envelope to the body for JSON parsing, so
250    /// without this hook the downstream `TryFrom` impls read an empty
251    /// `path.model` and the transformed upstream body is missing the model.
252    ///
253    /// Default: no-op for descriptors whose path carries no model.
254    fn with_model(self, _model: Option<&str>) -> Self {
255        self
256    }
257}
258
259impl RequestDescriptor
260    for crate::openai::create_chat_completions::request::OpenAiChatCompletionsRequest
261{
262    type Body = crate::openai::create_chat_completions::request::RequestBody;
263
264    fn from_body(body: Self::Body) -> Self {
265        Self {
266            body,
267            ..Self::default()
268        }
269    }
270
271    fn into_body(self) -> Self::Body {
272        self.body
273    }
274}
275
276impl RequestDescriptor for crate::openai::create_response::request::OpenAiCreateResponseRequest {
277    type Body = crate::openai::create_response::request::RequestBody;
278
279    fn from_body(body: Self::Body) -> Self {
280        Self {
281            body,
282            ..Self::default()
283        }
284    }
285
286    fn into_body(self) -> Self::Body {
287        self.body
288    }
289}
290
291impl RequestDescriptor for crate::claude::create_message::request::ClaudeCreateMessageRequest {
292    type Body = crate::claude::create_message::request::RequestBody;
293
294    fn from_body(body: Self::Body) -> Self {
295        Self {
296            body,
297            ..Self::default()
298        }
299    }
300
301    fn into_body(self) -> Self::Body {
302        self.body
303    }
304}
305
306impl RequestDescriptor for crate::gemini::generate_content::request::GeminiGenerateContentRequest {
307    type Body = crate::gemini::generate_content::request::RequestBody;
308
309    fn from_body(body: Self::Body) -> Self {
310        Self {
311            body,
312            ..Self::default()
313        }
314    }
315
316    fn into_body(self) -> Self::Body {
317        self.body
318    }
319
320    fn with_model(mut self, model: Option<&str>) -> Self {
321        if let Some(m) = model {
322            self.path.model = m.to_string();
323        }
324        self
325    }
326}
327
328impl RequestDescriptor
329    for crate::gemini::stream_generate_content::request::GeminiStreamGenerateContentRequest
330{
331    type Body = crate::gemini::stream_generate_content::request::RequestBody;
332
333    fn from_body(body: Self::Body) -> Self {
334        Self {
335            body,
336            ..Self::default()
337        }
338    }
339
340    fn into_body(self) -> Self::Body {
341        self.body
342    }
343
344    fn with_model(mut self, model: Option<&str>) -> Self {
345        if let Some(m) = model {
346            self.path.model = m.to_string();
347        }
348        self
349    }
350}
351
352/// Generate a [`RequestDescriptor`] impl that stores `body` and uses
353/// `Default::default()` for the rest of the envelope fields
354/// (`method`, `path`, `query`, `headers`).
355///
356/// The request wrapper structs in `crate::*::request` all carry a
357/// `{method, path, query, headers, body}` envelope — the rest of the gproxy
358/// pipeline only cares about `body`, and [`transform_request_descriptor`]
359/// reads the body JSON directly and reconstructs the wrapper around it.
360///
361/// Without this, attempting to `serde_json::from_slice::<Wrapper>` on a raw
362/// HTTP request body fails with `missing field 'method'` because real HTTP
363/// bodies only have the body fields at the top level, not the envelope.
364macro_rules! impl_request_descriptor_default_envelope {
365    ($wrapper:ty, body = $body:ty) => {
366        impl RequestDescriptor for $wrapper {
367            type Body = $body;
368
369            fn from_body(body: Self::Body) -> Self {
370                Self {
371                    body,
372                    ..Self::default()
373                }
374            }
375
376            fn into_body(self) -> Self::Body {
377                self.body
378            }
379        }
380    };
381    ($wrapper:ty, body = $body:ty, path_model = $field:ident) => {
382        impl RequestDescriptor for $wrapper {
383            type Body = $body;
384
385            fn from_body(body: Self::Body) -> Self {
386                Self {
387                    body,
388                    ..Self::default()
389                }
390            }
391
392            fn into_body(self) -> Self::Body {
393                self.body
394            }
395
396            fn with_model(mut self, model: Option<&str>) -> Self {
397                if let Some(m) = model {
398                    self.path.$field = m.to_string();
399                }
400                self
401            }
402        }
403    };
404}
405
406impl_request_descriptor_default_envelope!(
407    crate::claude::count_tokens::request::ClaudeCountTokensRequest,
408    body = crate::claude::count_tokens::request::RequestBody
409);
410impl_request_descriptor_default_envelope!(
411    crate::openai::count_tokens::request::OpenAiCountTokensRequest,
412    body = crate::openai::count_tokens::request::RequestBody
413);
414impl_request_descriptor_default_envelope!(
415    crate::gemini::count_tokens::request::GeminiCountTokensRequest,
416    body = crate::gemini::count_tokens::request::RequestBody,
417    path_model = model
418);
419impl_request_descriptor_default_envelope!(
420    crate::claude::model_get::request::ClaudeModelGetRequest,
421    body = crate::claude::model_get::request::RequestBody,
422    path_model = model_id
423);
424impl_request_descriptor_default_envelope!(
425    crate::openai::model_get::request::OpenAiModelGetRequest,
426    body = crate::openai::model_get::request::RequestBody,
427    path_model = model
428);
429impl_request_descriptor_default_envelope!(
430    crate::gemini::model_get::request::GeminiModelGetRequest,
431    body = crate::gemini::model_get::request::RequestBody,
432    path_model = name
433);
434impl_request_descriptor_default_envelope!(
435    crate::claude::model_list::request::ClaudeModelListRequest,
436    body = crate::claude::model_list::request::RequestBody
437);
438impl_request_descriptor_default_envelope!(
439    crate::openai::model_list::request::OpenAiModelListRequest,
440    body = crate::openai::model_list::request::RequestBody
441);
442impl_request_descriptor_default_envelope!(
443    crate::gemini::model_list::request::GeminiModelListRequest,
444    body = crate::gemini::model_list::request::RequestBody
445);
446impl_request_descriptor_default_envelope!(
447    crate::openai::embeddings::request::OpenAiEmbeddingsRequest,
448    body = crate::openai::embeddings::request::RequestBody
449);
450impl_request_descriptor_default_envelope!(
451    crate::gemini::embeddings::request::GeminiEmbedContentRequest,
452    body = crate::gemini::embeddings::request::RequestBody,
453    path_model = model
454);
455impl_request_descriptor_default_envelope!(
456    crate::openai::create_image::request::OpenAiCreateImageRequest,
457    body = crate::openai::create_image::request::RequestBody
458);
459impl_request_descriptor_default_envelope!(
460    crate::openai::create_image_edit::request::OpenAiCreateImageEditRequest,
461    body = crate::openai::create_image_edit::request::RequestBody
462);
463impl_request_descriptor_default_envelope!(
464    crate::openai::compact_response::request::OpenAiCompactRequest,
465    body = crate::openai::compact_response::request::RequestBody
466);
467
468/// Translate URL query keys across protocols for operations whose query
469/// semantics differ. Currently only ModelList (`pageSize`↔`limit`,
470/// `pageToken`↔`after_id`). Unknown keys are dropped since the upstream
471/// protocol won't understand them.
472fn translate_request_query(
473    src_operation: OperationFamily,
474    src_protocol: ProtocolKind,
475    dst_operation: OperationFamily,
476    dst_protocol: ProtocolKind,
477    query: Option<&str>,
478) -> Option<String> {
479    let raw = query?;
480    if raw.is_empty() {
481        return None;
482    }
483
484    // Only ModelList has cross-protocol query semantics worth translating.
485    // Everything else passes through verbatim — upstream will accept or
486    // ignore as per its contract.
487    if !(src_operation == OperationFamily::ModelList
488        && dst_operation == OperationFamily::ModelList
489        && src_protocol != dst_protocol)
490    {
491        return Some(raw.to_owned());
492    }
493
494    let mut page_size: Option<String> = None;
495    let mut page_token: Option<String> = None;
496    for (key, value) in url::form_urlencoded::parse(raw.as_bytes()) {
497        match (src_protocol, key.as_ref()) {
498            (ProtocolKind::Gemini | ProtocolKind::GeminiNDJson, "pageSize") => {
499                page_size = Some(value.into_owned())
500            }
501            (ProtocolKind::Gemini | ProtocolKind::GeminiNDJson, "pageToken") => {
502                page_token = Some(value.into_owned())
503            }
504            (ProtocolKind::Claude, "limit") => page_size = Some(value.into_owned()),
505            (ProtocolKind::Claude, "after_id") => page_token = Some(value.into_owned()),
506            (
507                ProtocolKind::OpenAi
508                | ProtocolKind::OpenAiChatCompletion
509                | ProtocolKind::OpenAiResponse,
510                "limit",
511            ) => page_size = Some(value.into_owned()),
512            (
513                ProtocolKind::OpenAi
514                | ProtocolKind::OpenAiChatCompletion
515                | ProtocolKind::OpenAiResponse,
516                "after",
517            ) => page_token = Some(value.into_owned()),
518            _ => {}
519        }
520    }
521
522    let mut out = url::form_urlencoded::Serializer::new(String::new());
523    match dst_protocol {
524        ProtocolKind::Gemini | ProtocolKind::GeminiNDJson => {
525            if let Some(v) = page_size {
526                out.append_pair("pageSize", &v);
527            }
528            if let Some(v) = page_token {
529                out.append_pair("pageToken", &v);
530            }
531        }
532        ProtocolKind::Claude => {
533            if let Some(v) = page_size {
534                out.append_pair("limit", &v);
535            }
536            if let Some(v) = page_token {
537                out.append_pair("after_id", &v);
538            }
539        }
540        ProtocolKind::OpenAi
541        | ProtocolKind::OpenAiChatCompletion
542        | ProtocolKind::OpenAiResponse => {
543            if let Some(v) = page_size {
544                out.append_pair("limit", &v);
545            }
546            if let Some(v) = page_token {
547                out.append_pair("after", &v);
548            }
549        }
550    }
551    let s = out.finish();
552    if s.is_empty() { None } else { Some(s) }
553}
554
555/// Transform a request body from one (operation, protocol) to another.
556///
557/// This dispatches to the appropriate `TryFrom` implementation in `crate::transform`.
558pub fn transform_request(
559    src_operation: OperationFamily,
560    src_protocol: ProtocolKind,
561    dst_operation: OperationFamily,
562    dst_protocol: ProtocolKind,
563    model: Option<&str>,
564    query: Option<&str>,
565    body: Vec<u8>,
566) -> Result<(Option<String>, Vec<u8>), TransformError> {
567    if src_operation == dst_operation && src_protocol == dst_protocol {
568        return Ok((query.map(str::to_owned), body));
569    }
570
571    // Translate URL query for operations whose query semantics differ
572    // across protocols. Body transform continues below as normal.
573    let translated_query = translate_request_query(
574        src_operation,
575        src_protocol,
576        dst_operation,
577        dst_protocol,
578        query,
579    );
580
581    tracing::debug!(
582        src_operation = %src_operation,
583        src_protocol = %src_protocol,
584        dst_operation = %dst_operation,
585        dst_protocol = %dst_protocol,
586        "transforming request"
587    );
588    let key = (src_operation, src_protocol, dst_operation, dst_protocol);
589
590    match key {
591        // =====================================================================
592        // generate_content
593        // =====================================================================
594
595        // === Claude source → OpenAI targets ===
596        (
597            OperationFamily::GenerateContent,
598            ProtocolKind::Claude,
599            OperationFamily::GenerateContent,
600            ProtocolKind::OpenAiChatCompletion,
601        ) => transform_request_descriptor::<
602            crate::claude::create_message::request::ClaudeCreateMessageRequest,
603            crate::openai::create_chat_completions::request::OpenAiChatCompletionsRequest,
604        >(&body, model),
605        (
606            OperationFamily::GenerateContent,
607            ProtocolKind::Claude,
608            OperationFamily::GenerateContent,
609            ProtocolKind::OpenAiResponse,
610        ) => transform_request_descriptor::<
611            crate::claude::create_message::request::ClaudeCreateMessageRequest,
612            crate::openai::create_response::request::OpenAiCreateResponseRequest,
613        >(&body, model),
614
615        // === Claude source → Gemini targets ===
616        (
617            OperationFamily::GenerateContent,
618            ProtocolKind::Claude,
619            OperationFamily::GenerateContent,
620            ProtocolKind::Gemini,
621        ) => transform_request_descriptor::<
622            crate::claude::create_message::request::ClaudeCreateMessageRequest,
623            crate::gemini::generate_content::request::GeminiGenerateContentRequest,
624        >(&body, model),
625
626        // === OpenAI ChatCompletions source → Claude ===
627        (
628            OperationFamily::GenerateContent,
629            ProtocolKind::OpenAiChatCompletion,
630            OperationFamily::GenerateContent,
631            ProtocolKind::Claude,
632        ) => transform_request_descriptor::<
633            crate::openai::create_chat_completions::request::OpenAiChatCompletionsRequest,
634            crate::claude::create_message::request::ClaudeCreateMessageRequest,
635        >(&body, model),
636
637        // === OpenAI ChatCompletions source → Gemini ===
638        (
639            OperationFamily::GenerateContent,
640            ProtocolKind::OpenAiChatCompletion,
641            OperationFamily::GenerateContent,
642            ProtocolKind::Gemini,
643        ) => transform_request_descriptor::<
644            crate::openai::create_chat_completions::request::OpenAiChatCompletionsRequest,
645            crate::gemini::generate_content::request::GeminiGenerateContentRequest,
646        >(&body, model),
647
648        // === OpenAI ChatCompletions source → OpenAI Response ===
649        (
650            OperationFamily::GenerateContent,
651            ProtocolKind::OpenAiChatCompletion,
652            OperationFamily::GenerateContent,
653            ProtocolKind::OpenAiResponse,
654        ) => transform_request_descriptor::<
655            crate::openai::create_chat_completions::request::OpenAiChatCompletionsRequest,
656            crate::openai::create_response::request::OpenAiCreateResponseRequest,
657        >(&body, model),
658
659        // === OpenAI Response source → OpenAI ChatCompletions ===
660        //
661        // Used by channels like deepseek that only expose the chat
662        // completions surface but advertise the OpenAI Response protocol
663        // to clients, so the dispatch table transforms on the way in.
664        (
665            OperationFamily::GenerateContent,
666            ProtocolKind::OpenAiResponse,
667            OperationFamily::GenerateContent,
668            ProtocolKind::OpenAiChatCompletion,
669        ) => transform_request_descriptor::<
670            crate::openai::create_response::request::OpenAiCreateResponseRequest,
671            crate::openai::create_chat_completions::request::OpenAiChatCompletionsRequest,
672        >(&body, model),
673
674        // === OpenAI Response source → Claude ===
675        (
676            OperationFamily::GenerateContent,
677            ProtocolKind::OpenAiResponse,
678            OperationFamily::GenerateContent,
679            ProtocolKind::Claude,
680        ) => transform_request_descriptor::<
681            crate::openai::create_response::request::OpenAiCreateResponseRequest,
682            crate::claude::create_message::request::ClaudeCreateMessageRequest,
683        >(&body, model),
684
685        // === OpenAI Response source → Gemini ===
686        (
687            OperationFamily::GenerateContent,
688            ProtocolKind::OpenAiResponse,
689            OperationFamily::GenerateContent,
690            ProtocolKind::Gemini,
691        ) => transform_request_descriptor::<
692            crate::openai::create_response::request::OpenAiCreateResponseRequest,
693            crate::gemini::generate_content::request::GeminiGenerateContentRequest,
694        >(&body, model),
695
696        // === Gemini source → Claude ===
697        (
698            OperationFamily::GenerateContent,
699            ProtocolKind::Gemini,
700            OperationFamily::GenerateContent,
701            ProtocolKind::Claude,
702        ) => transform_request_descriptor::<
703            crate::gemini::generate_content::request::GeminiGenerateContentRequest,
704            crate::claude::create_message::request::ClaudeCreateMessageRequest,
705        >(&body, model),
706
707        // === Gemini source → OpenAI ChatCompletions ===
708        (
709            OperationFamily::GenerateContent,
710            ProtocolKind::Gemini,
711            OperationFamily::GenerateContent,
712            ProtocolKind::OpenAiChatCompletion,
713        ) => transform_request_descriptor::<
714            crate::gemini::generate_content::request::GeminiGenerateContentRequest,
715            crate::openai::create_chat_completions::request::OpenAiChatCompletionsRequest,
716        >(&body, model),
717
718        // === Gemini source → OpenAI Response ===
719        (
720            OperationFamily::GenerateContent,
721            ProtocolKind::Gemini,
722            OperationFamily::GenerateContent,
723            ProtocolKind::OpenAiResponse,
724        ) => transform_request_descriptor::<
725            crate::gemini::generate_content::request::GeminiGenerateContentRequest,
726            crate::openai::create_response::request::OpenAiCreateResponseRequest,
727        >(&body, model),
728
729        // =====================================================================
730        // stream_generate_content (request transforms only)
731        // =====================================================================
732
733        // --- Claude source ---
734        (
735            OperationFamily::StreamGenerateContent,
736            ProtocolKind::Claude,
737            OperationFamily::StreamGenerateContent,
738            ProtocolKind::Gemini,
739        ) => transform_request_descriptor_ref::<
740            crate::claude::create_message::request::ClaudeCreateMessageRequest,
741            crate::gemini::stream_generate_content::request::GeminiStreamGenerateContentRequest,
742        >(&body, model),
743        (
744            OperationFamily::StreamGenerateContent,
745            ProtocolKind::Claude,
746            OperationFamily::StreamGenerateContent,
747            ProtocolKind::GeminiNDJson,
748        ) => transform_request_descriptor_ref::<
749            crate::claude::create_message::request::ClaudeCreateMessageRequest,
750            crate::gemini::stream_generate_content::request::GeminiStreamGenerateContentRequest,
751        >(&body, model),
752        (
753            OperationFamily::StreamGenerateContent,
754            ProtocolKind::Claude,
755            OperationFamily::StreamGenerateContent,
756            ProtocolKind::OpenAiChatCompletion,
757        ) => transform_request_descriptor_ref::<
758            crate::claude::create_message::request::ClaudeCreateMessageRequest,
759            crate::openai::create_chat_completions::request::OpenAiChatCompletionsRequest,
760        >(&body, model),
761        (
762            OperationFamily::StreamGenerateContent,
763            ProtocolKind::Claude,
764            OperationFamily::StreamGenerateContent,
765            ProtocolKind::OpenAiResponse,
766        ) => transform_request_descriptor_ref::<
767            crate::claude::create_message::request::ClaudeCreateMessageRequest,
768            crate::openai::create_response::request::OpenAiCreateResponseRequest,
769        >(&body, model),
770
771        // --- Gemini source ---
772        (
773            OperationFamily::StreamGenerateContent,
774            ProtocolKind::Gemini,
775            OperationFamily::StreamGenerateContent,
776            ProtocolKind::Claude,
777        ) => transform_request_descriptor::<
778            crate::gemini::stream_generate_content::request::GeminiStreamGenerateContentRequest,
779            crate::claude::create_message::request::ClaudeCreateMessageRequest,
780        >(&body, model),
781        (
782            OperationFamily::StreamGenerateContent,
783            ProtocolKind::GeminiNDJson,
784            OperationFamily::StreamGenerateContent,
785            ProtocolKind::Claude,
786        ) => transform_request_descriptor::<
787            crate::gemini::stream_generate_content::request::GeminiStreamGenerateContentRequest,
788            crate::claude::create_message::request::ClaudeCreateMessageRequest,
789        >(&body, model),
790        (
791            OperationFamily::StreamGenerateContent,
792            ProtocolKind::Gemini,
793            OperationFamily::StreamGenerateContent,
794            ProtocolKind::OpenAiChatCompletion,
795        ) => transform_request_descriptor::<
796            crate::gemini::stream_generate_content::request::GeminiStreamGenerateContentRequest,
797            crate::openai::create_chat_completions::request::OpenAiChatCompletionsRequest,
798        >(&body, model),
799        (
800            OperationFamily::StreamGenerateContent,
801            ProtocolKind::GeminiNDJson,
802            OperationFamily::StreamGenerateContent,
803            ProtocolKind::OpenAiChatCompletion,
804        ) => transform_request_descriptor::<
805            crate::gemini::stream_generate_content::request::GeminiStreamGenerateContentRequest,
806            crate::openai::create_chat_completions::request::OpenAiChatCompletionsRequest,
807        >(&body, model),
808        (
809            OperationFamily::StreamGenerateContent,
810            ProtocolKind::Gemini,
811            OperationFamily::StreamGenerateContent,
812            ProtocolKind::OpenAiResponse,
813        ) => transform_request_descriptor::<
814            crate::gemini::stream_generate_content::request::GeminiStreamGenerateContentRequest,
815            crate::openai::create_response::request::OpenAiCreateResponseRequest,
816        >(&body, model),
817        (
818            OperationFamily::StreamGenerateContent,
819            ProtocolKind::GeminiNDJson,
820            OperationFamily::StreamGenerateContent,
821            ProtocolKind::OpenAiResponse,
822        ) => transform_request_descriptor::<
823            crate::gemini::stream_generate_content::request::GeminiStreamGenerateContentRequest,
824            crate::openai::create_response::request::OpenAiCreateResponseRequest,
825        >(&body, model),
826
827        // --- OpenAI ChatCompletions source ---
828        (
829            OperationFamily::StreamGenerateContent,
830            ProtocolKind::OpenAiChatCompletion,
831            OperationFamily::StreamGenerateContent,
832            ProtocolKind::Claude,
833        ) => transform_request_descriptor_ref::<
834            crate::openai::create_chat_completions::request::OpenAiChatCompletionsRequest,
835            crate::claude::create_message::request::ClaudeCreateMessageRequest,
836        >(&body, model),
837        (
838            OperationFamily::StreamGenerateContent,
839            ProtocolKind::OpenAiChatCompletion,
840            OperationFamily::StreamGenerateContent,
841            ProtocolKind::Gemini,
842        ) => transform_request_descriptor_ref::<
843            crate::openai::create_chat_completions::request::OpenAiChatCompletionsRequest,
844            crate::gemini::stream_generate_content::request::GeminiStreamGenerateContentRequest,
845        >(&body, model),
846        (
847            OperationFamily::StreamGenerateContent,
848            ProtocolKind::OpenAiChatCompletion,
849            OperationFamily::StreamGenerateContent,
850            ProtocolKind::GeminiNDJson,
851        ) => transform_request_descriptor_ref::<
852            crate::openai::create_chat_completions::request::OpenAiChatCompletionsRequest,
853            crate::gemini::stream_generate_content::request::GeminiStreamGenerateContentRequest,
854        >(&body, model),
855        (
856            OperationFamily::StreamGenerateContent,
857            ProtocolKind::OpenAiChatCompletion,
858            OperationFamily::StreamGenerateContent,
859            ProtocolKind::OpenAiResponse,
860        ) => transform_request_descriptor_ref::<
861            crate::openai::create_chat_completions::request::OpenAiChatCompletionsRequest,
862            crate::openai::create_response::request::OpenAiCreateResponseRequest,
863        >(&body, model),
864        // Stream mirror of the non-stream arm above: deepseek and friends
865        // advertise OpenAI Response streaming to clients but only speak
866        // chat-completions upstream.
867        (
868            OperationFamily::StreamGenerateContent,
869            ProtocolKind::OpenAiResponse,
870            OperationFamily::StreamGenerateContent,
871            ProtocolKind::OpenAiChatCompletion,
872        ) => transform_request_descriptor_ref::<
873            crate::openai::create_response::request::OpenAiCreateResponseRequest,
874            crate::openai::create_chat_completions::request::OpenAiChatCompletionsRequest,
875        >(&body, model),
876
877        // --- OpenAI Response source ---
878        (
879            OperationFamily::StreamGenerateContent,
880            ProtocolKind::OpenAiResponse,
881            OperationFamily::StreamGenerateContent,
882            ProtocolKind::Claude,
883        ) => transform_request_descriptor_ref::<
884            crate::openai::create_response::request::OpenAiCreateResponseRequest,
885            crate::claude::create_message::request::ClaudeCreateMessageRequest,
886        >(&body, model),
887        (
888            OperationFamily::StreamGenerateContent,
889            ProtocolKind::OpenAiResponse,
890            OperationFamily::StreamGenerateContent,
891            ProtocolKind::Gemini,
892        ) => transform_request_descriptor_ref::<
893            crate::openai::create_response::request::OpenAiCreateResponseRequest,
894            crate::gemini::stream_generate_content::request::GeminiStreamGenerateContentRequest,
895        >(&body, model),
896        (
897            OperationFamily::StreamGenerateContent,
898            ProtocolKind::OpenAiResponse,
899            OperationFamily::StreamGenerateContent,
900            ProtocolKind::GeminiNDJson,
901        ) => transform_request_descriptor_ref::<
902            crate::openai::create_response::request::OpenAiCreateResponseRequest,
903            crate::gemini::stream_generate_content::request::GeminiStreamGenerateContentRequest,
904        >(&body, model),
905
906        // =====================================================================
907        // count_tokens
908        // =====================================================================
909
910        // --- Claude source ---
911        (
912            OperationFamily::CountToken,
913            ProtocolKind::Claude,
914            OperationFamily::CountToken,
915            ProtocolKind::Gemini,
916        ) => transform_request_descriptor::<
917            crate::claude::count_tokens::request::ClaudeCountTokensRequest,
918            crate::gemini::count_tokens::request::GeminiCountTokensRequest,
919        >(&body, model),
920        (
921            OperationFamily::CountToken,
922            ProtocolKind::Claude,
923            OperationFamily::CountToken,
924            ProtocolKind::OpenAi,
925        ) => transform_request_descriptor::<
926            crate::claude::count_tokens::request::ClaudeCountTokensRequest,
927            crate::openai::count_tokens::request::OpenAiCountTokensRequest,
928        >(&body, model),
929
930        // --- OpenAI source ---
931        (
932            OperationFamily::CountToken,
933            ProtocolKind::OpenAi,
934            OperationFamily::CountToken,
935            ProtocolKind::Claude,
936        ) => transform_request_descriptor::<
937            crate::openai::count_tokens::request::OpenAiCountTokensRequest,
938            crate::claude::count_tokens::request::ClaudeCountTokensRequest,
939        >(&body, model),
940        (
941            OperationFamily::CountToken,
942            ProtocolKind::OpenAi,
943            OperationFamily::CountToken,
944            ProtocolKind::Gemini,
945        ) => transform_request_descriptor::<
946            crate::openai::count_tokens::request::OpenAiCountTokensRequest,
947            crate::gemini::count_tokens::request::GeminiCountTokensRequest,
948        >(&body, model),
949
950        // --- Gemini source ---
951        (
952            OperationFamily::CountToken,
953            ProtocolKind::Gemini,
954            OperationFamily::CountToken,
955            ProtocolKind::Claude,
956        ) => transform_request_descriptor::<
957            crate::gemini::count_tokens::request::GeminiCountTokensRequest,
958            crate::claude::count_tokens::request::ClaudeCountTokensRequest,
959        >(&body, model),
960        (
961            OperationFamily::CountToken,
962            ProtocolKind::Gemini,
963            OperationFamily::CountToken,
964            ProtocolKind::OpenAi,
965        ) => transform_request_descriptor::<
966            crate::gemini::count_tokens::request::GeminiCountTokensRequest,
967            crate::openai::count_tokens::request::OpenAiCountTokensRequest,
968        >(&body, model),
969
970        // =====================================================================
971        // model_get
972        // =====================================================================
973
974        // --- Claude source ---
975        (
976            OperationFamily::ModelGet,
977            ProtocolKind::Claude,
978            OperationFamily::ModelGet,
979            ProtocolKind::Gemini,
980        ) => transform_request_descriptor::<
981            crate::claude::model_get::request::ClaudeModelGetRequest,
982            crate::gemini::model_get::request::GeminiModelGetRequest,
983        >(&body, model),
984        (
985            OperationFamily::ModelGet,
986            ProtocolKind::Claude,
987            OperationFamily::ModelGet,
988            ProtocolKind::OpenAi,
989        ) => transform_request_descriptor::<
990            crate::claude::model_get::request::ClaudeModelGetRequest,
991            crate::openai::model_get::request::OpenAiModelGetRequest,
992        >(&body, model),
993
994        // --- OpenAI source ---
995        (
996            OperationFamily::ModelGet,
997            ProtocolKind::OpenAi,
998            OperationFamily::ModelGet,
999            ProtocolKind::Claude,
1000        ) => transform_request_descriptor::<
1001            crate::openai::model_get::request::OpenAiModelGetRequest,
1002            crate::claude::model_get::request::ClaudeModelGetRequest,
1003        >(&body, model),
1004        (
1005            OperationFamily::ModelGet,
1006            ProtocolKind::OpenAi,
1007            OperationFamily::ModelGet,
1008            ProtocolKind::Gemini,
1009        ) => transform_request_descriptor::<
1010            crate::openai::model_get::request::OpenAiModelGetRequest,
1011            crate::gemini::model_get::request::GeminiModelGetRequest,
1012        >(&body, model),
1013
1014        // --- Gemini source ---
1015        (
1016            OperationFamily::ModelGet,
1017            ProtocolKind::Gemini,
1018            OperationFamily::ModelGet,
1019            ProtocolKind::Claude,
1020        ) => transform_request_descriptor::<
1021            crate::gemini::model_get::request::GeminiModelGetRequest,
1022            crate::claude::model_get::request::ClaudeModelGetRequest,
1023        >(&body, model),
1024        (
1025            OperationFamily::ModelGet,
1026            ProtocolKind::Gemini,
1027            OperationFamily::ModelGet,
1028            ProtocolKind::OpenAi,
1029        ) => transform_request_descriptor::<
1030            crate::gemini::model_get::request::GeminiModelGetRequest,
1031            crate::openai::model_get::request::OpenAiModelGetRequest,
1032        >(&body, model),
1033
1034        // =====================================================================
1035        // model_list
1036        // =====================================================================
1037
1038        // --- Claude source ---
1039        (
1040            OperationFamily::ModelList,
1041            ProtocolKind::Claude,
1042            OperationFamily::ModelList,
1043            ProtocolKind::Gemini,
1044        ) => transform_request_descriptor::<
1045            crate::claude::model_list::request::ClaudeModelListRequest,
1046            crate::gemini::model_list::request::GeminiModelListRequest,
1047        >(&body, model),
1048        (
1049            OperationFamily::ModelList,
1050            ProtocolKind::Claude,
1051            OperationFamily::ModelList,
1052            ProtocolKind::OpenAi,
1053        ) => transform_request_descriptor::<
1054            crate::claude::model_list::request::ClaudeModelListRequest,
1055            crate::openai::model_list::request::OpenAiModelListRequest,
1056        >(&body, model),
1057
1058        // --- OpenAI source ---
1059        (
1060            OperationFamily::ModelList,
1061            ProtocolKind::OpenAi,
1062            OperationFamily::ModelList,
1063            ProtocolKind::Claude,
1064        ) => transform_request_descriptor::<
1065            crate::openai::model_list::request::OpenAiModelListRequest,
1066            crate::claude::model_list::request::ClaudeModelListRequest,
1067        >(&body, model),
1068        (
1069            OperationFamily::ModelList,
1070            ProtocolKind::OpenAi,
1071            OperationFamily::ModelList,
1072            ProtocolKind::Gemini,
1073        ) => transform_request_descriptor::<
1074            crate::openai::model_list::request::OpenAiModelListRequest,
1075            crate::gemini::model_list::request::GeminiModelListRequest,
1076        >(&body, model),
1077
1078        // --- Gemini source ---
1079        (
1080            OperationFamily::ModelList,
1081            ProtocolKind::Gemini,
1082            OperationFamily::ModelList,
1083            ProtocolKind::Claude,
1084        ) => transform_request_descriptor::<
1085            crate::gemini::model_list::request::GeminiModelListRequest,
1086            crate::claude::model_list::request::ClaudeModelListRequest,
1087        >(&body, model),
1088        (
1089            OperationFamily::ModelList,
1090            ProtocolKind::Gemini,
1091            OperationFamily::ModelList,
1092            ProtocolKind::OpenAi,
1093        ) => transform_request_descriptor::<
1094            crate::gemini::model_list::request::GeminiModelListRequest,
1095            crate::openai::model_list::request::OpenAiModelListRequest,
1096        >(&body, model),
1097
1098        // =====================================================================
1099        // embeddings
1100        // =====================================================================
1101        (
1102            OperationFamily::Embedding,
1103            ProtocolKind::OpenAi,
1104            OperationFamily::Embedding,
1105            ProtocolKind::Gemini,
1106        ) => transform_request_descriptor::<
1107            crate::openai::embeddings::request::OpenAiEmbeddingsRequest,
1108            crate::gemini::embeddings::request::GeminiEmbedContentRequest,
1109        >(&body, model),
1110        (
1111            OperationFamily::Embedding,
1112            ProtocolKind::Gemini,
1113            OperationFamily::Embedding,
1114            ProtocolKind::OpenAi,
1115        ) => transform_request_descriptor::<
1116            crate::gemini::embeddings::request::GeminiEmbedContentRequest,
1117            crate::openai::embeddings::request::OpenAiEmbeddingsRequest,
1118        >(&body, model),
1119
1120        // =====================================================================
1121        // create_image
1122        // =====================================================================
1123        (
1124            OperationFamily::CreateImage,
1125            ProtocolKind::OpenAi,
1126            OperationFamily::GenerateContent,
1127            ProtocolKind::Gemini,
1128        )
1129        | (
1130            OperationFamily::CreateImage,
1131            ProtocolKind::OpenAi,
1132            OperationFamily::StreamGenerateContent,
1133            ProtocolKind::Gemini,
1134        ) => transform_json::<
1135            crate::openai::create_image::request::OpenAiCreateImageRequest,
1136            crate::gemini::generate_content::request::GeminiGenerateContentRequest,
1137        >(&body),
1138
1139        (
1140            OperationFamily::CreateImage,
1141            ProtocolKind::OpenAi,
1142            OperationFamily::StreamGenerateContent,
1143            ProtocolKind::OpenAiResponse,
1144        )
1145        | (
1146            OperationFamily::CreateImage,
1147            ProtocolKind::OpenAi,
1148            OperationFamily::GenerateContent,
1149            ProtocolKind::OpenAiResponse,
1150        ) => transform_json::<
1151            crate::openai::create_image::request::OpenAiCreateImageRequest,
1152            crate::openai::create_response::request::OpenAiCreateResponseRequest,
1153        >(&body),
1154
1155        // =====================================================================
1156        // create_image_edit
1157        // =====================================================================
1158        (
1159            OperationFamily::CreateImageEdit,
1160            ProtocolKind::OpenAi,
1161            OperationFamily::GenerateContent,
1162            ProtocolKind::Gemini,
1163        )
1164        | (
1165            OperationFamily::CreateImageEdit,
1166            ProtocolKind::OpenAi,
1167            OperationFamily::StreamGenerateContent,
1168            ProtocolKind::Gemini,
1169        ) => transform_json::<
1170            crate::openai::create_image_edit::request::OpenAiCreateImageEditRequest,
1171            crate::gemini::generate_content::request::GeminiGenerateContentRequest,
1172        >(&body),
1173
1174        (
1175            OperationFamily::CreateImageEdit,
1176            ProtocolKind::OpenAi,
1177            OperationFamily::StreamGenerateContent,
1178            ProtocolKind::OpenAiResponse,
1179        )
1180        | (
1181            OperationFamily::CreateImageEdit,
1182            ProtocolKind::OpenAi,
1183            OperationFamily::GenerateContent,
1184            ProtocolKind::OpenAiResponse,
1185        ) => transform_json::<
1186            crate::openai::create_image_edit::request::OpenAiCreateImageEditRequest,
1187            crate::openai::create_response::request::OpenAiCreateResponseRequest,
1188        >(&body),
1189
1190        // =====================================================================
1191        // compact
1192        // =====================================================================
1193        (
1194            OperationFamily::Compact,
1195            ProtocolKind::OpenAi,
1196            OperationFamily::GenerateContent,
1197            ProtocolKind::Claude,
1198        ) => transform_json::<
1199            crate::openai::compact_response::request::OpenAiCompactRequest,
1200            crate::claude::create_message::request::ClaudeCreateMessageRequest,
1201        >(&body),
1202        (
1203            OperationFamily::Compact,
1204            ProtocolKind::OpenAi,
1205            OperationFamily::GenerateContent,
1206            ProtocolKind::Gemini,
1207        ) => transform_json::<
1208            crate::openai::compact_response::request::OpenAiCompactRequest,
1209            crate::gemini::generate_content::request::GeminiGenerateContentRequest,
1210        >(&body),
1211
1212        _ => Err(TransformError::new(format!(
1213            "no request transform for ({}, {}) -> ({}, {})",
1214            src_operation, src_protocol, dst_operation, dst_protocol
1215        ))),
1216    }
1217    .map(|body| (translated_query, body))
1218}
1219
1220/// Transform a response body from upstream protocol back to client protocol.
1221///
1222/// When a client sends request in (src_op, src_proto) and upstream responds in
1223/// (dst_op, dst_proto), we need to convert the upstream response back to the
1224/// client's protocol. The key is looked up as (dst_op, dst_proto, src_op, src_proto)
1225/// because we're converting FROM the upstream format TO the client format.
1226pub fn transform_response(
1227    src_operation: OperationFamily,
1228    src_protocol: ProtocolKind,
1229    dst_operation: OperationFamily,
1230    dst_protocol: ProtocolKind,
1231    body: Vec<u8>,
1232) -> Result<Vec<u8>, TransformError> {
1233    tracing::debug!(
1234        src_operation = %src_operation,
1235        src_protocol = %src_protocol,
1236        dst_operation = %dst_operation,
1237        dst_protocol = %dst_protocol,
1238        "transforming response"
1239    );
1240    // Response direction: upstream responded in (dst_op, dst_proto),
1241    // client expects (src_op, src_proto).
1242    let key = (dst_operation, dst_protocol, src_operation, src_protocol);
1243
1244    match key {
1245        // =====================================================================
1246        // generate_content responses
1247        // =====================================================================
1248
1249        // Gemini response → Claude
1250        (
1251            OperationFamily::GenerateContent,
1252            ProtocolKind::Gemini,
1253            OperationFamily::GenerateContent,
1254            ProtocolKind::Claude,
1255        ) => transform_response_json::<
1256            crate::gemini::generate_content::response::GeminiGenerateContentResponse,
1257            crate::claude::create_message::response::ClaudeCreateMessageResponse,
1258        >(&body),
1259        // OpenAI ChatCompletions response → Claude
1260        (
1261            OperationFamily::GenerateContent,
1262            ProtocolKind::OpenAiChatCompletion,
1263            OperationFamily::GenerateContent,
1264            ProtocolKind::Claude,
1265        ) => transform_response_json::<
1266            crate::openai::create_chat_completions::response::OpenAiChatCompletionsResponse,
1267            crate::claude::create_message::response::ClaudeCreateMessageResponse,
1268        >(&body),
1269        // OpenAI Response response → Claude
1270        (
1271            OperationFamily::GenerateContent,
1272            ProtocolKind::OpenAiResponse,
1273            OperationFamily::GenerateContent,
1274            ProtocolKind::Claude,
1275        ) => transform_response_json::<
1276            crate::openai::create_response::response::OpenAiCreateResponseResponse,
1277            crate::claude::create_message::response::ClaudeCreateMessageResponse,
1278        >(&body),
1279
1280        // Claude response → Gemini
1281        (
1282            OperationFamily::GenerateContent,
1283            ProtocolKind::Claude,
1284            OperationFamily::GenerateContent,
1285            ProtocolKind::Gemini,
1286        ) => transform_response_json::<
1287            crate::claude::create_message::response::ClaudeCreateMessageResponse,
1288            crate::gemini::generate_content::response::GeminiGenerateContentResponse,
1289        >(&body),
1290        // OpenAI ChatCompletions response → Gemini
1291        (
1292            OperationFamily::GenerateContent,
1293            ProtocolKind::OpenAiChatCompletion,
1294            OperationFamily::GenerateContent,
1295            ProtocolKind::Gemini,
1296        ) => transform_response_json::<
1297            crate::openai::create_chat_completions::response::OpenAiChatCompletionsResponse,
1298            crate::gemini::generate_content::response::GeminiGenerateContentResponse,
1299        >(&body),
1300        // OpenAI Response response → Gemini
1301        (
1302            OperationFamily::GenerateContent,
1303            ProtocolKind::OpenAiResponse,
1304            OperationFamily::GenerateContent,
1305            ProtocolKind::Gemini,
1306        ) => transform_response_json::<
1307            crate::openai::create_response::response::OpenAiCreateResponseResponse,
1308            crate::gemini::generate_content::response::GeminiGenerateContentResponse,
1309        >(&body),
1310
1311        // Claude response → OpenAI ChatCompletions
1312        (
1313            OperationFamily::GenerateContent,
1314            ProtocolKind::Claude,
1315            OperationFamily::GenerateContent,
1316            ProtocolKind::OpenAiChatCompletion,
1317        ) => transform_response_json::<
1318            crate::claude::create_message::response::ClaudeCreateMessageResponse,
1319            crate::openai::create_chat_completions::response::OpenAiChatCompletionsResponse,
1320        >(&body),
1321        // Gemini response → OpenAI ChatCompletions
1322        (
1323            OperationFamily::GenerateContent,
1324            ProtocolKind::Gemini,
1325            OperationFamily::GenerateContent,
1326            ProtocolKind::OpenAiChatCompletion,
1327        ) => transform_response_json::<
1328            crate::gemini::generate_content::response::GeminiGenerateContentResponse,
1329            crate::openai::create_chat_completions::response::OpenAiChatCompletionsResponse,
1330        >(&body),
1331        // OpenAI Response response → OpenAI ChatCompletions
1332        (
1333            OperationFamily::GenerateContent,
1334            ProtocolKind::OpenAiResponse,
1335            OperationFamily::GenerateContent,
1336            ProtocolKind::OpenAiChatCompletion,
1337        ) => transform_response_json::<
1338            crate::openai::create_response::response::OpenAiCreateResponseResponse,
1339            crate::openai::create_chat_completions::response::OpenAiChatCompletionsResponse,
1340        >(&body),
1341        // OpenAI ChatCompletions response → OpenAI Response
1342        //
1343        // Mirror of the arm above, used when the client is speaking
1344        // OpenAI Response but the upstream only returns chat completions
1345        // (deepseek, groq, nvidia, etc.).
1346        (
1347            OperationFamily::GenerateContent,
1348            ProtocolKind::OpenAiChatCompletion,
1349            OperationFamily::GenerateContent,
1350            ProtocolKind::OpenAiResponse,
1351        ) => transform_response_json::<
1352            crate::openai::create_chat_completions::response::OpenAiChatCompletionsResponse,
1353            crate::openai::create_response::response::OpenAiCreateResponseResponse,
1354        >(&body),
1355
1356        // Claude response → OpenAI Response
1357        (
1358            OperationFamily::GenerateContent,
1359            ProtocolKind::Claude,
1360            OperationFamily::GenerateContent,
1361            ProtocolKind::OpenAiResponse,
1362        ) => transform_response_json::<
1363            crate::claude::create_message::response::ClaudeCreateMessageResponse,
1364            crate::openai::create_response::response::OpenAiCreateResponseResponse,
1365        >(&body),
1366        // Gemini response → OpenAI Response
1367        (
1368            OperationFamily::GenerateContent,
1369            ProtocolKind::Gemini,
1370            OperationFamily::GenerateContent,
1371            ProtocolKind::OpenAiResponse,
1372        ) => transform_response_json::<
1373            crate::gemini::generate_content::response::GeminiGenerateContentResponse,
1374            crate::openai::create_response::response::OpenAiCreateResponseResponse,
1375        >(&body),
1376
1377        // =====================================================================
1378        // count_tokens responses
1379        // =====================================================================
1380
1381        // Gemini response → Claude
1382        (
1383            OperationFamily::CountToken,
1384            ProtocolKind::Gemini,
1385            OperationFamily::CountToken,
1386            ProtocolKind::Claude,
1387        ) => transform_response_json::<
1388            crate::gemini::count_tokens::response::GeminiCountTokensResponse,
1389            crate::claude::count_tokens::response::ClaudeCountTokensResponse,
1390        >(&body),
1391        // OpenAI response → Claude
1392        (
1393            OperationFamily::CountToken,
1394            ProtocolKind::OpenAi,
1395            OperationFamily::CountToken,
1396            ProtocolKind::Claude,
1397        ) => transform_response_json::<
1398            crate::openai::count_tokens::response::OpenAiCountTokensResponse,
1399            crate::claude::count_tokens::response::ClaudeCountTokensResponse,
1400        >(&body),
1401
1402        // Claude response → OpenAI
1403        (
1404            OperationFamily::CountToken,
1405            ProtocolKind::Claude,
1406            OperationFamily::CountToken,
1407            ProtocolKind::OpenAi,
1408        ) => transform_response_json::<
1409            crate::claude::count_tokens::response::ClaudeCountTokensResponse,
1410            crate::openai::count_tokens::response::OpenAiCountTokensResponse,
1411        >(&body),
1412        // Gemini response → OpenAI
1413        (
1414            OperationFamily::CountToken,
1415            ProtocolKind::Gemini,
1416            OperationFamily::CountToken,
1417            ProtocolKind::OpenAi,
1418        ) => transform_response_json::<
1419            crate::gemini::count_tokens::response::GeminiCountTokensResponse,
1420            crate::openai::count_tokens::response::OpenAiCountTokensResponse,
1421        >(&body),
1422
1423        // Claude response → Gemini
1424        (
1425            OperationFamily::CountToken,
1426            ProtocolKind::Claude,
1427            OperationFamily::CountToken,
1428            ProtocolKind::Gemini,
1429        ) => transform_response_json::<
1430            crate::claude::count_tokens::response::ClaudeCountTokensResponse,
1431            crate::gemini::count_tokens::response::GeminiCountTokensResponse,
1432        >(&body),
1433        // OpenAI response → Gemini
1434        (
1435            OperationFamily::CountToken,
1436            ProtocolKind::OpenAi,
1437            OperationFamily::CountToken,
1438            ProtocolKind::Gemini,
1439        ) => transform_response_json::<
1440            crate::openai::count_tokens::response::OpenAiCountTokensResponse,
1441            crate::gemini::count_tokens::response::GeminiCountTokensResponse,
1442        >(&body),
1443
1444        // =====================================================================
1445        // model_get responses
1446        // =====================================================================
1447
1448        // Gemini response → Claude
1449        (
1450            OperationFamily::ModelGet,
1451            ProtocolKind::Gemini,
1452            OperationFamily::ModelGet,
1453            ProtocolKind::Claude,
1454        ) => transform_response_json::<
1455            crate::gemini::model_get::response::GeminiModelGetResponse,
1456            crate::claude::model_get::response::ClaudeModelGetResponse,
1457        >(&body),
1458        // OpenAI response → Claude
1459        (
1460            OperationFamily::ModelGet,
1461            ProtocolKind::OpenAi,
1462            OperationFamily::ModelGet,
1463            ProtocolKind::Claude,
1464        ) => transform_response_json::<
1465            crate::openai::model_get::response::OpenAiModelGetResponse,
1466            crate::claude::model_get::response::ClaudeModelGetResponse,
1467        >(&body),
1468
1469        // Claude response → OpenAI
1470        (
1471            OperationFamily::ModelGet,
1472            ProtocolKind::Claude,
1473            OperationFamily::ModelGet,
1474            ProtocolKind::OpenAi,
1475        ) => transform_response_json::<
1476            crate::claude::model_get::response::ClaudeModelGetResponse,
1477            crate::openai::model_get::response::OpenAiModelGetResponse,
1478        >(&body),
1479        // Gemini response → OpenAI
1480        (
1481            OperationFamily::ModelGet,
1482            ProtocolKind::Gemini,
1483            OperationFamily::ModelGet,
1484            ProtocolKind::OpenAi,
1485        ) => transform_response_json::<
1486            crate::gemini::model_get::response::GeminiModelGetResponse,
1487            crate::openai::model_get::response::OpenAiModelGetResponse,
1488        >(&body),
1489
1490        // Claude response → Gemini
1491        (
1492            OperationFamily::ModelGet,
1493            ProtocolKind::Claude,
1494            OperationFamily::ModelGet,
1495            ProtocolKind::Gemini,
1496        ) => transform_response_json::<
1497            crate::claude::model_get::response::ClaudeModelGetResponse,
1498            crate::gemini::model_get::response::GeminiModelGetResponse,
1499        >(&body),
1500        // OpenAI response → Gemini
1501        (
1502            OperationFamily::ModelGet,
1503            ProtocolKind::OpenAi,
1504            OperationFamily::ModelGet,
1505            ProtocolKind::Gemini,
1506        ) => transform_response_json::<
1507            crate::openai::model_get::response::OpenAiModelGetResponse,
1508            crate::gemini::model_get::response::GeminiModelGetResponse,
1509        >(&body),
1510
1511        // =====================================================================
1512        // model_list responses
1513        // =====================================================================
1514
1515        // Gemini response → Claude
1516        (
1517            OperationFamily::ModelList,
1518            ProtocolKind::Gemini,
1519            OperationFamily::ModelList,
1520            ProtocolKind::Claude,
1521        ) => transform_response_json::<
1522            crate::gemini::model_list::response::GeminiModelListResponse,
1523            crate::claude::model_list::response::ClaudeModelListResponse,
1524        >(&body),
1525        // OpenAI response → Claude
1526        (
1527            OperationFamily::ModelList,
1528            ProtocolKind::OpenAi,
1529            OperationFamily::ModelList,
1530            ProtocolKind::Claude,
1531        ) => transform_response_json::<
1532            crate::openai::model_list::response::OpenAiModelListResponse,
1533            crate::claude::model_list::response::ClaudeModelListResponse,
1534        >(&body),
1535
1536        // Claude response → OpenAI
1537        (
1538            OperationFamily::ModelList,
1539            ProtocolKind::Claude,
1540            OperationFamily::ModelList,
1541            ProtocolKind::OpenAi,
1542        ) => transform_response_json::<
1543            crate::claude::model_list::response::ClaudeModelListResponse,
1544            crate::openai::model_list::response::OpenAiModelListResponse,
1545        >(&body),
1546        // Gemini response → OpenAI
1547        (
1548            OperationFamily::ModelList,
1549            ProtocolKind::Gemini,
1550            OperationFamily::ModelList,
1551            ProtocolKind::OpenAi,
1552        ) => transform_response_json::<
1553            crate::gemini::model_list::response::GeminiModelListResponse,
1554            crate::openai::model_list::response::OpenAiModelListResponse,
1555        >(&body),
1556
1557        // Claude response → Gemini
1558        (
1559            OperationFamily::ModelList,
1560            ProtocolKind::Claude,
1561            OperationFamily::ModelList,
1562            ProtocolKind::Gemini,
1563        ) => transform_response_json::<
1564            crate::claude::model_list::response::ClaudeModelListResponse,
1565            crate::gemini::model_list::response::GeminiModelListResponse,
1566        >(&body),
1567        // OpenAI response → Gemini
1568        (
1569            OperationFamily::ModelList,
1570            ProtocolKind::OpenAi,
1571            OperationFamily::ModelList,
1572            ProtocolKind::Gemini,
1573        ) => transform_response_json::<
1574            crate::openai::model_list::response::OpenAiModelListResponse,
1575            crate::gemini::model_list::response::GeminiModelListResponse,
1576        >(&body),
1577
1578        // =====================================================================
1579        // embeddings responses
1580        // =====================================================================
1581        (
1582            OperationFamily::Embedding,
1583            ProtocolKind::Gemini,
1584            OperationFamily::Embedding,
1585            ProtocolKind::OpenAi,
1586        ) => transform_response_json::<
1587            crate::gemini::embeddings::response::GeminiEmbedContentResponse,
1588            crate::openai::embeddings::response::OpenAiEmbeddingsResponse,
1589        >(&body),
1590        (
1591            OperationFamily::Embedding,
1592            ProtocolKind::OpenAi,
1593            OperationFamily::Embedding,
1594            ProtocolKind::Gemini,
1595        ) => transform_response_json::<
1596            crate::openai::embeddings::response::OpenAiEmbeddingsResponse,
1597            crate::gemini::embeddings::response::GeminiEmbedContentResponse,
1598        >(&body),
1599
1600        // =====================================================================
1601        // create_image responses
1602        // =====================================================================
1603        (
1604            OperationFamily::GenerateContent,
1605            ProtocolKind::Gemini,
1606            OperationFamily::CreateImage,
1607            ProtocolKind::OpenAi,
1608        )
1609        | (
1610            OperationFamily::StreamGenerateContent,
1611            ProtocolKind::Gemini,
1612            OperationFamily::CreateImage,
1613            ProtocolKind::OpenAi,
1614        ) => transform_response_json::<
1615            crate::gemini::generate_content::response::GeminiGenerateContentResponse,
1616            crate::openai::create_image::response::OpenAiCreateImageResponse,
1617        >(&body),
1618
1619        (
1620            OperationFamily::StreamGenerateContent,
1621            ProtocolKind::OpenAiResponse,
1622            OperationFamily::CreateImage,
1623            ProtocolKind::OpenAi,
1624        )
1625        | (
1626            OperationFamily::GenerateContent,
1627            ProtocolKind::OpenAiResponse,
1628            OperationFamily::CreateImage,
1629            ProtocolKind::OpenAi,
1630        ) => transform_response_json::<
1631            crate::openai::create_response::response::OpenAiCreateResponseResponse,
1632            crate::openai::create_image::response::OpenAiCreateImageResponse,
1633        >(&body),
1634
1635        // =====================================================================
1636        // create_image_edit responses
1637        // =====================================================================
1638        (
1639            OperationFamily::GenerateContent,
1640            ProtocolKind::Gemini,
1641            OperationFamily::CreateImageEdit,
1642            ProtocolKind::OpenAi,
1643        )
1644        | (
1645            OperationFamily::StreamGenerateContent,
1646            ProtocolKind::Gemini,
1647            OperationFamily::CreateImageEdit,
1648            ProtocolKind::OpenAi,
1649        ) => transform_response_json::<
1650            crate::gemini::generate_content::response::GeminiGenerateContentResponse,
1651            crate::openai::create_image_edit::response::OpenAiCreateImageEditResponse,
1652        >(&body),
1653
1654        (
1655            OperationFamily::StreamGenerateContent,
1656            ProtocolKind::OpenAiResponse,
1657            OperationFamily::CreateImageEdit,
1658            ProtocolKind::OpenAi,
1659        )
1660        | (
1661            OperationFamily::GenerateContent,
1662            ProtocolKind::OpenAiResponse,
1663            OperationFamily::CreateImageEdit,
1664            ProtocolKind::OpenAi,
1665        ) => transform_response_json::<
1666            crate::openai::create_response::response::OpenAiCreateResponseResponse,
1667            crate::openai::create_image_edit::response::OpenAiCreateImageEditResponse,
1668        >(&body),
1669
1670        // =====================================================================
1671        // compact responses
1672        // =====================================================================
1673        (
1674            OperationFamily::GenerateContent,
1675            ProtocolKind::Claude,
1676            OperationFamily::Compact,
1677            ProtocolKind::OpenAi,
1678        ) => transform_response_json::<
1679            crate::claude::create_message::response::ClaudeCreateMessageResponse,
1680            crate::openai::compact_response::response::OpenAiCompactResponse,
1681        >(&body),
1682        (
1683            OperationFamily::GenerateContent,
1684            ProtocolKind::Gemini,
1685            OperationFamily::Compact,
1686            ProtocolKind::OpenAi,
1687        ) => transform_response_json::<
1688            crate::gemini::generate_content::response::GeminiGenerateContentResponse,
1689            crate::openai::compact_response::response::OpenAiCompactResponse,
1690        >(&body),
1691
1692        _ => Err(TransformError::new(format!(
1693            "no response transform from upstream ({}, {}) to client ({}, {})",
1694            dst_operation, dst_protocol, src_operation, src_protocol
1695        ))),
1696    }
1697}
1698
1699/// Generic JSON transform for request body structs.
1700///
1701/// Requests go through this path because their body types are plain structs
1702/// that deserialize directly from the raw HTTP body JSON — they don't have
1703/// the internal `{stats_code, headers, body}` envelope that response wrapper
1704/// enums use. The response-side equivalent is [`transform_response_json`].
1705fn transform_json<Src, Dst>(body: &[u8]) -> Result<Vec<u8>, TransformError>
1706where
1707    Src: serde::de::DeserializeOwned,
1708    Dst: TryFrom<Src> + serde::Serialize,
1709    Dst::Error: std::fmt::Display,
1710{
1711    let src: Src = serde_json::from_slice(body)
1712        .map_err(|e| TransformError::new(format!("request deserialize: {e}")))?;
1713    let dst = Dst::try_from(src).map_err(|e| TransformError::new(format!("transform: {e}")))?;
1714    serde_json::to_vec(&dst).map_err(|e| TransformError::new(format!("response serialize: {e}")))
1715}
1716
1717/// Generic JSON transform for response wrapper enums.
1718///
1719/// Deserializes the raw upstream response body into `Src` via its
1720/// [`BodyEnvelope::from_body_bytes`] impl (which wraps the raw body in the
1721/// wrapper's `Success`/`Error` variant with placeholder metadata), converts
1722/// into `Dst` via `TryFrom`, then serializes just the inner body of `Dst`
1723/// back out for the client via [`BodyEnvelope::into_body_bytes`].
1724///
1725/// This is the central helper that makes `transform_response` route arms
1726/// work with the `stats_code` / `headers` / `body` wrapper enum shape —
1727/// without it, `from_slice::<Wrapper>` fails on real HTTP bodies because
1728/// they don't have top-level `stats_code` / `headers` fields.
1729fn transform_response_json<Src, Dst>(body: &[u8]) -> Result<Vec<u8>, TransformError>
1730where
1731    Src: BodyEnvelope,
1732    Dst: BodyEnvelope + TryFrom<Src>,
1733    Dst::Error: std::fmt::Display,
1734{
1735    let src = Src::from_body_bytes(body)?;
1736    let dst = Dst::try_from(src).map_err(|e| TransformError::new(format!("transform: {e}")))?;
1737    dst.into_body_bytes()
1738}
1739
1740fn transform_request_descriptor<Src, Dst>(
1741    body: &[u8],
1742    model: Option<&str>,
1743) -> Result<Vec<u8>, TransformError>
1744where
1745    Src: RequestDescriptor,
1746    Dst: RequestDescriptor + TryFrom<Src>,
1747    Dst::Error: std::fmt::Display,
1748{
1749    let src_body: Src::Body = serde_json::from_slice(body)
1750        .map_err(|e| TransformError::new(format!("request deserialize: {}", e)))?;
1751    let src = Src::from_body(src_body).with_model(model);
1752    let dst = Dst::try_from(src).map_err(|e| TransformError::new(format!("transform: {}", e)))?;
1753
1754    serde_json::to_vec(&dst.into_body())
1755        .map_err(|e| TransformError::new(format!("response serialize: {}", e)))
1756}
1757
1758fn transform_request_descriptor_ref<Src, Dst>(
1759    body: &[u8],
1760    model: Option<&str>,
1761) -> Result<Vec<u8>, TransformError>
1762where
1763    Src: RequestDescriptor,
1764    for<'a> Dst: RequestDescriptor + TryFrom<&'a Src>,
1765    for<'a> <Dst as TryFrom<&'a Src>>::Error: std::fmt::Display,
1766{
1767    let src_body: Src::Body = serde_json::from_slice(body)
1768        .map_err(|e| TransformError::new(format!("request deserialize: {}", e)))?;
1769    let src = Src::from_body(src_body).with_model(model);
1770    let dst = Dst::try_from(&src).map_err(|e| TransformError::new(format!("transform: {}", e)))?;
1771
1772    serde_json::to_vec(&dst.into_body())
1773        .map_err(|e| TransformError::new(format!("response serialize: {}", e)))
1774}
1775
1776pub type StreamChunkNormalizer = Arc<dyn Fn(Vec<u8>) -> Vec<u8> + Send + Sync>;
1777
1778/// Convert an upstream error body (non-2xx response body) from the
1779/// upstream protocol's error schema to the client's expected error
1780/// schema, falling back to the raw bytes on any failure.
1781///
1782/// Each protocol uses a different error shape:
1783/// - Claude: `{"type":"error","error":{"type":"...","message":"..."}}`
1784/// - OpenAI: `{"error":{"message":"...","type":"...","code":"..."}}`
1785/// - Gemini: `{"error":{"code":N,"message":"...","status":"..."}}`
1786///
1787/// An OpenAI-speaking client that receives a Claude error body cannot
1788/// parse it, so we route error bodies through the same `transform_response`
1789/// pipeline that success bodies use. The `BodyEnvelope::from_body_bytes`
1790/// macro tries both the success and error variants, so if the upstream
1791/// error conforms to the declared error schema, it's converted via the
1792/// existing `TryFrom<SrcResponse> for DstResponse` impls (which handle
1793/// the `Error { .. }` variant separately).
1794///
1795/// If the upstream error schema doesn't match the declared `error_body`
1796/// type (e.g. codex returning `{"detail":{"code":"..."}}` which is not
1797/// `OpenAiApiErrorResponse`), `from_body_bytes` fails and the whole
1798/// transform errors out — in which case this helper forwards the raw
1799/// upstream bytes so the client at least sees what the upstream sent.
1800///
1801/// For streaming operations, the operation family is substituted with
1802/// `GenerateContent` before dispatch. Error bodies share the same schema
1803/// regardless of whether the request was streaming, but `transform_response`
1804/// only declares arms for non-stream ops.
1805pub fn convert_error_body_or_raw(
1806    src_operation: OperationFamily,
1807    src_protocol: ProtocolKind,
1808    dst_operation: OperationFamily,
1809    dst_protocol: ProtocolKind,
1810    body: Vec<u8>,
1811) -> Vec<u8> {
1812    let op_for_error = |op: OperationFamily| match op {
1813        OperationFamily::StreamGenerateContent => OperationFamily::GenerateContent,
1814        other => other,
1815    };
1816    let src_op = op_for_error(src_operation);
1817    let dst_op = op_for_error(dst_operation);
1818
1819    // Passthrough (src == dst): no conversion needed, the error is
1820    // already in the client's expected format.
1821    if src_op == dst_op && src_protocol == dst_protocol {
1822        return body;
1823    }
1824
1825    match transform_response(src_op, src_protocol, dst_op, dst_protocol, body.clone()) {
1826        Ok(converted) => converted,
1827        Err(err) => {
1828            tracing::debug!(
1829                error = %err,
1830                src_op = %src_operation,
1831                src_proto = %src_protocol,
1832                dst_op = %dst_operation,
1833                dst_proto = %dst_protocol,
1834                body_len = body.len(),
1835                "error body did not match declared schema; forwarding raw upstream bytes"
1836            );
1837            body
1838        }
1839    }
1840}
1841
1842pub struct StreamResponseTransformer {
1843    decoder: StreamChunkDecoder,
1844    inner: Box<dyn StreamChunkTransform>,
1845    normalizer: Option<StreamChunkNormalizer>,
1846}
1847
1848impl StreamResponseTransformer {
1849    pub fn push_chunk(&mut self, chunk: &[u8]) -> Result<Vec<u8>, TransformError> {
1850        let mut json_chunks = Vec::new();
1851        self.decoder.push_chunk(chunk, &mut json_chunks);
1852        self.process_json_chunks(json_chunks)
1853    }
1854
1855    pub fn finish(&mut self) -> Result<Vec<u8>, TransformError> {
1856        let mut json_chunks = Vec::new();
1857        self.decoder.finish(&mut json_chunks);
1858        let mut out = self.process_json_chunks(json_chunks)?;
1859        self.inner.finish(&mut out)?;
1860        Ok(out)
1861    }
1862
1863    fn process_json_chunks(
1864        &mut self,
1865        json_chunks: Vec<Vec<u8>>,
1866    ) -> Result<Vec<u8>, TransformError> {
1867        let mut out = Vec::new();
1868        for chunk in json_chunks {
1869            let chunk = if let Some(normalizer) = &self.normalizer {
1870                normalizer(chunk)
1871            } else {
1872                chunk
1873            };
1874            if chunk.is_empty() {
1875                continue;
1876            }
1877            self.inner.on_json_chunk(&chunk, &mut out)?;
1878        }
1879        Ok(out)
1880    }
1881}
1882
1883trait StreamChunkTransform: Send {
1884    fn on_json_chunk(&mut self, chunk: &[u8], out: &mut Vec<u8>) -> Result<(), TransformError>;
1885    fn finish(&mut self, out: &mut Vec<u8>) -> Result<(), TransformError>;
1886}
1887
1888trait EventConverter<Input, Output>: Send {
1889    fn on_input(&mut self, input: Input, out: &mut Vec<Output>) -> Result<(), TransformError>;
1890    fn finish(&mut self, out: &mut Vec<Output>) -> Result<(), TransformError>;
1891}
1892
1893struct TypedStreamTransform<Input, Output, Converter> {
1894    converter: Converter,
1895    encoder: StreamChunkEncoder,
1896    _marker: PhantomData<(Input, Output)>,
1897}
1898
1899impl<Input, Output, Converter> StreamChunkTransform
1900    for TypedStreamTransform<Input, Output, Converter>
1901where
1902    Input: DeserializeOwned + Send + 'static,
1903    Output: Serialize + Send + 'static,
1904    Converter: EventConverter<Input, Output> + Send + 'static,
1905{
1906    fn on_json_chunk(&mut self, chunk: &[u8], out: &mut Vec<u8>) -> Result<(), TransformError> {
1907        let input: Input = serde_json::from_slice(chunk)
1908            .map_err(|e| TransformError::new(format!("stream chunk deserialize failed: {e}")))?;
1909        let mut events = Vec::new();
1910        self.converter.on_input(input, &mut events)?;
1911        self.encoder.encode_events(&events, out)
1912    }
1913
1914    fn finish(&mut self, out: &mut Vec<u8>) -> Result<(), TransformError> {
1915        let mut events = Vec::new();
1916        self.converter.finish(&mut events)?;
1917        self.encoder.encode_events(&events, out)?;
1918        self.encoder.finish(out);
1919        Ok(())
1920    }
1921}
1922
1923enum StreamChunkDecoder {
1924    Sse(crate::stream::SseToNdjsonRewriter),
1925    Ndjson(Vec<u8>),
1926}
1927
1928impl StreamChunkDecoder {
1929    fn from_protocol(protocol: ProtocolKind) -> Result<Self, TransformError> {
1930        match protocol {
1931            ProtocolKind::Claude
1932            | ProtocolKind::OpenAiChatCompletion
1933            | ProtocolKind::OpenAiResponse
1934            | ProtocolKind::Gemini => Ok(Self::Sse(crate::stream::SseToNdjsonRewriter::default())),
1935            ProtocolKind::GeminiNDJson => Ok(Self::Ndjson(Vec::new())),
1936            _ => Err(TransformError::new(format!(
1937                "unsupported stream input protocol: {protocol}"
1938            ))),
1939        }
1940    }
1941
1942    fn push_chunk(&mut self, chunk: &[u8], out: &mut Vec<Vec<u8>>) {
1943        match self {
1944            Self::Sse(rewriter) => {
1945                let converted = rewriter.push_chunk(chunk);
1946                split_json_lines(&converted, out);
1947            }
1948            Self::Ndjson(pending) => {
1949                pending.extend_from_slice(chunk);
1950                drain_json_lines(pending, out);
1951            }
1952        }
1953    }
1954
1955    fn finish(&mut self, out: &mut Vec<Vec<u8>>) {
1956        match self {
1957            Self::Sse(rewriter) => {
1958                let converted = rewriter.finish();
1959                split_json_lines(&converted, out);
1960            }
1961            Self::Ndjson(pending) => {
1962                if pending.is_empty() {
1963                    return;
1964                }
1965                let mut line = std::mem::take(pending);
1966                if line.last().copied() == Some(b'\r') {
1967                    line.pop();
1968                }
1969                if !line.is_empty() {
1970                    out.push(line);
1971                }
1972            }
1973        }
1974    }
1975}
1976
1977enum StreamChunkEncoder {
1978    Sse { append_done_marker: bool },
1979    Ndjson,
1980}
1981
1982impl StreamChunkEncoder {
1983    fn from_protocol(protocol: ProtocolKind) -> Result<Self, TransformError> {
1984        match protocol {
1985            ProtocolKind::Claude | ProtocolKind::OpenAiResponse | ProtocolKind::Gemini => {
1986                Ok(Self::Sse {
1987                    append_done_marker: false,
1988                })
1989            }
1990            ProtocolKind::OpenAiChatCompletion => Ok(Self::Sse {
1991                append_done_marker: true,
1992            }),
1993            ProtocolKind::GeminiNDJson => Ok(Self::Ndjson),
1994            _ => Err(TransformError::new(format!(
1995                "unsupported stream output protocol: {protocol}"
1996            ))),
1997        }
1998    }
1999
2000    fn encode_events<T: Serialize>(
2001        &self,
2002        events: &[T],
2003        out: &mut Vec<u8>,
2004    ) -> Result<(), TransformError> {
2005        for event in events {
2006            let json = serde_json::to_vec(event)
2007                .map_err(|e| TransformError::new(format!("stream chunk serialize failed: {e}")))?;
2008            match self {
2009                Self::Sse { .. } => {
2010                    out.extend_from_slice(b"data: ");
2011                    out.extend_from_slice(&json);
2012                    out.extend_from_slice(b"\n\n");
2013                }
2014                Self::Ndjson => {
2015                    out.extend_from_slice(&json);
2016                    out.push(b'\n');
2017                }
2018            }
2019        }
2020        Ok(())
2021    }
2022
2023    fn finish(&self, out: &mut Vec<u8>) {
2024        if let Self::Sse {
2025            append_done_marker: true,
2026        } = self
2027        {
2028            out.extend_from_slice(b"data: [DONE]\n\n");
2029        }
2030    }
2031}
2032
2033use crate::stream::{drain_lines as drain_json_lines, split_lines as split_json_lines};
2034
2035struct IdentityConverter<T>(PhantomData<T>);
2036
2037impl<T> Default for IdentityConverter<T> {
2038    fn default() -> Self {
2039        Self(PhantomData)
2040    }
2041}
2042
2043impl<T: Send> EventConverter<T, T> for IdentityConverter<T> {
2044    fn on_input(&mut self, input: T, out: &mut Vec<T>) -> Result<(), TransformError> {
2045        out.push(input);
2046        Ok(())
2047    }
2048
2049    fn finish(&mut self, _out: &mut Vec<T>) -> Result<(), TransformError> {
2050        Ok(())
2051    }
2052}
2053
2054#[derive(Default)]
2055struct OpenAiChatToClaudeConverter(
2056    crate::transform::claude::stream_generate_content::openai_chat_completions::response::OpenAiChatCompletionsToClaudeStream,
2057);
2058
2059impl
2060    EventConverter<
2061        crate::openai::create_chat_completions::stream::ChatCompletionChunk,
2062        crate::claude::create_message::stream::ClaudeStreamEvent,
2063    > for OpenAiChatToClaudeConverter
2064{
2065    fn on_input(
2066        &mut self,
2067        input: crate::openai::create_chat_completions::stream::ChatCompletionChunk,
2068        out: &mut Vec<crate::claude::create_message::stream::ClaudeStreamEvent>,
2069    ) -> Result<(), TransformError> {
2070        self.0.on_chunk(input, out);
2071        Ok(())
2072    }
2073
2074    fn finish(
2075        &mut self,
2076        out: &mut Vec<crate::claude::create_message::stream::ClaudeStreamEvent>,
2077    ) -> Result<(), TransformError> {
2078        self.0.finish(out);
2079        Ok(())
2080    }
2081}
2082
2083#[derive(Default)]
2084struct GeminiToClaudeConverter(
2085    crate::transform::claude::stream_generate_content::gemini::response::GeminiToClaudeStream,
2086);
2087
2088impl
2089    EventConverter<
2090        crate::gemini::generate_content::response::ResponseBody,
2091        crate::claude::create_message::stream::ClaudeStreamEvent,
2092    > for GeminiToClaudeConverter
2093{
2094    fn on_input(
2095        &mut self,
2096        input: crate::gemini::generate_content::response::ResponseBody,
2097        out: &mut Vec<crate::claude::create_message::stream::ClaudeStreamEvent>,
2098    ) -> Result<(), TransformError> {
2099        self.0.on_chunk(input, out);
2100        Ok(())
2101    }
2102
2103    fn finish(
2104        &mut self,
2105        out: &mut Vec<crate::claude::create_message::stream::ClaudeStreamEvent>,
2106    ) -> Result<(), TransformError> {
2107        self.0.finish(out);
2108        Ok(())
2109    }
2110}
2111
2112#[derive(Default)]
2113struct OpenAiResponseToClaudeConverter(
2114    crate::transform::claude::stream_generate_content::openai_response::response::OpenAiResponseToClaudeStream,
2115);
2116
2117impl
2118    EventConverter<
2119        crate::openai::create_response::stream::ResponseStreamEvent,
2120        crate::claude::create_message::stream::ClaudeStreamEvent,
2121    > for OpenAiResponseToClaudeConverter
2122{
2123    fn on_input(
2124        &mut self,
2125        input: crate::openai::create_response::stream::ResponseStreamEvent,
2126        out: &mut Vec<crate::claude::create_message::stream::ClaudeStreamEvent>,
2127    ) -> Result<(), TransformError> {
2128        self.0.on_stream_event(input, out);
2129        Ok(())
2130    }
2131
2132    fn finish(
2133        &mut self,
2134        out: &mut Vec<crate::claude::create_message::stream::ClaudeStreamEvent>,
2135    ) -> Result<(), TransformError> {
2136        self.0.finish(out);
2137        Ok(())
2138    }
2139}
2140
2141#[derive(Default)]
2142struct ClaudeToOpenAiChatConverter(
2143    crate::transform::openai::stream_generate_content::openai_chat_completions::claude::response::ClaudeToOpenAiChatCompletionsStream,
2144);
2145
2146impl
2147    EventConverter<
2148        crate::claude::create_message::stream::ClaudeStreamEvent,
2149        crate::openai::create_chat_completions::stream::ChatCompletionChunk,
2150    > for ClaudeToOpenAiChatConverter
2151{
2152    fn on_input(
2153        &mut self,
2154        input: crate::claude::create_message::stream::ClaudeStreamEvent,
2155        out: &mut Vec<crate::openai::create_chat_completions::stream::ChatCompletionChunk>,
2156    ) -> Result<(), TransformError> {
2157        self.0
2158            .on_event(input, out)
2159            .map_err(|e| TransformError::new(format!("stream transform failed: {e}")))
2160    }
2161
2162    fn finish(
2163        &mut self,
2164        out: &mut Vec<crate::openai::create_chat_completions::stream::ChatCompletionChunk>,
2165    ) -> Result<(), TransformError> {
2166        self.0.finish(out);
2167        Ok(())
2168    }
2169}
2170
2171#[derive(Default)]
2172struct GeminiToOpenAiChatConverter(
2173    crate::transform::openai::stream_generate_content::openai_chat_completions::gemini::response::GeminiToOpenAiChatCompletionsStream,
2174);
2175
2176impl
2177    EventConverter<
2178        crate::gemini::generate_content::response::ResponseBody,
2179        crate::openai::create_chat_completions::stream::ChatCompletionChunk,
2180    > for GeminiToOpenAiChatConverter
2181{
2182    fn on_input(
2183        &mut self,
2184        input: crate::gemini::generate_content::response::ResponseBody,
2185        out: &mut Vec<crate::openai::create_chat_completions::stream::ChatCompletionChunk>,
2186    ) -> Result<(), TransformError> {
2187        self.0.on_chunk(input, out);
2188        Ok(())
2189    }
2190
2191    fn finish(
2192        &mut self,
2193        out: &mut Vec<crate::openai::create_chat_completions::stream::ChatCompletionChunk>,
2194    ) -> Result<(), TransformError> {
2195        self.0.finish(out);
2196        Ok(())
2197    }
2198}
2199
2200#[derive(Default)]
2201struct ClaudeToOpenAiResponseConverter(
2202    crate::transform::openai::stream_generate_content::openai_response::claude::response::ClaudeToOpenAiResponseStream,
2203);
2204
2205impl
2206    EventConverter<
2207        crate::claude::create_message::stream::ClaudeStreamEvent,
2208        crate::openai::create_response::stream::ResponseStreamEvent,
2209    > for ClaudeToOpenAiResponseConverter
2210{
2211    fn on_input(
2212        &mut self,
2213        input: crate::claude::create_message::stream::ClaudeStreamEvent,
2214        out: &mut Vec<crate::openai::create_response::stream::ResponseStreamEvent>,
2215    ) -> Result<(), TransformError> {
2216        self.0
2217            .on_event(input, out)
2218            .map_err(|e| TransformError::new(format!("stream transform failed: {e}")))
2219    }
2220
2221    fn finish(
2222        &mut self,
2223        out: &mut Vec<crate::openai::create_response::stream::ResponseStreamEvent>,
2224    ) -> Result<(), TransformError> {
2225        self.0.finish(out);
2226        Ok(())
2227    }
2228}
2229
2230#[derive(Default)]
2231struct GeminiToOpenAiResponseConverter(
2232    crate::transform::openai::stream_generate_content::openai_response::gemini::response::GeminiToOpenAiResponseStream,
2233);
2234
2235impl
2236    EventConverter<
2237        crate::gemini::generate_content::response::ResponseBody,
2238        crate::openai::create_response::stream::ResponseStreamEvent,
2239    > for GeminiToOpenAiResponseConverter
2240{
2241    fn on_input(
2242        &mut self,
2243        input: crate::gemini::generate_content::response::ResponseBody,
2244        out: &mut Vec<crate::openai::create_response::stream::ResponseStreamEvent>,
2245    ) -> Result<(), TransformError> {
2246        self.0.on_chunk(input, out);
2247        Ok(())
2248    }
2249
2250    fn finish(
2251        &mut self,
2252        out: &mut Vec<crate::openai::create_response::stream::ResponseStreamEvent>,
2253    ) -> Result<(), TransformError> {
2254        self.0.finish(out);
2255        Ok(())
2256    }
2257}
2258
2259#[derive(Default)]
2260struct ClaudeToGeminiConverter(
2261    crate::transform::gemini::stream_generate_content::claude::response::ClaudeToGeminiStream,
2262);
2263
2264impl
2265    EventConverter<
2266        crate::claude::create_message::stream::ClaudeStreamEvent,
2267        crate::gemini::generate_content::response::ResponseBody,
2268    > for ClaudeToGeminiConverter
2269{
2270    fn on_input(
2271        &mut self,
2272        input: crate::claude::create_message::stream::ClaudeStreamEvent,
2273        out: &mut Vec<crate::gemini::generate_content::response::ResponseBody>,
2274    ) -> Result<(), TransformError> {
2275        self.0
2276            .on_event(input, out)
2277            .map_err(|e| TransformError::new(format!("stream transform failed: {e}")))
2278    }
2279
2280    fn finish(
2281        &mut self,
2282        _out: &mut Vec<crate::gemini::generate_content::response::ResponseBody>,
2283    ) -> Result<(), TransformError> {
2284        Ok(())
2285    }
2286}
2287
2288#[derive(Default)]
2289struct OpenAiChatToGeminiConverter(
2290    crate::transform::gemini::stream_generate_content::openai_chat_completions::response::OpenAiChatCompletionsToGeminiStream,
2291);
2292
2293impl
2294    EventConverter<
2295        crate::openai::create_chat_completions::stream::ChatCompletionChunk,
2296        crate::gemini::generate_content::response::ResponseBody,
2297    > for OpenAiChatToGeminiConverter
2298{
2299    fn on_input(
2300        &mut self,
2301        input: crate::openai::create_chat_completions::stream::ChatCompletionChunk,
2302        out: &mut Vec<crate::gemini::generate_content::response::ResponseBody>,
2303    ) -> Result<(), TransformError> {
2304        self.0.on_chunk(input, out);
2305        Ok(())
2306    }
2307
2308    fn finish(
2309        &mut self,
2310        out: &mut Vec<crate::gemini::generate_content::response::ResponseBody>,
2311    ) -> Result<(), TransformError> {
2312        self.0.finish(out);
2313        Ok(())
2314    }
2315}
2316
2317#[derive(Default)]
2318struct OpenAiResponseToGeminiConverter(
2319    crate::transform::gemini::stream_generate_content::openai_response::response::OpenAiResponseToGeminiStream,
2320);
2321
2322impl
2323    EventConverter<
2324        crate::openai::create_response::stream::ResponseStreamEvent,
2325        crate::gemini::generate_content::response::ResponseBody,
2326    > for OpenAiResponseToGeminiConverter
2327{
2328    fn on_input(
2329        &mut self,
2330        input: crate::openai::create_response::stream::ResponseStreamEvent,
2331        out: &mut Vec<crate::gemini::generate_content::response::ResponseBody>,
2332    ) -> Result<(), TransformError> {
2333        self.0.on_stream_event(input, out);
2334        Ok(())
2335    }
2336
2337    fn finish(
2338        &mut self,
2339        out: &mut Vec<crate::gemini::generate_content::response::ResponseBody>,
2340    ) -> Result<(), TransformError> {
2341        self.0.finish(out);
2342        Ok(())
2343    }
2344}
2345
2346/// Stream converter for `OpenAI Responses stream` → `OpenAI Chat Completions stream`.
2347///
2348/// Used by the codex channel which forwards chat-completions traffic as
2349/// OpenAI Response streams upstream and must reverse the protocol on the
2350/// way back to the client. The wrapped stream converter lives in
2351/// `crate::transform::openai::stream_generate_content`.
2352#[derive(Default)]
2353struct OpenAiResponseToOpenAiChatCompletionsConverter(
2354    crate::transform::openai::stream_generate_content::openai_chat_completions::openai_response::response::OpenAiResponseToOpenAiChatCompletionsStream,
2355);
2356
2357impl
2358    EventConverter<
2359        crate::openai::create_response::stream::ResponseStreamEvent,
2360        crate::openai::create_chat_completions::stream::ChatCompletionChunk,
2361    > for OpenAiResponseToOpenAiChatCompletionsConverter
2362{
2363    fn on_input(
2364        &mut self,
2365        input: crate::openai::create_response::stream::ResponseStreamEvent,
2366        out: &mut Vec<crate::openai::create_chat_completions::stream::ChatCompletionChunk>,
2367    ) -> Result<(), TransformError> {
2368        self.0
2369            .on_stream_event(input, out)
2370            .map_err(|e| TransformError::new(format!("stream convert: {e}")))
2371    }
2372
2373    fn finish(
2374        &mut self,
2375        out: &mut Vec<crate::openai::create_chat_completions::stream::ChatCompletionChunk>,
2376    ) -> Result<(), TransformError> {
2377        self.0
2378            .finish(out)
2379            .map_err(|e| TransformError::new(format!("stream finish: {e}")))
2380    }
2381}
2382
2383/// Stream converter for `OpenAI Chat Completions stream` → `OpenAI Responses stream`.
2384///
2385/// The reverse of `OpenAiResponseToOpenAiChatCompletionsConverter`,
2386/// used when clients speak OpenAI Response but the upstream channel
2387/// only exposes chat completions (deepseek, groq, nvidia, etc.).
2388#[derive(Default)]
2389struct OpenAiChatCompletionsToOpenAiResponseConverter(
2390    crate::transform::openai::stream_generate_content::openai_response::openai_chat_completions::response::OpenAiChatCompletionsToOpenAiResponseStream,
2391);
2392
2393impl
2394    EventConverter<
2395        crate::openai::create_chat_completions::stream::ChatCompletionChunk,
2396        crate::openai::create_response::stream::ResponseStreamEvent,
2397    > for OpenAiChatCompletionsToOpenAiResponseConverter
2398{
2399    fn on_input(
2400        &mut self,
2401        input: crate::openai::create_chat_completions::stream::ChatCompletionChunk,
2402        out: &mut Vec<crate::openai::create_response::stream::ResponseStreamEvent>,
2403    ) -> Result<(), TransformError> {
2404        self.0
2405            .on_stream_event(input, out)
2406            .map_err(|e| TransformError::new(format!("stream convert: {e}")))
2407    }
2408
2409    fn finish(
2410        &mut self,
2411        out: &mut Vec<crate::openai::create_response::stream::ResponseStreamEvent>,
2412    ) -> Result<(), TransformError> {
2413        self.0
2414            .finish(out)
2415            .map_err(|e| TransformError::new(format!("stream finish: {e}")))
2416    }
2417}
2418
2419#[derive(Default)]
2420struct ResponseStreamToImageStreamConverter(
2421    crate::transform::openai::create_image::openai_response::stream::ResponseStreamToImageStream,
2422);
2423
2424impl
2425    EventConverter<
2426        crate::openai::create_response::stream::ResponseStreamEvent,
2427        crate::openai::create_image::stream::ImageGenerationStreamEvent,
2428    > for ResponseStreamToImageStreamConverter
2429{
2430    fn on_input(
2431        &mut self,
2432        input: crate::openai::create_response::stream::ResponseStreamEvent,
2433        out: &mut Vec<crate::openai::create_image::stream::ImageGenerationStreamEvent>,
2434    ) -> Result<(), TransformError> {
2435        self.0.on_event(input, out);
2436        Ok(())
2437    }
2438
2439    fn finish(
2440        &mut self,
2441        out: &mut Vec<crate::openai::create_image::stream::ImageGenerationStreamEvent>,
2442    ) -> Result<(), TransformError> {
2443        self.0.finish(out);
2444        Ok(())
2445    }
2446}
2447
2448#[derive(Default)]
2449struct GeminiToImageStreamConverter {
2450    partial_count: u32,
2451}
2452
2453impl
2454    EventConverter<
2455        crate::gemini::generate_content::response::ResponseBody,
2456        crate::openai::create_image::stream::ImageGenerationStreamEvent,
2457    > for GeminiToImageStreamConverter
2458{
2459    fn on_input(
2460        &mut self,
2461        input: crate::gemini::generate_content::response::ResponseBody,
2462        out: &mut Vec<crate::openai::create_image::stream::ImageGenerationStreamEvent>,
2463    ) -> Result<(), TransformError> {
2464        use crate::openai::create_image::stream::ImageGenerationStreamEvent;
2465        use crate::transform::openai::create_image::gemini::utils::{
2466            best_effort_openai_image_usage_from_gemini, gemini_inline_image_outputs_from_response,
2467        };
2468
2469        let is_finished = input
2470            .candidates
2471            .as_ref()
2472            .and_then(|cs| cs.first())
2473            .and_then(|c| c.finish_reason.as_ref())
2474            .is_some();
2475
2476        let images = gemini_inline_image_outputs_from_response(&input);
2477        let usage_metadata = input.usage_metadata.as_ref();
2478
2479        for img in &images {
2480            if is_finished {
2481                out.push(ImageGenerationStreamEvent::Completed {
2482                    b64_json: img.b64_json.clone(),
2483                    background: crate::openai::create_image::types::OpenAiImageBackground::Auto,
2484                    created_at: 0,
2485                    output_format: img.output_format.clone(),
2486                    quality: crate::openai::create_image::types::OpenAiImageQuality::Auto,
2487                    size: crate::openai::create_image::types::OpenAiImageSize::Auto,
2488                    usage: best_effort_openai_image_usage_from_gemini(usage_metadata),
2489                });
2490            } else {
2491                let index = self.partial_count;
2492                self.partial_count += 1;
2493                out.push(ImageGenerationStreamEvent::PartialImage {
2494                    b64_json: img.b64_json.clone(),
2495                    background: crate::openai::create_image::types::OpenAiImageBackground::Auto,
2496                    created_at: 0,
2497                    output_format: img.output_format.clone(),
2498                    partial_image_index: index,
2499                    quality: crate::openai::create_image::types::OpenAiImageQuality::Auto,
2500                    size: crate::openai::create_image::types::OpenAiImageSize::Auto,
2501                });
2502            }
2503        }
2504        Ok(())
2505    }
2506
2507    fn finish(
2508        &mut self,
2509        _out: &mut Vec<crate::openai::create_image::stream::ImageGenerationStreamEvent>,
2510    ) -> Result<(), TransformError> {
2511        Ok(())
2512    }
2513}
2514
2515fn build_stream_transform<Input, Output, Converter>(
2516    src_protocol: ProtocolKind,
2517    dst_protocol: ProtocolKind,
2518    converter: Converter,
2519    normalizer: Option<StreamChunkNormalizer>,
2520) -> Result<StreamResponseTransformer, TransformError>
2521where
2522    Input: DeserializeOwned + Send + 'static,
2523    Output: Serialize + Send + 'static,
2524    Converter: EventConverter<Input, Output> + Send + 'static,
2525{
2526    Ok(StreamResponseTransformer {
2527        decoder: StreamChunkDecoder::from_protocol(dst_protocol)?,
2528        inner: Box::new(TypedStreamTransform::<Input, Output, Converter> {
2529            converter,
2530            encoder: StreamChunkEncoder::from_protocol(src_protocol)?,
2531            _marker: PhantomData,
2532        }),
2533        normalizer,
2534    })
2535}
2536
2537pub fn create_stream_response_transformer(
2538    src_operation: OperationFamily,
2539    src_protocol: ProtocolKind,
2540    dst_operation: OperationFamily,
2541    dst_protocol: ProtocolKind,
2542    normalizer: Option<StreamChunkNormalizer>,
2543) -> Result<StreamResponseTransformer, TransformError> {
2544    let key = (src_operation, src_protocol, dst_operation, dst_protocol);
2545
2546    match key {
2547        (
2548            OperationFamily::StreamGenerateContent,
2549            ProtocolKind::Claude,
2550            OperationFamily::StreamGenerateContent,
2551            ProtocolKind::Claude,
2552        ) => build_stream_transform::<
2553            crate::claude::create_message::stream::ClaudeStreamEvent,
2554            crate::claude::create_message::stream::ClaudeStreamEvent,
2555            IdentityConverter<crate::claude::create_message::stream::ClaudeStreamEvent>,
2556        >(
2557            src_protocol,
2558            dst_protocol,
2559            IdentityConverter::default(),
2560            normalizer,
2561        ),
2562        (
2563            OperationFamily::StreamGenerateContent,
2564            ProtocolKind::OpenAiChatCompletion,
2565            OperationFamily::StreamGenerateContent,
2566            ProtocolKind::OpenAiChatCompletion,
2567        ) => build_stream_transform::<
2568            crate::openai::create_chat_completions::stream::ChatCompletionChunk,
2569            crate::openai::create_chat_completions::stream::ChatCompletionChunk,
2570            IdentityConverter<crate::openai::create_chat_completions::stream::ChatCompletionChunk>,
2571        >(
2572            src_protocol,
2573            dst_protocol,
2574            IdentityConverter::default(),
2575            normalizer,
2576        ),
2577        (
2578            OperationFamily::StreamGenerateContent,
2579            ProtocolKind::OpenAiResponse,
2580            OperationFamily::StreamGenerateContent,
2581            ProtocolKind::OpenAiResponse,
2582        ) => build_stream_transform::<
2583            crate::openai::create_response::stream::ResponseStreamEvent,
2584            crate::openai::create_response::stream::ResponseStreamEvent,
2585            IdentityConverter<crate::openai::create_response::stream::ResponseStreamEvent>,
2586        >(
2587            src_protocol,
2588            dst_protocol,
2589            IdentityConverter::default(),
2590            normalizer,
2591        ),
2592        (
2593            OperationFamily::StreamGenerateContent,
2594            ProtocolKind::Gemini,
2595            OperationFamily::StreamGenerateContent,
2596            ProtocolKind::Gemini,
2597        )
2598        | (
2599            OperationFamily::StreamGenerateContent,
2600            ProtocolKind::Gemini,
2601            OperationFamily::StreamGenerateContent,
2602            ProtocolKind::GeminiNDJson,
2603        )
2604        | (
2605            OperationFamily::StreamGenerateContent,
2606            ProtocolKind::GeminiNDJson,
2607            OperationFamily::StreamGenerateContent,
2608            ProtocolKind::Gemini,
2609        )
2610        | (
2611            OperationFamily::StreamGenerateContent,
2612            ProtocolKind::GeminiNDJson,
2613            OperationFamily::StreamGenerateContent,
2614            ProtocolKind::GeminiNDJson,
2615        ) => build_stream_transform::<
2616            crate::gemini::generate_content::response::ResponseBody,
2617            crate::gemini::generate_content::response::ResponseBody,
2618            IdentityConverter<crate::gemini::generate_content::response::ResponseBody>,
2619        >(
2620            src_protocol,
2621            dst_protocol,
2622            IdentityConverter::default(),
2623            normalizer,
2624        ),
2625
2626        (
2627            OperationFamily::StreamGenerateContent,
2628            ProtocolKind::Claude,
2629            OperationFamily::StreamGenerateContent,
2630            ProtocolKind::OpenAiChatCompletion,
2631        ) => build_stream_transform::<
2632            crate::openai::create_chat_completions::stream::ChatCompletionChunk,
2633            crate::claude::create_message::stream::ClaudeStreamEvent,
2634            OpenAiChatToClaudeConverter,
2635        >(
2636            src_protocol,
2637            dst_protocol,
2638            OpenAiChatToClaudeConverter::default(),
2639            normalizer,
2640        ),
2641        (
2642            OperationFamily::StreamGenerateContent,
2643            ProtocolKind::Claude,
2644            OperationFamily::StreamGenerateContent,
2645            ProtocolKind::OpenAiResponse,
2646        ) => build_stream_transform::<
2647            crate::openai::create_response::stream::ResponseStreamEvent,
2648            crate::claude::create_message::stream::ClaudeStreamEvent,
2649            OpenAiResponseToClaudeConverter,
2650        >(
2651            src_protocol,
2652            dst_protocol,
2653            OpenAiResponseToClaudeConverter::default(),
2654            normalizer,
2655        ),
2656        (
2657            OperationFamily::StreamGenerateContent,
2658            ProtocolKind::Claude,
2659            OperationFamily::StreamGenerateContent,
2660            ProtocolKind::Gemini,
2661        )
2662        | (
2663            OperationFamily::StreamGenerateContent,
2664            ProtocolKind::Claude,
2665            OperationFamily::StreamGenerateContent,
2666            ProtocolKind::GeminiNDJson,
2667        ) => build_stream_transform::<
2668            crate::gemini::generate_content::response::ResponseBody,
2669            crate::claude::create_message::stream::ClaudeStreamEvent,
2670            GeminiToClaudeConverter,
2671        >(
2672            src_protocol,
2673            dst_protocol,
2674            GeminiToClaudeConverter::default(),
2675            normalizer,
2676        ),
2677
2678        (
2679            OperationFamily::StreamGenerateContent,
2680            ProtocolKind::OpenAiChatCompletion,
2681            OperationFamily::StreamGenerateContent,
2682            ProtocolKind::Claude,
2683        ) => build_stream_transform::<
2684            crate::claude::create_message::stream::ClaudeStreamEvent,
2685            crate::openai::create_chat_completions::stream::ChatCompletionChunk,
2686            ClaudeToOpenAiChatConverter,
2687        >(
2688            src_protocol,
2689            dst_protocol,
2690            ClaudeToOpenAiChatConverter::default(),
2691            normalizer,
2692        ),
2693        (
2694            OperationFamily::StreamGenerateContent,
2695            ProtocolKind::OpenAiChatCompletion,
2696            OperationFamily::StreamGenerateContent,
2697            ProtocolKind::Gemini,
2698        )
2699        | (
2700            OperationFamily::StreamGenerateContent,
2701            ProtocolKind::OpenAiChatCompletion,
2702            OperationFamily::StreamGenerateContent,
2703            ProtocolKind::GeminiNDJson,
2704        ) => build_stream_transform::<
2705            crate::gemini::generate_content::response::ResponseBody,
2706            crate::openai::create_chat_completions::stream::ChatCompletionChunk,
2707            GeminiToOpenAiChatConverter,
2708        >(
2709            src_protocol,
2710            dst_protocol,
2711            GeminiToOpenAiChatConverter::default(),
2712            normalizer,
2713        ),
2714
2715        // OpenAI Responses stream → OpenAI Chat Completions stream.
2716        //
2717        // This arm exists for providers like codex which forward chat-completions
2718        // traffic as OpenAI Responses upstream; the client expects chat chunks
2719        // back so we reverse the protocol on the response path.
2720        (
2721            OperationFamily::StreamGenerateContent,
2722            ProtocolKind::OpenAiChatCompletion,
2723            OperationFamily::StreamGenerateContent,
2724            ProtocolKind::OpenAiResponse,
2725        ) => build_stream_transform::<
2726            crate::openai::create_response::stream::ResponseStreamEvent,
2727            crate::openai::create_chat_completions::stream::ChatCompletionChunk,
2728            OpenAiResponseToOpenAiChatCompletionsConverter,
2729        >(
2730            src_protocol,
2731            dst_protocol,
2732            OpenAiResponseToOpenAiChatCompletionsConverter::default(),
2733            normalizer,
2734        ),
2735
2736        // OpenAI Chat Completions stream → OpenAI Responses stream.
2737        //
2738        // Mirror of the arm above, used by channels that only expose the
2739        // chat completions surface but advertise the Response protocol to
2740        // clients — deepseek, groq, nvidia, etc. Client speaks OpenAI
2741        // Response, upstream returns chat chunks, and this converter
2742        // folds them back into Response stream events.
2743        (
2744            OperationFamily::StreamGenerateContent,
2745            ProtocolKind::OpenAiResponse,
2746            OperationFamily::StreamGenerateContent,
2747            ProtocolKind::OpenAiChatCompletion,
2748        ) => build_stream_transform::<
2749            crate::openai::create_chat_completions::stream::ChatCompletionChunk,
2750            crate::openai::create_response::stream::ResponseStreamEvent,
2751            OpenAiChatCompletionsToOpenAiResponseConverter,
2752        >(
2753            src_protocol,
2754            dst_protocol,
2755            OpenAiChatCompletionsToOpenAiResponseConverter::default(),
2756            normalizer,
2757        ),
2758
2759        (
2760            OperationFamily::StreamGenerateContent,
2761            ProtocolKind::OpenAiResponse,
2762            OperationFamily::StreamGenerateContent,
2763            ProtocolKind::Claude,
2764        ) => build_stream_transform::<
2765            crate::claude::create_message::stream::ClaudeStreamEvent,
2766            crate::openai::create_response::stream::ResponseStreamEvent,
2767            ClaudeToOpenAiResponseConverter,
2768        >(
2769            src_protocol,
2770            dst_protocol,
2771            ClaudeToOpenAiResponseConverter::default(),
2772            normalizer,
2773        ),
2774        (
2775            OperationFamily::StreamGenerateContent,
2776            ProtocolKind::OpenAiResponse,
2777            OperationFamily::StreamGenerateContent,
2778            ProtocolKind::Gemini,
2779        )
2780        | (
2781            OperationFamily::StreamGenerateContent,
2782            ProtocolKind::OpenAiResponse,
2783            OperationFamily::StreamGenerateContent,
2784            ProtocolKind::GeminiNDJson,
2785        ) => build_stream_transform::<
2786            crate::gemini::generate_content::response::ResponseBody,
2787            crate::openai::create_response::stream::ResponseStreamEvent,
2788            GeminiToOpenAiResponseConverter,
2789        >(
2790            src_protocol,
2791            dst_protocol,
2792            GeminiToOpenAiResponseConverter::default(),
2793            normalizer,
2794        ),
2795
2796        (
2797            OperationFamily::StreamGenerateContent,
2798            ProtocolKind::Gemini,
2799            OperationFamily::StreamGenerateContent,
2800            ProtocolKind::Claude,
2801        )
2802        | (
2803            OperationFamily::StreamGenerateContent,
2804            ProtocolKind::GeminiNDJson,
2805            OperationFamily::StreamGenerateContent,
2806            ProtocolKind::Claude,
2807        ) => build_stream_transform::<
2808            crate::claude::create_message::stream::ClaudeStreamEvent,
2809            crate::gemini::generate_content::response::ResponseBody,
2810            ClaudeToGeminiConverter,
2811        >(
2812            src_protocol,
2813            dst_protocol,
2814            ClaudeToGeminiConverter::default(),
2815            normalizer,
2816        ),
2817        (
2818            OperationFamily::StreamGenerateContent,
2819            ProtocolKind::Gemini,
2820            OperationFamily::StreamGenerateContent,
2821            ProtocolKind::OpenAiChatCompletion,
2822        )
2823        | (
2824            OperationFamily::StreamGenerateContent,
2825            ProtocolKind::GeminiNDJson,
2826            OperationFamily::StreamGenerateContent,
2827            ProtocolKind::OpenAiChatCompletion,
2828        ) => build_stream_transform::<
2829            crate::openai::create_chat_completions::stream::ChatCompletionChunk,
2830            crate::gemini::generate_content::response::ResponseBody,
2831            OpenAiChatToGeminiConverter,
2832        >(
2833            src_protocol,
2834            dst_protocol,
2835            OpenAiChatToGeminiConverter::default(),
2836            normalizer,
2837        ),
2838        (
2839            OperationFamily::StreamGenerateContent,
2840            ProtocolKind::Gemini,
2841            OperationFamily::StreamGenerateContent,
2842            ProtocolKind::OpenAiResponse,
2843        )
2844        | (
2845            OperationFamily::StreamGenerateContent,
2846            ProtocolKind::GeminiNDJson,
2847            OperationFamily::StreamGenerateContent,
2848            ProtocolKind::OpenAiResponse,
2849        ) => build_stream_transform::<
2850            crate::openai::create_response::stream::ResponseStreamEvent,
2851            crate::gemini::generate_content::response::ResponseBody,
2852            OpenAiResponseToGeminiConverter,
2853        >(
2854            src_protocol,
2855            dst_protocol,
2856            OpenAiResponseToGeminiConverter::default(),
2857            normalizer,
2858        ),
2859
2860        // =====================================================================
2861        // stream_create_image / stream_create_image_edit → openai_response
2862        // =====================================================================
2863        (
2864            OperationFamily::StreamCreateImage,
2865            ProtocolKind::OpenAi,
2866            OperationFamily::StreamGenerateContent,
2867            ProtocolKind::OpenAiResponse,
2868        )
2869        | (
2870            OperationFamily::StreamCreateImageEdit,
2871            ProtocolKind::OpenAi,
2872            OperationFamily::StreamGenerateContent,
2873            ProtocolKind::OpenAiResponse,
2874        ) => build_stream_transform::<
2875            crate::openai::create_response::stream::ResponseStreamEvent,
2876            crate::openai::create_image::stream::ImageGenerationStreamEvent,
2877            ResponseStreamToImageStreamConverter,
2878        >(
2879            src_protocol,
2880            dst_protocol,
2881            ResponseStreamToImageStreamConverter::default(),
2882            normalizer,
2883        ),
2884
2885        (
2886            OperationFamily::StreamCreateImage,
2887            ProtocolKind::OpenAi,
2888            OperationFamily::StreamGenerateContent,
2889            ProtocolKind::Gemini,
2890        )
2891        | (
2892            OperationFamily::StreamCreateImage,
2893            ProtocolKind::OpenAi,
2894            OperationFamily::StreamGenerateContent,
2895            ProtocolKind::GeminiNDJson,
2896        )
2897        | (
2898            OperationFamily::StreamCreateImageEdit,
2899            ProtocolKind::OpenAi,
2900            OperationFamily::StreamGenerateContent,
2901            ProtocolKind::Gemini,
2902        )
2903        | (
2904            OperationFamily::StreamCreateImageEdit,
2905            ProtocolKind::OpenAi,
2906            OperationFamily::StreamGenerateContent,
2907            ProtocolKind::GeminiNDJson,
2908        ) => build_stream_transform::<
2909            crate::gemini::generate_content::response::ResponseBody,
2910            crate::openai::create_image::stream::ImageGenerationStreamEvent,
2911            GeminiToImageStreamConverter,
2912        >(
2913            src_protocol,
2914            dst_protocol,
2915            GeminiToImageStreamConverter::default(),
2916            normalizer,
2917        ),
2918
2919        _ => Err(TransformError::new(format!(
2920            "no stream response transform from upstream ({}, {}) to client ({}, {})",
2921            dst_operation, dst_protocol, src_operation, src_protocol
2922        ))),
2923    }
2924}
2925
2926// =====================================================================
2927// Nonstream ↔ Stream conversions (same protocol, format change)
2928// =====================================================================
2929
2930/// Convert a non-streaming response to stream events (same protocol).
2931/// Output is NDJSON (one JSON line per event) written into `out`.
2932pub fn nonstream_to_stream(
2933    protocol: ProtocolKind,
2934    body: &[u8],
2935    out: &mut Vec<u8>,
2936) -> Result<(), TransformError> {
2937    match protocol {
2938        ProtocolKind::Claude => {
2939            use crate::claude::create_message::response::ClaudeCreateMessageResponse;
2940            use crate::claude::create_message::stream::ClaudeStreamEvent;
2941            use crate::transform::claude::nonstream_to_stream::nonstream_to_stream;
2942
2943            let response: ClaudeCreateMessageResponse = serde_json::from_slice(body)
2944                .map_err(|e| TransformError::new(format!("deserialize: {e}")))?;
2945
2946            let mut events: Vec<ClaudeStreamEvent> = Vec::new();
2947            nonstream_to_stream(response, &mut events)
2948                .map_err(|e| TransformError::new(format!("nonstream_to_stream: {e}")))?;
2949
2950            for event in &events {
2951                let json = serde_json::to_vec(event)
2952                    .map_err(|e| TransformError::new(format!("serialize event: {e}")))?;
2953                out.extend_from_slice(&json);
2954                out.push(b'\n');
2955            }
2956            Ok(())
2957        }
2958        ProtocolKind::OpenAiChatCompletion => {
2959            use crate::openai::create_chat_completions::response::OpenAiChatCompletionsResponse;
2960            use crate::openai::create_chat_completions::stream::ChatCompletionChunk;
2961
2962            let response: OpenAiChatCompletionsResponse = serde_json::from_slice(body)
2963                .map_err(|e| TransformError::new(format!("deserialize: {e}")))?;
2964
2965            let chunks = Vec::<ChatCompletionChunk>::try_from(response)
2966                .map_err(|e| TransformError::new(format!("nonstream_to_stream: {e}")))?;
2967
2968            for chunk in &chunks {
2969                let json = serde_json::to_vec(chunk)
2970                    .map_err(|e| TransformError::new(format!("serialize chunk: {e}")))?;
2971                out.extend_from_slice(&json);
2972                out.push(b'\n');
2973            }
2974            Ok(())
2975        }
2976        ProtocolKind::OpenAiResponse => {
2977            use crate::openai::create_response::response::OpenAiCreateResponseResponse;
2978            use crate::openai::create_response::stream::ResponseStreamEvent;
2979
2980            let response: OpenAiCreateResponseResponse = serde_json::from_slice(body)
2981                .map_err(|e| TransformError::new(format!("deserialize: {e}")))?;
2982
2983            let events = Vec::<ResponseStreamEvent>::try_from(response)
2984                .map_err(|e| TransformError::new(format!("nonstream_to_stream: {e}")))?;
2985
2986            for event in &events {
2987                let json = serde_json::to_vec(event)
2988                    .map_err(|e| TransformError::new(format!("serialize event: {e}")))?;
2989                out.extend_from_slice(&json);
2990                out.push(b'\n');
2991            }
2992            Ok(())
2993        }
2994        ProtocolKind::Gemini => {
2995            use crate::gemini::generate_content::response::GeminiGenerateContentResponse;
2996
2997            let response: GeminiGenerateContentResponse = serde_json::from_slice(body)
2998                .map_err(|e| TransformError::new(format!("deserialize: {e}")))?;
2999
3000            // Gemini non-stream and stream share the same chunk body shape
3001            if let GeminiGenerateContentResponse::Success { body: resp, .. } = response {
3002                let json = serde_json::to_vec(&resp)
3003                    .map_err(|e| TransformError::new(format!("serialize chunk: {e}")))?;
3004                out.extend_from_slice(&json);
3005                out.push(b'\n');
3006            }
3007            Ok(())
3008        }
3009        _ => Err(TransformError::new(format!(
3010            "no nonstream_to_stream for protocol: {protocol}"
3011        ))),
3012    }
3013}
3014
3015/// Convert stream events (NDJSON lines) to a non-streaming response (same protocol).
3016pub fn stream_to_nonstream(
3017    protocol: ProtocolKind,
3018    chunks: &[&[u8]],
3019) -> Result<Vec<u8>, TransformError> {
3020    match protocol {
3021        ProtocolKind::Claude => {
3022            use crate::claude::create_message::response::ClaudeCreateMessageResponse;
3023            use crate::claude::create_message::stream::ClaudeStreamEvent;
3024
3025            let events: Vec<ClaudeStreamEvent> = chunks
3026                .iter()
3027                .map(|c| serde_json::from_slice(c))
3028                .collect::<Result<_, _>>()
3029                .map_err(|e| TransformError::new(format!("deserialize events: {e}")))?;
3030
3031            let response = ClaudeCreateMessageResponse::try_from(events)
3032                .map_err(|e| TransformError::new(format!("stream_to_nonstream: {e}")))?;
3033
3034            // Emit only the inner body — callers of `stream_to_nonstream`
3035            // expect raw HTTP body shape, not the internal
3036            // `{stats_code, headers, body}` wrapper envelope.
3037            response.into_body_bytes()
3038        }
3039        ProtocolKind::OpenAiChatCompletion => {
3040            use crate::openai::create_chat_completions::response::OpenAiChatCompletionsResponse;
3041            use crate::openai::create_chat_completions::stream::ChatCompletionChunk;
3042
3043            let chunks_parsed: Vec<ChatCompletionChunk> = chunks
3044                .iter()
3045                .map(|c| serde_json::from_slice(c))
3046                .collect::<Result<_, _>>()
3047                .map_err(|e| TransformError::new(format!("deserialize chunks: {e}")))?;
3048
3049            let response = OpenAiChatCompletionsResponse::try_from(chunks_parsed)
3050                .map_err(|e| TransformError::new(format!("stream_to_nonstream: {e}")))?;
3051
3052            response.into_body_bytes()
3053        }
3054        ProtocolKind::OpenAiResponse => {
3055            use crate::openai::create_response::response::OpenAiCreateResponseResponse;
3056            use crate::openai::create_response::stream::ResponseStreamEvent;
3057
3058            let events: Vec<ResponseStreamEvent> = chunks
3059                .iter()
3060                .map(|c| serde_json::from_slice(c))
3061                .collect::<Result<_, _>>()
3062                .map_err(|e| TransformError::new(format!("deserialize events: {e}")))?;
3063
3064            let response = OpenAiCreateResponseResponse::try_from(events)
3065                .map_err(|e| TransformError::new(format!("stream_to_nonstream: {e}")))?;
3066
3067            response.into_body_bytes()
3068        }
3069        ProtocolKind::Gemini | ProtocolKind::GeminiNDJson => {
3070            use crate::gemini::generate_content::response::ResponseBody;
3071            use crate::gemini::generate_content::types::GeminiCandidate;
3072            use std::collections::BTreeMap;
3073
3074            let mut merged = ResponseBody::default();
3075            let mut candidate_map: BTreeMap<u32, GeminiCandidate> = BTreeMap::new();
3076
3077            for chunk in chunks {
3078                let body: ResponseBody = serde_json::from_slice(chunk)
3079                    .map_err(|e| TransformError::new(format!("deserialize chunk: {e}")))?;
3080                crate::transform::gemini::stream_to_nonstream::merge_chunk(
3081                    &mut merged,
3082                    &mut candidate_map,
3083                    body,
3084                );
3085            }
3086
3087            let body =
3088                crate::transform::gemini::stream_to_nonstream::finalize_body(merged, candidate_map);
3089
3090            serde_json::to_vec(&body).map_err(|e| TransformError::new(format!("serialize: {e}")))
3091        }
3092        _ => Err(TransformError::new(format!(
3093            "no stream_to_nonstream for protocol: {protocol}"
3094        ))),
3095    }
3096}
3097
3098#[cfg(test)]
3099mod tests {
3100    use crate::kinds::{OperationFamily, ProtocolKind};
3101    use serde_json::{Value, json};
3102
3103    use super::{
3104        convert_error_body_or_raw, transform_request, transform_response, translate_request_query,
3105    };
3106
3107    #[test]
3108    fn transform_request_supports_openai_chat_to_openai_response() {
3109        let body = br#"{
3110          "model": "gpt-5.4",
3111          "messages": [
3112            { "role": "user", "content": "reply ok" }
3113          ],
3114          "stream": false
3115        }"#
3116        .to_vec();
3117
3118        let (_qout, transformed) = transform_request(
3119            OperationFamily::GenerateContent,
3120            ProtocolKind::OpenAiChatCompletion,
3121            OperationFamily::GenerateContent,
3122            ProtocolKind::OpenAiResponse,
3123            None,
3124            None,
3125            body,
3126        )
3127        .expect("chat -> response request transform should succeed");
3128
3129        let json: Value = serde_json::from_slice(&transformed).expect("transformed json");
3130        assert_eq!(json.get("model").and_then(Value::as_str), Some("gpt-5.4"));
3131        assert!(json.get("input").is_some());
3132    }
3133
3134    /// Regression test for the request envelope bug: request wrapper structs
3135    /// such as `ClaudeCountTokensRequest` have shape
3136    /// `{method, path, query, headers, body}` for internal routing purposes.
3137    /// A real HTTP request body only has the body fields at the top level, so
3138    /// `serde_json::from_slice::<Wrapper>(body)` fails with "missing field
3139    /// method". The fix: route all envelope-shaped request types through
3140    /// `transform_request_descriptor`, which parses just the inner body JSON
3141    /// and reconstructs the envelope with `Default::default()`. Covers
3142    /// count_tokens (Claude→Gemini) as the user-visible case that failed with
3143    /// `POST /aistudio/v1/messages/count-tokens`.
3144    #[test]
3145    fn transform_request_count_tokens_claude_to_gemini_accepts_bare_body() {
3146        let body = br#"{
3147          "model": "gemini-3-flash-preview",
3148          "messages": [{"role": "user", "content": "hi"}]
3149        }"#
3150        .to_vec();
3151
3152        let (_qout, transformed) = transform_request(
3153            OperationFamily::CountToken,
3154            ProtocolKind::Claude,
3155            OperationFamily::CountToken,
3156            ProtocolKind::Gemini,
3157            None,
3158            None,
3159            body,
3160        )
3161        .expect("count_tokens Claude -> Gemini request transform should succeed");
3162
3163        // Output should be the Gemini countTokens body (contents field).
3164        let json: Value = serde_json::from_slice(&transformed).expect("transformed json");
3165        assert!(
3166            json.get("contents").is_some()
3167                || json.pointer("/generateContentRequest/contents").is_some(),
3168            "expected gemini countTokens body shape, got: {json}"
3169        );
3170        // Must NOT leak the envelope fields back out.
3171        assert!(
3172            json.get("method").is_none(),
3173            "transformed body leaked the request envelope method field"
3174        );
3175    }
3176
3177    /// Regression test for the non-stream response transform bug: upstream
3178    /// HTTP bodies are raw JSON like `{"id": "...", "output": [...]}`, not
3179    /// a `{stats_code, headers, body: {...}}` envelope. The wrapper enums in
3180    /// `crate::*::response` have the envelope shape for internal
3181    /// bookkeeping, so deserializing the raw body directly into the wrapper
3182    /// fails. `transform_json` must call [`BodyEnvelope::from_body_bytes`] to
3183    /// parse only the inner body and wrap it with placeholder metadata.
3184    ///
3185    /// Covers OpenAI Response → OpenAI ChatCompletions, the case the
3186    /// previous `transform_openai_response_wrapper_to_chat_completions`
3187    /// helper special-cased. With the `BodyEnvelope` trait the generic
3188    /// `transform_json` handles it without a dedicated function.
3189    #[test]
3190    fn transform_response_accepts_bare_openai_response_body_for_openai_chat() {
3191        let body = serde_json::to_vec(&json!({
3192          "id": "resp_123",
3193          "created_at": 1,
3194          "metadata": {},
3195          "model": "gpt-5.4",
3196          "object": "response",
3197          "output": [
3198            {
3199              "id": "msg_0",
3200              "content": [
3201                {
3202                  "annotations": [],
3203                  "text": "OK",
3204                  "type": "output_text"
3205                }
3206              ],
3207              "role": "assistant",
3208              "status": "completed",
3209              "type": "message"
3210            }
3211          ],
3212          "parallel_tool_calls": false,
3213          "temperature": 1.0,
3214          "tool_choice": "auto",
3215          "tools": [],
3216          "top_p": 1.0
3217        }))
3218        .expect("serialize response body");
3219
3220        let transformed = transform_response(
3221            OperationFamily::GenerateContent,
3222            ProtocolKind::OpenAiChatCompletion,
3223            OperationFamily::GenerateContent,
3224            ProtocolKind::OpenAiResponse,
3225            body,
3226        )
3227        .expect("bare responses body should now be accepted");
3228
3229        let json: Value = serde_json::from_slice(&transformed).expect("transformed json");
3230        assert_eq!(json.get("model").and_then(Value::as_str), Some("gpt-5.4"));
3231        assert_eq!(
3232            json.pointer("/choices/0/message/content")
3233                .and_then(Value::as_str),
3234            Some("OK")
3235        );
3236        // The serialized response must be the raw chat-completions body, not
3237        // the internal `{stats_code, headers, body}` wrapper envelope.
3238        assert!(
3239            json.get("stats_code").is_none(),
3240            "serialized response leaked the internal wrapper envelope"
3241        );
3242    }
3243
3244    /// Regression test for the gemini → claude non-stream response
3245    /// transform. The bug surfaced when posting to
3246    /// `/aistudio/v1/messages` (Claude format over an aistudio provider):
3247    /// gproxy transformed the request to Gemini, sent it upstream, got a
3248    /// raw `{"candidates":[...], "usageMetadata":{...}}` body back, then
3249    /// tried to deserialize it as `GeminiGenerateContentResponse` (the
3250    /// wrapper enum). That produced
3251    /// `data did not match any variant of untagged enum GeminiGenerateContentResponse`
3252    /// and the client saw a 500 "upstream provider error".
3253    #[test]
3254    fn transform_response_gemini_to_claude_accepts_bare_gemini_body() {
3255        let body = serde_json::to_vec(&json!({
3256            "candidates": [{
3257                "content": {
3258                    "parts": [{"text": "Hello"}],
3259                    "role": "model"
3260                },
3261                "finishReason": "STOP",
3262                "index": 0
3263            }],
3264            "usageMetadata": {
3265                "promptTokenCount": 7,
3266                "candidatesTokenCount": 1,
3267                "totalTokenCount": 8
3268            },
3269            "modelVersion": "gemini-3-flash-preview",
3270            "responseId": "test-response-id"
3271        }))
3272        .expect("serialize gemini body");
3273
3274        let transformed = transform_response(
3275            OperationFamily::GenerateContent,
3276            ProtocolKind::Claude,
3277            OperationFamily::GenerateContent,
3278            ProtocolKind::Gemini,
3279            body,
3280        )
3281        .expect("gemini -> claude transform must accept raw gemini body");
3282
3283        let json: Value = serde_json::from_slice(&transformed).expect("transformed json");
3284        // Claude response shape: top-level `id`, `content`, `role`, `type`, etc.
3285        assert_eq!(json.get("type").and_then(Value::as_str), Some("message"));
3286        assert_eq!(json.get("role").and_then(Value::as_str), Some("assistant"));
3287        assert!(
3288            json.get("stats_code").is_none(),
3289            "serialized response leaked the internal wrapper envelope"
3290        );
3291        assert!(json.get("content").is_some(), "missing content field");
3292    }
3293
3294    /// When upstream Claude returns a standard Claude error body
3295    /// (`{"type":"error","error":{...}}`), an OpenAI chat completions
3296    /// client must see the error in OpenAI's `{"error":{...}}` shape —
3297    /// not the raw Claude JSON, which an OpenAI SDK can't parse.
3298    #[test]
3299    fn convert_error_body_claude_to_openai_chat_rewrites_schema() {
3300        let claude_error = br#"{
3301            "type": "error",
3302            "error": {
3303                "type": "invalid_request_error",
3304                "message": "prompt is too long: 1057153 tokens > 1000000 maximum"
3305            },
3306            "request_id": "req_011Ca1mHSW1W47w6LbmKQNWf"
3307        }"#
3308        .to_vec();
3309
3310        let converted = convert_error_body_or_raw(
3311            OperationFamily::StreamGenerateContent,
3312            ProtocolKind::OpenAiChatCompletion,
3313            OperationFamily::StreamGenerateContent,
3314            ProtocolKind::Claude,
3315            claude_error.clone(),
3316        );
3317
3318        let json: Value =
3319            serde_json::from_slice(&converted).expect("converted body should be valid JSON");
3320        // OpenAI error shape: top-level `error.message`, `error.type`.
3321        let error_obj = json
3322            .get("error")
3323            .expect("OpenAI error body must have top-level `error` field");
3324        assert_eq!(
3325            error_obj.get("message").and_then(Value::as_str),
3326            Some("prompt is too long: 1057153 tokens > 1000000 maximum"),
3327            "error message must survive the schema conversion"
3328        );
3329        assert!(
3330            json.get("type").and_then(Value::as_str) != Some("error"),
3331            "converted body must not still be in Claude's top-level `type:error` shape"
3332        );
3333    }
3334
3335    /// When upstream returns an error body in a shape that doesn't match
3336    /// any declared `error_body` schema (e.g. codex's
3337    /// `{"detail":{"code":"deactivated_workspace"}}`), the helper must
3338    /// fall back to forwarding the raw upstream bytes so the error
3339    /// information isn't lost.
3340    #[test]
3341    fn convert_error_body_falls_back_to_raw_on_schema_mismatch() {
3342        let codex_error = br#"{"detail":{"code":"deactivated_workspace"}}"#.to_vec();
3343
3344        let result = convert_error_body_or_raw(
3345            OperationFamily::StreamGenerateContent,
3346            ProtocolKind::OpenAiChatCompletion,
3347            OperationFamily::StreamGenerateContent,
3348            ProtocolKind::OpenAiResponse,
3349            codex_error.clone(),
3350        );
3351
3352        assert_eq!(
3353            result, codex_error,
3354            "fallback must return the raw bytes verbatim"
3355        );
3356    }
3357
3358    /// Passthrough routes (same src/dst protocol, same op) should leave
3359    /// error bodies unchanged — conversion is unnecessary and would
3360    /// only add latency.
3361    #[test]
3362    fn convert_error_body_passthrough_returns_unchanged() {
3363        let claude_error =
3364            br#"{"type":"error","error":{"type":"overloaded_error","message":"Overloaded"}}"#
3365                .to_vec();
3366
3367        let result = convert_error_body_or_raw(
3368            OperationFamily::StreamGenerateContent,
3369            ProtocolKind::Claude,
3370            OperationFamily::StreamGenerateContent,
3371            ProtocolKind::Claude,
3372            claude_error.clone(),
3373        );
3374
3375        assert_eq!(result, claude_error);
3376    }
3377
3378    #[test]
3379    fn translate_request_query_gemini_to_claude_model_list() {
3380        let translated = translate_request_query(
3381            OperationFamily::ModelList,
3382            ProtocolKind::Gemini,
3383            OperationFamily::ModelList,
3384            ProtocolKind::Claude,
3385            Some("pageSize=25&pageToken=abc"),
3386        )
3387        .expect("translated query should be present");
3388        assert!(translated.contains("limit=25"), "got: {translated}");
3389        assert!(translated.contains("after_id=abc"), "got: {translated}");
3390    }
3391
3392    #[test]
3393    fn translate_request_query_claude_to_gemini_model_list() {
3394        let translated = translate_request_query(
3395            OperationFamily::ModelList,
3396            ProtocolKind::Claude,
3397            OperationFamily::ModelList,
3398            ProtocolKind::Gemini,
3399            Some("limit=50&after_id=cursor1"),
3400        )
3401        .expect("translated query should be present");
3402        assert!(translated.contains("pageSize=50"), "got: {translated}");
3403        assert!(
3404            translated.contains("pageToken=cursor1"),
3405            "got: {translated}"
3406        );
3407    }
3408
3409    #[test]
3410    fn translate_request_query_passes_through_for_same_protocol() {
3411        let translated = translate_request_query(
3412            OperationFamily::ModelList,
3413            ProtocolKind::Gemini,
3414            OperationFamily::ModelList,
3415            ProtocolKind::Gemini,
3416            Some("pageSize=10"),
3417        );
3418        assert_eq!(translated.as_deref(), Some("pageSize=10"));
3419    }
3420
3421    #[test]
3422    fn translate_request_query_passes_through_for_non_model_list() {
3423        let translated = translate_request_query(
3424            OperationFamily::GenerateContent,
3425            ProtocolKind::Claude,
3426            OperationFamily::GenerateContent,
3427            ProtocolKind::Gemini,
3428            Some("foo=bar"),
3429        );
3430        assert_eq!(translated.as_deref(), Some("foo=bar"));
3431    }
3432
3433    #[test]
3434    fn translate_request_query_drops_unknown_keys_on_cross_protocol_model_list() {
3435        let translated = translate_request_query(
3436            OperationFamily::ModelList,
3437            ProtocolKind::Gemini,
3438            OperationFamily::ModelList,
3439            ProtocolKind::Claude,
3440            Some("pageSize=5&unknown=x"),
3441        )
3442        .expect("translated query");
3443        assert!(translated.contains("limit=5"));
3444        assert!(!translated.contains("unknown"));
3445    }
3446}