@@ -11,7 +11,7 @@ use async_trait::async_trait;
1111use futures_util:: StreamExt ;
1212use http:: StatusCode ;
1313use reqwest:: RequestBuilder ;
14- use reqwest:: header:: { HeaderMap , RETRY_AFTER } ;
14+ use reqwest:: header:: { CONTENT_TYPE , HeaderMap , HeaderValue , RETRY_AFTER } ;
1515use serde_json:: { Map , Value } ;
1616use switchyard_protocol:: {
1717 LlmRequest , LlmResponse , Metadata , ModelId , Request , Response , RoutedLlmClient ,
@@ -94,6 +94,31 @@ pub struct TranslatingLlmClient {
9494 forward_auth_client : reqwest:: Client ,
9595}
9696
97+ /// Provider-native OpenAI request proxied without translation.
98+ pub enum OpenAiPassthroughRequest {
99+ /// Count input tokens using `POST /v1/responses/input_tokens`.
100+ ResponsesInputTokens ( Value ) ,
101+ /// Compact conversation input using `POST /v1/responses/compact`.
102+ ResponsesCompact ( Value ) ,
103+ /// Upload multipart form data using `POST /v1/files`.
104+ File {
105+ /// Uninspected multipart request body.
106+ body : reqwest:: Body ,
107+ /// Multipart content type, including its boundary.
108+ content_type : HeaderValue ,
109+ } ,
110+ }
111+
112+ impl OpenAiPassthroughRequest {
113+ const fn suffix ( & self ) -> & ' static str {
114+ match self {
115+ Self :: ResponsesInputTokens ( _) => "/responses/input_tokens" ,
116+ Self :: ResponsesCompact ( _) => "/responses/compact" ,
117+ Self :: File { .. } => "/files" ,
118+ }
119+ }
120+ }
121+
97122impl TranslatingLlmClient {
98123 /// Builds a client over the given [`ModelConfig`]s, with a fresh shared HTTP
99124 /// client and the built-in translation codecs.
@@ -150,6 +175,62 @@ impl TranslatingLlmClient {
150175 . is_some ( )
151176 }
152177
178+ /// Proxies an auxiliary OpenAI request through `model`'s Responses backend.
179+ ///
180+ /// Responses JSON remains provider-native except that `model` is replaced with
181+ /// the configured upstream model id. File bodies and all responses remain uninspected.
182+ pub async fn passthrough_openai (
183+ & self ,
184+ model : & ModelId ,
185+ request : OpenAiPassthroughRequest ,
186+ metadata : Option < & Metadata > ,
187+ ) -> Result < reqwest:: Response > {
188+ let backend = self
189+ . backend_for ( model, WireFormat :: OpenAiResponses )
190+ . ok_or_else ( || LlmClientError :: Configuration {
191+ message : format ! ( "model {model} has no OpenAI Responses backend" ) ,
192+ } ) ?;
193+ let url = backend. openai_endpoint_url ( request. suffix ( ) ) ;
194+ let builder = match request {
195+ OpenAiPassthroughRequest :: ResponsesInputTokens ( mut body)
196+ | OpenAiPassthroughRequest :: ResponsesCompact ( mut body) => {
197+ if !body. is_object ( ) {
198+ return Err ( LlmClientError :: InvalidRequest {
199+ message : "request body must be a JSON object" . to_string ( ) ,
200+ } ) ;
201+ }
202+ set_json_model ( & mut body, model) ;
203+ self . http_client ( backend) . post ( url) . json ( & body)
204+ }
205+ OpenAiPassthroughRequest :: File { body, content_type } => self
206+ . http_client ( backend)
207+ . post ( url)
208+ . header ( CONTENT_TYPE , content_type)
209+ . body ( body) ,
210+ } ;
211+ let builder = forward_metadata_headers ( builder, metadata) ;
212+ let builder = backend. apply_forwarded_auth ( builder, metadata) ;
213+ let builder = apply_extra_headers ( builder, backend) ;
214+ let builder = backend. apply_auth ( builder) ;
215+ let response = match builder. send ( ) . await {
216+ Ok ( response) => response,
217+ Err ( error) => {
218+ metrics:: record_upstream_attempt ( None ) ;
219+ return Err ( convert_reqwest_error ( error) ) ;
220+ }
221+ } ;
222+ metrics:: record_upstream_attempt ( Some ( response. status ( ) . as_u16 ( ) ) ) ;
223+ Ok ( response)
224+ }
225+
226+ fn http_client ( & self , backend : & Backend ) -> & reqwest:: Client {
227+ if backend. is_forwarding_auth ( ) {
228+ & self . forward_auth_client
229+ } else {
230+ & self . client
231+ }
232+ }
233+
153234 /// Counts input tokens with `model`'s Anthropic backend.
154235 ///
155236 /// Returns an error when the model has no Anthropic backend or the upstream
@@ -298,11 +379,7 @@ impl TranslatingLlmClient {
298379 model : & ModelId ,
299380 streaming : bool ,
300381 ) -> std:: result:: Result < EncodedResponse , AttemptFailure > {
301- let client = if backend. is_forwarding_auth ( ) {
302- & self . forward_auth_client
303- } else {
304- & self . client
305- } ;
382+ let client = self . http_client ( backend) ;
306383 let builder = client. post ( url) . json ( body) ;
307384 let builder = forward_metadata_headers ( builder, metadata) ;
308385 let builder = backend. apply_forwarded_auth ( builder, metadata) ;
0 commit comments