fix(llm): preserve tool_calls when response also contains text

This commit is contained in:
Greyson LaLonde
2026-04-30 22:53:01 +08:00
committed by GitHub
parent c7f01048b7
commit 9f13235037
2 changed files with 60 additions and 13 deletions

View File

@@ -1235,8 +1235,12 @@ class LLM(BaseLLM):
# --- 4) Check for tool calls
tool_calls = response_message.tool_calls or []
# --- 5) If no tool calls or no available functions, return the text response directly as long as there is a text response
if (not tool_calls or not available_functions) and text_response:
# --- 5) If there are tool calls but no available functions, return the tool calls
if tool_calls and not available_functions:
return tool_calls
# --- 6) If there are no tool calls to execute, return the text response directly
if not tool_calls and text_response:
self._handle_emit_call_events(
response=text_response,
call_type=LLMCallType.LLM_CALL,
@@ -1247,11 +1251,6 @@ class LLM(BaseLLM):
)
return text_response
# --- 6) If there are tool calls but no available functions, return the tool calls
# This allows the caller (e.g., executor) to handle tool execution
if tool_calls and not available_functions:
return tool_calls
# --- 7) Handle tool calls if present (execute when available_functions provided)
if tool_calls and available_functions:
tool_result = self._handle_tool_call(
@@ -1384,7 +1383,10 @@ class LLM(BaseLLM):
tool_calls = response_message.tool_calls or []
if (not tool_calls or not available_functions) and text_response:
if tool_calls and not available_functions:
return tool_calls
if not tool_calls and text_response:
self._handle_emit_call_events(
response=text_response,
call_type=LLMCallType.LLM_CALL,
@@ -1395,11 +1397,6 @@ class LLM(BaseLLM):
)
return text_response
# If there are tool calls but no available functions, return the tool calls
# This allows the caller (e.g., executor) to handle tool execution
if tool_calls and not available_functions:
return tool_calls
# Handle tool calls if present (execute when available_functions provided)
if tool_calls and available_functions:
tool_result = self._handle_tool_call(

View File

@@ -177,6 +177,7 @@ def test_llm_passes_additional_params():
# Create mocks for response structure
mock_message = MagicMock()
mock_message.content = "Test response"
mock_message.tool_calls = None
mock_choice = MagicMock()
mock_choice.message = mock_message
mock_response = MagicMock()
@@ -1146,3 +1147,52 @@ async def test_usage_info_streaming_with_acall():
assert llm._token_usage["total_tokens"] > 0
assert len(result) > 0
def _build_response_with_text_and_tool_calls():
"""Mimic a litellm ModelResponse that contains both content and tool_calls."""
from litellm.types.utils import ChatCompletionMessageToolCall, Function
response_message = MagicMock()
response_message.content = "I will search for the given query."
response_message.tool_calls = [
ChatCompletionMessageToolCall(
id="call_123",
type="function",
function=Function(name="search", arguments='{"q": "x"}'),
)
]
choice = MagicMock(message=response_message)
response = MagicMock(choices=[choice], model_extra=None)
return response
def test_non_streaming_returns_tool_calls_when_text_also_present():
"""A response with both text and tool_calls must not drop the tool_calls
when available_functions is None (executor-managed tool execution path).
"""
llm = LLM(model="gpt-4o-mini", is_litellm=True)
response = _build_response_with_text_and_tool_calls()
with patch("crewai.llm.litellm.completion", return_value=response):
result = llm.call("anything", available_functions=None)
assert isinstance(result, list)
assert len(result) == 1
assert result[0].function.name == "search"
@pytest.mark.asyncio
async def test_non_streaming_async_returns_tool_calls_when_text_also_present():
llm = LLM(model="openai/gpt-4o-mini", is_litellm=True, stream=False)
response = _build_response_with_text_and_tool_calls()
async def _ret(*args, **kwargs):
return response
with patch("crewai.llm.litellm.acompletion", side_effect=_ret):
result = await llm.acall("anything", available_functions=None)
assert isinstance(result, list)
assert len(result) == 1
assert result[0].function.name == "search"