@@ -36,8 +36,9 @@ describe('GroqProvider', () => {
3636 expect ( provider . name ) . toBe ( 'groq' ) ;
3737 expect ( provider . models ) . toContain ( 'llama-3.3-70b-versatile' ) ;
3838 expect ( provider . models ) . toContain ( 'llama-3.1-8b-instant' ) ;
39+ expect ( provider . models ) . toContain ( 'openai/gpt-oss-120b' ) ;
3940 expect ( provider . supportsStreaming ) . toBe ( true ) ;
40- expect ( provider . supportsTools ) . toBe ( false ) ;
41+ expect ( provider . supportsTools ) . toBe ( true ) ;
4142 expect ( provider . supportsBatching ) . toBe ( false ) ;
4243 } ) ;
4344
@@ -69,7 +70,7 @@ describe('GroqProvider', () => {
6970 describe ( 'getModels' , ( ) => {
7071 it ( 'should return available models' , ( ) => {
7172 const models = provider . getModels ( ) ;
72- expect ( models ) . toEqual ( [ 'llama-3.3-70b-versatile' , 'llama-3.1-8b-instant' ] ) ;
73+ expect ( models ) . toEqual ( [ 'llama-3.3-70b-versatile' , 'llama-3.1-8b-instant' , 'openai/gpt-oss-120b' ] ) ;
7374 } ) ;
7475
7576 it ( 'should return a copy of the models array' , ( ) => {
@@ -222,6 +223,14 @@ describe('GroqProvider', () => {
222223 expect ( cost ) . toBeGreaterThan ( 0 ) ;
223224 } ) ;
224225
226+ it ( 'should estimate cost for openai/gpt-oss-120b' , ( ) => {
227+ const cost = provider . estimateCost ( {
228+ ...testRequest ,
229+ model : 'openai/gpt-oss-120b'
230+ } ) ;
231+ expect ( cost ) . toBeGreaterThan ( 0 ) ;
232+ } ) ;
233+
225234 it ( 'should return 0 for unknown model' , ( ) => {
226235 const cost = provider . estimateCost ( {
227236 ...testRequest ,
@@ -237,6 +246,175 @@ describe('GroqProvider', () => {
237246 } ) ;
238247 } ) ;
239248
249+ describe ( 'tool calling' , ( ) => {
250+ const toolRequest : LLMRequest = {
251+ messages : [ { role : 'user' , content : 'What is the weather?' } ] ,
252+ model : 'openai/gpt-oss-120b' ,
253+ maxTokens : 100 ,
254+ tools : [ {
255+ type : 'function' ,
256+ function : {
257+ name : 'get_weather' ,
258+ description : 'Get current weather' ,
259+ parameters : { type : 'object' , properties : { location : { type : 'string' } } }
260+ }
261+ } ] ,
262+ toolChoice : 'auto'
263+ } ;
264+
265+ const toolCallResponse = {
266+ id : 'chatcmpl-tool-1' ,
267+ object : 'chat.completion' ,
268+ created : 1700000000 ,
269+ model : 'openai/gpt-oss-120b' ,
270+ choices : [ {
271+ index : 0 ,
272+ message : {
273+ role : 'assistant' ,
274+ content : null ,
275+ tool_calls : [ {
276+ id : 'call_abc123' ,
277+ type : 'function' ,
278+ function : { name : 'get_weather' , arguments : '{"location":"London"}' }
279+ } ]
280+ } ,
281+ finish_reason : 'tool_calls'
282+ } ] ,
283+ usage : { prompt_tokens : 20 , completion_tokens : 15 , total_tokens : 35 }
284+ } ;
285+
286+ it ( 'should pass tools and parse tool_calls in response' , async ( ) => {
287+ mockFetch . mockResolvedValueOnce ( {
288+ ok : true ,
289+ json : async ( ) => toolCallResponse ,
290+ headers : new Headers ( { 'content-type' : 'application/json' } )
291+ } ) ;
292+
293+ const response = await provider . generateResponse ( toolRequest ) ;
294+
295+ // Verify response has tool calls
296+ expect ( response . toolCalls ) . toHaveLength ( 1 ) ;
297+ expect ( response . toolCalls ! [ 0 ] . id ) . toBe ( 'call_abc123' ) ;
298+ expect ( response . toolCalls ! [ 0 ] . function . name ) . toBe ( 'get_weather' ) ;
299+ expect ( response . toolCalls ! [ 0 ] . function . arguments ) . toBe ( '{"location":"London"}' ) ;
300+ expect ( response . finishReason ) . toBe ( 'tool_calls' ) ;
301+
302+ // Verify request body included tools
303+ const body = JSON . parse ( mockFetch . mock . calls [ 0 ] [ 1 ] . body ) ;
304+ expect ( body . tools ) . toHaveLength ( 1 ) ;
305+ expect ( body . tools [ 0 ] . function . name ) . toBe ( 'get_weather' ) ;
306+ expect ( body . tool_choice ) . toBe ( 'auto' ) ;
307+ } ) ;
308+
309+ it ( 'should handle multi-turn tool conversations' , async ( ) => {
310+ const multiTurnRequest : LLMRequest = {
311+ messages : [
312+ { role : 'user' , content : 'What is the weather?' } ,
313+ {
314+ role : 'assistant' ,
315+ content : '' ,
316+ toolCalls : [ {
317+ id : 'call_abc123' ,
318+ type : 'function' ,
319+ function : { name : 'get_weather' , arguments : '{"location":"London"}' }
320+ } ]
321+ } ,
322+ {
323+ role : 'user' ,
324+ content : '' ,
325+ toolResults : [ {
326+ id : 'call_abc123' ,
327+ output : '{"temp": 15, "condition": "cloudy"}'
328+ } ]
329+ }
330+ ] ,
331+ model : 'openai/gpt-oss-120b' ,
332+ maxTokens : 100 ,
333+ tools : toolRequest . tools
334+ } ;
335+
336+ mockFetch . mockResolvedValueOnce ( {
337+ ok : true ,
338+ json : async ( ) => ( {
339+ id : 'chatcmpl-tool-2' ,
340+ object : 'chat.completion' ,
341+ created : 1700000000 ,
342+ model : 'openai/gpt-oss-120b' ,
343+ choices : [ {
344+ index : 0 ,
345+ message : { role : 'assistant' , content : 'It is 15°C and cloudy in London.' } ,
346+ finish_reason : 'stop'
347+ } ] ,
348+ usage : { prompt_tokens : 40 , completion_tokens : 15 , total_tokens : 55 }
349+ } ) ,
350+ headers : new Headers ( { 'content-type' : 'application/json' } )
351+ } ) ;
352+
353+ const response = await provider . generateResponse ( multiTurnRequest ) ;
354+
355+ expect ( response . message ) . toBe ( 'It is 15°C and cloudy in London.' ) ;
356+
357+ // Verify the serialized messages include tool_calls and tool role
358+ const body = JSON . parse ( mockFetch . mock . calls [ 0 ] [ 1 ] . body ) ;
359+ const assistantMsg = body . messages . find ( ( m : Record < string , unknown > ) => m . role === 'assistant' ) ;
360+ expect ( assistantMsg . tool_calls ) . toHaveLength ( 1 ) ;
361+ expect ( assistantMsg . tool_calls [ 0 ] . id ) . toBe ( 'call_abc123' ) ;
362+
363+ const toolMsg = body . messages . find ( ( m : Record < string , unknown > ) => m . role === 'tool' ) ;
364+ expect ( toolMsg . tool_call_id ) . toBe ( 'call_abc123' ) ;
365+ expect ( toolMsg . content ) . toBe ( '{"temp": 15, "condition": "cloudy"}' ) ;
366+ } ) ;
367+
368+ it ( 'should not include tools for non-tool-capable models' , async ( ) => {
369+ mockFetch . mockResolvedValueOnce ( {
370+ ok : true ,
371+ json : async ( ) => ( {
372+ id : 'chatcmpl-123' ,
373+ object : 'chat.completion' ,
374+ created : 1700000000 ,
375+ model : 'llama-3.1-8b-instant' ,
376+ choices : [ {
377+ index : 0 ,
378+ message : { role : 'assistant' , content : 'I cannot call tools.' } ,
379+ finish_reason : 'stop'
380+ } ] ,
381+ usage : { prompt_tokens : 10 , completion_tokens : 5 , total_tokens : 15 }
382+ } ) ,
383+ headers : new Headers ( { 'content-type' : 'application/json' } )
384+ } ) ;
385+
386+ await provider . generateResponse ( {
387+ ...toolRequest ,
388+ model : 'llama-3.1-8b-instant'
389+ } ) ;
390+
391+ const body = JSON . parse ( mockFetch . mock . calls [ 0 ] [ 1 ] . body ) ;
392+ expect ( body . tools ) . toBeUndefined ( ) ;
393+ expect ( body . tool_choice ) . toBeUndefined ( ) ;
394+ } ) ;
395+
396+ it ( 'should support tool calling on llama-3.3-70b-versatile' , async ( ) => {
397+ mockFetch . mockResolvedValueOnce ( {
398+ ok : true ,
399+ json : async ( ) => ( {
400+ ...toolCallResponse ,
401+ model : 'llama-3.3-70b-versatile'
402+ } ) ,
403+ headers : new Headers ( { 'content-type' : 'application/json' } )
404+ } ) ;
405+
406+ const response = await provider . generateResponse ( {
407+ ...toolRequest ,
408+ model : 'llama-3.3-70b-versatile'
409+ } ) ;
410+
411+ expect ( response . toolCalls ) . toHaveLength ( 1 ) ;
412+
413+ const body = JSON . parse ( mockFetch . mock . calls [ 0 ] [ 1 ] . body ) ;
414+ expect ( body . tools ) . toHaveLength ( 1 ) ;
415+ } ) ;
416+ } ) ;
417+
240418 describe ( 'healthCheck' , ( ) => {
241419 it ( 'should return true when API is healthy' , async ( ) => {
242420 mockFetch . mockResolvedValueOnce ( {
0 commit comments