@@ -13,6 +13,12 @@ pub fn maybe_transform_json_response(
1313 ( Some ( RequestFormat :: OpenAiChat ) , Some ( RequestFormat :: OpenAiResponses ) ) => {
1414 openai_chat_to_openai_responses_response ( body)
1515 }
16+ ( Some ( RequestFormat :: ClaudeChat ) , Some ( RequestFormat :: OpenAiResponses ) ) => {
17+ claude_to_openai_responses_response ( body)
18+ }
19+ ( Some ( RequestFormat :: GeminiChat ) , Some ( RequestFormat :: OpenAiResponses ) ) => {
20+ gemini_to_openai_responses_response ( body)
21+ }
1622 ( Some ( RequestFormat :: OpenAiChat ) , Some ( RequestFormat :: ClaudeChat ) ) => {
1723 openai_chat_to_claude_response ( body)
1824 }
@@ -262,6 +268,291 @@ fn openai_chat_to_openai_responses_response(body: Value) -> Result<Value, ApiErr
262268 Ok ( response)
263269}
264270
271+ fn claude_to_openai_responses_response ( body : Value ) -> Result < Value , ApiError > {
272+ let object = body. as_object ( ) . ok_or_else ( || {
273+ ApiError :: new ( StatusCode :: BAD_GATEWAY , "response body is not a JSON object" )
274+ . with_code ( "PROXY_ERROR" )
275+ } ) ?;
276+
277+ let content_blocks = object
278+ . get ( "content" )
279+ . and_then ( Value :: as_array)
280+ . ok_or_else ( || {
281+ ApiError :: new ( StatusCode :: BAD_GATEWAY , "claude response content is missing" )
282+ . with_code ( "PROXY_ERROR" )
283+ } ) ?;
284+
285+ let response_id = object. get ( "id" ) . and_then ( Value :: as_str) . unwrap_or ( "resp" ) ;
286+ let mut output = Vec :: new ( ) ;
287+ let mut message_content = Vec :: new ( ) ;
288+ let mut reasoning_parts = Vec :: new ( ) ;
289+
290+ for block in content_blocks. iter ( ) . filter_map ( Value :: as_object) {
291+ match block. get ( "type" ) . and_then ( Value :: as_str) {
292+ Some ( "text" ) => {
293+ if let Some ( text) = block. get ( "text" ) . and_then ( Value :: as_str) {
294+ if !text. is_empty ( ) {
295+ message_content. push ( json ! ( {
296+ "type" : "output_text" ,
297+ "text" : text
298+ } ) ) ;
299+ }
300+ }
301+ }
302+ Some ( "thinking" ) => {
303+ if let Some ( text) = block. get ( "thinking" ) . and_then ( Value :: as_str) {
304+ if !text. is_empty ( ) {
305+ reasoning_parts. push ( text. to_string ( ) ) ;
306+ }
307+ }
308+ }
309+ Some ( "tool_use" ) => {
310+ let arguments = stringify_function_arguments (
311+ block. get ( "input" ) . unwrap_or ( & Value :: Null ) ,
312+ ) ;
313+ output. push ( json ! ( {
314+ "type" : "function_call" ,
315+ "id" : block. get( "id" ) . cloned( ) . unwrap_or_else( || json!( "" ) ) ,
316+ "call_id" : block. get( "id" ) . cloned( ) . unwrap_or_else( || json!( "" ) ) ,
317+ "name" : block. get( "name" ) . cloned( ) . unwrap_or_else( || json!( "" ) ) ,
318+ "arguments" : arguments
319+ } ) ) ;
320+ }
321+ _ => { }
322+ }
323+ }
324+
325+ if !message_content. is_empty ( ) {
326+ output. insert ( 0 , json ! ( {
327+ "type" : "message" ,
328+ "id" : format!( "{response_id}_msg_0" ) ,
329+ "role" : "assistant" ,
330+ "content" : message_content
331+ } ) ) ;
332+ }
333+
334+ if !reasoning_parts. is_empty ( ) {
335+ let reasoning_text = reasoning_parts. join ( "\n " ) ;
336+ let insert_at = usize:: from ( !output. is_empty ( ) && output[ 0 ] . get ( "type" ) . and_then ( Value :: as_str) == Some ( "message" ) ) ;
337+ output. insert ( insert_at, json ! ( {
338+ "type" : "reasoning" ,
339+ "id" : format!( "{response_id}_reasoning_0" ) ,
340+ "summary" : [ {
341+ "type" : "summary_text" ,
342+ "text" : reasoning_text
343+ } ] ,
344+ "content" : [ {
345+ "type" : "reasoning_text" ,
346+ "text" : reasoning_text
347+ } ]
348+ } ) ) ;
349+ }
350+
351+ let stop_reason = object. get ( "stop_reason" ) . and_then ( Value :: as_str) ;
352+ let status = match stop_reason {
353+ Some ( "max_tokens" ) => "incomplete" ,
354+ Some ( "error" ) => "failed" ,
355+ _ => "completed" ,
356+ } ;
357+
358+ let mut response = json ! ( {
359+ "id" : object. get( "id" ) . cloned( ) . unwrap_or_else( || json!( "" ) ) ,
360+ "object" : "response" ,
361+ "model" : object. get( "model" ) . cloned( ) . unwrap_or_else( || json!( "" ) ) ,
362+ "created_at" : numeric_response_timestamp( object. get( "created_at" ) ) ,
363+ "status" : status,
364+ "output" : output
365+ } ) ;
366+
367+ if let Some ( usage) = object. get ( "usage" ) . and_then ( Value :: as_object) {
368+ let input_tokens = usage. get ( "input_tokens" ) . cloned ( ) . unwrap_or_else ( || json ! ( 0 ) ) ;
369+ let output_tokens = usage. get ( "output_tokens" ) . cloned ( ) . unwrap_or_else ( || json ! ( 0 ) ) ;
370+ let total_tokens = usage
371+ . get ( "total_tokens" )
372+ . cloned ( )
373+ . unwrap_or_else ( || {
374+ json ! (
375+ input_tokens. as_i64( ) . unwrap_or( 0 ) + output_tokens. as_i64( ) . unwrap_or( 0 )
376+ )
377+ } ) ;
378+ response[ "usage" ] = json ! ( {
379+ "input_tokens" : input_tokens,
380+ "output_tokens" : output_tokens,
381+ "total_tokens" : total_tokens
382+ } ) ;
383+ }
384+
385+ if let Some ( response_obj) = response. as_object_mut ( ) {
386+ for ( key, value) in object {
387+ if matches ! (
388+ key. as_str( ) ,
389+ "id" | "type" | "role" | "content" | "model" | "created_at" | "stop_reason" | "usage"
390+ ) {
391+ continue ;
392+ }
393+ response_obj. insert ( key. clone ( ) , value. clone ( ) ) ;
394+ }
395+ }
396+
397+ Ok ( response)
398+ }
399+
400+ fn gemini_to_openai_responses_response ( body : Value ) -> Result < Value , ApiError > {
401+ let object = body. as_object ( ) . ok_or_else ( || {
402+ ApiError :: new ( StatusCode :: BAD_GATEWAY , "response body is not a JSON object" )
403+ . with_code ( "PROXY_ERROR" )
404+ } ) ?;
405+
406+ let candidate = object
407+ . get ( "candidates" )
408+ . and_then ( Value :: as_array)
409+ . and_then ( |candidates| candidates. first ( ) )
410+ . and_then ( Value :: as_object)
411+ . ok_or_else ( || {
412+ ApiError :: new ( StatusCode :: BAD_GATEWAY , "gemini response candidates[0] is missing" )
413+ . with_code ( "PROXY_ERROR" )
414+ } ) ?;
415+
416+ let response_id = object
417+ . get ( "responseId" )
418+ . and_then ( Value :: as_str)
419+ . unwrap_or ( "gemini-response" ) ;
420+ let mut output = Vec :: new ( ) ;
421+ let mut message_content = Vec :: new ( ) ;
422+ let mut reasoning_parts = Vec :: new ( ) ;
423+
424+ if let Some ( parts) = candidate
425+ . get ( "content" )
426+ . and_then ( Value :: as_object)
427+ . and_then ( |content| content. get ( "parts" ) )
428+ . and_then ( Value :: as_array)
429+ {
430+ for part in parts. iter ( ) . filter_map ( Value :: as_object) {
431+ if part. get ( "thought" ) . and_then ( Value :: as_bool) == Some ( true ) {
432+ if let Some ( text) = part. get ( "text" ) . and_then ( Value :: as_str) {
433+ if !text. is_empty ( ) {
434+ reasoning_parts. push ( text. to_string ( ) ) ;
435+ }
436+ }
437+ continue ;
438+ }
439+
440+ if let Some ( text) = part. get ( "text" ) . and_then ( Value :: as_str) {
441+ if !text. is_empty ( ) {
442+ message_content. push ( json ! ( {
443+ "type" : "output_text" ,
444+ "text" : text
445+ } ) ) ;
446+ }
447+ continue ;
448+ }
449+
450+ if let Some ( function_call) = part. get ( "functionCall" ) . and_then ( Value :: as_object) {
451+ let call_id = function_call
452+ . get ( "id" )
453+ . cloned ( )
454+ . unwrap_or_else ( || json ! ( "gemini_call" ) ) ;
455+ output. push ( json ! ( {
456+ "type" : "function_call" ,
457+ "id" : call_id. clone( ) ,
458+ "call_id" : call_id,
459+ "name" : function_call. get( "name" ) . cloned( ) . unwrap_or_else( || json!( "" ) ) ,
460+ "arguments" : stringify_function_arguments(
461+ function_call. get( "args" ) . unwrap_or( & Value :: Null )
462+ )
463+ } ) ) ;
464+ }
465+ }
466+ }
467+
468+ if !message_content. is_empty ( ) {
469+ output. insert ( 0 , json ! ( {
470+ "type" : "message" ,
471+ "id" : format!( "{response_id}_msg_0" ) ,
472+ "role" : "assistant" ,
473+ "content" : message_content
474+ } ) ) ;
475+ }
476+
477+ if !reasoning_parts. is_empty ( ) {
478+ let reasoning_text = reasoning_parts. join ( "\n " ) ;
479+ let insert_at = usize:: from ( !output. is_empty ( ) && output[ 0 ] . get ( "type" ) . and_then ( Value :: as_str) == Some ( "message" ) ) ;
480+ output. insert ( insert_at, json ! ( {
481+ "type" : "reasoning" ,
482+ "id" : format!( "{response_id}_reasoning_0" ) ,
483+ "summary" : [ {
484+ "type" : "summary_text" ,
485+ "text" : reasoning_text
486+ } ] ,
487+ "content" : [ {
488+ "type" : "reasoning_text" ,
489+ "text" : reasoning_text
490+ } ]
491+ } ) ) ;
492+ }
493+
494+ let finish_reason = candidate. get ( "finishReason" ) . and_then ( Value :: as_str) ;
495+ let status = match finish_reason {
496+ Some ( "MAX_TOKENS" ) => "incomplete" ,
497+ Some ( "ERROR" ) => "failed" ,
498+ _ => "completed" ,
499+ } ;
500+
501+ let mut response = json ! ( {
502+ "id" : response_id,
503+ "object" : "response" ,
504+ "model" : object. get( "modelVersion" ) . cloned( ) . unwrap_or_else( || json!( "gemini" ) ) ,
505+ "created_at" : numeric_response_timestamp( object. get( "createTime" ) ) ,
506+ "status" : status,
507+ "output" : output
508+ } ) ;
509+
510+ if let Some ( usage) = object. get ( "usageMetadata" ) . and_then ( Value :: as_object) {
511+ let input_tokens = usage
512+ . get ( "promptTokenCount" )
513+ . cloned ( )
514+ . unwrap_or_else ( || json ! ( 0 ) ) ;
515+ let candidate_tokens = usage
516+ . get ( "candidatesTokenCount" )
517+ . cloned ( )
518+ . unwrap_or_else ( || json ! ( 0 ) ) ;
519+ let thought_tokens = usage
520+ . get ( "thoughtsTokenCount" )
521+ . cloned ( )
522+ . unwrap_or_else ( || json ! ( 0 ) ) ;
523+ let output_tokens = json ! (
524+ candidate_tokens. as_i64( ) . unwrap_or( 0 ) + thought_tokens. as_i64( ) . unwrap_or( 0 )
525+ ) ;
526+ let total_tokens = usage
527+ . get ( "totalTokenCount" )
528+ . cloned ( )
529+ . unwrap_or_else ( || {
530+ json ! (
531+ input_tokens. as_i64( ) . unwrap_or( 0 ) + output_tokens. as_i64( ) . unwrap_or( 0 )
532+ )
533+ } ) ;
534+ response[ "usage" ] = json ! ( {
535+ "input_tokens" : input_tokens,
536+ "output_tokens" : output_tokens,
537+ "total_tokens" : total_tokens
538+ } ) ;
539+ }
540+
541+ if let Some ( response_obj) = response. as_object_mut ( ) {
542+ for ( key, value) in object {
543+ if matches ! (
544+ key. as_str( ) ,
545+ "candidates" | "responseId" | "modelVersion" | "createTime" | "usageMetadata"
546+ ) {
547+ continue ;
548+ }
549+ response_obj. insert ( key. clone ( ) , value. clone ( ) ) ;
550+ }
551+ }
552+
553+ Ok ( response)
554+ }
555+
265556fn openai_responses_to_openai_chat ( body : Value ) -> Result < Value , ApiError > {
266557 let object = body. as_object ( ) . ok_or_else ( || {
267558 ApiError :: new ( StatusCode :: BAD_GATEWAY , "response body is not a JSON object" )
@@ -563,3 +854,11 @@ fn parse_tool_arguments(arguments: &Value) -> Value {
563854 _ => json ! ( { } ) ,
564855 }
565856}
857+
858+ fn numeric_response_timestamp ( value : Option < & Value > ) -> Value {
859+ match value {
860+ Some ( Value :: Number ( number) ) => Value :: Number ( number. clone ( ) ) ,
861+ Some ( Value :: String ( text) ) => text. parse :: < i64 > ( ) . map_or_else ( |_| json ! ( 0 ) , |value| json ! ( value) ) ,
862+ _ => json ! ( 0 ) ,
863+ }
864+ }
0 commit comments