diff --git a/python/sglang/srt/function_call/kimik3_structural_tag.py b/python/sglang/srt/function_call/kimik3_structural_tag.py index 2f10e6e48..04f29a1df 100644 --- a/python/sglang/srt/function_call/kimik3_structural_tag.py +++ b/python/sglang/srt/function_call/kimik3_structural_tag.py @@ -274,6 +274,33 @@ def _value_format( ) -> Format: if loose_string and json_type == "string": return AnyTextFormat() + # XGrammar 0.2.1 miscompiles a one-sided negative integer lower bound: + # {"type": "integer", "minimum": -N} accepts the incomplete value "-" + # and rejects every valid negative integer. Splitting the range at zero + # avoids that converter bug without weakening the schema. + 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", diff --git a/test/registered/unit/function_call/test_kimik3_structural_tag.py b/test/registered/unit/function_call/test_kimik3_structural_tag.py index 9e68edd74..8b96ecdb5 100644 --- a/test/registered/unit/function_call/test_kimik3_structural_tag.py +++ b/test/registered/unit/function_call/test_kimik3_structural_tag.py @@ -379,6 +379,39 @@ def test_strict_schema_handles_number_enums_and_all_of_integer_constraints(): ) +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))), + ) + + def test_strict_schema_preserves_additional_properties_default(): tool = Tool( type="function",