diff --git a/py/routes/handlers/misc_handlers.py b/py/routes/handlers/misc_handlers.py index 977be1d3..b62af8b8 100644 --- a/py/routes/handlers/misc_handlers.py +++ b/py/routes/handlers/misc_handlers.py @@ -38,7 +38,12 @@ from ...services.settings_manager import get_settings_manager from ...services.websocket_manager import ws_manager from ...services.downloader import get_downloader from ...services.errors import ResourceNotFoundError -from ...services.llm_service import get_provider_model_ids, fetch_ollama_models +from ...services.llm_service import ( + PROVIDER_PRESETS, + fetch_ollama_models, + get_all_provider_models, + get_provider_model_ids, +) from ...services.cache_health_monitor import CacheHealthMonitor, CacheHealthStatus from ...utils.models import BaseModelMetadata from ...utils.constants import ( @@ -1625,6 +1630,20 @@ class SettingsHandler: def _is_dedicated_example_images_folder(self, folder_path: str) -> bool: return is_valid_example_images_root(folder_path) + async def get_provider_models(self, request: web.Request) -> web.Response: + """Return the model catalog for all preset providers. + + This endpoint is called asynchronously by the settings UI so that + page rendering never blocks on the remote model catalog fetch. + """ + catalog_provider_ids = [p for p in PROVIDER_PRESETS if p != "custom"] + try: + provider_models = await get_all_provider_models(catalog_provider_ids) + return web.json_response({"success": True, "models": provider_models}) + except Exception as exc: + logger.warning("Failed to fetch provider models: %s", exc) + return web.json_response({"success": False, "models": {}, "error": str(exc)}) + class UsageStatsHandler: def __init__(self, usage_stats_factory: UsageStatsFactory = UsageStats) -> None: @@ -3395,6 +3414,7 @@ class MiscHandlerSet: "get_settings_libraries": self.settings.get_libraries, "activate_library": self.settings.activate_library, "get_llm_models": self.settings.get_llm_models, + "get_provider_models": self.settings.get_provider_models, "update_usage_stats": self.usage_stats.update_usage_stats, "get_usage_stats": self.usage_stats.get_usage_stats, "update_lora_code": self.lora_code.update_lora_code, diff --git a/py/routes/handlers/model_handlers.py b/py/routes/handlers/model_handlers.py index 1a082c63..ea9dfd8b 100644 --- a/py/routes/handlers/model_handlers.py +++ b/py/routes/handlers/model_handlers.py @@ -154,10 +154,13 @@ class ModelPageView: ) self._template_env._i18n_filter_added = True # type: ignore[attr-defined] - from ...services.llm_service import PROVIDER_PRESETS, get_all_provider_models + from ...services.llm_service import PROVIDER_PRESETS - catalog_provider_ids = [p for p in PROVIDER_PRESETS if p != "custom"] - provider_models = await get_all_provider_models(catalog_provider_ids) + # Provider presets are embedded directly (local, no await needed). + # Provider model catalogs are fetched asynchronously by the + # frontend via GET /api/lm/llm/provider-models so page rendering + # never blocks on the remote model catalog (which can take up to + # 30s on cold cache). template_context = { "is_initializing": is_initializing, @@ -167,7 +170,7 @@ class ModelPageView: "t": self._server_i18n.get_translation, "version": self._get_app_version(), "provider_presets_json": json.dumps(PROVIDER_PRESETS), - "provider_models_json": json.dumps(provider_models), + "provider_models_json": "{}", } if not is_initializing: diff --git a/py/routes/misc_route_registrar.py b/py/routes/misc_route_registrar.py index 8748f592..4c29bb33 100644 --- a/py/routes/misc_route_registrar.py +++ b/py/routes/misc_route_registrar.py @@ -23,6 +23,7 @@ MISC_ROUTE_DEFINITIONS: tuple[RouteDefinition, ...] = ( RouteDefinition("GET", "/api/lm/settings", "get_settings"), RouteDefinition("POST", "/api/lm/settings", "update_settings"), RouteDefinition("GET", "/api/lm/llm/models", "get_llm_models"), + RouteDefinition("GET", "/api/lm/llm/provider-models", "get_provider_models"), RouteDefinition("GET", "/api/lm/doctor/diagnostics", "get_doctor_diagnostics"), RouteDefinition("POST", "/api/lm/doctor/repair-cache", "repair_doctor_cache"), RouteDefinition("POST", "/api/lm/doctor/resolve-filename-conflicts", "resolve_doctor_filename_conflicts"), diff --git a/static/js/managers/SettingsManager.js b/static/js/managers/SettingsManager.js index fdf62b4f..a143a20f 100644 --- a/static/js/managers/SettingsManager.js +++ b/static/js/managers/SettingsManager.js @@ -789,6 +789,27 @@ export class SettingsManager { } } + async _fetchProviderModelsAsync() { + try { + const resp = await fetch('/api/lm/llm/provider-models'); + if (!resp.ok) return; + const data = await resp.json(); + if (data.success && data.models) { + this._providerModels = data.models; + // Refresh model combobox if the settings modal is still open. + // Skip when provider is Ollama — it fetches its own live list + // from the local Ollama API and we must not overwrite it. + const llmProviderSelect = document.getElementById('llmProvider'); + const provider = llmProviderSelect ? llmProviderSelect.value : 'openai'; + if (this._llmModelCombobox && provider !== 'ollama') { + this._llmModelCombobox.updatePresets(this._providerModels[provider] || []); + } + } + } catch (_) { + // Silently ignore — models stay empty until next modal open + } + } + async loadSettingsToUI() { // Set frontend settings from state const blurMatureContentCheckbox = document.getElementById('blurMatureContent'); @@ -850,6 +871,12 @@ export class SettingsManager { } } + // If the embedded provider models is empty (server did not block on + // the remote catalog during page render), fetch asynchronously. + if (!this._providerModels || Object.keys(this._providerModels).length === 0) { + this._fetchProviderModelsAsync(); + } + const llmProviderSelect = document.getElementById('llmProvider'); if (llmProviderSelect) { llmProviderSelect.value = state.global.settings.llm_provider || 'openai';