@@ -705,5 +705,249 @@ describe("MimoHandler", () => {
705705 const textChunks = chunks . filter ( ( c ) => c . type === "text" )
706706 expect ( textChunks ) . toHaveLength ( 0 )
707707 } )
708+
709+ it ( "should handle multiple tool calls in single response" , async ( ) => {
710+ mockCreate . mockImplementationOnce ( async ( ) => ( {
711+ [ Symbol . asyncIterator ] : async function * ( ) {
712+ yield {
713+ choices : [
714+ {
715+ delta : {
716+ tool_calls : [
717+ {
718+ index : 0 ,
719+ id : "call_1" ,
720+ function : { name : "read_file" , arguments : '{"path":' } ,
721+ } ,
722+ {
723+ index : 1 ,
724+ id : "call_2" ,
725+ function : { name : "list_files" , arguments : '{"path":' } ,
726+ } ,
727+ ] ,
728+ } ,
729+ index : 0 ,
730+ } ,
731+ ] ,
732+ usage : null ,
733+ }
734+ yield {
735+ choices : [
736+ {
737+ delta : {
738+ tool_calls : [
739+ { index : 0 , function : { arguments : '"a.txt"}' } } ,
740+ { index : 1 , function : { arguments : '"./"}' } } ,
741+ ] ,
742+ } ,
743+ index : 0 ,
744+ } ,
745+ ] ,
746+ usage : null ,
747+ }
748+ yield {
749+ choices : [ { delta : { } , index : 0 , finish_reason : "stop" } ] ,
750+ usage : { prompt_tokens : 10 , completion_tokens : 20 , total_tokens : 30 } ,
751+ }
752+ } ,
753+ } ) )
754+
755+ const tools : any [ ] = [
756+ {
757+ type : "function" ,
758+ function : { name : "read_file" , description : "Read" , parameters : { } } ,
759+ } ,
760+ {
761+ type : "function" ,
762+ function : { name : "list_files" , description : "List" , parameters : { } } ,
763+ } ,
764+ ]
765+
766+ const messages : Anthropic . Messages . MessageParam [ ] = [
767+ { role : "user" , content : [ { type : "text" , text : "Hello" } ] } ,
768+ ]
769+
770+ const chunks : any [ ] = [ ]
771+ const stream = handler . createMessage ( "System" , messages , { taskId : "test" , tools } )
772+ for await ( const chunk of stream ) {
773+ chunks . push ( chunk )
774+ }
775+
776+ const toolChunks = chunks . filter ( ( c ) => c . type === "tool_call_partial" )
777+ const readChunks = toolChunks . filter ( ( c ) => c . name === "read_file" )
778+ const listChunks = toolChunks . filter ( ( c ) => c . name === "list_files" )
779+ expect ( readChunks . length ) . toBeGreaterThan ( 0 )
780+ expect ( listChunks . length ) . toBeGreaterThan ( 0 )
781+ } )
782+
783+ it ( "should handle stream interruption gracefully" , async ( ) => {
784+ mockCreate . mockImplementationOnce ( async ( ) => ( {
785+ [ Symbol . asyncIterator ] : async function * ( ) {
786+ yield {
787+ choices : [ { delta : { content : "Partial " } , index : 0 } ] ,
788+ usage : null ,
789+ }
790+ // Stream ends without finish_reason (connection dropped)
791+ } ,
792+ } ) )
793+
794+ const messages : Anthropic . Messages . MessageParam [ ] = [
795+ { role : "user" , content : [ { type : "text" , text : "Hello" } ] } ,
796+ ]
797+
798+ const chunks : any [ ] = [ ]
799+ const stream = handler . createMessage ( "System" , messages )
800+ for await ( const chunk of stream ) {
801+ chunks . push ( chunk )
802+ }
803+
804+ const textChunks = chunks . filter ( ( c ) => c . type === "text" )
805+ expect ( textChunks ) . toHaveLength ( 1 )
806+ expect ( textChunks [ 0 ] . text ) . toBe ( "Partial " )
807+
808+ const usageChunks = chunks . filter ( ( c ) => c . type === "usage" )
809+ expect ( usageChunks ) . toHaveLength ( 0 )
810+ } )
811+
812+ it ( "should sanitize tool call IDs with invalid characters" , async ( ) => {
813+ mockCreate . mockImplementationOnce ( async ( ) => ( {
814+ [ Symbol . asyncIterator ] : async function * ( ) {
815+ yield {
816+ choices : [
817+ {
818+ delta : {
819+ tool_calls : [
820+ {
821+ index : 0 ,
822+ id : "call_with-special.chars@123" ,
823+ function : { name : "test_tool" , arguments : "{}" } ,
824+ } ,
825+ ] ,
826+ } ,
827+ index : 0 ,
828+ } ,
829+ ] ,
830+ usage : null ,
831+ }
832+ yield {
833+ choices : [ { delta : { } , index : 0 , finish_reason : "stop" } ] ,
834+ usage : { prompt_tokens : 1 , completion_tokens : 1 , total_tokens : 2 } ,
835+ }
836+ } ,
837+ } ) )
838+
839+ const tools : any [ ] = [
840+ {
841+ type : "function" ,
842+ function : { name : "test_tool" , description : "Test" , parameters : { } } ,
843+ } ,
844+ ]
845+
846+ const messages : Anthropic . Messages . MessageParam [ ] = [
847+ { role : "user" , content : [ { type : "text" , text : "Hello" } ] } ,
848+ ]
849+
850+ const chunks : any [ ] = [ ]
851+ const stream = handler . createMessage ( "System" , messages , { taskId : "test" , tools } )
852+ for await ( const chunk of stream ) {
853+ chunks . push ( chunk )
854+ }
855+
856+ const toolChunks = chunks . filter ( ( c ) => c . type === "tool_call_partial" )
857+ expect ( toolChunks . length ) . toBeGreaterThan ( 0 )
858+ expect ( toolChunks [ 0 ] . id ) . toBe ( sanitizeOpenAiCallId ( "call_with-special.chars@123" ) )
859+ expect ( toolChunks [ 0 ] . id ) . not . toMatch ( / [ ^ a - z A - Z 0 - 9 _ - ] / )
860+ } )
861+
862+ it ( "should convert system prompt to system message for MiMo" , async ( ) => {
863+ const userMessages : Anthropic . Messages . MessageParam [ ] = [
864+ { role : "user" , content : [ { type : "text" , text : "Hello" } ] } ,
865+ ]
866+
867+ const stream = handler . createMessage ( "You are a helpful assistant" , userMessages )
868+ for await ( const _chunk of stream ) {
869+ // drain
870+ }
871+
872+ const params = mockCreate . mock . calls [ 0 ] [ 0 ]
873+ expect ( params . messages [ 0 ] . role ) . toBe ( "system" )
874+ expect ( params . messages [ 0 ] . content ) . toBe ( "You are a helpful assistant" )
875+ expect ( params . messages [ 1 ] . role ) . toBe ( "user" )
876+ } )
877+ } )
878+
879+ describe ( "completePrompt" , ( ) => {
880+ it ( "should complete prompt successfully" , async ( ) => {
881+ mockCreate . mockResolvedValueOnce ( {
882+ choices : [ { message : { content : "Test response" } } ] ,
883+ } )
884+
885+ const result = await handler . completePrompt ( "Test prompt" )
886+ expect ( result ) . toBe ( "Test response" )
887+ } )
888+
889+ it ( "should send correct parameters to the API" , async ( ) => {
890+ mockCreate . mockResolvedValueOnce ( {
891+ choices : [ { message : { content : "Response" } } ] ,
892+ } )
893+
894+ await handler . completePrompt ( "What is 2+2?" )
895+
896+ const params = mockCreate . mock . calls [ 0 ] [ 0 ]
897+ expect ( params . model ) . toBe ( "mimo-v2.5-pro" )
898+ expect ( params . messages ) . toHaveLength ( 1 )
899+ expect ( params . messages [ 0 ] . role ) . toBe ( "user" )
900+ expect ( params . messages [ 0 ] . content ) . toBe ( "What is 2+2?" )
901+ } )
902+
903+ it ( "should handle API errors with provider prefix" , async ( ) => {
904+ mockCreate . mockRejectedValueOnce ( new Error ( "401 Unauthorized" ) )
905+
906+ await expect ( handler . completePrompt ( "Test prompt" ) ) . rejects . toThrow ( "OpenAI completion error:" )
907+ } )
908+
909+ it ( "should return empty string when choices array is empty" , async ( ) => {
910+ mockCreate . mockResolvedValueOnce ( { choices : [ ] } )
911+
912+ const result = await handler . completePrompt ( "Test prompt" )
913+ expect ( result ) . toBe ( "" )
914+ } )
915+
916+ it ( "should return empty string when message content is null" , async ( ) => {
917+ mockCreate . mockResolvedValueOnce ( {
918+ choices : [ { message : { content : null } } ] ,
919+ } )
920+
921+ const result = await handler . completePrompt ( "Test prompt" )
922+ expect ( result ) . toBe ( "" )
923+ } )
924+
925+ it ( "should propagate network errors with provider prefix" , async ( ) => {
926+ mockCreate . mockRejectedValueOnce ( new Error ( "ECONNREFUSED" ) )
927+
928+ await expect ( handler . completePrompt ( "Test prompt" ) ) . rejects . toThrow ( "OpenAI completion error:" )
929+ } )
930+
931+ it ( "should propagate rate limit errors with provider prefix" , async ( ) => {
932+ mockCreate . mockRejectedValueOnce ( new Error ( "429 Too Many Requests" ) )
933+
934+ await expect ( handler . completePrompt ( "Test prompt" ) ) . rejects . toThrow ( "OpenAI completion error:" )
935+ } )
936+
937+ it ( "should use correct model ID for mimo-v2.5 variant" , async ( ) => {
938+ const v25Handler = new MimoHandler ( {
939+ ...mockOptions ,
940+ apiModelId : "mimo-v2.5" ,
941+ } )
942+
943+ mockCreate . mockResolvedValueOnce ( {
944+ choices : [ { message : { content : "Response" } } ] ,
945+ } )
946+
947+ await v25Handler . completePrompt ( "Test" )
948+
949+ const params = mockCreate . mock . calls [ 0 ] [ 0 ]
950+ expect ( params . model ) . toBe ( "mimo-v2.5" )
951+ } )
708952 } )
709953} )
0 commit comments