[Fix] Work around xgrammar 0.2.1 negative integer minimum in Kimi-K3 structural tags (#34778)
Co-authored-by: James Liu <jamesl@modal.com>
This commit is contained in:
@@ -274,6 +274,33 @@ def _value_format(
|
|||||||
) -> Format:
|
) -> Format:
|
||||||
if loose_string and json_type == "string":
|
if loose_string and json_type == "string":
|
||||||
return AnyTextFormat()
|
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(
|
return JSONSchemaFormat(
|
||||||
json_schema=schema,
|
json_schema=schema,
|
||||||
style="qwen_xml" if json_type == "string" else "json",
|
style="qwen_xml" if json_type == "string" else "json",
|
||||||
|
|||||||
@@ -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():
|
def test_strict_schema_preserves_additional_properties_default():
|
||||||
tool = Tool(
|
tool = Tool(
|
||||||
type="function",
|
type="function",
|
||||||
|
|||||||
Reference in New Issue
Block a user