@@ -245,3 +245,130 @@ describe("Strategy Selection", () => {
245245 expect ( hasExtractToolCalls ) . toBe ( false ) ;
246246 } ) ;
247247} ) ;
248+
249+ describe ( "Thinking models with responseFormat" , ( ) => {
250+ it ( "should parse structured response from thinking model with providerStrategy" , async ( ) => {
251+ // Use Anthropic model with thinking enabled
252+ const model = new ChatAnthropic ( {
253+ model : "claude-haiku-4-5-20251001" ,
254+ thinking : { type : "enabled" , budget_tokens : 5000 } ,
255+ maxTokens : 15000 ,
256+ } ) ;
257+
258+ // Define tools that should be called before returning structured output
259+ const { tool : getDetailByName , mock : getDetailByNameMock } = makeTool (
260+ ( { name } : { name : string } ) =>
261+ JSON . stringify ( {
262+ name,
263+ color : "blue" ,
264+ size : "large" ,
265+ } ) ,
266+ {
267+ name : "get_detail_by_name" ,
268+ description : "Get details about a shape by its name" ,
269+ schema : z . object ( {
270+ name : z . string ( ) ,
271+ } ) ,
272+ }
273+ ) ;
274+
275+ const { tool : deleteShape , mock : deleteShapeMock } = makeTool (
276+ ( { name } : { name : string } ) =>
277+ `Shape "${ name } " has been deleted successfully.` ,
278+ {
279+ name : "delete_shape" ,
280+ description : "Delete a shape by its name" ,
281+ schema : z . object ( {
282+ name : z . string ( ) ,
283+ } ) ,
284+ }
285+ ) ;
286+
287+ const ResponseSchema = z . object ( {
288+ shapeName : z . string ( ) . describe ( "The name of the shape" ) ,
289+ details : z . string ( ) . describe ( "Details about the shape" ) ,
290+ wasDeleted : z . boolean ( ) . describe ( "Whether the shape was deleted" ) ,
291+ } ) ;
292+
293+ const agent = createAgent ( {
294+ model,
295+ tools : [ getDetailByName , deleteShape ] ,
296+ systemPrompt : `You are a helpful shape management assistant.
297+ When asked to perform operations on shapes, you MUST:
298+ 1. First use the get_detail_by_name tool to get information about the shape
299+ 2. Then use the delete_shape tool if deletion is requested
300+ 3. Only after using the tools, return the structured response` ,
301+ responseFormat : providerStrategy ( ResponseSchema ) ,
302+ } ) ;
303+
304+ const result = await agent . invoke ( {
305+ messages : [
306+ new HumanMessage (
307+ "Please delete the circle shape and tell me about it first"
308+ ) ,
309+ ] ,
310+ } ) ;
311+
312+ // Verify user tools were called
313+ expect ( getDetailByNameMock ) . toHaveBeenCalledTimes ( 1 ) ;
314+ expect ( deleteShapeMock ) . toHaveBeenCalledTimes ( 1 ) ;
315+
316+ // Verify structured response is present and correctly parsed
317+ // This was previously undefined due to thinking model content being an array
318+ expect ( result . structuredResponse ) . toBeDefined ( ) ;
319+ expect ( result . structuredResponse . shapeName ) . toBe ( "circle" ) ;
320+ expect ( result . structuredResponse . wasDeleted ) . toBe ( true ) ;
321+ expect ( typeof result . structuredResponse . details ) . toBe ( "string" ) ;
322+
323+ // Verify no extract- tool calls (providerStrategy uses native structured output)
324+ const hasExtractToolCalls = result . messages . some (
325+ ( msg ) =>
326+ AIMessage . isInstance ( msg ) &&
327+ msg . tool_calls ?. some ( ( tc ) => tc . name . startsWith ( "extract-" ) )
328+ ) ;
329+ expect ( hasExtractToolCalls ) . toBe ( false ) ;
330+
331+ // Verify AI messages contain thinking blocks (array content)
332+ const aiMessages = result . messages . filter ( AIMessage . isInstance ) ;
333+ const hasThinkingContent = aiMessages . some (
334+ ( msg ) =>
335+ Array . isArray ( msg . content ) &&
336+ msg . content . some (
337+ ( block : any ) =>
338+ typeof block === "object" &&
339+ block !== null &&
340+ block . type === "thinking"
341+ )
342+ ) ;
343+ expect ( hasThinkingContent ) . toBe ( true ) ;
344+ } ) ;
345+
346+ it ( "should fail with toolStrategy and thinking models due to tool_choice conflict" , async ( ) => {
347+ // Use Anthropic model with thinking enabled
348+ const model = new ChatAnthropic ( {
349+ model : "claude-haiku-4-5-20251001" ,
350+ thinking : { type : "enabled" , budget_tokens : 5000 } ,
351+ maxTokens : 15000 ,
352+ } ) ;
353+
354+ const ResponseSchema = z . object ( {
355+ answer : z . string ( ) ,
356+ } ) ;
357+
358+ const agent = createAgent ( {
359+ model,
360+ tools : [ ] ,
361+ // toolStrategy uses tool_choice: "any" which conflicts with thinking mode
362+ responseFormat : toolStrategy ( ResponseSchema ) ,
363+ } ) ;
364+
365+ // This should fail because Anthropic doesn't allow thinking with forced tool use
366+ await expect (
367+ agent . invoke ( {
368+ messages : [ new HumanMessage ( "What is 2+2?" ) ] ,
369+ } )
370+ ) . rejects . toThrow (
371+ / T h i n k i n g m a y n o t b e e n a b l e d w h e n t o o l _ c h o i c e f o r c e s t o o l u s e /
372+ ) ;
373+ } ) ;
374+ } ) ;
0 commit comments