@@ -657,6 +657,149 @@ describe("MimoHandler", () => {
657657 expect ( mockCreate ) . toHaveBeenCalledTimes ( 1 )
658658 } )
659659
660+ it ( "should not retry when rejection is a non-Error value (parallel_tool_calls path)" , async ( ) => {
661+ // A non-Error rejection (e.g. a string) must not trigger the
662+ // parallel_tool_calls fallback. isParallelToolCallsRejected
663+ // returns false for non-Error values.
664+ mockCreate . mockRejectedValueOnce ( "network failure" )
665+
666+ const messages : Anthropic . Messages . MessageParam [ ] = [
667+ { role : "user" , content : [ { type : "text" , text : "Hello" } ] } ,
668+ ]
669+
670+ await expect ( async ( ) => {
671+ const stream = handler . createMessage ( "System prompt" , messages , {
672+ taskId : "test-task" ,
673+ parallelToolCalls : false ,
674+ } )
675+ for await ( const _chunk of stream ) {
676+ // drain
677+ }
678+ } ) . rejects . toThrow ( )
679+
680+ expect ( mockCreate ) . toHaveBeenCalledTimes ( 1 )
681+ } )
682+
683+ it ( "should not retry strict-schema fallback for non-400 errors" , async ( ) => {
684+ // A 500 error must NOT trigger the strict-schema fallback.
685+ // isStrictToolSchemaRejected returns false when status !== 400.
686+ const rejectionError = Object . assign (
687+ new Error ( "500 - Internal server error" ) ,
688+ { status : 500 } ,
689+ )
690+ mockCreate . mockRejectedValueOnce ( rejectionError )
691+
692+ const tools : OpenAI . Chat . ChatCompletionTool [ ] = [
693+ {
694+ type : "function" ,
695+ function : { name : "read_file" , description : "Read" , parameters : { } } ,
696+ } ,
697+ ]
698+
699+ const messages : Anthropic . Messages . MessageParam [ ] = [
700+ { role : "user" , content : [ { type : "text" , text : "Hello" } ] } ,
701+ ]
702+
703+ await expect ( async ( ) => {
704+ const stream = handler . createMessage ( "System prompt" , messages , { taskId : "test-task" , tools } )
705+ for await ( const _chunk of stream ) {
706+ // drain
707+ }
708+ } ) . rejects . toThrow ( )
709+
710+ expect ( mockCreate ) . toHaveBeenCalledTimes ( 1 )
711+ } )
712+
713+ it ( "should not retry strict-schema fallback for non-Error rejections" , async ( ) => {
714+ // A non-Error rejection (e.g. a string) must not trigger the
715+ // strict-schema fallback. isStrictToolSchemaRejected returns
716+ // false for non-Error values.
717+ mockCreate . mockRejectedValueOnce ( "bad gateway" )
718+
719+ const tools : OpenAI . Chat . ChatCompletionTool [ ] = [
720+ {
721+ type : "function" ,
722+ function : { name : "read_file" , description : "Read" , parameters : { } } ,
723+ } ,
724+ ]
725+
726+ const messages : Anthropic . Messages . MessageParam [ ] = [
727+ { role : "user" , content : [ { type : "text" , text : "Hello" } ] } ,
728+ ]
729+
730+ await expect ( async ( ) => {
731+ const stream = handler . createMessage ( "System prompt" , messages , { taskId : "test-task" , tools } )
732+ for await ( const _chunk of stream ) {
733+ // drain
734+ }
735+ } ) . rejects . toThrow ( )
736+
737+ expect ( mockCreate ) . toHaveBeenCalledTimes ( 1 )
738+ } )
739+
740+ it ( "should pass non-function tools through unchanged during strict-schema retry" , async ( ) => {
741+ // When the endpoint rejects strict tool schemas, the retry
742+ // strips strict from function tools but passes non-function
743+ // tools (e.g. type "code_interpreter") through unchanged.
744+ const rejectionError = Object . assign ( new Error ( "400 - Unknown parameter: tools[0].function.strict" ) , {
745+ status : 400 ,
746+ } )
747+ mockCreate . mockRejectedValueOnce ( rejectionError )
748+
749+ // Second call (retry) succeeds
750+ mockCreate . mockImplementationOnce ( async ( ) => ( {
751+ [ Symbol . asyncIterator ] : async function * ( ) {
752+ yield {
753+ choices : [ { delta : { content : "Retried" } , index : 0 } ] ,
754+ usage : null ,
755+ }
756+ yield {
757+ choices : [ { delta : { } , index : 0 , finish_reason : "stop" } ] ,
758+ usage : { prompt_tokens : 1 , completion_tokens : 1 , total_tokens : 2 } ,
759+ }
760+ } ,
761+ } ) )
762+
763+ const tools : OpenAI . Chat . ChatCompletionTool [ ] = [
764+ {
765+ type : "function" ,
766+ function : {
767+ name : "read_file" ,
768+ description : "Read" ,
769+ parameters : { type : "object" , properties : { } } ,
770+ strict : true ,
771+ } ,
772+ } ,
773+ // Non-function tool — should pass through stripStrictFromTools unchanged
774+ {
775+ type : "code_interpreter" as OpenAI . Chat . ChatCompletionTool [ "type" ] ,
776+ code_interpreter : { name : "code_interpreter" } ,
777+ } as unknown as OpenAI . Chat . ChatCompletionTool ,
778+ ]
779+
780+ const messages : Anthropic . Messages . MessageParam [ ] = [
781+ { role : "user" , content : [ { type : "text" , text : "Hello" } ] } ,
782+ ]
783+
784+ const stream = handler . createMessage ( "System prompt" , messages , { taskId : "test-task" , tools } )
785+ for await ( const _chunk of stream ) {
786+ // drain
787+ }
788+
789+ // The retry call should have been made
790+ expect ( mockCreate ) . toHaveBeenCalledTimes ( 2 )
791+
792+ // The retry call's tools should have the function tool with strict removed
793+ // and the non-function tool preserved unchanged
794+ const retryCallParams = mockCreate . mock . calls [ 1 ] [ 0 ]
795+ expect ( retryCallParams . tools ) . toBeDefined ( )
796+ expect ( retryCallParams . tools ) . toHaveLength ( 2 )
797+ // Function tool should have strict removed
798+ expect ( retryCallParams . tools [ 0 ] . function ) . not . toHaveProperty ( "strict" )
799+ // Non-function tool should be preserved
800+ expect ( retryCallParams . tools [ 1 ] . type ) . toBe ( "code_interpreter" )
801+ } )
802+
660803 it ( "should send stream_options with include_usage" , async ( ) => {
661804 const messages : Anthropic . Messages . MessageParam [ ] = [
662805 { role : "user" , content : [ { type : "text" , text : "Hello" } ] } ,
0 commit comments