执行摘要
- 一句话:支持 Anthropic messages 数组内嵌 system 角色
- 推荐动作:建议精读该 PR,特别是 system 消息合并逻辑和其潜在的 KV-cache 性能影响。对于上游服务,可考虑等待 #44602 的更优方案或评估自身场景是否受前缀变更影响。
功能与动机
Claude Code CLI >= 2.1.154 发送 role:system 等角色到 messages 数组,vLLM 的 Anthropic API 角色验证只能接受 "user" 或 "assistant",导致 400 错误。需要兼容这种客户端行为。
实现拆解
-
放宽协议模型:在 vllm/entrypoints/anthropic/protocol.py 中将 AnthropicMessage.role 的 Literal 值从 ["user", "assistant"] 扩展为 ["user", "assistant", "system"],允许解析内联 system 角色。
-
重写 system 转换逻辑:_convert_system_message 不再只处理顶层 system 字段,而是先收集顶层 system 文本(剥离 billing header),然后遍历 messages 数组,取出所有 role 为 system 的文本(同样剥离 header),合并到 system_parts 列表,最后用 "".join(system_parts) 拼接为一条 system 消息附加到 openai_messages 开头。
-
跳过 messages 数组中的 system 角色:_convert_messages 遇到 msg.role == "system" 时直接 continue,避免重复添加。
-
新增测试:在 test_anthropic_messages_conversion.py 中添加 TestInlineSystemMessageInMessagesArray 类,覆盖:内联 system 与顶层 system 合并、纯字符串内联、列表内容内联、多个内联 system、内联 system 与顶层字符串 system 等场景,并验证 billing header 被剥离、cache_control 等字段被丢弃。
关键文件:
vllm/entrypoints/anthropic/protocol.py(模块 协议定义;类别 source;类型 core-logic;符号 AnthropicMessage): 协议模型变更入口,放宽 role 枚举以接受 system 角色
vllm/entrypoints/anthropic/serving.py(模块 消息转换;类别 source;类型 core-logic;符号 _convert_system_message, _convert_messages): 核心转换逻辑重写,合并顶层与内联 system 消息
tests/entrypoints/anthropic/test_anthropic_messages_conversion.py(模块 测试;类别 test;类型 test-coverage;符号 TestInlineSystemMessageInMessagesArray, test_inline_system_merged_with_top_level_system, test_inline_system_string_only, test_inline_system_list_content): 测试覆盖内联 system 合并的各种场景
关键符号:_convert_system_message, _convert_messages, AnthropicMessage
关键源码片段
vllm/entrypoints/anthropic/protocol.py
协议模型变更入口,放宽 role 枚举以接受 system 角色
class AnthropicMessage(BaseModel):
"""Message structure"""
# 变更 : 将 role 从 Literal["user", "assistant"] 扩展为
# Literal["user", "assistant", "system"]
role: Literal["user", "assistant", "system"]
content: str | list[AnthropicContentBlock]
vllm/entrypoints/anthropic/serving.py
核心转换逻辑重写,合并顶层与内联 system 消息
@classmethod
def _convert_system_message(
cls,
anthropic_request: AnthropicMessagesRequest | AnthropicCountTokensRequest,
openai_messages: list[dict[str, Any]],
) -> None:
"""将 Anthropic system 消息(顶层+内联)转换为 OpenAI 格式"""
system_parts: list[str] = []
# 1. 收集顶层 system 字段
if anthropic_request.system:
if isinstance(anthropic_request.system, str):
system_parts.append(anthropic_request.system)
else:
for block in anthropic_request.system:
if block.type == "text" and block.text:
# 剥离 Claude Code 的 billing header,避免破坏前缀缓存
if block.text.startswith("x-anthropic-billing-header"):
continue
system_parts.append(block.text)
# 2. 收集 messages 数组中的内联 system 消息
for msg in anthropic_request.messages:
if msg.role != "system":
continue
if isinstance(msg.content, str):
system_parts.append(msg.content)
else:
for block in msg.content:
if block.type == "text" and block.text:
if block.text.startswith("x-anthropic-billing-header"):
continue
system_parts.append(block.text)
# 3. 如果有任何 system 内容,合并为一条 system 消息
if system_parts:
openai_messages.append(
{"role": "system", "content": "".join(system_parts)}
)
@classmethod
def _convert_messages(
cls, messages: list, openai_messages: list[dict[str, Any]]
) -> None:
"""转换 Anthropic 消息到 OpenAI 格式,跳过 system 角色"""
for msg in messages:
if msg.role == "system":
continue # 已由 _convert_system_message 处理
# 原有转换逻辑 ...
openai_msg: dict[str, Any] = {"role": msg.role}
if isinstance(msg.content, str):
openai_msg["content"] = msg.content
else:
cls._convert_message_content(msg, openai_msg, openai_messages)
if not (msg.role == "user" and "content" not in openai_msg):
openai_messages.append(openai_msg)
评论区精华
在 review 中,felix0080 指出合并 system 消息会改变对话前缀,可能降低 KV-cache 命中率。作者 chaunceyjiang 承认问题并计划新方案。随后 felix0080 提出替代 PR #44602,保留内联 system 消息在原位以维持前缀结构,作为后续改进。当前 PR 为此问题的初步修复。
- KV-cache 前缀缓存影响 (design): 作者 chaunceyjiang 承认问题,felix0080 创建替代 PR #44602 保留内联 system 在原位以维持前缀缓存。当前 PR 合并作为初步修复。
风险与影响
- 风险:主要风险是 KV-cache 前缀变化:将原本可能出现在对话中段的 system 消息统一合并到开头,会改变完整对话的 token 序列,降低多轮对话的缓存命中率。此外,拼接字符串可能改变语义(如果 system 消息顺序重要)。但该 PR 针对的 Claude Code 场景中,内联 system 通常是全局指令,顺序无关紧要。billing header 剥离一致应用于所有 system 内容。
- 影响:直接修复 Claude Code CLI 2.1.154+ 与 vLLM 的兼容性问题。影响范围限定于 Anthropic API 用户,尤其是使用 Claude Code 的客户。对已有的顶层 system 消息无感知变化(合并后内容不变)。如果用户依赖消息数组中 system 角色保持原位(极少见),可能会受影响。
- 风险标记:KV-cache 前缀变更, 内联 system 合并语义
关联脉络
- PR #44602 [Anthropic] Alternative approach for inline system messages preserving conversation structure: 讨论中提出的替代方案,保留内联 system 消息原始位置以维持 KV-cache 前缀
- PR #44000 [Bug]: Claude Code CLI >= 2.1.154 sends ctx/msg/system roles and breaks vLLM Anthropic Messages API validation: 本 PR 直接修复的 issue
参与讨论