From 7e6e5efe51e2f6e478c505858050b15e44557c9b Mon Sep 17 00:00:00 2001 From: Liangsheng Yin Date: Tue, 26 May 2026 00:49:41 -0700 Subject: [PATCH] Revert "fix(tool_call): normalize non-standard JSON Schema types in tool params" (#26379) --- .../srt/entrypoints/openai/serving_chat.py | 15 +- python/sglang/srt/function_call/utils.py | 149 ------- .../test_normalize_json_schema_types.py | 391 ------------------ 3 files changed, 1 insertion(+), 554 deletions(-) delete mode 100644 test/registered/unit/function_call/test_normalize_json_schema_types.py diff --git a/python/sglang/srt/entrypoints/openai/serving_chat.py b/python/sglang/srt/entrypoints/openai/serving_chat.py index d55c3a6ad..5b31de242 100644 --- a/python/sglang/srt/entrypoints/openai/serving_chat.py +++ b/python/sglang/srt/entrypoints/openai/serving_chat.py @@ -60,10 +60,7 @@ from sglang.srt.environ import envs from sglang.srt.function_call.core_types import ToolCallItem from sglang.srt.function_call.function_call_parser import FunctionCallParser from sglang.srt.function_call.json_array_parser import JsonArrayParser -from sglang.srt.function_call.utils import ( - get_json_schema_constraint, - normalize_json_schema_types, -) +from sglang.srt.function_call.utils import get_json_schema_constraint from sglang.srt.managers.io_struct import GenerateReqInput from sglang.srt.parser.conversation import generate_chat_conv from sglang.srt.parser.jinja_template_utils import process_content_for_template_format @@ -419,19 +416,9 @@ class OpenAIServingChat(OpenAIServingBase): if tool.function.parameters is None: continue try: - # Rewrite DB/ORM-style aliases (e.g. "varchar", "enum", "int") - # to standard JSON Schema types before validation. RecursionError - # guards against hand-crafted cyclic schemas so the request gets - # a 400 instead of crashing into a 500. - normalize_json_schema_types(tool.function.parameters) Draft202012Validator.check_schema(tool.function.parameters) except SchemaError as e: return f"Tool {i} function has invalid 'parameters' schema: {str(e)}" - except RecursionError: - return ( - f"Tool {i} function 'parameters' schema is too deeply nested " - "or contains a cycle." - ) max_output_tokens = request.max_completion_tokens or request.max_tokens server_context_length = self.tokenizer_manager.server_args.context_length diff --git a/python/sglang/srt/function_call/utils.py b/python/sglang/srt/function_call/utils.py index 9f5078b75..1ef93e051 100644 --- a/python/sglang/srt/function_call/utils.py +++ b/python/sglang/srt/function_call/utils.py @@ -8,155 +8,6 @@ from partial_json_parser.core.options import Allow from sglang.srt.entrypoints.openai.protocol import Tool, ToolChoice -_STANDARD_JSON_SCHEMA_TYPES = { - "null", - "boolean", - "object", - "array", - "number", - "string", - "integer", -} - -# Non-standard ``type`` values commonly emitted by DB/ORM-driven tool-schema -# generators. Mapped to the closest JSON Schema 2020-12 primitive so that -# ``Draft202012Validator.check_schema`` does not reject an otherwise-usable -# tool definition. -_JSON_SCHEMA_TYPE_ALIASES: Dict[str, str] = { - "str": "string", - "text": "string", - "varchar": "string", - "char": "string", - "enum": "string", - "uuid": "string", - "date": "string", - "datetime": "string", - "time": "string", - "timestamp": "string", - "bool": "boolean", - "binary": "boolean", - "bigint": "integer", - "smallint": "integer", - "tinyint": "integer", - "double": "number", - "decimal": "number", - "real": "number", - "numeric": "number", - "arr": "array", - "tuple": "array", - "set": "array", - "map": "object", -} - -# Prefix-based matching so that parameterised names like ``int32`` / -# ``float64`` / ``list[str]`` / ``dict[str, int]`` resolve. A prefix only -# matches when it spans the entire token or is followed by a non-identifier -# char, so "int" does not swallow "internal" and "list" does not swallow -# "list_price". -_PREFIX_BOUNDARY_CHARS = frozenset("0123456789[<( \t") -_PREFIX_RULES: Tuple[Tuple[Tuple[str, ...], str], ...] = ( - (("int", "uint", "long", "short", "unsigned"), "integer"), - (("num", "float"), "number"), - (("list",), "array"), - (("dict",), "object"), -) - - -def _matches_type_prefix(base: str, prefixes: Tuple[str, ...]) -> bool: - for p in prefixes: - if base == p: - return True - if ( - len(base) > len(p) - and base.startswith(p) - and base[len(p)] in _PREFIX_BOUNDARY_CHARS - ): - return True - return False - - -def _normalize_single_type(raw: Any) -> Any: - if not isinstance(raw, str): - return raw - if raw in _STANDARD_JSON_SCHEMA_TYPES: - return raw - # ``split("(", 1)[0]`` strips parenthesized params like ``varchar(255)`` - # or ``decimal(10,2)`` without the overhead of a regex per call. - base = raw.split("(", 1)[0].strip().lower() - if base in _STANDARD_JSON_SCHEMA_TYPES: - return base - mapped = _JSON_SCHEMA_TYPE_ALIASES.get(base) - if mapped is not None: - return mapped - for prefixes, target in _PREFIX_RULES: - if _matches_type_prefix(base, prefixes): - return target - return raw - - -def normalize_json_schema_types(schema: Any) -> None: - """ - Walk a JSON Schema in place and rewrite non-standard ``"type"`` values - (e.g. ``"varchar"``, ``"enum"``, ``"int"``) to their standard JSON Schema - equivalents. - - Acts as a compatibility layer for tool ``parameters`` schemas exported - from database / ORM tooling, which often uses DB type names rather than - JSON Schema types. Unknown types are left untouched so that downstream - validation can still surface genuine errors. - - Mutates the input dict in place; the rewritten schema is also what gets - rendered into the model prompt, so e.g. a user-supplied ``"varchar"`` - reaches the model as ``"string"``. ``$ref`` values are not resolved — - callers pass tree-shaped schemas (HTTP JSON input is always a tree). - """ - if isinstance(schema, list): - for item in schema: - normalize_json_schema_types(item) - return - if not isinstance(schema, dict): - return - - if "type" in schema: - t = schema["type"] - if isinstance(t, str): - schema["type"] = _normalize_single_type(t) - elif isinstance(t, list): - schema["type"] = [_normalize_single_type(item) for item in t] - - for key in ( - "properties", - "patternProperties", - "$defs", - "definitions", - "dependentSchemas", - ): - nested = schema.get(key) - if isinstance(nested, dict): - for v in nested.values(): - normalize_json_schema_types(v) - - for key in ("anyOf", "oneOf", "allOf", "prefixItems"): - nested = schema.get(key) - if isinstance(nested, list): - for v in nested: - normalize_json_schema_types(v) - - for key in ( - "items", - "additionalProperties", - "not", - "if", - "then", - "else", - "contains", - "propertyNames", - "unevaluatedItems", - "unevaluatedProperties", - ): - if key in schema: - normalize_json_schema_types(schema[key]) - def _find_common_prefix(s1: str, s2: str) -> str: prefix = "" diff --git a/test/registered/unit/function_call/test_normalize_json_schema_types.py b/test/registered/unit/function_call/test_normalize_json_schema_types.py deleted file mode 100644 index 5402e24eb..000000000 --- a/test/registered/unit/function_call/test_normalize_json_schema_types.py +++ /dev/null @@ -1,391 +0,0 @@ -"""Unit tests for tool-parameter schema alias normalization.""" - -import json -import unittest - -from jsonschema import Draft202012Validator, SchemaError - -from sglang.srt.function_call.utils import normalize_json_schema_types -from sglang.test.ci.ci_register import register_cpu_ci -from sglang.test.test_utils import CustomTestCase - -register_cpu_ci(1.0, "stage-a-test-cpu") - - -class TestNormalizeJsonSchemaTypes(CustomTestCase): - def _assert_accepts(self, schema: dict) -> None: - Draft202012Validator.check_schema(schema) - - def test_enum_alias_becomes_string(self): - schema = { - "type": "object", - "properties": {"color": {"type": "enum", "enum": ["red", "green", "blue"]}}, - } - normalize_json_schema_types(schema) - self.assertEqual(schema["properties"]["color"]["type"], "string") - self.assertEqual( - schema["properties"]["color"]["enum"], ["red", "green", "blue"] - ) - self._assert_accepts(schema) - - def test_varchar_alias_becomes_string(self): - schema = { - "type": "object", - "properties": { - "name": {"type": "varchar"}, - "short_name": {"type": "VARCHAR(255)"}, - }, - } - normalize_json_schema_types(schema) - self.assertEqual(schema["properties"]["name"]["type"], "string") - self.assertEqual(schema["properties"]["short_name"]["type"], "string") - self._assert_accepts(schema) - - def test_numeric_aliases(self): - schema = { - "type": "object", - "properties": { - "age": {"type": "int"}, - "big": {"type": "bigint"}, - "price": {"type": "decimal(10,2)"}, - "ratio": {"type": "float"}, - }, - } - normalize_json_schema_types(schema) - props = schema["properties"] - self.assertEqual(props["age"]["type"], "integer") - self.assertEqual(props["big"]["type"], "integer") - self.assertEqual(props["price"]["type"], "number") - self.assertEqual(props["ratio"]["type"], "number") - self._assert_accepts(schema) - - def test_prefix_matched_numeric_types(self): - schema = { - "type": "object", - "properties": { - "a": {"type": "int32"}, - "b": {"type": "int64"}, - "c": {"type": "uint"}, - "d": {"type": "unsigned"}, - "e": {"type": "long"}, - "f": {"type": "short"}, - "g": {"type": "float32"}, - "h": {"type": "float64"}, - "i": {"type": "num"}, - "j": {"type": "numeric"}, - }, - } - normalize_json_schema_types(schema) - p = schema["properties"] - for k in ("a", "b", "c", "d", "e", "f"): - self.assertEqual(p[k]["type"], "integer") - for k in ("g", "h", "i", "j"): - self.assertEqual(p[k]["type"], "number") - self._assert_accepts(schema) - - def test_prefix_matched_compound_types(self): - schema = { - "type": "object", - "properties": { - "a": {"type": "list[str]"}, - "b": {"type": "list"}, - "c": {"type": "dict"}, - "d": {"type": "dict[str, int]"}, - "e": {"type": "long long"}, - }, - } - normalize_json_schema_types(schema) - p = schema["properties"] - self.assertEqual(p["a"]["type"], "array") - self.assertEqual(p["b"]["type"], "array") - self.assertEqual(p["c"]["type"], "object") - self.assertEqual(p["d"]["type"], "object") - self.assertEqual(p["e"]["type"], "integer") - self._assert_accepts(schema) - - def test_word_boundary_prevents_false_positives(self): - """Prefixes must end at a non-identifier char, so custom type names - that merely start with a known prefix are left alone.""" - schema = { - "type": "object", - "properties": { - "a": {"type": "internal"}, - "b": {"type": "list_price"}, - "c": {"type": "integer_enum"}, - "d": {"type": "dictionary_entry"}, - "e": {"type": "floating"}, - }, - } - normalize_json_schema_types(schema) - p = schema["properties"] - for key, expected in [ - ("a", "internal"), - ("b", "list_price"), - ("c", "integer_enum"), - ("d", "dictionary_entry"), - ("e", "floating"), - ]: - self.assertEqual(p[key]["type"], expected) - - def test_recurses_into_draft_2020_12_keywords(self): - schema = { - "type": "object", - "properties": { - "row": { - "dependentSchemas": { - "kind": { - "properties": {"sku": {"type": "varchar"}}, - }, - }, - "propertyNames": {"type": "str"}, - "unevaluatedProperties": {"type": "bigint"}, - }, - "rows": { - "type": "arr", - "unevaluatedItems": {"type": "int32"}, - }, - }, - } - normalize_json_schema_types(schema) - row = schema["properties"]["row"] - rows = schema["properties"]["rows"] - self.assertEqual( - row["dependentSchemas"]["kind"]["properties"]["sku"]["type"], "string" - ) - self.assertEqual(row["propertyNames"]["type"], "string") - self.assertEqual(row["unevaluatedProperties"]["type"], "integer") - self.assertEqual(rows["type"], "array") - self.assertEqual(rows["unevaluatedItems"]["type"], "integer") - self._assert_accepts(schema) - - def test_binary_and_arr_aliases(self): - schema = { - "type": "object", - "properties": { - "flag": {"type": "binary"}, - "items": {"type": "arr"}, - }, - } - normalize_json_schema_types(schema) - self.assertEqual(schema["properties"]["flag"]["type"], "boolean") - self.assertEqual(schema["properties"]["items"]["type"], "array") - self._assert_accepts(schema) - - def test_case_insensitive(self): - schema = { - "type": "object", - "properties": { - "a": {"type": "VARCHAR"}, - "b": {"type": "INT"}, - "c": {"type": "String"}, - }, - } - normalize_json_schema_types(schema) - p = schema["properties"] - self.assertEqual(p["a"]["type"], "string") - self.assertEqual(p["b"]["type"], "integer") - self.assertEqual(p["c"]["type"], "string") - self._assert_accepts(schema) - - def test_array_and_object_aliases(self): - schema = { - "type": "object", - "properties": { - "tags": {"type": "list", "items": {"type": "str"}}, - "meta": {"type": "dict"}, - }, - } - normalize_json_schema_types(schema) - self.assertEqual(schema["properties"]["tags"]["type"], "array") - self.assertEqual(schema["properties"]["tags"]["items"]["type"], "string") - self.assertEqual(schema["properties"]["meta"]["type"], "object") - self._assert_accepts(schema) - - def test_nested_anyof_and_defs(self): - schema = { - "type": "object", - "properties": { - "value": { - "anyOf": [ - {"type": "int"}, - {"type": "varchar"}, - ] - } - }, - "$defs": { - "Row": {"type": "object", "properties": {"id": {"type": "bigint"}}} - }, - } - normalize_json_schema_types(schema) - any_of = schema["properties"]["value"]["anyOf"] - self.assertEqual(any_of[0]["type"], "integer") - self.assertEqual(any_of[1]["type"], "string") - self.assertEqual(schema["$defs"]["Row"]["properties"]["id"]["type"], "integer") - self._assert_accepts(schema) - - def test_type_list_member_normalized(self): - schema = {"type": ["varchar", "null"]} - normalize_json_schema_types(schema) - self.assertEqual(schema["type"], ["string", "null"]) - self._assert_accepts(schema) - - def test_standard_types_untouched(self): - schema = { - "type": "object", - "properties": { - "a": {"type": "string"}, - "b": {"type": "integer"}, - "c": {"type": "boolean"}, - }, - } - normalize_json_schema_types(schema) - self.assertEqual(schema["properties"]["a"]["type"], "string") - self.assertEqual(schema["properties"]["b"]["type"], "integer") - self.assertEqual(schema["properties"]["c"]["type"], "boolean") - - def test_unknown_type_left_alone(self): - schema = {"type": "object", "properties": {"x": {"type": "geometry"}}} - normalize_json_schema_types(schema) - self.assertEqual(schema["properties"]["x"]["type"], "geometry") - with self.assertRaises(SchemaError): - self._assert_accepts(schema) - - def test_common_db_orm_type_names_accepted(self): - """Common non-standard DB/ORM type names all survive validation.""" - recognized = [ - # string family - "string", - "str", - "text", - "varchar", - "char", - "enum", - # integer via prefix - "int", - "int32", - "int64", - "uint", - "uint8", - "long", - "long long", - "short", - "unsigned", - # number via prefix - "num", - "numeric", - "float", - "float32", - "float64", - # boolean - "boolean", - "bool", - "binary", - # compound - "object", - "array", - "arr", - "dict", - "dict[str, int]", - "list", - "list[str]", - ] - for t in recognized: - schema = {"type": "object", "properties": {"x": {"type": t}}} - normalize_json_schema_types(schema) - try: - self._assert_accepts(schema) - except SchemaError as e: - self.fail(f"type {t!r} → {schema['properties']['x']['type']!r}: {e}") - - def test_pre_existing_400_schema_now_accepted(self): - schema = { - "type": "object", - "properties": { - "sql": {"type": "varchar"}, - "mode": {"type": "enum", "enum": ["read", "write"]}, - }, - "required": ["sql", "mode"], - } - normalize_json_schema_types(schema) - self._assert_accepts(schema) - - def test_idempotent(self): - """Running normalize twice produces the same result as running it once.""" - schema = { - "type": "object", - "properties": { - "a": {"type": "varchar"}, - "b": {"type": "int32"}, - "c": {"type": ["bigint", "null"]}, - "d": { - "anyOf": [{"type": "enum"}, {"type": "decimal(10,2)"}], - }, - }, - } - normalize_json_schema_types(schema) - once = json.loads(json.dumps(schema)) - normalize_json_schema_types(schema) - self.assertEqual(schema, once) - - def test_non_string_type_values_pass_through(self): - """``type`` that isn't str/list is left for the real validator to reject.""" - for bad in (None, 42, {"$ref": "#/$defs/Foo"}, ["string", 1, None]): - schema = {"properties": {"x": {"type": bad}}} - normalize_json_schema_types(schema) - self.assertEqual(schema["properties"]["x"]["type"], bad) - - def test_recurses_into_all_walked_keywords(self): - """Every keyword the walker recurses into must actually rewrite nested aliases.""" - schema = { - "patternProperties": {"^x_": {"type": "varchar"}}, - "definitions": {"Row": {"type": "bigint"}}, - "prefixItems": [{"type": "int32"}, {"type": "float64"}], - "if": {"type": "bool"}, - "then": {"type": "str"}, - "else": {"type": "decimal"}, - "not": {"type": "uuid"}, - "contains": {"type": "enum"}, - "additionalProperties": {"type": "binary"}, - } - normalize_json_schema_types(schema) - self.assertEqual(schema["patternProperties"]["^x_"]["type"], "string") - self.assertEqual(schema["definitions"]["Row"]["type"], "integer") - self.assertEqual(schema["prefixItems"][0]["type"], "integer") - self.assertEqual(schema["prefixItems"][1]["type"], "number") - self.assertEqual(schema["if"]["type"], "boolean") - self.assertEqual(schema["then"]["type"], "string") - self.assertEqual(schema["else"]["type"], "number") - self.assertEqual(schema["not"]["type"], "string") - self.assertEqual(schema["contains"]["type"], "string") - self.assertEqual(schema["additionalProperties"]["type"], "boolean") - - def test_cyclic_schema_raises_recursion_error(self): - """A pathological cyclic schema surfaces as RecursionError — caller - (``_validate_request``) converts it to a 400, not a 500.""" - schema = {"type": "object"} - schema["items"] = schema - with self.assertRaises(RecursionError): - normalize_json_schema_types(schema) - - def test_boolean_subschema_does_not_crash(self): - """``additionalProperties: True`` and ``items: false`` are valid 2020-12 - forms; the walker must pass through without raising.""" - schema = { - "type": "object", - "properties": { - "a": {"type": "varchar"}, - "b": {"type": "array", "items": False}, - }, - "additionalProperties": True, - "unevaluatedProperties": False, - } - normalize_json_schema_types(schema) - self.assertEqual(schema["properties"]["a"]["type"], "string") - self.assertIs(schema["properties"]["b"]["items"], False) - self.assertIs(schema["additionalProperties"], True) - self.assertIs(schema["unevaluatedProperties"], False) - self._assert_accepts(schema) - - -if __name__ == "__main__": - unittest.main()