diff --git a/python/sglang/srt/function_call/base_format_detector.py b/python/sglang/srt/function_call/base_format_detector.py
index 5d55caf95..13a4f2df9 100644
--- a/python/sglang/srt/function_call/base_format_detector.py
+++ b/python/sglang/srt/function_call/base_format_detector.py
@@ -337,30 +337,3 @@ class BaseFormatDetector(ABC):
A function that takes a tool name (str) and returns StructureInfo
"""
raise NotImplementedError()
-
- @abstractmethod
- def build_ebnf(self, tools: List[Tool]) -> str:
- """
- Build an EBNF grammar for constrained generation of function calls.
-
- This method generates an Extended Backus-Naur Form (EBNF) grammar that
- constrains the model's output to valid function calls in this format.
- The grammar should include all available tools and their parameter schemas.
-
- Args:
- tools: List of available tools/functions that can be called
-
- Returns:
- A string containing the EBNF grammar for this function call format
-
- The EBNF grammar should:
- - Define the overall structure of function calls in this format
- - Include all tool names from the provided tools list
- - Define valid JSON structures for function arguments
- - Handle multiple function calls if the format supports them
-
- Note:
- Most implementations use EBNFComposer.build_ebnf() utility with
- format-specific parameters rather than writing EBNF from scratch.
- """
- raise NotImplementedError()
diff --git a/python/sglang/srt/function_call/deepseekv31_detector.py b/python/sglang/srt/function_call/deepseekv31_detector.py
index 2045d8daa..cab924544 100644
--- a/python/sglang/srt/function_call/deepseekv31_detector.py
+++ b/python/sglang/srt/function_call/deepseekv31_detector.py
@@ -11,7 +11,6 @@ from sglang.srt.function_call.core_types import (
ToolCallItem,
_GetInfoFunc,
)
-from sglang.srt.function_call.ebnf_composer import EBNFComposer
from sglang.srt.function_call.utils import _is_complete_json
logger = logging.getLogger(__name__)
@@ -210,13 +209,3 @@ class DeepSeekV31Detector(BaseFormatDetector):
end="<|tool▁call▁end|>",
trigger="<|tool▁call▁begin|>" + name + "<|tool▁sep|>",
)
-
- def build_ebnf(self, tools: List[Tool]):
- return EBNFComposer.build_ebnf(
- tools,
- sequence_start_token=self.bot_token,
- sequence_end_token=self.eot_token,
- tool_call_separator="",
- call_rule_fmt='"<|tool▁call▁begin|>{name}<|tool▁sep|>{arguments_rule}<|tool▁call▁end|>"',
- function_format="json",
- )
diff --git a/python/sglang/srt/function_call/deepseekv3_detector.py b/python/sglang/srt/function_call/deepseekv3_detector.py
index 33c4dfc44..8dcc2da43 100644
--- a/python/sglang/srt/function_call/deepseekv3_detector.py
+++ b/python/sglang/srt/function_call/deepseekv3_detector.py
@@ -11,7 +11,6 @@ from sglang.srt.function_call.core_types import (
ToolCallItem,
_GetInfoFunc,
)
-from sglang.srt.function_call.ebnf_composer import EBNFComposer
from sglang.srt.function_call.utils import _is_complete_json
logger = logging.getLogger(__name__)
@@ -208,13 +207,3 @@ class DeepSeekV3Detector(BaseFormatDetector):
end="\n```<",
trigger=">" + name + "\n```json\n",
)
-
- def build_ebnf(self, tools: List[Tool]):
- return EBNFComposer.build_ebnf(
- tools,
- sequence_start_token=self.bot_token,
- sequence_end_token=self.eot_token,
- tool_call_separator="",
- call_rule_fmt='"<|tool▁call▁begin|>function<|tool▁sep|>{name}\\n```json\\n"{arguments_rule}"\\n```<|tool▁call▁end|>"',
- function_format="json",
- )
diff --git a/python/sglang/srt/function_call/ebnf_composer.py b/python/sglang/srt/function_call/ebnf_composer.py
deleted file mode 100644
index 21b313982..000000000
--- a/python/sglang/srt/function_call/ebnf_composer.py
+++ /dev/null
@@ -1,344 +0,0 @@
-from typing import Any, Dict, Literal, Optional
-
-
-class EBNFComposer:
- # Adapted from https://xgrammar.mlc.ai/docs/how_to/ebnf_guided_generation.html#try-out-via-hf-transformers
- # Shared primitive grammar rules used across all formats
- BASE_PRIMITIVE_GRAMMAR = r"""
- basic_string ::= (([\"] basic_string_1 [\"]))
- basic_string_1 ::= "" | [^"\\\x00-\x1F] basic_string_1 | "\\" escape basic_string_1
- escape ::= ["\\/bfnrt] | "u" [A-Fa-f0-9]{4}
- basic_integer ::= ("0" | "-"? [1-9] [0-9]*) ".0"?
- basic_number ::= ("0" | "-"? [1-9] [0-9]*) ("." [0-9]+)? ([eE] [+-]? [0-9]+)?
- basic_array ::= "[" ("" | ws basic_any (ws "," ws basic_any)*) ws "]"
- basic_object ::= "{" ("" | ws basic_string ws ":" ws basic_any ( ws "," ws basic_string ws ":" ws basic_any)*) ws "}"
- ws ::= [ \n\t]*
- """
-
- # Format-specific extensions
- json_grammar_ebnf_str = (
- r"""
- json ::= basic_array | basic_object
- basic_any ::= basic_number | basic_string | basic_boolean | basic_null | basic_array | basic_object
- basic_boolean ::= "true" | "false"
- basic_null ::= "null"
- """
- + BASE_PRIMITIVE_GRAMMAR
- )
-
- pythonic_grammar_ebnf_str = (
- r"""
- pythonic ::= basic_number | basic_string | basic_array | "True" | "False" | "None"
- basic_any ::= basic_number | basic_string | basic_array | basic_object
- basic_boolean ::= "True" | "False"
- basic_null ::= "None"
- """
- + BASE_PRIMITIVE_GRAMMAR
- )
-
- xml_grammar_ebnf_str = (
- r"""
- xml ::= xml_element | xml_text
- xml_element ::= basic_string | basic_number | basic_boolean | basic_null | basic_array | basic_object
- xml_text ::= [^<>]*
- basic_any ::= basic_number | basic_string | basic_boolean | basic_null | basic_array | basic_object
- basic_boolean ::= "true" | "false"
- basic_null ::= "null"
- """
- + BASE_PRIMITIVE_GRAMMAR
- )
-
- CALL_RULE_MAP = {
- "pythonic": 'call_{name} ::= "{name}" "(" {arguments_rule} ")"',
- "json": 'call_{name} ::= "{{" ws "\\"name\\"" ws ":" ws "\\"{name}\\"" ws "," ws "\\"arguments\\"" ws ":" ws {arguments_rule} ws "}}"',
- "xml": 'call_{name} ::= "\\n" {arguments_rule} "\\n"',
- }
-
- ARGUMENTS_RULE_MAP = {
- "pythonic": "{arg_rules}",
- "json": '"{{" ws {arg_rules} ws "}}"',
- "xml": "{arg_rules}",
- }
-
- KEY_VALUE_RULE_MAP = {
- "pythonic": '"{key}" "=" {valrule}',
- "json": '"\\"{key}\\"" ws ":" ws {valrule}',
- "xml": '"\\n" {valrule} "\\n"',
- }
-
- # Base type mapping - most types are the same across formats
- BASE_TYPE_MAPPING = {
- "string": "basic_string",
- "number": "basic_number",
- "integer": "basic_number",
- "boolean": "basic_boolean",
- "null": "basic_null",
- "array": "basic_array",
- "object": "basic_object",
- }
-
- # Format-specific overrides for types that differ
- FORMAT_TYPE_OVERRIDES = {
- "pythonic": {
- "boolean": '"True" | "False"',
- "null": '"None"',
- },
- "xml": {
- "string": "xml_text",
- },
- }
-
- @staticmethod
- def get_value_rule(
- prop: dict, function_format: Literal["pythonic", "json", "xml"] = "json"
- ) -> str:
- if "enum" in prop:
- return EBNFComposer._handle_enum(prop, function_format)
-
- if "type" in prop:
- return EBNFComposer._handle_type(prop, function_format)
-
- return function_format
-
- @staticmethod
- def _handle_enum(prop: dict, function_format: str) -> str:
- """Handle enum properties by formatting each value according to type and format."""
- enum_values = prop["enum"]
- prop_type = prop.get("type", "string")
-
- def format_enum_val(v: Any) -> str:
- if prop_type == "boolean":
- if function_format == "json" or function_format == "xml":
- return "true" if v else "false"
- elif function_format == "pythonic":
- return "True" if v else "False"
- else:
- return str(v) # fallback
-
- if prop_type == "string":
- if function_format == "xml":
- return f'"{v}"'
- else: # json or pythonic
- return f'"\\"{v}\\""' # escape quote-wrapped string
-
- # All other types (number, integer, etc.)
- return str(v)
-
- formatted_values = [format_enum_val(v) for v in enum_values]
- enum_rule = " | ".join(formatted_values)
- return f"({enum_rule})" if len(formatted_values) > 1 else enum_rule
-
- @staticmethod
- def get_type_mapping(function_format: str) -> Dict[str, str]:
- """Get the complete type mapping for a given format."""
- mapping = EBNFComposer.BASE_TYPE_MAPPING.copy()
- overrides = EBNFComposer.FORMAT_TYPE_OVERRIDES.get(function_format, {})
- mapping.update({k: v for k, v in overrides.items() if v is not None})
- return mapping
-
- @staticmethod
- def _handle_type(prop: dict, function_format: str) -> str:
- """Handle type properties using the appropriate type mapping."""
- prop_type = prop["type"]
- type_mapping = EBNFComposer.get_type_mapping(function_format)
-
- if isinstance(prop_type, list):
- type_rules = [
- type_mapping.get(single_type, function_format)
- for single_type in prop_type
- ]
- return " | ".join(type_rules) if type_rules else function_format
-
- return type_mapping.get(prop_type, function_format)
-
- @staticmethod
- def build_ebnf(
- tools,
- function_format: Literal["pythonic", "json", "xml"] = "json",
- # Parameters for wrapping the entire sequence of tool calls
- sequence_start_token: Optional[str] = None,
- sequence_end_token: Optional[str] = None,
- # Parameters for wrapping individual tool calls
- individual_call_start_token: Optional[str] = None,
- individual_call_end_token: Optional[str] = None,
- # Parameter for separating multiple tool calls
- tool_call_separator: Optional[str] = None,
- call_rule_fmt: Optional[str] = None,
- key_value_rule_fmt: Optional[str] = None,
- key_value_separator: str = 'ws "," ws',
- ):
- """
- Generalized EBNF builder for all detectors.
- Args:
- tools: List of Tool objects to generate EBNF grammar for
- function_format: The format of function calls, either "pythonic" or "json"
- sequence_start_token: Token that wraps the entire sequence of tool calls (start)
- sequence_end_token: Token that wraps the entire sequence of tool calls (end)
- individual_call_start_token: Token that wraps each individual tool call (start)
- individual_call_end_token: Token that wraps each individual tool call (end)
- tool_call_separator: The separator between multiple tool calls
- call_rule_fmt: Optional custom format string for call_{name} rule. It should define each function call's format, with
- the placeholders {name} for the function name and {arguments_rule} for the arguments rule. If None, a default
- format based on function_format will be used.
- key_value_rule_fmt: Optional custom format string for key-value pairs. It should define how each parameter is formatted,
- with placeholders {key} for the parameter name and {valrule} for the value rule. If None, a default format
- based on function_format will be used.
- key_value_separator: Raw EBNF fragment inserted between key-value pairs.
- This string is used verbatim (not auto-quoted). Pass:
- - Quoted terminals when you need a literal token (e.g. '","' or '"\\n"').
- - Raw/non-terminals when you need grammar tokens (e.g. 'ws "," ws').
- """
- # =================================================================
- # Step 1: Determine the root tool calls rule
- # =================================================================
- # Handle a single function call
- if individual_call_start_token and individual_call_end_token:
- function_call_unit = f'"{individual_call_start_token}" function_call "{individual_call_end_token}"'
- else:
- function_call_unit = "function_call"
-
- # Handle multiple function calls with separators
- if tool_call_separator is not None:
- base_pattern = f'{function_call_unit} ( "{tool_call_separator}" {function_call_unit} )*'
- else:
- # Assume only support single function call
- base_pattern = function_call_unit
-
- # Apply sequence-level wrapping if needed
- if sequence_start_token and sequence_end_token:
- root_rule = (
- f'"{sequence_start_token}" {base_pattern} "{sequence_end_token}"'
- )
- else:
- root_rule = base_pattern
-
- # =================================================================
- # Step 2: Build the header rules
- # =================================================================
- ebnf_lines = [
- f"root ::= {root_rule}",
- "function_call ::= "
- + " | ".join([f"call_{tool.function.name}" for tool in tools]),
- ]
-
- # =================================================================
- # Step 3: Set up formatting templates
- # =================================================================
- call_template = (
- f"call_{{name}} ::= {call_rule_fmt}"
- if call_rule_fmt
- else EBNFComposer.CALL_RULE_MAP[function_format]
- )
- args_template = EBNFComposer.ARGUMENTS_RULE_MAP[function_format]
- key_value_template = (
- key_value_rule_fmt
- if key_value_rule_fmt
- else EBNFComposer.KEY_VALUE_RULE_MAP[function_format]
- )
-
- # =================================================================
- # Step 4: Build rules for each tool
- # =================================================================
- for tool in tools:
- tool_name = tool.function.name
- params = tool.function.parameters or {}
- properties = params.get("properties", {})
- required_props = set(params.get("required", []))
-
- # The generated pattern ensures:
- # 1. Required properties appear first, joined by commas
- # 2. Optional properties are wrapped with comma included: ( "," ( "prop" : value )? )?
- # 3. For multiple optional properties, we allow flexible ordering:
- # - Each optional can be skipped entirely
- # - They can appear in any combination
- #
- # Example patterns generated:
- # - One required, one optional:
- # "{" "location" ":" string ( "," ( "unit" ":" enum ) )? "}"
- # Allows: {"location": "Paris"} or {"location": "Paris", "unit": "celsius"}
- #
- # - Multiple optional properties with flexible ordering:
- # "{" "req" ":" string ( "," ( "opt1" ":" value ( "," "opt2" ":" value )? | "opt2" ":" value ) )? "}"
- # Allows: {"req": "x"}, {"req": "x", "opt1": "y"}, {"req": "x", "opt2": "z"},
- # {"req": "x", "opt1": "y", "opt2": "z"}
- #
- # - All optional properties with flexible ordering:
- # "{" ( "opt1" ":" value ( "," "opt2" ":" value )? | "opt2" ":" value )? "}"
- # Allows: {}, {"opt1": "x"}, {"opt2": "y"}, {"opt1": "x", "opt2": "y"}
-
- prop_kv_pairs = {}
- ordered_props = list(properties.keys())
-
- for prop_name, prop_schema in properties.items():
- value_rule = EBNFComposer.get_value_rule(prop_schema, function_format)
- # Create key=value pair
- pair = key_value_template.format(key=prop_name, valrule=value_rule)
- prop_kv_pairs[prop_name] = pair
-
- # Separate into required and optional while preserving order
- required = [p for p in ordered_props if p in required_props]
- optional = [p for p in ordered_props if p not in required_props]
-
- # Build the combined rule
- rule_parts = []
-
- # Add required properties joined by commas
- if required:
- rule_parts.append(
- f" {key_value_separator} ".join(prop_kv_pairs[k] for k in required)
- )
-
- # Add optional properties with flexible ordering
- if optional:
- # Build alternatives where any optional property can appear first
- opt_alternatives = []
- for i in range(len(optional)):
- # Build pattern for optional[i] appearing first
- opt_parts = []
- for j in range(i, len(optional)):
- if j == i:
- opt_parts.append(prop_kv_pairs[optional[j]])
- else:
- opt_parts.append(
- f" ( {key_value_separator} {prop_kv_pairs[optional[j]]} )?"
- )
- opt_alternatives.append("".join(opt_parts))
-
- # Wrap with appropriate comma handling based on whether we have required properties
- if required:
- # Required properties exist, so optional group needs outer comma
- rule_parts.append(f" ( {key_value_separator} ( ")
- rule_parts.append(" | ".join(opt_alternatives))
- rule_parts.append(" ) )?")
- else:
- # All properties are optional
- rule_parts.append("( ")
- rule_parts.append(" | ".join(opt_alternatives))
- rule_parts.append(" )?")
-
- combined_args = "".join(rule_parts)
- arguments_rule = args_template.format(arg_rules=combined_args)
- arguments_rule = arguments_rule or '""'
-
- # Add the function call rule and its arguments rule
- ebnf_lines.append(
- call_template.format(
- name=tool_name, arguments_rule=f"arguments_{tool_name}"
- )
- )
- ebnf_lines.append(f"arguments_{tool_name} ::= {arguments_rule}")
-
- # =================================================================
- # Step 5: Add base grammar rules
- # =================================================================
- grammar_dict = {
- "pythonic": EBNFComposer.pythonic_grammar_ebnf_str,
- "json": EBNFComposer.json_grammar_ebnf_str,
- "xml": EBNFComposer.xml_grammar_ebnf_str,
- }
- base_grammar = grammar_dict.get(
- function_format, EBNFComposer.json_grammar_ebnf_str
- )
- ebnf_lines.append(base_grammar)
-
- return "\n".join(ebnf_lines)
diff --git a/python/sglang/srt/function_call/function_call_parser.py b/python/sglang/srt/function_call/function_call_parser.py
index 6df0ed4d7..fd4f7ceb3 100644
--- a/python/sglang/srt/function_call/function_call_parser.py
+++ b/python/sglang/srt/function_call/function_call_parser.py
@@ -195,41 +195,3 @@ class FunctionCallParser:
elif tool_choice == "required" or isinstance(tool_choice, ToolChoice):
json_schema = get_json_schema_constraint(self.tools, tool_choice)
return ("json_schema", json_schema)
-
- def get_ebnf(
- self, tool_choice: Union[ToolChoice, Literal["required"]]
- ) -> Optional[str]:
- """
- Get the EBNF grammar for the specified tool choice.
-
- Args:
- tool_choice: The tool choice specification
-
- Returns:
- EBNF grammar string, or None if no valid tools found
-
- Note:
- If a specific function is requested but not found in available tools,
- logs a warning and falls back to using all available tools for backward compatibility.
- """
- filtered_tools = []
- if isinstance(tool_choice, ToolChoice):
- fn_name = tool_choice.function.name
- filtered_tools = [t for t in self.tools if t.function.name == fn_name]
-
- # Check if the requested function exists in available tools
- if not filtered_tools:
- available_functions = [t.function.name for t in self.tools]
- logger.warning(
- f"Function '{fn_name}' not found in available tools. "
- f"Available functions: {available_functions}. "
- f"Skipping tool choice."
- )
-
- # TODO: Return a 400 error instead of warning when adapter supports proper error handling
- # For now, fall back to return None
- return None
- else:
- filtered_tools = self.tools
-
- return self.detector.build_ebnf(filtered_tools)
diff --git a/python/sglang/srt/function_call/glm4_moe_detector.py b/python/sglang/srt/function_call/glm4_moe_detector.py
index e66e62a11..b0fc78249 100644
--- a/python/sglang/srt/function_call/glm4_moe_detector.py
+++ b/python/sglang/srt/function_call/glm4_moe_detector.py
@@ -7,7 +7,6 @@ from typing import List
from sglang.srt.entrypoints.openai.protocol import Tool
from sglang.srt.function_call.base_format_detector import BaseFormatDetector
from sglang.srt.function_call.core_types import StreamingParseResult, _GetInfoFunc
-from sglang.srt.function_call.ebnf_composer import EBNFComposer
logger = logging.getLogger(__name__)
@@ -157,15 +156,3 @@ class Glm4MoeDetector(BaseFormatDetector):
def structure_info(self) -> _GetInfoFunc:
raise NotImplementedError()
-
- def build_ebnf(self, tools: List[Tool]):
- return EBNFComposer.build_ebnf(
- tools,
- individual_call_start_token=self.bot_token,
- individual_call_end_token=self.eot_token,
- tool_call_separator="\\n",
- function_format="xml",
- call_rule_fmt='"{name}" "\\n" ( {arguments_rule} "\\n" )?',
- key_value_rule_fmt='"{key}" "\\n" "" {valrule} ""',
- key_value_separator='"\\n"',
- )
diff --git a/python/sglang/srt/function_call/gpt_oss_detector.py b/python/sglang/srt/function_call/gpt_oss_detector.py
index b012bff82..b3fd5ac61 100644
--- a/python/sglang/srt/function_call/gpt_oss_detector.py
+++ b/python/sglang/srt/function_call/gpt_oss_detector.py
@@ -239,6 +239,3 @@ class GptOssDetector(BaseFormatDetector):
def structure_info(self) -> _GetInfoFunc:
raise NotImplementedError("structure_info not used with HarmonyParser")
-
- def build_ebnf(self, tools: List[Tool]) -> str:
- raise NotImplementedError("build_ebnf not used with HarmonyParser")
diff --git a/python/sglang/srt/function_call/json_array_parser.py b/python/sglang/srt/function_call/json_array_parser.py
index 6d6bffc99..e38e3b1b0 100644
--- a/python/sglang/srt/function_call/json_array_parser.py
+++ b/python/sglang/srt/function_call/json_array_parser.py
@@ -34,16 +34,6 @@ class JsonArrayParser(BaseFormatDetector):
"Detect and parse not supported for JSON schema constraints."
)
- def build_ebnf(self, tools: List[Tool]) -> str:
- """
- Build an EBNF grammar for constrained generation.
- This is not used for JSON schema constraints as they are handled
- by the constraint backends directly.
- """
- raise NotImplementedError(
- "EBNF generation is not supported for JSON schema constraints."
- )
-
def parse_streaming_increment(
self, new_text: str, tools: List[Tool]
) -> StreamingParseResult:
diff --git a/python/sglang/srt/function_call/kimik2_detector.py b/python/sglang/srt/function_call/kimik2_detector.py
index ff29e7faa..37d039c39 100644
--- a/python/sglang/srt/function_call/kimik2_detector.py
+++ b/python/sglang/srt/function_call/kimik2_detector.py
@@ -11,7 +11,6 @@ from sglang.srt.function_call.core_types import (
ToolCallItem,
_GetInfoFunc,
)
-from sglang.srt.function_call.ebnf_composer import EBNFComposer
from sglang.srt.function_call.utils import _is_complete_json
logger = logging.getLogger(__name__)
@@ -238,21 +237,3 @@ class KimiK2Detector(BaseFormatDetector):
)
return get_info
-
- def build_ebnf(self, tools: List[Tool]) -> str:
- """
- Build EBNF grammar for KimiK2 tool call format.
-
- NOTE: The call_rule_fmt uses [0-9]+ for the function index to allow the grammar
- to accept any numeric index (0, 1, 2, etc.) for proper sequential indexing in
- multiple function call scenarios, while still maintaining the correct KimiK2
- format structure for constrained generation.
- """
- return EBNFComposer.build_ebnf(
- tools,
- sequence_start_token=self.bot_token,
- sequence_end_token=self.eot_token,
- tool_call_separator="",
- call_rule_fmt='"<|tool_call_begin|>functions.{name}:"[0-9]+"<|tool_call_argument_begin|>"{arguments_rule}"<|tool_call_end|>"',
- function_format="json",
- )
diff --git a/python/sglang/srt/function_call/llama32_detector.py b/python/sglang/srt/function_call/llama32_detector.py
index 453bcbc9a..f34a59539 100644
--- a/python/sglang/srt/function_call/llama32_detector.py
+++ b/python/sglang/srt/function_call/llama32_detector.py
@@ -9,7 +9,6 @@ from sglang.srt.function_call.core_types import (
StructureInfo,
_GetInfoFunc,
)
-from sglang.srt.function_call.ebnf_composer import EBNFComposer
logger = logging.getLogger(__name__)
@@ -87,10 +86,3 @@ class Llama32Detector(BaseFormatDetector):
end="}",
trigger="<|python_tag|>",
)
-
- def build_ebnf(self, tools: List[Tool]):
- return EBNFComposer.build_ebnf(
- tools,
- function_format="json",
- tool_call_separator=self.tool_call_separator,
- )
diff --git a/python/sglang/srt/function_call/minimax_m2.py b/python/sglang/srt/function_call/minimax_m2.py
index 8ce740bb2..fb5555631 100644
--- a/python/sglang/srt/function_call/minimax_m2.py
+++ b/python/sglang/srt/function_call/minimax_m2.py
@@ -12,7 +12,6 @@ from sglang.srt.function_call.core_types import (
ToolCallItem,
_GetInfoFunc,
)
-from sglang.srt.function_call.ebnf_composer import EBNFComposer
logger = logging.getLogger(__name__)
@@ -353,15 +352,3 @@ class MinimaxM2Detector(BaseFormatDetector):
def structure_info(self) -> _GetInfoFunc:
raise NotImplementedError
-
- def build_ebnf(self, tools: List[Tool]):
- return EBNFComposer.build_ebnf(
- tools,
- individual_call_start_token=self.tool_call_start_token.replace("\n", "\\n"),
- individual_call_end_token=self.tool_call_end_token.replace("\n", "\\n"),
- tool_call_separator="\\n",
- function_format="xml",
- call_rule_fmt='"\\n" {arguments_rule} "\\n"',
- key_value_rule_fmt='"\\n" {valrule} "\\n"',
- key_value_separator='"\\n"',
- )
diff --git a/python/sglang/srt/function_call/mistral_detector.py b/python/sglang/srt/function_call/mistral_detector.py
index 49767fd53..12eb403fa 100644
--- a/python/sglang/srt/function_call/mistral_detector.py
+++ b/python/sglang/srt/function_call/mistral_detector.py
@@ -10,7 +10,6 @@ from sglang.srt.function_call.core_types import (
StructureInfo,
_GetInfoFunc,
)
-from sglang.srt.function_call.ebnf_composer import EBNFComposer
logger = logging.getLogger(__name__)
@@ -128,12 +127,3 @@ class MistralDetector(BaseFormatDetector):
end="}]",
trigger="[TOOL_CALLS]",
)
-
- def build_ebnf(self, tools: List[Tool]):
- return EBNFComposer.build_ebnf(
- tools,
- sequence_start_token=self.bot_token,
- sequence_end_token=self.eot_token,
- function_format="json",
- tool_call_separator=self.tool_call_separator,
- )
diff --git a/python/sglang/srt/function_call/pythonic_detector.py b/python/sglang/srt/function_call/pythonic_detector.py
index ee0827f1d..928a766ec 100644
--- a/python/sglang/srt/function_call/pythonic_detector.py
+++ b/python/sglang/srt/function_call/pythonic_detector.py
@@ -2,7 +2,7 @@ import ast
import json
import logging
import re
-from typing import List, Optional
+from typing import List
from sglang.srt.entrypoints.openai.protocol import Tool
from sglang.srt.environ import envs
@@ -12,7 +12,6 @@ from sglang.srt.function_call.core_types import (
ToolCallItem,
_GetInfoFunc,
)
-from sglang.srt.function_call.ebnf_composer import EBNFComposer
logger = logging.getLogger(__name__)
@@ -223,12 +222,3 @@ class PythonicDetector(BaseFormatDetector):
def structure_info(self) -> _GetInfoFunc:
raise NotImplementedError
-
- def build_ebnf(self, tools: List[Tool]) -> Optional[str]:
- return EBNFComposer.build_ebnf(
- tools,
- sequence_start_token="[",
- sequence_end_token="]",
- tool_call_separator=",",
- function_format="pythonic",
- )
diff --git a/python/sglang/srt/function_call/qwen25_detector.py b/python/sglang/srt/function_call/qwen25_detector.py
index 40a65e5df..33ae13cdd 100644
--- a/python/sglang/srt/function_call/qwen25_detector.py
+++ b/python/sglang/srt/function_call/qwen25_detector.py
@@ -10,7 +10,6 @@ from sglang.srt.function_call.core_types import (
StructureInfo,
_GetInfoFunc,
)
-from sglang.srt.function_call.ebnf_composer import EBNFComposer
logger = logging.getLogger(__name__)
@@ -119,12 +118,3 @@ class Qwen25Detector(BaseFormatDetector):
end="}\n",
trigger="",
)
-
- def build_ebnf(self, tools: List[Tool]):
- return EBNFComposer.build_ebnf(
- tools,
- individual_call_start_token=self.bot_token.replace("\n", "\\n"),
- individual_call_end_token=self.eot_token.replace("\n", "\\n"),
- tool_call_separator="\\n",
- function_format="json",
- )
diff --git a/python/sglang/srt/function_call/qwen3_coder_detector.py b/python/sglang/srt/function_call/qwen3_coder_detector.py
index 3f44711c2..597d8600f 100644
--- a/python/sglang/srt/function_call/qwen3_coder_detector.py
+++ b/python/sglang/srt/function_call/qwen3_coder_detector.py
@@ -13,7 +13,6 @@ from sglang.srt.function_call.core_types import (
ToolCallItem,
_GetInfoFunc,
)
-from sglang.srt.function_call.ebnf_composer import EBNFComposer
logger = logging.getLogger(__name__)
@@ -352,15 +351,3 @@ class Qwen3CoderDetector(BaseFormatDetector):
def structure_info(self) -> _GetInfoFunc:
raise NotImplementedError
-
- def build_ebnf(self, tools: List[Tool]):
- return EBNFComposer.build_ebnf(
- tools,
- individual_call_start_token=self.tool_call_start_token.replace("\n", "\\n"),
- individual_call_end_token=self.tool_call_end_token.replace("\n", "\\n"),
- tool_call_separator="\\n",
- function_format="xml",
- call_rule_fmt='"\\n" {arguments_rule} "\\n"',
- key_value_rule_fmt='"\\n" {valrule} "\\n"',
- key_value_separator='"\\n"',
- )
diff --git a/python/sglang/srt/function_call/step3_detector.py b/python/sglang/srt/function_call/step3_detector.py
index b46f4544f..3fba98774 100644
--- a/python/sglang/srt/function_call/step3_detector.py
+++ b/python/sglang/srt/function_call/step3_detector.py
@@ -11,7 +11,6 @@ from sglang.srt.function_call.core_types import (
ToolCallItem,
_GetInfoFunc,
)
-from sglang.srt.function_call.ebnf_composer import EBNFComposer
logger = logging.getLogger(__name__)
@@ -406,31 +405,3 @@ class Step3Detector(BaseFormatDetector):
def structure_info(self) -> _GetInfoFunc:
raise NotImplementedError()
-
- def build_ebnf(self, tools: List[Tool]) -> str:
- """
- Build EBNF grammar for Step3 tool call format.
- """
- # Custom call rule for steptml format
- call_rule_fmt = (
- '"function" "<|tool_sep|>" "" '
- '{arguments_rule} ""'
- )
-
- # Custom key-value rule for steptml parameters
- key_value_rule_fmt = (
- '"" {valrule} ""'
- )
-
- return EBNFComposer.build_ebnf(
- tools,
- sequence_start_token=self.bot_token,
- sequence_end_token=self.eot_token,
- individual_call_start_token=self.tool_call_begin,
- individual_call_end_token=self.tool_call_end,
- tool_call_separator="",
- function_format="xml",
- call_rule_fmt=call_rule_fmt,
- key_value_rule_fmt=key_value_rule_fmt,
- key_value_separator="",
- )
diff --git a/test/srt/function_call/test_json_schema_constraint.py b/test/srt/function_call/test_json_schema_constraint.py
index a4483042b..962d97ea2 100644
--- a/test/srt/function_call/test_json_schema_constraint.py
+++ b/test/srt/function_call/test_json_schema_constraint.py
@@ -222,58 +222,6 @@ class TestJsonSchemaConstraint(unittest.TestCase):
{"type": "object", "properties": {}},
)
- def test_json_schema_vs_ebnf_constraint_generation(self):
- """Test direct comparison between JSON schema and EBNF constraint generation"""
-
- # Test with specific tool choice
- tool_choice = ToolChoice(
- type="function", function=ToolChoiceFuncName(name="get_weather")
- )
-
- # Generate JSON schema constraint
- json_schema = get_json_schema_constraint(self.tools, tool_choice)
-
- self.assertIsNotNone(json_schema)
- jsonschema.Draft202012Validator.check_schema(json_schema)
-
- # Generate EBNF constraint using FunctionCallParser
- parser = FunctionCallParser(
- self.tools, "llama3"
- ) # Use a parser that supports EBNF
- ebnf_constraint = parser.get_ebnf(tool_choice)
-
- # Verify JSON schema constraint
- self.assertEqual(json_schema["type"], "array")
- self.assertEqual(json_schema["minItems"], 1)
- self.assertEqual(json_schema["maxItems"], 1)
-
- # Verify EBNF constraint
- self.assertIsNotNone(ebnf_constraint)
- self.assertIsInstance(ebnf_constraint, str)
- self.assertIn("get_weather", ebnf_constraint)
-
- # Test with required tool choice
- required_json_schema = get_json_schema_constraint(self.tools, "required")
-
- self.assertIsNotNone(required_json_schema)
- jsonschema.Draft202012Validator.check_schema(required_json_schema)
-
- required_ebnf_constraint = parser.get_ebnf("required")
-
- # Verify required JSON schema constraint
- self.assertEqual(required_json_schema["type"], "array")
- self.assertEqual(required_json_schema["minItems"], 1)
- self.assertIn("anyOf", required_json_schema["items"])
-
- # Verify required EBNF constraint
- self.assertIsNotNone(required_ebnf_constraint)
- self.assertIsInstance(required_ebnf_constraint, str)
-
- # Both should contain references to the available tools
- tool_names = [tool.function.name for tool in self.tools]
- for tool_name in tool_names:
- self.assertIn(tool_name, required_ebnf_constraint)
-
def test_conflicting_defs_raises_valueerror(self):
"""Test that conflicting tool definitions raise ValueError with proper message"""
tools_with_conflicting_defs = [
diff --git a/test/srt/test_function_call_parser.py b/test/srt/test_function_call_parser.py
index 91ff1dc3d..2fcd28ea2 100644
--- a/test/srt/test_function_call_parser.py
+++ b/test/srt/test_function_call_parser.py
@@ -1,8 +1,6 @@
import json
import unittest
-from xgrammar import GrammarCompiler, TokenizerInfo
-
from sglang.srt.entrypoints.openai.protocol import Function, Tool
from sglang.srt.function_call.base_format_detector import BaseFormatDetector
from sglang.srt.function_call.core_types import StreamingParseResult
@@ -458,452 +456,6 @@ class TestMistralDetector(unittest.TestCase):
self.assertEqual(params["content"], "The answer is 42")
-class TestEBNFGeneration(unittest.TestCase):
- def setUp(self):
- # Create sample tools for testing
- self.tools = [
- Tool(
- type="function",
- function=Function(
- name="get_weather",
- description="Get weather information",
- parameters={
- "properties": {
- "location": {
- "type": "string",
- "description": "Location to get weather for",
- },
- "unit": {
- "type": "string",
- "description": "Temperature unit",
- "enum": ["celsius", "fahrenheit"],
- },
- },
- "required": ["location"],
- },
- ),
- ),
- Tool(
- type="function",
- function=Function(
- name="search",
- description="Search for information",
- parameters={
- "properties": {
- "query": {
- "type": "string",
- "description": "Search query",
- },
- },
- "required": ["query"],
- },
- ),
- ),
- Tool(
- type="function",
- function=Function(
- name="empty_param_func",
- description="Function with empty parameters",
- parameters={
- "properties": {},
- "required": [],
- },
- ),
- ),
- ]
-
- self.tokenizer = get_tokenizer(DEFAULT_SMALL_MODEL_NAME_FOR_TEST)
- tokenizer_info = TokenizerInfo.from_huggingface(self.tokenizer)
- self.grammar_compiler = GrammarCompiler(tokenizer_info=tokenizer_info)
-
- # Initialize all detectors
- self.pythonic_detector = PythonicDetector()
- self.deepseekv3_detector = DeepSeekV3Detector()
- self.llama32_detector = Llama32Detector()
- self.mistral_detector = MistralDetector()
- self.qwen25_detector = Qwen25Detector()
- self.qwen3_coder_detector = Qwen3CoderDetector()
- self.kimik2_detector = KimiK2Detector()
- self.glm45_detector = Glm4MoeDetector()
-
- def test_pythonic_detector_ebnf(self):
- """Test that the PythonicDetector generates valid EBNF."""
- ebnf = self.pythonic_detector.build_ebnf(self.tools)
- self.assertIsNotNone(ebnf)
-
- # Check that the EBNF contains expected patterns
- self.assertIn('call_get_weather ::= "get_weather" "(" ', ebnf)
- self.assertIn('"location" "=" basic_string', ebnf)
- self.assertIn('( "unit" "=" ("\\"celsius\\"" | "\\"fahrenheit\\"") )', ebnf)
-
- # Validate that the EBNF can be compiled by GrammarCompiler
- try:
- ctx = self.grammar_compiler.compile_grammar(ebnf)
- self.assertIsNotNone(ctx, "EBNF should be valid and compile successfully")
- except RuntimeError as e:
- self.fail(f"Failed to compile EBNF: {e}")
-
- def test_deepseekv3_detector_ebnf(self):
- """Test that the DeepSeekV3Detector generates valid EBNF."""
- ebnf = self.deepseekv3_detector.build_ebnf(self.tools)
- self.assertIsNotNone(ebnf)
-
- # Check that the EBNF contains expected patterns
- self.assertIn("<|tool▁calls▁begin|>", ebnf)
- self.assertIn("<|tool▁call▁begin|>function<|tool▁sep|>get_weather", ebnf)
- self.assertIn('\\"location\\"" ws ":" ws basic_string ', ebnf)
-
- # Validate that the EBNF can be compiled by GrammarCompiler
- try:
- ctx = self.grammar_compiler.compile_grammar(ebnf)
- self.assertIsNotNone(ctx, "EBNF should be valid and compile successfully")
- except RuntimeError as e:
- self.fail(f"Failed to compile EBNF: {e}")
-
- def test_kimik2_detector_ebnf(self):
- """Test that the KimiK2Detector generates valid EBNF."""
- ebnf = self.kimik2_detector.build_ebnf(self.tools)
- self.assertIsNotNone(ebnf)
-
- # Check that the EBNF contains expected patterns for KimiK2 format
- self.assertIn("<|tool_calls_section_begin|>", ebnf)
- self.assertIn("<|tool_calls_section_end|>", ebnf)
-
- # Check for KimiK2-specific function call structure
- self.assertIn("<|tool_call_begin|>functions.get_weather:", ebnf)
- self.assertIn("<|tool_call_begin|>functions.search:", ebnf)
- self.assertIn("<|tool_call_argument_begin|>", ebnf)
- self.assertIn("<|tool_call_end|>", ebnf)
-
- # Check that it uses the correct namespace.function format with numeric index pattern
- self.assertIn("functions.get_weather:", ebnf)
- self.assertIn("functions.search:", ebnf)
- self.assertIn("[0-9]+", ebnf) # Numeric index pattern
-
- # Validate that the EBNF can be compiled by GrammarCompiler
- try:
- ctx = self.grammar_compiler.compile_grammar(ebnf)
- self.assertIsNotNone(ctx, "EBNF should be valid and compile successfully")
- except RuntimeError as e:
- self.fail(f"Failed to compile EBNF: {e}")
-
- def test_llama32_detector_ebnf(self):
- """Test that the Llama32Detector generates valid EBNF."""
- ebnf = self.llama32_detector.build_ebnf(self.tools)
- self.assertIsNotNone(ebnf)
-
- # Check that the EBNF contains expected patterns
- self.assertIn('\\"name\\"" ws ":" ws "\\"get_weather\\"', ebnf)
- self.assertIn('"\\"arguments\\"" ws ":"', ebnf)
-
- # Validate that the EBNF can be compiled by GrammarCompiler
- try:
- ctx = self.grammar_compiler.compile_grammar(ebnf)
- self.assertIsNotNone(ctx, "EBNF should be valid and compile successfully")
- except RuntimeError as e:
- self.fail(f"Failed to compile EBNF: {e}")
-
- def test_mistral_detector_ebnf(self):
- """Test that the MistralDetector generates valid EBNF."""
- ebnf = self.mistral_detector.build_ebnf(self.tools)
- self.assertIsNotNone(ebnf)
-
- # Check that the EBNF contains expected patterns
- self.assertIn('"[TOOL_CALLS] ["', ebnf)
- self.assertIn("call_get_weather | call_search", ebnf)
- self.assertIn('"\\"arguments\\"" ws ":"', ebnf)
-
- # Validate that the EBNF can be compiled by GrammarCompiler
- try:
- ctx = self.grammar_compiler.compile_grammar(ebnf)
- self.assertIsNotNone(ctx, "EBNF should be valid and compile successfully")
- except RuntimeError as e:
- self.fail(f"Failed to compile EBNF: {e}")
-
- def test_qwen25_detector_ebnf(self):
- """Test that the Qwen25Detector generates valid EBNF."""
- ebnf = self.qwen25_detector.build_ebnf(self.tools)
- self.assertIsNotNone(ebnf)
-
- # Check that the EBNF contains expected patterns
- self.assertIn("", ebnf)
- self.assertIn('\\"name\\"" ws ":" ws "\\"get_weather\\"', ebnf)
- self.assertIn('"\\"arguments\\"" ws ":"', ebnf)
-
- # Validate that the EBNF can be compiled by GrammarCompiler
- try:
- ctx = self.grammar_compiler.compile_grammar(ebnf)
- self.assertIsNotNone(ctx, "EBNF should be valid and compile successfully")
- except RuntimeError as e:
- self.fail(f"Failed to compile EBNF: {e}")
-
- def test_glm45_detector_ebnf(self):
- """Test that the Glm4MoeDetector generates valid EBNF."""
- ebnf = self.glm45_detector.build_ebnf(self.tools)
- self.assertIsNotNone(ebnf)
- # Check that the EBNF contains expected patterns for XML format
- self.assertIn('"" function_call ""', ebnf)
- self.assertIn('"get_weather" "\\n" ( arguments_get_weather "\\n" )?', ebnf)
- self.assertIn(
- '"location" "\\n" "" xml_text "" ( "\\n" ( "unit" "\\n" "" ("celsius" | "fahrenheit") "" ) )?',
- ebnf,
- )
- self.assertIn('"search" "\\n" ( arguments_search "\\n" )?', ebnf)
- self.assertIn(
- '"query" "\\n" "" xml_text ""',
- ebnf,
- )
- self.assertIn(
- '"empty_param_func" "\\n" ( arguments_empty_param_func "\\n" )?', ebnf
- )
- self.assertIn('arguments_empty_param_func ::= ""', ebnf)
-
- # Validate that the EBNF can be compiled by GrammarCompiler
- try:
- ctx = self.grammar_compiler.compile_grammar(ebnf)
- self.assertIsNotNone(ctx, "EBNF should be valid and compile successfully")
- except RuntimeError as e:
- self.fail(f"Failed to compile EBNF: {e}")
-
- def test_qwen3_coder_detector_ebnf(self):
- """Test that the Qwen3CoderDetector generates valid EBNF."""
- ebnf = self.qwen3_coder_detector.build_ebnf(self.tools)
- self.assertIsNotNone(ebnf)
- # Check that the EBNF contains expected patterns for XML format
- self.assertIn("", ebnf)
- self.assertIn("", ebnf)
- self.assertIn('"\\n"', ebnf)
- self.assertIn('"\\n"', ebnf)
- self.assertIn('"\\n"', ebnf)
- self.assertIn('"\\n"', ebnf)
- # Check that it uses xml_text for string parameters
- self.assertIn("xml_text", ebnf)
- # Validate that the EBNF can be compiled by GrammarCompiler
- try:
- ctx = self.grammar_compiler.compile_grammar(ebnf)
- self.assertIsNotNone(ctx, "EBNF should be valid and compile successfully")
- except RuntimeError as e:
- self.fail(f"Failed to compile EBNF: {e}")
-
- def test_weather_function_optional_parameter_handling(self):
- """Test that weather function with optional unit parameter generates correct EBNF without trailing commas."""
- # Create a weather tool with required location and optional unit
- weather_tool = Tool(
- type="function",
- function=Function(
- name="get_current_weather",
- description="Get the current weather in a given location",
- parameters={
- "type": "object",
- "properties": {
- "location": {
- "type": "string",
- "description": "The city and state, e.g. San Francisco, CA",
- },
- "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
- },
- "required": ["location"],
- },
- ),
- )
-
- # Test all detectors with the weather tool
- detectors = {
- "pythonic": self.pythonic_detector,
- "deepseekv3": self.deepseekv3_detector,
- "llama32": self.llama32_detector,
- "mistral": self.mistral_detector,
- "qwen25": self.qwen25_detector,
- }
-
- for name, detector in detectors.items():
- with self.subTest(detector=name):
- ebnf = detector.build_ebnf([weather_tool])
- self.assertIsNotNone(ebnf, f"{name} detector should generate EBNF")
-
- # Check that the EBNF properly handles optional parameters
- if name == "pythonic":
- # Pythonic format: location="Paris" ( , ( unit=("celsius" | "fahrenheit") )?
- self.assertIn('"location" "=" basic_string', ebnf)
- # The comma should be inside the optional brackets for unit
- self.assertIn('( ws "," ws ( "unit" "=" ', ebnf)
- else:
- # JSON format: "location": "Paris" ( , ( "unit": ("celsius" | "fahrenheit") )?
- self.assertIn('"location\\"" ws ":" ws basic_string', ebnf)
- # The comma should be part of the optional group
- # This pattern ensures no trailing comma when unit is omitted
- self.assertIn('( ws "," ws ( "\\"unit\\"" ws ":"', ebnf)
-
- # Validate that the EBNF can be compiled
- try:
- ctx = self.grammar_compiler.compile_grammar(ebnf)
- self.assertIsNotNone(
- ctx, f"{name} EBNF should compile successfully"
- )
- except RuntimeError as e:
- self.fail(f"Failed to compile {name} EBNF: {e}")
-
- def test_multiple_optional_parameters_flexible_ordering(self):
- """Test that multiple optional parameters allow flexible ordering using llama.cpp approach."""
- # Create a tool with one required and multiple optional parameters
- test_tool = Tool(
- type="function",
- function=Function(
- name="test_func",
- description="Test function with multiple optional parameters",
- parameters={
- "type": "object",
- "properties": {
- "required_field": {"type": "string"},
- "opt1": {"type": "number"},
- "opt2": {"type": "boolean"},
- "opt3": {"type": "string"},
- },
- "required": ["required_field"],
- },
- ),
- )
-
- # Test JSON-based detectors (not pythonic)
- json_detectors = {
- "deepseekv3": self.deepseekv3_detector,
- "llama32": self.llama32_detector,
- "mistral": self.mistral_detector,
- "qwen25": self.qwen25_detector,
- }
-
- for name, detector in json_detectors.items():
- with self.subTest(detector=name):
- ebnf = detector.build_ebnf([test_tool])
- self.assertIsNotNone(ebnf, f"{name} detector should generate EBNF")
-
- # Print the arguments rule for debugging
- lines = ebnf.split("\n")
- args_rule = None
- for line in lines:
- if line.startswith("arguments_test_func ::="):
- args_rule = line
- break
-
- self.assertIsNotNone(
- args_rule, f"{name} should have arguments_test_func rule"
- )
-
- # Check required field
- self.assertIn('"required_field\\"" ws ":" ws basic_string', ebnf)
-
- # Check the structure for optional parameters
- # The pattern should be: required_field ( "," ( opt1 ... | opt2 ... | opt3 ... ) )?
- # This allows flexible ordering where any optional can be first
-
- # Check that optional parameters are in a group with comma
- if args_rule: # Only check if args_rule was found
- self.assertIn(
- '( ws "," ws (',
- args_rule,
- f"{name} should have comma grouped with optional parameters",
- )
-
- # Check for the alternation pattern that allows flexible ordering
- # Should contain patterns like: opt1 ... | opt2 ... | opt3
- self.assertIn('"opt1\\"" ws ":" ws basic_number', args_rule)
- self.assertIn('"opt2\\"" ws ":" ws basic_boolean', args_rule)
- self.assertIn('"opt3\\"" ws ":" ws basic_string', args_rule)
-
- # Check for alternation (|) which allows skipping optional parameters
- self.assertIn(
- "|",
- args_rule,
- f"{name} should use alternation for flexible optional ordering",
- )
-
- # Check that the pattern ends properly with closing braces
- self.assertTrue(
- args_rule.endswith('"}"'),
- f"{name} arguments rule should end with closing brace",
- )
-
- # Validate compilation
- try:
- ctx = self.grammar_compiler.compile_grammar(ebnf)
- self.assertIsNotNone(
- ctx, f"{name} EBNF should compile successfully"
- )
- except RuntimeError as e:
- self.fail(f"Failed to compile {name} EBNF: {e}")
-
- def test_all_optional_parameters_ordering(self):
- """Test the behavior when ALL parameters are optional - verifies ordering constraints."""
- # Create a tool with only optional parameters
- all_optional_tool = Tool(
- type="function",
- function=Function(
- name="optional_func",
- description="Function with all optional parameters",
- parameters={
- "type": "object",
- "properties": {
- "opt1": {"type": "string"},
- "opt2": {"type": "number"},
- "opt3": {"type": "boolean"},
- },
- "required": [], # No required parameters
- },
- ),
- )
-
- # Test JSON-based detectors
- json_detectors = {
- "deepseekv3": self.deepseekv3_detector,
- "llama32": self.llama32_detector,
- "mistral": self.mistral_detector,
- "qwen25": self.qwen25_detector,
- }
-
- for name, detector in json_detectors.items():
- with self.subTest(detector=name):
- ebnf = detector.build_ebnf([all_optional_tool])
- self.assertIsNotNone(ebnf, f"{name} detector should generate EBNF")
-
- # Extract the arguments rule
- lines = ebnf.split("\n")
- args_rule = None
- for line in lines:
- if line.startswith("arguments_optional_func ::="):
- args_rule = line
- break
-
- self.assertIsNotNone(
- args_rule, f"{name} should have arguments_optional_func rule"
- )
-
- if args_rule:
- # When all parameters are optional, the pattern now uses alternation:
- # "{" ( opt1 ... | opt2 ... | opt3 ... )? "}"
- # This allows flexible ordering where any optional can appear first
-
- # Check the structure
- self.assertIn('"opt1\\"" ws ":" ws basic_string', args_rule)
- self.assertIn('"opt2\\"" ws ":" ws basic_number', args_rule)
- self.assertIn('"opt3\\"" ws ":" ws basic_boolean', args_rule)
-
- # The pattern SHOULD have alternation (|) for flexible ordering
- self.assertIn(
- "|",
- args_rule,
- f"{name} should use alternation for flexible ordering even when all properties are optional",
- )
-
- # Validate compilation
- try:
- ctx = self.grammar_compiler.compile_grammar(ebnf)
- self.assertIsNotNone(
- ctx, f"{name} EBNF should compile successfully"
- )
- except RuntimeError as e:
- self.fail(f"Failed to compile {name} EBNF: {e}")
-
-
class TestBaseFormatDetector(unittest.TestCase):
"""Test buffer management and sequential tool index assignment in BaseFormatDetector."""
@@ -928,10 +480,6 @@ class TestBaseFormatDetector(unittest.TestCase):
# Not used in streaming tests
pass
- def build_ebnf(self, tools):
- # Not used in streaming tests
- pass
-
self.detector = TestFormatDetector()
self.tools = [
Tool(
@@ -2339,13 +1887,11 @@ class TestJsonArrayParser(unittest.TestCase):
]
self.detector = JsonArrayParser()
- def test_json_detector_ebnf(self):
- """Test that the JsonArrayParser returns NotImplementedError for EBNF."""
- with self.assertRaises(NotImplementedError) as context:
- self.detector.build_ebnf(self.tools)
- self.assertIn(
- "EBNF generation is not supported for JSON schema constraints",
- str(context.exception),
+ def test_json_detector_has_no_ebnf(self):
+ """JsonArrayParser no longer exposes EBNF generation helpers."""
+ self.assertFalse(
+ hasattr(self.detector, "build_ebnf"),
+ "JsonArrayParser should not expose EBNF helpers after cleanup",
)
def test_parse_streaming_increment_malformed_json(self):