From fb31c91076192859dd1632aa93bf25820db3c2ed Mon Sep 17 00:00:00 2001 From: Roli Bosch Date: Wed, 4 Mar 2026 18:09:48 -0800 Subject: [PATCH] fix(anthropic): drop forced tool_choice when thinking is enabled (#35544) --- .../langchain_anthropic/chat_models.py | 20 +++++ .../tests/unit_tests/test_chat_models.py | 87 +++++++++++++++++++ 2 files changed, 107 insertions(+) diff --git a/libs/partners/anthropic/langchain_anthropic/chat_models.py b/libs/partners/anthropic/langchain_anthropic/chat_models.py index ee9f480427a..6ed53ae497e 100644 --- a/libs/partners/anthropic/langchain_anthropic/chat_models.py +++ b/libs/partners/anthropic/langchain_anthropic/chat_models.py @@ -1567,6 +1567,26 @@ class ChatAnthropic(BaseChatModel): msg, ) + # Anthropic API rejects forced tool use when thinking is enabled: + # "Thinking may not be enabled when tool_choice forces tool use." + # Drop forced tool_choice and warn, matching the behavior in + # _get_llm_for_structured_output_when_thinking_is_enabled. + if ( + self.thinking is not None + and self.thinking.get("type") == "enabled" + and "tool_choice" in kwargs + and kwargs["tool_choice"].get("type") in ("any", "tool") + ): + warnings.warn( + "tool_choice is forced but thinking is enabled. The Anthropic " + "API does not support forced tool use with thinking. " + "Dropping tool_choice to avoid an API error. Tool calls are " + "not guaranteed. Consider disabling thinking or adjusting " + "your prompt to ensure the tool is called.", + stacklevel=2, + ) + del kwargs["tool_choice"] + if parallel_tool_calls is not None: disable_parallel_tool_use = not parallel_tool_calls if "tool_choice" in kwargs: diff --git a/libs/partners/anthropic/tests/unit_tests/test_chat_models.py b/libs/partners/anthropic/tests/unit_tests/test_chat_models.py index 2f6ffbffe36..f361857cee5 100644 --- a/libs/partners/anthropic/tests/unit_tests/test_chat_models.py +++ b/libs/partners/anthropic/tests/unit_tests/test_chat_models.py @@ -4,6 +4,7 @@ from __future__ import annotations import copy import os +import warnings from collections.abc import Callable from typing import Any, Literal, cast from unittest.mock import MagicMock, patch @@ -2520,3 +2521,89 @@ def test_context_overflow_error_backwards_compatibility() -> None: # Verify it's both types (multiple inheritance) assert isinstance(exc_info.value, anthropic.BadRequestError) assert isinstance(exc_info.value, ContextOverflowError) + + +def test_bind_tools_drops_forced_tool_choice_when_thinking_enabled() -> None: + """Regression test for https://github.com/langchain-ai/langchain/issues/35539. + + Anthropic API rejects forced tool_choice when thinking is enabled: + "Thinking may not be enabled when tool_choice forces tool use." + bind_tools should drop forced tool_choice and warn. + """ + chat_model = ChatAnthropic( + model=MODEL_NAME, + anthropic_api_key="secret-api-key", + thinking={"type": "enabled", "budget_tokens": 5000}, + ) + + # tool_choice="any" should be dropped with warning + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + result = chat_model.bind_tools([GetWeather], tool_choice="any") + assert "tool_choice" not in cast("RunnableBinding", result).kwargs + assert len(w) == 1 + assert "thinking is enabled" in str(w[0].message) + + # tool_choice="auto" should NOT be dropped (auto is allowed) + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + result = chat_model.bind_tools([GetWeather], tool_choice="auto") + assert cast("RunnableBinding", result).kwargs["tool_choice"] == {"type": "auto"} + assert len(w) == 0 + + # tool_choice=specific tool name should be dropped with warning + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + result = chat_model.bind_tools([GetWeather], tool_choice="GetWeather") + assert "tool_choice" not in cast("RunnableBinding", result).kwargs + assert len(w) == 1 + + # tool_choice=dict with type "tool" should be dropped with warning + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + result = chat_model.bind_tools( + [GetWeather], + tool_choice={"type": "tool", "name": "GetWeather"}, + ) + assert "tool_choice" not in cast("RunnableBinding", result).kwargs + assert len(w) == 1 + + # tool_choice=dict with type "any" should also be dropped + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + result = chat_model.bind_tools( + [GetWeather], + tool_choice={"type": "any"}, + ) + assert "tool_choice" not in cast("RunnableBinding", result).kwargs + assert len(w) == 1 + + +def test_bind_tools_keeps_forced_tool_choice_when_thinking_disabled() -> None: + """When thinking is not enabled, forced tool_choice should pass through.""" + chat_model = ChatAnthropic( + model=MODEL_NAME, + anthropic_api_key="secret-api-key", + ) + + # No thinking — tool_choice="any" should pass through + result = chat_model.bind_tools([GetWeather], tool_choice="any") + assert cast("RunnableBinding", result).kwargs["tool_choice"] == {"type": "any"} + + # Thinking explicitly None + chat_model_none = ChatAnthropic( + model=MODEL_NAME, + anthropic_api_key="secret-api-key", + thinking=None, + ) + result = chat_model_none.bind_tools([GetWeather], tool_choice="any") + assert cast("RunnableBinding", result).kwargs["tool_choice"] == {"type": "any"} + + # Thinking explicitly disabled — should NOT drop tool_choice + chat_model_disabled = ChatAnthropic( + model=MODEL_NAME, + anthropic_api_key="secret-api-key", + thinking={"type": "disabled"}, + ) + result = chat_model_disabled.bind_tools([GetWeather], tool_choice="any") + assert cast("RunnableBinding", result).kwargs["tool_choice"] == {"type": "any"}