Prhub

#34778 [Fix] Work around xgrammar 0.2.1 negative integer minimum in Kimi-K3 structural tags

原始 PR 作者 gilfordting 合并时间 2026-08-15 07:41 文件变更 2 提交数 1 评论 4 代码增减 +60 / -0

执行摘要

规避 xgrammar 0.2.1 负整数下限编译错误,修复 Kimi-K3 工具调用

PR body 明确说明:Kimi-K3 工具参数 schema 若使用负整数下限,在受约束解码下会得到结构性非法的工具调用。根因是 xgrammar 0.2.1 的 JSON-schema-to-grammar 转换对 {"type": "integer", "minimum": -N} 编译错误,生成的文法接受不完整的字面量 "-" 且拒绝所有合法负整数。

值得精读,可作为处理第三方依赖转换缺陷的典型 workaround 案例。关注点:零拆分的 anyOf 策略如何保持语义等价;exclusiveMinimum 的边界转换;以及 CI 中真实 xgrammar 编译测试的写法。

讨论亮点

Review 无评论。PR 评论仅有 CI rerun 请求(gongy 两次 /rerun-test,第二次针对 test_json_schema_constraint.py),官方 bot 均反馈通过。PR body 已明确这是 converter 侧 workaround,xgrammar 修复后应移除。

实现拆解

  1. 定位转换入口:在 python/sglang/srt/function_call/kimik3_structural_tag.py 的 _value_format 中拦截 integer 类型且 schema 为 dict 的情况。

  2. 条件检测:仅当 schema 不含 maximum、exclusiveMaximum、multipleOf 时,收集 minimum 与 exclusiveMinimum(+1 后)作为下界;若 max(lower_bounds) < 0 则触发重写。

  3. 零处拆分:构造 negative = {schema, maximum: -1} 与 nonnegative = {schema, minimum: 0}(移除 exclusiveMinimum),组成 {"anyOf": [negative, nonnegative]}。语义与原 schema 等价,但每一分支都是 xgrammar 0.2.1 能正确编译的形式。

  4. 配套测试:test/registered/unit/function_call/test_kimik3_structural_tag.py 新增 test_strict_schema_handles_one_sided_negative_integer_minimum,跑在真实锁定的 xgrammar 上,断言 -1000000、-1、0、1000001 被接受,而 "-"、-1000001、1.5 被拒绝;既有测试(覆盖 maximum、multipleOf、additionalProperties 默认等)保持不变,验证未触碰场景不受影响。

文件 模块 状态 重要度
python/sglang/srt/function_call/kimik3_structural_tag.py 工具调用 modified 6.35
test/registered/unit/function_call/test_kimik3_structural_tag.py 工具调用 modified 5.77

关键符号

_value_format

关键源码片段

python/sglang/srt/function_call/kimik3_structural_tag.py core-logic

核心修复:在 _value_format 中拦截负下限 integer schema,按零拆分为 anyOf 双分支,规避 xgrammar 0.2.1 编译缺陷,同时保持 schema 语义。

def _value_format(
    schema: Union[bool, Dict[str, Any]],
    json_type: str,
    loose_string: bool = False,
) -> Format:
    if loose_string and json_type == "string":
        return AnyTextFormat()
    # XGrammar 0.2.1 对仅含负下限的 integer schema 编译错误:
    # {"type": "integer", "minimum": -N} 会接受不完整的 "-" 且拒绝所有负整数。
    # 这里把区间在零处拆成 anyOf 双分支,语义不变但每支都能被正确编译。
    if (
        json_type == "integer"
        and isinstance(schema, dict)
        and "maximum" not in schema
        and "exclusiveMaximum" not in schema
        and "multipleOf" not in schema
    ):
        lower_bounds = []
        minimum = schema.get("minimum")
        if isinstance(minimum, int) and not isinstance(minimum, bool):
            lower_bounds.append(minimum)
        exclusive_minimum = schema.get("exclusiveMinimum")
        if isinstance(exclusive_minimum, int) and not isinstance(
            exclusive_minimum, bool
        ):
            lower_bounds.append(exclusive_minimum + 1) # 严格下界转为下界
        if lower_bounds and max(lower_bounds) < 0: # 只处理纯负下界场景
            negative = dict(schema)
            negative["maximum"] = -1
            nonnegative = dict(schema)
            nonnegative.pop("exclusiveMinimum", None)
            nonnegative["minimum"] = 0
            schema = {"anyOf": [negative, nonnegative]}
    return JSONSchemaFormat(
        json_schema=schema,
        style="qwen_xml" if json_type == "string" else "json",
    )
test/registered/unit/function_call/test_kimik3_structural_tag.py test-coverage

新增 test_strict_schema_handles_one_sided_negative_integer_minimum,使用真实锁定的 xgrammar 编译并断言正负边界值与非法值的接受 / 拒绝,验证 workaround 语义等价。

def test_strict_schema_handles_one_sided_negative_integer_minimum():
    tool = Tool(
        type="function",
        function=Function(
            name="submit",
            strict=True,
            parameters={
                "type": "object",
                "properties": {
                    "value": {
                        "type": "integer",
                        "minimum": -1000000,
                    }
                },
                "required": ["value"],
                "additionalProperties": False,
            },
        ),
    )
    grammar = _grammar([tool], tool_choice="required")
    # 合法负整数与零、正数都应被接受
    for value in ("-1000000", "-999999", "-1", "0", "1000001"):
        assert _accepts(
            grammar,
            _tools_section(_call("submit", 1, _argument("value", "number", value))),
        )
    # 不完整 "-"、越界负数、非整数都应被拒绝
    for value in ("-", "-1000001", "1.5"):
        assert not _accepts(
            grammar,
            _tools_section(_call("submit", 1, _argument("value", "number", value))),
        )

评论区精华

没有提炼出高价值讨论线程

当前评论区没有形成足够清晰的争议点或结论,后续有更多讨论时会体现在这里。

风险与影响

  1. 范围限制:重写仅作用于 integer 类型且无 upper bound/multipleOf 的 schema,语义等价性有测试覆盖;但 number 类型仍可能受 xgrammar 缺陷影响,需后续跟进。
  2. anyOf 分支:schema 拆分后通过 JSONSchemaFormat 传给 xgrammar,需确认 anyOf 在 qwen_xml style 下编译行为一致(测试已覆盖 integer 场景)。
  3. exclusiveMinimum 处理:exclusive_minimum 转 minimum+1 是严格下界语义,若 xgrammar 对 exclusiveMinimum 支持不足,该转换可能影响边界值,应关注边界 -1/0 的行为。
  4. 版本依赖:测试跑在锁定的 xgrammar 0.2.1 上,依赖升级后重写逻辑可能变成死代码,README 或代码注释应提醒清理。

影响范围:Kimi-K3 工具调用语法编译路径,只在 strict schema 且使用负整数下限时生效;不影响其他模型与常规工具调用。影响程度:修复了受约束解码下的结构性非法工具调用,属于精度/可用性改进;同时保留 schema 语义,不引入 breaking change。对团队:新增了针对 xgrammar 转换缺陷的 workaround 模式,后续 pin 升级后需跟进清理。

第三方依赖缺陷 workaround 依赖升级后需清理

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论