From ff759ba7e4d852c288723dd96e0a1fb7e935db37 Mon Sep 17 00:00:00 2001 From: Lee Jackson <130007945+Imagineer99@users.noreply.github.com> Date: Tue, 28 Apr 2026 22:49:13 +0100 Subject: [PATCH] Studio: Fix image-only chat requests failing validation (#5212) * fix: allow image-only chat messages * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * test: deduplicate empty content validation coverage --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- studio/backend/models/inference.py | 11 ++++++----- .../tests/test_openai_tool_passthrough.py | 19 ++++++++++++++----- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/studio/backend/models/inference.py b/studio/backend/models/inference.py index bf0177efbf..eb2bb9c5ce 100644 --- a/studio/backend/models/inference.py +++ b/studio/backend/models/inference.py @@ -396,7 +396,10 @@ class ChatMessage(BaseModel): if self.name is not None and self.role != "tool": raise ValueError('"name" is only valid on role="tool" messages.') - # Per-role content requirements. + # Per-role content requirements. OpenAI-compatible clients may send + # ``content=""`` for image-only turns when the image travels in a + # companion field such as Studio's ``image_base64`` extension, so treat + # empty strings as present content for user/system messages. if self.role == "tool": if not self.tool_call_id: raise ValueError( @@ -411,10 +414,8 @@ class ChatMessage(BaseModel): 'role="assistant" messages require either "content" or "tool_calls".' ) else: # "user" | "system" - if not self.content: - raise ValueError( - f'role="{self.role}" messages require non-empty "content".' - ) + if self.content is None or self.content == []: + raise ValueError(f'role="{self.role}" messages require "content".') return self diff --git a/studio/backend/tests/test_openai_tool_passthrough.py b/studio/backend/tests/test_openai_tool_passthrough.py index ccb0dba325..cdb7f5d270 100644 --- a/studio/backend/tests/test_openai_tool_passthrough.py +++ b/studio/backend/tests/test_openai_tool_passthrough.py @@ -144,13 +144,14 @@ class TestChatMessageToolRoles: # ── Role-aware content requirements ──────────────────────────── - def test_user_empty_content_rejected(self): - with pytest.raises(ValidationError): - ChatMessage(role = "user", content = "") + @pytest.mark.parametrize("role", ["user", "system"]) + def test_empty_string_content_allowed(self, role): + msg = ChatMessage(role = role, content = "") + assert msg.content == "" - def test_system_empty_content_rejected(self): + def test_user_missing_content_rejected(self): with pytest.raises(ValidationError): - ChatMessage(role = "system", content = "") + ChatMessage(role = "user") def test_user_empty_list_content_rejected(self): with pytest.raises(ValidationError): @@ -226,6 +227,14 @@ class TestChatCompletionRequestToolFields: assert len(req.tools) == 1 assert req.tools[0]["function"]["name"] == "get_weather" + def test_image_base64_allows_empty_user_text(self): + req = ChatCompletionRequest( + messages = [{"role": "user", "content": ""}], + image_base64 = "aW1hZ2U=", + ) + assert req.messages[0].content == "" + assert req.image_base64 == "aW1hZ2U=" + def test_tool_choice_string_auto(self): assert self._make(tool_choice = "auto").tool_choice == "auto"