[Bugfix][Frontend] Emit a content block for empty Anthropic completions (#46525)

Signed-off-by: EazyReal <8047065+EazyReal@users.noreply.github.com>

Maxwill Lin committed Jun 23, 2026 at 17:04 UTC e48f2aa4cab84eaca9e8473552ae704835bbbf61
2 files changed +45
tests/entrypoints/anthropic/test_anthropic_messages_conversion.py
+39
@@ -26,8 +26,11 @@ from vllm.entrypoints.anthropic.serving import (
26 _get_cached_tokens,
27 )
28 from vllm.entrypoints.openai.chat_completion.protocol import (
29 + ChatCompletionResponse,
30 + ChatCompletionResponseChoice,
31 ChatCompletionResponseStreamChoice,
32 ChatCompletionStreamResponse,
33 + ChatMessage,
34 )
35 from vllm.entrypoints.openai.engine.protocol import (
36 DeltaFunctionCall,
@@ -1381,3 +1384,39 @@ class TestDetectMergeInlineSystem:
1384 def test_no_template_defaults_merge(self):
1385 """No chat_template → conservative default: merge."""
1386 assert AnthropicServingMessages._detect_merge_inline_system(None) is True
1387 +
1388 +
1389 +# ======================================================================
1390 +# Full (non-streaming) response conversion: messages_full_converter
1391 +# ======================================================================
1392 +
1393 +
1394 +def _make_full_converter():
1395 + obj = MagicMock(spec=AnthropicServingMessages)
1396 + obj.messages_full_converter = (
1397 + AnthropicServingMessages.messages_full_converter.__get__(obj)
1398 + )
1399 + return obj
1400 +
1401 +
1402 +class TestMessagesFullConverter:
1403 + def test_empty_completion_emits_one_text_block(self):
1404 + """An empty completion still yields exactly one (empty) text block."""
1405 + generator = ChatCompletionResponse(
1406 + id="chatcmpl-empty",
1407 + model="test-model",
1408 + choices=[
1409 + ChatCompletionResponseChoice(
1410 + index=0,
1411 + message=ChatMessage(role="assistant", content=None),
1412 + finish_reason="stop",
1413 + )
1414 + ],
1415 + usage=UsageInfo(prompt_tokens=10, completion_tokens=0, total_tokens=10),
1416 + )
1417 +
1418 + result = _make_full_converter().messages_full_converter(generator)
1419 +
1420 + assert len(result.content) == 1
1421 + assert result.content[0].type == "text"
1422 + assert result.content[0].text == ""
vllm/entrypoints/anthropic/serving.py
+6
@@ -665,6 +665,12 @@ class AnthropicServingMessages(OpenAIServingChat):
665 )
666 content += [anthropic_tool_call]
667
668 + # Anthropic's canonical shape for an empty completion is a single
669 + # empty text block, not []. Some strict clients assume content[0]
670 + # exists, so emit one here.
671 + if not content:
672 + content.append(AnthropicContentBlock(type="text", text=""))
673 +
674 result.content = content
675
676 return result