Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 24 additions & 6 deletions crates/tokscale-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1887,7 +1887,10 @@ fn run_models_report(
entry.input + entry.output + entry.cache_read + entry.cache_write;
table.add_row(vec![
Cell::new(capitalized_clients),
Cell::new(&entry.provider).add_attribute(Attribute::Dim),
Cell::new(crate::tui::ui::widgets::get_provider_display_name(
&entry.provider,
))
.add_attribute(Attribute::Dim),
Cell::new(&entry.model),
Cell::new(format_tokens_with_commas(entry.input))
.set_alignment(CellAlignment::Right),
Expand Down Expand Up @@ -1946,7 +1949,10 @@ fn run_models_report(
entry.input + entry.output + entry.cache_read + entry.cache_write;
table.add_row(vec![
Cell::new(capitalize_client(&entry.client)),
Cell::new(&entry.provider).add_attribute(Attribute::Dim),
Cell::new(crate::tui::ui::widgets::get_provider_display_name(
&entry.provider,
))
.add_attribute(Attribute::Dim),
Cell::new(&entry.model),
Cell::new(format_tokens_with_commas(entry.input))
.set_alignment(CellAlignment::Right),
Expand Down Expand Up @@ -2118,7 +2124,10 @@ fn run_models_report(
.join(", ");
table.add_row(vec![
Cell::new(capitalized_clients),
Cell::new(&entry.provider).add_attribute(Attribute::Dim),
Cell::new(crate::tui::ui::widgets::get_provider_display_name(
&entry.provider,
))
.add_attribute(Attribute::Dim),
Cell::new(&entry.model),
Cell::new(format_tokens_with_commas(entry.input))
.set_alignment(CellAlignment::Right),
Expand Down Expand Up @@ -2206,7 +2215,10 @@ fn run_models_report(
}
row.extend([
Cell::new(session_label),
Cell::new(&entry.provider).add_attribute(Attribute::Dim),
Cell::new(crate::tui::ui::widgets::get_provider_display_name(
&entry.provider,
))
.add_attribute(Attribute::Dim),
Cell::new(&entry.model),
Cell::new(format_tokens_with_commas(entry.input))
.set_alignment(CellAlignment::Right),
Expand Down Expand Up @@ -2285,7 +2297,10 @@ fn run_models_report(

table.add_row(vec![
Cell::new(capitalize_client(&entry.client)),
Cell::new(&entry.provider).add_attribute(Attribute::Dim),
Cell::new(crate::tui::ui::widgets::get_provider_display_name(
&entry.provider,
))
.add_attribute(Attribute::Dim),
Cell::new(&entry.model),
Cell::new(format_model_name(&entry.model)),
Cell::new(format_tokens_with_commas(entry.input))
Expand Down Expand Up @@ -2371,7 +2386,10 @@ fn run_models_report(

table.add_row(vec![
Cell::new(workspace_name(entry.workspace_label.as_deref())),
Cell::new(&entry.provider).add_attribute(Attribute::Dim),
Cell::new(crate::tui::ui::widgets::get_provider_display_name(
&entry.provider,
))
.add_attribute(Attribute::Dim),
Cell::new(capitalized_clients),
Cell::new(&entry.model),
Cell::new(format_tokens_with_commas(entry.input))
Expand Down
2 changes: 1 addition & 1 deletion crates/tokscale-cli/src/tui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ pub(crate) mod privacy;
pub mod remote;
pub mod settings;
mod themes;
mod ui;
pub(crate) mod ui;

pub use app::{App, Tab, TuiConfig};
pub use cache::{
Expand Down
145 changes: 134 additions & 11 deletions crates/tokscale-cli/src/tui/ui/widgets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -375,19 +375,65 @@ pub fn get_provider_display_name(provider: &str) -> String {
if let Some(name) = config.get_provider_display_name(provider) {
return name.to_string();
}
match provider.to_lowercase().as_str() {
"anthropic" => "Anthropic".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(),
// `canonical_provider` rewrites `google-vertex` → `google_vertex`, so
// accept both spellings here.
"google-vertex" | "google_vertex" => return "Google Vertex".to_string(),
_ => {}
}

// Brand families: any provider id that starts with these stems collapses to
// the brand name. Covers `openai`, `openai-codex`, `kimi`, `kimi-code`,
// `kimi-for-coding`, etc. without enumerating every variant.
if lower.starts_with("openai") {
return "OpenAI".to_string();
Comment on lines +398 to +399

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Split merged provider lists before display mapping

When a row merges multiple providers, aggregate_model_usage_entries stores them as a comma-separated string for every grouping except ClientProviderModel; this mapper now treats the whole string as one provider. In a real Models row such as the same model used via openai and openrouter, the sorted provider value becomes openai, openrouter, this prefix branch returns only OpenAI, and the Providers column silently drops openrouter. Split entry.provider on ", " and map each provider before joining, or make this helper list-aware.

Useful? React with 👍 / 👎.

}
if lower.starts_with("kimi") {
return "Kimi".to_string();
}
if lower.starts_with("github-cop") || lower.contains("copilot") {
return "GitHub Copilot".to_string();
}

// Smart fallback: split on `-`, `_`, and whitespace, title-case each word,
// and map known acronyms/brands per word. So unknown multi-word providers
// like `google-vertex` → "Google Vertex" and `some-new-provider` →
// "Some New Provider".
smart_titlecase(provider)
}

/// Title-cases a provider/brand identifier word-by-word, splitting on `-`, `_`,
/// and whitespace. Per-word acronym/brand overrides (e.g. `ai` → "AI",
/// `gpt` → "GPT") win over plain capitalization. Empty input yields an empty
/// string; runs of separators are collapsed.
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(),
"google" => "Google".to_string(),
"cursor" => "Cursor".to_string(),
"deepseek" => "DeepSeek".to_string(),
"xai" => "xAI".to_string(),
"meta" => "Meta".to_string(),
"mistral" => "Mistral".to_string(),
"cohere" => "Cohere".to_string(),
"opencode" => "OpenCode".to_string(),
s if s.starts_with("github-cop") || s.contains("copilot") => "GitHub Copilot".to_string(),
_ => capitalize_first(provider),
"vertex" => "Vertex".to_string(),
"llm" => "LLM".to_string(),
"api" => "API".to_string(),
_ => capitalize_first(word),
}
}

Expand Down Expand Up @@ -523,6 +569,83 @@ mod tests {
assert_eq!(last, past_end);
}

#[test]
fn provider_display_name_target_cases() {
// The four cases the user reported as rendering wrong.
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() {
// Any openai* id collapses to the brand name.
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() {
// `canonical_provider` rewrites the hyphen to an underscore, so both
// spellings must map to the same clean label.
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() {
// Unknown multi-word providers get split + title-cased instead of the
// old naive capitalize-first ("Some-new-provider").
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() {
// Per-word acronym map applies inside the smart 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_empty_is_empty() {
assert_eq!(get_provider_display_name(""), "");
// A string of only separators collapses to empty rather than panicking.
assert_eq!(get_provider_display_name("--_-"), "");
}

#[test]
fn get_provider_shade_fuzzy_matching() {
assert_eq!(
Expand Down
Loading