use crate::types::{AgentSummary, ResolvedModelAvailability};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum ModelPickerChoice {
InheritDefault,
Model { model: String },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct ModelPickerRow {
pub(super) choice: ModelPickerChoice,
pub(super) title: String,
pub(super) detail: String,
pub(super) searchable: String,
pub(super) available: bool,
}
pub(super) fn model_picker_rows(
agent: Option<&AgentSummary>,
model_availability: &[ResolvedModelAvailability],
filter: &str,
) -> Vec<ModelPickerRow> {
let Some(agent) = agent else {
return Vec::new();
};
let inherit_row = inherit_default_row(agent);
let query = filter.trim().to_ascii_lowercase();
let model_rows = model_availability
.iter()
.filter(|entry| entry.available)
.map(model_availability_row);
if query.is_empty() {
let mut rows = vec![inherit_row];
rows.extend(model_rows);
return rows;
}
let mut rows = vec![inherit_row];
rows.extend(model_rows.filter(|row| row.searchable.contains(&query)));
rows
}
pub(super) fn selected_model_choice(
agent: Option<&AgentSummary>,
model_availability: &[ResolvedModelAvailability],
filter: &str,
selected: usize,
) -> Option<ModelPickerChoice> {
model_picker_rows(agent, model_availability, filter)
.into_iter()
.nth(selected)
.map(|row| row.choice)
}
pub(super) fn clamp_model_picker_selection(
agent: Option<&AgentSummary>,
model_availability: &[ResolvedModelAvailability],
filter: &str,
selected: usize,
) -> usize {
let len = model_picker_rows(agent, model_availability, filter).len();
if len == 0 {
0
} else {
selected.min(len - 1)
}
}
fn inherit_default_row(agent: &AgentSummary) -> ModelPickerRow {
let default_model = agent.model.runtime_default_model.as_string();
let current = agent.model.override_model.is_none();
let title = if current {
format!("inherit runtime default: {default_model} (current)")
} else {
format!("inherit runtime default: {default_model}")
};
ModelPickerRow {
choice: ModelPickerChoice::InheritDefault,
detail: "clear this agent's model override".into(),
searchable: format!("inherit default runtime {default_model}").to_ascii_lowercase(),
title,
available: true,
}
}
fn model_availability_row(entry: &ResolvedModelAvailability) -> ModelPickerRow {
let status = if entry.available {
"ready".to_string()
} else {
format!(
"unavailable: {}",
entry
.unavailable_reason
.as_deref()
.unwrap_or("provider unavailable")
)
};
let provider = entry
.provider_source
.as_deref()
.map(|source| format!("{} provider:{source}", entry.provider))
.unwrap_or_else(|| entry.provider.clone());
let transport = entry
.transport
.as_deref()
.map(|transport| format!(" transport:{transport}"))
.unwrap_or_default();
ModelPickerRow {
choice: ModelPickerChoice::Model {
model: entry.model.clone(),
},
title: format!("{} {}", entry.model, entry.display_name),
detail: format!(
"{status} {provider}{transport} source:{}",
entry.metadata_source
),
searchable: format!(
"{} {} {} {} {}",
entry.model, entry.display_name, entry.provider, entry.metadata_source, status
)
.to_ascii_lowercase(),
available: entry.available,
}
}
#[cfg(test)]
mod tests {
use super::{clamp_model_picker_selection, model_picker_rows, selected_model_choice};
use crate::system::{ExecutionProfile, ExecutionSnapshot};
use crate::{
config::{ModelRef, ProviderId},
model_catalog::{ModelCapabilityFlags, ModelMetadataSource, ResolvedRuntimeModelPolicy},
types::{
AgentIdentityView, AgentKind, AgentLifecycleHint, AgentModelSource, AgentModelState,
AgentOwnership, AgentProfilePreset, AgentRegistryStatus, AgentState, AgentSummary,
AgentTokenUsageSummary, AgentVisibility, ChildAgentSummary, ClosureDecision,
ClosureOutcome, LoadedAgentsMdView, ResolvedModelAvailability, RuntimePosture,
SkillsRuntimeView, TokenUsage, WaitingIntentSummary,
},
};
fn policy(model: &str, display_name: &str) -> ResolvedRuntimeModelPolicy {
ResolvedRuntimeModelPolicy {
model_ref: ModelRef::parse(model).unwrap(),
display_name: display_name.into(),
description: "test".into(),
context_window_tokens: Some(200_000),
effective_context_window_percent: 90,
prompt_budget_estimated_tokens: 180_000,
compaction_trigger_estimated_tokens: 180_000,
compaction_keep_recent_estimated_tokens: 68_400,
runtime_max_output_tokens: 32_000,
tool_output_truncation_estimated_tokens: 2_500,
max_output_tokens_upper_limit: Some(128_000),
capabilities: ModelCapabilityFlags::default(),
source: ModelMetadataSource::BuiltInCatalog,
}
}
fn availability(model: &str, display_name: &str, available: bool) -> ResolvedModelAvailability {
let model_ref = ModelRef::parse(model).unwrap();
ResolvedModelAvailability {
model: model.into(),
provider: model_ref.provider.as_str().into(),
display_name: display_name.into(),
metadata_source: "built_in_catalog".into(),
provider_configured: true,
provider_source: Some("built_in".into()),
transport: Some("chat_completions".into()),
credential_source: Some("env".into()),
credential_kind: Some("api_key".into()),
credential_configured: available,
available,
unavailable_reason: (!available).then_some("credential_missing".into()),
policy: policy(model, display_name),
}
}
fn model_availability() -> Vec<ResolvedModelAvailability> {
vec![
availability("openai/gpt-5.4", "GPT-5.4", true),
availability("anthropic/claude-sonnet-4-6", "Claude Sonnet 4.6", false),
]
}
fn summary() -> AgentSummary {
AgentSummary {
identity: AgentIdentityView {
agent_id: "default".into(),
kind: AgentKind::Default,
visibility: AgentVisibility::Public,
ownership: AgentOwnership::SelfOwned,
profile_preset: AgentProfilePreset::PublicNamed,
status: AgentRegistryStatus::Active,
is_default_agent: true,
parent_agent_id: None,
lineage_parent_agent_id: None,
delegated_from_task_id: None,
},
agent: AgentState::new("default"),
active_task_count: 0,
lifecycle: AgentLifecycleHint::default(),
scheduling_posture: Default::default(),
model: AgentModelState {
source: AgentModelSource::RuntimeDefault,
runtime_default_model: ModelRef::new(ProviderId::openai(), "gpt-5.4"),
effective_model: ModelRef::new(ProviderId::openai(), "gpt-5.4"),
requested_model: Some(ModelRef::new(ProviderId::openai(), "gpt-5.4")),
active_model: Some(ModelRef::new(ProviderId::openai(), "gpt-5.4")),
fallback_active: false,
effective_fallback_models: Vec::new(),
override_model: None,
override_reasoning_effort: None,
resolved_policy: policy("openai/gpt-5.4", "GPT-5.4"),
},
token_usage: AgentTokenUsageSummary {
total: TokenUsage::new(0, 0),
total_model_rounds: 0,
last_turn: None,
},
closure: ClosureDecision {
outcome: ClosureOutcome::Completed,
waiting_reason: None,
work_signal: None,
runtime_posture: RuntimePosture::Awake,
evidence: Vec::new(),
},
execution: ExecutionSnapshot {
profile: ExecutionProfile::default(),
policy: ExecutionProfile::default().policy_snapshot(),
attached_workspaces: Vec::new(),
workspace_id: None,
workspace_anchor: "/tmp".into(),
execution_root: "/tmp".into(),
cwd: "/tmp".into(),
execution_root_id: None,
projection_kind: None,
access_mode: None,
worktree_root: None,
},
active_workspace_occupancy: None,
loaded_agents_md: LoadedAgentsMdView::default(),
skills: SkillsRuntimeView::default(),
active_children: Vec::<ChildAgentSummary>::new(),
active_waiting_intents: Vec::<WaitingIntentSummary>::new(),
active_wait_conditions: Vec::new(),
active_external_triggers: Vec::new(),
recent_operator_notifications: Vec::new(),
recent_brief_count: 0,
recent_event_count: 0,
}
}
#[test]
fn picker_rows_include_inherit_and_runtime_availability() {
let agent = summary();
let availability = model_availability();
let rows = model_picker_rows(Some(&agent), &availability, "");
assert_eq!(rows.len(), 2);
assert!(rows[0].title.contains("inherit runtime default"));
assert!(rows[1].title.contains("openai/gpt-5.4"));
assert!(rows[1].available);
}
#[test]
fn picker_rows_filter_by_model_and_label() {
let agent = summary();
let availability = model_availability();
let rows = model_picker_rows(Some(&agent), &availability, "sonnet");
assert_eq!(rows.len(), 1);
assert!(rows[0].title.contains("inherit runtime default"));
}
#[test]
fn picker_selection_clamps_to_filtered_rows() {
let agent = summary();
let availability = model_availability();
assert_eq!(
clamp_model_picker_selection(Some(&agent), &availability, "gpt", 10),
1
);
assert!(selected_model_choice(Some(&agent), &availability, "", 1).is_some());
}
}