This commit is contained in:
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,
|
||||
)
|
||||
}
|
||||
@@ -12,7 +12,8 @@ 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.ModelCatalogStore
|
||||
import com.m57.hermescontrol.data.ws.WsMethods
|
||||
import com.m57.hermescontrol.data.ws.contract.ConfigSetParams
|
||||
import com.m57.hermescontrol.data.ws.contract.RpcMethods
|
||||
import com.m57.hermescontrol.data.ws.toAny
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
@@ -51,7 +52,7 @@ class ChatModelSwitchDelegate(
|
||||
private val ioDispatcher: CoroutineDispatcher,
|
||||
private val uiState: MutableStateFlow<ChatUiState>,
|
||||
private val runtimeSessionId: () -> String?,
|
||||
private val wsSend: (method: String, params: Map<String, Any>, onSent: ((String) -> Unit)?) -> Unit,
|
||||
private val wsSend: (params: ConfigSetParams, onSent: ((String) -> Unit)?) -> Unit,
|
||||
private val trackRequest: (id: String, method: String) -> Unit,
|
||||
private val addAssistantMessage: (text: String) -> Unit,
|
||||
private val handleSlashCommand: (command: String) -> Unit,
|
||||
@@ -64,7 +65,7 @@ class ChatModelSwitchDelegate(
|
||||
{ refresh -> ModelCatalogStore.shared.ensureLoaded(forceRefresh = refresh) },
|
||||
private val getPinnedModels: () -> List<PinnedModel> = { AuthManager.getPinnedModels() },
|
||||
private val savePinnedModels: (List<PinnedModel>) -> Unit = { AuthManager.savePinnedModels(it) },
|
||||
private val wsRequest: (suspend (method: String, params: Map<String, Any>) -> Any?)? = null,
|
||||
private val wsRequest: (suspend (params: ConfigSetParams) -> JsonElement)? = null,
|
||||
) {
|
||||
private val pendingModelSwitchRequests = ConcurrentHashMap<String, ActiveModelSwitch>()
|
||||
private val pendingFastSwitchRequests = ConcurrentHashMap<String, ActiveFastSwitch>()
|
||||
@@ -300,20 +301,15 @@ class ChatModelSwitchDelegate(
|
||||
unconfirmedTargetModel = spec
|
||||
val switchSeq = ++modelSwitchSequence
|
||||
val params =
|
||||
mutableMapOf<String, Any>(
|
||||
"key" to "model",
|
||||
"value" to spec,
|
||||
"session_id" to sessionId,
|
||||
ConfigSetParams(
|
||||
key = "model",
|
||||
value = spec,
|
||||
sessionId = sessionId,
|
||||
confirmExpensiveModel = true.takeIf { confirmExpensive },
|
||||
)
|
||||
if (confirmExpensive) {
|
||||
params["confirm_expensive_model"] = true
|
||||
}
|
||||
scope.launch(ioDispatcher) {
|
||||
wsSend(
|
||||
WsMethods.CONFIG_SET,
|
||||
params,
|
||||
) { id ->
|
||||
trackRequest(id, WsMethods.CONFIG_SET)
|
||||
wsSend(params) { id ->
|
||||
trackRequest(id, RpcMethods.CONFIG_SET.name)
|
||||
pendingModelSwitchRequests[id] = ActiveModelSwitch(spec, previousModel, switchSeq)
|
||||
}
|
||||
}
|
||||
@@ -408,14 +404,13 @@ class ChatModelSwitchDelegate(
|
||||
uiState.update { it.copy(isFastModeChanging = true) }
|
||||
scope.launch(ioDispatcher) {
|
||||
wsSend(
|
||||
WsMethods.CONFIG_SET,
|
||||
mapOf(
|
||||
"key" to "fast",
|
||||
"value" to if (target) "fast" else "normal",
|
||||
"session_id" to sessionId,
|
||||
ConfigSetParams(
|
||||
key = "fast",
|
||||
value = if (target) "fast" else "normal",
|
||||
sessionId = sessionId,
|
||||
),
|
||||
) { id ->
|
||||
trackRequest(id, WsMethods.CONFIG_SET)
|
||||
trackRequest(id, RpcMethods.CONFIG_SET.name)
|
||||
pendingFastSwitchRequests[id] = ActiveFastSwitch(target)
|
||||
}
|
||||
}
|
||||
@@ -539,18 +534,18 @@ class ChatModelSwitchDelegate(
|
||||
scope.launch(ioDispatcher) {
|
||||
try {
|
||||
val params =
|
||||
mapOf(
|
||||
"key" to "reasoning",
|
||||
"value" to level,
|
||||
"session_id" to sessionId,
|
||||
"scope" to scopeName,
|
||||
ConfigSetParams(
|
||||
key = "reasoning",
|
||||
value = level,
|
||||
sessionId = sessionId,
|
||||
scope = scopeName,
|
||||
)
|
||||
val result =
|
||||
if (wsRequest != null) {
|
||||
wsRequest.invoke(WsMethods.CONFIG_SET, params)
|
||||
wsRequest.invoke(params)
|
||||
} else {
|
||||
wsSend(WsMethods.CONFIG_SET, params) { reqId ->
|
||||
trackRequest(reqId, WsMethods.CONFIG_SET)
|
||||
wsSend(params) { reqId ->
|
||||
trackRequest(reqId, RpcMethods.CONFIG_SET.name)
|
||||
}
|
||||
null
|
||||
}
|
||||
@@ -558,14 +553,7 @@ class ChatModelSwitchDelegate(
|
||||
if (opSeq == reasoningOpSequence && capturedScope == dataScopeFlow.value) {
|
||||
val ackLevel =
|
||||
if (wsRequest != null) {
|
||||
// HermesWsClient.request() completes with the RAW JsonElement.
|
||||
// Tests and a few injected callers may provide an already-decoded map.
|
||||
val map =
|
||||
when (result) {
|
||||
is JsonElement -> result.toAny() as? Map<*, *>
|
||||
is Map<*, *> -> result
|
||||
else -> null
|
||||
}
|
||||
val map = result?.toAny() as? Map<*, *>
|
||||
val resKey = map?.get("key") as? String
|
||||
val resVal = map?.get("value") as? String
|
||||
if (resKey != "reasoning" || resVal.isNullOrEmpty()) {
|
||||
|
||||
@@ -37,6 +37,8 @@ import com.m57.hermescontrol.data.ws.HermesWsClient
|
||||
import com.m57.hermescontrol.data.ws.JsonRpcError
|
||||
import com.m57.hermescontrol.data.ws.WsEvent
|
||||
import com.m57.hermescontrol.data.ws.WsMethods
|
||||
import com.m57.hermescontrol.data.ws.contract.ConfigGetParams
|
||||
import com.m57.hermescontrol.data.ws.contract.ConfigSetParams
|
||||
import com.m57.hermescontrol.data.ws.contract.PromptBtwParams
|
||||
import com.m57.hermescontrol.data.ws.contract.RpcMethod
|
||||
import com.m57.hermescontrol.data.ws.contract.RpcMethods
|
||||
@@ -694,13 +696,13 @@ class ChatViewModel(
|
||||
ioDispatcher = ioDispatcher,
|
||||
uiState = _uiState,
|
||||
runtimeSessionId = { runtimeSessionId },
|
||||
wsSend = { method, params, onSent -> wsClient.send(method, params, onSent) },
|
||||
wsSend = { params, onSent -> wsClient.send(RpcMethods.CONFIG_SET, params, onSent) },
|
||||
trackRequest = { id, method -> trackRequest(id, method) },
|
||||
addAssistantMessage = { text -> addAssistantMessage(text) },
|
||||
handleSlashCommand = { cmd -> handleSlashCommand(cmd) },
|
||||
fetchContextUsage = { fetchContextUsage() },
|
||||
onModelSwitchInitiated = { onModelSwitchInitiated() },
|
||||
wsRequest = { method, params -> wsClient.request(method, params).await() },
|
||||
wsRequest = { params -> wsClient.call(RpcMethods.CONFIG_SET, params) },
|
||||
).apply {
|
||||
attachScopeObserver(viewModelScope)
|
||||
}
|
||||
@@ -3846,14 +3848,10 @@ class ChatViewModel(
|
||||
viewModelScope.launch(ioDispatcher) {
|
||||
try {
|
||||
val result =
|
||||
wsClient
|
||||
.request(
|
||||
WsMethods.CONFIG_GET,
|
||||
mapOf(
|
||||
"key" to "reasoning",
|
||||
"session_id" to sessionId,
|
||||
),
|
||||
).await()
|
||||
wsClient.call(
|
||||
RpcMethods.CONFIG_GET,
|
||||
ConfigGetParams(key = "reasoning", sessionId = sessionId),
|
||||
)
|
||||
val map = rpcResultMap(result) ?: error("Invalid reasoning config.get response")
|
||||
val value = (map["value"] as? String)?.takeIf { it.isNotBlank() } ?: "unknown"
|
||||
val display = (map["display"] as? String)?.takeIf { it.isNotBlank() } ?: "unknown"
|
||||
@@ -3901,13 +3899,13 @@ class ChatViewModel(
|
||||
viewModelScope.launch(ioDispatcher) {
|
||||
try {
|
||||
val params =
|
||||
buildMap<String, Any> {
|
||||
put("key", "reasoning")
|
||||
put("value", parsed.value)
|
||||
put("session_id", sessionId)
|
||||
parsed.scopeName?.let { put("scope", it) }
|
||||
}
|
||||
val result = wsClient.request(WsMethods.CONFIG_SET, params).await()
|
||||
ConfigSetParams(
|
||||
key = "reasoning",
|
||||
value = parsed.value,
|
||||
sessionId = sessionId,
|
||||
scope = parsed.scopeName,
|
||||
)
|
||||
val result = wsClient.call(RpcMethods.CONFIG_SET, params)
|
||||
val map = rpcResultMap(result) ?: error("Invalid reasoning config.set response")
|
||||
val responseKey = map["key"] as? String
|
||||
val responseValue = map["value"] as? String
|
||||
|
||||
Reference in new issue
Block a user