[Model] Support Hy3 token suffix and JSON Schema array types (#47192)

Signed-off-by: stevenkuang-tencent <stevenkuang@tencent.com>

stevenkuang committed Jul 1, 2026 at 18:16 UTC cc56379e28a0600cbabe290508ef49d88cc2afd5
2 files changed +24 -11
vllm/reasoning/hy_v3_reasoning_parser.py
+4 -2
@@ -26,6 +26,8 @@ class HYV3ReasoningParser(BaseThinkingReasoningParser):
26 """
27
28 def __init__(self, tokenizer: TokenizerLike, *args, **kwargs):
29 + init_kwargs = getattr(tokenizer, "init_kwargs", None) or {}
30 + self.suffix: str = init_kwargs.get("token_suffix") or ""
31 super().__init__(tokenizer, *args, **kwargs)
32
33 # First, If there is reasoning_effort in chat_kwargs,
@@ -52,12 +54,12 @@ class HYV3ReasoningParser(BaseThinkingReasoningParser):
54 @property
55 def start_token(self) -> str:
56 """The token that starts reasoning content."""
55 - return "<think>"
57 + return f"<think{self.suffix}>"
58
59 @property
60 def end_token(self) -> str:
61 """The token that ends reasoning content."""
60 - return "</think>"
62 + return f"</think{self.suffix}>"
63
64 def is_reasoning_end(self, input_ids: Sequence[int]) -> bool:
65 if self._identity_parser is not None:
vllm/tool_parsers/hy_v3_tool_parser.py
+20 -9
@@ -108,6 +108,14 @@ class HYV3ToolParser(ToolParser):
108 Note: single ``type`` has the highest priority.
109 """
110 if "type" in arg_schema:
111 + type_val = arg_schema["type"]
112 + # JSON Schema allows "type" to be an array to represent union types,
113 + # e.g. "type": ["string", "object"].
114 + # Expand it into an anyOf-equivalent format:
115 + # [{"type": "string"}, {"type": "object"}]
116 + # so that _get_types / _parse_value can handle it uniformly later.
117 + if isinstance(type_val, list):
118 + return [{"type": t} for t in type_val]
119 return [arg_schema]
120 if "anyOf" in arg_schema:
121 return arg_schema["anyOf"]
@@ -261,19 +269,22 @@ class HYV3ToolParser(ToolParser):
269 self._current_arg_is_string: bool = False # is current arg pure string?
270 self._streamed_json_len: int = 0 # bytes of JSON already sent
271
264 - self.tool_calls_start_token: str = "<tool_calls>"
265 - self.tool_calls_end_token: str = "</tool_calls>"
272 + init_kwargs = getattr(tokenizer, "init_kwargs", None) or {}
273 + self.suffix: str = init_kwargs.get("token_suffix") or ""
274
267 - self.tool_call_start_token: str = "<tool_call>"
268 - self.tool_call_end_token: str = "</tool_call>"
275 + self.tool_calls_start_token: str = f"<tool_calls{self.suffix}>"
276 + self.tool_calls_end_token: str = f"</tool_calls{self.suffix}>"
277
270 - self.tool_sep_token: str = "<tool_sep>"
278 + self.tool_call_start_token: str = f"<tool_call{self.suffix}>"
279 + self.tool_call_end_token: str = f"</tool_call{self.suffix}>"
280
272 - self.arg_key_start_token: str = "<arg_key>"
273 - self.arg_key_end_token: str = "</arg_key>"
281 + self.tool_sep_token: str = f"<tool_sep{self.suffix}>"
282
275 - self.arg_value_start_token: str = "<arg_value>"
276 - self.arg_value_end_token: str = "</arg_value>"
283 + self.arg_key_start_token: str = f"<arg_key{self.suffix}>"
284 + self.arg_key_end_token: str = f"</arg_key{self.suffix}>"
285 +
286 + self.arg_value_start_token: str = f"<arg_value{self.suffix}>"
287 + self.arg_value_end_token: str = f"</arg_value{self.suffix}>"
288
289 self.tool_call_regex = re.compile(
290 rf"{self.tool_call_start_token}(.*?){self.tool_sep_token}"