Prhub

#50764 [Bugfix][Frontend] Constrain Anthropic cache_salt to non-empty

原始 PR 作者 omkar-droid 合并时间 2026-08-03 13:33 文件变更 2 提交数 3 评论 9 代码增减 +49 / -0

执行摘要

约束 Anthropic cache_salt 非空,修复间歇 500

根据 PR body:test_openapi_stateless[POST /v1/messages] 间歇性 500,根因是 AnthropicMessagesRequest.cache_salt(#49498 引入)无长度约束,OpenAPI schema 将空字符串标记为合法值;Schemathesis 在 positive-data 模式下按 schema 生成 cache_salt: "",该值在内部转换到 ChatCompletionRequest 时被 check_cache_salt_support 拒绝,异常以 500 而非客户端错误逃逸。由于 Hypothesis 随机生成输入,该失败只在部分构建复现。

值得快速阅读:一行源码修复 + 两层测试,展示了"校验时机决定错误码"的经典问题,以及如何用 OpenAPI schema 断言守护契约。可关注后续统一 cache_salt 约束的 follow-up。

讨论亮点
  1. DarkLight1337 在 vllm/entrypoints/anthropic/protocol.py 第 138 行提问 "Maybe we should update this for all the other occurrences as well?",AndreasKaratzas 回应 "True, but probably best as a follow up I think. CI already completed the run too.",DarkLight1337 回复 "Sure"——统一约束被拆为后续工作。
  2. noooop 对 test_openapi_stateless 的随机性表示困惑(build 81852 重试即过);AndreasKaratzas 确认多数构建不会生成空字符串、min_length=1 合理,并主动补强测试。
  3. 一个值得注意的细节:PR body 预期 422,测试实际断言 400(HTTPStatus.BAD_REQUEST);差异来自自定义 validation_exception_handler,对客户端而言核心是从 500 变为 4xx。

实现拆解

  1. 入口约束:在 vllm/entrypoints/anthropic/protocol.pyAnthropicMessagesRequest.cache_salt 字段追加 min_length=1,一举两得——OpenAPI schema 自动发布 minLength: 1,且空字符串在请求解析层被 Pydantic 拒绝;None(省略)与非空 salt 行为不变。
  2. 错误路径收敛:此前空 salt 要到 _convert_anthropic_to_openai_request 内部转换时才由 check_cache_salt_support 拒绝而变成 500;现在在入口即返回客户端错误。注意测试实际断言 400(BAD_REQUEST),因服务端注册了自定义 validation_exception_handler;PR body 描述的 422 是 FastAPI 默认行为,两种情况下都不再是 500。
  3. 测试配套tests/entrypoints/anthropic/test_anthropic_messages_conversion.py 新增 TestCacheSalt._make_api_app 静态方法,用真实 FastAPI + attach_router + validation_exception_handler 搭建入口,并用 MagicMock 替换 serving handler 以确保无效请求不进入业务层;新增两个测试:test_cache_salt_openapi_requires_non_empty_stringapp.openapi() 解析 cache_salt 的 string schema 并断言 minLength == 1test_empty_cache_salt_returns_bad_requestTestClient 发送空 salt 请求,断言返回 400 且 handler 未被调用。
  4. 演进:第一个 commit 由作者完成修复,第三个 commit(AndreasKaratzas)补强测试覆盖;无配置、schema 或部署改动。
文件 模块 状态 重要度
vllm/entrypoints/anthropic/protocol.py 协议层 modified 4.75
tests/entrypoints/anthropic/test_anthropic_messages_conversion.py 入口测试 modified 5.97

关键符号

_make_api_app test_cache_salt_openapi_requires_non_empty_string test_empty_cache_salt_returns_bad_request

关键源码片段

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

修复入口:给 AnthropicMessagesRequest.cache_salt 加 min_length=1,从源头杜绝空字符串进入转换链路,同时让 OpenAPI schema 发布约束。

class AnthropicMessagesRequest(BaseModel):
    """Anthropic Messages API 请求体。"""
​
    model: str
    messages: list[AnthropicMessage]
    max_tokens: int
    metadata: dict[str, Any] | None = None
    output_config: AnthropicOutputConfig | None = None
    stop_sequences: list[str] | None = None
    stream: bool | None = False
    system: str | list[AnthropicContentBlock] | None = None
    temperature: float | None = None
    tool_choice: AnthropicToolChoice | None = None
    tools: list[AnthropicTool] | None = None
    top_k: int | None = None
    top_p: float | None = None
​
    # vLLM 扩展字段,不属于 Anthropic 原生规范。
    # min_length=1 有两个作用:
    # 1) OpenAPI schema 会发布 minLength: 1,阻止 schema 驱动客户端
    # (如 Schemathesis)生成空字符串;
    # 2) 显式传入空字符串时,Pydantic 在请求解析阶段直接拒绝,
    # 返回 4xx 客户端错误,而不会进入内部转换,
    # 把 check_cache_salt_support 的校验异常变成 500。
    cache_salt: str | None = Field(
        default=None,
        min_length=1,
        description=(
            "If specified, the prefix cache will be salted with the provided "
            "string to prevent an attacker to guess prompts in multi-user "
            "environments. The salt should be random, protected from "
            "access by 3rd parties, and long enough to be "
            "unpredictable (e.g., 43 characters base64-encoded, corresponding "
            "to 256 bit)."
        ),
    )
    kv_transfer_params: dict[str, Any] | None = Field(
        default=None,
        description="KVTransfer parameters used for disaggregated serving.",
    )
    ec_transfer_params: dict[str, Any] | None = Field(
        default=None,
        description=(
            "ECTransfer parameters used for encoder-cache disaggregated serving."
        ),
    )
    chat_template_kwargs: dict[str, Any] | None = Field(
        default=None,
        description=(
            "Additional keyword args to pass to the chat template renderer. "
            "Will be accessible by the template."
        ),
    )
tests/entrypoints/anthropic/test_anthropic_messages_conversion.py test-coverage

新增两层回归测试:schema 断言 + 端到端 400 请求,防止未来去掉 min_length 或 schema 结构回归。

class TestCacheSalt:
    def test_cache_salt_passed_through(self):
        """cache_salt 从 Anthropic 请求传递到转换后的
        ChatCompletionRequest,使 /v1/messages 前缀缓存隔离生效。"""
        request = _make_request(
            [{"role": "user", "content": "Hello"}],
            cache_salt="tenant-abc-secret-salt",
        )
        result = _convert(request)
        assert result.cache_salt == "tenant-abc-secret-salt"
​
    def test_cache_salt_defaults_to_none(self):
        """省略 cache_salt 时保持默认为 None,行为不变。"""
        request = _make_request([{"role": "user", "content": "Hello"}])
        result = _convert(request)
        assert result.cache_salt is None
​
    @staticmethod
    def _make_api_app():
        # 构造真实的 FastAPI 应用并挂载 Anthropic 路由,
        # 用 MagicMock 替换 serving handler,
        # 确保无效请求不会到达业务层——到达即触发 AssertionError。
        app = FastAPI()
        attach_router(app)
        app.state.args = Namespace(log_error_stack=False)
        app.exception_handler(RequestValidationError)(validation_exception_handler)
​
        handler = MagicMock(spec=AnthropicServingMessages)
        handler.create_messages.side_effect = AssertionError(
            "invalid requests must not reach the serving handler"
        )
        app.state.anthropic_serving_messages = handler
        return app, handler
​
    def test_cache_salt_openapi_requires_non_empty_string(self):
        # 从生成的 OpenAPI schema 中读取 cache_salt 的 string 分支,
        # 断言其带有 minLength: 1,防止 schema 驱动测试
        # (如 Schemathesis)再生成空字符串请求。
        app, _ = self._make_api_app()
        field_schema = app.openapi()["components"]["schemas"][
            "AnthropicMessagesRequest"
        ]["properties"]["cache_salt"]
        string_schema = next(
            option for option in field_schema["anyOf"] if option.get("type") == "string"
        )
​
        assert string_schema["minLength"] == 1
​
    def test_empty_cache_salt_returns_bad_request(self):
        # 显式传空字符串应在请求解析层返回 400,
        # 且不该进入 serving handler(否则会变成 500)。
        app, handler = self._make_api_app()
        with TestClient(app, raise_server_exceptions=False) as client:
            response = client.post(
                "/v1/messages",
                json={
                    "model": "test-model",
                    "max_tokens": 1,
                    "messages": [{"role": "user", "content": "Hello"}],
                    "cache_salt": "",
                },
            )
​
        assert response.status_code == HTTPStatus.BAD_REQUEST
        handler.create_messages.assert_not_awaited()

评论区精华

是否统一所有 cache_salt 声明的 min_length 约束 设计

DarkLight1337 在 protocol.py 第 138 行提问 "Maybe we should update this for all the other occurrences as well?",AndreasKaratzas 回应 "True, but probably best as a follow up I think. CI already completed the run too.",DarkLight1337 回复 "Sure"。

结论:同意作为后续跟进,本 PR 只约束 Anthropic 入口。 · 已解决

test_openapi_stateless 随机失败原因 测试

noooop 指出 build 81852 重试后通过,不解为何测试有随机性;AndreasKaratzas 认为多数构建不会生成 cache_salt: "",而 #49498 之前空字符串是合法的,校验发生在转换期导致 500,min_length=1 合理,并主动补强测试。

结论:确认是 Schemathesis 随机生成空字符串导致间歇失败,min_length=1 修复,并由 AndreasKaratzas 补充测试。 · 已解决

风险与影响

  1. 错误码行为变更:显式传空 cache_salt 的请求原本必然在转换期 500,现变为 4xx;没有 "合法" 用法被破坏,属于修正性变更。
  2. 同类遗漏cache_salt 可能存在于其他协议入口(如 OpenAI 侧同类字段),review 中已提出统一约束,本 PR 未处理,存在一致性问题,需 follow-up。
  3. 测试脆弱性:schema 测试直接依赖 anyOf 结构与 minLength 键名,pydantic 版本升级可能改变 schema 布局,需同步维护;但这也正是守护 schema 契约的测试目的。
  4. 影响面:仅限 Anthropic 入口校验层,不触碰模型执行、采样、输出路径,回归风险低。

对调用 Anthropic Messages API 的用户而言,无效请求现在提前得到 4xx 客户端错误,错误语义更清晰;对 CI 而言,消除了 test_openapi_stateless[POST /v1/messages] 的间歇性 500;对团队而言,为后续"统一各入口 cache_salt 约束"提供了测试范式——通过 OpenAPI schema 断言与端到端请求双重验证。

错误码行为变更(500 → 4xx) 同类字段约束待统一 测试依赖 OpenAPI schema 结构

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论