Merge branch 'main' into codex/fix-diffusion-allrank-rpc
This commit is contained in:
20 files changed
+1045
-208
No files matched your search
@@ -0,0 +1,101 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Unit tests for the TTS serving adapter registry (RFC #4327).
|
||||
|
||||
Pure-Python registry/resolution logic; no model or GPU resources are loaded.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from vllm_omni.entrypoints.openai.tts_adapters import (
|
||||
TTS_ADAPTER_REGISTRY,
|
||||
ARTTSAdapter,
|
||||
DiffusionTTSAdapter,
|
||||
all_tts_model_types,
|
||||
resolve_adapter,
|
||||
)
|
||||
from vllm_omni.entrypoints.openai.tts_adapters.qwen3_tts import Qwen3TTSAdapter
|
||||
|
||||
# Every dedicated TTS model-type must have an adapter so the orchestrator's
|
||||
# uniform ``self._adapter.build(...)`` dispatch covers it.
|
||||
EXPECTED_MODEL_TYPES = {
|
||||
"qwen3_tts",
|
||||
"voxcpm2",
|
||||
"voxtral_tts",
|
||||
"fish_tts",
|
||||
"cosyvoice3",
|
||||
"omnivoice",
|
||||
"covo_audio",
|
||||
"ming_tts",
|
||||
"moss_tts_nano",
|
||||
"moss_tts",
|
||||
"higgs_audio_v2",
|
||||
"higgs_audio_v3",
|
||||
"glm_tts",
|
||||
"step_audio2",
|
||||
}
|
||||
|
||||
|
||||
def test_all_model_types_registered():
|
||||
assert EXPECTED_MODEL_TYPES <= all_tts_model_types()
|
||||
|
||||
|
||||
def test_registry_keyed_by_name():
|
||||
for name, cls in TTS_ADAPTER_REGISTRY.items():
|
||||
assert cls.name == name
|
||||
|
||||
|
||||
def test_resolve_each_model_type():
|
||||
for model_type in EXPECTED_MODEL_TYPES:
|
||||
cls = resolve_adapter(model_type)
|
||||
assert cls is not None, model_type
|
||||
assert cls.name == model_type
|
||||
|
||||
|
||||
def test_resolve_qwen3_tts_class():
|
||||
assert resolve_adapter("qwen3_tts") is Qwen3TTSAdapter
|
||||
|
||||
|
||||
def test_resolve_unknown_returns_none():
|
||||
assert resolve_adapter("not_a_real_model") is None
|
||||
assert resolve_adapter(None) is None
|
||||
|
||||
|
||||
def test_ming_flash_omni_not_migrated():
|
||||
"""ming_flash_omni is intentionally excluded from the adapter migration in
|
||||
this PR; it stays on the legacy inline dispatch in serving_speech.py."""
|
||||
assert resolve_adapter("ming_flash_omni_tts") is None
|
||||
|
||||
|
||||
def test_voxcpm2_resolves():
|
||||
"""VoxCPM2 (the served ``latent_generator`` model) resolves cleanly.
|
||||
|
||||
Detection never returns the legacy ``voxcpm`` type, so there is no shared
|
||||
stage-key ambiguity to resolve.
|
||||
"""
|
||||
assert resolve_adapter("voxcpm2") is not None
|
||||
assert resolve_adapter("voxcpm") is None
|
||||
|
||||
|
||||
def test_all_adapters_are_ar_or_diffusion():
|
||||
for cls in TTS_ADAPTER_REGISTRY.values():
|
||||
assert issubclass(cls, (ARTTSAdapter, DiffusionTTSAdapter))
|
||||
assert cls.backend in ("ar", "diffusion")
|
||||
|
||||
|
||||
def test_qwen3_tts_metadata():
|
||||
assert Qwen3TTSAdapter.backend == "ar"
|
||||
assert issubclass(Qwen3TTSAdapter, ARTTSAdapter)
|
||||
|
||||
|
||||
def test_diffusion_adapter_extra_body_params_fallback():
|
||||
class _DiffAdapter(DiffusionTTSAdapter):
|
||||
name = "diff_probe"
|
||||
|
||||
async def build(self, request, sampling_params_list): # pragma: no cover
|
||||
raise NotImplementedError
|
||||
|
||||
assert _DiffAdapter.extra_body_params() == frozenset()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
@@ -520,9 +520,25 @@ def assert_omni_response(response: Any, request_config: dict[str, Any], run_leve
|
||||
def assert_audio_speech_response(response: Any, request_config: dict[str, Any], run_level: str) -> None:
|
||||
"""Validate speech API results from :class:`~tests.helpers.runtime.OmniResponse`.
|
||||
|
||||
Success path only. Use :meth:`OpenAIClientHandler.send_audio_speech_http_request` with ``err_code`` /
|
||||
``err_message`` for expected HTTP errors.
|
||||
When ``request_config`` carries ``status_code`` and/or ``err_message``, the
|
||||
request is expected to be rejected: assert it failed and that the HTTP status
|
||||
/ error text match. Otherwise the normal success-path checks run.
|
||||
"""
|
||||
expected_status = request_config.get("status_code")
|
||||
expected_err = request_config.get("err_message")
|
||||
if expected_status is not None or expected_err is not None:
|
||||
assert not response.success, "Expected an error response, but the request succeeded."
|
||||
if expected_status is not None:
|
||||
allowed = expected_status if isinstance(expected_status, (list, tuple)) else (expected_status,)
|
||||
assert response.status_code in allowed, f"Expected HTTP status in {allowed}, got {response.status_code}"
|
||||
if expected_err is not None:
|
||||
alternatives = expected_err if isinstance(expected_err, (list, tuple)) else (expected_err,)
|
||||
error_text = response.error_message or ""
|
||||
assert any(alt in error_text for alt in alternatives), (
|
||||
f"Expected one of {alternatives} in error text, got: {error_text!r}"
|
||||
)
|
||||
return
|
||||
|
||||
assert response.success, "The request failed."
|
||||
|
||||
# Optional floor on decoded audio size (models with very short clips may use a lower value).
|
||||
|
||||
@@ -762,6 +762,10 @@ class OmniResponse:
|
||||
prompt_tokens: int | None = None
|
||||
cached_tokens: int | None = None
|
||||
logprobs: list | None = None
|
||||
#: HTTP status + error text for the error-handling path (e.g. validator
|
||||
#: rejections); populated when the OpenAI client raises an APIError.
|
||||
status_code: int | None = None
|
||||
error_message: str | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
Reference in new issue
Block a user