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
@@ -782,6 +782,7 @@ private fun ProviderCard(
|
||||
val isActive =
|
||||
activeProfile?.provider == provider.slug &&
|
||||
activeProfile.model == model
|
||||
val caps = provider.capabilities?.get(model)
|
||||
Card(
|
||||
onClick = { onModelClick(provider.slug, model) },
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
|
||||
@@ -810,21 +811,37 @@ private fun ProviderCard(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
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.
|
||||
Column(
|
||||
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(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
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
|
||||
* screen's pin section) so frequently-used models are one tap away.
|
||||
* [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
|
||||
fun ModelPickerDialog(
|
||||
@@ -123,6 +126,11 @@ fun ModelPickerDialog(
|
||||
pinnedModels.map { "${it.providerSlug}:${it.modelName}" }.toSet()
|
||||
}
|
||||
|
||||
val providerBySlug =
|
||||
remember(providers) {
|
||||
providers.associateBy { it.slug }
|
||||
}
|
||||
|
||||
val filteredProvidersWithModels =
|
||||
remember(pickerQuery, providers) {
|
||||
providers.mapNotNull { provider ->
|
||||
@@ -178,6 +186,7 @@ fun ModelPickerDialog(
|
||||
filteredPinned,
|
||||
key = { "pinned:${it.providerSlug}:${it.modelName}" },
|
||||
) { pinned ->
|
||||
val caps = providerBySlug[pinned.providerSlug]?.capabilities?.get(pinned.modelName)
|
||||
ModelItemCard(
|
||||
modelName = pinned.modelName,
|
||||
isPinned = true,
|
||||
@@ -188,6 +197,8 @@ fun ModelPickerDialog(
|
||||
null
|
||||
},
|
||||
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" },
|
||||
) { model ->
|
||||
val isPinned = "${provider.slug}:$model" in pinnedSet
|
||||
val caps = provider.capabilities?.get(model)
|
||||
ModelItemCard(
|
||||
modelName = model,
|
||||
isPinned = isPinned,
|
||||
@@ -229,6 +241,8 @@ fun ModelPickerDialog(
|
||||
null
|
||||
},
|
||||
onClick = { onSelect(provider.slug, model) },
|
||||
canDisableReasoning = caps?.can_disable_reasoning,
|
||||
supportsReasoning = caps?.reasoning,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -247,6 +261,8 @@ private fun ModelItemCard(
|
||||
onPinToggle: (() -> Unit)?,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
canDisableReasoning: Boolean? = null,
|
||||
supportsReasoning: Boolean? = null,
|
||||
) {
|
||||
Surface(
|
||||
modifier =
|
||||
@@ -267,15 +283,31 @@ private fun ModelItemCard(
|
||||
modifier = Modifier.padding(start = 12.dp, end = 4.dp, top = 4.dp, bottom = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
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.
|
||||
Column(
|
||||
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) {
|
||||
IconButton(
|
||||
onClick = onPinToggle,
|
||||
|
||||
Reference in new issue
Block a user