feat(backend): gate Other Models behind opt-in management toggles

Other Models management is now opt-in: enable_other_models (default false)
plus the enabled_other_sub_types allow-list replace the unreleased additive
enabled_other_folders key.

- config._get_enabled_other_folder_keys() is the single scan gate; a new
  refresh_other_roots() rebuilds roots and preview roots on toggle.
- ModelScanner gains a _should_keep_cached_entry() hydration hook and
  on_library_changed(reconcile=...) so switching a sub_type off drops its
  entries (and hash/autov3 rows) at load time and switching it on rescans.
- OtherScanner filters location-derived entries accordingly.
- Other routes reject every other type while off (or a disabled sub_type) and
  expose an "other_disabled" page flag; download routing returns a disabled
  marker instead of guessing; the download manager refuses other-type
  downloads and default-path routing for switched-off sub_types.
- Doctor / init-status / refresh-all skip the other scanner while off; the
  scanner stays registered so staged pending-deletes still merge.
- Tests updated with explicit opt-in fixtures plus new gating coverage.
This commit is contained in:
Will Miao
2026-09-13 07:59:28 +08:00
parent f88fe2665c
commit 28fbb86dce
19 changed files with 583 additions and 84 deletions
@@ -5,6 +5,22 @@ import json
import pytest
from py.routes.handlers.download_routing_handlers import DownloadRoutingHandler
from py.services.settings_manager import get_settings_manager
@pytest.fixture(autouse=True)
def enable_other_models():
"""Other Models is opt-in; enable every sub_type for the routing tests."""
manager = get_settings_manager()
manager.settings["enable_other_models"] = True
manager.settings["enabled_other_sub_types"] = [
"vae",
"upscaler",
"text_encoder",
"clip_vision",
"controlnet",
]
yield
class FakeRequest:
@@ -149,3 +165,32 @@ async def test_other_invalid_selected_file_type_rejected():
FakeRequest({"model_type": "VAE", "selected_file_type": 123})
)
assert response.status == 400
@pytest.mark.asyncio
async def test_other_routing_disabled_when_feature_off():
get_settings_manager().settings["enable_other_models"] = False
handler = DownloadRoutingHandler()
response = await handler.get_download_routing(
FakeRequest({"model_type": "VAE", "file_types": ["Model"]})
)
payload = json.loads(response.text)
assert payload["sub_type"] is None
assert payload["disabled"] is True
assert payload["reason"] == "other_models_disabled"
@pytest.mark.asyncio
async def test_other_routing_disabled_for_switched_off_sub_type():
get_settings_manager().settings["enabled_other_sub_types"] = ["vae"]
handler = DownloadRoutingHandler()
response = await handler.get_download_routing(
FakeRequest({"model_type": "Upscaler", "file_types": ["Model"]})
)
payload = json.loads(response.text)
assert payload["sub_type"] is None
assert payload["disabled"] is True
assert payload["reason"] == "other_sub_type_disabled"
assert payload["requested_sub_type"] == "upscaler"
+6
View File
@@ -61,6 +61,12 @@ class DummySettings:
def get(self, key, default=None):
return self.data.get(key, default)
def is_other_models_enabled(self):
return bool(self.data.get("enable_other_models", False))
def get_enabled_other_sub_types(self):
return list(self.data.get("enabled_other_sub_types") or [])
def set(self, key, value):
self.data[key] = value
+47
View File
@@ -30,6 +30,20 @@ def routes():
return handler
@pytest.fixture(autouse=True)
def enable_other_models():
"""Other Models is opt-in; these tests exercise the enabled state."""
from py.services.settings_manager import get_settings_manager
manager = get_settings_manager()
manager.set("enable_other_models", True)
manager.set(
"enabled_other_sub_types",
["vae", "upscaler", "text_encoder", "clip_vision", "controlnet"],
)
yield
def test_common_and_specific_routes_registered():
"""Registration smoke test: /api/lm/other/* surface plus the /other page."""
app = web.Application()
@@ -64,6 +78,39 @@ def test_validate_civitai_model_type_rejects_foreign_types(model_type):
assert OtherRoutes()._validate_civitai_model_type(model_type) is False
def test_validate_rejects_everything_when_feature_disabled():
from py.services.settings_manager import get_settings_manager
get_settings_manager().set("enable_other_models", False)
handler = OtherRoutes()
for model_type in ("VAE", "Upscaler", "TextEncoder", "CLIPVision", "Other"):
assert handler._validate_civitai_model_type(model_type) is False
def test_validate_rejects_switched_off_sub_type():
from py.services.settings_manager import get_settings_manager
get_settings_manager().set("enabled_other_sub_types", ["vae"])
handler = OtherRoutes()
assert handler._validate_civitai_model_type("VAE") is True
assert handler._validate_civitai_model_type("Upscaler") is False
def test_page_context_reports_feature_state():
from py.services.settings_manager import get_settings_manager
manager = get_settings_manager()
handler = OtherRoutes()
provider = handler._get_page_context_provider()
assert provider(None) == {"other_disabled": False}
manager.set("enable_other_models", False)
assert provider(None) == {"other_disabled": True}
def test_get_expected_model_types_mentions_supported_types():
expected = OtherRoutes()._get_expected_model_types()
for name in ("VAE", "Upscaler", "TextEncoder", "CLIPVision", "Controlnet"):