From e42df37df091b8d19fb23cca1c3946080cf48c3f Mon Sep 17 00:00:00 2001 From: Tejesh Anand Date: Wed, 12 Nov 2025 17:55:30 -0800 Subject: [PATCH] Remove EBNF Composer (#13163) --- .../srt/function_call/base_format_detector.py | 27 - .../srt/function_call/deepseekv31_detector.py | 11 - .../srt/function_call/deepseekv3_detector.py | 11 - .../sglang/srt/function_call/ebnf_composer.py | 344 ------------- .../srt/function_call/function_call_parser.py | 38 -- .../srt/function_call/glm4_moe_detector.py | 13 - .../srt/function_call/gpt_oss_detector.py | 3 - .../srt/function_call/json_array_parser.py | 10 - .../srt/function_call/kimik2_detector.py | 19 - .../srt/function_call/llama32_detector.py | 8 - python/sglang/srt/function_call/minimax_m2.py | 13 - .../srt/function_call/mistral_detector.py | 10 - .../srt/function_call/pythonic_detector.py | 12 +- .../srt/function_call/qwen25_detector.py | 10 - .../srt/function_call/qwen3_coder_detector.py | 13 - .../srt/function_call/step3_detector.py | 29 -- .../test_json_schema_constraint.py | 52 -- test/srt/test_function_call_parser.py | 464 +----------------- 18 files changed, 6 insertions(+), 1081 deletions(-) delete mode 100644 python/sglang/srt/function_call/ebnf_composer.py 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):