@@ -227,7 +227,25 @@ const errorFixture: Fixture = {
227227 } ,
228228} ;
229229
230- const allFixtures : Fixture [ ] = [ textFixture , toolFixture , errorFixture ] ;
230+ const contentWithToolCallsFixture : Fixture = {
231+ match : { userMessage : "search-and-explain" } ,
232+ response : {
233+ content : "Let me look that up." ,
234+ toolCalls : [
235+ {
236+ name : "web_search" ,
237+ arguments : '{"query":"vitest testing"}' ,
238+ } ,
239+ ] ,
240+ } ,
241+ } ;
242+
243+ const allFixtures : Fixture [ ] = [
244+ textFixture ,
245+ toolFixture ,
246+ errorFixture ,
247+ contentWithToolCallsFixture ,
248+ ] ;
231249
232250// --- test lifecycle ---
233251
@@ -294,9 +312,7 @@ describe("POST /model/{modelId}/invoke-with-response-stream", () => {
294312 expect ( stopBlock ! . payload ) . toEqual ( { type : "content_block_stop" , index : 0 } ) ;
295313
296314 // message_delta/message_stop
297- const msgDelta = frames . find (
298- ( f ) => ( f . payload as { type ?: string } ) . type === "message_delta" ,
299- ) ;
315+ const msgDelta = frames . find ( ( f ) => ( f . payload as { type ?: string } ) . type === "message_delta" ) ;
300316 expect ( msgDelta ) . toBeDefined ( ) ;
301317 expect ( msgDelta ! . payload ) . toMatchObject ( {
302318 type : "message_delta" ,
@@ -344,9 +360,7 @@ describe("POST /model/{modelId}/invoke-with-response-stream", () => {
344360 . join ( "" ) ;
345361 expect ( JSON . parse ( fullJson ) ) . toEqual ( { city : "SF" } ) ;
346362
347- const msgDelta = frames . find (
348- ( f ) => ( f . payload as { type ?: string } ) . type === "message_delta" ,
349- ) ;
363+ const msgDelta = frames . find ( ( f ) => ( f . payload as { type ?: string } ) . type === "message_delta" ) ;
350364 expect ( msgDelta ! . payload ) . toMatchObject ( {
351365 type : "message_delta" ,
352366 delta : { stop_reason : "tool_use" } ,
@@ -511,13 +525,103 @@ describe("POST /model/{modelId}/invoke-with-response-stream (multiple tool calls
511525 expect ( ( blockStops [ 1 ] . payload as { index : number } ) . index ) . toBe ( 1 ) ;
512526
513527 // message_delta should indicate tool_use
514- const msgDelta = frames . find (
515- ( f ) => ( f . payload as { type ?: string } ) . type === "message_delta" ,
516- ) ;
528+ const msgDelta = frames . find ( ( f ) => ( f . payload as { type ?: string } ) . type === "message_delta" ) ;
517529 expect ( msgDelta ! . payload ) . toMatchObject ( { delta : { stop_reason : "tool_use" } } ) ;
518530 } ) ;
519531} ) ;
520532
533+ // ─── invoke-with-response-stream: content + tool calls ────────────────────
534+
535+ describe ( "POST /model/{modelId}/invoke-with-response-stream (content + toolCalls)" , ( ) => {
536+ const MODEL_ID = "anthropic.claude-3-5-sonnet-20241022-v2:0" ;
537+
538+ it ( "streams text block followed by tool_use block in Anthropic-native format" , async ( ) => {
539+ instance = await createServer ( allFixtures ) ;
540+ const res = await postBinary ( `${ instance . url } /model/${ MODEL_ID } /invoke-with-response-stream` , {
541+ anthropic_version : "bedrock-2023-05-31" ,
542+ max_tokens : 512 ,
543+ messages : [ { role : "user" , content : "search-and-explain" } ] ,
544+ } ) ;
545+
546+ expect ( res . status ) . toBe ( 200 ) ;
547+ expect ( res . headers [ "content-type" ] ) . toBe ( "application/vnd.amazon.eventstream" ) ;
548+
549+ const frames = parseFrames ( res . body ) ;
550+
551+ // All frames should be "chunk" eventType (Anthropic-native wrapping)
552+ expect ( frames . every ( ( f ) => f . eventType === "chunk" ) ) . toBe ( true ) ;
553+
554+ // message_start
555+ expect ( frames [ 0 ] . payload ) . toMatchObject ( {
556+ type : "message_start" ,
557+ message : { role : "assistant" , model : MODEL_ID } ,
558+ } ) ;
559+
560+ // Text content_block_start at index 0
561+ const textBlockStart = frames . find (
562+ ( f ) =>
563+ ( f . payload as { type ?: string } ) . type === "content_block_start" &&
564+ ( f . payload as { content_block ?: { type : string } } ) . content_block ?. type === "text" ,
565+ ) ;
566+ expect ( textBlockStart ) . toBeDefined ( ) ;
567+ expect ( textBlockStart ! . payload ) . toMatchObject ( {
568+ type : "content_block_start" ,
569+ index : 0 ,
570+ content_block : { type : "text" , text : "" } ,
571+ } ) ;
572+
573+ // Text deltas — collect and verify full text
574+ const textDeltas = frames . filter (
575+ ( f ) =>
576+ ( f . payload as { type ?: string } ) . type === "content_block_delta" &&
577+ ( f . payload as { delta ?: { type ?: string } } ) . delta ?. type === "text_delta" ,
578+ ) ;
579+ expect ( textDeltas . length ) . toBeGreaterThanOrEqual ( 1 ) ;
580+ const fullText = textDeltas
581+ . map ( ( f ) => ( f . payload as { delta : { text : string } } ) . delta . text )
582+ . join ( "" ) ;
583+ expect ( fullText ) . toBe ( "Let me look that up." ) ;
584+
585+ // Tool use content_block_start at index 1
586+ const toolBlockStart = frames . find (
587+ ( f ) =>
588+ ( f . payload as { type ?: string } ) . type === "content_block_start" &&
589+ ( f . payload as { content_block ?: { type : string } } ) . content_block ?. type === "tool_use" ,
590+ ) ;
591+ expect ( toolBlockStart ) . toBeDefined ( ) ;
592+ const toolStartPayload = toolBlockStart ! . payload as {
593+ index : number ;
594+ content_block : { type : string ; id : string ; name : string ; input : object } ;
595+ } ;
596+ expect ( toolStartPayload . index ) . toBe ( 1 ) ;
597+ expect ( toolStartPayload . content_block . name ) . toBe ( "web_search" ) ;
598+ expect ( toolStartPayload . content_block . id ) . toBeDefined ( ) ;
599+
600+ // Tool deltas — input_json_delta with partial_json
601+ const toolDeltas = frames . filter (
602+ ( f ) =>
603+ ( f . payload as { type ?: string } ) . type === "content_block_delta" &&
604+ ( f . payload as { delta ?: { type ?: string } } ) . delta ?. type === "input_json_delta" ,
605+ ) ;
606+ expect ( toolDeltas . length ) . toBeGreaterThanOrEqual ( 1 ) ;
607+ const fullJson = toolDeltas
608+ . map ( ( f ) => ( f . payload as { delta : { partial_json : string } } ) . delta . partial_json )
609+ . join ( "" ) ;
610+ expect ( JSON . parse ( fullJson ) ) . toEqual ( { query : "vitest testing" } ) ;
611+
612+ // message_delta with stop_reason "tool_use"
613+ const msgDelta = frames . find ( ( f ) => ( f . payload as { type ?: string } ) . type === "message_delta" ) ;
614+ expect ( msgDelta ! . payload ) . toMatchObject ( {
615+ type : "message_delta" ,
616+ delta : { stop_reason : "tool_use" } ,
617+ } ) ;
618+
619+ // message_stop
620+ const msgStop = frames . find ( ( f ) => ( f . payload as { type ?: string } ) . type === "message_stop" ) ;
621+ expect ( msgStop ) . toBeDefined ( ) ;
622+ } ) ;
623+ } ) ;
624+
521625// ─── invoke-with-response-stream: interruption ─────────────────────────────
522626
523627describe ( "POST /model/{modelId}/invoke-with-response-stream (interruption)" , ( ) => {
@@ -759,6 +863,93 @@ describe("POST /model/{modelId}/converse-stream", () => {
759863 } ) ;
760864} ) ;
761865
866+ // ─── converse-stream: content + tool calls ─────────────────────────────────
867+
868+ describe ( "POST /model/{modelId}/converse-stream (content + toolCalls)" , ( ) => {
869+ const MODEL_ID = "anthropic.claude-3-5-sonnet-20241022-v2:0" ;
870+
871+ it ( "streams text block followed by toolUse block in Converse camelCase format" , async ( ) => {
872+ instance = await createServer ( allFixtures ) ;
873+ const res = await postBinary ( `${ instance . url } /model/${ MODEL_ID } /converse-stream` , {
874+ messages : [ { role : "user" , content : [ { text : "search-and-explain" } ] } ] ,
875+ } ) ;
876+
877+ expect ( res . status ) . toBe ( 200 ) ;
878+ expect ( res . headers [ "content-type" ] ) . toBe ( "application/vnd.amazon.eventstream" ) ;
879+
880+ const frames = parseFrames ( res . body ) ;
881+
882+ // messageStart
883+ expect ( frames [ 0 ] . eventType ) . toBe ( "messageStart" ) ;
884+ expect ( frames [ 0 ] . payload ) . toEqual ( { messageStart : { role : "assistant" } } ) ;
885+
886+ // Text contentBlockStart
887+ const textBlockStart = frames . find (
888+ ( f ) =>
889+ f . eventType === "contentBlockStart" &&
890+ ( f . payload as { contentBlockStart ?: { start ?: { type ?: string } } } ) . contentBlockStart
891+ ?. start ?. type === "text" ,
892+ ) ;
893+ expect ( textBlockStart ) . toBeDefined ( ) ;
894+
895+ // Text deltas
896+ const textDeltas = frames . filter (
897+ ( f ) =>
898+ f . eventType === "contentBlockDelta" &&
899+ ( f . payload as { contentBlockDelta ?: { delta ?: { text ?: string } } } ) . contentBlockDelta
900+ ?. delta ?. text !== undefined ,
901+ ) ;
902+ expect ( textDeltas . length ) . toBeGreaterThanOrEqual ( 1 ) ;
903+ const fullText = textDeltas
904+ . map (
905+ ( f ) =>
906+ ( f . payload as { contentBlockDelta : { delta : { text : string } } } ) . contentBlockDelta . delta
907+ . text ,
908+ )
909+ . join ( "" ) ;
910+ expect ( fullText ) . toBe ( "Let me look that up." ) ;
911+
912+ // Tool use contentBlockStart
913+ const toolBlockStart = frames . find (
914+ ( f ) =>
915+ f . eventType === "contentBlockStart" &&
916+ ( f . payload as { contentBlockStart ?: { start ?: { toolUse ?: unknown } } } ) . contentBlockStart
917+ ?. start ?. toolUse !== undefined ,
918+ ) ;
919+ expect ( toolBlockStart ) . toBeDefined ( ) ;
920+ const toolStartPayload = toolBlockStart ! . payload as {
921+ contentBlockIndex : number ;
922+ contentBlockStart : {
923+ contentBlockIndex : number ;
924+ start : { toolUse : { toolUseId : string ; name : string } } ;
925+ } ;
926+ } ;
927+ expect ( toolStartPayload . contentBlockStart . start . toolUse . name ) . toBe ( "web_search" ) ;
928+ expect ( toolStartPayload . contentBlockStart . start . toolUse . toolUseId ) . toBeDefined ( ) ;
929+
930+ // Tool deltas — toolUse.input
931+ const toolDeltas = frames . filter (
932+ ( f ) =>
933+ f . eventType === "contentBlockDelta" &&
934+ ( f . payload as { contentBlockDelta ?: { delta ?: { toolUse ?: unknown } } } ) . contentBlockDelta
935+ ?. delta ?. toolUse !== undefined ,
936+ ) ;
937+ expect ( toolDeltas . length ) . toBeGreaterThanOrEqual ( 1 ) ;
938+ const fullJson = toolDeltas
939+ . map (
940+ ( f ) =>
941+ ( f . payload as { contentBlockDelta : { delta : { toolUse : { input : string } } } } )
942+ . contentBlockDelta . delta . toolUse . input ,
943+ )
944+ . join ( "" ) ;
945+ expect ( JSON . parse ( fullJson ) ) . toEqual ( { query : "vitest testing" } ) ;
946+
947+ // messageStop with tool_use stop reason
948+ const msgStop = frames . find ( ( f ) => f . eventType === "messageStop" ) ;
949+ expect ( msgStop ! . payload ) . toEqual ( { stopReason : "tool_use" } ) ;
950+ } ) ;
951+ } ) ;
952+
762953// ─── converseToCompletionRequest unit tests ─────────────────────────────────
763954
764955describe ( "converseToCompletionRequest" , ( ) => {
@@ -1080,7 +1271,9 @@ describe("POST /model/{modelId}/invoke-with-response-stream (empty content)", ()
10801271 expect (
10811272 frames . find ( ( f ) => ( f . payload as { type ?: string } ) . type === "content_block_stop" ) ,
10821273 ) . toBeDefined ( ) ;
1083- expect ( frames . find ( ( f ) => ( f . payload as { type ?: string } ) . type === "message_stop" ) ) . toBeDefined ( ) ;
1274+ expect (
1275+ frames . find ( ( f ) => ( f . payload as { type ?: string } ) . type === "message_stop" ) ,
1276+ ) . toBeDefined ( ) ;
10841277
10851278 // Content deltas should be zero (empty string → no chunks)
10861279 const deltas = frames . filter (
0 commit comments