11import { beforeAll , describe , expect , it } from "vitest" ;
2- import { OpenRouter } from "../../src/sdk/sdk.js" ;
2+ import { OpenRouter , ToolType } from "../../src/sdk/sdk.js" ;
33import { Message } from "../../src/models/message.js" ;
4+ import { AssistantMessage } from "../../src/models/assistantmessage.js" ;
5+ import { ToolResponseMessage } from "../../src/models/toolresponsemessage.js" ;
6+ import { z } from "zod/v4" ;
47
58describe ( "callModel E2E Tests" , ( ) => {
69 let client : OpenRouter ;
@@ -226,7 +229,7 @@ describe("callModel E2E Tests", () => {
226229 ] ,
227230 } ) ;
228231
229- const messages : Message [ ] = [ ] ;
232+ const messages : ( AssistantMessage | ToolResponseMessage ) [ ] = [ ] ;
230233
231234 for await ( const message of response . getNewMessagesStream ( ) ) {
232235 expect ( message ) . toBeDefined ( ) ;
@@ -248,6 +251,212 @@ describe("callModel E2E Tests", () => {
248251 expect ( lastText . length ) . toBeGreaterThanOrEqual ( firstText . length ) ;
249252 }
250253 } , 15000 ) ;
254+
255+ it ( "should return AssistantMessages with correct shape" , async ( ) => {
256+ const response = client . callModel ( {
257+ model : "meta-llama/llama-3.2-1b-instruct" ,
258+ input : [
259+ {
260+ role : "user" ,
261+ content : "Say 'hello world'." ,
262+ } ,
263+ ] ,
264+ } ) ;
265+
266+ const messages : ( AssistantMessage | ToolResponseMessage ) [ ] = [ ] ;
267+
268+ for await ( const message of response . getNewMessagesStream ( ) ) {
269+ messages . push ( message ) ;
270+
271+ // Deep validation of AssistantMessage shape
272+ expect ( message ) . toHaveProperty ( "role" ) ;
273+ expect ( message ) . toHaveProperty ( "content" ) ;
274+
275+ if ( message . role === "assistant" ) {
276+ // Validate AssistantMessage structure
277+ expect ( message . role ) . toBe ( "assistant" ) ;
278+
279+ // content must be string, array, null, or undefined
280+ const contentType = typeof message . content ;
281+ const isValidContent =
282+ contentType === "string" ||
283+ Array . isArray ( message . content ) ||
284+ message . content === null ||
285+ message . content === undefined ;
286+ expect ( isValidContent ) . toBe ( true ) ;
287+
288+ // If content is an array, each item must have a type
289+ if ( Array . isArray ( message . content ) ) {
290+ for ( const item of message . content ) {
291+ expect ( item ) . toHaveProperty ( "type" ) ;
292+ expect ( typeof item . type ) . toBe ( "string" ) ;
293+ }
294+ }
295+
296+ // If toolCalls present, validate their shape
297+ if ( "toolCalls" in message && message . toolCalls ) {
298+ expect ( Array . isArray ( message . toolCalls ) ) . toBe ( true ) ;
299+ for ( const toolCall of message . toolCalls ) {
300+ expect ( toolCall ) . toHaveProperty ( "id" ) ;
301+ expect ( toolCall ) . toHaveProperty ( "type" ) ;
302+ expect ( toolCall ) . toHaveProperty ( "function" ) ;
303+ expect ( typeof toolCall . id ) . toBe ( "string" ) ;
304+ expect ( typeof toolCall . type ) . toBe ( "string" ) ;
305+ expect ( toolCall . function ) . toHaveProperty ( "name" ) ;
306+ expect ( toolCall . function ) . toHaveProperty ( "arguments" ) ;
307+ expect ( typeof toolCall . function . name ) . toBe ( "string" ) ;
308+ expect ( typeof toolCall . function . arguments ) . toBe ( "string" ) ;
309+ }
310+ }
311+ }
312+ }
313+
314+ expect ( messages . length ) . toBeGreaterThan ( 0 ) ;
315+
316+ // Verify last message has the complete assistant response shape
317+ const lastMessage = messages [ messages . length - 1 ] ;
318+ expect ( lastMessage . role ) . toBe ( "assistant" ) ;
319+ } , 15000 ) ;
320+
321+ it ( "should include ToolResponseMessages with correct shape when tools are executed" , async ( ) => {
322+ const response = client . callModel ( {
323+ model : "openai/gpt-4o-mini" ,
324+ input : [
325+ {
326+ role : "user" ,
327+ content : "What's the weather in Tokyo? Use the get_weather tool." ,
328+ } ,
329+ ] ,
330+ tools : [
331+ {
332+ type : ToolType . Function ,
333+ function : {
334+ name : "get_weather" ,
335+ description : "Get weather for a location" ,
336+ inputSchema : z . object ( {
337+ location : z . string ( ) . describe ( "City name" ) ,
338+ } ) ,
339+ outputSchema : z . object ( {
340+ temperature : z . number ( ) ,
341+ condition : z . string ( ) ,
342+ } ) ,
343+ execute : async ( params : { location : string } ) => {
344+ return {
345+ temperature : 22 ,
346+ condition : "Sunny" ,
347+ } ;
348+ } ,
349+ } ,
350+ } ,
351+ ] ,
352+ } ) ;
353+
354+ const messages : ( AssistantMessage | ToolResponseMessage ) [ ] = [ ] ;
355+ let hasAssistantMessage = false ;
356+ let hasToolResponseMessage = false ;
357+
358+ for await ( const message of response . getNewMessagesStream ( ) ) {
359+ messages . push ( message ) ;
360+
361+ // Validate each message has correct shape based on role
362+ expect ( message ) . toHaveProperty ( "role" ) ;
363+ expect ( message ) . toHaveProperty ( "content" ) ;
364+
365+ if ( message . role === "assistant" ) {
366+ hasAssistantMessage = true ;
367+
368+ // Validate AssistantMessage shape
369+ const contentType = typeof message . content ;
370+ const isValidContent =
371+ contentType === "string" ||
372+ Array . isArray ( message . content ) ||
373+ message . content === null ||
374+ message . content === undefined ;
375+ expect ( isValidContent ) . toBe ( true ) ;
376+ } else if ( message . role === "tool" ) {
377+ hasToolResponseMessage = true ;
378+
379+ // Deep validation of ToolResponseMessage shape
380+ expect ( message ) . toHaveProperty ( "toolCallId" ) ;
381+ expect ( typeof ( message as ToolResponseMessage ) . toolCallId ) . toBe ( "string" ) ;
382+ expect ( ( message as ToolResponseMessage ) . toolCallId . length ) . toBeGreaterThan ( 0 ) ;
383+
384+ // content must be string or array
385+ const contentType = typeof message . content ;
386+ const isValidContent =
387+ contentType === "string" ||
388+ Array . isArray ( message . content ) ;
389+ expect ( isValidContent ) . toBe ( true ) ;
390+
391+ // If content is string, it should be parseable JSON (our tool result)
392+ if ( typeof message . content === "string" && message . content . length > 0 ) {
393+ const parsed = JSON . parse ( message . content ) ;
394+ expect ( parsed ) . toBeDefined ( ) ;
395+ // Verify it matches our tool output schema
396+ expect ( parsed ) . toHaveProperty ( "temperature" ) ;
397+ expect ( parsed ) . toHaveProperty ( "condition" ) ;
398+ expect ( typeof parsed . temperature ) . toBe ( "number" ) ;
399+ expect ( typeof parsed . condition ) . toBe ( "string" ) ;
400+ }
401+ }
402+ }
403+
404+ expect ( messages . length ) . toBeGreaterThan ( 0 ) ;
405+ // We must have tool responses since we have an executable tool
406+ expect ( hasToolResponseMessage ) . toBe ( true ) ;
407+
408+ // If the model provided a final text response, verify proper ordering
409+ if ( hasAssistantMessage ) {
410+ const lastToolIndex = messages . reduce ( ( lastIdx , m , i ) =>
411+ m . role === "tool" ? i : lastIdx , - 1 ) ;
412+ const lastAssistantIndex = messages . reduce ( ( lastIdx , m , i ) =>
413+ m . role === "assistant" ? i : lastIdx , - 1 ) ;
414+
415+ // The final assistant message should come after tool responses
416+ if ( lastToolIndex !== - 1 && lastAssistantIndex !== - 1 ) {
417+ expect ( lastAssistantIndex ) . toBeGreaterThan ( lastToolIndex ) ;
418+ }
419+ }
420+ } , 30000 ) ;
421+
422+ it ( "should return messages with all required fields and correct types" , async ( ) => {
423+ const response = client . callModel ( {
424+ model : "meta-llama/llama-3.2-1b-instruct" ,
425+ input : [
426+ {
427+ role : "user" ,
428+ content : "Count from 1 to 3." ,
429+ } ,
430+ ] ,
431+ } ) ;
432+
433+ for await ( const message of response . getNewMessagesStream ( ) ) {
434+ // role must be a string and one of the valid values
435+ expect ( typeof message . role ) . toBe ( "string" ) ;
436+ expect ( [ "assistant" , "tool" ] ) . toContain ( message . role ) ;
437+
438+ // content must exist (even if null)
439+ expect ( "content" in message ) . toBe ( true ) ;
440+
441+ if ( message . role === "assistant" ) {
442+ // AssistantMessage specific validations
443+ const validContentTypes = [ "string" , "object" , "undefined" ] ;
444+ expect ( validContentTypes ) . toContain ( typeof message . content ) ;
445+
446+ // If content is array, validate structure
447+ if ( Array . isArray ( message . content ) ) {
448+ expect ( message . content . every ( item =>
449+ typeof item === "object" && item !== null && "type" in item
450+ ) ) . toBe ( true ) ;
451+ }
452+ } else if ( message . role === "tool" ) {
453+ // ToolResponseMessage specific validations
454+ const toolMsg = message as ToolResponseMessage ;
455+ expect ( typeof toolMsg . toolCallId ) . toBe ( "string" ) ;
456+ expect ( toolMsg . toolCallId . length ) . toBeGreaterThan ( 0 ) ;
457+ }
458+ }
459+ } , 15000 ) ;
251460 } ) ;
252461
253462 describe ( "response.reasoningStream - Streaming reasoning deltas" , ( ) => {
0 commit comments