refactor(#1375): type config.set, config.get, model.options and client.capabilities (#1393)

This commit is contained in:
m57 authored and GitHub committed 2026-09-30 00:40:24 +03:00
1 parent cb0463aefc
commit 1456e642e3
12 files changed
+339 -207

No files matched your search

@@ -10,6 +10,7 @@ import com.m57.hermescontrol.data.remote.NetworkMonitor
import com.m57.hermescontrol.data.remote.OkHttpProvider
import com.m57.hermescontrol.data.remote.await
import com.m57.hermescontrol.data.session.ActiveSessionHolder
import com.m57.hermescontrol.data.ws.contract.ClientCapabilitiesParams
import com.m57.hermescontrol.data.ws.contract.PromptSubmitParams
import com.m57.hermescontrol.data.ws.contract.RpcMethod
import com.m57.hermescontrol.data.ws.contract.RpcMethods
@@ -364,8 +365,11 @@ object HermesWsClient {
// Older gateways may reject this method; that is harmless.
runCatching {
send(
WsMethods.CLIENT_CAPABILITIES,
mapOf("server_requests" to true),
RpcMethods.CLIENT_CAPABILITIES.name,
encodeParams(
RpcMethods.CLIENT_CAPABILITIES,
ClientCapabilitiesParams(serverRequests = true),
),
onSent = { id -> capabilityRequestIds.add(id) },
queueIfDisconnected = false,
)
@@ -5,6 +5,8 @@ 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 com.m57.hermescontrol.data.ws.contract.ModelOptionsParams
import com.m57.hermescontrol.data.ws.contract.RpcMethods
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.ensureActive
@@ -16,13 +18,8 @@ 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 request: suspend (ModelOptionsParams) -> JsonElement = { params ->
HermesWsClient.call(RpcMethods.MODEL_OPTIONS, params)
},
private val rest: suspend (Boolean) -> NetworkResult<ModelOptionsResponse> = { refresh ->
safeApiCall { ApiClient.hermesApi.getModelOptions(refresh = refresh, includeUnconfigured = false) }
@@ -32,9 +29,7 @@ class ModelOptionsRepository(
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")
val json = request(ModelOptionsParams(refresh = refresh, includeUnconfigured = false))
return NetworkResult.Success(OkHttpProvider.json.decodeFromJsonElement<ModelOptionsResponse>(json))
} catch (e: CancellationException) {
// A disconnected socket may cancel the independent RPC deferred. Only fall back
@@ -0,0 +1,41 @@
package com.m57.hermescontrol.data.ws.contract
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* `config.set` params. [key] and [value] are always encoded (no defaults); the rest are omitted
* when null. `profile` is injected by WsProfileParams, so it is not a field here.
*/
@Serializable
data class ConfigSetParams(
@SerialName("key") val key: String,
@SerialName("value") val value: String,
@SerialName("session_id") val sessionId: String? = null,
@SerialName("scope") val scope: String? = null,
@SerialName("confirm_expensive_model") val confirmExpensiveModel: Boolean? = null,
)
@Serializable
data class ConfigGetParams(
@SerialName("key") val key: String,
@SerialName("session_id") val sessionId: String? = null,
@SerialName("cwd") val cwd: String? = null,
)
/**
* `model.options` params. Flags are `Boolean?` so an explicit `false` is still sent while an
* unset flag is omitted (the client Json has `encodeDefaults = false`).
*/
@Serializable
data class ModelOptionsParams(
@SerialName("session_id") val sessionId: String? = null,
@SerialName("explicit_only") val explicitOnly: Boolean? = null,
@SerialName("include_unconfigured") val includeUnconfigured: Boolean? = null,
@SerialName("refresh") val refresh: Boolean? = null,
)
@Serializable
data class ClientCapabilitiesParams(
@SerialName("server_requests") val serverRequests: Boolean,
)
@@ -395,6 +395,38 @@ object RpcMethods {
JsonElement.serializer(),
)
// Passthrough JsonElement results: callers keep their existing legacy decoders.
val CONFIG_SET: RpcMethod<ConfigSetParams, JsonElement> =
RpcMethod(
WsMethods.CONFIG_SET,
ConfigSetParams.serializer(),
JsonElement.serializer(),
)
// Passthrough JsonElement results: callers keep their existing legacy decoders.
val CONFIG_GET: RpcMethod<ConfigGetParams, JsonElement> =
RpcMethod(
WsMethods.CONFIG_GET,
ConfigGetParams.serializer(),
JsonElement.serializer(),
)
// Passthrough JsonElement results: callers keep their existing legacy decoders.
val MODEL_OPTIONS: RpcMethod<ModelOptionsParams, JsonElement> =
RpcMethod(
WsMethods.MODEL_OPTIONS,
ModelOptionsParams.serializer(),
JsonElement.serializer(),
)
// Passthrough JsonElement results: callers keep their existing legacy decoders.
val CLIENT_CAPABILITIES: RpcMethod<ClientCapabilitiesParams, JsonElement> =
RpcMethod(
WsMethods.CLIENT_CAPABILITIES,
ClientCapabilitiesParams.serializer(),
JsonElement.serializer(),
)
/** Every typed method. GatewayContractTest iterates this; future migrations append here. */
val all: List<RpcMethod<*, *>> =
listOf(
@@ -446,5 +478,9 @@ object RpcMethods {
PROJECTS_LIST,
PROFILES_LIST,
PROFILES_CONFIGURE,
CONFIG_SET,
CONFIG_GET,
MODEL_OPTIONS,
CLIENT_CAPABILITIES,
)
}