use codewhale_config::catalog::CatalogSource;
use codewhale_config::route::{CapabilityState, RouteCapabilities, RouteLimits};
use crate::config::ApiProvider;
use crate::model_registry::{self, ModelMetadata};
use crate::tui::model_picker::format_picker_context_window;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RouteCapabilityBadges {
pub badges: Vec<String>,
pub provenance: &'static str,
}
impl RouteCapabilityBadges {
#[must_use]
pub fn summary(&self) -> String {
format!("{} ({})", self.badges.join(" · "), self.provenance)
}
}
#[must_use]
pub fn resolve_route_capability_badges(
provider_id: Option<&str>,
model: &str,
) -> Option<RouteCapabilityBadges> {
let model = effective_model(model)?;
if let Some(provider) = provider_id.and_then(exact_builtin_provider)
&& let Some(offering) = crate::provider_lake::catalog_offering_for_model(provider, model)
{
let route = offering.to_offering();
let badges = badges_from_route(&route.limits, &route.capabilities);
if !badges.is_empty() {
return Some(RouteCapabilityBadges {
badges,
provenance: catalog_provenance(&offering.source),
});
}
}
let meta = model_registry::lookup(model)?;
let badges = badges_from_registry(&meta);
(!badges.is_empty()).then_some(RouteCapabilityBadges {
badges,
provenance: "registry",
})
}
fn effective_model(model: &str) -> Option<&str> {
let model = model
.split_once("->")
.map_or(model, |(_, effective)| effective)
.trim();
(!model.is_empty() && !model.eq_ignore_ascii_case("auto")).then_some(model)
}
fn exact_builtin_provider(provider_id: &str) -> Option<ApiProvider> {
ApiProvider::parse(provider_id).filter(|provider| provider.as_str() == provider_id)
}
const fn catalog_provenance(source: &CatalogSource) -> &'static str {
match source {
CatalogSource::Bundled => "bundled catalog",
CatalogSource::Live { .. } => "live catalog",
CatalogSource::UserOverride => "override",
}
}
fn badges_from_route(limits: &RouteLimits, capabilities: &RouteCapabilities) -> Vec<String> {
let mut badges = Vec::new();
if let Some(context) = limits.context_tokens {
badges.push(format!("{} ctx", format_picker_context_window(context)));
}
if let Some(output) = limits.output_tokens {
badges.push(format!("{} out", format_picker_context_window(output)));
}
push_state_badge(&mut badges, capabilities.native_tool_calls, "tools");
push_state_badge(&mut badges, capabilities.reasoning, "reasoning");
push_state_badge(&mut badges, capabilities.image_input, "vision");
badges
}
fn badges_from_registry(meta: &ModelMetadata) -> Vec<String> {
let mut badges = Vec::new();
if let Some(context) = meta.context_window {
badges.push(format!(
"{} ctx",
format_picker_context_window(u64::from(context))
));
}
if let Some(output) = meta.max_output {
badges.push(format!(
"{} out",
format_picker_context_window(u64::from(output))
));
}
if meta.supports_reasoning {
badges.push("reasoning".to_string());
}
badges
}
fn push_state_badge(badges: &mut Vec<String>, state: CapabilityState, name: &str) {
match state {
CapabilityState::Supported => badges.push(name.to_string()),
CapabilityState::Unsupported => badges.push(format!("no {name}")),
CapabilityState::Unknown => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
use codewhale_config::catalog::CatalogOffering;
use codewhale_config::models_dev::{ModelsDevLimit, ModelsDevModalities};
#[test]
fn known_catalog_model_resolves_provider_aware_badges() {
let badges = resolve_route_capability_badges(Some("deepseek"), "deepseek-v4-pro")
.expect("bundled catalog knows deepseek-v4-pro");
assert!(
badges.badges.contains(&"1M ctx".to_string()),
"missing context badge: {:?}",
badges.badges
);
assert!(badges.badges.contains(&"384K out".to_string()));
assert!(badges.badges.contains(&"tools".to_string()));
assert!(badges.badges.contains(&"reasoning".to_string()));
assert!(badges.badges.contains(&"no vision".to_string()));
assert!(
badges.provenance.contains("catalog"),
"catalog facts must carry catalog provenance, got {}",
badges.provenance
);
assert!(badges.summary().contains(" · "));
}
#[test]
fn unknown_model_resolves_to_graceful_absence() {
assert_eq!(
resolve_route_capability_badges(Some("deepseek"), "totally-made-up-model-xyz"),
None
);
assert_eq!(resolve_route_capability_badges(None, ""), None);
assert_eq!(resolve_route_capability_badges(None, "auto"), None);
}
#[test]
fn registry_fallback_covers_routes_without_catalog_rows() {
let badges = resolve_route_capability_badges(Some("my-custom-endpoint"), "claude-fable-5")
.expect("registry knows claude-fable-5");
assert_eq!(badges.provenance, "registry");
assert!(badges.badges.contains(&"1M ctx".to_string()));
assert!(badges.badges.contains(&"reasoning".to_string()));
assert!(
!badges
.badges
.iter()
.any(|badge| badge.contains("tools") || badge.contains("vision")),
"unsourced facts must not appear: {:?}",
badges.badges
);
}
#[test]
fn auto_display_route_resolves_the_effective_model() {
let badges = resolve_route_capability_badges(None, "auto -> deepseek-v4-pro")
.expect("effective model resolves");
assert!(badges.badges.contains(&"1M ctx".to_string()));
}
#[test]
fn explicit_unsupported_catalog_facts_render_as_no_badges() {
let offering = CatalogOffering {
provider: "deepseek".to_string(),
wire_model_id: "fixture-model".to_string(),
endpoint_key: "chat".to_string(),
limit: Some(ModelsDevLimit {
context: Some(131_072),
input: None,
output: None,
}),
reasoning: Some(false),
tool_call: Some(false),
modalities: Some(ModelsDevModalities {
input: vec!["text".to_string(), "image".to_string()],
output: vec!["text".to_string()],
}),
..CatalogOffering::default()
};
let route = offering.to_offering();
let badges = badges_from_route(&route.limits, &route.capabilities);
assert_eq!(
badges,
vec![
"131K ctx".to_string(),
"no tools".to_string(),
"no reasoning".to_string(),
"vision".to_string(),
]
);
}
#[test]
fn token_labels_match_picker_vocabulary() {
assert_eq!(format_picker_context_window(1_000_000), "1M");
assert_eq!(format_picker_context_window(1_050_000), "1.05M");
assert_eq!(format_picker_context_window(262_144), "262K");
assert_eq!(format_picker_context_window(500), "500");
}
}