diff --git a/app/src/main/java/com/m57/hermescontrol/data/model/ModelAssignment.kt b/app/src/main/java/com/m57/hermescontrol/data/model/ModelAssignment.kt index ec25c2af..3a51c9b4 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/model/ModelAssignment.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/model/ModelAssignment.kt @@ -25,14 +25,20 @@ data class MainModelAssignment( data class MoaModelSlot( val provider: String, val model: String, + val reasoning_effort: String? = null, + val enabled: Boolean = true, ) @Serializable data class MoaConfigPreset( val reference_models: List, val aggregator: MoaModelSlot, - val reference_temperature: Double = 0.7, - val aggregator_temperature: Double = 0.3, + // null = omitted from API calls (provider default); the backend sends null when unset. + val reference_temperature: Double? = null, + val aggregator_temperature: Double? = null, + val reference_timeout: Double? = null, + val degraded_reference_policy: String = "loud", + val fanout: String? = null, val max_tokens: Int = 4096, val enabled: Boolean = true, ) @@ -44,8 +50,12 @@ data class MoaConfigResponse( val presets: Map = emptyMap(), val reference_models: List = emptyList(), val aggregator: MoaModelSlot = MoaModelSlot("", ""), - val reference_temperature: Double = 0.7, - val aggregator_temperature: Double = 0.3, + // null = omitted from API calls (provider default); the backend sends null when unset. + val reference_temperature: Double? = null, + val aggregator_temperature: Double? = null, + val reference_timeout: Double? = null, + val degraded_reference_policy: String = "loud", + val fanout: String? = null, val max_tokens: Int = 4096, val enabled: Boolean = true, ) diff --git a/app/src/main/java/com/m57/hermescontrol/ui/chat/components/ChatLifecycleEffects.kt b/app/src/main/java/com/m57/hermescontrol/ui/chat/components/ChatLifecycleEffects.kt index b6161f6d..da4353e4 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/chat/components/ChatLifecycleEffects.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/chat/components/ChatLifecycleEffects.kt @@ -19,6 +19,7 @@ import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.platform.ClipEntry import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.core.content.ContextCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver @@ -146,7 +147,7 @@ fun ChatLifecycleEffects( // Show error as snackbar val clipboard = LocalClipboard.current - val copyLabel = context.getString(R.string.action_copy_error) + val copyLabel = stringResource(R.string.action_copy_error) // Own scope + immediate clear: a later `errorMessage = null` elsewhere must not cancel a visible popup. val snackbarScope = rememberCoroutineScope() LaunchedEffect(errorMessage) { diff --git a/app/src/test/java/com/m57/hermescontrol/data/model/ModelSerializationTest.kt b/app/src/test/java/com/m57/hermescontrol/data/model/ModelSerializationTest.kt index 2fda9252..898e1dd4 100644 --- a/app/src/test/java/com/m57/hermescontrol/data/model/ModelSerializationTest.kt +++ b/app/src/test/java/com/m57/hermescontrol/data/model/ModelSerializationTest.kt @@ -622,4 +622,33 @@ class ModelSerializationTest { json.decodeFromString(jsonInput) } } + + @Test + fun moaConfigDecodesNullTemperaturesAndNewFields() { + val payload = + """ + {"default_preset":"default","active_preset":"","privacy_filter":"", + "presets":{"default":{"enabled":true, + "reference_models":[{"provider":"openrouter","model":"x/y","enabled":false,"reasoning_effort":"high"}], + "aggregator":{"provider":"openrouter","model":"a/b"}, + "reference_temperature":null,"aggregator_temperature":null, + "reference_timeout":null,"degraded_reference_policy":"silent","fanout":"every_n:3"}}, + "reference_models":[{"provider":"openrouter","model":"x/y","enabled":true}], + "aggregator":{"provider":"openrouter","model":"a/b"}, + "reference_temperature":null,"aggregator_temperature":null, + "reference_timeout":null,"degraded_reference_policy":"silent","fanout":"every_n:3","enabled":true} + """.trimIndent() + val cfg = json.decodeFromString(payload) + assertNull(cfg.reference_temperature) + assertNull(cfg.aggregator_temperature) + assertEquals("every_n:3", cfg.fanout) + val preset = cfg.presets.getValue("default") + assertEquals("silent", preset.degraded_reference_policy) + assertEquals(false, preset.reference_models.first().enabled) + assertEquals("high", preset.reference_models.first().reasoning_effort) + // Round-trip keeps the non-default knobs so a save doesn't reset them. + val back = json.encodeToString(cfg) + assertTrue(back.contains("every_n:3")) + assertTrue(back.contains("silent")) + } }