From 3234032baed12f31b65fc20dade31569375a042e Mon Sep 17 00:00:00 2001 From: Oz Date: Fri, 7 Aug 2026 05:14:12 +0000 Subject: [PATCH 1/2] Show the Kimi logo for Kimi models in the model picker Kimi models are served through Fireworks, which the server reports as `LlmProvider::UNKNOWN`, so they fell through to the `Icon::Agent` fallback and the picker branded them with the Warp "W". The provider enum can't carry the logo without adding a first-party Kimi provider end to end, so resolve the icon from the model's own names instead. The match is anchored to a leading `kimi` token so the other Fireworks-hosted families (GLM, MiniMax, Qwen, DeepSeek) are unaffected. The asset follows the bundled-icon convention (24x24 viewBox, single tintable path), so `warpui_core::elements::Icon` recolors it from the theme in both light and dark. Co-Authored-By: Warp Agent --- app/assets/bundled/svg/kimi.svg | 3 + app/src/ai/llms.rs | 36 +++++++- app/src/ai/llms_tests.rs | 85 +++++++++++++++++++ .../view/ambient_agent/model_selector.rs | 4 +- crates/warp_core/src/ui/icons.rs | 2 + 5 files changed, 127 insertions(+), 3 deletions(-) create mode 100644 app/assets/bundled/svg/kimi.svg diff --git a/app/assets/bundled/svg/kimi.svg b/app/assets/bundled/svg/kimi.svg new file mode 100644 index 00000000000..8c206c93107 --- /dev/null +++ b/app/assets/bundled/svg/kimi.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/src/ai/llms.rs b/app/src/ai/llms.rs index 76c27b9d465..a2aa1faba9d 100644 --- a/app/src/ai/llms.rs +++ b/app/src/ai/llms.rs @@ -187,10 +187,44 @@ pub fn model_leading_icon(llm: &LLMInfo, flags: ModelIconFlags) -> Icon { } else if flags.is_using_gemini_enterprise { Icon::GeminiEnterpriseAgentPlatform } else { - llm.provider.icon().unwrap_or(Icon::Agent) + model_provider_icon(llm) } } +/// The brand icon for the model's provider, falling back to the agent glyph. +pub fn model_provider_icon(llm: &LLMInfo) -> Icon { + if is_kimi_model(llm) { + return Icon::KimiLogo; + } + llm.provider.icon().unwrap_or(Icon::Agent) +} + +/// Kimi models are hosted through Fireworks, which the server reports as +/// [`LLMProvider::Unknown`], so their branding can't be carried by the provider +/// enum. Match on the model's own names instead, and keep the match anchored to +/// the leading `kimi` token so the other Fireworks-hosted families (GLM, +/// MiniMax, Qwen, DeepSeek) are unaffected. +fn is_kimi_model(llm: &LLMInfo) -> bool { + [ + llm.id.as_str(), + llm.display_name.as_str(), + llm.base_model_name.as_str(), + ] + .iter() + .any(|name| starts_with_kimi_token(name)) +} + +fn starts_with_kimi_token(name: &str) -> bool { + name.trim() + .to_lowercase() + .strip_prefix("kimi") + .is_some_and(|rest| { + rest.is_empty() + || rest.starts_with(['-', '_', '.', ' ', '/', ':']) + || rest.starts_with(|c: char| c.is_ascii_digit()) + }) +} + /// Key for cached LLM metadata in user preferences. /// /// Note: this key used to store a single [`AvailableLLMs`] diff --git a/app/src/ai/llms_tests.rs b/app/src/ai/llms_tests.rs index ed81bfdcc69..2b04f385abd 100644 --- a/app/src/ai/llms_tests.rs +++ b/app/src/ai/llms_tests.rs @@ -286,6 +286,91 @@ fn models_without_a_host_fall_back_to_the_provider_icon() { ); } +#[test] +fn kimi_models_show_the_kimi_logo() { + // Kimi ships through Fireworks, which the server reports as `Unknown`, so + // these rows would otherwise land on the agent glyph. + for id in [ + "kimi-k25-fireworks", + "kimi-k26-fireworks", + "kimi-k27-code-fireworks", + "kimi-k3-fireworks", + ] { + let llm = server_llm(id, None); + assert_eq!(llm.provider, LLMProvider::Unknown); + assert_eq!( + model_leading_icon(&llm, ModelIconFlags::default()), + Icon::KimiLogo, + "{id} should show the Kimi logo" + ); + } + + // The display name alone is enough when the id doesn't carry the brand. + let mut llm = server_llm("moonshotai/kimi-k2-instruct", None); + llm.display_name = "Kimi K2.5".to_string(); + llm.base_model_name = "Kimi K2.5".to_string(); + assert_eq!( + model_leading_icon(&llm, ModelIconFlags::default()), + Icon::KimiLogo + ); +} + +#[test] +fn other_fireworks_hosted_models_keep_the_agent_glyph() { + // The Kimi match is anchored to a leading `kimi` token precisely so the + // other Fireworks-hosted families don't inherit its branding. + for id in [ + "glm-46-fireworks", + "minimax-m2-fireworks", + "qwen3-coder-fireworks", + "deepseek-v3-fireworks", + "kimika", + ] { + let llm = server_llm(id, None); + assert_eq!( + model_leading_icon(&llm, ModelIconFlags::default()), + Icon::Agent, + "{id} should keep the agent glyph" + ); + } +} + +#[test] +fn kimi_models_still_yield_to_auto_and_host_badges() { + let llm = server_llm("kimi-k3-fireworks", None); + + assert_eq!( + model_leading_icon( + &llm, + ModelIconFlags { + is_auto: true, + ..Default::default() + } + ), + Icon::Agent + ); + assert_eq!( + model_leading_icon( + &llm, + ModelIconFlags { + is_custom_router: true, + ..Default::default() + } + ), + Icon::Dataflow + ); + assert_eq!( + model_leading_icon( + &llm, + ModelIconFlags { + is_using_bedrock: true, + ..Default::default() + } + ), + Icon::Aws + ); +} + // -- build_custom_llm_infos / display label tests -- fn endpoint( diff --git a/app/src/terminal/view/ambient_agent/model_selector.rs b/app/src/terminal/view/ambient_agent/model_selector.rs index 9c999783a23..78d2d8f9f02 100644 --- a/app/src/terminal/view/ambient_agent/model_selector.rs +++ b/app/src/terminal/view/ambient_agent/model_selector.rs @@ -22,7 +22,7 @@ use crate::ai::custom_model_routers::is_custom_router_id; use crate::ai::execution_profiles::model_menu_items::is_auto; use crate::ai::harness_availability::{HarnessAvailabilityEvent, HarnessAvailabilityModel}; use crate::ai::harness_display::icon_for as harness_icon_for; -use crate::ai::llms::{LLMId, LLMPreferences, LLMPreferencesEvent}; +use crate::ai::llms::{LLMId, LLMPreferences, LLMPreferencesEvent, model_provider_icon}; use crate::editor::{ EditorView, Event as EditorEvent, PropagateAndNoOpEscapeKey, PropagateAndNoOpNavigationKeys, SingleLineEditorOptions, TextOptions, @@ -510,7 +510,7 @@ impl ModelSelector { let leading_icon = if is_custom_router_id(llm.id.as_str()) { Icon::Dataflow } else { - llm.provider.icon().unwrap_or(Icon::Agent) + model_provider_icon(llm) }; let fields = MenuItemFields::new(display_name) .with_icon(leading_icon) diff --git a/crates/warp_core/src/ui/icons.rs b/crates/warp_core/src/ui/icons.rs index 875055648da..3ebd9ee31bc 100644 --- a/crates/warp_core/src/ui/icons.rs +++ b/crates/warp_core/src/ui/icons.rs @@ -275,6 +275,7 @@ pub enum Icon { ClaudeLogo, GeminiLogo, GrokLogo, + KimiLogo, OpenAILogo, XLogo, AmpLogo, @@ -623,6 +624,7 @@ impl From for &'static str { Icon::ClaudeLogo => "bundled/svg/claude.svg", Icon::GeminiLogo => "bundled/svg/gemini_cli.svg", Icon::GrokLogo => "bundled/svg/grok.svg", + Icon::KimiLogo => "bundled/svg/kimi.svg", Icon::OpenAILogo => "bundled/svg/openai.svg", Icon::XLogo => "bundled/svg/x-logo.svg", Icon::AmpLogo => "bundled/svg/amp.svg", From 2dc0412a3d3a6912eaf78f271695c6da5cb84d23 Mon Sep 17 00:00:00 2001 From: Oz Date: Fri, 7 Aug 2026 07:30:45 +0000 Subject: [PATCH 2/2] Shrink the Kimi icon check to an id-prefix match Every Kimi model the server ships is `kimi-*`, so the name-scanning helper and its tests were more machinery than the mapping needs. Fold the check into a single branch on the model id. Co-Authored-By: Warp Agent --- app/src/ai/llms.rs | 40 ++------- app/src/ai/llms_tests.rs | 85 ------------------- .../view/ambient_agent/model_selector.rs | 6 +- 3 files changed, 11 insertions(+), 120 deletions(-) diff --git a/app/src/ai/llms.rs b/app/src/ai/llms.rs index a2aa1faba9d..73ab3765430 100644 --- a/app/src/ai/llms.rs +++ b/app/src/ai/llms.rs @@ -186,43 +186,17 @@ pub fn model_leading_icon(llm: &LLMInfo, flags: ModelIconFlags) -> Icon { Icon::Aws } else if flags.is_using_gemini_enterprise { Icon::GeminiEnterpriseAgentPlatform + } else if is_kimi_model(llm) { + Icon::KimiLogo } else { - model_provider_icon(llm) + llm.provider.icon().unwrap_or(Icon::Agent) } } -/// The brand icon for the model's provider, falling back to the agent glyph. -pub fn model_provider_icon(llm: &LLMInfo) -> Icon { - if is_kimi_model(llm) { - return Icon::KimiLogo; - } - llm.provider.icon().unwrap_or(Icon::Agent) -} - -/// Kimi models are hosted through Fireworks, which the server reports as -/// [`LLMProvider::Unknown`], so their branding can't be carried by the provider -/// enum. Match on the model's own names instead, and keep the match anchored to -/// the leading `kimi` token so the other Fireworks-hosted families (GLM, -/// MiniMax, Qwen, DeepSeek) are unaffected. -fn is_kimi_model(llm: &LLMInfo) -> bool { - [ - llm.id.as_str(), - llm.display_name.as_str(), - llm.base_model_name.as_str(), - ] - .iter() - .any(|name| starts_with_kimi_token(name)) -} - -fn starts_with_kimi_token(name: &str) -> bool { - name.trim() - .to_lowercase() - .strip_prefix("kimi") - .is_some_and(|rest| { - rest.is_empty() - || rest.starts_with(['-', '_', '.', ' ', '/', ':']) - || rest.starts_with(|c: char| c.is_ascii_digit()) - }) +/// Kimi is Fireworks-hosted, which the server reports as `LLMProvider::Unknown`, +/// so the provider enum can't carry its logo. +pub fn is_kimi_model(llm: &LLMInfo) -> bool { + llm.id.as_str().starts_with("kimi-") } /// Key for cached LLM metadata in user preferences. diff --git a/app/src/ai/llms_tests.rs b/app/src/ai/llms_tests.rs index 2b04f385abd..ed81bfdcc69 100644 --- a/app/src/ai/llms_tests.rs +++ b/app/src/ai/llms_tests.rs @@ -286,91 +286,6 @@ fn models_without_a_host_fall_back_to_the_provider_icon() { ); } -#[test] -fn kimi_models_show_the_kimi_logo() { - // Kimi ships through Fireworks, which the server reports as `Unknown`, so - // these rows would otherwise land on the agent glyph. - for id in [ - "kimi-k25-fireworks", - "kimi-k26-fireworks", - "kimi-k27-code-fireworks", - "kimi-k3-fireworks", - ] { - let llm = server_llm(id, None); - assert_eq!(llm.provider, LLMProvider::Unknown); - assert_eq!( - model_leading_icon(&llm, ModelIconFlags::default()), - Icon::KimiLogo, - "{id} should show the Kimi logo" - ); - } - - // The display name alone is enough when the id doesn't carry the brand. - let mut llm = server_llm("moonshotai/kimi-k2-instruct", None); - llm.display_name = "Kimi K2.5".to_string(); - llm.base_model_name = "Kimi K2.5".to_string(); - assert_eq!( - model_leading_icon(&llm, ModelIconFlags::default()), - Icon::KimiLogo - ); -} - -#[test] -fn other_fireworks_hosted_models_keep_the_agent_glyph() { - // The Kimi match is anchored to a leading `kimi` token precisely so the - // other Fireworks-hosted families don't inherit its branding. - for id in [ - "glm-46-fireworks", - "minimax-m2-fireworks", - "qwen3-coder-fireworks", - "deepseek-v3-fireworks", - "kimika", - ] { - let llm = server_llm(id, None); - assert_eq!( - model_leading_icon(&llm, ModelIconFlags::default()), - Icon::Agent, - "{id} should keep the agent glyph" - ); - } -} - -#[test] -fn kimi_models_still_yield_to_auto_and_host_badges() { - let llm = server_llm("kimi-k3-fireworks", None); - - assert_eq!( - model_leading_icon( - &llm, - ModelIconFlags { - is_auto: true, - ..Default::default() - } - ), - Icon::Agent - ); - assert_eq!( - model_leading_icon( - &llm, - ModelIconFlags { - is_custom_router: true, - ..Default::default() - } - ), - Icon::Dataflow - ); - assert_eq!( - model_leading_icon( - &llm, - ModelIconFlags { - is_using_bedrock: true, - ..Default::default() - } - ), - Icon::Aws - ); -} - // -- build_custom_llm_infos / display label tests -- fn endpoint( diff --git a/app/src/terminal/view/ambient_agent/model_selector.rs b/app/src/terminal/view/ambient_agent/model_selector.rs index 78d2d8f9f02..63daa920e26 100644 --- a/app/src/terminal/view/ambient_agent/model_selector.rs +++ b/app/src/terminal/view/ambient_agent/model_selector.rs @@ -22,7 +22,7 @@ use crate::ai::custom_model_routers::is_custom_router_id; use crate::ai::execution_profiles::model_menu_items::is_auto; use crate::ai::harness_availability::{HarnessAvailabilityEvent, HarnessAvailabilityModel}; use crate::ai::harness_display::icon_for as harness_icon_for; -use crate::ai::llms::{LLMId, LLMPreferences, LLMPreferencesEvent, model_provider_icon}; +use crate::ai::llms::{LLMId, LLMPreferences, LLMPreferencesEvent, is_kimi_model}; use crate::editor::{ EditorView, Event as EditorEvent, PropagateAndNoOpEscapeKey, PropagateAndNoOpNavigationKeys, SingleLineEditorOptions, TextOptions, @@ -509,8 +509,10 @@ impl ModelSelector { let display_name = llm.menu_display_name(); let leading_icon = if is_custom_router_id(llm.id.as_str()) { Icon::Dataflow + } else if is_kimi_model(llm) { + Icon::KimiLogo } else { - model_provider_icon(llm) + llm.provider.icon().unwrap_or(Icon::Agent) }; let fields = MenuItemFields::new(display_name) .with_icon(leading_icon)