| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | """DeepSeek V3.2 parser: DSML tool calls with ``function_calls`` wrapper. |
| 4 | |
| 5 | DeepSeek V3.2 output format:: |
| 6 | |
| 7 | <|DSML|function_calls> |
| 8 | <|DSML|invoke name="func_name"> |
| 9 | <|DSML|parameter name="location" string="true">杭州</|DSML|parameter> |
| 10 | <|DSML|parameter name="count" string="false">5</|DSML|parameter> |
| 11 | </|DSML|invoke> |
| 12 | </|DSML|function_calls> |
| 13 | |
| 14 | This is identical to DeepSeek V4 except for the outer wrapper |
| 15 | (``function_calls`` instead of ``tool_calls``) and the absence of |
| 16 | ``<think>``/``</think>`` reasoning tags. |
| 17 | """ |
| 18 | |
| 19 | from __future__ import annotations |
| 20 | |
| 21 | import functools |
| 22 | from typing import TYPE_CHECKING |
| 23 | |
| 24 | from vllm.parser.deepseek_v4 import ( |
| 25 | DSML_INVOKE_END, |
| 26 | DSML_INVOKE_NAME_END, |
| 27 | DSML_INVOKE_PREFIX, |
| 28 | DSML_PARAM_CLOSE, |
| 29 | DSML_PARAM_START, |
| 30 | _dsml_arg_converter, |
| 31 | _unwrap_wrapper_args, |
| 32 | ) |
| 33 | from vllm.parser.engine.events import EventType |
| 34 | from vllm.parser.engine.parser_engine import ParserEngine |
| 35 | from vllm.parser.engine.parser_engine_config import ( |
| 36 | ParserEngineConfig, |
| 37 | ParserState, |
| 38 | Transition, |
| 39 | ) |
| 40 | |
| 41 | if TYPE_CHECKING: |
| 42 | from vllm.tokenizers import TokenizerLike |
| 43 | from vllm.tool_parsers.abstract_tool_parser import Tool |
| 44 | |
| 45 | _DSML = "|DSML|" |
| 46 | |
| 47 | DSML_FUNC_START = f"<{_DSML}function_calls>" |
| 48 | DSML_FUNC_END = f"</{_DSML}function_calls>" |
| 49 | |
| 50 | |
| 51 | @functools.cache |
| 52 | def deepseek_v32_config() -> ParserEngineConfig: |
| 53 | return ParserEngineConfig( |
| 54 | name="deepseek_v32", |
| 55 | initial_state=ParserState.CONTENT, |
| 56 | wait_for_reasoning=False, |
| 57 | terminals={ |
| 58 | "TOOL_START": DSML_FUNC_START, |
| 59 | "TOOL_END": DSML_FUNC_END, |
| 60 | "INVOKE_PREFIX": DSML_INVOKE_PREFIX, |
| 61 | "INVOKE_NAME_END": DSML_INVOKE_NAME_END, |
| 62 | "INVOKE_END": DSML_INVOKE_END, |
| 63 | "PARAM_START": DSML_PARAM_START, |
| 64 | "PARAM_CLOSE": DSML_PARAM_CLOSE, |
| 65 | }, |
| 66 | token_id_terminals={ |
| 67 | "TOOL_START": DSML_FUNC_START, |
| 68 | "TOOL_END": DSML_FUNC_END, |
| 69 | }, |
| 70 | transitions={ |
| 71 | (ParserState.CONTENT, "TOOL_START"): Transition( |
| 72 | ParserState.TOOL_PREAMBLE, |
| 73 | (), |
| 74 | ), |
| 75 | (ParserState.TOOL_PREAMBLE, "INVOKE_PREFIX"): Transition( |
| 76 | ParserState.TOOL_NAME, |
| 77 | (EventType.TOOL_CALL_START,), |
| 78 | ), |
| 79 | (ParserState.CONTENT, "INVOKE_PREFIX"): Transition( |
| 80 | ParserState.TOOL_NAME, |
| 81 | (EventType.TOOL_CALL_START,), |
| 82 | ), |
| 83 | (ParserState.TOOL_NAME, "INVOKE_NAME_END"): Transition( |
| 84 | ParserState.TOOL_ARGS, |
| 85 | (), |
| 86 | ), |
| 87 | (ParserState.TOOL_ARGS, "INVOKE_END"): Transition( |
| 88 | ParserState.TOOL_BETWEEN, |
| 89 | (EventType.TOOL_CALL_END,), |
| 90 | ), |
| 91 | (ParserState.TOOL_ARGS, "TOOL_END"): Transition( |
| 92 | ParserState.TOOL_BETWEEN, |
| 93 | (EventType.TOOL_CALL_END,), |
| 94 | ), |
| 95 | # Parallel tool calls |
| 96 | (ParserState.TOOL_BETWEEN, "INVOKE_PREFIX"): Transition( |
| 97 | ParserState.TOOL_NAME, |
| 98 | (EventType.TOOL_CALL_START,), |
| 99 | ), |
| 100 | # A tool call ends the turn: stay in TOOL_BETWEEN so any text |
| 101 | # after the block is dropped. |
| 102 | (ParserState.TOOL_BETWEEN, "TOOL_END"): Transition( |
| 103 | ParserState.TOOL_BETWEEN, |
| 104 | (), |
| 105 | ), |
| 106 | (ParserState.TOOL_BETWEEN, "TOOL_START"): Transition( |
| 107 | ParserState.TOOL_PREAMBLE, |
| 108 | (), |
| 109 | ), |
| 110 | }, |
| 111 | content_events={ |
| 112 | ParserState.CONTENT: EventType.TEXT_CHUNK, |
| 113 | ParserState.TOOL_NAME: EventType.TOOL_NAME, |
| 114 | ParserState.TOOL_ARGS: EventType.ARG_VALUE_CHUNK, |
| 115 | }, |
| 116 | arg_converter=_dsml_arg_converter, |
| 117 | arg_structural_chars=frozenset(">"), |
| 118 | strip_content_whitespace_with_tools=False, |
| 119 | tool_args_json=False, |
| 120 | ) |
| 121 | |
| 122 | |
| 123 | class DeepSeekV32Parser(ParserEngine): |
| 124 | def __init__( |
| 125 | self, |
| 126 | tokenizer: TokenizerLike, |
| 127 | tools: list[Tool] | None = None, |
| 128 | **kwargs, |
| 129 | ) -> None: |
| 130 | kwargs.pop("chat_template_kwargs", None) |
| 131 | super().__init__( |
| 132 | tokenizer, |
| 133 | tools, |
| 134 | parser_engine_config=deepseek_v32_config(), |
| 135 | **kwargs, |
| 136 | ) |
| 137 | self._arg_converter = self._convert_args |
| 138 | |
| 139 | def _convert_args(self, raw_args: str, partial: bool) -> str: |
| 140 | result = _dsml_arg_converter(raw_args, partial) |
| 141 | if not self._tools: |
| 142 | return result |
| 143 | func_name = next((s.name for s in self._tool_slots if s.args == raw_args), None) |
| 144 | return _unwrap_wrapper_args(result, self._tools, func_name) |