diff --git a/src/client/model.rs b/src/client/model.rs index cbd9623..51d3a20 100644 --- a/src/client/model.rs +++ b/src/client/model.rs @@ -1,5 +1,7 @@ use super::{ - ApiPatch, MessageContentToolCalls, RequestPatch, list_all_models, list_client_names, + ApiPatch, MessageContentToolCalls, RequestPatch, + catalog::{ALL_QUIRKS, apply_rules, quirks_for}, + list_all_models, list_client_names, message::{Message, MessageContent, MessageContentPart}, }; @@ -77,6 +79,9 @@ impl Model { { let mut new_model = Self::new(client_name, model_name); new_model.data.model_type = model_type.to_string(); + if let Some(quirks) = quirks_for(&ALL_QUIRKS, client_name) { + apply_rules(quirks, &mut new_model.data); + } return Ok(new_model); } } @@ -132,6 +137,7 @@ impl Model { output_price, supports_vision, supports_function_calling, + reasoning_levels, .. } = &self.data; let max_input_tokens = stringify_option_value(max_input_tokens); @@ -145,6 +151,9 @@ impl Model { if *supports_function_calling { capabilities.push('⚒'); }; + if !reasoning_levels.is_empty() { + capabilities.push('🧠'); + }; let capabilities: String = capabilities .into_iter() .map(|v| format!("{v} ")) @@ -419,3 +428,43 @@ where None => "-".to_string(), } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::client::ClientConfig; + + #[test] + fn created_from_name_inherits_family_rules() { + // The model registries are process-wide caches seeded by a ctor (see + // the request_context tests) that registers a "claude" client; the + // config below documents the shape a real lookup would use. + let claude: ClientConfig = serde_yaml::from_str( + "type: claude\nmodels:\n - name: test-claude-embedder\n type: embedding\n", + ) + .unwrap(); + let config = AppConfig { + clients: vec![claude], + ..AppConfig::default() + }; + let model = Model::retrieve_model( + &config, + "claude:claude-definitely-not-a-real-model", + ModelType::Chat, + ) + .unwrap(); + assert_eq!(model.name(), "claude-definitely-not-a-real-model"); + assert_eq!(model.model_type(), ModelType::Chat); + assert!(model.data.require_max_tokens); + } + + #[test] + fn description_shows_brain_glyph() { + let mut model = Model::new("claude", "some-model"); + model.data.reasoning_levels = vec!["low".to_string(), "high".to_string()]; + assert!(model.description().contains('🧠')); + + model.data.reasoning_levels.clear(); + assert!(!model.description().contains('🧠')); + } +} diff --git a/src/config/request_context.rs b/src/config/request_context.rs index f614356..38172ca 100644 --- a/src/config/request_context.rs +++ b/src/config/request_context.rs @@ -5232,7 +5232,7 @@ mod tests { // with an empty client list, which would permanently pin every later // model lookup in this process to "unknown model" and make any test that // needs a resolvable model dependent on test ordering. Seed the caches - // before any test runs with one client, "test-seeded", exposing a single + // before any test runs with a client, "test-seeded", exposing a single // embedding model. Deliberately NO chat model: some tests assert that no // chat model is available, and chat lookups don't need one — a // "test-seeded:" chat id resolves through the create-from-name @@ -5252,8 +5252,17 @@ mod tests { embedder.model_type = "embedding".to_string(); config.models = vec![embedder]; } + // A claude client is seeded alongside it so tests can exercise + // provider-quirk inheritance through the create-from-name fallback. + // It deliberately carries one embedding model: an empty `models` list + // would fall back to the full embedded claude catalog and register + // real chat models, breaking the no-chat-model invariant above. + let claude: ClientConfig = serde_yaml::from_str( + "type: claude\nmodels:\n - name: test-claude-embedder\n type: embedding\n", + ) + .unwrap(); let config = AppConfig { - clients: vec![client], + clients: vec![client, claude], ..AppConfig::default() }; let _ = list_client_names(&config);