Skip to content

Commit d4e4b3a

Browse files
fix: bind_tools should not force tool_choice_option='auto' by default
The tool_choice docstring documents that None/False should have "no effect, default OpenAI behavior", but the else branch unconditionally set tool_choice_option="auto", causing WatsonX models to always attempt a tool call even when no tool was relevant to the query. Added unit tests covering the unset, 'auto', required/True, and named-tool cases for bind_tools, since none previously existed. Fixes #152
1 parent a2a97e1 commit d4e4b3a

2 files changed

Lines changed: 59 additions & 2 deletions

File tree

libs/ibm/langchain_ibm/chat_models.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1752,8 +1752,12 @@ def bind_tools(
17521752
kwargs["tool_choice_option"] = tool_choice
17531753
else:
17541754
kwargs["tool_choice"] = tool_choice
1755-
else:
1756-
kwargs["tool_choice_option"] = "auto"
1755+
# When tool_choice is not specified (None/False), leave tool_choice_option
1756+
# unset so the request has no effect on tool selection, matching the
1757+
# documented default OpenAI-style behavior (see `tool_choice` docstring
1758+
# above). Previously this branch forced tool_choice_option="auto",
1759+
# which caused WatsonX models to always attempt a tool call even when
1760+
# no tools were relevant to the user's query.
17571761

17581762
return super().bind(tools=formatted_tools, **kwargs)
17591763

libs/ibm/tests/unit_tests/test_chat_models.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,3 +503,56 @@ def test_initialize_chat_watsonx_model_id_without_project_or_space_id(
503503
url="https://us-south.ml.cloud.ibm.com",
504504
apikey="test_apikey",
505505
)
506+
507+
508+
# ── bind_tools should not force tool use when tool_choice is unset (#152) ───────
509+
510+
_WEATHER_TOOL = {
511+
"type": "function",
512+
"function": {
513+
"name": "get_weather",
514+
"description": "Get the weather for a city.",
515+
"parameters": {
516+
"type": "object",
517+
"properties": {"city": {"type": "string"}},
518+
},
519+
},
520+
}
521+
522+
523+
def test_bind_tools_without_tool_choice_has_no_effect() -> None:
524+
"""No tool_choice given should not force tool_choice_option at all.
525+
526+
Previously this branch defaulted to `tool_choice_option="auto"`, which
527+
caused WatsonX models to always attempt a tool call even when no tool was
528+
relevant to the user's query. The documented contract for `tool_choice`
529+
is that `None`/`False` should have "no effect, default OpenAI behavior".
530+
"""
531+
chat = ChatWatsonx(watsonx_model=model_inference_mock)
532+
bound = chat.bind_tools([_WEATHER_TOOL])
533+
assert "tool_choice_option" not in bound.kwargs
534+
assert "tool_choice" not in bound.kwargs
535+
536+
537+
def test_bind_tools_with_explicit_auto_sets_tool_choice_option() -> None:
538+
"""Explicitly passing tool_choice='auto' should still set tool_choice_option."""
539+
chat = ChatWatsonx(watsonx_model=model_inference_mock)
540+
bound = chat.bind_tools([_WEATHER_TOOL], tool_choice="auto")
541+
assert bound.kwargs["tool_choice_option"] == "auto"
542+
543+
544+
def test_bind_tools_with_required_forces_tool_choice() -> None:
545+
"""tool_choice=True (or 'required'/'any') should force tool_choice_option."""
546+
chat = ChatWatsonx(watsonx_model=model_inference_mock)
547+
bound = chat.bind_tools([_WEATHER_TOOL], tool_choice=True)
548+
assert bound.kwargs["tool_choice_option"] == "required"
549+
550+
551+
def test_bind_tools_with_specific_tool_name() -> None:
552+
"""tool_choice=<tool name> should force that specific tool via a dict."""
553+
chat = ChatWatsonx(watsonx_model=model_inference_mock)
554+
bound = chat.bind_tools([_WEATHER_TOOL], tool_choice="get_weather")
555+
assert bound.kwargs["tool_choice"] == {
556+
"type": "function",
557+
"function": {"name": "get_weather"},
558+
}

0 commit comments

Comments
 (0)