@@ -6,6 +6,25 @@ vi.mock("openai", () => {
66 chat : {
77 completions : {
88 create : mockCreate . mockImplementation ( async ( options ) => {
9+ // Non-streaming response (used by completePrompt)
10+ if ( ! options . stream ) {
11+ return {
12+ choices : [
13+ {
14+ message : { role : "assistant" , content : "Test response" } ,
15+ index : 0 ,
16+ finish_reason : "stop" ,
17+ } ,
18+ ] ,
19+ usage : {
20+ prompt_tokens : 10 ,
21+ completion_tokens : 5 ,
22+ total_tokens : 15 ,
23+ prompt_tokens_details : { cached_tokens : 2 } ,
24+ } ,
25+ }
26+ }
27+ // Streaming response (used by createMessage)
928 return {
1029 [ Symbol . asyncIterator ] : async function * ( ) {
1130 yield {
@@ -705,5 +724,259 @@ describe("MimoHandler", () => {
705724 const textChunks = chunks . filter ( ( c ) => c . type === "text" )
706725 expect ( textChunks ) . toHaveLength ( 0 )
707726 } )
727+
728+ describe ( "completePrompt" , ( ) => {
729+ it ( "should return the assistant content from a non-streaming response" , async ( ) => {
730+ const result = await handler . completePrompt ( "Hello, how are you?" )
731+ expect ( result ) . toBe ( "Test response" )
732+ } )
733+
734+ it ( "should call create without stream option" , async ( ) => {
735+ await handler . completePrompt ( "What is 2+2?" )
736+
737+ const params = mockCreate . mock . calls [ 0 ] [ 0 ]
738+ expect ( params . stream ) . toBeFalsy ( )
739+ } )
740+
741+ it ( "should include the prompt as a user message" , async ( ) => {
742+ await handler . completePrompt ( "Explain quantum physics" )
743+
744+ const params = mockCreate . mock . calls [ 0 ] [ 0 ]
745+ expect ( params . messages ) . toContainEqual ( {
746+ role : "user" ,
747+ content : "Explain quantum physics" ,
748+ } )
749+ } )
750+
751+ it ( "should include the model ID in the request" , async ( ) => {
752+ await handler . completePrompt ( "Hello" )
753+
754+ const params = mockCreate . mock . calls [ 0 ] [ 0 ]
755+ expect ( params . model ) . toBe ( "mimo-v2.5-pro" )
756+ } )
757+
758+ it ( "should not include tools or parallel_tool_calls" , async ( ) => {
759+ await handler . completePrompt ( "Hello" )
760+
761+ const params = mockCreate . mock . calls [ 0 ] [ 0 ]
762+ expect ( params . tools ) . toBeUndefined ( )
763+ expect ( params . parallel_tool_calls ) . toBeUndefined ( )
764+ } )
765+
766+ it ( "should work with mimo-v2.5 model" , async ( ) => {
767+ const h = new MimoHandler ( { ...mockOptions , apiModelId : "mimo-v2.5" } )
768+ const result = await h . completePrompt ( "Hello" )
769+ expect ( result ) . toBe ( "Test response" )
770+
771+ const params = mockCreate . mock . calls [ 0 ] [ 0 ]
772+ expect ( params . model ) . toBe ( "mimo-v2.5" )
773+ } )
774+
775+ it ( "should propagate API errors" , async ( ) => {
776+ mockCreate . mockRejectedValueOnce ( new Error ( "500 Internal Server Error" ) )
777+
778+ await expect ( handler . completePrompt ( "Hello" ) ) . rejects . toThrow ( )
779+ } )
780+
781+ it ( "should return empty string when content is null" , async ( ) => {
782+ mockCreate . mockImplementationOnce ( async ( options ) => {
783+ if ( ! options . stream ) {
784+ return {
785+ choices : [
786+ {
787+ message : { role : "assistant" , content : null } ,
788+ index : 0 ,
789+ finish_reason : "stop" ,
790+ } ,
791+ ] ,
792+ usage : {
793+ prompt_tokens : 5 ,
794+ completion_tokens : 0 ,
795+ total_tokens : 5 ,
796+ } ,
797+ }
798+ }
799+ return { [ Symbol . asyncIterator ] : async function * ( ) { } }
800+ } )
801+
802+ const result = await handler . completePrompt ( "Hello" )
803+ expect ( result ) . toBe ( "" )
804+ } )
805+ } )
806+
807+ describe ( "advanced streaming scenarios" , ( ) => {
808+ it ( "should handle stream with multiple text chunks concatenated" , async ( ) => {
809+ mockCreate . mockImplementationOnce ( async ( ) => ( {
810+ [ Symbol . asyncIterator ] : async function * ( ) {
811+ yield {
812+ choices : [ { delta : { content : "Hello" } , index : 0 } ] ,
813+ usage : null ,
814+ }
815+ yield {
816+ choices : [ { delta : { content : " world" } , index : 0 } ] ,
817+ usage : null ,
818+ }
819+ yield {
820+ choices : [ { delta : { content : "!" } , index : 0 } ] ,
821+ usage : null ,
822+ }
823+ yield {
824+ choices : [ { delta : { } , index : 0 , finish_reason : "stop" } ] ,
825+ usage : { prompt_tokens : 10 , completion_tokens : 3 , total_tokens : 13 } ,
826+ }
827+ } ,
828+ } ) )
829+
830+ const messages : Anthropic . Messages . MessageParam [ ] = [
831+ { role : "user" , content : [ { type : "text" , text : "Hi" } ] } ,
832+ ]
833+
834+ const chunks : any [ ] = [ ]
835+ const stream = handler . createMessage ( "System prompt" , messages )
836+ for await ( const chunk of stream ) {
837+ chunks . push ( chunk )
838+ }
839+
840+ const textChunks = chunks . filter ( ( c ) => c . type === "text" )
841+ expect ( textChunks ) . toHaveLength ( 3 )
842+ expect ( textChunks . map ( ( c : any ) => c . text ) . join ( "" ) ) . toBe ( "Hello world!" )
843+ } )
844+
845+ it ( "should handle stream with both reasoning and tool calls" , async ( ) => {
846+ mockCreate . mockImplementationOnce ( async ( ) => ( {
847+ [ Symbol . asyncIterator ] : async function * ( ) {
848+ yield {
849+ choices : [ { delta : { reasoning_content : "Let me think" } , index : 0 } ] ,
850+ usage : null ,
851+ }
852+ yield {
853+ choices : [ { delta : { reasoning_content : " about this" } , index : 0 } ] ,
854+ usage : null ,
855+ }
856+ yield {
857+ choices : [ { delta : { content : "I'll read it" } , index : 0 } ] ,
858+ usage : null ,
859+ }
860+ yield {
861+ choices : [
862+ {
863+ delta : {
864+ tool_calls : [
865+ {
866+ index : 0 ,
867+ id : "call_read" ,
868+ function : { name : "read_file" , arguments : '{"path":"test.ts"}' } ,
869+ } ,
870+ ] ,
871+ } ,
872+ index : 0 ,
873+ } ,
874+ ] ,
875+ usage : null ,
876+ }
877+ yield {
878+ choices : [ { delta : { } , index : 0 , finish_reason : "tool_calls" } ] ,
879+ usage : { prompt_tokens : 20 , completion_tokens : 15 , total_tokens : 35 } ,
880+ }
881+ } ,
882+ } ) )
883+
884+ const messages : Anthropic . Messages . MessageParam [ ] = [
885+ { role : "user" , content : [ { type : "text" , text : "Read test.ts" } ] } ,
886+ ]
887+
888+ const chunks : any [ ] = [ ]
889+ const stream = handler . createMessage ( "System prompt" , messages )
890+ for await ( const chunk of stream ) {
891+ chunks . push ( chunk )
892+ }
893+
894+ const reasoningChunks = chunks . filter ( ( c ) => c . type === "reasoning" )
895+ expect ( reasoningChunks ) . toHaveLength ( 2 )
896+ expect ( reasoningChunks . map ( ( c : any ) => c . text ) . join ( "" ) ) . toBe ( "Let me think about this" )
897+
898+ const textChunks = chunks . filter ( ( c ) => c . type === "text" )
899+ expect ( textChunks ) . toHaveLength ( 1 )
900+ expect ( textChunks [ 0 ] . text ) . toBe ( "I'll read it" )
901+
902+ const toolChunks = chunks . filter ( ( c ) => c . type === "tool_call_partial" )
903+ expect ( toolChunks ) . toHaveLength ( 1 )
904+ expect ( toolChunks [ 0 ] . id ) . toBe ( "call_read" )
905+ expect ( toolChunks [ 0 ] . name ) . toBe ( "read_file" )
906+ } )
907+
908+ it ( "should handle stream with no usage in final chunk" , async ( ) => {
909+ mockCreate . mockImplementationOnce ( async ( ) => ( {
910+ [ Symbol . asyncIterator ] : async function * ( ) {
911+ yield {
912+ choices : [ { delta : { content : "Done" } , index : 0 } ] ,
913+ usage : null ,
914+ }
915+ yield {
916+ choices : [ { delta : { } , index : 0 , finish_reason : "stop" } ] ,
917+ usage : null ,
918+ }
919+ } ,
920+ } ) )
921+
922+ const messages : Anthropic . Messages . MessageParam [ ] = [
923+ { role : "user" , content : [ { type : "text" , text : "Hello" } ] } ,
924+ ]
925+
926+ const chunks : any [ ] = [ ]
927+ const stream = handler . createMessage ( "System prompt" , messages )
928+ for await ( const chunk of stream ) {
929+ chunks . push ( chunk )
930+ }
931+
932+ const usageChunks = chunks . filter ( ( c ) => c . type === "usage" )
933+ expect ( usageChunks ) . toHaveLength ( 0 )
934+
935+ const textChunks = chunks . filter ( ( c ) => c . type === "text" )
936+ expect ( textChunks ) . toHaveLength ( 1 )
937+ expect ( textChunks [ 0 ] . text ) . toBe ( "Done" )
938+ } )
939+
940+ it ( "should handle stream with zero cache tokens in usage" , async ( ) => {
941+ mockCreate . mockImplementationOnce ( async ( ) => ( {
942+ [ Symbol . asyncIterator ] : async function * ( ) {
943+ yield {
944+ choices : [ { delta : { content : "Hi" } , index : 0 } ] ,
945+ usage : null ,
946+ }
947+ yield {
948+ choices : [ { delta : { } , index : 0 , finish_reason : "stop" } ] ,
949+ usage : {
950+ prompt_tokens : 50 ,
951+ completion_tokens : 10 ,
952+ total_tokens : 60 ,
953+ prompt_tokens_details : {
954+ cache_write_tokens : 0 ,
955+ cached_tokens : 0 ,
956+ } ,
957+ } ,
958+ }
959+ } ,
960+ } ) )
961+
962+ const messages : Anthropic . Messages . MessageParam [ ] = [
963+ { role : "user" , content : [ { type : "text" , text : "Hello" } ] } ,
964+ ]
965+
966+ const chunks : any [ ] = [ ]
967+ const stream = handler . createMessage ( "System prompt" , messages )
968+ for await ( const chunk of stream ) {
969+ chunks . push ( chunk )
970+ }
971+
972+ const usageChunks = chunks . filter ( ( c ) => c . type === "usage" )
973+ expect ( usageChunks ) . toHaveLength ( 1 )
974+ expect ( usageChunks [ 0 ] . inputTokens ) . toBe ( 50 )
975+ expect ( usageChunks [ 0 ] . outputTokens ) . toBe ( 10 )
976+ // Handler uses `|| undefined` so zero-valued cache tokens are omitted
977+ expect ( usageChunks [ 0 ] . cacheWriteTokens ) . toBeUndefined ( )
978+ expect ( usageChunks [ 0 ] . cacheReadTokens ) . toBeUndefined ( )
979+ } )
980+ } )
708981 } )
709982} )
0 commit comments