@@ -627,6 +627,189 @@ describe("callModel E2E Tests", () => {
627627 const hasContentDeltas = chunks . some ( ( c ) => c . type === "content.delta" ) ;
628628 expect ( hasContentDeltas ) . toBe ( true ) ;
629629 } , 15000 ) ;
630+
631+ it ( "should return events with correct shape for each event type" , async ( ) => {
632+ const response = client . callModel ( {
633+ model : "meta-llama/llama-3.2-1b-instruct" ,
634+ input : [
635+ {
636+ role : "user" ,
637+ content : "Count from 1 to 3." ,
638+ } ,
639+ ] ,
640+ } ) ;
641+
642+ let hasContentDelta = false ;
643+ let hasMessageComplete = false ;
644+
645+ for await ( const event of response . getFullChatStream ( ) ) {
646+ // Every event must have a type
647+ expect ( event ) . toHaveProperty ( "type" ) ;
648+ expect ( typeof event . type ) . toBe ( "string" ) ;
649+ expect ( event . type . length ) . toBeGreaterThan ( 0 ) ;
650+
651+ // Validate shape based on event type
652+ switch ( event . type ) {
653+ case "content.delta" :
654+ hasContentDelta = true ;
655+ // Must have delta property
656+ expect ( event ) . toHaveProperty ( "delta" ) ;
657+ expect ( typeof event . delta ) . toBe ( "string" ) ;
658+ // Delta can be empty string but must be string
659+ break ;
660+
661+ case "message.complete" :
662+ hasMessageComplete = true ;
663+ // Must have response property
664+ expect ( event ) . toHaveProperty ( "response" ) ;
665+ expect ( event . response ) . toBeDefined ( ) ;
666+ // Response should be an object (the full response)
667+ expect ( typeof event . response ) . toBe ( "object" ) ;
668+ expect ( event . response ) . not . toBeNull ( ) ;
669+ break ;
670+
671+ case "tool.preliminary_result" :
672+ // Must have toolCallId and result
673+ expect ( event ) . toHaveProperty ( "toolCallId" ) ;
674+ expect ( event ) . toHaveProperty ( "result" ) ;
675+ expect ( typeof event . toolCallId ) . toBe ( "string" ) ;
676+ expect ( event . toolCallId . length ) . toBeGreaterThan ( 0 ) ;
677+ // result can be any type
678+ break ;
679+
680+ default :
681+ // Pass-through events must have event property
682+ expect ( event ) . toHaveProperty ( "event" ) ;
683+ expect ( event . event ) . toBeDefined ( ) ;
684+ break ;
685+ }
686+ }
687+
688+ // Should have at least content deltas for a text response
689+ expect ( hasContentDelta ) . toBe ( true ) ;
690+ } , 15000 ) ;
691+
692+ it ( "should validate content.delta events have proper structure" , async ( ) => {
693+ const response = client . callModel ( {
694+ model : "meta-llama/llama-3.2-1b-instruct" ,
695+ input : [
696+ {
697+ role : "user" ,
698+ content : "Say 'hello world'." ,
699+ } ,
700+ ] ,
701+ } ) ;
702+
703+ const contentDeltas : any [ ] = [ ] ;
704+
705+ for await ( const event of response . getFullChatStream ( ) ) {
706+ if ( event . type === "content.delta" ) {
707+ contentDeltas . push ( event ) ;
708+
709+ // Validate exact shape
710+ const keys = Object . keys ( event ) ;
711+ expect ( keys ) . toContain ( "type" ) ;
712+ expect ( keys ) . toContain ( "delta" ) ;
713+
714+ // type must be exactly "content.delta"
715+ expect ( event . type ) . toBe ( "content.delta" ) ;
716+
717+ // delta must be a string
718+ expect ( typeof event . delta ) . toBe ( "string" ) ;
719+ }
720+ }
721+
722+ expect ( contentDeltas . length ) . toBeGreaterThan ( 0 ) ;
723+
724+ // Concatenated deltas should form readable text
725+ const fullText = contentDeltas . map ( e => e . delta ) . join ( "" ) ;
726+ expect ( fullText . length ) . toBeGreaterThan ( 0 ) ;
727+ } , 15000 ) ;
728+
729+ it ( "should include tool.preliminary_result events with correct shape when generator tools are executed" , async ( ) => {
730+ const response = client . callModel ( {
731+ model : "openai/gpt-4o-mini" ,
732+ input : [
733+ {
734+ role : "user" ,
735+ content : "What time is it? Use the get_time tool." ,
736+ } ,
737+ ] ,
738+ tools : [
739+ {
740+ type : ToolType . Function ,
741+ function : {
742+ name : "get_time" ,
743+ description : "Get current time" ,
744+ inputSchema : z . object ( {
745+ timezone : z . string ( ) . optional ( ) . describe ( "Timezone" ) ,
746+ } ) ,
747+ // Generator tools need eventSchema for intermediate results
748+ eventSchema : z . object ( {
749+ status : z . string ( ) ,
750+ } ) ,
751+ outputSchema : z . object ( {
752+ time : z . string ( ) ,
753+ timezone : z . string ( ) ,
754+ } ) ,
755+ // Use generator function to emit preliminary results
756+ execute : async function * ( params : { timezone ?: string } ) {
757+ // Emit preliminary result (validated against eventSchema)
758+ yield { status : "fetching time..." } ;
759+
760+ // Final result (validated against outputSchema)
761+ yield {
762+ time : "14:30:00" ,
763+ timezone : params . timezone || "UTC" ,
764+ } ;
765+ } ,
766+ } ,
767+ } ,
768+ ] ,
769+ } ) ;
770+
771+ let hasPreliminaryResult = false ;
772+ const preliminaryResults : any [ ] = [ ] ;
773+
774+ for await ( const event of response . getFullChatStream ( ) ) {
775+ expect ( event ) . toHaveProperty ( "type" ) ;
776+ expect ( typeof event . type ) . toBe ( "string" ) ;
777+
778+ if ( event . type === "tool.preliminary_result" ) {
779+ hasPreliminaryResult = true ;
780+ preliminaryResults . push ( event ) ;
781+
782+ // Validate exact shape
783+ expect ( event ) . toHaveProperty ( "toolCallId" ) ;
784+ expect ( event ) . toHaveProperty ( "result" ) ;
785+
786+ // toolCallId must be non-empty string
787+ expect ( typeof event . toolCallId ) . toBe ( "string" ) ;
788+ expect ( event . toolCallId . length ) . toBeGreaterThan ( 0 ) ;
789+
790+ // result is defined
791+ expect ( event . result ) . toBeDefined ( ) ;
792+ }
793+ }
794+
795+ // Validate that if we got preliminary results, they have the correct shape
796+ if ( hasPreliminaryResult ) {
797+ expect ( preliminaryResults . length ) . toBeGreaterThan ( 0 ) ;
798+
799+ // Should have status update or final result
800+ const hasStatusUpdate = preliminaryResults . some (
801+ ( e ) => e . result && typeof e . result === "object" && "status" in e . result
802+ ) ;
803+ const hasFinalResult = preliminaryResults . some (
804+ ( e ) => e . result && typeof e . result === "object" && "time" in e . result
805+ ) ;
806+
807+ expect ( hasStatusUpdate || hasFinalResult ) . toBe ( true ) ;
808+ }
809+
810+ // The stream should complete without errors regardless of tool execution
811+ expect ( true ) . toBe ( true ) ;
812+ } , 30000 ) ;
630813 } ) ;
631814
632815 describe ( "Multiple concurrent consumption patterns" , ( ) => {
0 commit comments