feat: apply provider quirks to models created from a name

Unknown chat/reranker models resolved through the create-from-name
fallback now inherit their provider's family rules (require_max_tokens,
reasoning defaults, etc.) from the quirks overlay, and the model
completion table marks reasoning-capable models with a brain glyph.
This commit is contained in:
Dark-Alex-17 committed 2026-09-02 14:57:06 -06:00
1 parent b19ac80783
commit e137866c0e
2 files changed
+61 -3

No files matched your search

+50 -1
View File
@@ -1,5 +1,7 @@
use super::{ 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}, message::{Message, MessageContent, MessageContentPart},
}; };
@@ -77,6 +79,9 @@ impl Model {
{ {
let mut new_model = Self::new(client_name, model_name); let mut new_model = Self::new(client_name, model_name);
new_model.data.model_type = model_type.to_string(); 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); return Ok(new_model);
} }
} }
@@ -132,6 +137,7 @@ impl Model {
output_price, output_price,
supports_vision, supports_vision,
supports_function_calling, supports_function_calling,
reasoning_levels,
.. ..
} = &self.data; } = &self.data;
let max_input_tokens = stringify_option_value(max_input_tokens); let max_input_tokens = stringify_option_value(max_input_tokens);
@@ -145,6 +151,9 @@ impl Model {
if *supports_function_calling { if *supports_function_calling {
capabilities.push('⚒'); capabilities.push('⚒');
}; };
if !reasoning_levels.is_empty() {
capabilities.push('🧠');
};
let capabilities: String = capabilities let capabilities: String = capabilities
.into_iter() .into_iter()
.map(|v| format!("{v} ")) .map(|v| format!("{v} "))
@@ -419,3 +428,43 @@ where
None => "-".to_string(), 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('🧠'));
}
}
+11 -2
View File
@@ -5219,7 +5219,7 @@ mod tests {
// with an empty client list, which would permanently pin every later // with an empty client list, which would permanently pin every later
// model lookup in this process to "unknown model" and make any test that // model lookup in this process to "unknown model" and make any test that
// needs a resolvable model dependent on test ordering. Seed the caches // 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 // embedding model. Deliberately NO chat model: some tests assert that no
// chat model is available, and chat lookups don't need one — a // chat model is available, and chat lookups don't need one — a
// "test-seeded:<anything>" chat id resolves through the create-from-name // "test-seeded:<anything>" chat id resolves through the create-from-name
@@ -5239,8 +5239,17 @@ mod tests {
embedder.model_type = "embedding".to_string(); embedder.model_type = "embedding".to_string();
config.models = vec![embedder]; 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 { let config = AppConfig {
clients: vec![client], clients: vec![client, claude],
..AppConfig::default() ..AppConfig::default()
}; };
let _ = list_client_names(&config); let _ = list_client_names(&config);