mirror of
https://github.com/hwchase17/langchain.git
synced 2026-03-18 11:07:36 +00:00
fix(anthropic): drop forced tool_choice when thinking is enabled (#35544)
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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"}
|
||||
|
||||
Reference in New Issue
Block a user