@@ -95,6 +95,49 @@ describe('inference.ts', () => {
9595 expect ( result ) . toBeNull ( )
9696 expect ( core . info ) . toHaveBeenCalledWith ( 'Model response: No response content' )
9797 } )
98+
99+ it ( 'includes response format when specified' , async ( ) => {
100+ const requestWithResponseFormat = {
101+ ...mockRequest ,
102+ responseFormat : {
103+ type : 'json_schema' as const ,
104+ json_schema : { type : 'object' } ,
105+ } ,
106+ }
107+
108+ const mockResponse = {
109+ choices : [
110+ {
111+ message : {
112+ content : '{"result": "success"}' ,
113+ } ,
114+ } ,
115+ ] ,
116+ }
117+
118+ mockCreate . mockResolvedValue ( mockResponse )
119+
120+ const result = await simpleInference ( requestWithResponseFormat )
121+
122+ expect ( result ) . toBe ( '{"result": "success"}' )
123+
124+ // Verify response format was included in the request
125+ expect ( mockCreate ) . toHaveBeenCalledWith ( {
126+ messages : [
127+ {
128+ role : 'system' ,
129+ content : 'You are a test assistant' ,
130+ } ,
131+ {
132+ role : 'user' ,
133+ content : 'Hello, AI!' ,
134+ } ,
135+ ] ,
136+ max_tokens : 100 ,
137+ model : 'gpt-4' ,
138+ response_format : requestWithResponseFormat . responseFormat ,
139+ } )
140+ } )
98141 } )
99142
100143 describe ( 'mcpInference' , ( ) => {
@@ -140,6 +183,7 @@ describe('inference.ts', () => {
140183 // eslint-disable-next-line @typescript-eslint/no-explicit-any
141184 const callArgs = mockCreate . mock . calls [ 0 ] [ 0 ] as any
142185 expect ( callArgs . tools ) . toEqual ( mockMcpClient . tools )
186+ expect ( callArgs . response_format ) . toBeUndefined ( )
143187 expect ( callArgs . model ) . toBe ( 'gpt-4' )
144188 expect ( callArgs . max_tokens ) . toBe ( 100 )
145189 } )
@@ -315,5 +359,191 @@ describe('inference.ts', () => {
315359
316360 expect ( result ) . toBe ( 'Second message' )
317361 } )
362+
363+ it ( 'makes additional loop with response format when no tool calls are made' , async ( ) => {
364+ const requestWithResponseFormat = {
365+ ...mockRequest ,
366+ responseFormat : {
367+ type : 'json_schema' as const ,
368+ json_schema : { type : 'object' } ,
369+ } ,
370+ }
371+
372+ // First response without tool calls
373+ const firstResponse = {
374+ choices : [
375+ {
376+ message : {
377+ content : 'First response' ,
378+ tool_calls : null ,
379+ } ,
380+ } ,
381+ ] ,
382+ }
383+
384+ // Second response with response format applied
385+ const secondResponse = {
386+ choices : [
387+ {
388+ message : {
389+ content : '{"result": "formatted response"}' ,
390+ tool_calls : null ,
391+ } ,
392+ } ,
393+ ] ,
394+ }
395+
396+ mockCreate . mockResolvedValueOnce ( firstResponse ) . mockResolvedValueOnce ( secondResponse )
397+
398+ const result = await mcpInference ( requestWithResponseFormat , mockMcpClient )
399+
400+ expect ( result ) . toBe ( '{"result": "formatted response"}' )
401+ expect ( mockCreate ) . toHaveBeenCalledTimes ( 2 )
402+ expect ( core . info ) . toHaveBeenCalledWith ( 'Making one more MCP loop with the requested response format...' )
403+
404+ // First call should have tools but no response format
405+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
406+ const firstCall = mockCreate . mock . calls [ 0 ] [ 0 ] as any
407+ expect ( firstCall . tools ) . toEqual ( mockMcpClient . tools )
408+ expect ( firstCall . response_format ) . toBeUndefined ( )
409+
410+ // Second call should have response format but no tools
411+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
412+ const secondCall = mockCreate . mock . calls [ 1 ] [ 0 ] as any
413+ expect ( secondCall . tools ) . toBeUndefined ( )
414+ expect ( secondCall . response_format ) . toEqual ( requestWithResponseFormat . responseFormat )
415+
416+ // Second call should include the user message requesting JSON format
417+ expect ( secondCall . messages ) . toHaveLength ( 5 ) // system, user, assistant, user, assistant
418+ expect ( secondCall . messages [ 3 ] . role ) . toBe ( 'user' )
419+ expect ( secondCall . messages [ 3 ] . content ) . toContain ( 'Please provide your response in the exact' )
420+ } )
421+
422+ it ( 'uses response format only on final iteration after tool calls' , async ( ) => {
423+ const requestWithResponseFormat = {
424+ ...mockRequest ,
425+ responseFormat : {
426+ type : 'json_schema' as const ,
427+ json_schema : { type : 'object' } ,
428+ } ,
429+ }
430+
431+ const toolCalls = [
432+ {
433+ id : 'call-123' ,
434+ function : {
435+ name : 'test-tool' ,
436+ arguments : '{"param": "value"}' ,
437+ } ,
438+ } ,
439+ ]
440+
441+ const toolResults = [
442+ {
443+ tool_call_id : 'call-123' ,
444+ role : 'tool' ,
445+ name : 'test-tool' ,
446+ content : 'Tool result' ,
447+ } ,
448+ ]
449+
450+ // First response with tool calls
451+ const firstResponse = {
452+ choices : [
453+ {
454+ message : {
455+ content : 'Using tool' ,
456+ tool_calls : toolCalls ,
457+ } ,
458+ } ,
459+ ] ,
460+ }
461+
462+ // Second response without tool calls, but should trigger final message loop
463+ const secondResponse = {
464+ choices : [
465+ {
466+ message : {
467+ content : 'Intermediate result' ,
468+ tool_calls : null ,
469+ } ,
470+ } ,
471+ ] ,
472+ }
473+
474+ // Third response with response format
475+ const thirdResponse = {
476+ choices : [
477+ {
478+ message : {
479+ content : '{"final": "result"}' ,
480+ tool_calls : null ,
481+ } ,
482+ } ,
483+ ] ,
484+ }
485+
486+ mockCreate
487+ . mockResolvedValueOnce ( firstResponse )
488+ . mockResolvedValueOnce ( secondResponse )
489+ . mockResolvedValueOnce ( thirdResponse )
490+
491+ mockExecuteToolCalls . mockResolvedValue ( toolResults )
492+
493+ const result = await mcpInference ( requestWithResponseFormat , mockMcpClient )
494+
495+ expect ( result ) . toBe ( '{"final": "result"}' )
496+ expect ( mockCreate ) . toHaveBeenCalledTimes ( 3 )
497+
498+ // First call: tools but no response format
499+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
500+ const firstCall = mockCreate . mock . calls [ 0 ] [ 0 ] as any
501+ expect ( firstCall . tools ) . toEqual ( mockMcpClient . tools )
502+ expect ( firstCall . response_format ) . toBeUndefined ( )
503+
504+ // Second call: tools but no response format (after tool execution)
505+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
506+ const secondCall = mockCreate . mock . calls [ 1 ] [ 0 ] as any
507+ expect ( secondCall . tools ) . toEqual ( mockMcpClient . tools )
508+ expect ( secondCall . response_format ) . toBeUndefined ( )
509+
510+ // Third call: response format but no tools (final message)
511+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
512+ const thirdCall = mockCreate . mock . calls [ 2 ] [ 0 ] as any
513+ expect ( thirdCall . tools ) . toBeUndefined ( )
514+ expect ( thirdCall . response_format ) . toEqual ( requestWithResponseFormat . responseFormat )
515+ } )
516+
517+ it ( 'returns immediately when response format is set and finalMessage is already true' , async ( ) => {
518+ const requestWithResponseFormat = {
519+ ...mockRequest ,
520+ responseFormat : {
521+ type : 'json_schema' as const ,
522+ json_schema : { type : 'object' } ,
523+ } ,
524+ }
525+
526+ // Response without tool calls on what would be the final message iteration
527+ const mockResponse = {
528+ choices : [
529+ {
530+ message : {
531+ content : '{"immediate": "result"}' ,
532+ tool_calls : null ,
533+ } ,
534+ } ,
535+ ] ,
536+ }
537+
538+ mockCreate . mockResolvedValue ( mockResponse )
539+
540+ // We need to test a scenario where finalMessage would already be true
541+ // This happens when we're already in the final iteration
542+ const result = await mcpInference ( requestWithResponseFormat , mockMcpClient )
543+
544+ // The function should make two calls: one normal, then one with response format
545+ expect ( mockCreate ) . toHaveBeenCalledTimes ( 2 )
546+ expect ( result ) . toBe ( '{"immediate": "result"}' )
547+ } )
318548 } )
319549} )
0 commit comments