Add ModelCapabilities (capabilities: Map<String, ModelCapabilities>?)
to ModelProvider matching backend /api/model/options shape
{model: {fast, reasoning, can_disable_reasoning}}. Nullable/defaulted
for backward compat with old backends (ignoreUnknownKeys+SnakeCase).
Chat composer: gate reasoning dropdown on can_disable_reasoning and
supportsReasoning — "None" disabled with "reasoning always on" hint
when can_disable==false, chip disabled with "No reasoning" when
reasoning==false. Missing keys offer full scale.
Sync currentModelCapabilities from cachedModelOptions on
SessionInfo, resume, preload/refresh, and optimistic /model swap;
clear on resetSessionState. Used by ComposerToolbar via ChatScreen.
Model picker: surface same hints in ModelPickerDialog (pinned+all)
and ModelScreen ProviderCard.
Verified: ktlintCheck OK, compileDebugKotlin OK (rerun-tasks).
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
1 parent
90c07e0aee
commit
e9f2c726e4
7 files changed
+168
-27
No files matched your search
@@ -18,6 +18,14 @@ data class ModelProvider(
|
|||||||
val authenticated: Boolean? = null,
|
val authenticated: Boolean? = null,
|
||||||
val auth_type: String? = null,
|
val auth_type: String? = null,
|
||||||
val warning: String? = null,
|
val warning: String? = null,
|
||||||
|
val capabilities: Map<String, ModelCapabilities>? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ModelCapabilities(
|
||||||
|
val fast: Boolean? = null,
|
||||||
|
val reasoning: Boolean? = null,
|
||||||
|
val can_disable_reasoning: Boolean? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|||||||
@@ -760,6 +760,8 @@ fun ChatScreen(
|
|||||||
reasoningLevel = state.reasoningLevel,
|
reasoningLevel = state.reasoningLevel,
|
||||||
onModelTap = { viewModel.openModelPicker() },
|
onModelTap = { viewModel.openModelPicker() },
|
||||||
onReasoningTap = { level -> viewModel.setReasoningLevel(level) },
|
onReasoningTap = { level -> viewModel.setReasoningLevel(level) },
|
||||||
|
canDisableReasoning = state.currentModelCapabilities?.can_disable_reasoning,
|
||||||
|
supportsReasoning = state.currentModelCapabilities?.reasoning,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import com.m57.hermescontrol.data.local.HermesDatabase
|
|||||||
import com.m57.hermescontrol.data.local.SlashUsageStore
|
import com.m57.hermescontrol.data.local.SlashUsageStore
|
||||||
import com.m57.hermescontrol.data.model.Attachment
|
import com.m57.hermescontrol.data.model.Attachment
|
||||||
import com.m57.hermescontrol.data.model.AttachmentSource
|
import com.m57.hermescontrol.data.model.AttachmentSource
|
||||||
|
import com.m57.hermescontrol.data.model.ModelCapabilities
|
||||||
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.SessionMessage
|
import com.m57.hermescontrol.data.model.SessionMessage
|
||||||
@@ -291,6 +292,9 @@ data class ChatUiState(
|
|||||||
val modelPickerLoading: Boolean = false,
|
val modelPickerLoading: Boolean = false,
|
||||||
// Current session's active model label (provider/model), shown in the chip
|
// Current session's active model label (provider/model), shown in the chip
|
||||||
val currentSessionModel: String? = null,
|
val currentSessionModel: String? = null,
|
||||||
|
// Per-model reasoning capabilities for the current session's model
|
||||||
|
// (issue #946). Null when unknown — UI offers full scale.
|
||||||
|
val currentModelCapabilities: ModelCapabilities? = null,
|
||||||
// Reasoning effort level for the current session
|
// Reasoning effort level for the current session
|
||||||
val reasoningLevel: String? = null,
|
val reasoningLevel: String? = null,
|
||||||
val terminalBackend: String? = null,
|
val terminalBackend: String? = null,
|
||||||
@@ -810,6 +814,7 @@ class ChatViewModel(
|
|||||||
fullContextTokens = if (modelSwapped) null else state.fullContextTokens,
|
fullContextTokens = if (modelSwapped) null else state.fullContextTokens,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
syncCurrentModelCapabilities()
|
||||||
if (modelSwapped) {
|
if (modelSwapped) {
|
||||||
// Issue #817: after a swap the REST model/info window is
|
// Issue #817: after a swap the REST model/info window is
|
||||||
// PROFILE-scoped and may describe the old model (e.g. a
|
// PROFILE-scoped and may describe the old model (e.g. a
|
||||||
@@ -1070,6 +1075,7 @@ class ChatViewModel(
|
|||||||
terminalBackend = terminalBackend ?: it.terminalBackend,
|
terminalBackend = terminalBackend ?: it.terminalBackend,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
syncCurrentModelCapabilities()
|
||||||
// Mirror the active runtime session id app-wide (issue #532).
|
// Mirror the active runtime session id app-wide (issue #532).
|
||||||
ActiveSessionHolder.set(runtimeSessionId ?: sessionId, sessionId)
|
ActiveSessionHolder.set(runtimeSessionId ?: sessionId, sessionId)
|
||||||
addSystemMessage("Session resumed")
|
addSystemMessage("Session resumed")
|
||||||
@@ -1836,6 +1842,7 @@ class ChatViewModel(
|
|||||||
if (result is NetworkResult.Success) {
|
if (result is NetworkResult.Success) {
|
||||||
cachedModelOptions = result.data.providers.orEmpty()
|
cachedModelOptions = result.data.providers.orEmpty()
|
||||||
_uiState.update { it.copy(modelPickerPinned = AuthManager.getPinnedModels()) }
|
_uiState.update { it.copy(modelPickerPinned = AuthManager.getPinnedModels()) }
|
||||||
|
syncCurrentModelCapabilities()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1878,6 +1885,7 @@ class ChatViewModel(
|
|||||||
modelPickerLoading = false,
|
modelPickerLoading = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
syncCurrentModelCapabilities()
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkResult.Failure -> {
|
is NetworkResult.Failure -> {
|
||||||
@@ -1932,6 +1940,7 @@ class ChatViewModel(
|
|||||||
currentSessionModel = "$provider/$model",
|
currentSessionModel = "$provider/$model",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
syncCurrentModelCapabilities()
|
||||||
// Model switch changes the context-window denominator — refetch it.
|
// Model switch changes the context-window denominator — refetch it.
|
||||||
fetchContextUsage()
|
fetchContextUsage()
|
||||||
handleSlashCommand("/model $model --provider $provider --session")
|
handleSlashCommand("/model $model --provider $provider --session")
|
||||||
@@ -1963,6 +1972,40 @@ class ChatViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getModelCapabilities(
|
||||||
|
providerSlug: String,
|
||||||
|
modelName: String,
|
||||||
|
): ModelCapabilities? = cachedModelOptions.find { it.slug == providerSlug }?.capabilities?.get(modelName)
|
||||||
|
|
||||||
|
fun getCurrentModelCapabilities(): ModelCapabilities? {
|
||||||
|
val label = _uiState.value.currentSessionModel ?: return null
|
||||||
|
val idx = label.indexOf('/')
|
||||||
|
if (idx <= 0) return null
|
||||||
|
val provider = label.substring(0, idx)
|
||||||
|
val model = label.substring(idx + 1)
|
||||||
|
return getModelCapabilities(provider, model)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun syncCurrentModelCapabilities() {
|
||||||
|
val label = _uiState.value.currentSessionModel
|
||||||
|
val caps =
|
||||||
|
if (label == null) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
val idx = label.indexOf('/')
|
||||||
|
if (idx <= 0) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
val provider = label.substring(0, idx)
|
||||||
|
val model = label.substring(idx + 1)
|
||||||
|
cachedModelOptions.find { it.slug == provider }?.capabilities?.get(model)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_uiState.update { state ->
|
||||||
|
if (state.currentModelCapabilities != caps) state.copy(currentModelCapabilities = caps) else state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun switchSession(sessionId: String) {
|
fun switchSession(sessionId: String) {
|
||||||
if (sessionId == _uiState.value.currentSessionId) return
|
if (sessionId == _uiState.value.currentSessionId) return
|
||||||
|
|
||||||
@@ -2167,6 +2210,7 @@ class ChatViewModel(
|
|||||||
showModelPicker = false,
|
showModelPicker = false,
|
||||||
modelPickerLoading = false,
|
modelPickerLoading = false,
|
||||||
currentSessionModel = null,
|
currentSessionModel = null,
|
||||||
|
currentModelCapabilities = null,
|
||||||
reasoningLevel = null,
|
reasoningLevel = null,
|
||||||
terminalBackend = null,
|
terminalBackend = null,
|
||||||
usedContextTokens = null,
|
usedContextTokens = null,
|
||||||
|
|||||||
@@ -89,6 +89,8 @@ fun ChatInputBar(
|
|||||||
reasoningLevel: String? = null,
|
reasoningLevel: String? = null,
|
||||||
onModelTap: () -> Unit = {},
|
onModelTap: () -> Unit = {},
|
||||||
onReasoningTap: (String?) -> Unit = {},
|
onReasoningTap: (String?) -> Unit = {},
|
||||||
|
canDisableReasoning: Boolean? = null,
|
||||||
|
supportsReasoning: Boolean? = null,
|
||||||
) {
|
) {
|
||||||
// Allow sending while the agent is mid-turn or awaiting approval: the
|
// Allow sending while the agent is mid-turn or awaiting approval: the
|
||||||
// gateway's prompt.submit busy-input policy queues it as the next turn
|
// gateway's prompt.submit busy-input policy queues it as the next turn
|
||||||
@@ -319,6 +321,8 @@ fun ChatInputBar(
|
|||||||
onReasoningSelected = onReasoningTap,
|
onReasoningSelected = onReasoningTap,
|
||||||
onMicTap = onMicTap,
|
onMicTap = onMicTap,
|
||||||
modifier = Modifier.testTag("chat_composer_toolbar"),
|
modifier = Modifier.testTag("chat_composer_toolbar"),
|
||||||
|
canDisableReasoning = canDisableReasoning,
|
||||||
|
supportsReasoning = supportsReasoning,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Attachment dropdown (anchored to the attach button in ComposerToolbar)
|
// Attachment dropdown (anchored to the attach button in ComposerToolbar)
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ import com.m57.hermescontrol.R
|
|||||||
* Layout: [📎 attach] [model chip] [←spacer→] [🧠 reasoning] [🎙 mic]
|
* Layout: [📎 attach] [model chip] [←spacer→] [🧠 reasoning] [🎙 mic]
|
||||||
*
|
*
|
||||||
* The reasoning chip opens a dropdown menu to pick a level (instead of cycling).
|
* The reasoning chip opens a dropdown menu to pick a level (instead of cycling).
|
||||||
|
* When [canDisableReasoning] is false the "None" level is disabled with a
|
||||||
|
* "reasoning always on" hint (issue #946). Absent key (null) means no
|
||||||
|
* restriction is known — full scale offered.
|
||||||
|
* When [supportsReasoning] is false the model takes no reasoning parameter
|
||||||
|
* and the chip is disabled.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun ComposerToolbar(
|
fun ComposerToolbar(
|
||||||
@@ -53,8 +58,12 @@ fun ComposerToolbar(
|
|||||||
onReasoningSelected: (String?) -> Unit,
|
onReasoningSelected: (String?) -> Unit,
|
||||||
onMicTap: () -> Unit,
|
onMicTap: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
canDisableReasoning: Boolean? = null,
|
||||||
|
supportsReasoning: Boolean? = null,
|
||||||
) {
|
) {
|
||||||
var showReasoningMenu by remember { mutableStateOf(false) }
|
var showReasoningMenu by remember { mutableStateOf(false) }
|
||||||
|
val reasoningDisabledForModel = supportsReasoning == false
|
||||||
|
val canDisable = canDisableReasoning
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
@@ -105,9 +114,15 @@ fun ComposerToolbar(
|
|||||||
FilterChip(
|
FilterChip(
|
||||||
selected = reasoningLevel != null,
|
selected = reasoningLevel != null,
|
||||||
onClick = { showReasoningMenu = true },
|
onClick = { showReasoningMenu = true },
|
||||||
|
enabled = !reasoningDisabledForModel,
|
||||||
label = {
|
label = {
|
||||||
Text(
|
Text(
|
||||||
text = buildReasoningLabel(reasoningLevel),
|
text =
|
||||||
|
if (reasoningDisabledForModel) {
|
||||||
|
"No reasoning"
|
||||||
|
} else {
|
||||||
|
buildReasoningLabel(reasoningLevel)
|
||||||
|
},
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
@@ -129,6 +144,22 @@ fun ComposerToolbar(
|
|||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
|
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||||
)
|
)
|
||||||
|
if (canDisable == false) {
|
||||||
|
Text(
|
||||||
|
text = "reasoning always on",
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
|
||||||
|
modifier = Modifier.padding(horizontal = 12.dp, vertical = 2.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (reasoningDisabledForModel) {
|
||||||
|
Text(
|
||||||
|
text = "no reasoning parameter for this model",
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
|
||||||
|
modifier = Modifier.padding(horizontal = 12.dp, vertical = 2.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
val allLevels =
|
val allLevels =
|
||||||
listOf(
|
listOf(
|
||||||
@@ -142,6 +173,8 @@ fun ComposerToolbar(
|
|||||||
"ultra" to "Ultra",
|
"ultra" to "Ultra",
|
||||||
)
|
)
|
||||||
allLevels.forEach { (level, label) ->
|
allLevels.forEach { (level, label) ->
|
||||||
|
val isNone = level == "none"
|
||||||
|
val noneDisabled = isNone && (canDisable == false || reasoningDisabledForModel)
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = {
|
text = {
|
||||||
Text(
|
Text(
|
||||||
@@ -153,10 +186,10 @@ fun ComposerToolbar(
|
|||||||
null
|
null
|
||||||
},
|
},
|
||||||
color =
|
color =
|
||||||
if (reasoningLevel == level) {
|
when {
|
||||||
MaterialTheme.colorScheme.primary
|
noneDisabled -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
|
||||||
} else {
|
reasoningLevel == level -> MaterialTheme.colorScheme.primary
|
||||||
MaterialTheme.colorScheme.onSurface
|
else -> MaterialTheme.colorScheme.onSurface
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -164,6 +197,7 @@ fun ComposerToolbar(
|
|||||||
showReasoningMenu = false
|
showReasoningMenu = false
|
||||||
onReasoningSelected(level)
|
onReasoningSelected(level)
|
||||||
},
|
},
|
||||||
|
enabled = !noneDisabled && !reasoningDisabledForModel,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -782,6 +782,7 @@ private fun ProviderCard(
|
|||||||
val isActive =
|
val isActive =
|
||||||
activeProfile?.provider == provider.slug &&
|
activeProfile?.provider == provider.slug &&
|
||||||
activeProfile.model == model
|
activeProfile.model == model
|
||||||
|
val caps = provider.capabilities?.get(model)
|
||||||
Card(
|
Card(
|
||||||
onClick = { onModelClick(provider.slug, model) },
|
onClick = { onModelClick(provider.slug, model) },
|
||||||
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
|
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
|
||||||
@@ -810,21 +811,37 @@ private fun ProviderCard(
|
|||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Text(
|
Column(
|
||||||
text = model,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
fontWeight = if (isActive) FontWeight.Bold else FontWeight.Normal,
|
|
||||||
color =
|
|
||||||
if (isActive) {
|
|
||||||
MaterialTheme.colorScheme.onPrimaryContainer
|
|
||||||
} else {
|
|
||||||
MaterialTheme.colorScheme.onSurface
|
|
||||||
},
|
|
||||||
// Weight + wrap: a long model name grows
|
|
||||||
// the row vertically instead of pushing
|
|
||||||
// the check/pin buttons off-screen.
|
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
)
|
) {
|
||||||
|
Text(
|
||||||
|
text = model,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
fontWeight = if (isActive) FontWeight.Bold else FontWeight.Normal,
|
||||||
|
color =
|
||||||
|
if (isActive) {
|
||||||
|
MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.onSurface
|
||||||
|
},
|
||||||
|
// Weight + wrap: a long model name grows
|
||||||
|
// the row vertically instead of pushing
|
||||||
|
// the check/pin buttons off-screen.
|
||||||
|
)
|
||||||
|
val hint =
|
||||||
|
when {
|
||||||
|
caps?.reasoning == false -> "no reasoning"
|
||||||
|
caps?.can_disable_reasoning == false -> "reasoning always on"
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
if (hint != null) {
|
||||||
|
Text(
|
||||||
|
text = hint,
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
|||||||
@@ -50,6 +50,9 @@ import com.m57.hermescontrol.ui.common.SearchBar
|
|||||||
* [pinnedModels] renders a pinned section at the top (mirrors the global model
|
* [pinnedModels] renders a pinned section at the top (mirrors the global model
|
||||||
* screen's pin section) so frequently-used models are one tap away.
|
* screen's pin section) so frequently-used models are one tap away.
|
||||||
* [onPinToggle] allows pinning and unpinning models directly inside the dialog.
|
* [onPinToggle] allows pinning and unpinning models directly inside the dialog.
|
||||||
|
* Per-model reasoning capabilities (issue #946) are surfaced as a small hint
|
||||||
|
* when `can_disable_reasoning == false` ("reasoning always on") or
|
||||||
|
* `reasoning == false` ("no reasoning").
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun ModelPickerDialog(
|
fun ModelPickerDialog(
|
||||||
@@ -123,6 +126,11 @@ fun ModelPickerDialog(
|
|||||||
pinnedModels.map { "${it.providerSlug}:${it.modelName}" }.toSet()
|
pinnedModels.map { "${it.providerSlug}:${it.modelName}" }.toSet()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val providerBySlug =
|
||||||
|
remember(providers) {
|
||||||
|
providers.associateBy { it.slug }
|
||||||
|
}
|
||||||
|
|
||||||
val filteredProvidersWithModels =
|
val filteredProvidersWithModels =
|
||||||
remember(pickerQuery, providers) {
|
remember(pickerQuery, providers) {
|
||||||
providers.mapNotNull { provider ->
|
providers.mapNotNull { provider ->
|
||||||
@@ -178,6 +186,7 @@ fun ModelPickerDialog(
|
|||||||
filteredPinned,
|
filteredPinned,
|
||||||
key = { "pinned:${it.providerSlug}:${it.modelName}" },
|
key = { "pinned:${it.providerSlug}:${it.modelName}" },
|
||||||
) { pinned ->
|
) { pinned ->
|
||||||
|
val caps = providerBySlug[pinned.providerSlug]?.capabilities?.get(pinned.modelName)
|
||||||
ModelItemCard(
|
ModelItemCard(
|
||||||
modelName = pinned.modelName,
|
modelName = pinned.modelName,
|
||||||
isPinned = true,
|
isPinned = true,
|
||||||
@@ -188,6 +197,8 @@ fun ModelPickerDialog(
|
|||||||
null
|
null
|
||||||
},
|
},
|
||||||
onClick = { onSelect(pinned.providerSlug, pinned.modelName) },
|
onClick = { onSelect(pinned.providerSlug, pinned.modelName) },
|
||||||
|
canDisableReasoning = caps?.can_disable_reasoning,
|
||||||
|
supportsReasoning = caps?.reasoning,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -219,6 +230,7 @@ fun ModelPickerDialog(
|
|||||||
key = { model -> "${provider.slug}:$model" },
|
key = { model -> "${provider.slug}:$model" },
|
||||||
) { model ->
|
) { model ->
|
||||||
val isPinned = "${provider.slug}:$model" in pinnedSet
|
val isPinned = "${provider.slug}:$model" in pinnedSet
|
||||||
|
val caps = provider.capabilities?.get(model)
|
||||||
ModelItemCard(
|
ModelItemCard(
|
||||||
modelName = model,
|
modelName = model,
|
||||||
isPinned = isPinned,
|
isPinned = isPinned,
|
||||||
@@ -229,6 +241,8 @@ fun ModelPickerDialog(
|
|||||||
null
|
null
|
||||||
},
|
},
|
||||||
onClick = { onSelect(provider.slug, model) },
|
onClick = { onSelect(provider.slug, model) },
|
||||||
|
canDisableReasoning = caps?.can_disable_reasoning,
|
||||||
|
supportsReasoning = caps?.reasoning,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -247,6 +261,8 @@ private fun ModelItemCard(
|
|||||||
onPinToggle: (() -> Unit)?,
|
onPinToggle: (() -> Unit)?,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
canDisableReasoning: Boolean? = null,
|
||||||
|
supportsReasoning: Boolean? = null,
|
||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
modifier =
|
modifier =
|
||||||
@@ -267,15 +283,31 @@ private fun ModelItemCard(
|
|||||||
modifier = Modifier.padding(start = 12.dp, end = 4.dp, top = 4.dp, bottom = 4.dp),
|
modifier = Modifier.padding(start = 12.dp, end = 4.dp, top = 4.dp, bottom = 4.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Text(
|
Column(
|
||||||
text = modelName,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
|
||||||
// No maxLines/ellipsis: a long model name wraps so the full
|
|
||||||
// name stays readable while the pin button keeps its spot.
|
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
)
|
) {
|
||||||
|
Text(
|
||||||
|
text = modelName,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
// No maxLines/ellipsis: a long model name wraps so the full
|
||||||
|
// name stays readable while the pin button keeps its spot.
|
||||||
|
)
|
||||||
|
val hint =
|
||||||
|
when {
|
||||||
|
supportsReasoning == false -> "no reasoning"
|
||||||
|
canDisableReasoning == false -> "reasoning always on"
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
if (hint != null) {
|
||||||
|
Text(
|
||||||
|
text = hint,
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
if (onPinToggle != null) {
|
if (onPinToggle != null) {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = onPinToggle,
|
onClick = onPinToggle,
|
||||||
|
|||||||
Reference in new issue
Block a user