use ratatui::prelude::*;
use ratatui::widgets::{Cell, ScrollbarState};
use tokmesh_core::ClientId;
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use crate::tui::client_ui;
use crate::tui::config::TokmeshConfig;
use crate::tui::themes::Theme;
pub fn format_tokens_compact(tokens: u64) -> String {
if tokens >= 1_000_000_000 {
format!("{:.1}B", tokens as f64 / 1_000_000_000.0)
} else if tokens >= 1_000_000 {
format!("{:.1}M", tokens as f64 / 1_000_000.0)
} else if tokens >= 1_000 {
format!("{}K", tokens / 1_000)
} else {
format_tokens_with_commas(tokens)
}
}
pub fn format_tokens(tokens: u64) -> String {
format_tokens_compact(tokens)
}
pub(crate) fn total_tokens_cell(total_tokens: u64, theme: &Theme) -> Cell<'static> {
Cell::from(format_tokens(total_tokens)).style(theme.metric_total_style())
}
pub fn format_tokens_with_commas(n: u64) -> String {
let s = n.to_string();
let mut result = String::new();
for (i, c) in s.chars().rev().enumerate() {
if i > 0 && i % 3 == 0 {
result.insert(0, ',');
}
result.insert(0, c);
}
result
}
pub fn format_cost(cost: f64) -> String {
if !cost.is_finite() || cost < 0.0 {
return "$0.00".to_string();
}
if cost >= 1000.0 {
format!("${:.1}K", cost / 1000.0)
} else {
format!("${:.2}", cost)
}
}
pub fn format_cost_per_million(cost: f64, total_tokens: u64) -> String {
if total_tokens == 0 || !cost.is_finite() || cost < 0.0 {
return "\u{2014}".to_string(); }
let per_m = cost / (total_tokens as f64) * 1_000_000.0;
format!("${:.2}", per_m)
}
pub fn format_cache_hit_rate(cache_read: u64, input: u64, cache_write: u64) -> String {
let paid = input.saturating_add(cache_write);
if paid == 0 {
return if cache_read > 0 {
"∞".to_string()
} else {
"—".to_string()
};
}
let ratio = cache_read as f64 / paid as f64;
format!("{:.1}x", ratio)
}
pub fn format_ms_per_1k(ms_per_1k_tokens: Option<f64>) -> String {
let Some(value) = ms_per_1k_tokens else {
return "—".to_string();
};
if !value.is_finite() || value <= 0.0 {
"—".to_string()
} else if value >= 1000.0 {
format!("{:.1}s", value / 1000.0)
} else {
format!("{:.0}ms", value)
}
}
pub fn viewport_scrollbar_state(
content_len: usize,
scroll_offset: usize,
viewport_len: usize,
) -> ScrollbarState {
let viewport_len = viewport_len.max(1);
ScrollbarState::new(content_len)
.position(scrollbar_position(scroll_offset, content_len, viewport_len))
.viewport_content_length(viewport_len)
}
pub fn truncate_text(s: &str, max_chars: usize) -> String {
if max_chars == 0 {
return String::new();
}
let char_count = s.chars().count();
if char_count <= max_chars {
s.to_string()
} else if max_chars <= 3 {
s.chars().take(max_chars).collect()
} else {
let head: String = s.chars().take(max_chars - 3).collect();
format!("{}...", head)
}
}
pub fn display_width(s: &str) -> usize {
UnicodeWidthStr::width(s)
}
pub fn prefix_to_width(s: &str, max_cells: usize) -> &str {
let mut used = 0usize;
for (offset, cluster) in s.grapheme_indices(true) {
let w = UnicodeWidthStr::width(cluster);
if used + w > max_cells {
return &s[..offset];
}
used += w;
}
s
}
pub fn truncate_to_width(s: &str, max_cells: usize) -> String {
if max_cells == 0 {
return String::new();
}
if display_width(s) <= max_cells {
return s.to_string();
}
if max_cells <= 3 {
return prefix_to_width(s, max_cells).to_string();
}
format!("{}...", prefix_to_width(s, max_cells - 3))
}
fn scrollbar_position(scroll_offset: usize, content_len: usize, viewport_len: usize) -> usize {
let max_scroll = content_len.saturating_sub(viewport_len);
if max_scroll == 0 {
0
} else {
((scroll_offset.min(max_scroll) as u128) * (content_len.saturating_sub(1) as u128)
/ (max_scroll as u128)) as usize
}
}
pub(crate) fn light_ratio_bar_spans(
ratio: f64,
width: usize,
fill_style: Style,
empty_style: Style,
) -> Vec<Span<'static>> {
if width == 0 {
return Vec::new();
}
let ratio = ratio.clamp(0.0, 1.0);
let scaled = ratio * width as f64;
let trace = ratio > 0.0 && ratio < 0.01 && scaled < 1.0;
let filled = if ratio > 0.0 && !trace {
(scaled.round() as usize).clamp(1, width)
} else {
0
};
let empty = width.saturating_sub(filled + usize::from(trace));
let mut spans = Vec::with_capacity(3);
if filled > 0 {
spans.push(Span::styled("█".repeat(filled), fill_style));
}
if trace {
spans.push(Span::styled("▏", fill_style));
}
if empty > 0 {
spans.push(Span::styled("·".repeat(empty), empty_style));
}
spans
}
pub(crate) fn truncate_ellipsis(s: &str, max_chars: usize) -> String {
if max_chars == 0 {
return String::new();
}
let char_count = s.chars().count();
if char_count <= max_chars {
s.to_string()
} else if max_chars == 1 {
"…".to_string()
} else {
let head: String = s.chars().take(max_chars - 1).collect();
format!("{head}…")
}
}
pub fn get_model_color(model: &str) -> Color {
get_provider_shade(get_provider_from_model(model), 0)
}
fn known_provider_palette(provider_lower: &str) -> Option<&'static [(u8, u8, u8)]> {
let palette: &[(u8, u8, u8)] = match provider_lower {
s if s.contains("anthropic") => &ANTHROPIC_SHADES,
s if s.contains("openai") => &OPENAI_SHADES,
s if s.contains("google") || s.contains("gemini") => &GOOGLE_SHADES,
s if s.contains("deepseek") => &DEEPSEEK_SHADES,
s if s.contains("xai") || s.contains("grok") => &XAI_SHADES,
s if s.contains("zai") || s.contains("z_ai") || s.contains("zhipu") => &ZAI_SHADES,
s if s.contains("moonshot") || s.contains("kimi") => &MOONSHOT_SHADES,
s if s.contains("meta") || s.contains("llama") => &META_SHADES,
s if s.contains("cursor") => &CURSOR_SHADES,
s if s.contains("sakana") || s.contains("fugu") => &SAKANA_SHADES,
_ => return None,
};
Some(palette)
}
pub fn provider_has_palette(provider: &str) -> bool {
TokmeshConfig::load().get_provider_color(provider).is_some()
|| known_provider_palette(&provider.to_lowercase()).is_some()
}
pub fn get_provider_shade(provider: &str, rank: usize) -> Color {
if let Some(base) = TokmeshConfig::load().get_provider_color(provider) {
return shade_from_base(base, rank);
}
let p = provider.to_lowercase();
let palette = known_provider_palette(&p).unwrap_or(&UNKNOWN_SHADES);
let idx = rank.min(palette.len() - 1);
let (r, g, b) = palette[idx];
Color::Rgb(r, g, b)
}
fn shade_from_base(base: Color, rank: usize) -> Color {
const FACTORS: [f32; 7] = [0.00, 0.11, 0.22, 0.33, 0.44, 0.56, 0.67];
let Color::Rgb(r, g, b) = base else {
return base;
};
let idx = rank.min(FACTORS.len() - 1);
let f = FACTORS[idx];
let lerp = |c: u8| -> u8 {
let c = c as f32;
(c + (255.0 - c) * f).round().clamp(0.0, 255.0) as u8
};
Color::Rgb(lerp(r), lerp(g), lerp(b))
}
const ANTHROPIC_SHADES: [(u8, u8, u8); 7] = [
(218, 119, 86), (223, 136, 107), (227, 153, 128), (232, 170, 149), (236, 184, 166), (239, 197, 183), (243, 210, 199), ];
const OPENAI_SHADES: [(u8, u8, u8); 7] = [
(16, 185, 129), (18, 208, 145), (20, 232, 162), (41, 236, 172), (61, 238, 179), (97, 241, 193), (133, 244, 208), ];
const GOOGLE_SHADES: [(u8, u8, u8); 7] = [
(59, 130, 246), (83, 146, 247), (108, 161, 248), (132, 177, 249), (153, 190, 250), (172, 202, 251), (190, 214, 252), ];
const DEEPSEEK_SHADES: [(u8, u8, u8); 7] = [
(6, 182, 212), (7, 203, 237), (21, 215, 248), (45, 219, 249), (66, 223, 250), (85, 226, 250), (105, 229, 251), ];
const XAI_SHADES: [(u8, u8, u8); 7] = [
(234, 179, 8), (247, 192, 21), (248, 199, 45), (249, 205, 70), (249, 211, 91), (250, 216, 110), (251, 221, 129), ];
const ZAI_SHADES: [(u8, u8, u8); 7] = [
(168, 85, 247), (181, 110, 249), (193, 132, 250), (204, 153, 251), (214, 172, 252), (224, 192, 253), (235, 213, 254), ];
const MOONSHOT_SHADES: [(u8, u8, u8); 7] = [
(20, 184, 166), (35, 197, 178), (58, 207, 190), (85, 216, 202), (112, 224, 212), (143, 232, 222), (174, 240, 232), ];
const META_SHADES: [(u8, u8, u8); 7] = [
(99, 102, 241), (122, 125, 243), (146, 148, 245), (169, 171, 247), (189, 190, 249), (207, 208, 251), (225, 226, 252), ];
const CURSOR_SHADES: [(u8, u8, u8); 7] = [
(139, 92, 246), (154, 114, 247), (169, 135, 248), (184, 156, 250), (199, 177, 251), (215, 199, 252), (230, 220, 253), ];
const SAKANA_SHADES: [(u8, u8, u8); 7] = [
(219, 43, 31), (223, 66, 56), (227, 90, 80), (231, 113, 105), (235, 136, 130), (239, 162, 156), (243, 185, 181), ];
const UNKNOWN_SHADES: [(u8, u8, u8); 7] = [
(136, 136, 136), (156, 156, 156), (176, 176, 176), (196, 196, 196), (212, 212, 212), (228, 228, 228), (244, 244, 244), ];
fn is_delimited_model_family(model_lower: &str, family: &str) -> bool {
model_lower
.split(|c: char| !c.is_ascii_alphanumeric())
.any(|token| {
token == family
|| token
.strip_prefix(family)
.and_then(|suffix| suffix.chars().next())
.is_some_and(|c| c.is_ascii_digit())
})
}
pub fn get_provider_from_model(model: &str) -> &'static str {
let model_lower = model.to_lowercase();
if model_lower.contains("claude")
|| model_lower.contains("sonnet")
|| model_lower.contains("opus")
|| model_lower.contains("haiku")
|| model_lower
.split(|c: char| !c.is_ascii_alphanumeric())
.any(|token| token == "fable")
{
"anthropic"
} else if model_lower.contains("gpt")
|| model_lower.starts_with("o1")
|| model_lower.starts_with("o3")
|| model_lower.contains("codex")
|| model_lower.contains("text-embedding")
|| model_lower.contains("dall-e")
|| model_lower.contains("whisper")
|| model_lower.contains("tts")
{
"openai"
} else if model_lower.contains("gemini") {
"google"
} else if model_lower.contains("deepseek") {
"deepseek"
} else if model_lower.contains("grok") {
"xai"
} else if is_delimited_model_family(&model_lower, "glm") {
"zai"
} else if is_delimited_model_family(&model_lower, "kimi") {
"moonshotai"
} else if model_lower.contains("llama") {
"meta"
} else if model_lower.contains("mixtral") {
"mistral"
} else if model_lower == "auto" || model_lower.contains("cursor") {
"cursor"
} else {
"unknown"
}
}
pub fn get_client_color(client: &str) -> Color {
let config = TokmeshConfig::load();
if let Some(color) = config.get_client_color(client) {
return color;
}
match client.to_lowercase().as_str() {
"opencode" => Color::Rgb(34, 197, 94), "claude" => Color::Rgb(218, 119, 86), "codex" => Color::Rgb(59, 130, 246), "cursor" => Color::Rgb(168, 85, 247), "gemini" => Color::Rgb(6, 182, 212), "amp" => Color::Rgb(236, 72, 153), "droid" => Color::Rgb(16, 185, 129), "openclaw" => Color::Rgb(239, 68, 68), "hermes" => Color::Rgb(255, 215, 0), "goose" => Color::Rgb(100, 180, 220), "codebuff" => Color::Rgb(124, 58, 237), "antigravity" => Color::Rgb(99, 102, 241), "zed" => Color::Rgb(8, 76, 207), "warp" => Color::Rgb(1, 155, 150), "gjc" => Color::Rgb(220, 38, 38), "jcode" => Color::Rgb(245, 158, 11), "junie" => Color::Rgb(123, 97, 255), _ => Color::Rgb(136, 136, 136), }
}
pub fn get_client_display_name(client: &str) -> String {
let config = TokmeshConfig::load();
if let Some(name) = config.get_client_display_name(client) {
return name.to_string();
}
let client_lower = client.to_lowercase();
if client_lower == ClientId::OpenClaw.as_str() {
return "🦞 OpenClaw".to_string();
}
if let Some(client_id) = ClientId::from_str(&client_lower) {
return client_ui::display_name(client_id).to_string();
}
client.to_string()
}
pub fn get_provider_display_name(provider: &str) -> String {
let config = TokmeshConfig::load();
if let Some(name) = config.get_provider_display_name(provider) {
return name.to_string();
}
if provider.contains(", ") {
return provider
.split(", ")
.map(|segment| map_single_provider(segment, config))
.collect::<Vec<_>>()
.join(", ");
}
map_single_provider(provider, config)
}
fn map_single_provider(provider: &str, config: &TokmeshConfig) -> String {
if let Some(name) = config.get_provider_display_name(provider) {
return name.to_string();
}
let lower = provider.to_lowercase();
match lower.as_str() {
"anthropic" => return "Anthropic".to_string(),
"google" => return "Google".to_string(),
"cursor" => return "Cursor".to_string(),
"deepseek" => return "DeepSeek".to_string(),
"xai" => return "xAI".to_string(),
"meta" => return "Meta".to_string(),
"mistral" => return "Mistral".to_string(),
"cohere" => return "Cohere".to_string(),
"opencode" => return "OpenCode".to_string(),
"openrouter" => return "OpenRouter".to_string(),
"google-vertex" | "google_vertex" => return "Google Vertex".to_string(),
_ => {}
}
if lower.starts_with("openai") {
return "OpenAI".to_string();
}
if lower.starts_with("kimi") {
return "Kimi".to_string();
}
if lower.starts_with("github-cop") || lower.contains("copilot") {
return "GitHub Copilot".to_string();
}
smart_titlecase(provider)
}
fn smart_titlecase(s: &str) -> String {
s.split(['-', '_', ' '])
.filter(|word| !word.is_empty())
.map(titlecase_word)
.collect::<Vec<_>>()
.join(" ")
}
fn titlecase_word(word: &str) -> String {
match word.to_lowercase().as_str() {
"ai" => "AI".to_string(),
"gpt" => "GPT".to_string(),
"openai" => "OpenAI".to_string(),
"xai" => "xAI".to_string(),
"vertex" => "Vertex".to_string(),
"llm" => "LLM".to_string(),
"api" => "API".to_string(),
_ => capitalize_first(word),
}
}
fn capitalize_first(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().chain(chars).collect(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn truncate_to_width_never_exceeds_its_budget() {
let cjk = "세션제목한글로";
assert_eq!(truncate_text(cjk, 10), cjk, "code points say it fits");
assert!(
display_width(&truncate_to_width(cjk, 10)) <= 10,
"cells say it does not"
);
for budget in 0..=20 {
for s in [cjk, "ascii-session-title", "🦞 OpenClaw", ""] {
assert!(
display_width(&truncate_to_width(s, budget)) <= budget,
"{s:?} overflowed a {budget}-cell budget"
);
}
}
}
#[test]
fn truncate_to_width_marks_what_it_dropped() {
assert_eq!(truncate_to_width("abcdefghij", 10), "abcdefghij");
assert_eq!(truncate_to_width("abcdefghijk", 10), "abcdefg...");
assert_eq!(truncate_to_width("abcdefghijk", 3), "abc");
assert_eq!(truncate_to_width("abcdefghijk", 0), "");
assert_eq!(truncate_to_width("세션제목", 7), "세션...");
}
#[test]
fn prefix_to_width_never_splits_a_grapheme() {
assert_eq!(prefix_to_width("세션제목", 5), "세션");
assert_eq!(prefix_to_width("세션제목", 6), "세션제");
assert_eq!(prefix_to_width("abc", 10), "abc");
assert_eq!(prefix_to_width("abc", 0), "");
let flag = "\u{1F1F0}\u{1F1F7}"; let family = "\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}";
for (label, s) in [("flag", flag), ("family", family)] {
assert_eq!(
prefix_to_width(s, 1),
"",
"{label}: a 2-cell grapheme must not be halved to fit 1 cell"
);
assert_eq!(prefix_to_width(s, 2), s, "{label}: fits whole at 2 cells");
assert_eq!(
prefix_to_width(&format!("ab{s}"), 3),
"ab",
"{label}: a cluster that does not fit is dropped, not split"
);
}
}
#[test]
fn display_width_counts_cells_not_code_points() {
assert_eq!(display_width("OpenCode"), 8);
assert_eq!(display_width("🦞 OpenClaw"), 11);
assert_eq!("🦞 OpenClaw".chars().count(), 10, "the disagreement");
}
#[test]
fn scrollbar_position_maps_bottom_offset_to_last_position() {
assert_eq!(scrollbar_position(15, 20, 5), 19);
}
#[test]
fn scrollbar_position_keeps_top_at_zero() {
assert_eq!(scrollbar_position(0, 20, 5), 0);
}
#[test]
fn scrollbar_position_clamps_overscroll_to_bottom() {
assert_eq!(scrollbar_position(999, 20, 5), 19);
}
#[test]
fn scrollbar_position_single_page_stays_at_zero() {
assert_eq!(scrollbar_position(0, 5, 10), 0);
}
#[test]
fn scrollbar_position_uses_wide_math_for_large_lengths() {
let content_len = usize::MAX;
let viewport_len = 2;
let max_scroll = content_len - viewport_len;
assert_eq!(scrollbar_position(0, content_len, viewport_len), 0);
assert_eq!(
scrollbar_position(max_scroll, content_len, viewport_len),
usize::MAX - 1
);
assert_eq!(
scrollbar_position(usize::MAX, content_len, viewport_len),
usize::MAX - 1
);
}
#[test]
fn viewport_scrollbar_state_handles_zero_viewport() {
let state = viewport_scrollbar_state(20, 5, 0);
assert_eq!(
state,
ScrollbarState::new(20)
.position(5)
.viewport_content_length(1)
);
}
#[test]
fn shade_from_base_rank_0_equals_base() {
let base = Color::Rgb(255, 0, 0);
assert_eq!(shade_from_base(base, 0), base);
}
#[test]
fn shade_from_base_lightens_monotonically_toward_white() {
let base = Color::Rgb(0, 0, 0);
let mut prev_r: u8 = 0;
for rank in 0..7 {
let Color::Rgb(r, _, _) = shade_from_base(base, rank) else {
panic!("expected Rgb")
};
assert!(
r >= prev_r,
"shade at rank {} should not be darker than rank {}",
rank,
rank - 1
);
prev_r = r;
}
}
#[test]
fn shade_from_base_clamps_beyond_palette_length() {
let base = Color::Rgb(100, 100, 100);
assert_eq!(shade_from_base(base, 100), shade_from_base(base, 6));
}
#[test]
fn shade_from_base_passes_through_non_rgb() {
assert_eq!(shade_from_base(Color::Indexed(42), 5), Color::Indexed(42));
}
#[test]
fn unknown_provider_returns_gray_ramp_not_pure_white() {
let rank_0 = get_provider_shade("some-new-provider", 0);
let rank_3 = get_provider_shade("some-new-provider", 3);
assert_ne!(rank_0, rank_3);
assert_ne!(rank_0, Color::Rgb(255, 255, 255));
}
#[test]
fn cursor_provider_has_distinct_shades_per_rank() {
let rank_0 = get_provider_shade("cursor", 0);
let rank_6 = get_provider_shade("cursor", 6);
assert_ne!(rank_0, rank_6);
}
#[test]
fn get_provider_shade_saturates_at_palette_end() {
let last = get_provider_shade("anthropic", 6);
let past_end = get_provider_shade("anthropic", 99);
assert_eq!(last, past_end);
}
#[test]
fn fable_is_recognized_as_anthropic() {
assert_eq!(get_provider_from_model("fable-5"), "anthropic");
assert_eq!(get_provider_from_model("claude-fable-5"), "anthropic");
assert_eq!(get_provider_from_model("claude-fable-5[1m]"), "anthropic");
}
#[test]
fn glm_and_kimi_use_their_vendor_color_ramps() {
assert_eq!(get_provider_from_model("glm-5.2"), "zai");
assert_eq!(get_provider_from_model("kimi-k2.7-code"), "moonshotai");
assert!(provider_has_palette("zai"));
assert!(provider_has_palette("moonshotai"));
assert_eq!(get_provider_from_model("glmish-1"), "unknown");
assert_eq!(get_provider_from_model("kimiko-1"), "unknown");
}
#[test]
fn fable_gets_same_base_color_as_opus() {
let fable = get_model_color("fable-5");
let opus = get_model_color("claude-opus-4-1");
assert_eq!(fable, opus);
assert_eq!(fable, get_model_color("claude-fable-5"));
assert_eq!(get_provider_from_model("some-unknown-model"), "unknown");
}
#[test]
fn fable_substring_does_not_misattribute_to_anthropic() {
assert_eq!(get_provider_from_model("unfabled-model"), "unknown");
assert_eq!(get_provider_from_model("fableton-1"), "unknown");
assert_eq!(get_provider_from_model("fable-5"), "anthropic");
assert_eq!(get_provider_from_model("claude-fable-5[1m]"), "anthropic");
}
#[test]
fn provider_display_name_target_cases() {
assert_eq!(get_provider_display_name("openai"), "OpenAI");
assert_eq!(get_provider_display_name("kimi-for-coding"), "Kimi");
assert_eq!(get_provider_display_name("google-vertex"), "Google Vertex");
assert_eq!(get_provider_display_name("opencode"), "OpenCode");
}
#[test]
fn provider_display_name_openai_family() {
assert_eq!(get_provider_display_name("openai"), "OpenAI");
assert_eq!(get_provider_display_name("openai-codex"), "OpenAI");
assert_eq!(get_provider_display_name("OpenAI"), "OpenAI");
}
#[test]
fn provider_display_name_kimi_family() {
assert_eq!(get_provider_display_name("kimi"), "Kimi");
assert_eq!(get_provider_display_name("kimi-code"), "Kimi");
assert_eq!(get_provider_display_name("kimi-for-coding"), "Kimi");
}
#[test]
fn provider_display_name_google_vertex_both_spellings() {
assert_eq!(get_provider_display_name("google-vertex"), "Google Vertex");
assert_eq!(get_provider_display_name("google_vertex"), "Google Vertex");
}
#[test]
fn provider_display_name_smart_fallback_multiword() {
assert_eq!(
get_provider_display_name("some-new-provider"),
"Some New Provider"
);
assert_eq!(
get_provider_display_name("some_new_provider"),
"Some New Provider"
);
}
#[test]
fn provider_display_name_known_regressions() {
assert_eq!(get_provider_display_name("anthropic"), "Anthropic");
assert_eq!(get_provider_display_name("google"), "Google");
assert_eq!(get_provider_display_name("xai"), "xAI");
assert_eq!(get_provider_display_name("deepseek"), "DeepSeek");
assert_eq!(get_provider_display_name("meta"), "Meta");
assert_eq!(get_provider_display_name("mistral"), "Mistral");
assert_eq!(get_provider_display_name("cohere"), "Cohere");
assert_eq!(get_provider_display_name("cursor"), "Cursor");
assert_eq!(
get_provider_display_name("github-copilot"),
"GitHub Copilot"
);
assert_eq!(get_provider_display_name("copilot"), "GitHub Copilot");
}
#[test]
fn provider_display_name_acronym_words_in_fallback() {
assert_eq!(get_provider_display_name("acme-ai"), "Acme AI");
assert_eq!(get_provider_display_name("foo-api"), "Foo API");
}
#[test]
fn provider_display_name_merged_list_maps_each_segment() {
assert_eq!(
get_provider_display_name("openai, openrouter"),
"OpenAI, OpenRouter"
);
assert_eq!(
get_provider_display_name("kimi, anthropic"),
"Kimi, Anthropic"
);
assert_eq!(
get_provider_display_name("anthropic, copilot"),
"Anthropic, GitHub Copilot"
);
assert_eq!(
get_provider_display_name("anthropic, openai"),
"Anthropic, OpenAI"
);
}
#[test]
fn provider_display_name_empty_is_empty() {
assert_eq!(get_provider_display_name(""), "");
assert_eq!(get_provider_display_name("--_-"), "");
}
#[test]
fn get_provider_shade_fuzzy_matching() {
assert_eq!(
get_provider_shade("test-anthropic", 0),
get_provider_shade("anthropic", 0)
);
assert_eq!(
get_provider_shade("company-google", 0),
get_provider_shade("google", 0)
);
assert_eq!(
get_provider_shade("openrouter-gemini-prod", 0),
get_provider_shade("google", 0)
);
assert_eq!(
get_provider_shade("deepseek-api", 0),
get_provider_shade("deepseek", 0)
);
assert_eq!(
get_provider_shade("meta-llama-endpoint", 0),
get_provider_shade("meta", 0)
);
}
#[test]
fn provider_has_palette_flags_only_known_vendors() {
assert!(provider_has_palette("anthropic"));
assert!(provider_has_palette("openai"));
assert!(provider_has_palette("openrouter-gemini-prod"));
assert!(provider_has_palette("zai"));
assert!(provider_has_palette("moonshotai"));
assert!(!provider_has_palette("github-copilot"));
assert!(!provider_has_palette("openrouter"));
assert!(!provider_has_palette("unknown"));
assert!(!provider_has_palette(""));
}
}