fix(tool_call): reland schema type normalization (#26433)

This commit is contained in:
Xinyuan Tong
2026-05-28 14:31:18 +08:00
committed by GitHub
parent 6f85957ff2
commit bed20249f1
3 changed files with 585 additions and 1 deletions
@@ -60,7 +60,10 @@ 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
from sglang.srt.function_call.utils import (
get_json_schema_constraint,
normalize_json_schema_types,
)
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
@@ -416,9 +419,19 @@ 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
+162
View File
@@ -8,6 +8,168 @@ 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",
"binary": "string",
"blob": "string",
"bytea": "string",
"bytes": "string",
"varbinary": "string",
"bool": "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_type_list(raw_items: List[Any]) -> List[Any]:
normalized_items: List[Any] = []
for item in raw_items:
normalized_item = _normalize_single_type(item)
if normalized_item not in normalized_items:
normalized_items.append(normalized_item)
return normalized_items
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_type_list(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 = ""