Skip to content

Commit a574a9f

Browse files
chuenchen309claude
andcommitted
fix: handle empty or missing tool-call arguments in from_openai_dict_format
`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 <noreply@anthropic.com>
1 parent acc9e58 commit a574a9f

3 files changed

Lines changed: 33 additions & 1 deletion

File tree

haystack/dataclasses/chat_message.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -792,10 +792,13 @@ def from_openai_dict_format(cls, message: dict[str, Any]) -> "ChatMessage":
792792
if tool_calls:
793793
haystack_tool_calls = []
794794
for tc in tool_calls:
795+
# Zero-argument tool calls from OpenAI-compatible servers may send an
796+
# empty string, null, or omit `arguments` entirely; treat all as {}.
797+
raw_arguments = tc["function"].get("arguments")
795798
haystack_tc = ToolCall(
796799
id=tc.get("id"),
797800
tool_name=tc["function"]["name"],
798-
arguments=json.loads(tc["function"]["arguments"]),
801+
arguments=json.loads(raw_arguments) if raw_arguments else {},
799802
)
800803
haystack_tool_calls.append(haystack_tc)
801804
return cls.from_assistant(text=content, name=name, tool_calls=haystack_tool_calls)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
fixes:
3+
- |
4+
Fixed `ChatMessage.from_openai_dict_format` crashing on zero-argument tool
5+
calls. OpenAI-compatible servers (vLLM, llama.cpp, Ollama, LM Studio, ...) may
6+
send an empty string or omit the `arguments` field for a tool call that takes
7+
no arguments; this previously raised `json.JSONDecodeError` or `KeyError`.
8+
Such tool calls are now parsed into a `ToolCall` with empty `arguments`.

test/dataclasses/test_chat_message.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -966,6 +966,27 @@ def test_from_openai_dict_format_assistant_message_with_tool_calls(self):
966966
assert tool_call.tool_name == "get_weather"
967967
assert tool_call.arguments == {"location": "Berlin"}
968968

969+
def test_from_openai_dict_format_tool_call_with_empty_arguments(self):
970+
# OpenAI-compatible servers (vLLM, llama.cpp, Ollama, ...) emit an empty
971+
# string for a zero-argument tool call; it must not crash.
972+
openai_msg = {
973+
"role": "assistant",
974+
"content": None,
975+
"tool_calls": [{"id": "call_1", "function": {"name": "now", "arguments": ""}}],
976+
}
977+
message = ChatMessage.from_openai_dict_format(openai_msg)
978+
assert message.tool_call == ToolCall(id="call_1", tool_name="now", arguments={})
979+
980+
def test_from_openai_dict_format_tool_call_with_missing_arguments(self):
981+
# Some servers omit the `arguments` key entirely for zero-argument calls.
982+
openai_msg = {
983+
"role": "assistant",
984+
"content": None,
985+
"tool_calls": [{"id": "call_1", "function": {"name": "now"}}],
986+
}
987+
message = ChatMessage.from_openai_dict_format(openai_msg)
988+
assert message.tool_call.arguments == {}
989+
969990
def test_from_openai_dict_format_tool_message(self):
970991
openai_msg = {"role": "tool", "content": "The weather is sunny", "tool_call_id": "call_123"}
971992
message = ChatMessage.from_openai_dict_format(openai_msg)

0 commit comments

Comments
 (0)