@@ -35,20 +35,65 @@ pub fn anthropic_to_openai(
3535 if let Some ( msgs) = anthropic. get ( "messages" ) . and_then ( |m| m. as_array ( ) ) {
3636 for msg in msgs {
3737 let role = msg. get ( "role" ) . and_then ( |r| r. as_str ( ) ) . unwrap_or ( "user" ) ;
38- let content = convert_content_to_openai ( msg. get ( "content" ) ) ;
3938
4039 match role {
41- "tool" => {
42- // Anthropic tool_result → OpenAI tool message
43- let tool_use_id = msg
44- . get ( "tool_use_id" )
45- . and_then ( |v| v. as_str ( ) )
46- . unwrap_or ( "" ) ;
47- messages. push ( json ! ( {
48- "role" : "tool" ,
49- "tool_call_id" : tool_use_id,
50- "content" : content_to_string( & content) ,
51- } ) ) ;
40+ "user" => {
41+ // Anthropic user messages may contain tool_result blocks alongside text.
42+ // tool_result blocks must be extracted into separate "role: tool" messages,
43+ // while non-tool_result blocks remain in a user message.
44+ if let Some ( content_arr) = msg. get ( "content" ) . and_then ( |c| c. as_array ( ) ) {
45+ let has_tool_result = content_arr
46+ . iter ( )
47+ . any ( |b| b. get ( "type" ) . and_then ( |t| t. as_str ( ) ) == Some ( "tool_result" ) ) ;
48+
49+ if has_tool_result {
50+ let mut user_parts = Vec :: new ( ) ;
51+ for block in content_arr {
52+ match block. get ( "type" ) . and_then ( |t| t. as_str ( ) ) {
53+ Some ( "tool_result" ) => {
54+ let call_id = block
55+ . get ( "tool_use_id" )
56+ . and_then ( |v| v. as_str ( ) )
57+ . unwrap_or ( "" ) ;
58+ let result_text = extract_tool_result_content ( block) ;
59+ messages. push ( json ! ( {
60+ "role" : "tool" ,
61+ "tool_call_id" : call_id,
62+ "content" : result_text,
63+ } ) ) ;
64+ }
65+ Some ( "text" ) => {
66+ if let Some ( text) =
67+ block. get ( "text" ) . and_then ( |t| t. as_str ( ) )
68+ {
69+ if !text. is_empty ( ) {
70+ user_parts. push ( text. to_string ( ) ) ;
71+ }
72+ }
73+ }
74+ _ => { }
75+ }
76+ }
77+ if !user_parts. is_empty ( ) {
78+ messages. push ( json ! ( {
79+ "role" : "user" ,
80+ "content" : user_parts. join( "\n " ) ,
81+ } ) ) ;
82+ }
83+ } else {
84+ let content = convert_content_to_openai ( msg. get ( "content" ) ) ;
85+ messages. push ( json ! ( {
86+ "role" : "user" ,
87+ "content" : content,
88+ } ) ) ;
89+ }
90+ } else {
91+ let content = convert_content_to_openai ( msg. get ( "content" ) ) ;
92+ messages. push ( json ! ( {
93+ "role" : "user" ,
94+ "content" : content,
95+ } ) ) ;
96+ }
5297 }
5398 "assistant" => {
5499 let mut assistant_msg = json ! ( { "role" : "assistant" } ) ;
@@ -94,12 +139,14 @@ pub fn anthropic_to_openai(
94139 assistant_msg[ "tool_calls" ] = json ! ( tool_calls) ;
95140 }
96141 } else {
142+ let content = convert_content_to_openai ( msg. get ( "content" ) ) ;
97143 assistant_msg[ "content" ] = content;
98144 }
99145
100146 messages. push ( assistant_msg) ;
101147 }
102148 _ => {
149+ let content = convert_content_to_openai ( msg. get ( "content" ) ) ;
103150 messages. push ( json ! ( {
104151 "role" : role,
105152 "content" : content,
@@ -299,13 +346,9 @@ fn convert_content_to_openai(content: Option<&Value>) -> Value {
299346 }
300347 } ) )
301348 }
302- Some ( "tool_result" ) => {
303- let result_content = part. get ( "content" ) ;
304- Some ( json ! ( {
305- "type" : "text" ,
306- "text" : content_to_string( & convert_content_to_openai( result_content) ) ,
307- } ) )
308- }
349+ // tool_result blocks are handled at the message level,
350+ // not inside convert_content_to_openai
351+ Some ( "tool_result" ) => None ,
309352 _ => None ,
310353 }
311354 } )
@@ -322,6 +365,19 @@ fn convert_content_to_openai(content: Option<&Value>) -> Value {
322365 }
323366}
324367
368+ fn extract_tool_result_content ( block : & Value ) -> String {
369+ let content = block. get ( "content" ) ;
370+ match content {
371+ Some ( Value :: String ( s) ) => s. clone ( ) ,
372+ Some ( Value :: Array ( parts) ) => parts
373+ . iter ( )
374+ . filter_map ( |p| p. get ( "text" ) . and_then ( |t| t. as_str ( ) ) )
375+ . collect :: < Vec < _ > > ( )
376+ . join ( "\n " ) ,
377+ _ => String :: new ( ) ,
378+ }
379+ }
380+
325381fn content_to_string ( content : & Value ) -> String {
326382 match content {
327383 Value :: String ( s) => s. clone ( ) ,
@@ -336,18 +392,26 @@ fn content_to_string(content: &Value) -> String {
336392
337393fn convert_tool_choice ( tc : & Value , _tool_name_map : & ToolNameMap ) -> Value {
338394 match tc {
395+ // Anthropic string shorthand (legacy/convenience)
339396 Value :: String ( s) => match s. as_str ( ) {
340397 "auto" => json ! ( "auto" ) ,
341398 "any" => json ! ( "required" ) ,
342399 "none" => json ! ( "none" ) ,
343400 _ => json ! ( "auto" ) ,
344401 } ,
402+ // Anthropic Object format: {"type": "auto|any|none|tool", "name": "..."}
345403 Value :: Object ( obj) => {
346- if let Some ( name) = obj. get ( "name" ) . and_then ( |n| n. as_str ( ) ) {
347- let truncated = truncate_tool_name ( name) ;
348- json ! ( { "type" : "function" , "function" : { "name" : truncated} } )
349- } else {
350- json ! ( "auto" )
404+ let tc_type = obj. get ( "type" ) . and_then ( |t| t. as_str ( ) ) . unwrap_or ( "auto" ) ;
405+ match tc_type {
406+ "auto" => json ! ( "auto" ) ,
407+ "any" => json ! ( "required" ) ,
408+ "none" => json ! ( "none" ) ,
409+ "tool" => {
410+ let name = obj. get ( "name" ) . and_then ( |n| n. as_str ( ) ) . unwrap_or ( "" ) ;
411+ let truncated = truncate_tool_name ( name) ;
412+ json ! ( { "type" : "function" , "function" : { "name" : truncated} } )
413+ }
414+ _ => json ! ( "auto" ) ,
351415 }
352416 }
353417 _ => json ! ( "auto" ) ,
@@ -456,18 +520,134 @@ mod tests {
456520 }
457521
458522 #[ test]
459- fn test_tool_result_message ( ) {
523+ fn test_tool_result_in_user_message ( ) {
524+ // Anthropic format: tool_result blocks are inside user messages
525+ let req = json ! ( {
526+ "messages" : [ {
527+ "role" : "user" ,
528+ "content" : [
529+ { "type" : "tool_result" , "tool_use_id" : "call_1" , "content" : "search result here" }
530+ ]
531+ } ]
532+ } ) ;
533+ let result = a2o ( & req, "m" ) ;
534+ let msg = & result[ "messages" ] [ 0 ] ;
535+ assert_eq ! ( msg[ "role" ] , "tool" ) ;
536+ assert_eq ! ( msg[ "tool_call_id" ] , "call_1" ) ;
537+ assert_eq ! ( msg[ "content" ] , "search result here" ) ;
538+ }
539+
540+ #[ test]
541+ fn test_tool_result_with_array_content ( ) {
460542 let req = json ! ( {
461543 "messages" : [ {
462- "role" : "tool" ,
463- "tool_use_id" : "call_1" ,
464- "content" : "search result here"
544+ "role" : "user" ,
545+ "content" : [
546+ { "type" : "tool_result" , "tool_use_id" : "call_1" , "content" : [
547+ { "type" : "text" , "text" : "line 1" } ,
548+ { "type" : "text" , "text" : "line 2" }
549+ ] }
550+ ]
465551 } ]
466552 } ) ;
467553 let result = a2o ( & req, "m" ) ;
468554 let msg = & result[ "messages" ] [ 0 ] ;
469555 assert_eq ! ( msg[ "role" ] , "tool" ) ;
470556 assert_eq ! ( msg[ "tool_call_id" ] , "call_1" ) ;
557+ assert_eq ! ( msg[ "content" ] , "line 1\n line 2" ) ;
558+ }
559+
560+ #[ test]
561+ fn test_multiple_tool_results_in_one_user_message ( ) {
562+ let req = json ! ( {
563+ "messages" : [ {
564+ "role" : "user" ,
565+ "content" : [
566+ { "type" : "tool_result" , "tool_use_id" : "call_1" , "content" : "result 1" } ,
567+ { "type" : "tool_result" , "tool_use_id" : "call_2" , "content" : "result 2" }
568+ ]
569+ } ]
570+ } ) ;
571+ let result = a2o ( & req, "m" ) ;
572+ let msgs = result[ "messages" ] . as_array ( ) . unwrap ( ) ;
573+ assert_eq ! ( msgs. len( ) , 2 ) ;
574+ assert_eq ! ( msgs[ 0 ] [ "role" ] , "tool" ) ;
575+ assert_eq ! ( msgs[ 0 ] [ "tool_call_id" ] , "call_1" ) ;
576+ assert_eq ! ( msgs[ 0 ] [ "content" ] , "result 1" ) ;
577+ assert_eq ! ( msgs[ 1 ] [ "role" ] , "tool" ) ;
578+ assert_eq ! ( msgs[ 1 ] [ "tool_call_id" ] , "call_2" ) ;
579+ assert_eq ! ( msgs[ 1 ] [ "content" ] , "result 2" ) ;
580+ }
581+
582+ #[ test]
583+ fn test_tool_result_mixed_with_text ( ) {
584+ // User message with both tool_result and text blocks
585+ let req = json ! ( {
586+ "messages" : [ {
587+ "role" : "user" ,
588+ "content" : [
589+ { "type" : "tool_result" , "tool_use_id" : "call_1" , "content" : "result here" } ,
590+ { "type" : "text" , "text" : "Now do something else" }
591+ ]
592+ } ]
593+ } ) ;
594+ let result = a2o ( & req, "m" ) ;
595+ let msgs = result[ "messages" ] . as_array ( ) . unwrap ( ) ;
596+ assert_eq ! ( msgs. len( ) , 2 ) ;
597+ // tool_result becomes role: tool
598+ assert_eq ! ( msgs[ 0 ] [ "role" ] , "tool" ) ;
599+ assert_eq ! ( msgs[ 0 ] [ "tool_call_id" ] , "call_1" ) ;
600+ // remaining text becomes user message
601+ assert_eq ! ( msgs[ 1 ] [ "role" ] , "user" ) ;
602+ assert_eq ! ( msgs[ 1 ] [ "content" ] , "Now do something else" ) ;
603+ }
604+
605+ #[ test]
606+ fn test_tool_result_empty_content ( ) {
607+ let req = json ! ( {
608+ "messages" : [ {
609+ "role" : "user" ,
610+ "content" : [
611+ { "type" : "tool_result" , "tool_use_id" : "call_1" }
612+ ]
613+ } ]
614+ } ) ;
615+ let result = a2o ( & req, "m" ) ;
616+ let msg = & result[ "messages" ] [ 0 ] ;
617+ assert_eq ! ( msg[ "role" ] , "tool" ) ;
618+ assert_eq ! ( msg[ "content" ] , "" ) ;
619+ }
620+
621+ #[ test]
622+ fn test_full_tool_use_roundtrip ( ) {
623+ // Simulate a complete tool use conversation
624+ let req = json ! ( {
625+ "messages" : [
626+ { "role" : "user" , "content" : "Search for Rust tutorials" } ,
627+ { "role" : "assistant" , "content" : [
628+ { "type" : "text" , "text" : "Let me search for that." } ,
629+ { "type" : "tool_use" , "id" : "toolu_1" , "name" : "search" , "input" : { "q" : "Rust tutorials" } }
630+ ] } ,
631+ { "role" : "user" , "content" : [
632+ { "type" : "tool_result" , "tool_use_id" : "toolu_1" , "content" : "Found 10 results" }
633+ ] }
634+ ]
635+ } ) ;
636+ let result = a2o ( & req, "m" ) ;
637+ let msgs = result[ "messages" ] . as_array ( ) . unwrap ( ) ;
638+ assert_eq ! ( msgs. len( ) , 3 ) ;
639+ // user message
640+ assert_eq ! ( msgs[ 0 ] [ "role" ] , "user" ) ;
641+ assert_eq ! ( msgs[ 0 ] [ "content" ] , "Search for Rust tutorials" ) ;
642+ // assistant with tool call
643+ assert_eq ! ( msgs[ 1 ] [ "role" ] , "assistant" ) ;
644+ assert_eq ! ( msgs[ 1 ] [ "content" ] , "Let me search for that." ) ;
645+ assert_eq ! ( msgs[ 1 ] [ "tool_calls" ] [ 0 ] [ "id" ] , "toolu_1" ) ;
646+ assert_eq ! ( msgs[ 1 ] [ "tool_calls" ] [ 0 ] [ "function" ] [ "name" ] , "search" ) ;
647+ // tool result
648+ assert_eq ! ( msgs[ 2 ] [ "role" ] , "tool" ) ;
649+ assert_eq ! ( msgs[ 2 ] [ "tool_call_id" ] , "toolu_1" ) ;
650+ assert_eq ! ( msgs[ 2 ] [ "content" ] , "Found 10 results" ) ;
471651 }
472652
473653 #[ test]
@@ -488,30 +668,55 @@ mod tests {
488668 assert ! ( tool[ "function" ] [ "parameters" ] [ "properties" ] [ "city" ] . is_object( ) ) ;
489669 }
490670
671+ // --- tool_choice: string shorthand ---
672+
491673 #[ test]
492- fn test_tool_choice_auto ( ) {
674+ fn test_tool_choice_string_auto ( ) {
493675 let req = json ! ( { "messages" : [ ] , "tool_choice" : "auto" } ) ;
494676 let result = a2o ( & req, "m" ) ;
495677 assert_eq ! ( result[ "tool_choice" ] , "auto" ) ;
496678 }
497679
498680 #[ test]
499- fn test_tool_choice_any ( ) {
681+ fn test_tool_choice_string_any ( ) {
500682 let req = json ! ( { "messages" : [ ] , "tool_choice" : "any" } ) ;
501683 let result = a2o ( & req, "m" ) ;
502684 assert_eq ! ( result[ "tool_choice" ] , "required" ) ;
503685 }
504686
505687 #[ test]
506- fn test_tool_choice_none ( ) {
688+ fn test_tool_choice_string_none ( ) {
507689 let req = json ! ( { "messages" : [ ] , "tool_choice" : "none" } ) ;
508690 let result = a2o ( & req, "m" ) ;
509691 assert_eq ! ( result[ "tool_choice" ] , "none" ) ;
510692 }
511693
694+ // --- tool_choice: Anthropic Object format (actual Claude Code format) ---
695+
696+ #[ test]
697+ fn test_tool_choice_object_auto ( ) {
698+ let req = json ! ( { "messages" : [ ] , "tool_choice" : { "type" : "auto" } } ) ;
699+ let result = a2o ( & req, "m" ) ;
700+ assert_eq ! ( result[ "tool_choice" ] , "auto" ) ;
701+ }
702+
703+ #[ test]
704+ fn test_tool_choice_object_any ( ) {
705+ let req = json ! ( { "messages" : [ ] , "tool_choice" : { "type" : "any" } } ) ;
706+ let result = a2o ( & req, "m" ) ;
707+ assert_eq ! ( result[ "tool_choice" ] , "required" ) ;
708+ }
709+
710+ #[ test]
711+ fn test_tool_choice_object_none ( ) {
712+ let req = json ! ( { "messages" : [ ] , "tool_choice" : { "type" : "none" } } ) ;
713+ let result = a2o ( & req, "m" ) ;
714+ assert_eq ! ( result[ "tool_choice" ] , "none" ) ;
715+ }
716+
512717 #[ test]
513- fn test_tool_choice_specific ( ) {
514- let req = json ! ( { "messages" : [ ] , "tool_choice" : { "name" : "my_tool" } } ) ;
718+ fn test_tool_choice_object_specific_tool ( ) {
719+ let req = json ! ( { "messages" : [ ] , "tool_choice" : { "type" : "tool" , " name": "my_tool" } } ) ;
515720 let result = a2o ( & req, "m" ) ;
516721 assert_eq ! ( result[ "tool_choice" ] [ "type" ] , "function" ) ;
517722 assert_eq ! ( result[ "tool_choice" ] [ "function" ] [ "name" ] , "my_tool" ) ;
0 commit comments