[Frontend] Add site-packages support for reasoning/tool parser plugins (#45241)
Signed-off-by: odashi <yusuke.oda@predicate.jp> Co-authored-by: Chauncey <chaunceyjiang@gmail.com>
Yusuke Oda committed
Sep 2, 2026 at 16:41 UTC
798b557e061b5b7554e2a26ef92f1ec88e9518e9
4 files changed
+63
-28
tests/utils_/test_import_utils.py
+35
-1
@@ -1,10 +1,11 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+import sys
4
from unittest.mock import MagicMock, patch
5
6
import pytest
7
7
-from vllm.utils.import_utils import PlaceholderModule, _has_module
8
+from vllm.utils.import_utils import PlaceholderModule, _has_module, import_plugin
9
10
11
def _raises_module_not_found():
@@ -101,3 +102,36 @@ class TestHasModule:
102
result = _has_module("json") # should hit cache
103
mock_spec.assert_not_called()
104
assert result is True
105
+
106
+
107
+class TestImportPlugin:
108
+ def test_importing_from_site_packages(self):
109
+ import json
110
+
111
+ result = import_plugin("json")
112
+ assert result is json
113
+
114
+ def test_importing_from_file(self, tmp_path):
115
+ plugin_file = tmp_path / "my_test_plugin.py"
116
+ plugin_file.write_text("VALUE = 42\n")
117
+
118
+ try:
119
+ result = import_plugin(str(plugin_file))
120
+ assert result is not None
121
+ assert result.VALUE == 42
122
+ finally:
123
+ sys.modules.pop("my_test_plugin", None)
124
+
125
+ def test_returns_none_when_both_attempts_fail(self):
126
+ with (
127
+ patch(
128
+ "vllm.utils.import_utils.import_from_path",
129
+ side_effect=FileNotFoundError("no such file"),
130
+ ),
131
+ patch(
132
+ "vllm.utils.import_utils.importlib.import_module",
133
+ side_effect=ModuleNotFoundError("no such module"),
134
+ ),
135
+ ):
136
+ result = import_plugin("nonexistent_plugin_xyz")
137
+ assert result is None
vllm/reasoning/abs_reasoning_parsers.py
+3
-15
@@ -2,7 +2,6 @@
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
import importlib
5
-import os
5
from abc import abstractmethod
6
from collections.abc import Callable, Iterable, Sequence
7
from functools import cached_property
@@ -11,7 +10,7 @@ from typing import TYPE_CHECKING, cast
10
from vllm.entrypoints.mcp.tool_server import ToolServer
11
from vllm.logger import init_logger
12
from vllm.utils.collection_utils import is_list_of
14
-from vllm.utils.import_utils import import_from_path
13
+from vllm.utils.import_utils import import_plugin
14
15
if TYPE_CHECKING:
16
from vllm.config import ModelConfig
@@ -359,16 +358,5 @@ class ReasoningParserManager:
358
359
@classmethod
360
def import_reasoning_parser(cls, plugin_path: str) -> None:
362
- """
363
- Import a user-defined reasoning parser by the path
364
- of the reasoning parser define file.
365
- """
366
- module_name = os.path.splitext(os.path.basename(plugin_path))[0]
367
-
368
- try:
369
- import_from_path(module_name, plugin_path)
370
- except Exception:
371
- logger.exception(
372
- "Failed to load module '%s' from %s.", module_name, plugin_path
373
- )
374
- return
361
+ """Import a user-defined reasoning parser."""
362
+ import_plugin(plugin_path)
vllm/tool_parsers/abstract_tool_parser.py
+3
-11
@@ -3,7 +3,6 @@
3
4
import importlib
5
import json
6
-import os
6
from collections.abc import Callable, Sequence
7
from functools import cached_property
8
from typing import Any
@@ -33,7 +32,7 @@ from vllm.sampling_params import (
32
from vllm.tokenizers import TokenizerLike
33
from vllm.tool_parsers.utils import Tool, get_json_schema_from_tools
34
from vllm.utils.collection_utils import is_list_of
36
-from vllm.utils.import_utils import import_from_path
35
+from vllm.utils.import_utils import import_plugin
36
37
__all__ = ["Tool"]
38
@@ -367,12 +366,5 @@ class ToolParserManager:
366
367
@classmethod
368
def import_tool_parser(cls, plugin_path: str) -> None:
370
- """Import a user-defined parser file from arbitrary path."""
371
-
372
- module_name = os.path.splitext(os.path.basename(plugin_path))[0]
373
- try:
374
- import_from_path(module_name, plugin_path)
375
- except Exception:
376
- logger.exception(
377
- "Failed to load module '%s' from %s.", module_name, plugin_path
378
- )
369
+ """Import a user-defined tool parser."""
370
+ import_plugin(plugin_path)
vllm/utils/import_utils.py
+22
-1
@@ -82,7 +82,28 @@ def import_triton_kernels():
82
)
83
84
85
-def import_from_path(module_name: str, file_path: str | os.PathLike):
85
+def import_plugin(plugin_path: str) -> ModuleType | None:
86
+ """
87
+ Import a user-defined plugin.
88
+
89
+ Plugin can be either:
90
+ * a module in site-packages
91
+ * a Python file specified by its path
92
+ """
93
+ try:
94
+ return importlib.import_module(plugin_path)
95
+ except ModuleNotFoundError:
96
+ module_name = os.path.splitext(os.path.basename(plugin_path))[0]
97
+ try:
98
+ return import_from_path(module_name, plugin_path)
99
+ except Exception:
100
+ logger.exception(
101
+ "Failed to load module '%s' from %s.", module_name, plugin_path
102
+ )
103
+ return None
104
+
105
+
106
+def import_from_path(module_name: str, file_path: str | os.PathLike) -> ModuleType:
107
"""
108
Import a Python file according to its file path.
109