diff --git a/lib/crewai/src/crewai/llms/providers/openai/completion.py b/lib/crewai/src/crewai/llms/providers/openai/completion.py index 042c13e0a..155d29d7d 100644 --- a/lib/crewai/src/crewai/llms/providers/openai/completion.py +++ b/lib/crewai/src/crewai/llms/providers/openai/completion.py @@ -771,15 +771,17 @@ class OpenAICompletion(BaseLLM): items.append({"role": "assistant", "content": content}) for call in message["tool_calls"]: function = call.get("function", {}) - args = function.get("arguments", "") + args = function.get("arguments") + if args is None or args == "": + args = "{}" + elif not isinstance(args, str): + args = json.dumps(args) items.append( { "type": "function_call", "call_id": call.get("id") or f"call_{id(call)}", "name": function.get("name", ""), - "arguments": args - if isinstance(args, str) - else json.dumps(args), + "arguments": args, } ) return items @@ -810,7 +812,7 @@ class OpenAICompletion(BaseLLM): - Internally-tagged tool format (flat structure) """ instructions: str | None = self.instructions - input_messages: list[Any] = [] + input_messages: list[dict[str, Any] | LLMMessage] = [] for message in messages: if message.get("role") == "system": @@ -824,7 +826,7 @@ class OpenAICompletion(BaseLLM): input_messages.extend(self._to_responses_input(message)) # Prepend reasoning items for ZDR (zero-data-retention) chaining when configured - final_input: list[Any] = [] + final_input: list[dict[str, Any] | LLMMessage] = [] if self.auto_chain_reasoning and self._last_reasoning_items: final_input.extend(self._last_reasoning_items) final_input.extend(input_messages if input_messages else messages) diff --git a/lib/crewai/tests/llms/openai/test_openai.py b/lib/crewai/tests/llms/openai/test_openai.py index c0a772883..925a941cb 100644 --- a/lib/crewai/tests/llms/openai/test_openai.py +++ b/lib/crewai/tests/llms/openai/test_openai.py @@ -1017,6 +1017,7 @@ def test_openai_responses_api_preserves_assistant_content_with_tool_calls(): "tool_calls": [ { "type": "function", + "id": "call_fetch_page", "function": { "name": "fetch_page", "arguments": {"url": "https://example.com"}, @@ -1033,10 +1034,39 @@ def test_openai_responses_api_preserves_assistant_content_with_tool_calls(): "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]["call_id"] == "call_fetch_page" assert params["input"][1]["arguments"] == '{"url": "https://example.com"}' +def test_openai_responses_api_defaults_missing_tool_call_arguments(): + """Missing or empty tool-call arguments must become a valid JSON object.""" + llm = OpenAICompletion(model="gpt-4o-mini", api="responses") + + messages = [ + { + "role": "assistant", + "content": None, + "tool_calls": [ + { + "id": "call_no_args", + "type": "function", + "function": {"name": "ping"}, + }, + { + "id": "call_empty_args", + "type": "function", + "function": {"name": "ping", "arguments": ""}, + }, + ], + } + ] + + params = llm._prepare_responses_params(messages) + + assert params["input"][0]["arguments"] == "{}" + assert params["input"][1]["arguments"] == "{}" + + 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.