From 85840e7e4d9a92903be155a4f2159787f1993000 Mon Sep 17 00:00:00 2001 From: Tai An Date: Sat, 6 Jun 2026 12:09:34 -0700 Subject: [PATCH 1/2] fix(studio/responses): emit placeholder for empty tool output _normalise_responses_input builds a role="tool" ChatMessage straight from item.output. When a function_call_output carries an empty/falsy output (e.g. an image-only result whose payload lives outside the output field), content becomes "" and ChatMessage._validate_role_shape raises the non-empty-content error, surfacing as a 500 on /v1/responses. Substitute a placeholder so the turn normalises gracefully. Fixes #6047 --- studio/backend/routes/inference.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/studio/backend/routes/inference.py b/studio/backend/routes/inference.py index 6b559b9c45..037606ced8 100644 --- a/studio/backend/routes/inference.py +++ b/studio/backend/routes/inference.py @@ -2797,6 +2797,13 @@ def _normalise_responses_input(payload: ResponsesRequest) -> list[ChatMessage]: output = item.output if not isinstance(output, str): output = json.dumps(output) + if not output: + # An empty/falsy result (e.g. an image-only tool output whose + # payload lives outside `output`) would otherwise build a + # role="tool" ChatMessage with content="", which the strict + # validator rejects with a 500. Emit a placeholder so the turn + # normalises gracefully. + output = "(no output)" messages.append( ChatMessage( role = "tool", From 09f470b45b435557b67836e69e207b275814e2b6 Mon Sep 17 00:00:00 2001 From: Datta Nimmaturi Date: Mon, 8 Jun 2026 04:30:39 +0000 Subject: [PATCH 2/2] test: add regression tests for empty tool output fix (issue #6047) --- .../tests/test_responses_tool_passthrough.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/studio/backend/tests/test_responses_tool_passthrough.py b/studio/backend/tests/test_responses_tool_passthrough.py index 2f1161c329..be99454483 100644 --- a/studio/backend/tests/test_responses_tool_passthrough.py +++ b/studio/backend/tests/test_responses_tool_passthrough.py @@ -374,6 +374,44 @@ class TestNormaliseResponsesInputWithTools: # Content is serialised so llama-server sees a string. assert json.loads(msgs[0].content) == [{"type": "output_text", "text": "ok"}] + def test_empty_tool_output_replaced_with_placeholder(self): + """Image-only tool results (Anthropic format) send empty output. + Before the fix this crashed the validator with + "role=\"tool\" messages require non-empty \"content\"". + Now it emits "(no output)" so the turn normalises cleanly.""" + payload = ResponsesRequest( + input = [ + { + "type": "function_call_output", + "call_id": "call_1", + "output": "", + } + ], + ) + msgs = _normalise_responses_input(payload) + assert msgs[0].role == "tool" + assert msgs[0].tool_call_id == "call_1" + assert msgs[0].content == "(no output)" + # Round-trip through ChatMessage validator must not raise. + ChatMessage(**msgs[0].model_dump(exclude_none = True)) + + def test_empty_list_output_serialised_to_json_array(self): + """Empty list output is not falsy after json.dumps (becomes "[]"), + so it should not trigger the placeholder.""" + payload = ResponsesRequest( + input = [ + { + "type": "function_call_output", + "call_id": "call_1", + "output": [], + } + ], + ) + msgs = _normalise_responses_input(payload) + assert msgs[0].role == "tool" + assert msgs[0].content == "[]" + ChatMessage(**msgs[0].model_dump(exclude_none = True)) + # ===================================================================== # Response mapping — tool_calls → function_call output items