feat(#445): full Models tab parity — settings, aux tasks, MOA config
* feat(#445): full Models tab — settings panel, aux tasks, MOA config, model assignment Main features added: - Model Settings card at top of screen with main model, aux tasks, MOA summary - Main model assignment via /api/model/set with expensive model confirm flow - Auxiliary tasks dialog with per-task model override and reset-all - MOA config dialog with preset management and reference/aggregator model picker - Model picker dialog reused for main model, aux tasks, and MOA slot selection - Kept existing provider browse + pin/unpin/search New data models: AuxiliaryModelsResponse, MoaConfigResponse, ModelAssignmentRequest/Response New API endpoints: GET /api/model/auxiliary, GET/PUT /api/model/moa, POST /api/model/set * test(#445): add aux + MOA mock endpoints to E2eIntegrationTest testModelOptionsSelection_success was failing because the ViewModel's init block now calls getAuxiliaryModels() and getMoaConfig() which weren't mocked. Added proper mock returns + imports for the new types. * fix(#445): correct mock endpoint name in E2eIntegrationTest - getMoaConfig -> getMoaModels (matches API service method name) - Add explicit type parameter Response.success<MoaConfigResponse> to fix Kotlin type inference * fix(#445): add missing main param to AuxiliaryModelsResponse mock AuxiliaryModelsResponse requires a MainModelAssignment for its 'main' field. Added the parameter with a dummy value and the corresponding import. * fix(#445): update toast assertion in test to match new message format ViewModel now produces 'Successfully set profile model to ronia' instead of the old 'Successfully set model to ronia'. --------- Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
1 parent
efb8e528ef
commit
dc808daee8
5 files changed
+1674
-397
No files matched your search
@@ -0,0 +1,64 @@
|
|||||||
|
package com.m57.hermescontrol.data.model
|
||||||
|
|
||||||
|
data class AuxiliaryTaskAssignment(
|
||||||
|
val task: String,
|
||||||
|
val provider: String,
|
||||||
|
val model: String,
|
||||||
|
val base_url: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class AuxiliaryModelsResponse(
|
||||||
|
val tasks: List<AuxiliaryTaskAssignment>,
|
||||||
|
val main: MainModelAssignment,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class MainModelAssignment(
|
||||||
|
val provider: String,
|
||||||
|
val model: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class MoaModelSlot(
|
||||||
|
val provider: String,
|
||||||
|
val model: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class MoaConfigPreset(
|
||||||
|
val reference_models: List<MoaModelSlot>,
|
||||||
|
val aggregator: MoaModelSlot,
|
||||||
|
val reference_temperature: Double = 0.7,
|
||||||
|
val aggregator_temperature: Double = 0.3,
|
||||||
|
val max_tokens: Int = 4096,
|
||||||
|
val enabled: Boolean = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class MoaConfigResponse(
|
||||||
|
val default_preset: String = "",
|
||||||
|
val active_preset: String = "",
|
||||||
|
val presets: Map<String, MoaConfigPreset> = emptyMap(),
|
||||||
|
val reference_models: List<MoaModelSlot> = emptyList(),
|
||||||
|
val aggregator: MoaModelSlot = MoaModelSlot("", ""),
|
||||||
|
val reference_temperature: Double = 0.7,
|
||||||
|
val aggregator_temperature: Double = 0.3,
|
||||||
|
val max_tokens: Int = 4096,
|
||||||
|
val enabled: Boolean = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ModelAssignmentRequest(
|
||||||
|
val confirm_expensive_model: Boolean = false,
|
||||||
|
val scope: String,
|
||||||
|
val provider: String,
|
||||||
|
val model: String,
|
||||||
|
val base_url: String? = null,
|
||||||
|
val task: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ModelAssignmentResponse(
|
||||||
|
val confirm_message: String? = null,
|
||||||
|
val confirm_required: Boolean? = null,
|
||||||
|
val ok: Boolean = false,
|
||||||
|
val scope: String? = null,
|
||||||
|
val provider: String? = null,
|
||||||
|
val model: String? = null,
|
||||||
|
val tasks: List<String>? = null,
|
||||||
|
val reset: Boolean? = null,
|
||||||
|
)
|
||||||
@@ -4,6 +4,7 @@ import com.google.gson.JsonElement
|
|||||||
import com.m57.hermescontrol.data.model.AchievementsResponse
|
import com.m57.hermescontrol.data.model.AchievementsResponse
|
||||||
import com.m57.hermescontrol.data.model.ActiveProfileResponse
|
import com.m57.hermescontrol.data.model.ActiveProfileResponse
|
||||||
import com.m57.hermescontrol.data.model.AgentPluginInstallBody
|
import com.m57.hermescontrol.data.model.AgentPluginInstallBody
|
||||||
|
import com.m57.hermescontrol.data.model.AuxiliaryModelsResponse
|
||||||
import com.m57.hermescontrol.data.model.ConfigSchemaResponse
|
import com.m57.hermescontrol.data.model.ConfigSchemaResponse
|
||||||
import com.m57.hermescontrol.data.model.ConfigUpdateRequest
|
import com.m57.hermescontrol.data.model.ConfigUpdateRequest
|
||||||
import com.m57.hermescontrol.data.model.CreateCronJobRequest
|
import com.m57.hermescontrol.data.model.CreateCronJobRequest
|
||||||
@@ -25,6 +26,9 @@ import com.m57.hermescontrol.data.model.McpServersResponse
|
|||||||
import com.m57.hermescontrol.data.model.MessagingPlatformResponse
|
import com.m57.hermescontrol.data.model.MessagingPlatformResponse
|
||||||
import com.m57.hermescontrol.data.model.MessagingPlatformTestResult
|
import com.m57.hermescontrol.data.model.MessagingPlatformTestResult
|
||||||
import com.m57.hermescontrol.data.model.MessagingPlatformUpdate
|
import com.m57.hermescontrol.data.model.MessagingPlatformUpdate
|
||||||
|
import com.m57.hermescontrol.data.model.MoaConfigResponse
|
||||||
|
import com.m57.hermescontrol.data.model.ModelAssignmentRequest
|
||||||
|
import com.m57.hermescontrol.data.model.ModelAssignmentResponse
|
||||||
import com.m57.hermescontrol.data.model.ModelOptionsResponse
|
import com.m57.hermescontrol.data.model.ModelOptionsResponse
|
||||||
import com.m57.hermescontrol.data.model.PairingApproveRequest
|
import com.m57.hermescontrol.data.model.PairingApproveRequest
|
||||||
import com.m57.hermescontrol.data.model.PairingResponse
|
import com.m57.hermescontrol.data.model.PairingResponse
|
||||||
@@ -285,6 +289,22 @@ interface HermesApiService {
|
|||||||
@GET("api/model/options")
|
@GET("api/model/options")
|
||||||
suspend fun getModelOptions(): Response<ModelOptionsResponse>
|
suspend fun getModelOptions(): Response<ModelOptionsResponse>
|
||||||
|
|
||||||
|
@GET("api/model/auxiliary")
|
||||||
|
suspend fun getAuxiliaryModels(): Response<AuxiliaryModelsResponse>
|
||||||
|
|
||||||
|
@GET("api/model/moa")
|
||||||
|
suspend fun getMoaModels(): Response<MoaConfigResponse>
|
||||||
|
|
||||||
|
@PUT("api/model/moa")
|
||||||
|
suspend fun saveMoaModels(
|
||||||
|
@Body body: MoaConfigResponse,
|
||||||
|
): Response<MoaConfigResponse>
|
||||||
|
|
||||||
|
@POST("api/model/set")
|
||||||
|
suspend fun setModelAssignment(
|
||||||
|
@Body body: ModelAssignmentRequest,
|
||||||
|
): Response<ModelAssignmentResponse>
|
||||||
|
|
||||||
@GET("api/logs")
|
@GET("api/logs")
|
||||||
suspend fun getLogs(
|
suspend fun getLogs(
|
||||||
@Query("limit") limit: Int? = null,
|
@Query("limit") limit: Int? = null,
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -3,6 +3,9 @@ package com.m57.hermescontrol.ui.model
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.m57.hermescontrol.data.local.AuthManager
|
import com.m57.hermescontrol.data.local.AuthManager
|
||||||
|
import com.m57.hermescontrol.data.model.AuxiliaryTaskAssignment
|
||||||
|
import com.m57.hermescontrol.data.model.MoaConfigResponse
|
||||||
|
import com.m57.hermescontrol.data.model.ModelAssignmentRequest
|
||||||
import com.m57.hermescontrol.data.model.ModelProvider
|
import com.m57.hermescontrol.data.model.ModelProvider
|
||||||
import com.m57.hermescontrol.data.model.PinnedModel
|
import com.m57.hermescontrol.data.model.PinnedModel
|
||||||
import com.m57.hermescontrol.data.model.ProfileInfo
|
import com.m57.hermescontrol.data.model.ProfileInfo
|
||||||
@@ -20,6 +23,22 @@ import kotlinx.coroutines.flow.update
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
data class AuxTaskInfo(
|
||||||
|
val key: String,
|
||||||
|
val label: String,
|
||||||
|
val hint: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
val AUX_TASKS =
|
||||||
|
listOf(
|
||||||
|
AuxTaskInfo("vision", "Vision", "image/video analysis"),
|
||||||
|
AuxTaskInfo("session_search", "Session search", "past conversation search"),
|
||||||
|
AuxTaskInfo("summarizer", "Compression", "context compression"),
|
||||||
|
AuxTaskInfo("code_exec", "Code execution", "runtime for code blocks"),
|
||||||
|
AuxTaskInfo("computer_use", "Computer use", "desktop/browser automation"),
|
||||||
|
AuxTaskInfo("web_search", "Web search", "live search queries"),
|
||||||
|
)
|
||||||
|
|
||||||
data class ModelUiState(
|
data class ModelUiState(
|
||||||
val isLoading: Boolean = false,
|
val isLoading: Boolean = false,
|
||||||
val providers: List<ModelProvider> = emptyList(),
|
val providers: List<ModelProvider> = emptyList(),
|
||||||
@@ -27,6 +46,24 @@ data class ModelUiState(
|
|||||||
val errorMessage: String? = null,
|
val errorMessage: String? = null,
|
||||||
val toastMessage: String? = null,
|
val toastMessage: String? = null,
|
||||||
val pinnedModels: List<PinnedModel> = emptyList(),
|
val pinnedModels: List<PinnedModel> = emptyList(),
|
||||||
|
// Main model
|
||||||
|
val mainModelProvider: String = "",
|
||||||
|
val mainModelModel: String = "",
|
||||||
|
// Auxiliary tasks
|
||||||
|
val auxTasks: List<AuxiliaryTaskAssignment> = emptyList(),
|
||||||
|
// MOA config
|
||||||
|
val moaConfig: MoaConfigResponse? = null,
|
||||||
|
// Dialog state
|
||||||
|
val showAuxDialog: Boolean = false,
|
||||||
|
val showMoaDialog: Boolean = false,
|
||||||
|
val showMainModelPicker: Boolean = false,
|
||||||
|
val showAuxModelPicker: Boolean = false,
|
||||||
|
val auxPickerTask: String = "",
|
||||||
|
// Model picker busy (for expensive model confirmation)
|
||||||
|
val modelPickerBusy: Boolean = false,
|
||||||
|
val modelPickerConfirmMessage: String? = null,
|
||||||
|
// Pending resolve
|
||||||
|
val pendingModelPickerResolve: ((Boolean) -> Unit)? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
class ModelViewModel :
|
class ModelViewModel :
|
||||||
@@ -39,37 +76,69 @@ class ModelViewModel :
|
|||||||
_uiState.update { it.copy(pinnedModels = AuthManager.getPinnedModels()) }
|
_uiState.update { it.copy(pinnedModels = AuthManager.getPinnedModels()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun loadAll() {
|
||||||
|
loadModelOptions()
|
||||||
|
}
|
||||||
|
|
||||||
fun loadModelOptions() {
|
fun loadModelOptions() {
|
||||||
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val responseResultDeferred =
|
val optionsDeferred =
|
||||||
async(Dispatchers.IO) {
|
async(Dispatchers.IO) {
|
||||||
safeApiCall { ApiClient.hermesApi.getModelOptions() }
|
safeApiCall { ApiClient.hermesApi.getModelOptions() }
|
||||||
}
|
}
|
||||||
val activeProfileNameResResultDeferred =
|
val activeProfileDeferred =
|
||||||
async(Dispatchers.IO) {
|
async(Dispatchers.IO) {
|
||||||
safeApiCall { ApiClient.hermesApi.getActiveProfile() }
|
safeApiCall { ApiClient.hermesApi.getActiveProfile() }
|
||||||
}
|
}
|
||||||
val profilesResResultDeferred =
|
val profilesDeferred =
|
||||||
async(Dispatchers.IO) {
|
async(Dispatchers.IO) {
|
||||||
safeApiCall { ApiClient.hermesApi.getProfiles() }
|
safeApiCall { ApiClient.hermesApi.getProfiles() }
|
||||||
}
|
}
|
||||||
|
val auxDeferred =
|
||||||
|
async(Dispatchers.IO) {
|
||||||
|
safeApiCall { ApiClient.hermesApi.getAuxiliaryModels() }
|
||||||
|
}
|
||||||
|
val moaDeferred =
|
||||||
|
async(Dispatchers.IO) {
|
||||||
|
safeApiCall { ApiClient.hermesApi.getMoaModels() }
|
||||||
|
}
|
||||||
|
|
||||||
val responseResult = responseResultDeferred.await()
|
val optionsResult = optionsDeferred.await()
|
||||||
val activeProfileNameResResult = activeProfileNameResResultDeferred.await()
|
val activeProfileResult = activeProfileDeferred.await()
|
||||||
val profilesResResult = profilesResResultDeferred.await()
|
val profilesResult = profilesDeferred.await()
|
||||||
|
val auxResult = auxDeferred.await()
|
||||||
|
val moaResult = moaDeferred.await()
|
||||||
|
|
||||||
when (responseResult) {
|
when (optionsResult) {
|
||||||
is NetworkResult.Success -> {
|
is NetworkResult.Success -> {
|
||||||
val activeName =
|
val activeName =
|
||||||
if (activeProfileNameResResult is NetworkResult.Success) {
|
if (activeProfileResult is NetworkResult.Success) {
|
||||||
activeProfileNameResResult.data.active
|
activeProfileResult.data.active
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
val activeProfile =
|
val activeProfile =
|
||||||
if (profilesResResult is NetworkResult.Success && activeName != null) {
|
if (profilesResult is NetworkResult.Success && activeName != null) {
|
||||||
profilesResResult.data.profiles.find { it.name == activeName }
|
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 {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
@@ -77,18 +146,21 @@ class ModelViewModel :
|
|||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
providers = responseResult.data.providers.orEmpty(),
|
providers = optionsResult.data.providers.orEmpty(),
|
||||||
activeProfile = activeProfile,
|
activeProfile = activeProfile,
|
||||||
pinnedModels = AuthManager.getPinnedModels(),
|
pinnedModels = AuthManager.getPinnedModels(),
|
||||||
|
mainModelProvider = mainModel.first,
|
||||||
|
mainModelModel = mainModel.second,
|
||||||
|
auxTasks = auxTasks,
|
||||||
|
moaConfig = moaConfig,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkResult.Failure -> {
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
errorMessage = "Failed to load model options: ${responseResult.error.message}",
|
errorMessage = "Failed to load model options: ${optionsResult.error.message}",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,6 +168,273 @@ class ModelViewModel :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Open the main model picker dialog. */
|
||||||
|
fun openMainModelPicker() {
|
||||||
|
_uiState.update { it.copy(showMainModelPicker = true) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun closeMainModelPicker() {
|
||||||
|
_uiState.update { it.copy(showMainModelPicker = false) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Set the main model via /api/model/set. */
|
||||||
|
fun setMainModel(
|
||||||
|
provider: String,
|
||||||
|
model: String,
|
||||||
|
) {
|
||||||
|
_uiState.update { it.copy(modelPickerBusy = true) }
|
||||||
|
viewModelScope.launch {
|
||||||
|
val result =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
safeApiCall {
|
||||||
|
ApiClient.hermesApi.setModelAssignment(
|
||||||
|
ModelAssignmentRequest(
|
||||||
|
scope = "main",
|
||||||
|
provider = provider,
|
||||||
|
model = model,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
when (result) {
|
||||||
|
is NetworkResult.Success -> {
|
||||||
|
val data = result.data
|
||||||
|
if (data.confirm_required == true) {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
modelPickerConfirmMessage =
|
||||||
|
data.confirm_message ?: "This model is expensive. Continue?",
|
||||||
|
pendingModelPickerResolve = { confirmed ->
|
||||||
|
if (confirmed) {
|
||||||
|
setMainModelWithConfirm(provider, model)
|
||||||
|
}
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
modelPickerConfirmMessage = null,
|
||||||
|
pendingModelPickerResolve = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
toastMessage = "Main model set to $provider / $model",
|
||||||
|
showMainModelPicker = false,
|
||||||
|
modelPickerBusy = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
loadModelOptions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is NetworkResult.Failure -> {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
toastMessage = "Failed to set main model: ${result.error.message}",
|
||||||
|
modelPickerBusy = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setMainModelWithConfirm(
|
||||||
|
provider: String,
|
||||||
|
model: String,
|
||||||
|
) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val result =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
safeApiCall {
|
||||||
|
ApiClient.hermesApi.setModelAssignment(
|
||||||
|
ModelAssignmentRequest(
|
||||||
|
confirm_expensive_model = true,
|
||||||
|
scope = "main",
|
||||||
|
provider = provider,
|
||||||
|
model = model,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
when (result) {
|
||||||
|
is NetworkResult.Success -> {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
toastMessage = "Main model set to $provider / $model",
|
||||||
|
showMainModelPicker = false,
|
||||||
|
modelPickerBusy = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
loadModelOptions()
|
||||||
|
}
|
||||||
|
is NetworkResult.Failure -> {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
toastMessage = "Failed to set main model: ${result.error.message}",
|
||||||
|
modelPickerBusy = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dismissModelPickerConfirm() {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
modelPickerConfirmMessage = null,
|
||||||
|
pendingModelPickerResolve = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_uiState.value.pendingModelPickerResolve?.invoke(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun confirmModelPickerExpensive() {
|
||||||
|
_uiState.value.pendingModelPickerResolve?.invoke(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------- Auxiliary tasks --------------------
|
||||||
|
|
||||||
|
fun openAuxDialog() {
|
||||||
|
_uiState.update { it.copy(showAuxDialog = true) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun closeAuxDialog() {
|
||||||
|
_uiState.update { it.copy(showAuxDialog = false) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun openAuxModelPicker(task: String) {
|
||||||
|
_uiState.update { it.copy(showAuxModelPicker = true, auxPickerTask = task) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun closeAuxModelPicker() {
|
||||||
|
_uiState.update { it.copy(showAuxModelPicker = false, auxPickerTask = "") }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Set an auxiliary task model override. */
|
||||||
|
fun setAuxTask(
|
||||||
|
task: String,
|
||||||
|
provider: String,
|
||||||
|
model: String,
|
||||||
|
) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val result =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
safeApiCall {
|
||||||
|
ApiClient.hermesApi.setModelAssignment(
|
||||||
|
ModelAssignmentRequest(
|
||||||
|
scope = "auxiliary",
|
||||||
|
task = task,
|
||||||
|
provider = provider,
|
||||||
|
model = model,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
when (result) {
|
||||||
|
is NetworkResult.Success -> {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
toastMessage = "Aux task '$task' set to $provider / $model",
|
||||||
|
showAuxModelPicker = false,
|
||||||
|
auxPickerTask = "",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
loadModelOptions()
|
||||||
|
}
|
||||||
|
is NetworkResult.Failure -> {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
toastMessage = "Failed to set aux task: ${result.error.message}",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reset all auxiliary tasks to auto. */
|
||||||
|
fun resetAllAuxTasks() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_uiState.update { it.copy(modelPickerBusy = true) }
|
||||||
|
val result =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
safeApiCall {
|
||||||
|
ApiClient.hermesApi.setModelAssignment(
|
||||||
|
ModelAssignmentRequest(
|
||||||
|
scope = "auxiliary",
|
||||||
|
task = "__reset__",
|
||||||
|
provider = "",
|
||||||
|
model = "",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
when (result) {
|
||||||
|
is NetworkResult.Success -> {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(toastMessage = "All auxiliary tasks reset to auto", modelPickerBusy = false)
|
||||||
|
}
|
||||||
|
loadModelOptions()
|
||||||
|
}
|
||||||
|
is NetworkResult.Failure -> {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
toastMessage = "Failed to reset aux tasks: ${result.error.message}",
|
||||||
|
modelPickerBusy = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------- MOA config --------------------
|
||||||
|
|
||||||
|
fun openMoaDialog() {
|
||||||
|
_uiState.update { it.copy(showMoaDialog = true) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun closeMoaDialog() {
|
||||||
|
_uiState.update { it.copy(showMoaDialog = false) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveMoaConfig(config: MoaConfigResponse) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_uiState.update { it.copy(modelPickerBusy = true) }
|
||||||
|
val result =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
safeApiCall {
|
||||||
|
ApiClient.hermesApi.saveMoaModels(config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
when (result) {
|
||||||
|
is NetworkResult.Success -> {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
moaConfig = result.data,
|
||||||
|
toastMessage = "MOA config saved",
|
||||||
|
showMoaDialog = false,
|
||||||
|
modelPickerBusy = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is NetworkResult.Failure -> {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
toastMessage = "Failed to save MOA config: ${result.error.message}",
|
||||||
|
modelPickerBusy = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------- Profile model selection (existing) --------------------
|
||||||
|
|
||||||
fun selectModel(
|
fun selectModel(
|
||||||
providerSlug: String,
|
providerSlug: String,
|
||||||
modelName: String,
|
modelName: String,
|
||||||
@@ -115,7 +454,6 @@ class ModelViewModel :
|
|||||||
}
|
}
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkResult.Success -> {
|
is NetworkResult.Success -> {
|
||||||
val activeProfileName = activeProfileNameResResult.data.active
|
val activeProfileName = activeProfileNameResResult.data.active
|
||||||
val updateResResult =
|
val updateResResult =
|
||||||
@@ -129,14 +467,13 @@ class ModelViewModel :
|
|||||||
}
|
}
|
||||||
when (updateResResult) {
|
when (updateResResult) {
|
||||||
is NetworkResult.Success -> {
|
is NetworkResult.Success -> {
|
||||||
_uiState.update { it.copy(toastMessage = "Successfully set model to $modelName") }
|
_uiState.update { it.copy(toastMessage = "Successfully set profile model to $modelName") }
|
||||||
loadModelOptions()
|
loadModelOptions()
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkResult.Failure -> {
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
toastMessage = "Failed to set model: ${updateResResult.error.message}",
|
toastMessage = "Failed to set profile model: ${updateResResult.error.message}",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import androidx.navigation3.runtime.NavBackStack
|
|||||||
import androidx.navigation3.runtime.NavKey
|
import androidx.navigation3.runtime.NavKey
|
||||||
import com.m57.hermescontrol.data.local.AuthManager
|
import com.m57.hermescontrol.data.local.AuthManager
|
||||||
import com.m57.hermescontrol.data.model.ActiveProfileResponse
|
import com.m57.hermescontrol.data.model.ActiveProfileResponse
|
||||||
|
import com.m57.hermescontrol.data.model.AuxiliaryModelsResponse
|
||||||
import com.m57.hermescontrol.data.model.CronJob
|
import com.m57.hermescontrol.data.model.CronJob
|
||||||
import com.m57.hermescontrol.data.model.DoctorResponse
|
import com.m57.hermescontrol.data.model.DoctorResponse
|
||||||
import com.m57.hermescontrol.data.model.EnvVarConfig
|
import com.m57.hermescontrol.data.model.EnvVarConfig
|
||||||
@@ -14,8 +15,11 @@ import com.m57.hermescontrol.data.model.KanbanBoardsResponse
|
|||||||
import com.m57.hermescontrol.data.model.KanbanColumn
|
import com.m57.hermescontrol.data.model.KanbanColumn
|
||||||
import com.m57.hermescontrol.data.model.KanbanTask
|
import com.m57.hermescontrol.data.model.KanbanTask
|
||||||
import com.m57.hermescontrol.data.model.LogResponse
|
import com.m57.hermescontrol.data.model.LogResponse
|
||||||
|
import com.m57.hermescontrol.data.model.MainModelAssignment
|
||||||
import com.m57.hermescontrol.data.model.MessagingPlatform
|
import com.m57.hermescontrol.data.model.MessagingPlatform
|
||||||
import com.m57.hermescontrol.data.model.MessagingPlatformResponse
|
import com.m57.hermescontrol.data.model.MessagingPlatformResponse
|
||||||
|
import com.m57.hermescontrol.data.model.MoaConfigResponse
|
||||||
|
import com.m57.hermescontrol.data.model.MoaModelSlot
|
||||||
import com.m57.hermescontrol.data.model.ModelOptionsResponse
|
import com.m57.hermescontrol.data.model.ModelOptionsResponse
|
||||||
import com.m57.hermescontrol.data.model.ModelProvider
|
import com.m57.hermescontrol.data.model.ModelProvider
|
||||||
import com.m57.hermescontrol.data.model.PluginInfo
|
import com.m57.hermescontrol.data.model.PluginInfo
|
||||||
@@ -1047,6 +1051,25 @@ class E2eIntegrationTest {
|
|||||||
Response.success(ActiveProfileResponse("default", null))
|
Response.success(ActiveProfileResponse("default", null))
|
||||||
coEvery { mockApiService.getProfiles() } returns Response.success(ProfilesResponse(listOf(profile)))
|
coEvery { mockApiService.getProfiles() } returns Response.success(ProfilesResponse(listOf(profile)))
|
||||||
coEvery { mockApiService.updateProfileModel("default", any()) } returns Response.success(Unit)
|
coEvery { mockApiService.updateProfileModel("default", any()) } returns Response.success(Unit)
|
||||||
|
coEvery { mockApiService.getAuxiliaryModels() } returns
|
||||||
|
Response.success(
|
||||||
|
AuxiliaryModelsResponse(
|
||||||
|
tasks = emptyList(),
|
||||||
|
main = MainModelAssignment("openai", "gpt-4"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
coEvery { mockApiService.getMoaModels() } returns
|
||||||
|
Response.success<MoaConfigResponse>(
|
||||||
|
MoaConfigResponse(
|
||||||
|
presets = emptyMap(),
|
||||||
|
default_preset = "",
|
||||||
|
reference_models = emptyList(),
|
||||||
|
aggregator = MoaModelSlot("openai", "gpt-4"),
|
||||||
|
reference_temperature = 0.7,
|
||||||
|
aggregator_temperature = 0.7,
|
||||||
|
max_tokens = 4096,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
val viewModel = ModelViewModel()
|
val viewModel = ModelViewModel()
|
||||||
viewModel.loadModelOptions()
|
viewModel.loadModelOptions()
|
||||||
@@ -1068,7 +1091,7 @@ class E2eIntegrationTest {
|
|||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
|
|
||||||
coVerify { mockApiService.updateProfileModel("default", any()) }
|
coVerify { mockApiService.updateProfileModel("default", any()) }
|
||||||
assertEquals("Successfully set model to llama3", viewModel.uiState.value.toastMessage)
|
assertEquals("Successfully set profile model to llama3", viewModel.uiState.value.toastMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
Reference in new issue
Block a user