refactor(#1164): load model inventory over WS with REST fallback

This commit is contained in:
M57 committed 2026-09-17 16:27:05 +03:00
1 parent 8007bd529d
commit be6104b903
4 files changed
+153 -3

No files matched your search

@@ -0,0 +1,50 @@
package com.m57.hermescontrol.data.ws
import com.m57.hermescontrol.data.model.ModelOptionsResponse
import com.m57.hermescontrol.data.remote.ApiClient
import com.m57.hermescontrol.data.remote.NetworkResult
import com.m57.hermescontrol.data.remote.OkHttpProvider
import com.m57.hermescontrol.data.remote.safeApiCall
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.ensureActive
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.decodeFromJsonElement
/** Issue #1164: profile-configured inventory; never pass a live chat session override. */
class ModelOptionsRepository(
private val connected: () -> Boolean = {
HermesWsClient.connectionStatus.value == ConnectionStatus.CONNECTED
},
private val request: suspend (Map<String, Any>) -> Any? = { params ->
val deferred = HermesWsClient.request(WsMethods.MODEL_OPTIONS, params)
try {
deferred.await()
} finally {
if (!deferred.isCompleted) deferred.cancel()
}
},
private val rest: suspend (Boolean) -> NetworkResult<ModelOptionsResponse> = { refresh ->
safeApiCall { ApiClient.hermesApi.getModelOptions(refresh = refresh, includeUnconfigured = false) }
},
) {
suspend fun load(refresh: Boolean = false): NetworkResult<ModelOptionsResponse> {
currentCoroutineContext().ensureActive()
if (connected()) {
try {
val result = request(mapOf("refresh" to refresh, "include_unconfigured" to false))
// request() returns raw JsonElement, unlike the pushed-event Map representation.
val json = result as? JsonElement ?: error("Invalid model.options result")
return NetworkResult.Success(OkHttpProvider.json.decodeFromJsonElement<ModelOptionsResponse>(json))
} catch (e: CancellationException) {
// A disconnected socket may cancel the independent RPC deferred. Only fall back
// if the screen's own coroutine is still active; never swallow caller cancellation.
currentCoroutineContext().ensureActive()
} catch (_: Exception) {
// Older gateways and malformed payloads keep the existing HTTP behavior.
}
}
currentCoroutineContext().ensureActive()
return rest(refresh)
}
}
@@ -28,6 +28,7 @@ object WsMethods {
const val SESSION_USAGE = "session.usage"
const val PROFILES_CONFIGURE = "profiles.configure"
const val PROFILES_LIST = "profiles.list"
const val MODEL_OPTIONS = "model.options"
// ── Projects ──────────────────────────────────────────────────────────
@@ -154,6 +155,7 @@ object WsMethods {
// Evidence: methods_session.py lines 42, 164, 232, 317, 845
SESSION_CREATE, // session.create
SESSION_LIST, // session.list
MODEL_OPTIONS, // model.options: @_profile_scoped
SESSION_RESUME, // session.resume
SESSION_DELETE, // session.delete
SESSION_STATUS, // session.status
@@ -13,6 +13,7 @@ import com.m57.hermescontrol.data.model.UpdateProfileModelRequest
import com.m57.hermescontrol.data.remote.ApiClient
import com.m57.hermescontrol.data.remote.NetworkResult
import com.m57.hermescontrol.data.remote.safeApiCall
import com.m57.hermescontrol.data.ws.ModelOptionsRepository
import com.m57.hermescontrol.ui.common.ToastHost
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
@@ -66,8 +67,9 @@ data class ModelUiState(
val pendingModelPickerResolve: ((Boolean) -> Unit)? = null,
)
class ModelViewModel :
ViewModel(),
class ModelViewModel(
private val modelOptionsRepository: ModelOptionsRepository = ModelOptionsRepository(),
) : ViewModel(),
ToastHost {
private val _uiState = MutableStateFlow(ModelUiState())
val uiState: StateFlow<ModelUiState> = _uiState.asStateFlow()
@@ -100,7 +102,7 @@ class ModelViewModel :
// Phase 2: Launch model options concurrently
val optionsDeferred =
async(Dispatchers.IO) {
safeApiCall { ApiClient.hermesApi.getModelOptions(refresh = refresh, includeUnconfigured = false) }
modelOptionsRepository.load(refresh)
}
// Await Phase 1 calls (~40-190ms total) and update UI immediately with available state