Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion haystack/components/generators/chat/openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
ToolCallDelta,
select_streaming_callback,
)
from haystack.dataclasses.chat_message import _parse_openai_tool_call_arguments
from haystack.dataclasses.streaming_chunk import _invoke_streaming_callback
from haystack.tools import (
ToolsType,
Expand Down Expand Up @@ -668,7 +669,7 @@ def _convert_chat_completion_to_chat_message(
for openai_tc in openai_tool_calls:
arguments_str = openai_tc.function.arguments
try:
arguments = json.loads(arguments_str)
arguments = _parse_openai_tool_call_arguments(arguments_str)
tool_calls.append(ToolCall(id=openai_tc.id, tool_name=openai_tc.function.name, arguments=arguments))
except json.JSONDecodeError:
logger.warning(
Expand Down
3 changes: 2 additions & 1 deletion haystack/components/generators/chat/openai_responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
ToolCallDelta,
select_streaming_callback,
)
from haystack.dataclasses.chat_message import _parse_openai_tool_call_arguments
from haystack.dataclasses.streaming_chunk import FinishReason, _invoke_streaming_callback
from haystack.tools import (
ToolsType,
Expand Down Expand Up @@ -692,7 +693,7 @@ def _convert_response_to_chat_message(responses: Response | ParsedResponse) -> C

elif output.type == "function_call":
try:
arguments = json.loads(output.arguments)
arguments = _parse_openai_tool_call_arguments(output.arguments)
tool_calls.append(
ToolCall(
id=output.id, tool_name=output.name, arguments=arguments, extra={"call_id": output.call_id}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
fixes:
- |
``OpenAIChatGenerator`` and ``OpenAIResponsesChatGenerator`` no longer drop a tool call whose arguments are an
empty string, and no longer raise ``TypeError`` when they are ``null``, in non-streaming responses.
OpenAI-compatible servers such as vLLM send these for a tool with no parameters. Both now become empty
arguments, as they already did in streaming responses and in ``ChatMessage.from_openai_dict_format``.
29 changes: 29 additions & 0 deletions test/components/generators/chat/test_openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
OpenAIChatGenerator,
_check_finish_reason,
_convert_chat_completion_chunk_to_streaming_chunk,
_convert_chat_completion_to_chat_message,
_make_schema_strict,
)
from haystack.components.generators.utils import print_streaming_chunk
Expand Down Expand Up @@ -2031,6 +2032,34 @@ def test_convert_usage_chunk_to_streaming_chunk(self) -> None:
assert result.meta["model"] == "gpt-5-mini"
assert result.meta["received_at"] is not None

@pytest.mark.parametrize("arguments", ["", None])
def test_convert_chat_completion_with_zero_argument_tool_call(self, arguments: str | None) -> None:
# OpenAI-compatible servers such as vLLM send an empty string or null for a tool with no parameters
completion = ChatCompletion(
id="1",
model="gpt-5-mini",
object="chat.completion",
created=1234567890,
choices=[
Choice(
finish_reason="tool_calls",
index=0,
message=ChatCompletionMessage(
role="assistant",
tool_calls=[
ChatCompletionMessageFunctionToolCall(
id="1",
type="function",
function=Function.model_construct(name="get_time", arguments=arguments),
)
],
),
)
],
)
message = _convert_chat_completion_to_chat_message(completion, completion.choices[0])
assert message.tool_calls == [ToolCall(id="1", tool_name="get_time", arguments={})]


class TestMakeSchemaStrict:
def test_flat_object(self) -> None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1296,6 +1296,20 @@ def test_convert_incomplete_response_with_finish_reason(

assert message.meta["finish_reason"] == finish_reason

@pytest.mark.parametrize("arguments", ["", None])
def test_convert_zero_argument_function_call(self, arguments: str | None) -> None:
# OpenAI-compatible servers such as vLLM send an empty string or null for a tool with no parameters
function_call = ResponseFunctionToolCall.model_construct(
arguments=arguments, call_id="call_1", name="get_time", type="function_call", id="fc_1", status="completed"
)
response = Response.model_construct(output=[function_call], output_text=None, status="completed")

message = _convert_response_to_chat_message(response)

assert message.tool_calls == [
ToolCall(id="fc_1", tool_name="get_time", arguments={}, extra={"call_id": "call_1"})
]

def test_convert_system_message(self) -> None:

message = ChatMessage.from_system("You are good assistant")
Expand Down
Loading