feat(model-picker): each row names its connected provider
The shell stamps meta.provider (platform display name via
parse_managed_model_key) on every managed {platform}/{model} catalog
entry in to_acp_model_info; user-defined [model.*] entries stay
provider-less. The /model dropdown surfaces it in the description column
(a model's own description still wins), so the picker reads as 'choose a
model from your connected providers'.
Verified: kigi-shell 5261 + kigi-tui 6873 tests green, clippy clean.
This commit is contained in:
@@ -4229,6 +4229,21 @@ pub fn to_acp_model_info(
|
||||
"agentType".to_string(),
|
||||
serde_json::Value::String(info.agent_type.clone()),
|
||||
);
|
||||
// Provider display name for managed `{platform}/{model}`
|
||||
// entries — lets the client's model picker say which
|
||||
// connected provider each model belongs to. Absent for
|
||||
// user-defined `[model.*]` entries (no known platform).
|
||||
if let Some((platform, _)) = info
|
||||
.id
|
||||
.as_deref()
|
||||
.or(Some(key.as_str()))
|
||||
.and_then(kigi_models::parse_managed_model_key)
|
||||
{
|
||||
map.insert(
|
||||
"provider".to_string(),
|
||||
serde_json::Value::String(platform.display_name().to_string()),
|
||||
);
|
||||
}
|
||||
if info.supports_reasoning_effort {
|
||||
map.insert(
|
||||
"supportsReasoningEffort".to_string(),
|
||||
@@ -5925,6 +5940,49 @@ reasoning_effort = "low"
|
||||
"agentType should always be in meta, defaulting to DEFAULT_AGENT_TYPE"
|
||||
);
|
||||
}
|
||||
/// Managed `{platform}/{model}` entries stamp `meta.provider` with the
|
||||
/// platform's display name so the client's model picker can say which
|
||||
/// connected provider each model belongs to. User-defined `[model.*]`
|
||||
/// entries (no known platform prefix) stay provider-less.
|
||||
#[test]
|
||||
fn acp_model_meta_stamps_provider_for_managed_entries() {
|
||||
let mut models = IndexMap::new();
|
||||
let mut managed = test_model_entry(
|
||||
"claude-opus-4-8",
|
||||
"https://api.anthropic.com/v1",
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
managed.info.id = Some("claude-pro-max/claude-opus-4-8".to_string());
|
||||
models.insert("claude-pro-max/claude-opus-4-8".to_string(), managed);
|
||||
models.insert(
|
||||
"my-custom".to_string(),
|
||||
test_model_entry("my-custom", "https://test.api/v1", None, None, None),
|
||||
);
|
||||
|
||||
let acp_models = to_acp_model_info(&models);
|
||||
let claude = acp_models
|
||||
.get(&acp::ModelId::new(Arc::from(
|
||||
"claude-pro-max/claude-opus-4-8",
|
||||
)))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
claude.meta.as_ref().unwrap()["provider"],
|
||||
kigi_models::PlatformId::ClaudeProMax.display_name(),
|
||||
);
|
||||
let custom = acp_models
|
||||
.get(&acp::ModelId::new(Arc::from("my-custom")))
|
||||
.unwrap();
|
||||
assert!(
|
||||
custom
|
||||
.meta
|
||||
.as_ref()
|
||||
.is_none_or(|m| m.get("provider").is_none()),
|
||||
"a user-defined entry carries no provider"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn acp_model_meta_emits_reasoning_effort_when_supported() {
|
||||
let mut models = IndexMap::new();
|
||||
|
||||
@@ -150,6 +150,9 @@ fn detect_effort_phase(models: &ModelState, args_query: &str) -> Option<acp::Mod
|
||||
|
||||
/// One row per logical model. Reasoning models get a trailing space in
|
||||
/// `insert_text` so the prompt widget chains into the effort sub-menu.
|
||||
/// The description column names the model's provider (shell-stamped
|
||||
/// `meta.provider` — the connected platform the model was fetched from)
|
||||
/// when the model carries no description of its own.
|
||||
fn build_model_items(models: &ModelState) -> Vec<ArgItem> {
|
||||
let current_id = models.current.as_ref();
|
||||
let mut items: Vec<ArgItem> = Vec::with_capacity(models.available.len());
|
||||
@@ -172,11 +175,22 @@ fn build_model_items(models: &ModelState) -> Vec<ArgItem> {
|
||||
info.name.clone()
|
||||
};
|
||||
|
||||
let provider = info
|
||||
.meta
|
||||
.as_ref()
|
||||
.and_then(|m| m.get("provider"))
|
||||
.and_then(|v| v.as_str());
|
||||
let description = match (info.description.as_deref(), provider) {
|
||||
(Some(d), _) if !d.is_empty() => d.to_string(),
|
||||
(_, Some(p)) => p.to_string(),
|
||||
_ => String::new(),
|
||||
};
|
||||
|
||||
items.push(ArgItem {
|
||||
display,
|
||||
match_text: info.name.clone(),
|
||||
insert_text,
|
||||
description: info.description.clone().unwrap_or_default(),
|
||||
description,
|
||||
});
|
||||
}
|
||||
items
|
||||
@@ -294,6 +308,37 @@ mod tests {
|
||||
assert_eq!(plain.insert_text, "Kigi 4.5");
|
||||
}
|
||||
|
||||
/// Rows for shell-managed platform models show their provider (stamped
|
||||
/// `meta.provider`) in the description column, so the picker says which
|
||||
/// connected provider each model belongs to. A model's own description
|
||||
/// wins when present; entries without provider meta stay blank.
|
||||
#[test]
|
||||
fn model_rows_show_provider_in_description() {
|
||||
let mut state = ModelState::default();
|
||||
|
||||
let claude_id = acp::ModelId::new(Arc::from("claude-pro-max/claude-opus-4-8"));
|
||||
let mut meta = serde_json::Map::new();
|
||||
meta.insert(
|
||||
"provider".into(),
|
||||
serde_json::Value::String("Claude Pro/Max".into()),
|
||||
);
|
||||
let claude =
|
||||
acp::ModelInfo::new(claude_id.clone(), "Claude Opus 4.8".to_string()).meta(Some(meta));
|
||||
state.available.insert(claude_id, claude);
|
||||
|
||||
let (plain_id, plain_info) = plain_model("my-custom", "My Custom");
|
||||
state.available.insert(plain_id, plain_info);
|
||||
|
||||
let items = build_model_items(&state);
|
||||
let claude_row = items
|
||||
.iter()
|
||||
.find(|i| i.match_text == "Claude Opus 4.8")
|
||||
.unwrap();
|
||||
assert_eq!(claude_row.description, "Claude Pro/Max");
|
||||
let plain_row = items.iter().find(|i| i.match_text == "My Custom").unwrap();
|
||||
assert_eq!(plain_row.description, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trailing_space_after_reasoning_model_enters_effort_phase() {
|
||||
let mut state = ModelState::default();
|
||||
|
||||
Reference in New Issue
Block a user