Skip to content

Commit 1e2214a

Browse files
authored
feat: ChatMessage.to_openai_dict_format - add require_tool_call_ids parameter (#9481)
1 parent ce0917e commit 1e2214a

File tree

3 files changed

+53
-15
lines changed

3 files changed

+53
-15
lines changed

haystack/dataclasses/chat_message.py

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -388,9 +388,19 @@ def from_dict(cls, data: Dict[str, Any]) -> "ChatMessage":
388388

389389
raise ValueError(f"Missing 'content' or '_content' in serialized ChatMessage: `{data}`")
390390

391-
def to_openai_dict_format(self) -> Dict[str, Any]:
391+
def to_openai_dict_format(self, require_tool_call_ids: bool = True) -> Dict[str, Any]:
392392
"""
393393
Convert a ChatMessage to the dictionary format expected by OpenAI's Chat API.
394+
395+
:param require_tool_call_ids:
396+
If True (default), enforces that each Tool Call includes a non-null `id` attribute.
397+
Set to False to allow Tool Calls without `id`, which may be suitable for shallow OpenAI-compatible APIs.
398+
:returns:
399+
The ChatMessage in the format expected by OpenAI's Chat API.
400+
401+
:raises ValueError:
402+
If the message format is invalid, or if `require_tool_call_ids` is True and any Tool Call is missing an
403+
`id` attribute.
394404
"""
395405
text_contents = self.texts
396406
tool_calls = self.tool_calls
@@ -411,28 +421,32 @@ def to_openai_dict_format(self) -> Dict[str, Any]:
411421

412422
if tool_call_results:
413423
result = tool_call_results[0]
414-
if result.origin.id is None:
415-
raise ValueError("`ToolCall` must have a non-null `id` attribute to be used with OpenAI.")
416424
openai_msg["content"] = result.result
417-
openai_msg["tool_call_id"] = result.origin.id
425+
if result.origin.id is not None:
426+
openai_msg["tool_call_id"] = result.origin.id
427+
elif require_tool_call_ids:
428+
raise ValueError("`ToolCall` must have a non-null `id` attribute to be used with OpenAI.")
429+
418430
# OpenAI does not provide a way to communicate errors in tool invocations, so we ignore the error field
419431
return openai_msg
420432

421433
if text_contents:
422434
openai_msg["content"] = text_contents[0]
423435
if tool_calls:
424436
openai_tool_calls = []
437+
425438
for tc in tool_calls:
426-
if tc.id is None:
439+
openai_tool_call = {
440+
"type": "function",
441+
# We disable ensure_ascii so special chars like emojis are not converted
442+
"function": {"name": tc.tool_name, "arguments": json.dumps(tc.arguments, ensure_ascii=False)},
443+
}
444+
if tc.id is not None:
445+
openai_tool_call["id"] = tc.id
446+
elif require_tool_call_ids:
427447
raise ValueError("`ToolCall` must have a non-null `id` attribute to be used with OpenAI.")
428-
openai_tool_calls.append(
429-
{
430-
"id": tc.id,
431-
"type": "function",
432-
# We disable ensure_ascii so special chars like emojis are not converted
433-
"function": {"name": tc.tool_name, "arguments": json.dumps(tc.arguments, ensure_ascii=False)},
434-
}
435-
)
448+
openai_tool_calls.append(openai_tool_call)
449+
436450
openai_msg["tool_calls"] = openai_tool_calls
437451
return openai_msg
438452

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
enhancements:
3+
- |
4+
Add a new parameter `require_tool_call_ids` to `ChatMessage.to_openai_dict_format`.
5+
The default is `True`, for compatibility with OpenAI's Chat API: if the `id` field is missing in a Tool Call,
6+
an error is raised. Using `False` is useful for shallow OpenAI-compatible APIs, where the `id` field is not
7+
required.

test/dataclasses/test_chat_message.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -342,14 +342,31 @@ def test_to_openai_dict_format_invalid():
342342
with pytest.raises(ValueError):
343343
message.to_openai_dict_format()
344344

345+
346+
def test_to_openai_dict_format_require_tool_call_ids():
345347
tool_call_null_id = ToolCall(id=None, tool_name="weather", arguments={"city": "Paris"})
346348
message = ChatMessage.from_assistant(tool_calls=[tool_call_null_id])
347349
with pytest.raises(ValueError):
348-
message.to_openai_dict_format()
350+
message.to_openai_dict_format(require_tool_call_ids=True)
349351

350352
message = ChatMessage.from_tool(tool_result="result", origin=tool_call_null_id)
351353
with pytest.raises(ValueError):
352-
message.to_openai_dict_format()
354+
message.to_openai_dict_format(require_tool_call_ids=True)
355+
356+
357+
def test_to_openai_dict_format_require_tool_call_ids_false():
358+
tool_call_null_id = ToolCall(id=None, tool_name="weather", arguments={"city": "Paris"})
359+
message = ChatMessage.from_assistant(tool_calls=[tool_call_null_id])
360+
openai_msg = message.to_openai_dict_format(require_tool_call_ids=False)
361+
362+
assert openai_msg == {
363+
"role": "assistant",
364+
"tool_calls": [{"type": "function", "function": {"name": "weather", "arguments": '{"city": "Paris"}'}}],
365+
}
366+
367+
message = ChatMessage.from_tool(tool_result="result", origin=tool_call_null_id)
368+
openai_msg = message.to_openai_dict_format(require_tool_call_ids=False)
369+
assert openai_msg == {"role": "tool", "content": "result"}
353370

354371

355372
def test_from_openai_dict_format_user_message():

0 commit comments

Comments
 (0)