Skip to content

Commit f69514c

Browse files
committed
Fix: Prevent infinite tool calling loop with Meta Llama models
Meta Llama models continue calling tools even after receiving results when tools are present in the API request. This causes infinite loops in agentic applications. This fix automatically sets tool_choice='none' when ToolMessages are detected in the conversation history, signaling to the model that it should generate a final response instead of making additional tool calls. Fixes infinite loop issue with Meta Llama 4 Scout and other Meta models when using bind_tools() in LangGraph agents. Tested with 3 different tools - all correctly stop after receiving results. Signed-off-by: Federico Kamelhar <federico.kamelhar@oracle.com>
1 parent 5ce1280 commit f69514c

File tree

2 files changed

+70
-1
lines changed

2 files changed

+70
-1
lines changed

libs/oci/langchain_oci/chat_models/oci_generative_ai.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -736,11 +736,21 @@ def messages_to_oci_params(
736736
oci_message = self.oci_chat_message[role](content=content)
737737
oci_messages.append(oci_message)
738738

739-
return {
739+
result = {
740740
"messages": oci_messages,
741741
"api_format": self.chat_api_format,
742742
}
743743

744+
# BUGFIX: If tool results have been received and tools are bound, set tool_choice to "none"
745+
# to prevent the model from making additional tool calls in a loop.
746+
# This addresses a known issue with Meta Llama models that continue calling tools
747+
# even after receiving results.
748+
has_tool_results = any(isinstance(msg, ToolMessage) for msg in messages)
749+
if has_tool_results and "tools" in kwargs and "tool_choice" not in kwargs:
750+
result["tool_choice"] = self.oci_tool_choice_none()
751+
752+
return result
753+
744754
def _process_message_content(
745755
self, content: Union[str, List[Union[str, Dict]]]
746756
) -> List[Any]:

libs/oci/tests/unit_tests/chat_models/test_oci_generative_ai.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -746,3 +746,62 @@ def test_get_provider():
746746
ChatOCIGenAI(model_id=model_id)._provider.__class__.__name__
747747
== provider_name
748748
)
749+
750+
751+
@pytest.mark.requires("oci")
752+
def test_tool_choice_none_after_tool_results() -> None:
753+
"""Test that tool_choice is set to 'none' when ToolMessages are present.
754+
755+
This prevents infinite loops with Meta Llama models that continue calling
756+
tools even after receiving results when tools are bound to the model.
757+
"""
758+
from langchain_core.messages import ToolMessage
759+
from oci.generative_ai_inference import models
760+
761+
oci_gen_ai_client = MagicMock()
762+
llm = ChatOCIGenAI(
763+
model_id="meta.llama-3.3-70b-instruct",
764+
client=oci_gen_ai_client
765+
)
766+
767+
# Mock tools
768+
mock_tools = [
769+
models.Tool(
770+
type="FUNCTION",
771+
function=models.FunctionDefinition(
772+
name="get_weather",
773+
description="Get weather for a city",
774+
parameters={}
775+
)
776+
)
777+
]
778+
779+
# Bind tools to model
780+
llm_with_tools = llm.bind_tools(mock_tools)
781+
782+
# Create conversation with ToolMessage
783+
messages = [
784+
HumanMessage(content="What's the weather?"),
785+
AIMessage(
786+
content="",
787+
tool_calls=[{
788+
"id": "call_123",
789+
"name": "get_weather",
790+
"args": {"city": "Chicago"}
791+
}]
792+
),
793+
ToolMessage(
794+
content="Sunny, 65°F",
795+
tool_call_id="call_123"
796+
)
797+
]
798+
799+
# Prepare the request
800+
request = llm_with_tools._prepare_request(messages, stream=False)
801+
802+
# Verify that tool_choice is set to 'none'
803+
assert hasattr(request.chat_request, 'tool_choice')
804+
assert isinstance(request.chat_request.tool_choice, models.ToolChoiceNone)
805+
# Verify tools are still present (not removed, just choice is 'none')
806+
assert hasattr(request.chat_request, 'tools')
807+
assert len(request.chat_request.tools) > 0

0 commit comments

Comments
 (0)