perf(model): optimize model tab load speed with two-phase fetch and loadQuick (#677)

This commit is contained in:
M57
2026-07-22 14:46:38 +03:00
committed by GitHub
parent 0bb418b4a7
commit f7afbfae85
2 changed files with 157 additions and 79 deletions
@@ -443,7 +443,7 @@ interface HermesApiService {
@GET("api/model/options") @GET("api/model/options")
suspend fun getModelOptions( suspend fun getModelOptions(
@Query("refresh") refresh: Boolean = false, @Query("refresh") refresh: Boolean = false,
@Query("include_unconfigured") includeUnconfigured: Boolean = true, @Query("include_unconfigured") includeUnconfigured: Boolean = false,
): Response<ModelOptionsResponse> ): Response<ModelOptionsResponse>
@GET("api/model/auxiliary") @GET("api/model/auxiliary")
@@ -79,10 +79,7 @@ class ModelViewModel :
fun loadAll(refresh: Boolean = false) { fun loadAll(refresh: Boolean = false) {
_uiState.update { it.copy(isLoading = true, errorMessage = null) } _uiState.update { it.copy(isLoading = true, errorMessage = null) }
viewModelScope.launch { viewModelScope.launch {
val optionsDeferred = // Phase 1: Launch fast lightweight calls first (profiles, aux, moa)
async(Dispatchers.IO) {
safeApiCall { ApiClient.hermesApi.getModelOptions(refresh = refresh) }
}
val activeProfileDeferred = val activeProfileDeferred =
async(Dispatchers.IO) { async(Dispatchers.IO) {
safeApiCall { ApiClient.hermesApi.getActiveProfile() } safeApiCall { ApiClient.hermesApi.getActiveProfile() }
@@ -100,55 +97,69 @@ class ModelViewModel :
safeApiCall { ApiClient.hermesApi.getMoaModels() } safeApiCall { ApiClient.hermesApi.getMoaModels() }
} }
val optionsResult = optionsDeferred.await() // Phase 2: Launch model options concurrently
val optionsDeferred =
async(Dispatchers.IO) {
safeApiCall { ApiClient.hermesApi.getModelOptions(refresh = refresh, includeUnconfigured = false) }
}
// Await Phase 1 calls (~40-190ms total) and update UI immediately with available state
val activeProfileResult = activeProfileDeferred.await() val activeProfileResult = activeProfileDeferred.await()
val profilesResult = profilesDeferred.await() val profilesResult = profilesDeferred.await()
val auxResult = auxDeferred.await() val auxResult = auxDeferred.await()
val moaResult = moaDeferred.await() val moaResult = moaDeferred.await()
val activeName =
if (activeProfileResult is NetworkResult.Success) {
activeProfileResult.data.active
} else {
null
}
val activeProfile =
if (profilesResult is NetworkResult.Success && activeName != null) {
profilesResult.data.profiles.find { it.name == activeName }
} else {
null
}
val mainModel =
if (auxResult is NetworkResult.Success) {
Pair(auxResult.data.main.provider, auxResult.data.main.model)
} else {
Pair("", "")
}
val auxTasks =
if (auxResult is NetworkResult.Success) {
auxResult.data.tasks
} else {
emptyList()
}
val moaConfig =
if (moaResult is NetworkResult.Success) {
moaResult.data
} else {
null
}
// Render Phase 1 results right away so UI controls update without waiting for model options
_uiState.update {
it.copy(
activeProfile = activeProfile ?: it.activeProfile,
pinnedModels = AuthManager.getPinnedModels(),
mainModelProvider = mainModel.first,
mainModelModel = mainModel.second,
auxTasks = auxTasks,
moaConfig = moaConfig ?: it.moaConfig,
)
}
// Await Phase 2 (model catalog option provider tree)
val optionsResult = optionsDeferred.await()
when (optionsResult) { when (optionsResult) {
is NetworkResult.Success -> { is NetworkResult.Success -> {
val activeName =
if (activeProfileResult is NetworkResult.Success) {
activeProfileResult.data.active
} else {
null
}
val activeProfile =
if (profilesResult is NetworkResult.Success && activeName != null) {
profilesResult.data.profiles.find { it.name == activeName }
} else {
null
}
val mainModel =
if (auxResult is NetworkResult.Success) {
Pair(auxResult.data.main.provider, auxResult.data.main.model)
} else {
Pair("", "")
}
val auxTasks =
if (auxResult is NetworkResult.Success) {
auxResult.data.tasks
} else {
emptyList()
}
val moaConfig =
if (moaResult is NetworkResult.Success) {
moaResult.data
} else {
null
}
_uiState.update { _uiState.update {
it.copy( it.copy(
isLoading = false, isLoading = false,
providers = optionsResult.data.providers.orEmpty(), providers = optionsResult.data.providers.orEmpty(),
activeProfile = activeProfile,
pinnedModels = AuthManager.getPinnedModels(),
mainModelProvider = mainModel.first,
mainModelModel = mainModel.second,
auxTasks = auxTasks,
moaConfig = moaConfig,
) )
} }
} }
@@ -157,7 +168,12 @@ class ModelViewModel :
_uiState.update { _uiState.update {
it.copy( it.copy(
isLoading = false, isLoading = false,
errorMessage = "Failed to load model options: ${optionsResult.error.message}", errorMessage =
if (it.providers.isEmpty()) {
"Failed to load model options: ${optionsResult.error.message}"
} else {
null
},
) )
} }
} }
@@ -165,6 +181,63 @@ class ModelViewModel :
} }
} }
/** Fast refresh after model assignments/updates without re-fetching full provider catalog. */
fun loadQuick() {
viewModelScope.launch {
val activeProfileDeferred =
async(Dispatchers.IO) {
safeApiCall { ApiClient.hermesApi.getActiveProfile() }
}
val profilesDeferred =
async(Dispatchers.IO) {
safeApiCall { ApiClient.hermesApi.getProfiles() }
}
val auxDeferred =
async(Dispatchers.IO) {
safeApiCall { ApiClient.hermesApi.getAuxiliaryModels() }
}
val activeProfileResult = activeProfileDeferred.await()
val profilesResult = profilesDeferred.await()
val auxResult = auxDeferred.await()
val activeName =
if (activeProfileResult is NetworkResult.Success) {
activeProfileResult.data.active
} else {
null
}
val activeProfile =
if (profilesResult is NetworkResult.Success && activeName != null) {
profilesResult.data.profiles.find { it.name == activeName }
} else {
null
}
val mainModel =
if (auxResult is NetworkResult.Success) {
Pair(auxResult.data.main.provider, auxResult.data.main.model)
} else {
Pair("", "")
}
val auxTasks =
if (auxResult is NetworkResult.Success) {
auxResult.data.tasks
} else {
emptyList()
}
_uiState.update {
it.copy(
activeProfile = activeProfile ?: it.activeProfile,
mainModelProvider = mainModel.first,
mainModelModel = mainModel.second,
auxTasks = auxTasks,
modelPickerBusy = false,
)
}
}
}
/** Open the main model picker dialog. */ /** Open the main model picker dialog. */
fun openMainModelPicker() { fun openMainModelPicker() {
_uiState.update { it.copy(showMainModelPicker = true) } _uiState.update { it.copy(showMainModelPicker = true) }
@@ -222,7 +295,7 @@ class ModelViewModel :
modelPickerBusy = false, modelPickerBusy = false,
) )
} }
loadAll() loadQuick()
} }
} }
@@ -265,7 +338,7 @@ class ModelViewModel :
modelPickerBusy = false, modelPickerBusy = false,
) )
} }
loadAll() loadQuick()
} }
is NetworkResult.Failure -> { is NetworkResult.Failure -> {
@@ -341,7 +414,7 @@ class ModelViewModel :
auxPickerTask = "", auxPickerTask = "",
) )
} }
loadAll() loadQuick()
} }
is NetworkResult.Failure -> { is NetworkResult.Failure -> {
@@ -377,7 +450,7 @@ class ModelViewModel :
_uiState.update { _uiState.update {
it.copy(toastMessage = "All auxiliary tasks reset to auto", modelPickerBusy = false) it.copy(toastMessage = "All auxiliary tasks reset to auto", modelPickerBusy = false)
} }
loadAll() loadQuick()
} }
is NetworkResult.Failure -> { is NetworkResult.Failure -> {
@@ -442,45 +515,50 @@ class ModelViewModel :
modelName: String, modelName: String,
) { ) {
viewModelScope.launch { viewModelScope.launch {
val activeProfileNameResResult = val cachedActiveProfileName = _uiState.value.activeProfile?.name
withContext(Dispatchers.IO) { val activeProfileName =
safeApiCall { ApiClient.hermesApi.getActiveProfile() } if (!cachedActiveProfileName.isNullOrBlank()) {
} cachedActiveProfileName
when (activeProfileNameResResult) { } else {
is NetworkResult.Failure -> { val activeProfileNameResResult =
_uiState.update {
it.copy(
toastMessage =
"Failed to fetch active profile: ${activeProfileNameResResult.error.message}",
)
}
return@launch
}
is NetworkResult.Success -> {
val activeProfileName = activeProfileNameResResult.data.active
val updateResResult =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
safeApiCall { safeApiCall { ApiClient.hermesApi.getActiveProfile() }
ApiClient.hermesApi.updateProfileModel(
activeProfileName,
UpdateProfileModelRequest(providerSlug, modelName),
)
}
} }
when (updateResResult) { when (activeProfileNameResResult) {
is NetworkResult.Success -> {
_uiState.update { it.copy(toastMessage = "Successfully set profile model to $modelName") }
loadAll()
}
is NetworkResult.Failure -> { is NetworkResult.Failure -> {
_uiState.update { _uiState.update {
it.copy( it.copy(
toastMessage = "Failed to set profile model: ${updateResResult.error.message}", toastMessage =
"Failed to fetch active profile: ${activeProfileNameResResult.error.message}",
) )
} }
return@launch
} }
is NetworkResult.Success -> activeProfileNameResResult.data.active
}
}
val updateResResult =
withContext(Dispatchers.IO) {
safeApiCall {
ApiClient.hermesApi.updateProfileModel(
activeProfileName,
UpdateProfileModelRequest(providerSlug, modelName),
)
}
}
when (updateResResult) {
is NetworkResult.Success -> {
_uiState.update { it.copy(toastMessage = "Successfully set profile model to $modelName") }
loadQuick()
}
is NetworkResult.Failure -> {
_uiState.update {
it.copy(
toastMessage = "Failed to set profile model: ${updateResResult.error.message}",
)
} }
} }
} }