Skip to content

vllm.parser.qwen3

Qwen3 parser for tool calls and reasoning.

Qwen3 XML tool call format::

<tool_call>
<function=func_name>
<parameter=key>value</parameter>
</function>
</tool_call>

The argument body consists of <parameter=NAME>VALUE</parameter> tags. The _qwen3_arg_converter parses these into a JSON object.

Classes:

  • Qwen3Parser –

    Qwen3 parser: <think>/</think> reasoning +

Qwen3Parser

Bases: ParserEngine

Qwen3 parser: <think>/</think> reasoning + <tool_call> XML tool calls in a single engine.

  • <tool_call> as implicit reasoning end (a grammar transition, so it also feeds is_reasoning_end and the structured-output gate)

Subclasses that share the grammar but differ only in the four wrapper token strings (reasoning + tool-call) override the class attributes below; everything else is inherited unchanged.

Source code in vllm/parser/qwen3.py
class Qwen3Parser(ParserEngine):
    """Qwen3 parser: ``<think>``/``</think>`` reasoning +
    ``<tool_call>`` XML tool calls in a single engine.

    - ``<tool_call>`` as implicit reasoning end (a grammar transition, so it
      also feeds ``is_reasoning_end`` and the structured-output gate)

    Subclasses that share the grammar but differ only in the four wrapper
    token strings (reasoning + tool-call) override the class attributes
    below; everything else is inherited unchanged.
    """

    CONFIG_NAME = "qwen3"
    THINK_START = THINK_START
    THINK_END = THINK_END
    TOOL_START = TOOL_CALL_START
    TOOL_END = TOOL_CALL_END
    TURN_BOUNDARIES: frozenset[str] = CHATML_TURN_BOUNDARIES

    def __init__(
        self,
        tokenizer: TokenizerLike,
        tools: list[Tool] | None = None,
        **kwargs,
    ) -> None:
        chat_kwargs = kwargs.get("chat_template_kwargs", {}) or {}
        self.thinking_enabled = chat_kwargs.get("enable_thinking", True)
        kwargs.setdefault(
            "parser_engine_config",
            qwen3_config(
                thinking=self.thinking_enabled,
                name=self.CONFIG_NAME,
                think_start=self.THINK_START,
                think_end=self.THINK_END,
                tool_start=self.TOOL_START,
                tool_end=self.TOOL_END,
                turn_boundary_tokens=self.TURN_BOUNDARIES,
            ),
        )
        super().__init__(
            tokenizer,
            tools,
            **kwargs,
        )
        # Each streaming conversion resumes after the last closed parameter.
        if self._arg_converter is _qwen3_arg_converter:
            closed = _ClosedParams()
            self._arg_converter = functools.partial(_qwen3_arg_converter, closed=closed)
            self._arg_value_extender = functools.partial(
                _qwen3_arg_value_extender, closed=closed
            )

    def extract_reasoning(
        self,
        model_output: str,
        request: ChatCompletionRequest | ResponsesRequest,
    ) -> tuple[str | None, str | None]:
        if not self.thinking_enabled:
            return None, model_output
        return super().extract_reasoning(model_output, request)

_ClosedParams

Closed parameters of a raw argument text, reused by longer texts.

Raw arguments only grow while a call streams, so a parse of a text that extends text resumes after its last closed parameter.

Source code in vllm/parser/qwen3.py
class _ClosedParams:
    """Closed parameters of a raw argument text, reused by longer texts.

    Raw arguments only grow while a call streams, so a parse of a text that
    extends ``text`` resumes after its last closed parameter.
    """

    __slots__ = ("text", "end", "params", "outside")

    def __init__(self) -> None:
        self.text = ""  # raw text the closed parameters depend on
        self.end = 0  # end of the last closed parameter in ``text``
        self.params: dict[str, str] = {}
        self.outside = ""  # text before ``end`` outside the closed parameters

_param_value_end_window(text)

Return the suffix of text that may still grow into a value end.

Source code in vllm/parser/qwen3.py
def _param_value_end_window(text: str) -> str:
    """Return the suffix of *text* that may still grow into a value end."""
    start = text.rfind("<")
    if start == -1:
        return ""
    tail = text[start:]
    return tail if _PARAM_VALUE_END_RE.fullmatch(tail, partial=True) else ""

_param_value_extender(raw_args, trim_newlines, closed=None)

Extend the open trailing <parameter=...> value of raw_args.

The extender streams exactly what the converter would for the same text. With trim_newlines it follows _trim_wrapping_newlines: it drops a leading newline of the value and holds back a trailing one.

Source code in vllm/parser/qwen3.py
def _param_value_extender(
    raw_args: str, trim_newlines: bool, closed: _ClosedParams | None = None
) -> ArgValueExtender | None:
    """Extend the open trailing ``<parameter=...>`` value of *raw_args*.

    The extender streams exactly what the converter would for the same text.
    With *trim_newlines* it follows ``_trim_wrapping_newlines``: it drops a
    leading newline of the value and holds back a trailing one.
    """
    closed_params, match = _qwen3_parse_params(raw_args, True, closed)
    if match is None:
        return None
    name, value = match.group(1), match.group(2)
    # A repeated key keeps its first position, so its value is not last.
    if name in closed_params:
        return None

    window = _param_value_end_window(value)
    started = bool(value)
    held = "\n" if trim_newlines and value.endswith("\n") and value != "\n" else ""

    def extend(delta: str) -> str | None:
        nonlocal window, started, held
        text = window + delta
        if _PARAM_VALUE_END_RE.search(text):
            return None
        window = _param_value_end_window(text)
        if trim_newlines:
            text = held + delta
            if not started and text:
                started = True
                text = text.removeprefix("\n")
            held = "\n" if text.endswith("\n") else ""
            text = text[: len(text) - len(held)]
        else:
            text = delta
        return json.dumps(text, ensure_ascii=False)[1:-1]

    return extend

_qwen3_parse_params(raw_args, partial, closed=None)

Return the closed parameters, trimmed, and the open trailing one.

Source code in vllm/parser/qwen3.py
def _qwen3_parse_params(
    raw_args: str, partial: bool, closed: _ClosedParams | None = None
) -> tuple[dict[str, str], re.Match | None]:
    """Return the closed parameters, trimmed, and the open trailing one."""
    params: dict[str, str] = {}
    outside: list[str] = []
    pos = 0
    if closed is not None and raw_args.startswith(closed.text):
        params.update(closed.params)
        outside.append(closed.outside)
        pos = closed.end
    start = pos

    for match in _PARAM_RE.finditer(raw_args, pos):
        outside.append(raw_args[pos : match.start()])
        params[match.group(1)] = _trim_wrapping_newlines(match.group(2))
        pos = match.end()

    if closed is not None and pos > start:
        # A value closed by the next opener holds only while that opener follows.
        opener = _PARAM_OPENER_RE.match(raw_args, pos)
        closed.text = raw_args[: opener.end() if opener else pos]
        closed.end = pos
        closed.params = dict(params)
        closed.outside = "".join(outside)

    open_param = None
    if partial:
        open_param = _PARTIAL_PARAM_RE.search("".join(outside) + raw_args[pos:])
    return params, open_param

_trim_wrapping_newlines(value)

Strip one leading and one trailing newline (the Qwen3 template markup).

Source code in vllm/parser/qwen3.py
def _trim_wrapping_newlines(value: str) -> str:
    """Strip one leading and one trailing newline (the Qwen3 template markup)."""
    if value.startswith("\n"):
        value = value[1:]
    if value.endswith("\n"):
        value = value[:-1]
    return value