diff --git a/lib/crewai/src/crewai/llms/providers/openai/completion.py b/lib/crewai/src/crewai/llms/providers/openai/completion.py index 6110640b5..ead2675c8 100644 --- a/lib/crewai/src/crewai/llms/providers/openai/completion.py +++ b/lib/crewai/src/crewai/llms/providers/openai/completion.py @@ -683,14 +683,17 @@ class OpenAICompletion(BaseLLM): if role == "assistant" and message.get("tool_calls"): items: list[dict[str, Any] | LLMMessage] = [] + if message.get("content"): + items.append({"role": "assistant", "content": message["content"]}) for tool_call in message["tool_calls"]: function = tool_call.get("function", {}) + args = function.get("arguments", "") items.append( { "type": "function_call", - "call_id": tool_call.get("id", ""), + "call_id": tool_call.get("id") or f"call_{id(tool_call)}", "name": function.get("name", ""), - "arguments": function.get("arguments", ""), + "arguments": args if isinstance(args, str) else json.dumps(args), } ) return items diff --git a/lib/crewai/src/crewai/utilities/agent_utils.py b/lib/crewai/src/crewai/utilities/agent_utils.py index c6330bd05..7bbe3ed49 100644 --- a/lib/crewai/src/crewai/utilities/agent_utils.py +++ b/lib/crewai/src/crewai/utilities/agent_utils.py @@ -1277,7 +1277,9 @@ def is_tool_call_list(response: list[Any]) -> bool: # Bedrock-style if isinstance(first_item, dict) and "name" in first_item and "input" in first_item: return True - # OpenAI Responses API-style (flat dict, no nested "function" key) + # OpenAI Responses API-style (flat dict, no nested "function" key). This + # intentionally accepts the same broad shape as the Bedrock check above; + # only provider paths that return lists reach this classifier. if ( isinstance(first_item, dict) and "name" in first_item diff --git a/lib/crewai/tests/llms/openai/test_openai.py b/lib/crewai/tests/llms/openai/test_openai.py index 4bbe93a60..c0a772883 100644 --- a/lib/crewai/tests/llms/openai/test_openai.py +++ b/lib/crewai/tests/llms/openai/test_openai.py @@ -1006,6 +1006,37 @@ def test_openai_responses_api_converts_assistant_tool_calls_message(): } +def test_openai_responses_api_preserves_assistant_content_with_tool_calls(): + """Assistant text must be retained when it accompanies tool calls.""" + llm = OpenAICompletion(model="gpt-4o-mini", api="responses") + + messages = [ + { + "role": "assistant", + "content": "I'll fetch that page now.", + "tool_calls": [ + { + "type": "function", + "function": { + "name": "fetch_page", + "arguments": {"url": "https://example.com"}, + }, + } + ], + } + ] + + params = llm._prepare_responses_params(messages) + + assert params["input"][0] == { + "role": "assistant", + "content": "I'll fetch that page now.", + } + assert params["input"][1]["type"] == "function_call" + assert params["input"][1]["call_id"].startswith("call_") + assert params["input"][1]["arguments"] == '{"url": "https://example.com"}' + + def test_openai_responses_api_converts_tool_result_message(): """Regression: tool-role messages (Chat-Completions shape) must become function_call_output input items for the Responses API.