Prhub

#49996 fix: reject string schemas that mix pattern/format with length bounds

原始 PR 作者 he-yufeng 合并时间 2026-08-19 17:29 文件变更 2 提交数 2 评论 23 代码增减 +20 / -0

执行摘要

拒绝 pattern/format 与长度约束混用 schema

PR body 指出 has_xgrammar_unsupported_json_features misses a class of silently-wrong input,且验证了 xgrammar 编译 pattern/format 一侧、丢弃 minLength / maxLength、词法输出可越界却无任何错误。评论中 arpera 主张优先修复 xgrammar 上游,he-yufeng 随后提交 mlc-ai/xgrammar#749 并附编译 EBNF 证据;双方一致同意在上游修复落地前,vLLM 侧先用拒绝式守卫兜底,让调用方拿到干净的 400 而不是静默越界的输出。

值得精读:这是一个 fail-closed 守卫与上游协作的典型样本,展示了如何在依赖库存在静默语义错误时,以最小代价在服务边界兜底。值得关注的设计决策:坚持宁可 400 也不静默出错;把长期修复责任明确移交上游 issue;在对其他后端(outlines、llguidance)做行为核对后再决定是否推广。若后续要扩展结构化输出支持面,abmfy 提出的 schema 位置感知遍历(只跟随 schema 承载字段、跳过 const/enum 数据)值得纳入重构。

讨论亮点

核心讨论围绕“修复归属”展开:arpera 认为应优先在 xgrammar 上游修复(I think it's better to fix the issue on xgrammar's side),he-yufeng 提交 mlc-ai/xgrammar#749 并以编译 EBNF 证明长度界限确实被丢弃,随后双方同意在 vLLM 侧先做拒绝式守卫。另一个话题是后端一致性:he-yufeng 实测 outlines_core 是镜像问题(保留长度、丢弃 pattern),llguidance 则同时保留两者但缺乏完整匹配器验证,aarnphm 据此建议对所有后端统一测试。abmfy 的两个内联评论指出边界情况:type 数组形式(['string', 'null'])会绕过检查,递归遍历会把 const / enum 数据字段误判为子 schema;两者均标注 minor,合并前未处理。

实现拆解

  1. 核心校验扩展:在 vllm/v1/structured_output/backend_xgrammar.pyhas_xgrammar_unsupported_json_features 内部 check_object 中新增分支——当 type == "string" 且(存在 patternformat)且(存在 minLengthmaxLength)时直接返回 True。新分支与 multipleOf、数组关键字、未收录 format 等既有判定并列;由于 check_object 递归遍历对象属性和数组项,嵌套位置的同类 schema 一并被拦截,无需额外遍历逻辑。
  2. 测试配套:在 tests/v1/structured_output/test_utils.pyunsupported_string_schemas fixture 中补充 pattern + maxLengthpattern + minLengthformat + maxLength 三个用例;原有 pattern-only、format-only、length-only 用例保持不变,确保独立约束不被误判。该测试标记 cpu_test,可脱离 GPU 运行。
  3. 临时性设计:代码注释明确这是“在 xgrammar 组合两个约束之前”的过渡保护;PR 作者与 reviewer 一致认为上游修复(mlc-ai/xgrammar#749)落地后应移除该守卫,避免长期双份维护。本 PR 无配置、无部署配套改动。
文件 模块 状态 重要度
vllm/v1/structured_output/backend_xgrammar.py 结构化输出 modified 5.75
tests/v1/structured_output/test_utils.py 测试用例 modified 3.91

关键符号

has_xgrammar_unsupported_json_features check_object

分析完成后,这里会展示 LLM 生成的相对完整源码片段和详细注释。

评论区精华

修复归属:xgrammar 上游 vs vLLM 侧拦截 设计

arpera 在 issue 评论中要求优先上报 xgrammar;he-yufeng 提交 mlc-ai/xgrammar#749 并给出编译 EBNF 证据,确认长度界限被静默丢弃。aarnphm 表示等待 Yixin 的结论,并怀疑其他后端也有同样行为。

结论:先保留 vLLM 侧拒绝式守卫作为纵深防御,上游修复落地后移除,长期真理来源是 xgrammar issue。 · 已解决

其他后端(outlines_core / llguidance)是否同样丢约束 question

he-yufeng 实测 outlines_core 保留长度、丢弃 pattern,是 xgrammar 的镜像问题;llguidance 同时保留两者但未做匹配器级验证。aarnphm 建议统一测试所有后端。

结论:未在本 PR 解决,后续应做跨后端统一测试。 · 待处理

type 数组形式绕过新检查 正确性

abmfy 指出 `{'type': ['string', 'null'], 'pattern': '^a+$', 'maxLength': 2}` 可绕过新增判断,而 xgrammar 0.2.3 仍会丢 maxLength 并接受 'aaa',建议把含 string 的 type 数组也视为字符串 schema。

结论:评论标注 minor,合并前未处理,留作后续改进。 · unresolved

递归遍历把 const/enum 数据误判为子 schema 正确性

abmfy 指出递归会进入 const/enum/default/examples 等数据字段,`{'const': {...}}` 会被误判为不支持,而 xgrammar 能正确编译该常量。

结论:评论标注 minor,合并前未处理;建议遍历时跳过数据承载关键字。 · unresolved

分支混入无关 commit other

arpera 发现 PR 里出现了 anthropic messages api 的改动;he-yufeng 解释是另一个 PR 的提交被误带进来,已重建分支并 force-push。

结论:已通过 rebase 清理,diff 仅剩预期两个文件。 · 已解决

风险与影响

  1. 误报风险:check_object 递归遍历所有 dict 值,包括 constenumdefaultexamples 等数据承载字段,{'const': {'type': 'string', 'pattern': '^a+$', 'maxLength': 2}} 会被误判为不支持,虽是小概率但有功能回归隐患(abmfy 指出)。
  2. 漏检风险:{'type': ['string', 'null'], 'pattern': '^a+$', 'maxLength': 2}type 是数组而绕过检查,xgrammar 依旧静默丢长度,守卫不完整。
  3. 行为变更:原本被接受的请求现在返回 400,对已上线调用方是破坏性变更,需要发布说明。
  4. 上游依赖:真正修复在 mlc-ai/xgrammar#749,目前上游无动静,若长期不修复,守卫会一直保留并可能被遗忘维护。

用户侧:使用 v1 结构化输出且 schema 混用 pattern/format 与 minLength/maxLength 的请求,从“静默越界输出”变为“明确 400”,可控性显著提升;但 type 数组形式仍可能漏过。系统侧:新增检查是纯 Python 字典遍历,位于请求验证路径,无额外 IO 或显存开销;仅影响 xgrammar 后端的字符串 schema 分支。团队侧:需要在 xgrammar 修复后回收守卫,并考虑 aarnphm 提出的跨后端统一测试;本次改动仅 20 行,风险面小。

误拒含 const/enum 数据的合法请求 type 数组形式绕过新检查 上游 xgrammar 修复后需移除守卫 行为变更:原本接受的请求现在返回 400

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论