From 12203344ee9079bc8d6e382f000a1d042fb29b6c Mon Sep 17 00:00:00 2001 From: Andrew Chen <48723787+chuenchen309@users.noreply.github.com> Date: Mon, 13 Jul 2026 09:32:03 +0800 Subject: [PATCH 1/5] fix: handle empty or missing tool-call arguments in from_openai_dict_format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ChatMessage.from_openai_dict_format` did `json.loads(tc["function"]["arguments"])`, assuming `arguments` is always present and a valid JSON string. Zero-argument tool calls from OpenAI-compatible servers (vLLM, llama.cpp, Ollama, LM Studio, ...) send an empty string, null, or omit the key entirely, which raised `json.JSONDecodeError` or `KeyError` — even though the method docstring courts these 'shallow OpenAI-compatible APIs'. The message validator only checks that a `function` field exists, so nothing guarded this. Treat empty/null/missing arguments as an empty dict, leaving valid-JSON parsing unchanged. Co-Authored-By: Claude Opus 4.8 --- haystack/dataclasses/chat_message.py | 5 ++++- ...call-empty-arguments-53c5621622bc1866.yaml | 8 +++++++ test/dataclasses/test_chat_message.py | 21 +++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/fix-openai-toolcall-empty-arguments-53c5621622bc1866.yaml diff --git a/haystack/dataclasses/chat_message.py b/haystack/dataclasses/chat_message.py index 39da893f665..6547b27919a 100644 --- a/haystack/dataclasses/chat_message.py +++ b/haystack/dataclasses/chat_message.py @@ -792,10 +792,13 @@ def from_openai_dict_format(cls, message: dict[str, Any]) -> "ChatMessage": if tool_calls: haystack_tool_calls = [] for tc in tool_calls: + # Zero-argument tool calls from OpenAI-compatible servers may send an + # empty string, null, or omit `arguments` entirely; treat all as {}. + raw_arguments = tc["function"].get("arguments") haystack_tc = ToolCall( id=tc.get("id"), tool_name=tc["function"]["name"], - arguments=json.loads(tc["function"]["arguments"]), + arguments=json.loads(raw_arguments) if raw_arguments else {}, ) haystack_tool_calls.append(haystack_tc) return cls.from_assistant(text=content, name=name, tool_calls=haystack_tool_calls) diff --git a/releasenotes/notes/fix-openai-toolcall-empty-arguments-53c5621622bc1866.yaml b/releasenotes/notes/fix-openai-toolcall-empty-arguments-53c5621622bc1866.yaml new file mode 100644 index 00000000000..fbc9ca24f66 --- /dev/null +++ b/releasenotes/notes/fix-openai-toolcall-empty-arguments-53c5621622bc1866.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + Fixed `ChatMessage.from_openai_dict_format` crashing on zero-argument tool + calls. OpenAI-compatible servers (vLLM, llama.cpp, Ollama, LM Studio, ...) may + send an empty string or omit the `arguments` field for a tool call that takes + no arguments; this previously raised `json.JSONDecodeError` or `KeyError`. + Such tool calls are now parsed into a `ToolCall` with empty `arguments`. diff --git a/test/dataclasses/test_chat_message.py b/test/dataclasses/test_chat_message.py index 52c600446ee..4a6a9824c9b 100644 --- a/test/dataclasses/test_chat_message.py +++ b/test/dataclasses/test_chat_message.py @@ -966,6 +966,27 @@ def test_from_openai_dict_format_assistant_message_with_tool_calls(self): assert tool_call.tool_name == "get_weather" assert tool_call.arguments == {"location": "Berlin"} + def test_from_openai_dict_format_tool_call_with_empty_arguments(self): + # OpenAI-compatible servers (vLLM, llama.cpp, Ollama, ...) emit an empty + # string for a zero-argument tool call; it must not crash. + openai_msg = { + "role": "assistant", + "content": None, + "tool_calls": [{"id": "call_1", "function": {"name": "now", "arguments": ""}}], + } + message = ChatMessage.from_openai_dict_format(openai_msg) + assert message.tool_call == ToolCall(id="call_1", tool_name="now", arguments={}) + + def test_from_openai_dict_format_tool_call_with_missing_arguments(self): + # Some servers omit the `arguments` key entirely for zero-argument calls. + openai_msg = { + "role": "assistant", + "content": None, + "tool_calls": [{"id": "call_1", "function": {"name": "now"}}], + } + message = ChatMessage.from_openai_dict_format(openai_msg) + assert message.tool_call.arguments == {} + def test_from_openai_dict_format_tool_message(self): openai_msg = {"role": "tool", "content": "The weather is sunny", "tool_call_id": "call_123"} message = ChatMessage.from_openai_dict_format(openai_msg) From e13dde8000c66e611147c205efce5aebd3422df3 Mon Sep 17 00:00:00 2001 From: Andrew Chen <48723787+chuenchen309@users.noreply.github.com> Date: Mon, 13 Jul 2026 17:35:47 +0800 Subject: [PATCH 2/5] docs: use double backticks in release note for reno RST check Co-Authored-By: Claude Opus 4.8 --- .../notes/chatmsg-from-openai-dict-f15b50d38bdf9abb.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/releasenotes/notes/chatmsg-from-openai-dict-f15b50d38bdf9abb.yaml b/releasenotes/notes/chatmsg-from-openai-dict-f15b50d38bdf9abb.yaml index c51ec7f0fd9..d4378b733cf 100644 --- a/releasenotes/notes/chatmsg-from-openai-dict-f15b50d38bdf9abb.yaml +++ b/releasenotes/notes/chatmsg-from-openai-dict-f15b50d38bdf9abb.yaml @@ -1,5 +1,5 @@ --- enhancements: - | - Add the `from_openai_dict_format` class method to the `ChatMessage` class. It allows you to create a `ChatMessage` + Add the ``from_openai_dict_format`` class method to the ``ChatMessage`` class. It allows you to create a ``ChatMessage`` from a dictionary in the format expected by OpenAI's Chat API. From 952f73e47a558be79e3be751b2eeb8509e9477a3 Mon Sep 17 00:00:00 2001 From: Andrew Chen <48723787+chuenchen309@users.noreply.github.com> Date: Wed, 15 Jul 2026 20:54:07 +0800 Subject: [PATCH 3/5] test: narrow optional tool call before accessing arguments Co-Authored-By: Claude Opus 4.8 --- test/dataclasses/test_chat_message.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/dataclasses/test_chat_message.py b/test/dataclasses/test_chat_message.py index 4a6a9824c9b..22193802dcd 100644 --- a/test/dataclasses/test_chat_message.py +++ b/test/dataclasses/test_chat_message.py @@ -985,6 +985,7 @@ def test_from_openai_dict_format_tool_call_with_missing_arguments(self): "tool_calls": [{"id": "call_1", "function": {"name": "now"}}], } message = ChatMessage.from_openai_dict_format(openai_msg) + assert message.tool_call is not None assert message.tool_call.arguments == {} def test_from_openai_dict_format_tool_message(self): From ca1ebccdba4a8dd9e43ebccacfec779489e7e4f5 Mon Sep 17 00:00:00 2001 From: Andrew Chen <48723787+chuenchen309@users.noreply.github.com> Date: Wed, 15 Jul 2026 20:57:35 +0800 Subject: [PATCH 4/5] docs: use RST inline code in tool-call release note Co-Authored-By: Claude Opus 4.8 --- ...-openai-toolcall-empty-arguments-53c5621622bc1866.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/releasenotes/notes/fix-openai-toolcall-empty-arguments-53c5621622bc1866.yaml b/releasenotes/notes/fix-openai-toolcall-empty-arguments-53c5621622bc1866.yaml index fbc9ca24f66..b09d7021b65 100644 --- a/releasenotes/notes/fix-openai-toolcall-empty-arguments-53c5621622bc1866.yaml +++ b/releasenotes/notes/fix-openai-toolcall-empty-arguments-53c5621622bc1866.yaml @@ -1,8 +1,8 @@ --- fixes: - | - Fixed `ChatMessage.from_openai_dict_format` crashing on zero-argument tool + Fixed ``ChatMessage.from_openai_dict_format`` crashing on zero-argument tool calls. OpenAI-compatible servers (vLLM, llama.cpp, Ollama, LM Studio, ...) may - send an empty string or omit the `arguments` field for a tool call that takes - no arguments; this previously raised `json.JSONDecodeError` or `KeyError`. - Such tool calls are now parsed into a `ToolCall` with empty `arguments`. + send an empty string or omit the ``arguments`` field for a tool call that takes + no arguments; this previously raised ``json.JSONDecodeError`` or ``KeyError``. + Such tool calls are now parsed into a ``ToolCall`` with empty ``arguments``. From 28d7ca83bbc7bd4000f4ffef2238fe86eb875827 Mon Sep 17 00:00:00 2001 From: Andrew Chen <48723787+chuenchen309@users.noreply.github.com> Date: Wed, 15 Jul 2026 22:38:53 +0800 Subject: [PATCH 5/5] docs: keep #11954 release note unchanged from main (out of scope for this PR) --- .../notes/chatmsg-from-openai-dict-f15b50d38bdf9abb.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/releasenotes/notes/chatmsg-from-openai-dict-f15b50d38bdf9abb.yaml b/releasenotes/notes/chatmsg-from-openai-dict-f15b50d38bdf9abb.yaml index d4378b733cf..c51ec7f0fd9 100644 --- a/releasenotes/notes/chatmsg-from-openai-dict-f15b50d38bdf9abb.yaml +++ b/releasenotes/notes/chatmsg-from-openai-dict-f15b50d38bdf9abb.yaml @@ -1,5 +1,5 @@ --- enhancements: - | - Add the ``from_openai_dict_format`` class method to the ``ChatMessage`` class. It allows you to create a ``ChatMessage`` + Add the `from_openai_dict_format` class method to the `ChatMessage` class. It allows you to create a `ChatMessage` from a dictionary in the format expected by OpenAI's Chat API.