Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions tests/entrypoints/openai/test_response_api_mcp_tools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project

import pytest
import pytest_asyncio
from openai import OpenAI

from ...utils import RemoteOpenAIServer

MODEL_NAME = "openai/gpt-oss-20b"


@pytest.fixture(scope="module")
def monkeypatch_module():
from _pytest.monkeypatch import MonkeyPatch
mpatch = MonkeyPatch()
yield mpatch
mpatch.undo()


@pytest.fixture(scope="module")
def mcp_disabled_server(monkeypatch_module: pytest.MonkeyPatch):
args = ["--enforce-eager", "--tool-server", "demo"]

with monkeypatch_module.context() as m:
m.setenv("VLLM_ENABLE_RESPONSES_API_STORE", "1")
m.setenv("PYTHON_EXECUTION_BACKEND", "dangerously_use_uv")
with RemoteOpenAIServer(MODEL_NAME, args) as remote_server:
yield remote_server


@pytest.fixture(scope="function")
def mcp_enabled_server(monkeypatch_module: pytest.MonkeyPatch):
args = ["--enforce-eager", "--tool-server", "demo"]

with monkeypatch_module.context() as m:
m.setenv("VLLM_ENABLE_RESPONSES_API_STORE", "1")
m.setenv("PYTHON_EXECUTION_BACKEND", "dangerously_use_uv")
m.setenv("GPT_OSS_SYSTEM_TOOL_MCP_LABELS",
"code_interpreter,container")
with RemoteOpenAIServer(MODEL_NAME, args) as remote_server:
yield remote_server


@pytest_asyncio.fixture
async def mcp_disabled_client(mcp_disabled_server):
async with mcp_disabled_server.get_async_client() as async_client:
yield async_client


@pytest_asyncio.fixture
async def mcp_enabled_client(mcp_enabled_server):
async with mcp_enabled_server.get_async_client() as async_client:
yield async_client


@pytest.mark.asyncio
@pytest.mark.parametrize("model_name", [MODEL_NAME])
@pytest.mark.skip(reason="Code interpreter tool is not available in CI yet.")
async def test_mcp_tool_env_flag_enabled(mcp_enabled_client: OpenAI,
model_name: str):
response = await mcp_enabled_client.responses.create(
model=model_name,
# TODO: Ideally should be able to set max tool calls
# to prevent multi-turn, but it is not currently supported
# would speed up the test
input=("What's the first 4 digits after the decimal point of "
"cube root of `19910212 * 20250910`? "
"Show only the digits. The python interpreter is not stateful "
"and you must print to see the output."),
tools=[{
"type": "mcp",
"server_label": "code_interpreter",
# URL unused for DemoToolServer
"server_url": "http://localhost:8888"
}],
)
assert response is not None
assert response.status == "completed"
assert response.usage.output_tokens_details.tool_output_tokens > 0


@pytest.mark.asyncio
@pytest.mark.parametrize("model_name", [MODEL_NAME])
@pytest.mark.skip(reason="Code interpreter tool is not available in CI yet.")
async def test_mcp_tool_env_flag_disabled(mcp_disabled_client: OpenAI,
model_name: str):
response = await mcp_disabled_client.responses.create(
model=model_name,
# TODO: Ideally should be able to set max tool calls
# to prevent multi-turn, but it is not currently supported
# would speed up the test
input=("What's the first 4 digits after the decimal point of "
"cube root of `19910212 * 20250910`? "
"Show only the digits. The python interpreter is not stateful "
"and you must print to see the output."),
tools=[{
"type": "mcp",
"server_label": "code_interpreter",
# URL unused for DemoToolServer
"server_url": "http://localhost:8888"
}],
)
assert response is not None
assert response.status == "completed"
assert response.usage.output_tokens_details.tool_output_tokens == 0
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,13 @@ async def test_web_search(client: OpenAI, model_name: str):
async def test_code_interpreter(client: OpenAI, model_name: str):
response = await client.responses.create(
model=model_name,
input="Multiply 64548*15151 using builtin python interpreter.",
# TODO: Ideally should be able to set max tool calls
# to prevent multi-turn, but it is not currently supported
# would speed up the test
input=("What's the first 4 digits after the decimal point of "
"cube root of `19910212 * 20250910`? "
"Show only the digits. The python interpreter is not stateful "
"and you must print to see the output."),
tools=[{
"type": "code_interpreter",
"container": {
Expand All @@ -464,6 +470,7 @@ async def test_code_interpreter(client: OpenAI, model_name: str):
)
assert response is not None
assert response.status == "completed"
assert response.usage.output_tokens_details.tool_output_tokens > 0


def get_weather(latitude, longitude):
Expand Down
216 changes: 216 additions & 0 deletions tests/test_envs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project

import os
from unittest.mock import patch

import pytest

from vllm.envs import env_list_with_choices, env_with_choices


class TestEnvWithChoices:
"""Test cases for env_with_choices function."""

def test_default_value_returned_when_env_not_set(self):
"""Test default is returned when env var is not set."""
env_func = env_with_choices("NONEXISTENT_ENV", "default",
["option1", "option2"])
assert env_func() == "default"

def test_none_default_returned_when_env_not_set(self):
"""Test that None is returned when env not set and default is None."""
env_func = env_with_choices("NONEXISTENT_ENV", None,
["option1", "option2"])
assert env_func() is None

def test_valid_value_returned_case_sensitive(self):
"""Test that valid value is returned in case sensitive mode."""
with patch.dict(os.environ, {"TEST_ENV": "option1"}):
env_func = env_with_choices("TEST_ENV",
"default", ["option1", "option2"],
case_sensitive=True)
assert env_func() == "option1"

def test_valid_lowercase_value_returned_case_insensitive(self):
"""Test that lowercase value is accepted in case insensitive mode."""
with patch.dict(os.environ, {"TEST_ENV": "option1"}):
env_func = env_with_choices("TEST_ENV",
"default", ["OPTION1", "OPTION2"],
case_sensitive=False)
assert env_func() == "option1"

def test_valid_uppercase_value_returned_case_insensitive(self):
"""Test that uppercase value is accepted in case insensitive mode."""
with patch.dict(os.environ, {"TEST_ENV": "OPTION1"}):
env_func = env_with_choices("TEST_ENV",
"default", ["option1", "option2"],
case_sensitive=False)
assert env_func() == "OPTION1"

def test_invalid_value_raises_error_case_sensitive(self):
"""Test that invalid value raises ValueError in case sensitive mode."""
with patch.dict(os.environ, {"TEST_ENV": "invalid"}):
env_func = env_with_choices("TEST_ENV",
"default", ["option1", "option2"],
case_sensitive=True)
with pytest.raises(ValueError,
match="Invalid value 'invalid' for TEST_ENV"):
env_func()

def test_case_mismatch_raises_error_case_sensitive(self):
"""Test that case mismatch raises ValueError in case sensitive mode."""
with patch.dict(os.environ, {"TEST_ENV": "OPTION1"}):
env_func = env_with_choices("TEST_ENV",
"default", ["option1", "option2"],
case_sensitive=True)
with pytest.raises(ValueError,
match="Invalid value 'OPTION1' for TEST_ENV"):
env_func()

def test_invalid_value_raises_error_case_insensitive(self):
"""Test that invalid value raises ValueError when case insensitive."""
with patch.dict(os.environ, {"TEST_ENV": "invalid"}):
env_func = env_with_choices("TEST_ENV",
"default", ["option1", "option2"],
case_sensitive=False)
with pytest.raises(ValueError,
match="Invalid value 'invalid' for TEST_ENV"):
env_func()

def test_callable_choices_resolved_correctly(self):
"""Test that callable choices are resolved correctly."""

def get_choices():
return ["dynamic1", "dynamic2"]

with patch.dict(os.environ, {"TEST_ENV": "dynamic1"}):
env_func = env_with_choices("TEST_ENV", "default", get_choices)
assert env_func() == "dynamic1"

def test_callable_choices_with_invalid_value(self):
"""Test that callable choices raise error for invalid values."""

def get_choices():
return ["dynamic1", "dynamic2"]

with patch.dict(os.environ, {"TEST_ENV": "invalid"}):
env_func = env_with_choices("TEST_ENV", "default", get_choices)
with pytest.raises(ValueError,
match="Invalid value 'invalid' for TEST_ENV"):
env_func()


class TestEnvListWithChoices:
"""Test cases for env_list_with_choices function."""

def test_default_list_returned_when_env_not_set(self):
"""Test that default list is returned when env var is not set."""
env_func = env_list_with_choices("NONEXISTENT_ENV",
["default1", "default2"],
["option1", "option2"])
assert env_func() == ["default1", "default2"]

def test_empty_default_list_returned_when_env_not_set(self):
"""Test that empty default list is returned when env not set."""
env_func = env_list_with_choices("NONEXISTENT_ENV", [],
["option1", "option2"])
assert env_func() == []

def test_single_valid_value_parsed_correctly(self):
"""Test that single valid value is parsed correctly."""
with patch.dict(os.environ, {"TEST_ENV": "option1"}):
env_func = env_list_with_choices("TEST_ENV", [],
["option1", "option2"])
assert env_func() == ["option1"]

def test_multiple_valid_values_parsed_correctly(self):
"""Test that multiple valid values are parsed correctly."""
with patch.dict(os.environ, {"TEST_ENV": "option1,option2"}):
env_func = env_list_with_choices("TEST_ENV", [],
["option1", "option2"])
assert env_func() == ["option1", "option2"]

def test_values_with_whitespace_trimmed(self):
"""Test that values with whitespace are trimmed correctly."""
with patch.dict(os.environ, {"TEST_ENV": " option1 , option2 "}):
env_func = env_list_with_choices("TEST_ENV", [],
["option1", "option2"])
assert env_func() == ["option1", "option2"]

def test_empty_values_filtered_out(self):
"""Test that empty values are filtered out."""
with patch.dict(os.environ, {"TEST_ENV": "option1,,option2,"}):
env_func = env_list_with_choices("TEST_ENV", [],
["option1", "option2"])
assert env_func() == ["option1", "option2"]

def test_empty_string_returns_default(self):
"""Test that empty string returns default."""
with patch.dict(os.environ, {"TEST_ENV": ""}):
env_func = env_list_with_choices("TEST_ENV", ["default"],
["option1", "option2"])
assert env_func() == ["default"]

def test_only_commas_returns_default(self):
"""Test that string with only commas returns default."""
with patch.dict(os.environ, {"TEST_ENV": ",,,"}):
env_func = env_list_with_choices("TEST_ENV", ["default"],
["option1", "option2"])
assert env_func() == ["default"]

def test_case_sensitive_validation(self):
"""Test case sensitive validation."""
with patch.dict(os.environ, {"TEST_ENV": "option1,OPTION2"}):
env_func = env_list_with_choices("TEST_ENV", [],
["option1", "option2"],
case_sensitive=True)
with pytest.raises(ValueError,
match="Invalid value 'OPTION2' in TEST_ENV"):
env_func()

def test_case_insensitive_validation(self):
"""Test case insensitive validation."""
with patch.dict(os.environ, {"TEST_ENV": "OPTION1,option2"}):
env_func = env_list_with_choices("TEST_ENV", [],
["option1", "option2"],
case_sensitive=False)
assert env_func() == ["OPTION1", "option2"]

def test_invalid_value_in_list_raises_error(self):
"""Test that invalid value in list raises ValueError."""
with patch.dict(os.environ, {"TEST_ENV": "option1,invalid,option2"}):
env_func = env_list_with_choices("TEST_ENV", [],
["option1", "option2"])
with pytest.raises(ValueError,
match="Invalid value 'invalid' in TEST_ENV"):
env_func()

def test_callable_choices_resolved_correctly(self):
"""Test that callable choices are resolved correctly."""

def get_choices():
return ["dynamic1", "dynamic2"]

with patch.dict(os.environ, {"TEST_ENV": "dynamic1,dynamic2"}):
env_func = env_list_with_choices("TEST_ENV", [], get_choices)
assert env_func() == ["dynamic1", "dynamic2"]

def test_callable_choices_with_invalid_value(self):
"""Test that callable choices raise error for invalid values."""

def get_choices():
return ["dynamic1", "dynamic2"]

with patch.dict(os.environ, {"TEST_ENV": "dynamic1,invalid"}):
env_func = env_list_with_choices("TEST_ENV", [], get_choices)
with pytest.raises(ValueError,
match="Invalid value 'invalid' in TEST_ENV"):
env_func()

def test_duplicate_values_preserved(self):
"""Test that duplicate values in the list are preserved."""
with patch.dict(os.environ, {"TEST_ENV": "option1,option1,option2"}):
env_func = env_list_with_choices("TEST_ENV", [],
["option1", "option2"])
assert env_func() == ["option1", "option1", "option2"]
Loading