feat(ui): load provider model catalog asynchronously to avoid blocking page render

This commit is contained in:
Will Miao
2026-07-06 10:02:09 +08:00
parent e0e1b804a7
commit 2373edf73c
4 changed files with 56 additions and 5 deletions

View File

@@ -38,7 +38,12 @@ from ...services.settings_manager import get_settings_manager
from ...services.websocket_manager import ws_manager from ...services.websocket_manager import ws_manager
from ...services.downloader import get_downloader from ...services.downloader import get_downloader
from ...services.errors import ResourceNotFoundError 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 ...services.cache_health_monitor import CacheHealthMonitor, CacheHealthStatus
from ...utils.models import BaseModelMetadata from ...utils.models import BaseModelMetadata
from ...utils.constants import ( from ...utils.constants import (
@@ -1625,6 +1630,20 @@ class SettingsHandler:
def _is_dedicated_example_images_folder(self, folder_path: str) -> bool: def _is_dedicated_example_images_folder(self, folder_path: str) -> bool:
return is_valid_example_images_root(folder_path) 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: class UsageStatsHandler:
def __init__(self, usage_stats_factory: UsageStatsFactory = UsageStats) -> None: def __init__(self, usage_stats_factory: UsageStatsFactory = UsageStats) -> None:
@@ -3395,6 +3414,7 @@ class MiscHandlerSet:
"get_settings_libraries": self.settings.get_libraries, "get_settings_libraries": self.settings.get_libraries,
"activate_library": self.settings.activate_library, "activate_library": self.settings.activate_library,
"get_llm_models": self.settings.get_llm_models, "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, "update_usage_stats": self.usage_stats.update_usage_stats,
"get_usage_stats": self.usage_stats.get_usage_stats, "get_usage_stats": self.usage_stats.get_usage_stats,
"update_lora_code": self.lora_code.update_lora_code, "update_lora_code": self.lora_code.update_lora_code,

View File

@@ -154,10 +154,13 @@ class ModelPageView:
) )
self._template_env._i18n_filter_added = True # type: ignore[attr-defined] 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 presets are embedded directly (local, no await needed).
provider_models = await get_all_provider_models(catalog_provider_ids) # 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 = { template_context = {
"is_initializing": is_initializing, "is_initializing": is_initializing,
@@ -167,7 +170,7 @@ class ModelPageView:
"t": self._server_i18n.get_translation, "t": self._server_i18n.get_translation,
"version": self._get_app_version(), "version": self._get_app_version(),
"provider_presets_json": json.dumps(PROVIDER_PRESETS), "provider_presets_json": json.dumps(PROVIDER_PRESETS),
"provider_models_json": json.dumps(provider_models), "provider_models_json": "{}",
} }
if not is_initializing: if not is_initializing:

View File

@@ -23,6 +23,7 @@ MISC_ROUTE_DEFINITIONS: tuple[RouteDefinition, ...] = (
RouteDefinition("GET", "/api/lm/settings", "get_settings"), RouteDefinition("GET", "/api/lm/settings", "get_settings"),
RouteDefinition("POST", "/api/lm/settings", "update_settings"), RouteDefinition("POST", "/api/lm/settings", "update_settings"),
RouteDefinition("GET", "/api/lm/llm/models", "get_llm_models"), 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("GET", "/api/lm/doctor/diagnostics", "get_doctor_diagnostics"),
RouteDefinition("POST", "/api/lm/doctor/repair-cache", "repair_doctor_cache"), RouteDefinition("POST", "/api/lm/doctor/repair-cache", "repair_doctor_cache"),
RouteDefinition("POST", "/api/lm/doctor/resolve-filename-conflicts", "resolve_doctor_filename_conflicts"), RouteDefinition("POST", "/api/lm/doctor/resolve-filename-conflicts", "resolve_doctor_filename_conflicts"),

View File

@@ -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() { async loadSettingsToUI() {
// Set frontend settings from state // Set frontend settings from state
const blurMatureContentCheckbox = document.getElementById('blurMatureContent'); 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'); const llmProviderSelect = document.getElementById('llmProvider');
if (llmProviderSelect) { if (llmProviderSelect) {
llmProviderSelect.value = state.global.settings.llm_provider || 'openai'; llmProviderSelect.value = state.global.settings.llm_provider || 'openai';