diff --git a/tests/config/test_config_factory.py b/tests/config/test_config_factory.py index 5f7359ddc..bcb5064f1 100644 --- a/tests/config/test_config_factory.py +++ b/tests/config/test_config_factory.py @@ -15,7 +15,11 @@ from transformers import PretrainedConfig, Qwen3OmniMoeConfig from tests.helpers.stage_config import get_deploy_config_path, get_deploy_config_stage from vllm_omni.config import config_factory as config_factory_module -from vllm_omni.config.config_factory import StageConfigFactory, _materialize_object_storage_configs +from vllm_omni.config.config_factory import ( + StageConfigFactory, + _materialize_object_storage_configs, + _name_match_candidate, +) from vllm_omni.config.endpoint_policy import EndpointRestriction, OmniServingCapability from vllm_omni.config.omni_config import VllmOmniConfig from vllm_omni.config.pipeline_registry import OMNI_PIPELINES, register_pipeline, resolve_pipeline_config @@ -2932,3 +2936,44 @@ class TestObjectStorageConfigResolution: matching = "s3://any-bucket/my-cosyvoice3-model" assert StageConfigFactory.try_infer_model_type(model=matching, trust_remote_code=False) == "cosyvoice3" + + +class TestNameMatchCandidateSnapshotPaths: + """Name-based matching must survive resolved HF cache snapshot paths.""" + + _SNAPSHOT_REV = "29e01c4e8d000f4bcd70751be16fa94bf3d85a18" + + def test_candidate_recovers_repo_name_from_hf_cache_layout(self): + path = f"/data/hub/models--FunAudioLLM--Fun-CosyVoice3-0.5B-2512/snapshots/{self._SNAPSHOT_REV}" + assert _name_match_candidate(path) == "Fun-CosyVoice3-0.5B-2512" + assert _name_match_candidate(path + "/") == "Fun-CosyVoice3-0.5B-2512" + + def test_candidate_keeps_basename_for_non_cache_paths(self): + assert _name_match_candidate("org/My-Model") == "My-Model" + assert _name_match_candidate("/data/snapshots-of/My-Model") == "My-Model" + # A ``snapshots`` parent without the ``models----`` encoding + # falls back to the basename, same as before. + assert _name_match_candidate(f"/data/notcache/snapshots/{self._SNAPSHOT_REV}") == self._SNAPSHOT_REV + + def test_candidate_recovers_bare_repo_name_from_hf_cache_layout(self): + # Legacy un-namespaced repos (e.g. ``gpt2``) cache as ``models--``. + path = f"/data/hub/models--my-cosyvoice3-model/snapshots/{self._SNAPSHOT_REV}" + assert _name_match_candidate(path) == "my-cosyvoice3-model" + + def test_candidate_ignores_malformed_cache_repo_dirs(self): + # Hub validation rejects ``--`` inside repo ids, so more than three + # ``--``-separated segments cannot be a real cache dir; keep basename. + assert _name_match_candidate("/hub/models--a--b--c/snapshots/abc123") == "abc123" + assert _name_match_candidate("/hub/models--/snapshots/abc123") == "abc123" + + def test_try_infer_model_type_matches_cosyvoice3_snapshot_dir(self, tmp_path): + """Regression for the HF-cache path shape: CosyVoice3 ships an empty + config.json and relies on name matching, but a resolved snapshot dir + basename is a bare revision hash, so basename-only matching (#5036) + misclassified it as a diffusion pipeline and stage init crashed with + "Diffusers pipeline index not found".""" + snapshot = tmp_path / "hub" / "models--FunAudioLLM--Fun-CosyVoice3-0.5B-2512" / "snapshots" / self._SNAPSHOT_REV + snapshot.mkdir(parents=True) + (snapshot / "config.json").write_text("{}") + + assert StageConfigFactory.try_infer_model_type(model=str(snapshot), trust_remote_code=False) == "cosyvoice3" diff --git a/vllm_omni/config/config_factory.py b/vllm_omni/config/config_factory.py index 48bd0b476..c2a8d7759 100644 --- a/vllm_omni/config/config_factory.py +++ b/vllm_omni/config/config_factory.py @@ -69,14 +69,31 @@ def _materialize_object_storage_configs(model: str) -> str: def _name_match_candidate(model: str) -> str: - """Last path component of a model reference, used for name-based matching. + """Model-name component of a model reference, used for name-based matching. Object-storage URIs and HF repo ids carry non-model segments (bucket name, organization) that must not participate in substring matching; e.g. a bucket named ``qwen3-tts-models`` holding a ``Qwen3-Omni`` checkpoint must - not resolve to the ``qwen3_tts`` pipeline. + not resolve to the ``qwen3_tts`` pipeline. Usually that means the last + path component. + + A resolved HF hub cache snapshot path is the exception: it ends in + ``models----/snapshots/`` (or ``models--`` for + legacy un-namespaced repos such as ``gpt2``), so its basename is a bare + revision hash with no model identity (models with an empty ``config.json`` + such as CosyVoice3 then lose their only detection route). Recover ``name`` + from the repo segment; the organization still stays out of the match. The + two forms are exhaustive because hub validation rejects ``--`` inside repo + ids; anything else keeps the plain basename. """ - return model.rstrip("/").rsplit("/", 1)[-1] + parts = [part for part in str(model).rstrip("/").split("/") if part] + if not parts: + return "" + if len(parts) >= 3 and parts[-2] == "snapshots": + repo_dir = parts[-3].split("--") + if repo_dir[0] == "models" and len(repo_dir) in (2, 3) and all(repo_dir): + return repo_dir[-1] + return parts[-1] def with_trust_remote_code_override(