Prhub

#44283 [Anthropic] Support system role messages inside messages array

原始 PR 作者 chaunceyjiang 合并时间 2026-06-03 02:13 文件变更 3 提交数 1 评论 8 代码增减 +173 / -17

执行摘要

支持 Anthropic messages 数组内嵌 system 角色

Claude Code CLI >= 2.1.154 发送 role:system 等角色到 messages 数组,vLLM 的 Anthropic API 角色验证只能接受 "user" 或 "assistant",导致 400 错误。需要兼容这种客户端行为。

建议精读该 PR,特别是 system 消息合并逻辑和其潜在的 KV-cache 性能影响。对于上游服务,可考虑等待 #44602 的更优方案或评估自身场景是否受前缀变更影响。

讨论亮点

在 review 中,felix0080 指出合并 system 消息会改变对话前缀,可能降低 KV-cache 命中率。作者 chaunceyjiang 承认问题并计划新方案。随后 felix0080 提出替代 PR #44602,保留内联 system 消息在原位以维持前缀结构,作为后续改进。当前 PR 为此问题的初步修复。

实现拆解

  1. 放宽协议模型:在 vllm/entrypoints/anthropic/protocol.py 中将 AnthropicMessage.roleLiteral 值从 ["user", "assistant"] 扩展为 ["user", "assistant", "system"],允许解析内联 system 角色。

  2. 重写 system 转换逻辑_convert_system_message 不再只处理顶层 system 字段,而是先收集顶层 system 文本(剥离 billing header),然后遍历 messages 数组,取出所有 role 为 system 的文本(同样剥离 header),合并到 system_parts 列表,最后用 "".join(system_parts) 拼接为一条 system 消息附加到 openai_messages 开头。

  3. 跳过 messages 数组中的 system 角色_convert_messages 遇到 msg.role == "system" 时直接 continue,避免重复添加。

  4. 新增测试:在 test_anthropic_messages_conversion.py 中添加 TestInlineSystemMessageInMessagesArray 类,覆盖:内联 system 与顶层 system 合并、纯字符串内联、列表内容内联、多个内联 system、内联 system 与顶层字符串 system 等场景,并验证 billing header 被剥离、cache_control 等字段被丢弃。

文件 模块 状态 重要度
vllm/entrypoints/anthropic/protocol.py 协议定义 modified 4.89
vllm/entrypoints/anthropic/serving.py 消息转换 modified 6.72
tests/entrypoints/anthropic/test_anthropic_messages_conversion.py 测试 modified 7.04

关键符号

_convert_system_message _convert_messages AnthropicMessage

关键源码片段

vllm/entrypoints/anthropic/protocol.py core-logic

协议模型变更入口,放宽 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 core-logic

核心转换逻辑重写,合并顶层与内联 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)

评论区精华

KV-cache 前缀缓存影响 设计

felix0080 评论 : "I'm a bit concerned about the system role fix... merging a mid-conversation system:role message into a single system message could cause issues with KV-cache hits."

结论:作者 chaunceyjiang 承认问题,felix0080 创建替代 PR #44602 保留内联 system 在原位以维持前缀缓存。当前 PR 合并作为初步修复。 · resolved by alternative

风险与影响

主要风险是 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 合并语义

关联 Issue

#44000 [Bug]: Claude Code CLI >= 2.1.154 sends ctx/msg/system roles and breaks vLLM Anthropic Messages API validation

完整报告

参与讨论