[Core / Bugfix] Add Mechanism for Endpoint Rejection (#4762)

Signed-off-by: Alex Brooks <albrooks@redhat.com>
This commit is contained in:
Alex Brooks authored and GitHub committed 2026-07-08 09:00:47 -06:00
1 parent 7d4decc1d8
commit b7d08fe5f6
14 files changed
+508 -158

No files matched your search

+87
View File
@@ -0,0 +1,87 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Tests for endpoint restrictions logic."""
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from vllm_omni.config.endpoint_policy import (
EndpointRestriction,
OmniServingCapability,
shutdown_unsupported_routes,
)
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
SERVER_ROUTE = "/v1/completions"
REJECTION_REASON = "This route is banned!"
def _make_app_with_server_route():
"""Create a FastAPI app with a route to reject."""
app = FastAPI()
@app.post(SERVER_ROUTE)
async def existing_handler():
"""Returns a response; we want to disable this."""
return {"ok": True}
return app
def test_restricted_completions_returns_400():
"""Ensure that simple route rejection works."""
app = _make_app_with_server_route()
restrictions = (EndpointRestriction(OmniServingCapability.COMPLETIONS, REJECTION_REASON),)
shutdown_unsupported_routes(app, restrictions)
client = TestClient(app)
resp = client.post(SERVER_ROUTE, json={"prompt": "hello", "model": "m"})
assert resp.status_code == 400
body = resp.json()
assert body["error"]["message"] == REJECTION_REASON
assert body["error"]["type"] == "BadRequestError"
def test_unrestricted_completions_not_blocked():
"""Ensure that if we don't shutdown any routes, the route stays."""
app = _make_app_with_server_route()
shutdown_unsupported_routes(app, ())
client = TestClient(app)
resp = client.post(SERVER_ROUTE)
assert resp.status_code == 200
assert resp.json() == {"ok": True}
def test_route_rejection_is_idempotent():
"""Ensure that calling rejection twice doesn't break things."""
app = _make_app_with_server_route()
restrictions = (EndpointRestriction(OmniServingCapability.COMPLETIONS, REJECTION_REASON),)
shutdown_unsupported_routes(app, restrictions)
shutdown_unsupported_routes(app, restrictions)
client = TestClient(app)
resp = client.post(SERVER_ROUTE, json={"prompt": "hello", "model": "m"})
assert resp.status_code == 400
body = resp.json()
assert body["error"]["message"] == REJECTION_REASON
assert body["error"]["type"] == "BadRequestError"
def test_invalid_route_rejection():
"""Ensure that a restriction won't crash if a route doesn't exist."""
# NOTE: This test is for safety since the route organization is still messy.
# Since the restriction needs a valid OmniServingCapability, its implied the route
# is a valid vLLM route, but was already removed from the app by earlier processing.
app = FastAPI()
restrictions = (EndpointRestriction(OmniServingCapability.COMPLETIONS, REJECTION_REASON),)
shutdown_unsupported_routes(app, restrictions)
client = TestClient(app)
resp = client.post(SERVER_ROUTE, json={"prompt": "hello", "model": "m"})
assert resp.status_code == 400
body = resp.json()
assert body["error"]["message"] == REJECTION_REASON
assert body["error"]["type"] == "BadRequestError"
@@ -226,3 +226,27 @@ def test_thinker_prefix_caching_audio_output(omni_server, openai_client) -> None
}
_run_prefix_cache_check(openai_client, request_config)
@pytest.mark.advanced_model
@pytest.mark.core_model
@pytest.mark.omni
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2)
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
def test_completions_rejected_for_thinker_talker(omni_server, openai_client) -> None:
"""Ensure Thinker-talker models reject /v1/completions; we do this because the
thinker-talker handoff implementations currently use ChatML <|im_start|> and
<|im_end|> markers to segment the input sequence; when we don't have them,
the talker does not get any embeddings, which breaks the server.
"""
responses = openai_client.send_completions_http_request(
{
"json": {
"model": omni_server.model,
"prompt": "Hello, how are you?",
"max_tokens": 10,
},
},
err_code=400,
)
assert not responses[0].success
@@ -107,6 +107,7 @@ class FakeAsyncOmniEngine:
self.stage_vllm_configs = [None for _ in range(self.num_stages)]
self.output_processors = [SimpleNamespace(tokenizer=None) for _ in range(self.num_stages)]
self.input_processor = None
self.endpoint_restrictions = ()
self.output_q: queue.Queue[Any] = queue.Queue()
self.submitted: list[dict[str, Any]] = []
@@ -24,8 +24,8 @@ def test_dreamzero_vla_resolves_to_dreamzero_config(monkeypatch):
def test_dreamzero_config_sets_model_class_and_policy_config(monkeypatch):
monkeypatch.setattr(
"vllm_omni.config.config_factory.StageConfigFactory._auto_detect_model_type",
classmethod(lambda _cls, _model, trust_remote_code=True: ("vla", None)),
"vllm_omni.config.config_factory.StageConfigFactory._try_infer_model_type",
classmethod(lambda _cls, model, trust_remote_code=True: "vla"),
)
monkeypatch.setattr(
"vllm_omni.diffusion.utils.hf_utils._looks_like_dreamzero",
+23
View File
@@ -1064,6 +1064,29 @@ class OpenAIClientHandler:
)
return [resp]
def send_completions_http_request(
self,
request_config: dict[str, Any],
*,
err_code: int | tuple[int, ...] | list[int] | None = None,
err_message: str | tuple[str, ...] | list[str] | None = None,
) -> list[HttpResponse]:
"""POST ``/v1/completions`` with ``json`` or ``raw_body``."""
# TODO (Alex): A lot of these helpers should be consolidated as they differ only by endpoint
cfg = _merge_http_expectation_kwargs(
request_config,
err_code=err_code,
err_message=err_message,
)
r = self._post_json_endpoint("/v1/completions", cfg, default_timeout=120.0)
resp = self._http_response_from_requests(r)
assert_http_error(
resp,
err_code=cfg.get("err_code"),
err_message=cfg.get("err_message"),
)
return [resp]
def send_omni_sleep_http_request(
self,
request_config: dict[str, Any],
+47
View File
@@ -16,6 +16,7 @@ from transformers import PretrainedConfig, Qwen3OmniMoeConfig
from tests.helpers.stage_config import get_deploy_config_path, get_deploy_config_stage
from vllm_omni.config.config_factory import StageConfigFactory
from vllm_omni.config.endpoint_policy import EndpointRestriction, OmniServingCapability
from vllm_omni.config.pipeline_registry import OMNI_PIPELINES, register_pipeline
from vllm_omni.config.stage_config import (
DeployConfig,
@@ -39,6 +40,15 @@ from vllm_omni.engine.arg_utils import SHARED_FIELDS, EngineArgs, internal_black
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
@pytest.fixture(autouse=True)
def clear_config_factory_caches():
"""Clear cached classmethods from the StageConfigFactory to prevent test pollution."""
yield
StageConfigFactory.get_hf_config.cache_clear()
StageConfigFactory.try_infer_model_type.cache_clear()
StageConfigFactory.get_pipeline_config.cache_clear()
Q3_OMNI_ALL_STAGES_HF_CONFIG = Qwen3OmniMoeConfig(enable_audio_output=True)
Q3_OMNI_THINKER_HF_CONFIG = Qwen3OmniMoeConfig(enable_audio_output=False)
@@ -666,6 +676,43 @@ class TestPipelineRegistration:
assert resolved_config is not None
assert len(resolved_config) > 0
def test_deploy_override_uses_correct_endpoint_restrictions(self, clean_pipeline_registry, tmp_path):
"""Ensure endpoint restrictions must come from the final pipeline
after deploy config overrides, not the auto-detected pipeline.
"""
# Register two pipeline configs, where one has an endpoint restriction, and one doesn't
restriction = EndpointRestriction(
OmniServingCapability.COMPLETIONS,
"pipeline_a blocks completions",
)
pipe_a = PipelineConfig(model_type="detect_type", endpoint_restrictions=(restriction,))
pipe_b = PipelineConfig(model_type="override_type", endpoint_restrictions=())
register_pipeline(pipe_a)
register_pipeline(pipe_b)
# Create a config with the autodetected type, and write the
# deploy config specifying the override type to a temp path
class FakeConfig(PretrainedConfig):
model_type = "detect_type"
deploy_yaml = tmp_path / "override.yaml"
deploy_yaml.write_text("pipeline: override_type\n")
# Get the endpoint restrictions, passing the deploy config with the override
# type + patching the config for the detected type. Ensure that the endpoint
# restrictions correspond to the type in the deploy config.
with patch(
"vllm_omni.config.config_factory.get_config",
return_value=FakeConfig(),
):
restrictions = StageConfigFactory.get_pipeline_endpoint_restrictions(
model="fake/model",
trust_remote_code=False,
deploy_config_path=str(deploy_yaml),
)
assert restrictions == ()
class TestResolveScheduler:
def test_all_execution_types_handled(self):