From 25a13a0dd1c3580f6c320622d968ae3264b074d1 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 12:10:02 +0000 Subject: [PATCH] fix(llm): use validated messages variable instead of raw params['messages'] access MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace all direct params['messages'] accesses with params.get('messages', []) or the locally validated 'messages' variable in _handle_non_streaming_response, _ahandle_non_streaming_response, and _handle_streaming_response. When the 'messages' key is missing from params, the code now gracefully handles it instead of raising a confusing KeyError. Closes #5164 Co-Authored-By: João --- lib/crewai/src/crewai/llm.py | 24 ++++---- lib/crewai/tests/test_llm.py | 106 +++++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 12 deletions(-) diff --git a/lib/crewai/src/crewai/llm.py b/lib/crewai/src/crewai/llm.py index 75b1f65468..52d60515be 100644 --- a/lib/crewai/src/crewai/llm.py +++ b/lib/crewai/src/crewai/llm.py @@ -1021,7 +1021,7 @@ def _handle_streaming_response( call_type=LLMCallType.LLM_CALL, from_task=from_task, from_agent=from_agent, - messages=params["messages"], + messages=params.get("messages", []), ) return structured_response @@ -1030,7 +1030,7 @@ def _handle_streaming_response( call_type=LLMCallType.LLM_CALL, from_task=from_task, from_agent=from_agent, - messages=params["messages"], + messages=params.get("messages", []), ) return full_response @@ -1045,7 +1045,7 @@ def _handle_streaming_response( call_type=LLMCallType.LLM_CALL, from_task=from_task, from_agent=from_agent, - messages=params["messages"], + messages=params.get("messages", []), ) return full_response @@ -1066,7 +1066,7 @@ def _handle_streaming_response( call_type=LLMCallType.LLM_CALL, from_task=from_task, from_agent=from_agent, - messages=params["messages"], + messages=params.get("messages", []), ) return full_response @@ -1217,7 +1217,7 @@ def _handle_non_streaming_response( call_type=LLMCallType.LLM_CALL, from_task=from_task, from_agent=from_agent, - messages=params["messages"], + messages=messages, ) return structured_response @@ -1258,7 +1258,7 @@ def _handle_non_streaming_response( call_type=LLMCallType.LLM_CALL, from_task=from_task, from_agent=from_agent, - messages=params["messages"], + messages=params.get("messages", []), ) return structured_response @@ -1289,7 +1289,7 @@ def _handle_non_streaming_response( call_type=LLMCallType.LLM_CALL, from_task=from_task, from_agent=from_agent, - messages=params["messages"], + messages=params.get("messages", []), ) return text_response @@ -1312,7 +1312,7 @@ def _handle_non_streaming_response( call_type=LLMCallType.LLM_CALL, from_task=from_task, from_agent=from_agent, - messages=params["messages"], + messages=params.get("messages", []), ) return text_response @@ -1361,7 +1361,7 @@ async def _ahandle_non_streaming_response( call_type=LLMCallType.LLM_CALL, from_task=from_task, from_agent=from_agent, - messages=params["messages"], + messages=messages, ) return structured_response @@ -1396,7 +1396,7 @@ async def _ahandle_non_streaming_response( call_type=LLMCallType.LLM_CALL, from_task=from_task, from_agent=from_agent, - messages=params["messages"], + messages=params.get("messages", []), ) return structured_response @@ -1425,7 +1425,7 @@ async def _ahandle_non_streaming_response( call_type=LLMCallType.LLM_CALL, from_task=from_task, from_agent=from_agent, - messages=params["messages"], + messages=params.get("messages", []), ) return text_response @@ -1447,7 +1447,7 @@ async def _ahandle_non_streaming_response( call_type=LLMCallType.LLM_CALL, from_task=from_task, from_agent=from_agent, - messages=params["messages"], + messages=params.get("messages", []), ) return text_response diff --git a/lib/crewai/tests/test_llm.py b/lib/crewai/tests/test_llm.py index 1ed2171664..ff31a46128 100644 --- a/lib/crewai/tests/test_llm.py +++ b/lib/crewai/tests/test_llm.py @@ -1024,3 +1024,109 @@ async def test_usage_info_streaming_with_acall(): assert llm._token_usage["total_tokens"] > 0 assert len(result) > 0 + + +def test_non_streaming_response_no_keyerror_when_messages_missing_from_params(): + """Test that _handle_non_streaming_response does not raise KeyError when + params dict lacks a 'messages' key. Covers the fix for issue #5164.""" + llm = LLM(model="gpt-4o-mini", is_litellm=True) + + mock_message = MagicMock() + mock_message.content = "Test response" + mock_message.tool_calls = [] + mock_choice = MagicMock() + mock_choice.message = mock_message + mock_response = MagicMock() + mock_response.choices = [mock_choice] + mock_response.usage = MagicMock() + + with patch("litellm.completion", return_value=mock_response): + # Pass params WITHOUT "messages" key — before the fix this raised KeyError + result = llm._handle_non_streaming_response(params={"model": "gpt-4o-mini"}) + + assert result == "Test response" + + +def test_non_streaming_response_uses_validated_messages_for_litellm_response_model(): + """Test that _handle_non_streaming_response uses the locally validated + 'messages' variable (not params['messages']) in the response_model + is_litellm + branch. Covers the fix for issue #5164.""" + llm = LLM(model="gpt-4o-mini", is_litellm=True) + + class DummyModel(BaseModel): + answer: str + + messages = [{"role": "user", "content": "test"}] + params = {"model": "gpt-4o-mini", "messages": messages} + + mock_result = MagicMock() + mock_result.model_dump_json.return_value = '{"answer": "ok"}' + + with patch( + "crewai.utilities.internal_instructor.InternalInstructor" + ) as MockInstructor: + instance = MockInstructor.return_value + instance.to_pydantic.return_value = mock_result + + result = llm._handle_non_streaming_response( + params=params, response_model=DummyModel + ) + + assert result == '{"answer": "ok"}' + + +def test_non_streaming_response_with_response_model_no_keyerror(): + """Test that _handle_non_streaming_response does not raise KeyError + in the response_model + is_litellm branch when messages key is missing. + Before the fix, this would raise KeyError at the _handle_emit_call_events call.""" + llm = LLM(model="gpt-4o-mini", is_litellm=True) + + class DummyModel(BaseModel): + answer: str + + # No "messages" key in params — should raise ValueError, not KeyError + params = {"model": "gpt-4o-mini"} + + with pytest.raises(ValueError, match="Messages are required"): + llm._handle_non_streaming_response(params=params, response_model=DummyModel) + + +@pytest.mark.asyncio +async def test_async_non_streaming_response_no_keyerror_when_messages_missing(): + """Test that _ahandle_non_streaming_response does not raise KeyError when + params dict lacks a 'messages' key. Covers the async fix for issue #5164.""" + llm = LLM(model="gpt-4o-mini", is_litellm=True) + + mock_message = MagicMock() + mock_message.content = "Async response" + mock_message.tool_calls = [] + mock_choice = MagicMock() + mock_choice.message = mock_message + mock_response = MagicMock() + mock_response.choices = [mock_choice] + mock_response.usage = MagicMock() + + with patch("litellm.acompletion", return_value=mock_response): + result = await llm._ahandle_non_streaming_response( + params={"model": "gpt-4o-mini"} + ) + + assert result == "Async response" + + +@pytest.mark.asyncio +async def test_async_non_streaming_response_with_response_model_no_keyerror(): + """Test that _ahandle_non_streaming_response does not raise KeyError + in the response_model + is_litellm branch when messages key is missing. + Covers the async fix for issue #5164.""" + llm = LLM(model="gpt-4o-mini", is_litellm=True) + + class DummyModel(BaseModel): + answer: str + + params = {"model": "gpt-4o-mini"} + + with pytest.raises(ValueError, match="Messages are required"): + await llm._ahandle_non_streaming_response( + params=params, response_model=DummyModel + )