diff --git a/src/ai/ui/ai_sdk/outbound_messages.py b/src/ai/ui/ai_sdk/outbound_messages.py index d6055555..af22767a 100644 --- a/src/ai/ui/ai_sdk/outbound_messages.py +++ b/src/ai/ui/ai_sdk/outbound_messages.py @@ -118,7 +118,8 @@ def bundle_to_wire_output(bundle: messages_.MessageBundle) -> Any: The inbound side accepts either shape (see ``_build_result_part``). """ dumped = [ - m.model_dump(mode="json") for m in to_ui_messages(list(bundle.messages)) + m.model_dump(mode="json", exclude_none=True) + for m in to_ui_messages(list(bundle.messages)) ] if not dumped: return None diff --git a/src/ai/ui/ai_sdk/ui_messages.py b/src/ai/ui/ai_sdk/ui_messages.py index 20d905fc..19a0a105 100644 --- a/src/ai/ui/ai_sdk/ui_messages.py +++ b/src/ai/ui/ai_sdk/ui_messages.py @@ -14,7 +14,9 @@ from ...types import messages as messages_ -_UI_MODEL_CONFIG = pydantic.ConfigDict(populate_by_name=True, extra="allow") +_UI_MODEL_CONFIG = pydantic.ConfigDict( + populate_by_name=True, extra="allow", serialize_by_alias=True +) class UITextPart(pydantic.BaseModel): @@ -330,6 +332,10 @@ class UIMessage(pydantic.BaseModel): """Message in AI SDK v6 format. Reference: https://ai-sdk.dev/docs/reference/ai-sdk-core/ui-message + + To send one to an AI SDK client, dump it with + ``model_dump(mode="json", exclude_none=True)``: the TS schemas mark + optional fields ``.optional()``, which rejects ``null``. """ model_config = _UI_MODEL_CONFIG diff --git a/tests/ui/ai_sdk/test_outbound_messages.py b/tests/ui/ai_sdk/test_outbound_messages.py index e83be7a1..09cec9f2 100644 --- a/tests/ui/ai_sdk/test_outbound_messages.py +++ b/tests/ui/ai_sdk/test_outbound_messages.py @@ -666,3 +666,38 @@ def test_deferred_approval_hook_roundtrips_tool_call_id() -> None: assert hook.hook_id == hook_id assert hook.status == "pending" assert hook.tool_call_id == "tc-1" + + +def test_bundle_to_wire_output_uses_wire_aliases() -> None: + bundle = messages_.MessageBundle( + messages=( + messages_.Message( + role="assistant", + parts=[ + messages_.ToolCallPart( + tool_call_id="tc1", tool_name="bash", tool_args="{}" + ), + messages_.FilePart(data=b"png", media_type="image/png"), + ], + ), + messages_.Message( + role="tool", + parts=[ + messages_.ToolResultPart( + tool_call_id="tc1", + tool_name="bash", + result="boom", + result_kind="error", + ) + ], + ), + ) + ) + + wire = outbound_messages.bundle_to_wire_output(bundle) + + tool_part, file_part = wire["parts"] + assert tool_part["toolCallId"] == "tc1" + assert tool_part["errorText"] == "boom" + assert file_part["mediaType"] == "image/png" + assert "providerExecuted" not in tool_part