feat: ChatMessage.to_openai_dict_format - add require_tool_call_ids parameter (#9481)

This commit is contained in:
Stefano Fiorucci 2025-06-03 16:55:13 +02:00 committed by GitHub
parent ce0917e586
commit 1e2214a1a0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 53 additions and 15 deletions

View File

@ -388,9 +388,19 @@ class ChatMessage:
raise ValueError(f"Missing 'content' or '_content' in serialized ChatMessage: `{data}`")
def to_openai_dict_format(self) -> Dict[str, Any]:
def to_openai_dict_format(self, require_tool_call_ids: bool = True) -> Dict[str, Any]:
"""
Convert a ChatMessage to the dictionary format expected by OpenAI's Chat API.
:param require_tool_call_ids:
If True (default), enforces that each Tool Call includes a non-null `id` attribute.
Set to False to allow Tool Calls without `id`, which may be suitable for shallow OpenAI-compatible APIs.
:returns:
The ChatMessage in the format expected by OpenAI's Chat API.
:raises ValueError:
If the message format is invalid, or if `require_tool_call_ids` is True and any Tool Call is missing an
`id` attribute.
"""
text_contents = self.texts
tool_calls = self.tool_calls
@ -411,10 +421,12 @@ class ChatMessage:
if tool_call_results:
result = tool_call_results[0]
if result.origin.id is None:
raise ValueError("`ToolCall` must have a non-null `id` attribute to be used with OpenAI.")
openai_msg["content"] = result.result
openai_msg["tool_call_id"] = result.origin.id
if result.origin.id is not None:
openai_msg["tool_call_id"] = result.origin.id
elif require_tool_call_ids:
raise ValueError("`ToolCall` must have a non-null `id` attribute to be used with OpenAI.")
# OpenAI does not provide a way to communicate errors in tool invocations, so we ignore the error field
return openai_msg
@ -422,17 +434,19 @@ class ChatMessage:
openai_msg["content"] = text_contents[0]
if tool_calls:
openai_tool_calls = []
for tc in tool_calls:
if tc.id is None:
openai_tool_call = {
"type": "function",
# We disable ensure_ascii so special chars like emojis are not converted
"function": {"name": tc.tool_name, "arguments": json.dumps(tc.arguments, ensure_ascii=False)},
}
if tc.id is not None:
openai_tool_call["id"] = tc.id
elif require_tool_call_ids:
raise ValueError("`ToolCall` must have a non-null `id` attribute to be used with OpenAI.")
openai_tool_calls.append(
{
"id": tc.id,
"type": "function",
# We disable ensure_ascii so special chars like emojis are not converted
"function": {"name": tc.tool_name, "arguments": json.dumps(tc.arguments, ensure_ascii=False)},
}
)
openai_tool_calls.append(openai_tool_call)
openai_msg["tool_calls"] = openai_tool_calls
return openai_msg

View File

@ -0,0 +1,7 @@
---
enhancements:
- |
Add a new parameter `require_tool_call_ids` to `ChatMessage.to_openai_dict_format`.
The default is `True`, for compatibility with OpenAI's Chat API: if the `id` field is missing in a Tool Call,
an error is raised. Using `False` is useful for shallow OpenAI-compatible APIs, where the `id` field is not
required.

View File

@ -342,14 +342,31 @@ def test_to_openai_dict_format_invalid():
with pytest.raises(ValueError):
message.to_openai_dict_format()
def test_to_openai_dict_format_require_tool_call_ids():
tool_call_null_id = ToolCall(id=None, tool_name="weather", arguments={"city": "Paris"})
message = ChatMessage.from_assistant(tool_calls=[tool_call_null_id])
with pytest.raises(ValueError):
message.to_openai_dict_format()
message.to_openai_dict_format(require_tool_call_ids=True)
message = ChatMessage.from_tool(tool_result="result", origin=tool_call_null_id)
with pytest.raises(ValueError):
message.to_openai_dict_format()
message.to_openai_dict_format(require_tool_call_ids=True)
def test_to_openai_dict_format_require_tool_call_ids_false():
tool_call_null_id = ToolCall(id=None, tool_name="weather", arguments={"city": "Paris"})
message = ChatMessage.from_assistant(tool_calls=[tool_call_null_id])
openai_msg = message.to_openai_dict_format(require_tool_call_ids=False)
assert openai_msg == {
"role": "assistant",
"tool_calls": [{"type": "function", "function": {"name": "weather", "arguments": '{"city": "Paris"}'}}],
}
message = ChatMessage.from_tool(tool_result="result", origin=tool_call_null_id)
openai_msg = message.to_openai_dict_format(require_tool_call_ids=False)
assert openai_msg == {"role": "tool", "content": "result"}
def test_from_openai_dict_format_user_message():