sglang 0.4.9.post3__py3-none-any.whl → 0.4.9.post5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sglang/lang/chat_template.py +21 -0
- sglang/srt/_custom_ops.py +29 -1
- sglang/srt/configs/internvl.py +3 -0
- sglang/srt/configs/model_config.py +5 -1
- sglang/srt/constrained/base_grammar_backend.py +10 -2
- sglang/srt/constrained/xgrammar_backend.py +7 -5
- sglang/srt/conversation.py +17 -2
- sglang/srt/debug_utils/__init__.py +0 -0
- sglang/srt/debug_utils/dump_comparator.py +131 -0
- sglang/srt/debug_utils/dumper.py +108 -0
- sglang/srt/debug_utils/text_comparator.py +172 -0
- sglang/srt/disaggregation/common/conn.py +34 -6
- sglang/srt/disaggregation/decode_schedule_batch_mixin.py +13 -1
- sglang/srt/disaggregation/mini_lb.py +3 -2
- sglang/srt/disaggregation/mooncake/conn.py +65 -20
- sglang/srt/disaggregation/mooncake/transfer_engine.py +4 -2
- sglang/srt/disaggregation/nixl/conn.py +17 -13
- sglang/srt/disaggregation/prefill.py +13 -1
- sglang/srt/distributed/device_communicators/custom_all_reduce.py +3 -91
- sglang/srt/distributed/device_communicators/custom_all_reduce_utils.py +96 -1
- sglang/srt/distributed/device_communicators/quick_all_reduce.py +273 -0
- sglang/srt/distributed/device_communicators/shm_broadcast.py +12 -5
- sglang/srt/distributed/parallel_state.py +70 -15
- sglang/srt/entrypoints/engine.py +5 -9
- sglang/srt/entrypoints/http_server.py +20 -32
- sglang/srt/entrypoints/openai/protocol.py +3 -3
- sglang/srt/entrypoints/openai/serving_chat.py +148 -72
- sglang/srt/function_call/base_format_detector.py +74 -12
- sglang/srt/function_call/deepseekv3_detector.py +26 -11
- sglang/srt/function_call/ebnf_composer.py +105 -66
- sglang/srt/function_call/function_call_parser.py +6 -4
- sglang/srt/function_call/glm4_moe_detector.py +164 -0
- sglang/srt/function_call/kimik2_detector.py +41 -16
- sglang/srt/function_call/llama32_detector.py +6 -3
- sglang/srt/function_call/mistral_detector.py +11 -3
- sglang/srt/function_call/pythonic_detector.py +16 -14
- sglang/srt/function_call/qwen25_detector.py +12 -3
- sglang/srt/function_call/{qwen3_detector.py → qwen3_coder_detector.py} +11 -9
- sglang/srt/layers/activation.py +11 -3
- sglang/srt/layers/attention/base_attn_backend.py +3 -1
- sglang/srt/layers/attention/hybrid_attn_backend.py +100 -0
- sglang/srt/layers/attention/vision.py +56 -8
- sglang/srt/layers/communicator.py +12 -12
- sglang/srt/layers/dp_attention.py +72 -24
- sglang/srt/layers/layernorm.py +26 -1
- sglang/srt/layers/logits_processor.py +46 -25
- sglang/srt/layers/moe/ep_moe/layer.py +172 -206
- sglang/srt/layers/moe/fused_moe_triton/configs/triton_3_2_0/E=160,N=320,device_name=NVIDIA_A800-SXM4-80GB,dtype=int8_w8a8.json +146 -0
- sglang/srt/layers/moe/fused_moe_triton/configs/triton_3_3_1/E=160,N=320,device_name=NVIDIA_H20-3e.json +146 -0
- sglang/srt/layers/moe/fused_moe_triton/fused_moe.py +25 -224
- sglang/srt/layers/moe/fused_moe_triton/layer.py +38 -48
- sglang/srt/layers/moe/fused_moe_triton/triton_kernels_moe.py +11 -8
- sglang/srt/layers/moe/topk.py +88 -34
- sglang/srt/layers/multimodal.py +11 -8
- sglang/srt/layers/quantization/compressed_tensors/compressed_tensors_moe.py +2 -9
- sglang/srt/layers/quantization/fp8.py +25 -247
- sglang/srt/layers/quantization/fp8_kernel.py +78 -48
- sglang/srt/layers/quantization/modelopt_quant.py +33 -14
- sglang/srt/layers/quantization/unquant.py +24 -76
- sglang/srt/layers/quantization/utils.py +0 -9
- sglang/srt/layers/quantization/w4afp8.py +68 -17
- sglang/srt/layers/radix_attention.py +5 -3
- sglang/srt/lora/lora_manager.py +133 -169
- sglang/srt/lora/lora_registry.py +188 -0
- sglang/srt/lora/mem_pool.py +2 -2
- sglang/srt/managers/cache_controller.py +62 -13
- sglang/srt/managers/io_struct.py +19 -1
- sglang/srt/managers/mm_utils.py +154 -35
- sglang/srt/managers/multimodal_processor.py +3 -14
- sglang/srt/managers/schedule_batch.py +27 -11
- sglang/srt/managers/scheduler.py +48 -26
- sglang/srt/managers/tokenizer_manager.py +62 -28
- sglang/srt/managers/tp_worker.py +5 -4
- sglang/srt/mem_cache/allocator.py +67 -7
- sglang/srt/mem_cache/hicache_storage.py +17 -1
- sglang/srt/mem_cache/hiradix_cache.py +35 -18
- sglang/srt/mem_cache/memory_pool_host.py +3 -0
- sglang/srt/model_executor/cuda_graph_runner.py +61 -25
- sglang/srt/model_executor/forward_batch_info.py +201 -29
- sglang/srt/model_executor/model_runner.py +109 -37
- sglang/srt/models/deepseek_v2.py +63 -30
- sglang/srt/models/glm4_moe.py +1035 -0
- sglang/srt/models/glm4_moe_nextn.py +167 -0
- sglang/srt/models/interns1.py +328 -0
- sglang/srt/models/internvl.py +143 -47
- sglang/srt/models/llava.py +9 -5
- sglang/srt/models/minicpmo.py +4 -1
- sglang/srt/models/mllama4.py +10 -3
- sglang/srt/models/qwen2_moe.py +2 -6
- sglang/srt/models/qwen3_moe.py +6 -8
- sglang/srt/multimodal/processors/base_processor.py +20 -6
- sglang/srt/multimodal/processors/clip.py +2 -2
- sglang/srt/multimodal/processors/deepseek_vl_v2.py +2 -2
- sglang/srt/multimodal/processors/gemma3.py +2 -2
- sglang/srt/multimodal/processors/gemma3n.py +2 -2
- sglang/srt/multimodal/processors/internvl.py +21 -8
- sglang/srt/multimodal/processors/janus_pro.py +2 -2
- sglang/srt/multimodal/processors/kimi_vl.py +2 -2
- sglang/srt/multimodal/processors/llava.py +4 -4
- sglang/srt/multimodal/processors/minicpm.py +2 -3
- sglang/srt/multimodal/processors/mlama.py +2 -2
- sglang/srt/multimodal/processors/mllama4.py +18 -111
- sglang/srt/multimodal/processors/phi4mm.py +2 -2
- sglang/srt/multimodal/processors/pixtral.py +2 -2
- sglang/srt/multimodal/processors/qwen_audio.py +2 -2
- sglang/srt/multimodal/processors/qwen_vl.py +2 -2
- sglang/srt/multimodal/processors/vila.py +3 -1
- sglang/srt/reasoning_parser.py +48 -5
- sglang/srt/sampling/sampling_batch_info.py +6 -5
- sglang/srt/server_args.py +132 -60
- sglang/srt/speculative/eagle_draft_cuda_graph_runner.py +33 -28
- sglang/srt/speculative/eagle_draft_extend_cuda_graph_runner.py +37 -36
- sglang/srt/speculative/eagle_utils.py +51 -23
- sglang/srt/speculative/eagle_worker.py +59 -44
- sglang/srt/two_batch_overlap.py +9 -5
- sglang/srt/utils.py +113 -69
- sglang/srt/weight_sync/utils.py +119 -0
- sglang/test/runners.py +4 -0
- sglang/test/test_activation.py +50 -1
- sglang/test/test_utils.py +65 -5
- sglang/utils.py +19 -0
- sglang/version.py +1 -1
- {sglang-0.4.9.post3.dist-info → sglang-0.4.9.post5.dist-info}/METADATA +6 -6
- {sglang-0.4.9.post3.dist-info → sglang-0.4.9.post5.dist-info}/RECORD +127 -114
- sglang/srt/debug_utils.py +0 -74
- {sglang-0.4.9.post3.dist-info → sglang-0.4.9.post5.dist-info}/WHEEL +0 -0
- {sglang-0.4.9.post3.dist-info → sglang-0.4.9.post5.dist-info}/licenses/LICENSE +0 -0
- {sglang-0.4.9.post3.dist-info → sglang-0.4.9.post5.dist-info}/top_level.txt +0 -0
@@ -1,51 +1,73 @@
|
|
1
|
-
from typing import Literal, Optional
|
1
|
+
from typing import Any, Dict, Literal, Optional
|
2
2
|
|
3
3
|
|
4
4
|
class EBNFComposer:
|
5
5
|
# Adapted from https://xgrammar.mlc.ai/docs/how_to/ebnf_guided_generation.html#try-out-via-hf-transformers
|
6
|
-
|
7
|
-
|
8
|
-
basic_any ::= basic_number | basic_string | basic_boolean | basic_null | basic_array | basic_object
|
9
|
-
basic_integer ::= ("0" | "-"? [1-9] [0-9]*) ".0"?
|
10
|
-
basic_number ::= ("0" | "-"? [1-9] [0-9]*) ("." [0-9]+)? ([eE] [+-]? [0-9]+)?
|
6
|
+
# Shared primitive grammar rules used across all formats
|
7
|
+
BASE_PRIMITIVE_GRAMMAR = r"""
|
11
8
|
basic_string ::= (([\"] basic_string_1 [\"]))
|
12
9
|
basic_string_1 ::= "" | [^"\\\x00-\x1F] basic_string_1 | "\\" escape basic_string_1
|
13
|
-
escape ::= ["\\/bfnrt] | "u" [A-Fa-f0-9]
|
14
|
-
|
15
|
-
|
10
|
+
escape ::= ["\\/bfnrt] | "u" [A-Fa-f0-9]{4}
|
11
|
+
basic_integer ::= ("0" | "-"? [1-9] [0-9]*) ".0"?
|
12
|
+
basic_number ::= ("0" | "-"? [1-9] [0-9]*) ("." [0-9]+)? ([eE] [+-]? [0-9]+)?
|
16
13
|
basic_array ::= "[" ("" | ws basic_any (ws "," ws basic_any)*) ws "]"
|
17
14
|
basic_object ::= "{" ("" | ws basic_string ws ":" ws basic_any ( ws "," ws basic_string ws ":" ws basic_any)*) ws "}"
|
18
15
|
ws ::= [ \n\t]*
|
19
|
-
|
16
|
+
"""
|
20
17
|
|
21
|
-
|
18
|
+
# Format-specific extensions
|
19
|
+
json_grammar_ebnf_str = (
|
20
|
+
r"""
|
21
|
+
json ::= basic_array | basic_object
|
22
|
+
basic_any ::= basic_number | basic_string | basic_boolean | basic_null | basic_array | basic_object
|
23
|
+
basic_boolean ::= "true" | "false"
|
24
|
+
basic_null ::= "null"
|
25
|
+
"""
|
26
|
+
+ BASE_PRIMITIVE_GRAMMAR
|
27
|
+
)
|
28
|
+
|
29
|
+
pythonic_grammar_ebnf_str = (
|
30
|
+
r"""
|
22
31
|
pythonic ::= basic_number | basic_string | basic_array | "True" | "False" | "None"
|
23
32
|
basic_any ::= basic_number | basic_string | basic_array | basic_object
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
33
|
+
basic_boolean ::= "True" | "False"
|
34
|
+
basic_null ::= "None"
|
35
|
+
"""
|
36
|
+
+ BASE_PRIMITIVE_GRAMMAR
|
37
|
+
)
|
38
|
+
|
39
|
+
xml_grammar_ebnf_str = (
|
40
|
+
r"""
|
41
|
+
xml ::= xml_element | xml_text
|
42
|
+
xml_element ::= basic_string | basic_number | basic_boolean | basic_null | basic_array | basic_object
|
43
|
+
xml_text ::= [^<>]*
|
44
|
+
basic_any ::= basic_number | basic_string | basic_boolean | basic_null | basic_array | basic_object
|
45
|
+
basic_boolean ::= "true" | "false"
|
46
|
+
basic_null ::= "null"
|
31
47
|
"""
|
48
|
+
+ BASE_PRIMITIVE_GRAMMAR
|
49
|
+
)
|
32
50
|
|
33
51
|
CALL_RULE_MAP = {
|
34
52
|
"pythonic": 'call_{name} ::= "{name}" "(" {arguments_rule} ")"',
|
35
53
|
"json": 'call_{name} ::= "{{" "\\"name\\"" ":" "\\"{name}\\"" ", " "\\"arguments\\"" ":" {arguments_rule} "}}"',
|
54
|
+
"xml": 'call_{name} ::= "<function={name}>\\n" {arguments_rule} "\\n</function>"',
|
36
55
|
}
|
37
56
|
|
38
57
|
ARGUMENTS_RULE_MAP = {
|
39
58
|
"pythonic": "{arg_rules}",
|
40
59
|
"json": '"{{" {arg_rules} "}}"',
|
60
|
+
"xml": "{arg_rules}",
|
41
61
|
}
|
42
62
|
|
43
63
|
KEY_VALUE_RULE_MAP = {
|
44
64
|
"pythonic": '"{key}" "=" {valrule}',
|
45
65
|
"json": '"\\"{key}\\"" ":" {valrule}',
|
66
|
+
"xml": '"<parameter={key}>\\n" {valrule} "\\n</parameter>"',
|
46
67
|
}
|
47
68
|
|
48
|
-
|
69
|
+
# Base type mapping - most types are the same across formats
|
70
|
+
BASE_TYPE_MAPPING = {
|
49
71
|
"string": "basic_string",
|
50
72
|
"number": "basic_number",
|
51
73
|
"integer": "basic_number",
|
@@ -55,19 +77,20 @@ class EBNFComposer:
|
|
55
77
|
"object": "basic_object",
|
56
78
|
}
|
57
79
|
|
58
|
-
|
59
|
-
|
60
|
-
"
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
"
|
65
|
-
|
80
|
+
# Format-specific overrides for types that differ
|
81
|
+
FORMAT_TYPE_OVERRIDES = {
|
82
|
+
"pythonic": {
|
83
|
+
"boolean": '"True" | "False"',
|
84
|
+
"null": '"None"',
|
85
|
+
},
|
86
|
+
"xml": {
|
87
|
+
"string": "xml_text",
|
88
|
+
},
|
66
89
|
}
|
67
90
|
|
68
91
|
@staticmethod
|
69
92
|
def get_value_rule(
|
70
|
-
prop: dict, function_format: Literal["pythonic", "json"] = "json"
|
93
|
+
prop: dict, function_format: Literal["pythonic", "json", "xml"] = "json"
|
71
94
|
) -> str:
|
72
95
|
if "enum" in prop:
|
73
96
|
return EBNFComposer._handle_enum(prop, function_format)
|
@@ -83,48 +106,46 @@ class EBNFComposer:
|
|
83
106
|
enum_values = prop["enum"]
|
84
107
|
prop_type = prop.get("type", "string")
|
85
108
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
("boolean", "json"): lambda v: "true" if v else "false",
|
95
|
-
("boolean", "pythonic"): lambda v: "True" if v else "False",
|
96
|
-
}
|
109
|
+
def format_enum_val(v: Any) -> str:
|
110
|
+
if prop_type == "boolean":
|
111
|
+
if function_format == "json" or function_format == "xml":
|
112
|
+
return "true" if v else "false"
|
113
|
+
elif function_format == "pythonic":
|
114
|
+
return "True" if v else "False"
|
115
|
+
else:
|
116
|
+
return str(v) # fallback
|
97
117
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
118
|
+
if prop_type == "string":
|
119
|
+
if function_format == "xml":
|
120
|
+
return f'"{v}"'
|
121
|
+
else: # json or pythonic
|
122
|
+
return f'"\\"{v}\\""' # escape quote-wrapped string
|
103
123
|
|
104
|
-
|
105
|
-
|
124
|
+
# All other types (number, integer, etc.)
|
125
|
+
return str(v)
|
106
126
|
|
107
|
-
|
108
|
-
|
109
|
-
|
127
|
+
formatted_values = [format_enum_val(v) for v in enum_values]
|
128
|
+
enum_rule = " | ".join(formatted_values)
|
129
|
+
return f"({enum_rule})" if len(formatted_values) > 1 else enum_rule
|
110
130
|
|
111
|
-
|
131
|
+
@staticmethod
|
132
|
+
def get_type_mapping(function_format: str) -> Dict[str, str]:
|
133
|
+
"""Get the complete type mapping for a given format."""
|
134
|
+
mapping = EBNFComposer.BASE_TYPE_MAPPING.copy()
|
135
|
+
overrides = EBNFComposer.FORMAT_TYPE_OVERRIDES.get(function_format, {})
|
136
|
+
mapping.update({k: v for k, v in overrides.items() if v is not None})
|
137
|
+
return mapping
|
112
138
|
|
113
139
|
@staticmethod
|
114
140
|
def _handle_type(prop: dict, function_format: str) -> str:
|
115
141
|
"""Handle type properties using the appropriate type mapping."""
|
116
142
|
prop_type = prop["type"]
|
117
|
-
type_mapping = (
|
118
|
-
EBNFComposer.PYTHONIC_TYPE_MAPPING
|
119
|
-
if function_format == "pythonic"
|
120
|
-
else EBNFComposer.JSON_TYPE_MAPPING
|
121
|
-
)
|
143
|
+
type_mapping = EBNFComposer.get_type_mapping(function_format)
|
122
144
|
|
123
145
|
if isinstance(prop_type, list):
|
124
146
|
type_rules = [
|
125
|
-
type_mapping
|
147
|
+
type_mapping.get(single_type, function_format)
|
126
148
|
for single_type in prop_type
|
127
|
-
if single_type in type_mapping
|
128
149
|
]
|
129
150
|
return " | ".join(type_rules) if type_rules else function_format
|
130
151
|
|
@@ -133,7 +154,7 @@ class EBNFComposer:
|
|
133
154
|
@staticmethod
|
134
155
|
def build_ebnf(
|
135
156
|
tools,
|
136
|
-
function_format: Literal["pythonic", "json"] = "json",
|
157
|
+
function_format: Literal["pythonic", "json", "xml"] = "json",
|
137
158
|
# Parameters for wrapping the entire sequence of tool calls
|
138
159
|
sequence_start_token: Optional[str] = None,
|
139
160
|
sequence_end_token: Optional[str] = None,
|
@@ -143,6 +164,8 @@ class EBNFComposer:
|
|
143
164
|
# Parameter for separating multiple tool calls
|
144
165
|
tool_call_separator: Optional[str] = None,
|
145
166
|
call_rule_fmt: Optional[str] = None,
|
167
|
+
key_value_rule_fmt: Optional[str] = None,
|
168
|
+
key_value_separator: str = ",",
|
146
169
|
):
|
147
170
|
"""
|
148
171
|
Generalized EBNF builder for all detectors.
|
@@ -157,6 +180,9 @@ class EBNFComposer:
|
|
157
180
|
call_rule_fmt: Optional custom format string for call_{name} rule. It should define each function call's format, with
|
158
181
|
the placeholders {name} for the function name and {arguments_rule} for the arguments rule. If None, a default
|
159
182
|
format based on function_format will be used.
|
183
|
+
key_value_rule_fmt: Optional custom format string for key-value pairs. It should define how each parameter is formatted,
|
184
|
+
with placeholders {key} for the parameter name and {valrule} for the value rule. If None, a default format
|
185
|
+
based on function_format will be used.
|
160
186
|
"""
|
161
187
|
# =================================================================
|
162
188
|
# Step 1: Determine the root tool calls rule
|
@@ -200,7 +226,11 @@ class EBNFComposer:
|
|
200
226
|
else EBNFComposer.CALL_RULE_MAP[function_format]
|
201
227
|
)
|
202
228
|
args_template = EBNFComposer.ARGUMENTS_RULE_MAP[function_format]
|
203
|
-
key_value_template =
|
229
|
+
key_value_template = (
|
230
|
+
key_value_rule_fmt
|
231
|
+
if key_value_rule_fmt
|
232
|
+
else EBNFComposer.KEY_VALUE_RULE_MAP[function_format]
|
233
|
+
)
|
204
234
|
|
205
235
|
# =================================================================
|
206
236
|
# Step 4: Build rules for each tool
|
@@ -250,7 +280,11 @@ class EBNFComposer:
|
|
250
280
|
|
251
281
|
# Add required properties joined by commas
|
252
282
|
if required:
|
253
|
-
rule_parts.append(
|
283
|
+
rule_parts.append(
|
284
|
+
f' "{key_value_separator}" '.join(
|
285
|
+
prop_kv_pairs[k] for k in required
|
286
|
+
)
|
287
|
+
)
|
254
288
|
|
255
289
|
# Add optional properties with flexible ordering
|
256
290
|
if optional:
|
@@ -263,13 +297,15 @@ class EBNFComposer:
|
|
263
297
|
if j == i:
|
264
298
|
opt_parts.append(prop_kv_pairs[optional[j]])
|
265
299
|
else:
|
266
|
-
opt_parts.append(
|
300
|
+
opt_parts.append(
|
301
|
+
f' ( "{key_value_separator}" {prop_kv_pairs[optional[j]]} )?'
|
302
|
+
)
|
267
303
|
opt_alternatives.append("".join(opt_parts))
|
268
304
|
|
269
305
|
# Wrap with appropriate comma handling based on whether we have required properties
|
270
306
|
if required:
|
271
307
|
# Required properties exist, so optional group needs outer comma
|
272
|
-
rule_parts.append(' ( "
|
308
|
+
rule_parts.append(f' ( "{key_value_separator}" ( ')
|
273
309
|
rule_parts.append(" | ".join(opt_alternatives))
|
274
310
|
rule_parts.append(" ) )?")
|
275
311
|
else:
|
@@ -292,10 +328,13 @@ class EBNFComposer:
|
|
292
328
|
# =================================================================
|
293
329
|
# Step 5: Add base grammar rules
|
294
330
|
# =================================================================
|
295
|
-
|
296
|
-
EBNFComposer.pythonic_grammar_ebnf_str
|
297
|
-
|
298
|
-
|
331
|
+
grammar_dict = {
|
332
|
+
"pythonic": EBNFComposer.pythonic_grammar_ebnf_str,
|
333
|
+
"json": EBNFComposer.json_grammar_ebnf_str,
|
334
|
+
"xml": EBNFComposer.xml_grammar_ebnf_str,
|
335
|
+
}
|
336
|
+
base_grammar = grammar_dict.get(
|
337
|
+
function_format, EBNFComposer.json_grammar_ebnf_str
|
299
338
|
)
|
300
339
|
ebnf_lines.append(base_grammar)
|
301
340
|
|
@@ -10,11 +10,12 @@ from sglang.srt.entrypoints.openai.protocol import (
|
|
10
10
|
from sglang.srt.function_call.base_format_detector import BaseFormatDetector
|
11
11
|
from sglang.srt.function_call.core_types import ToolCallItem
|
12
12
|
from sglang.srt.function_call.deepseekv3_detector import DeepSeekV3Detector
|
13
|
+
from sglang.srt.function_call.glm4_moe_detector import Glm4MoeDetector
|
13
14
|
from sglang.srt.function_call.kimik2_detector import KimiK2Detector
|
14
15
|
from sglang.srt.function_call.llama32_detector import Llama32Detector
|
15
16
|
from sglang.srt.function_call.mistral_detector import MistralDetector
|
16
17
|
from sglang.srt.function_call.pythonic_detector import PythonicDetector
|
17
|
-
from sglang.srt.function_call.
|
18
|
+
from sglang.srt.function_call.qwen3_coder_detector import Qwen3CoderDetector
|
18
19
|
from sglang.srt.function_call.qwen25_detector import Qwen25Detector
|
19
20
|
|
20
21
|
logger = logging.getLogger(__name__)
|
@@ -36,7 +37,8 @@ class FunctionCallParser:
|
|
36
37
|
"deepseekv3": DeepSeekV3Detector,
|
37
38
|
"pythonic": PythonicDetector,
|
38
39
|
"kimi_k2": KimiK2Detector,
|
39
|
-
"
|
40
|
+
"qwen3_coder": Qwen3CoderDetector,
|
41
|
+
"glm45": Glm4MoeDetector,
|
40
42
|
}
|
41
43
|
|
42
44
|
def __init__(self, tools: List[Tool], tool_call_parser: str):
|
@@ -155,9 +157,9 @@ class FunctionCallParser:
|
|
155
157
|
or None if no constraint applies.
|
156
158
|
"""
|
157
159
|
# NOTE: structural_tag only supports JSON-compatible content between the begin and end.
|
158
|
-
# It cannot parse or validate
|
160
|
+
# It cannot parse or validate function call Pythonic or XML-ish syntax.
|
159
161
|
if (
|
160
|
-
|
162
|
+
self.detector.supports_structural_tag()
|
161
163
|
and tool_choice == "auto"
|
162
164
|
and any(tool.function.strict for tool in self.tools)
|
163
165
|
):
|
@@ -0,0 +1,164 @@
|
|
1
|
+
import ast
|
2
|
+
import json
|
3
|
+
import logging
|
4
|
+
import re
|
5
|
+
from typing import List
|
6
|
+
|
7
|
+
from sglang.srt.entrypoints.openai.protocol import Tool
|
8
|
+
from sglang.srt.function_call.base_format_detector import BaseFormatDetector
|
9
|
+
from sglang.srt.function_call.core_types import (
|
10
|
+
StreamingParseResult,
|
11
|
+
StructureInfo,
|
12
|
+
_GetInfoFunc,
|
13
|
+
)
|
14
|
+
from sglang.srt.function_call.ebnf_composer import EBNFComposer
|
15
|
+
|
16
|
+
logger = logging.getLogger(__name__)
|
17
|
+
|
18
|
+
|
19
|
+
def get_argument_type(func_name: str, arg_key: str, defined_tools: list):
|
20
|
+
name2tool = {tool.function.name: tool for tool in defined_tools}
|
21
|
+
if func_name not in name2tool:
|
22
|
+
return None
|
23
|
+
tool = name2tool[func_name]
|
24
|
+
if arg_key not in tool.function.parameters["properties"]:
|
25
|
+
return None
|
26
|
+
return tool.function.parameters["properties"][arg_key].get("type", None)
|
27
|
+
|
28
|
+
|
29
|
+
def parse_arguments(json_value):
|
30
|
+
try:
|
31
|
+
try:
|
32
|
+
parsed_value = json.loads(json_value)
|
33
|
+
except:
|
34
|
+
parsed_value = ast.literal_eval(json_value)
|
35
|
+
return parsed_value, True
|
36
|
+
except:
|
37
|
+
return json_value, False
|
38
|
+
|
39
|
+
|
40
|
+
class Glm4MoeDetector(BaseFormatDetector):
|
41
|
+
"""
|
42
|
+
Detector for GLM-4.5 models.
|
43
|
+
Assumes function call format:
|
44
|
+
<tool_call>get_weather\n<arg_key>city</arg_key>\n<arg_value>北京</arg_value>\n<arg_key>date</arg_key>\n<arg_value>2024-06-27</arg_value>\n</tool_call>\n<tool_call>get_weather\n<arg_key>city</arg_key>\n<arg_value>上海</arg_value>\n<arg_key>date</arg_key>\n<arg_value>2024-06-27</arg_value>\n</tool_call>
|
45
|
+
"""
|
46
|
+
|
47
|
+
def __init__(self):
|
48
|
+
super().__init__()
|
49
|
+
self.bot_token = "<tool_call>"
|
50
|
+
self.eot_token = "</tool_call>"
|
51
|
+
self.func_call_regex = r"<tool_call>.*?</tool_call>"
|
52
|
+
self.func_detail_regex = r"<tool_call>([^\n]*)\n(.*)</tool_call>"
|
53
|
+
self.func_arg_regex = r"<arg_key>(.*?)</arg_key>\s*<arg_value>(.*?)</arg_value>"
|
54
|
+
|
55
|
+
def has_tool_call(self, text: str) -> bool:
|
56
|
+
"""Check if the text contains a glm-4.5 format tool call."""
|
57
|
+
return self.bot_token in text
|
58
|
+
|
59
|
+
def detect_and_parse(self, text: str, tools: List[Tool]) -> StreamingParseResult:
|
60
|
+
"""
|
61
|
+
One-time parsing: Detects and parses tool calls in the provided text.
|
62
|
+
|
63
|
+
:param text: The complete text to parse.
|
64
|
+
:param tools: List of available tools.
|
65
|
+
:return: ParseResult indicating success or failure, consumed text, leftover text, and parsed calls.
|
66
|
+
"""
|
67
|
+
idx = text.find(self.bot_token)
|
68
|
+
normal_text = text[:idx].strip() if idx != -1 else text
|
69
|
+
if self.bot_token not in text:
|
70
|
+
return StreamingParseResult(normal_text=normal_text, calls=[])
|
71
|
+
match_result_list = re.findall(self.func_call_regex, text, re.DOTALL)
|
72
|
+
calls = []
|
73
|
+
try:
|
74
|
+
for match_result in match_result_list:
|
75
|
+
# Get function name
|
76
|
+
func_detail = re.search(self.func_detail_regex, match_result, re.DOTALL)
|
77
|
+
func_name = func_detail.group(1)
|
78
|
+
func_args = func_detail.group(2)
|
79
|
+
pairs = re.findall(
|
80
|
+
r"<arg_key>(.*?)</arg_key>\s*<arg_value>(.*?)</arg_value>",
|
81
|
+
func_args,
|
82
|
+
re.DOTALL,
|
83
|
+
)
|
84
|
+
arguments = {}
|
85
|
+
for arg_key, arg_value in pairs:
|
86
|
+
arg_key = arg_key.strip()
|
87
|
+
arg_value = arg_value.strip()
|
88
|
+
arg_type = get_argument_type(func_name, arg_key, tools)
|
89
|
+
if arg_type != "string":
|
90
|
+
arg_value, is_good_json = parse_arguments(arg_value)
|
91
|
+
arguments[arg_key] = arg_value
|
92
|
+
# construct match_result for parse_base_json
|
93
|
+
match_result = {"name": func_name, "parameters": arguments}
|
94
|
+
calls.extend(self.parse_base_json(match_result, tools))
|
95
|
+
return StreamingParseResult(normal_text=normal_text, calls=calls)
|
96
|
+
except Exception as e:
|
97
|
+
logger.error(f"Error in detect_and_parse: {e}")
|
98
|
+
# return the normal text if parsing fails
|
99
|
+
return StreamingParseResult(normal_text=text)
|
100
|
+
|
101
|
+
def parse_streaming_increment(
|
102
|
+
self, new_text: str, tools: List[Tool]
|
103
|
+
) -> StreamingParseResult:
|
104
|
+
"""
|
105
|
+
Streaming incremental parsing tool calls for GLM-4.5 format.
|
106
|
+
"""
|
107
|
+
self._buffer += new_text
|
108
|
+
current_text = self._buffer
|
109
|
+
|
110
|
+
start = current_text.find(self.bot_token)
|
111
|
+
if start == -1:
|
112
|
+
self._buffer = ""
|
113
|
+
if self.current_tool_id > 0:
|
114
|
+
current_text = ""
|
115
|
+
return StreamingParseResult(normal_text=current_text)
|
116
|
+
# find ensures we find the first self.eot_token so there will be at most one tool_call in current_text[:end+len(self.eot_token)
|
117
|
+
end = current_text.find(self.eot_token)
|
118
|
+
if end != -1:
|
119
|
+
# Initialize state if this is the first tool call
|
120
|
+
if self.current_tool_id == -1:
|
121
|
+
self.current_tool_id = 0
|
122
|
+
self.prev_tool_call_arr = []
|
123
|
+
self.streamed_args_for_tool = [""]
|
124
|
+
# Ensure we have enough entries in our tracking arrays
|
125
|
+
while len(self.prev_tool_call_arr) <= self.current_tool_id:
|
126
|
+
self.prev_tool_call_arr.append({})
|
127
|
+
while len(self.streamed_args_for_tool) <= self.current_tool_id:
|
128
|
+
self.streamed_args_for_tool.append("")
|
129
|
+
result = self.detect_and_parse(
|
130
|
+
current_text[: end + len(self.eot_token)], tools=tools
|
131
|
+
)
|
132
|
+
if result.calls:
|
133
|
+
self.prev_tool_call_arr[self.current_tool_id] = {
|
134
|
+
"name": result.calls[0].name,
|
135
|
+
"arguments": json.loads(result.calls[0].parameters),
|
136
|
+
}
|
137
|
+
self.streamed_args_for_tool[self.current_tool_id] = result.calls[
|
138
|
+
0
|
139
|
+
].parameters
|
140
|
+
result.calls[0].tool_index = self.current_tool_id
|
141
|
+
self.current_tool_id += 1
|
142
|
+
self._buffer = current_text[end + len(self.eot_token) :]
|
143
|
+
return result
|
144
|
+
normal_text = current_text[:start]
|
145
|
+
self._buffer = current_text[start:]
|
146
|
+
return StreamingParseResult(normal_text=normal_text)
|
147
|
+
|
148
|
+
def supports_structural_tag(self) -> bool:
|
149
|
+
return False
|
150
|
+
|
151
|
+
def structure_info(self) -> _GetInfoFunc:
|
152
|
+
raise NotImplementedError()
|
153
|
+
|
154
|
+
def build_ebnf(self, tools: List[Tool]):
|
155
|
+
return EBNFComposer.build_ebnf(
|
156
|
+
tools,
|
157
|
+
individual_call_start_token=self.bot_token,
|
158
|
+
individual_call_end_token=self.eot_token,
|
159
|
+
tool_call_separator="\\n",
|
160
|
+
function_format="xml",
|
161
|
+
call_rule_fmt='"{name}" "\\n" {arguments_rule} "\\n"',
|
162
|
+
key_value_rule_fmt='"<arg_key>{key}</arg_key>" "\\n" "<arg_value>" {valrule} "</arg_value>"',
|
163
|
+
key_value_separator="\\n",
|
164
|
+
)
|
@@ -18,16 +18,21 @@ logger = logging.getLogger(__name__)
|
|
18
18
|
|
19
19
|
|
20
20
|
class KimiK2Detector(BaseFormatDetector):
|
21
|
+
"""
|
22
|
+
Detector for Kimi K2 model function call format.
|
23
|
+
|
24
|
+
Format Structure:
|
25
|
+
```
|
26
|
+
<|tool_calls_section_begin|>
|
27
|
+
<|tool_call_begin|>functions.{func_name}:{index} <|tool_call_argument_begin|>{json_args}<|tool_call_end|>
|
28
|
+
<|tool_calls_section_end|>
|
29
|
+
```
|
30
|
+
|
31
|
+
Reference: https://huggingface.co/moonshotai/Kimi-K2-Instruct/blob/main/docs/tool_call_guidance.md
|
32
|
+
"""
|
21
33
|
|
22
34
|
def __init__(self):
|
23
35
|
super().__init__()
|
24
|
-
self._buffer = ""
|
25
|
-
self.current_tool_name_sent: bool = False
|
26
|
-
self.prev_tool_call_arr: list[dict] = []
|
27
|
-
self.current_tool_id: int = -1
|
28
|
-
self.streamed_args_for_tool: list[str] = (
|
29
|
-
[]
|
30
|
-
) # map what has been streamed for each tool so far to a list
|
31
36
|
|
32
37
|
self.bot_token: str = "<|tool_calls_section_begin|>"
|
33
38
|
self.eot_token: str = "<|tool_calls_section_end|>"
|
@@ -114,11 +119,7 @@ class KimiK2Detector(BaseFormatDetector):
|
|
114
119
|
return StreamingParseResult(normal_text=new_text)
|
115
120
|
|
116
121
|
if not hasattr(self, "_tool_indices"):
|
117
|
-
self._tool_indices =
|
118
|
-
tool.function.name: i
|
119
|
-
for i, tool in enumerate(tools)
|
120
|
-
if tool.function and tool.function.name
|
121
|
-
}
|
122
|
+
self._tool_indices = self._get_tool_indices(tools)
|
122
123
|
|
123
124
|
calls: list[ToolCallItem] = []
|
124
125
|
try:
|
@@ -150,7 +151,7 @@ class KimiK2Detector(BaseFormatDetector):
|
|
150
151
|
)
|
151
152
|
)
|
152
153
|
self.current_tool_name_sent = True
|
153
|
-
# Store the tool call info for
|
154
|
+
# Store the tool call info for serving layer completions endpoint
|
154
155
|
self.prev_tool_call_arr[self.current_tool_id] = {
|
155
156
|
"name": function_name,
|
156
157
|
"arguments": {},
|
@@ -214,7 +215,31 @@ class KimiK2Detector(BaseFormatDetector):
|
|
214
215
|
return StreamingParseResult(normal_text=current_text)
|
215
216
|
|
216
217
|
def structure_info(self) -> _GetInfoFunc:
|
217
|
-
|
218
|
+
"""Return function that creates StructureInfo for guided generation."""
|
219
|
+
|
220
|
+
def get_info(name: str) -> StructureInfo:
|
221
|
+
return StructureInfo(
|
222
|
+
begin=f"<|tool_calls_section_begin|><|tool_call_begin|>functions.{name}:0 <|tool_call_argument_begin|>",
|
223
|
+
end="<|tool_call_end|><|tool_calls_section_end|>",
|
224
|
+
trigger="<|tool_calls_section_begin|>",
|
225
|
+
)
|
226
|
+
|
227
|
+
return get_info
|
218
228
|
|
219
|
-
def build_ebnf(self, tools: List[Tool]):
|
220
|
-
|
229
|
+
def build_ebnf(self, tools: List[Tool]) -> str:
|
230
|
+
"""
|
231
|
+
Build EBNF grammar for KimiK2 tool call format.
|
232
|
+
|
233
|
+
NOTE: The call_rule_fmt uses [0-9]+ for the function index to allow the grammar
|
234
|
+
to accept any numeric index (0, 1, 2, etc.) for proper sequential indexing in
|
235
|
+
multiple function call scenarios, while still maintaining the correct KimiK2
|
236
|
+
format structure for constrained generation.
|
237
|
+
"""
|
238
|
+
return EBNFComposer.build_ebnf(
|
239
|
+
tools,
|
240
|
+
sequence_start_token=self.bot_token,
|
241
|
+
sequence_end_token=self.eot_token,
|
242
|
+
tool_call_separator="",
|
243
|
+
call_rule_fmt='"<|tool_call_begin|>functions.{name}:" [0-9]+ " <|tool_call_argument_begin|>" {arguments_rule} "<|tool_call_end|>"',
|
244
|
+
function_format="json",
|
245
|
+
)
|
@@ -16,9 +16,12 @@ logger = logging.getLogger(__name__)
|
|
16
16
|
|
17
17
|
class Llama32Detector(BaseFormatDetector):
|
18
18
|
"""
|
19
|
-
Detector for Llama 3.2 models.
|
20
|
-
|
21
|
-
|
19
|
+
Detector for Llama 3.2 models with json tool call format.
|
20
|
+
|
21
|
+
Format Structure:
|
22
|
+
```
|
23
|
+
<python_tag>{"name":"xxx", "arguments":{...}}
|
24
|
+
```
|
22
25
|
"""
|
23
26
|
|
24
27
|
def __init__(self):
|
@@ -17,9 +17,17 @@ logger = logging.getLogger(__name__)
|
|
17
17
|
|
18
18
|
class MistralDetector(BaseFormatDetector):
|
19
19
|
"""
|
20
|
-
Detector for Mistral
|
21
|
-
|
22
|
-
|
20
|
+
Detector for Mistral model function call format.
|
21
|
+
|
22
|
+
The Mistral format uses a simple bracket-delimited structure with JSON arrays
|
23
|
+
containing function call objects.
|
24
|
+
|
25
|
+
Format Structure:
|
26
|
+
```
|
27
|
+
[TOOL_CALLS] [{"name": "function_name", "arguments": {json_args}}, ...]
|
28
|
+
```
|
29
|
+
|
30
|
+
Reference: https://huggingface.co/mistralai/Mistral-7B-Instruct-v0.3?chat_template=default
|
23
31
|
"""
|
24
32
|
|
25
33
|
def __init__(self):
|