fix(anthropic): drop forced tool_choice when thinking is enabled (#35544)

This commit is contained in:
Roli Bosch
2026-03-04 18:09:48 -08:00
committed by GitHub
parent c8f394208b
commit fb31c91076
2 changed files with 107 additions and 0 deletions

View File

@@ -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:

View File

@@ -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"}