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.
Files changed (128) hide show
  1. sglang/lang/chat_template.py +21 -0
  2. sglang/srt/_custom_ops.py +29 -1
  3. sglang/srt/configs/internvl.py +3 -0
  4. sglang/srt/configs/model_config.py +5 -1
  5. sglang/srt/constrained/base_grammar_backend.py +10 -2
  6. sglang/srt/constrained/xgrammar_backend.py +7 -5
  7. sglang/srt/conversation.py +17 -2
  8. sglang/srt/debug_utils/__init__.py +0 -0
  9. sglang/srt/debug_utils/dump_comparator.py +131 -0
  10. sglang/srt/debug_utils/dumper.py +108 -0
  11. sglang/srt/debug_utils/text_comparator.py +172 -0
  12. sglang/srt/disaggregation/common/conn.py +34 -6
  13. sglang/srt/disaggregation/decode_schedule_batch_mixin.py +13 -1
  14. sglang/srt/disaggregation/mini_lb.py +3 -2
  15. sglang/srt/disaggregation/mooncake/conn.py +65 -20
  16. sglang/srt/disaggregation/mooncake/transfer_engine.py +4 -2
  17. sglang/srt/disaggregation/nixl/conn.py +17 -13
  18. sglang/srt/disaggregation/prefill.py +13 -1
  19. sglang/srt/distributed/device_communicators/custom_all_reduce.py +3 -91
  20. sglang/srt/distributed/device_communicators/custom_all_reduce_utils.py +96 -1
  21. sglang/srt/distributed/device_communicators/quick_all_reduce.py +273 -0
  22. sglang/srt/distributed/device_communicators/shm_broadcast.py +12 -5
  23. sglang/srt/distributed/parallel_state.py +70 -15
  24. sglang/srt/entrypoints/engine.py +5 -9
  25. sglang/srt/entrypoints/http_server.py +20 -32
  26. sglang/srt/entrypoints/openai/protocol.py +3 -3
  27. sglang/srt/entrypoints/openai/serving_chat.py +148 -72
  28. sglang/srt/function_call/base_format_detector.py +74 -12
  29. sglang/srt/function_call/deepseekv3_detector.py +26 -11
  30. sglang/srt/function_call/ebnf_composer.py +105 -66
  31. sglang/srt/function_call/function_call_parser.py +6 -4
  32. sglang/srt/function_call/glm4_moe_detector.py +164 -0
  33. sglang/srt/function_call/kimik2_detector.py +41 -16
  34. sglang/srt/function_call/llama32_detector.py +6 -3
  35. sglang/srt/function_call/mistral_detector.py +11 -3
  36. sglang/srt/function_call/pythonic_detector.py +16 -14
  37. sglang/srt/function_call/qwen25_detector.py +12 -3
  38. sglang/srt/function_call/{qwen3_detector.py → qwen3_coder_detector.py} +11 -9
  39. sglang/srt/layers/activation.py +11 -3
  40. sglang/srt/layers/attention/base_attn_backend.py +3 -1
  41. sglang/srt/layers/attention/hybrid_attn_backend.py +100 -0
  42. sglang/srt/layers/attention/vision.py +56 -8
  43. sglang/srt/layers/communicator.py +12 -12
  44. sglang/srt/layers/dp_attention.py +72 -24
  45. sglang/srt/layers/layernorm.py +26 -1
  46. sglang/srt/layers/logits_processor.py +46 -25
  47. sglang/srt/layers/moe/ep_moe/layer.py +172 -206
  48. 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
  49. sglang/srt/layers/moe/fused_moe_triton/configs/triton_3_3_1/E=160,N=320,device_name=NVIDIA_H20-3e.json +146 -0
  50. sglang/srt/layers/moe/fused_moe_triton/fused_moe.py +25 -224
  51. sglang/srt/layers/moe/fused_moe_triton/layer.py +38 -48
  52. sglang/srt/layers/moe/fused_moe_triton/triton_kernels_moe.py +11 -8
  53. sglang/srt/layers/moe/topk.py +88 -34
  54. sglang/srt/layers/multimodal.py +11 -8
  55. sglang/srt/layers/quantization/compressed_tensors/compressed_tensors_moe.py +2 -9
  56. sglang/srt/layers/quantization/fp8.py +25 -247
  57. sglang/srt/layers/quantization/fp8_kernel.py +78 -48
  58. sglang/srt/layers/quantization/modelopt_quant.py +33 -14
  59. sglang/srt/layers/quantization/unquant.py +24 -76
  60. sglang/srt/layers/quantization/utils.py +0 -9
  61. sglang/srt/layers/quantization/w4afp8.py +68 -17
  62. sglang/srt/layers/radix_attention.py +5 -3
  63. sglang/srt/lora/lora_manager.py +133 -169
  64. sglang/srt/lora/lora_registry.py +188 -0
  65. sglang/srt/lora/mem_pool.py +2 -2
  66. sglang/srt/managers/cache_controller.py +62 -13
  67. sglang/srt/managers/io_struct.py +19 -1
  68. sglang/srt/managers/mm_utils.py +154 -35
  69. sglang/srt/managers/multimodal_processor.py +3 -14
  70. sglang/srt/managers/schedule_batch.py +27 -11
  71. sglang/srt/managers/scheduler.py +48 -26
  72. sglang/srt/managers/tokenizer_manager.py +62 -28
  73. sglang/srt/managers/tp_worker.py +5 -4
  74. sglang/srt/mem_cache/allocator.py +67 -7
  75. sglang/srt/mem_cache/hicache_storage.py +17 -1
  76. sglang/srt/mem_cache/hiradix_cache.py +35 -18
  77. sglang/srt/mem_cache/memory_pool_host.py +3 -0
  78. sglang/srt/model_executor/cuda_graph_runner.py +61 -25
  79. sglang/srt/model_executor/forward_batch_info.py +201 -29
  80. sglang/srt/model_executor/model_runner.py +109 -37
  81. sglang/srt/models/deepseek_v2.py +63 -30
  82. sglang/srt/models/glm4_moe.py +1035 -0
  83. sglang/srt/models/glm4_moe_nextn.py +167 -0
  84. sglang/srt/models/interns1.py +328 -0
  85. sglang/srt/models/internvl.py +143 -47
  86. sglang/srt/models/llava.py +9 -5
  87. sglang/srt/models/minicpmo.py +4 -1
  88. sglang/srt/models/mllama4.py +10 -3
  89. sglang/srt/models/qwen2_moe.py +2 -6
  90. sglang/srt/models/qwen3_moe.py +6 -8
  91. sglang/srt/multimodal/processors/base_processor.py +20 -6
  92. sglang/srt/multimodal/processors/clip.py +2 -2
  93. sglang/srt/multimodal/processors/deepseek_vl_v2.py +2 -2
  94. sglang/srt/multimodal/processors/gemma3.py +2 -2
  95. sglang/srt/multimodal/processors/gemma3n.py +2 -2
  96. sglang/srt/multimodal/processors/internvl.py +21 -8
  97. sglang/srt/multimodal/processors/janus_pro.py +2 -2
  98. sglang/srt/multimodal/processors/kimi_vl.py +2 -2
  99. sglang/srt/multimodal/processors/llava.py +4 -4
  100. sglang/srt/multimodal/processors/minicpm.py +2 -3
  101. sglang/srt/multimodal/processors/mlama.py +2 -2
  102. sglang/srt/multimodal/processors/mllama4.py +18 -111
  103. sglang/srt/multimodal/processors/phi4mm.py +2 -2
  104. sglang/srt/multimodal/processors/pixtral.py +2 -2
  105. sglang/srt/multimodal/processors/qwen_audio.py +2 -2
  106. sglang/srt/multimodal/processors/qwen_vl.py +2 -2
  107. sglang/srt/multimodal/processors/vila.py +3 -1
  108. sglang/srt/reasoning_parser.py +48 -5
  109. sglang/srt/sampling/sampling_batch_info.py +6 -5
  110. sglang/srt/server_args.py +132 -60
  111. sglang/srt/speculative/eagle_draft_cuda_graph_runner.py +33 -28
  112. sglang/srt/speculative/eagle_draft_extend_cuda_graph_runner.py +37 -36
  113. sglang/srt/speculative/eagle_utils.py +51 -23
  114. sglang/srt/speculative/eagle_worker.py +59 -44
  115. sglang/srt/two_batch_overlap.py +9 -5
  116. sglang/srt/utils.py +113 -69
  117. sglang/srt/weight_sync/utils.py +119 -0
  118. sglang/test/runners.py +4 -0
  119. sglang/test/test_activation.py +50 -1
  120. sglang/test/test_utils.py +65 -5
  121. sglang/utils.py +19 -0
  122. sglang/version.py +1 -1
  123. {sglang-0.4.9.post3.dist-info → sglang-0.4.9.post5.dist-info}/METADATA +6 -6
  124. {sglang-0.4.9.post3.dist-info → sglang-0.4.9.post5.dist-info}/RECORD +127 -114
  125. sglang/srt/debug_utils.py +0 -74
  126. {sglang-0.4.9.post3.dist-info → sglang-0.4.9.post5.dist-info}/WHEEL +0 -0
  127. {sglang-0.4.9.post3.dist-info → sglang-0.4.9.post5.dist-info}/licenses/LICENSE +0 -0
  128. {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
- json_grammar_ebnf_str = r"""
7
- json ::= basic_array | basic_object
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] [A-Fa-f0-9] [A-Fa-f0-9] [A-Fa-f0-9]
14
- basic_boolean ::= "true" | "false"
15
- basic_null ::= "null"
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
- pythonic_grammar_ebnf_str = r"""
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
- basic_number ::= ("0" | "-"? [1-9] [0-9]*) ("." [0-9]+)? ([eE] [+-]? [0-9]+)?
25
- basic_string ::= (([\"] basic_string_1 [\"]))
26
- basic_string_1 ::= "" | [^"\\\x00-\x1F] basic_string_1 | "\\" escape basic_string_1
27
- escape ::= ["\\/bfnrt] | "u" [A-Fa-f0-9] [A-Fa-f0-9] [A-Fa-f0-9] [A-Fa-f0-9]
28
- basic_array ::= "[" ("" | ws basic_any (ws "," ws basic_any)*) ws "]"
29
- basic_object ::= "{" ("" | ws basic_string ws ":" ws basic_any ( ws "," ws basic_string ws ":" ws basic_any)*) ws "}"
30
- ws ::= [ \n\t]*
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
- JSON_TYPE_MAPPING = {
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
- PYTHONIC_TYPE_MAPPING = {
59
- "string": "basic_string",
60
- "number": "basic_number",
61
- "integer": "basic_number",
62
- "boolean": '"True" | "False"',
63
- "null": '"None"',
64
- "array": "basic_array",
65
- "object": "basic_object",
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
- # Define formatters for different type/format combinations
87
- formatters = {
88
- ("string", "json"): lambda v: f'"\\"{v}\\""',
89
- ("string", "pythonic"): lambda v: f'"\\"{v}\\""',
90
- ("number", "json"): str,
91
- ("number", "pythonic"): str,
92
- ("integer", "json"): str,
93
- ("integer", "pythonic"): str,
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
- # Get the formatter or default to string handling
99
- formatter = formatters.get(
100
- (prop_type, function_format),
101
- formatters[("string", function_format)], # Default to string handling
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
- formatted_values = [formatter(value) for value in enum_values]
105
- enum_rule = " | ".join(formatted_values)
124
+ # All other types (number, integer, etc.)
125
+ return str(v)
106
126
 
107
- # Wrap in parentheses if there are multiple values to ensure correct EBNF precedence
108
- if len(formatted_values) > 1:
109
- enum_rule = f"({enum_rule})"
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
- return enum_rule
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[single_type]
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 = EBNFComposer.KEY_VALUE_RULE_MAP[function_format]
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(' "," '.join(prop_kv_pairs[k] for k in required))
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(f' ( "," {prop_kv_pairs[optional[j]]} )?')
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
- base_grammar = (
296
- EBNFComposer.pythonic_grammar_ebnf_str
297
- if function_format == "pythonic"
298
- else EBNFComposer.json_grammar_ebnf_str
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.qwen3_detector import Qwen3XMLDetector
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
- "qwen3": Qwen3XMLDetector,
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 Python syntax like function calls.
160
+ # It cannot parse or validate function call Pythonic or XML-ish syntax.
159
161
  if (
160
- not isinstance(self.detector, PythonicDetector)
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 adapter.py
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
- raise NotImplementedError()
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
- raise NotImplementedError()
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
- Assumes function call format:
21
- <|python_tag|>{"name":"xxx", "arguments":{...}}
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 models.
21
- Assumes function call format:
22
- [TOOL_CALLS] [{"name":"func1", "arguments":{...}}, {"name":"func2", "arguments":{...}}]
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):