2
其他
新人贴
K100AI 八卡服务器修改官方镜像,支持 MiniMax-M2 Tool Call & Reasoning(vLLM 0.11.0)
2026-01-26 11:12:16
398
## 一、问题背景 镜像: image.sourcefind.cn:5000/dcu/admin/base/custom:vllm-ubuntu22.04-dtk25.04.2-py3.10-minimax-m2 模型: https://huggingface.co/MiniMaxAI/MiniMax-M2 在 **K100AI(海光 DCU)八卡服务器** 上,直接使用官方 vLLM 镜像部署 **MiniMax-M2**,会遇到以下问题之一: * Tool call **无法被正确解析** * Reasoning(think 段)与 tool call **混乱 / 丢失** * 多卡(8 卡)下 **TP=8 不可用** 解决办法: > **官方镜像可用,但需要针对 M2 + DCU 做tool call parser和thinking parser对vllm 0110版本的回退支持,否则 Agent 能力不可用。** ## 二、官方镜像的问题点(核心) 1. **未启用 auto tool choice** 2. **默认 MiniMax-M2 parser 为vllm高版本, 0110版本语法不适配** 3. **默认 reasoning parser 为vllm高版本, 0110版本语法不适配** 4. **config.json中对应的量化配置仍然存在** ## 三、修改思路 ### 1. 镜像回退适配 vLLM 0.11.0 的 parser 目标:在 0.11.0 中可用 `minimax_m2` tool parser 与 `minimax_m2_append_think` reasoning parser。(源文件见评论区, 一共三份文件) 关键修改: * 工具解析器放到 `vllm/entrypoints/openai/tool_parsers/` * 统一使用 0.11.0 的协议导入:`vllm.entrypoints.openai.protocol` * 通过 `ToolParserManager.register_module("minimax_m2")` 注册 * `__init__.py` 显式导入新 parser ### 2. 启用 MiniMax-M2 专用解析器 必须显式指定: ```bash --enable-auto-tool-choice \ --tool-call-parser minimax_m2 \ --reasoning-parser minimax_m2_append_think ``` ### 3.删除模型配置文件config.json中对应的量化配置 ```json //这一块全部删除掉 { "quantization": "..." } ``` ## 四、最终可用启动命令 ```bash vllm serve MiniMaxAI/MiniMax-M2 \ --data-parallel-size 8 \ --enable-expert-parallel \ --enable-auto-tool-choice \ --tool-call-parser minimax_m2 \ --reasoning-parser minimax_m2_append_think \ ``` ### 能力验证结果 * ✅ Chat 正常 * ✅ Tool call 可被正确结构化解析 * ✅ Reasoning(think)段稳定 * ✅ 多轮 Agent 调用可用
全部评论(16)
只看楼主
网
网友B3ZN40
8楼
 现在部署是按这个方法还是直接用官方https://developer.sourcefind.cn/codes/modelzoo/minimax-m2_vllm的方法更好?

3/31
樱
樱桃大球球
7楼
attachmentId-image.png
换成2.1 速度可以最大提升到~30,不错不错。
不知道2.5 是不是可以直接使用。怕vllm不兼容,暂时没试。
3/6
樱
樱桃大球球
6楼
感谢楼主,楼主提供的代码中有一些双下划线的变量被替换掉了。比如
`__init__` 被替换成了 `init` 导致失败,但是修改过代码之后能用。
实测输出大概在18~19 tokens/s。而且必须参考 https://developer.sourcefind.cn/modelzoo/list/minimax-m2_vllm/detail?post_id=66b4acaf-cc26-11f0-a654-0242ac150003&active=ModelOverview 把模型转换一下。不知道楼主有没有进行转换。实测如果使用原版权重会报错 `24layerxxx`(具体记不得了)如果楼主看到了,希望把全过程都贴一下。在此感谢楼主无私分享。
2/14
樱
樱桃大球球
回复 @樱桃大球球
另外实测当前能pull的 镜像 `image.sourcefind.cn:5000/dcu/admin/base/custom:vllm-ubuntu22.04-dtk25.04.2-py3.10-minimax-m2` 已经包含了楼主说的这两个文件,但是和楼主提供的文件并不一致,附上我的Dockerfile 以及compose.yaml
有什么错误或者可优化之处请大神指出。希望也能达到150 tokens/s

```
FROM image.sourcefind.cn:5000/dcu/admin/base/custom:vllm-ubuntu22.04-dtk25.04.2-py3.10-minimax-m2
COPY . .
RUN mv minimax_m2_tool_parser.py /usr/local/lib/python3.10/dist-packages/vllm/entrypoints/openai/tool_parsers/minimax_m2_tool_parser.py
RUN mv minimax_m2_reasoning_parser.py /usr/local/lib/python3.10/dist-packages/vllm/reasoning/minimax_m2_reasoning_parser.py
```
把两个文件放在当前目录,然后执行 `docker build -t newminimax .`

```
services:
  minimax:
    image: newminimax
    privileged: true
    network_mode: host
    container_name: minimax
    ipc: host
    restart: unless-stopped
    devices:
      - /dev/kfd
      - /dev/dri
    cap_add:
      - SYS_PTRACE
    security_opt:
      - seccomp=unconfined
    environment:
      HIP_VISIBLE_DEVICES: "0,1,2,3,4,5,6,7"
      VLLM_USE_NN: "0"
      TRITON_MOVE_LOAD_TOFRONT_DOT: "0"
      HF_HUB_OFFLINE: "1"
      VLLM_USE_FLASHINFER_MOE_FP16: "1"
      SAFETENSORS_FAST_GPU: "1"
    volumes:
      - /opt/hyhal:/opt/hyhal:ro
      - /srv/disk1/models/MiniMax-M2-bf16:/app/models/MiniMax-M2
    command: [
      "vllm", "serve",
      "/app/models/MiniMax-M2",
      "--host", "0.0.0.0",
      "--trust_remote_code",
      "--enable_expert_parallel",
      "--served-model-name", "MiniMax-M2",
      "--port", "8000",
      "--gpu-memory-utilization", "0.92",
      "--max-num-seqs", "1",
      "--max_model_len", "65536",
      "--tool-call-parser", "minimax_m2",
      "--reasoning-parser", "minimax_m2_append_think",
      "--enable-auto-tool-choice",
      "--tensor-parallel-size", "8"
    ]
```
attachmentId-image.png
2/14
杨
杨涌全
回复 @樱桃大球球
你好,  官方的镜像也能用,本质上是一样的, 因为官方的vllm镜像更新的逻辑是增量支持具体的模型, 所以他们做的事情和我的事情是一样的. 具体如何到150t/s, 肯定不是单个请求的150t/s哈, 是多个请求进来了之后, 多个专家都在跑的时候的总的吞吐量. 我这边单个的吞吐量是和你差不多的.
5/11
吕
吕达
5楼
文中提到使用的镜像是custom:vllm-ubuntu22.04-dtk25.04.2-py3.10-minimax-m2,这个镜像内的vllm是0.9.2的版本,后面又提到需要适配到0.11.0,目前能找到的0.11.0就是vllm:0.11.0-ubuntu22.04-dtk25.04.2-1226-das1.7-py3.10-20251226这个版本,请问楼主具体是使用的哪个版本的
1/29
Y
YUANCHUN
回复 @吕达
当前minimax-m2.1已经支持相关修改,可以参考https://developer.sourcefind.cn/modelzoo/list/minimax-m2_vllm/detail?post_id=66b4acaf-cc26-11f0-a654-0242ac150003&active=ModelOverview 最新内容。
1/29
吕
吕达
回复 @YUANCHUN
安装文档方案确实可以推理,不过我这里测试了,8卡K100AI 推理速度 (APIServer pid=26850) INFO 01-29 10:36:23 [loggers.py:127] Engine 000: Avg prompt throughput: 0.0 tokens/s, Avg generation throughput: 6.4 tokens/s, Running: 1 reqs, Waiting: 0 reqs, GPU KV cache usage: 4.6%, Prefix cache hit rate: 63.3% 一直保持在6.6 tokens/s 看到这里的楼主说 150tokens,我才想试一下这个方案 
1/29
杨
杨涌全
回复 @吕达
单个请求肯定没有150这么高, 因为本身是moe的架构的, moe需要增加请求量,把所有专家打满之后, 才能够看到peak perf
5/9
C
codingmiao
4楼
请问下8卡大概可以跑到每秒多少tokens
1/27
杨
杨涌全
回复 @codingmiao
150左右
1/27
网
网友B3ZN40
回复 @杨涌全
150是说单个请求吗?
3/31
杨
杨涌全
回复 @网友B3ZN40
肯定不是呀, 多个请求之后的peak perf
5/9
杨
杨涌全
楼主
3楼
vllm/reasoning/minimax_m2_reasoning_parser.py 直接替换: # SPDX-License-Identifier: Apache-2.0

# SPDX-FileCopyrightText: Copyright contributors to the vLLM project


from collections.abc import Sequence


from vllm.entrypoints.openai.protocol import (ChatCompletionRequest,

                                              DeltaMessage, ResponsesRequest)

from vllm.reasoning.abs_reasoning_parsers import ReasoningParserManager

from vllm.reasoning.abs_reasoning_parsers import ReasoningParser

from vllm.reasoning.basic_parsers import BaseThinkingReasoningParser

from vllm.transformers_utils.tokenizer import AnyTokenizer


@ReasoningParserManager.register_module("minimazax_m2")

class MiniMaxM2ReasoningParser(BaseThinkingReasoningParser):

    """

    Reasoning parser for MiniMax M2 model.


    MiniMax M2 models don't generate <think> start token, only </think> end

    token. All content before </think> is reasoning, content after is the

    actual response.

    """


    @property

    def start_token(self) -> str:

        """The token that starts reasoning content."""

        return "<think>"


    @property

    def end_token(self) -> str:

        """The token that ends reasoning content."""

        return "</think>"


    # NOTE: vLLM serving code calls `extract_reasoning_content_streaming`.

    # Keep `extract_reasoning_streaming` as a compatibility alias since some

    # forks/older branches may still call it.

    def extract_reasoning_content_streaming(

        self,

        previous_text: str,

        current_text: str,

        delta_text: str,

        previous_token_ids: Sequence[int],

        current_token_ids: Sequence[int],

        delta_token_ids: Sequence[int],

    ) -> DeltaMessage | None:

        """

        Extract reasoning content from a delta message for streaming.


        MiniMax M2 models don't generate <think> start token, so we assume

        all content is reasoning until we encounter the </think> end token.

        """

        # Skip single end token

        if len(delta_token_ids) == 1 and delta_token_ids[0] == self.end_token_id:

            return None


        # Check if end token has already appeared in previous tokens

        # meaning we're past the reasoning phase

        if self.end_token_id in previous_token_ids:

            # We're past the reasoning phase, this is content

            return DeltaMessage(content=delta_text)


        # Check if end token is in delta tokens

        if self.end_token_id in delta_token_ids:

            # End token in delta, split reasoning and content

            end_index = delta_text.find(self.end_token)

            reasoning = delta_text[:end_index]

            content = delta_text[end_index + len(self.end_token) :]

            return DeltaMessage(

                reasoning_content=reasoning if reasoning else None,

                content=content if content else None,

            )


        # No end token yet, all content is reasoning

        return DeltaMessage(reasoning_content=delta_text)


    def extract_reasoning_streaming(

        self,

        previous_text: str,

        current_text: str,

        delta_text: str,

        previous_token_ids: Sequence[int],

        current_token_ids: Sequence[int],

        delta_token_ids: Sequence[int],

    ) -> DeltaMessage | None:

        return self.extract_reasoning_content_streaming(

            previous_text,

            current_text,

            delta_text,

            previous_token_ids,

            current_token_ids,

            delta_token_ids,

        )



@ReasoningParserManager.register_module("minimax_m2_append_think")

class MiniMaxM2AppendThinkReasoningParser(ReasoningParser):

    """

    Reasoning parser for MiniMax M2 model.

    """


    def __init__(self, tokenizer: AnyTokenizer, *args, **kwargs):

        super().__init__(tokenizer, *args, **kwargs)

        self.end_token = "</think>"

        self.end_token_id = self.vocab.get(self.end_token)

        if self.end_token_id is None:

            raise RuntimeError(

                f"{self.__class__.__name__} reasoning parser could not locate "

                f"{self.end_token} token in the tokenizer!")


    def is_reasoning_end(self, input_ids: list[int]) -> bool:

        end_token_id = self.end_token_id

        return any(input_id == end_token_id for input_id in reversed(input_ids))


    def extract_content_ids(self, input_ids: list[int]) -> list[int]:

        return input_ids


    # NOTE: vLLM serving code calls `extract_reasoning_content_streaming`.

    def extract_reasoning_content_streaming(

        self,

        previous_text: str,

        current_text: str,

        delta_text: str,

        previous_token_ids: Sequence[int],

        current_token_ids: Sequence[int],

        delta_token_ids: Sequence[int],

    ) -> DeltaMessage | None:

        if len(previous_token_ids) == 0:

            delta_text = "<think>" + delta_text

        return DeltaMessage(content=delta_text)


    def extract_reasoning_streaming(

        self,

        previous_text: str,

        current_text: str,

        delta_text: str,

        previous_token_ids: Sequence[int],

        current_token_ids: Sequence[int],

        delta_token_ids: Sequence[int],

    ) -> DeltaMessage | None:

        return self.extract_reasoning_content_streaming(

            previous_text,

            current_text,

            delta_text,

            previous_token_ids,

            current_token_ids,

            delta_token_ids,

        )


    # NOTE: vLLM serving code calls `extract_reasoning_content` and expects a

    # (reasoning_content, content) tuple. This parser keeps everything in

    # `content` but prefixes "<think>".

    def extract_reasoning_content(

        self, model_output: str, request: ChatCompletionRequest | ResponsesRequest

    ) -> tuple[str | None, str | None]:

        return None, "<think>" + model_output


    def extract_reasoning(

        self, model_output: str, request: ChatCompletionRequest | ResponsesRequest

    ) -> tuple[str | None, str | None]:

        return self.extract_reasoning_content(model_output, request)


1/26
杨
杨涌全
楼主
2楼
vllm/entrypoints/openai/tool_parsers/minimax_m2_tool_parser.py 直接替换: # SPDX-License-Identifier: Apache-2.0

# SPDX-FileCopyrightText: Copyright contributors to the vLLM project


import json

import uuid

from collections.abc import Sequence

from typing import Any


import regex as re


from vllm.entrypoints.openai.protocol import (ChatCompletionRequest,

                                              DeltaFunctionCall, DeltaMessage,

                                              DeltaToolCall,

                                              ExtractedToolCallInformation,

                                              FunctionCall, ToolCall)

from vllm.logger import init_logger

from vllm.entrypoints.openai.tool_parsers.abstract_tool_parser import (

    ToolParser, ToolParserManager)

from vllm.transformers_utils.tokenizer import AnyTokenizer


logger = init_logger(__name__)



@ToolParserManager.register_module("minimax_m2")

class MinimaxM2ToolParser(ToolParser):

    def __init__(self, tokenizer: AnyTokenizer):

        super().__init__(tokenizer)


        self.prev_tool_call_arr: list[dict] = []


        # Sentinel tokens

        self.tool_call_start_token: str = "<minimax:tool_call>"

        self.tool_call_end_token: str = "</minimax:tool_call>"

        self.invoke_start_prefix: str = "<invoke name="

        self.invoke_end_token: str = "</invoke>"

        self.parameter_prefix: str = "<parameter name="

        self.parameter_end_token: str = "</parameter>"


        # Streaming state variables

        self.current_tool_name_sent: bool = False

        # Override base class type - we use string IDs for tool calls

        self.current_tool_id: str | None = None # type: ignore

        self.streamed_args_for_tool: list[str] = []

        self.is_tool_call_started: bool = False

        self.failed_count: int = 0


        # Initialize streaming state variables

        self.current_tool_index: int = 0

        self.invoke_index: int = 0

        self.header_sent: bool = False

        self.current_function_name: str | None = None

        self.current_param_name: str | None = None

        self.current_param_value: str = ""

        self.param_count: int = 0

        self.in_param: bool = False

        self.in_function: bool = False

        self.accumulated_text: str = ""

        self.json_started: bool = False

        self.json_closed: bool = False

        self.accumulated_params: dict = {}

        self.streaming_request: ChatCompletionRequest | None = None


        # Enhanced streaming state - reset for each new message

        self._reset_streaming_state()


        # Regex patterns for complete parsing

        self.tool_call_complete_regex = re.compile(

            r"<minimax:tool_call>(.*?)</minimax:tool_call>", re.DOTALL

        )

        self.invoke_complete_regex = re.compile(

            r"<invoke name=(.*?)</invoke>", re.DOTALL

        )

        self.parameter_complete_regex = re.compile(

            r"<parameter name=(.*?)</parameter>", re.DOTALL

        )


        if not self.model_tokenizer:

            raise ValueError(

                "The model tokenizer must be passed to the ToolParser "

                "constructor during construction."

            )


        self.tool_call_start_token_id = self.vocab.get(self.tool_call_start_token)

        self.tool_call_end_token_id = self.vocab.get(self.tool_call_end_token)


        if self.tool_call_start_token_id is None or self.tool_call_end_token_id is None:

            raise RuntimeError(

                "MiniMax M2 Tool parser could not locate tool call start/end "

                "tokens in the tokenizer!"

            )


        logger.debug(

            "vLLM Successfully import tool parser %s !", self.__class__.__name__

        )


    def _generate_tool_call_id(self) -> str:

        """Generate a unique tool call ID."""

        return f"call_{uuid.uuid4().hex[:24]}"


    def _reset_streaming_state(self):

        """Reset all streaming state."""

        self.current_tool_index = 0

        self.invoke_index = 0

        self.is_tool_call_started = False

        self.header_sent = False

        self.current_tool_id = None

        self.current_function_name = None

        self.current_param_name = None

        self.current_param_value = ""

        self.param_count = 0

        self.in_param = False

        self.in_function = False

        self.accumulated_text = ""

        self.json_started = False

        self.json_closed = False

        # Store accumulated parameters for type conversion

        self.accumulated_params = {}

        self.streaming_request = None

        # Clear previous tool call history to avoid state pollution

        self.prev_tool_call_arr.clear()

        # Reset streamed args tracking

        self.streamed_args_for_tool.clear()


    def _extract_name(self, name_str: str) -> str:

        """Extract name from quoted string."""

        name_str = name_str.strip()

        if (

            name_str.startswith('"')

            and name_str.endswith('"')

            or name_str.startswith("'")

            and name_str.endswith("'")

        ):

            return name_str[1:-1]

        return name_str


    def _convert_param_value(self, value: str, param_type: str) -> Any:

        """Convert parameter value to the correct type (legacy single-type version)."""

        return self._convert_param_value_with_types(value, [param_type])


    def _extract_types_from_schema(self, schema: Any) -> list[str]:

        """

        Extract all possible types from a JSON schema definition.

        Handles anyOf, oneOf, allOf, type arrays, and enum fields.


        Args:

            schema: The JSON schema definition for a parameter


        Returns:

            List of type strings (e.g., ["string", "integer", "null"])

        """

        if schema is None:

            return ["string"]


        if not isinstance(schema, dict):

            return ["string"]


        types: set[str] = set()


        # Handle direct "type" field

        if "type" in schema:

            type_value = schema["type"]

            if isinstance(type_value, str):

                types.add(type_value)

            elif isinstance(type_value, list):

                for t in type_value:

                    if isinstance(t, str):

                        types.add(t)


        # Handle enum - infer types from enum values

        if "enum" in schema and isinstance(schema["enum"], list) and schema["enum"]:

            for value in schema["enum"]:

                if value is None:

                    types.add("null")

                elif isinstance(value, bool):

                    types.add("boolean")

                elif isinstance(value, int):

                    types.add("integer")

                elif isinstance(value, float):

                    types.add("number")

                elif isinstance(value, str):

                    types.add("string")

                elif isinstance(value, list):

                    types.add("array")

                elif isinstance(value, dict):

                    types.add("object")


        # Handle anyOf, oneOf, allOf - recursively extract types

        for choice_field in ("anyOf", "oneOf", "allOf"):

            if choice_field in schema and isinstance(schema[choice_field], list):

                for choice in schema[choice_field]:

                    extracted = self._extract_types_from_schema(choice)

                    types.update(extracted)


        # If no types found, default to string

        if not types:

            return ["string"]


        return list(types)


    def _convert_param_value_with_types(

        self, value: str, param_types: list[str]

    ) -> Any:

        """

        Convert parameter value to the correct type based on a list of possible types.

        Tries each type in order until one succeeds.


        Args:

            value: The string value to convert

            param_types: List of possible type strings


        Returns:

            The converted value

        """

        # Check if the VALUE itself indicates null (not just if null is allowed)

        if value.lower() in ("null", "none", "nil"):

            return None


        # Normalize types

        normalized_types = [t.lower() for t in param_types]


        # Try each type in order of preference (most specific first, string as fallback)

        # Priority: integer > number > boolean > object > array > string

        type_priority = [

            "integer",

            "int",

            "number",

            "float",

            "boolean",

            "bool",

            "object",

            "array",

            "string",

            "str",

            "text",

        ]


        for param_type in type_priority:

            if param_type not in normalized_types:

                continue


            if param_type in ["string", "str", "text"]:

                return value

            elif param_type in ["integer", "int"]:

                try:

                    return int(value)

                except (ValueError, TypeError):

                    continue

            elif param_type in ["number", "float"]:

                try:

                    val = float(value)

                    return val if val != int(val) else int(val)

                except (ValueError, TypeError):

                    continue

            elif param_type in ["boolean", "bool"]:

                lower_val = value.lower().strip()

                if lower_val in ["true", "1", "yes", "on"]:

                    return True

                elif lower_val in ["false", "0", "no", "off"]:

                    return False

                continue

            elif param_type in ["object", "array"]:

                try:

                    return json.loads(value)

                except json.JSONDecodeError:

                    continue


        # Fallback: try JSON parse, then return as string

        try:

            return json.loads(value)

        except json.JSONDecodeError:

            return value


    def _get_param_types_from_config(

        self, param_name: str, param_config: dict

    ) -> list[str]:

        """

        Get parameter types from parameter configuration.

        Handles anyOf, oneOf, allOf, and direct type definitions.


        Args:

            param_name: The name of the parameter

            param_config: The properties dict from the tool schema


        Returns:

            List of type strings

        """

        if param_name not in param_config:

            return ["string"]


        param_schema = param_config[param_name]

        if not isinstance(param_schema, dict):

            return ["string"]


        return self._extract_types_from_schema(param_schema)


    def _parse_single_invoke(

        self, invoke_str: str, tools: list | None

    ) -> ToolCall | None:

        """Parse a single <invoke> block."""

        # Extract function name

        name_match = re.search(r"^([^>]+)", invoke_str)

        if not name_match:

            return None


        function_name = self._extract_name(name_match.group(1))


        # Get parameter configuration

        param_config = {}

        if tools:

            for tool in tools:

                if (

                    hasattr(tool, "function")

                    and tool.function.name == function_name

                    and hasattr(tool.function, "parameters")

                ):

                    params = tool.function.parameters

                    if isinstance(params, dict) and "properties" in params:

                        param_config = params["properties"]

                    break


        # Extract parameters

        param_dict = {}

        for match in self.parameter_complete_regex.findall(invoke_str):

            param_match = re.search(r"^([^>]+)>(.*)", match, re.DOTALL)

            if param_match:

                param_name = self._extract_name(param_match.group(1))

                param_value = param_match.group(2).strip()

                if param_value.startswith("\n"):

                    param_value = param_value[1:]

                if param_value.endswith("\n"):

                    param_value = param_value[:-1]


                # Get parameter types (supports anyOf/oneOf/allOf)

                param_type = self._get_param_types_from_config(param_name, param_config)


                # Convert value

                param_dict[param_name] = self._convert_param_value_with_types(

                    param_value, param_type

                )


        return ToolCall(

            type="function",

            function=FunctionCall(

                name=function_name,

                arguments=json.dumps(param_dict, ensure_ascii=False),

            ),

        )


    def extract_tool_calls(

        self,

        model_output: str,

        request: ChatCompletionRequest,

    ) -> ExtractedToolCallInformation:

        """Extract tool calls from complete model output (non-streaming)."""

        # Quick check

        if self.tool_call_start_token not in model_output:

            return ExtractedToolCallInformation(

                tools_called=False, tool_calls=[], content=model_output

            )


        try:

            tool_calls = []


            # Find all complete tool_call blocks

            for tool_call_match in self.tool_call_complete_regex.findall(model_output):

                # Find all invokes within this tool_call

                for invoke_match in self.invoke_complete_regex.findall(tool_call_match):

                    tool_call = self._parse_single_invoke(

                        invoke_match, request.tools if request else None

                    )

                    if tool_call:

                        tool_calls.append(tool_call)


            if not tool_calls:

                return ExtractedToolCallInformation(

                    tools_called=False, tool_calls=[], content=model_output

                )


            # Update prev_tool_call_arr

            self.prev_tool_call_arr.clear()

            for tool_call in tool_calls:

                self.prev_tool_call_arr.append(

                    {

                        "name": tool_call.function.name,

                        "arguments": tool_call.function.arguments,

                    }

                )


            # Extract content before first tool call

            first_tool_idx = model_output.find(self.tool_call_start_token)

            content = model_output[:first_tool_idx] if first_tool_idx > 0 else None


            return ExtractedToolCallInformation(

                tools_called=True, tool_calls=tool_calls, content=content

            )


        except Exception:

            logger.exception("Error extracting tool calls")

            return ExtractedToolCallInformation(

                tools_called=False, tool_calls=[], content=model_output

            )


    def extract_tool_calls_streaming(

        self,

        previous_text: str,

        current_text: str,

        delta_text: str,

        previous_token_ids: Sequence[int], # pylint: disable=unused-argument

        current_token_ids: Sequence[int], # pylint: disable=unused-argument

        delta_token_ids: Sequence[int],

        request: ChatCompletionRequest,

    ) -> DeltaMessage | None:

        """Extract tool calls from streaming model output."""


        # Store request for type conversion

        if not previous_text or self.tool_call_start_token in delta_text:

            self._reset_streaming_state()

            self.streaming_request = request


        # If no delta text, return None unless it's an EOS token after tools

        if not delta_text:

            # Check if this is an EOS token after all tool calls are complete

            if delta_token_ids and self.tool_call_end_token_id not in delta_token_ids:

                # Count complete tool calls

                complete_calls = len(

                    self.tool_call_complete_regex.findall(current_text)

                )


                # If we have completed tool calls and populated prev_tool_call_arr

                if complete_calls > 0 and len(self.prev_tool_call_arr) > 0:

                    # Check if all tool calls are closed

                    open_calls = current_text.count(

                        self.tool_call_start_token

                    ) - current_text.count(self.tool_call_end_token)

                    if open_calls == 0:

                        # Return empty delta for finish_reason processing

                        return DeltaMessage(content="")

                elif not self.is_tool_call_started and current_text:

                    # This is a regular content response that's now complete

                    return DeltaMessage(content="")

            return None


        # Update accumulated text

        self.accumulated_text = current_text


        # Check if we need to advance to next tool

        if self.json_closed and not self.in_function:

            # Check if this tool call has ended

            invoke_ends = current_text.count(self.invoke_end_token)

            if invoke_ends > self.current_tool_index:

                # This tool has ended, advance to next

                self.current_tool_index += 1

                self.header_sent = False

                self.param_count = 0

                self.json_started = False

                self.json_closed = False

                self.in_function = False # Now we can safely set this to False

                self.accumulated_params = {}

                # Continue processing next tool

                return None


        # Handle normal content before tool calls

        if not self.is_tool_call_started:

            # Check if tool call is starting

            if (

                self.tool_call_start_token_id in delta_token_ids

                or self.tool_call_start_token in delta_text

            ):

                self.is_tool_call_started = True

                # Return any content before the tool call

                if self.tool_call_start_token in delta_text:

                    content_before = delta_text[

                        : delta_text.index(self.tool_call_start_token)

                    ]

                    if content_before:

                        return DeltaMessage(content=content_before)

                return None

            else:

                # Check if we're between tool calls - skip whitespace

                if (

                    current_text.rstrip().endswith(self.tool_call_end_token)

                    and delta_text.strip() == ""

                ):

                    # We just ended a tool call, skip whitespace

                    return None

                # Normal content, no tool call

                return DeltaMessage(content=delta_text)


        # Check if we're between tool calls (waiting for next one)

        invoke_starts_count = current_text.count(self.invoke_start_prefix)

        if self.current_tool_index >= invoke_starts_count:

            # We're past all tool calls, shouldn't be here

            return None


        # Find the current tool call portion

        invoke_start_positions: list[int] = []

        idx = 0

        while True:

            idx = current_text.find(self.invoke_start_prefix, idx)

            if idx == -1:

                break

            invoke_start_positions.append(idx)

            idx += len(self.invoke_start_prefix)


        if self.current_tool_index >= len(invoke_start_positions):

            # No more tool calls to process yet

            return None


        invoke_start_idx = invoke_start_positions[self.current_tool_index]

        # Find where this tool call ends (or current position if not ended yet)

        invoke_end_idx = current_text.find(self.invoke_end_token, invoke_start_idx)

        if invoke_end_idx == -1:

            tool_text = current_text[invoke_start_idx:]

        else:

            tool_text = current_text[

                invoke_start_idx : invoke_end_idx + len(self.invoke_end_token)

            ]


        # Looking for function header

        if not self.header_sent:

            if self.invoke_start_prefix in tool_text:

                func_start = tool_text.find(self.invoke_start_prefix) + len(

                    self.invoke_start_prefix

                )

                # Find the end quote for the function name

                func_end = tool_text.find(">", func_start)


                if func_end != -1:

                    # Found complete function name

                    function_name_raw = tool_text[func_start:func_end]

                    self.current_function_name = self._extract_name(function_name_raw)

                    self.current_tool_id = self._generate_tool_call_id()

                    self.header_sent = True

                    self.in_function = True


                    # Add to prev_tool_call_arr immediately when we detect a tool call

                    # Each tool call should be recorded regardless of function name

                    # Ensure we don't add the same tool call index multiple times

                    if len(self.prev_tool_call_arr) <= self.current_tool_index:

                        self.prev_tool_call_arr.append(

                            {

                                "name": self.current_function_name,

                                "arguments": {}, # Placeholder, will be updated later

                            }

                        )

                        # Initialize streamed_args_for_tool for this tool call

                        if len(self.streamed_args_for_tool) <= self.current_tool_index:

                            self.streamed_args_for_tool.append("")


                    # Send header with function info

                    return DeltaMessage(

                        tool_calls=[

                            DeltaToolCall(

                                index=self.current_tool_index,

                                id=self.current_tool_id,

                                function=DeltaFunctionCall(

                                    name=self.current_function_name, arguments=""

                                ),

                                type="function",

                            )

                        ]

                    )

            return None


        # We've sent header, now handle function body

        if self.in_function:

            # Send opening brace if not sent yet

            if self.in_function and not self.json_started:

                self.json_started = True

                # Update streamed_args_for_tool for opening brace

                if self.current_tool_index < len(self.streamed_args_for_tool):

                    self.streamed_args_for_tool[self.current_tool_index] += "{"

                return DeltaMessage(

                    tool_calls=[

                        DeltaToolCall(

                            index=self.current_tool_index,

                            function=DeltaFunctionCall(arguments="{"),

                        )

                    ]

                )


            # Make sure json_started is set if we're processing parameters

            if not self.json_started:

                self.json_started = True


            # Check for function end in accumulated text

            if not self.json_closed and self.invoke_end_token in tool_text:

                # Count total parameters in the tool text

                total_param_count = tool_text.count(self.parameter_prefix)


                # Only close JSON if all parameters have been processed

                if self.param_count >= total_param_count:

                    # Close JSON

                    self.json_closed = True


                    # Extract complete tool call

                    # Find the invoke content

                    invoke_start = tool_text.find(self.invoke_start_prefix) + len(

                        self.invoke_start_prefix

                    )

                    invoke_content_end = tool_text.find(

                        self.invoke_end_token, invoke_start

                    )

                    if invoke_content_end != -1:

                        invoke_content = tool_text[invoke_start:invoke_content_end]

                        # Parse to get the complete arguments

                        try:

                            parsed_tool = self._parse_single_invoke(

                                invoke_content,

                                self.streaming_request.tools

                                if self.streaming_request

                                else None,

                            )

                            if parsed_tool and self.current_tool_index < len(

                                self.prev_tool_call_arr

                            ):

                                # Update existing entry in prev_tool_call_arr

                                args = parsed_tool.function.arguments

                                self.prev_tool_call_arr[self.current_tool_index][

                                    "arguments"

                                ] = json.loads(args)

                        except Exception:

                            pass # Ignore parsing errors during streaming


                    result = DeltaMessage(

                        tool_calls=[

                            DeltaToolCall(

                                index=self.current_tool_index,

                                function=DeltaFunctionCall(arguments="}"),

                            )

                        ]

                    )

                    # Update streamed_args_for_tool for closing brace

                    if self.current_tool_index < len(self.streamed_args_for_tool):

                        self.streamed_args_for_tool[self.current_tool_index] += "}"

                    # Reset state for next tool

                    self.json_closed = True

                    self.in_function = False

                    self.accumulated_params = {}


                    logger.debug("[M2_STREAMING] Tool call completed")


                    return result

                else:

                    # Don't close JSON yet, continue processing parameters

                    return None


            # Look for parameters

            # Find all parameter starts

            param_starts = []

            idx = 0

            while True:

                idx = tool_text.find(self.parameter_prefix, idx)

                if idx == -1:

                    break

                param_starts.append(idx)

                idx += len(self.parameter_prefix)


            # Check if we should start a new parameter

            if (

                not self.in_param

                and self.param_count < len(param_starts)

                and len(param_starts) > self.param_count

            ):

                # Process the next parameter

                param_idx = param_starts[self.param_count]

                param_start = param_idx + len(self.parameter_prefix)

                remaining = tool_text[param_start:]


                if ">" in remaining:

                    # We have the complete parameter name

                    name_end = remaining.find(">")

                    param_name_raw = remaining[:name_end]

                    self.current_param_name = self._extract_name(param_name_raw)


                    # Find the parameter value

                    value_start = param_start + name_end + 1

                    value_text = tool_text[value_start:]

                    if value_text.startswith("\n"):

                        value_text = value_text[1:]


                    # Find where this parameter ends

                    param_end_idx = value_text.find(self.parameter_end_token)

                    if param_end_idx == -1:

                        # No closing tag, look for next parameter or function end

                        next_param_idx = value_text.find(self.parameter_prefix)

                        func_end_idx = value_text.find(self.invoke_end_token)


                        if next_param_idx != -1 and (

                            func_end_idx == -1 or next_param_idx < func_end_idx

                        ):

                            param_end_idx = next_param_idx

                        elif func_end_idx != -1:

                            param_end_idx = func_end_idx

                        else:

                            # Neither found, check if tool call is complete

                            if self.invoke_end_token in tool_text:

                                # Tool call and parameter is complete

                                param_end_idx = len(value_text)

                            else:

                                # Still streaming, wait for more content

                                return None


                    if param_end_idx != -1:

                        # Complete parameter found

                        param_value = value_text[:param_end_idx]

                        if param_value.endswith("\n"):

                            param_value = param_value[:-1]


                        # Store raw value for later processing

                        self.accumulated_params[self.current_param_name] = param_value


                        # Get parameter configuration with anyOf support

                        param_config = {}

                        if self.streaming_request and self.streaming_request.tools:

                            for tool in self.streaming_request.tools:

                                if (

                                    hasattr(tool, "function")

                                    and tool.function.name == self.current_function_name

                                    and hasattr(tool.function, "parameters")

                                ):

                                    params = tool.function.parameters

                                    if (

                                        isinstance(params, dict)

                                        and "properties" in params

                                    ):

                                        param_config = params["properties"]

                                    break


                        # Get parameter types (supports anyOf/oneOf/allOf)

                        param_type = self._get_param_types_from_config(

                            self.current_param_name, param_config

                        )


                        converted_value = self._convert_param_value_with_types(

                            param_value, param_type

                        )


                        # Build JSON fragment based on the converted type

                        # Use json.dumps to properly serialize the value

                        serialized_value = json.dumps(

                            converted_value, ensure_ascii=False

                        )


                        if self.param_count == 0:

                            json_fragment = (

                                f'"{self.current_param_name}": {serialized_value}'

                            )

                        else:

                            json_fragment = (

                                f', "{self.current_param_name}": {serialized_value}'

                            )


                        self.param_count += 1

                        # Update streamed_args_for_tool for this tool call

                        if self.current_tool_index < len(self.streamed_args_for_tool):

                            self.streamed_args_for_tool[self.current_tool_index] += (

                                json_fragment

                            )

                        return DeltaMessage(

                            tool_calls=[

                                DeltaToolCall(

                                    index=self.current_tool_index,

                                    function=DeltaFunctionCall(arguments=json_fragment),

                                )

                            ]

                        )


        return None


1/26
杨
杨涌全
楼主
1楼
vllm/entrypoints/openai/tool_parsers/__init__.py 中新增: from .minimax_m2_tool_parser import MinimaxM2ToolParser
1/26
没有更多内容了
--
楼主
Level.3
0 帖子
楼主热帖
暂无数据