AI & Agent Dev Bug Sandbox logo
AI & Agent Dev Bug Sandbox
Back to Radar

Invocation-Time Thinking Bypasses Bind_tools Forced-Tool-Choice Guard In Langchain-Anthropic

ChatAnthropic.bind_tools() warns and removes forced tool_choice when thinking is set on the constructor, but the same thinking supplied via invoke() is not checked. This sends an invalid combination (thinking enabled + tool_choice={'type':'any'}) to Anthropic API, resulting in HTTP 400.

mediumConfidence 92%LangChainAffected V<=1.6.1

Origin Analysis

The compatibility guard in bind_tools() checks only self.thinking at bind time. Invocation-time kwargs are merged later in _get_request_payload(), so a thinking kwarg is not assessed against the already-bound forced tool_choice. The forced tool_choice remains and is sent to the API, which rejects the request.
1. Set ANTHROPIC_API_KEY. 2. Create ChatAnthropic(model=MODEL, max_tokens=1025, max_retries=0) and call .bind_tools([TOOL], tool_choice='any'). 3. Invoke with prompt 'Reply with exactly: hi. Do not call tools.' and pass thinking={'type':'enabled','budget_tokens':1024}. 4. Observe no local warning and HTTP 400 from Anthropic: 'Thinking may not be enabled when tool_choice forces tool use.'

Fixing Code Block

import warnings from langchain_anthropic import ChatAnthropic class SafeChatAnthropic(ChatAnthropic): '''ChatAnthropic hotfix: enforce thinking/tool_choice compatibility at request time.''' def _get_request_payload(self, messages, *, stop=None, **kwargs): thinking = kwargs.get('thinking', self.thinking) tool_choice = kwargs.get('tool_choice', self.tool_choice) if thinking and tool_choice is not None: forced = False if isinstance(tool_choice, dict): forced = tool_choice.get('type') in ('any', 'tool') else: forced = tool_choice not in ('auto', 'none') if forced: warnings.warn( 'thinking is not compatible with forced tool_choice; removing tool_choice.', UserWarning, stacklevel=2, ) kwargs = {**kwargs, 'tool_choice': None} return super()._get_request_payload(messages, stop=stop, **kwargs)
The hotfix subclasses ChatAnthropic and overrides _get_request_payload to perform the same compatibility check at request construction time. It examines both self.thinking and any thinking supplied as an invocation kwarg. When thinking is present and tool_choice is forced (either 'any'/'tool' string or dict {'type':'any'/'tool'}), it emits the same warning and removes the forced tool_choice from the local request kwargs before delegating to the parent implementation. This prevents the invalid API call while preserving constructor-time behavior.

Edge Case Audit

This hotfix relies on the private _get_request_payload method and may break if the upstream signature changes. It warns and removes forced tool_choice at each request, which can silently alter behavior if callers unintentionally pass thinking with forced tools. The warning may be emitted multiple times in loops or concurrent scenarios. Rollback: revert to the original ChatAnthropic class or remove the subclass after the upstream fix is applied. Additionally, the forced-tool detection should be kept in sync with any future Anthropic tool_choice values.

Ecosystem Topology