From e48f2aa4cab84eaca9e8473552ae704835bbbf61 Mon Sep 17 00:00:00 2001 From: Maxwill Lin <0312fs3@gmail.com> Date: Tue, 23 Jun 2026 17:04:26 -0700 Subject: [PATCH] [Bugfix][Frontend] Emit a content block for empty Anthropic completions (#46525) Signed-off-by: EazyReal <8047065+EazyReal@users.noreply.github.com> --- .../test_anthropic_messages_conversion.py | 39 +++++++++++++++++++ vllm/entrypoints/anthropic/serving.py | 6 +++ 2 files changed, 45 insertions(+) diff --git a/tests/entrypoints/anthropic/test_anthropic_messages_conversion.py b/tests/entrypoints/anthropic/test_anthropic_messages_conversion.py index b3447387c8..f89d12553b 100644 --- a/tests/entrypoints/anthropic/test_anthropic_messages_conversion.py +++ b/tests/entrypoints/anthropic/test_anthropic_messages_conversion.py @@ -26,8 +26,11 @@ from vllm.entrypoints.anthropic.serving import ( _get_cached_tokens, ) from vllm.entrypoints.openai.chat_completion.protocol import ( + ChatCompletionResponse, + ChatCompletionResponseChoice, ChatCompletionResponseStreamChoice, ChatCompletionStreamResponse, + ChatMessage, ) from vllm.entrypoints.openai.engine.protocol import ( DeltaFunctionCall, @@ -1381,3 +1384,39 @@ class TestDetectMergeInlineSystem: def test_no_template_defaults_merge(self): """No chat_template → conservative default: merge.""" assert AnthropicServingMessages._detect_merge_inline_system(None) is True + + +# ====================================================================== +# Full (non-streaming) response conversion: messages_full_converter +# ====================================================================== + + +def _make_full_converter(): + obj = MagicMock(spec=AnthropicServingMessages) + obj.messages_full_converter = ( + AnthropicServingMessages.messages_full_converter.__get__(obj) + ) + return obj + + +class TestMessagesFullConverter: + def test_empty_completion_emits_one_text_block(self): + """An empty completion still yields exactly one (empty) text block.""" + generator = ChatCompletionResponse( + id="chatcmpl-empty", + model="test-model", + choices=[ + ChatCompletionResponseChoice( + index=0, + message=ChatMessage(role="assistant", content=None), + finish_reason="stop", + ) + ], + usage=UsageInfo(prompt_tokens=10, completion_tokens=0, total_tokens=10), + ) + + result = _make_full_converter().messages_full_converter(generator) + + assert len(result.content) == 1 + assert result.content[0].type == "text" + assert result.content[0].text == "" diff --git a/vllm/entrypoints/anthropic/serving.py b/vllm/entrypoints/anthropic/serving.py index 15550b262b..2cb4832d47 100644 --- a/vllm/entrypoints/anthropic/serving.py +++ b/vllm/entrypoints/anthropic/serving.py @@ -665,6 +665,12 @@ class AnthropicServingMessages(OpenAIServingChat): ) content += [anthropic_tool_call] + # Anthropic's canonical shape for an empty completion is a single + # empty text block, not []. Some strict clients assume content[0] + # exists, so emit one here. + if not content: + content.append(AnthropicContentBlock(type="text", text="")) + result.content = content return result