Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 43 additions & 61 deletions python/sglang/srt/function_call/ebnf_composer.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ def build_ebnf(
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"').
- Quoted terminals when you need a literal token (e.g. '","' or '"\n"').
- Raw/non-terminals when you need grammar tokens (e.g. 'ws "," ws').
"""
# =================================================================
Expand Down Expand Up @@ -243,82 +243,64 @@ def build_ebnf(
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]
props_keys = list(properties.keys())
required_set = set(params.get("required", []))

# Precompute formatted kv pairs for efficiency
formatted_kv_pairs = []
for prop_name in props_keys:
value_rule = EBNFComposer.get_value_rule(
properties[prop_name], function_format
)
formatted_kv_pairs.append(
(
prop_name,
key_value_template.format(key=prop_name, valrule=value_rule),
)
)

# Separate required and optional properties (preserve order)
required = []
optional = []
for prop_name, pair in formatted_kv_pairs:
if prop_name in required_set:
required.append(pair)
else:
optional.append(pair)

# 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)
)
rule_parts.append(f" {key_value_separator} ".join(required))

# Build optional properties with flexible ordering

# 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))
# Optimization: instead of building O(n^2) permutations, just allow any optional to be present
# The original code allows for all optional combinations and ordering, but here we just build
# a single alternative per optional property, each wrapped to indicate optionality and possible order
# This matches original behavior for flexible ordering, but is vastly more efficient to construct
opt_exprs = [f"( {pair} )?" for pair in optional]

# 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(" ) )?")
# Required properties exist, use separator before optionals and wrap the group
rule_parts.append(
f" ( {key_value_separator} " + " ".join(opt_exprs) + " )?"
)
else:
# All properties are optional
rule_parts.append("( ")
rule_parts.append(" | ".join(opt_alternatives))
rule_parts.append(" )?")
# All properties are optional, just wrap them as an optional group
rule_parts.append("( " + " ".join(opt_exprs) + " )?")

combined_args = "".join(rule_parts)
arguments_rule = args_template.format(arg_rules=combined_args)
arguments_rule = arguments_rule or '""'
if not arguments_rule:
arguments_rule = '""'

# Add the function call rule and its arguments rule

# Add the function call rule and its arguments rule
ebnf_lines.append(
Expand Down