add client-side ModelInfo overrides

This commit is contained in:
Sayan Sisodiya
2026-02-10 22:56:39 -08:00
parent 476c1a7160
commit a5d2b31065
8 changed files with 723 additions and 9 deletions
+13
View File
@@ -57,6 +57,7 @@ use codex_protocol::config_types::TrustLevel;
use codex_protocol::config_types::Verbosity;
use codex_protocol::config_types::WebSearchMode;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::openai_models::ModelInfoPatch;
use codex_protocol::openai_models::ReasoningEffort;
use codex_rmcp_client::OAuthCredentialsStoreMode;
use codex_utils_absolute_path::AbsolutePathBuf;
@@ -317,6 +318,9 @@ pub struct Config {
/// Optional override to force-enable reasoning summaries for the configured model.
pub model_supports_reasoning_summaries: Option<bool>,
/// Per-model metadata overrides keyed by model slug.
pub model_info_overrides: HashMap<String, ModelInfoPatch>,
/// Optional verbosity control for GPT-5 models (Responses API `text.verbosity`).
pub model_verbosity: Option<Verbosity>,
@@ -975,6 +979,10 @@ pub struct ConfigToml {
/// Override to force-enable reasoning summaries for the configured model.
pub model_supports_reasoning_summaries: Option<bool>,
/// Per-model metadata overrides keyed by model slug.
#[serde(default)]
pub model_info_overrides: HashMap<String, ModelInfoPatch>,
/// Optionally specify a personality for the model
pub personality: Option<Personality>,
@@ -1766,6 +1774,7 @@ impl Config {
.or(cfg.model_reasoning_summary)
.unwrap_or_default(),
model_supports_reasoning_summaries: cfg.model_supports_reasoning_summaries,
model_info_overrides: cfg.model_info_overrides,
model_verbosity: config_profile.model_verbosity.or(cfg.model_verbosity),
chatgpt_base_url: config_profile
.chatgpt_base_url
@@ -4054,6 +4063,7 @@ model_verbosity = "high"
model_reasoning_effort: Some(ReasoningEffort::High),
model_reasoning_summary: ReasoningSummary::Detailed,
model_supports_reasoning_summaries: None,
model_info_overrides: HashMap::new(),
model_verbosity: None,
personality: Some(Personality::Pragmatic),
chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(),
@@ -4160,6 +4170,7 @@ model_verbosity = "high"
model_reasoning_effort: None,
model_reasoning_summary: ReasoningSummary::default(),
model_supports_reasoning_summaries: None,
model_info_overrides: HashMap::new(),
model_verbosity: None,
personality: Some(Personality::Pragmatic),
chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(),
@@ -4264,6 +4275,7 @@ model_verbosity = "high"
model_reasoning_effort: None,
model_reasoning_summary: ReasoningSummary::default(),
model_supports_reasoning_summaries: None,
model_info_overrides: HashMap::new(),
model_verbosity: None,
personality: Some(Personality::Pragmatic),
chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(),
@@ -4354,6 +4366,7 @@ model_verbosity = "high"
model_reasoning_effort: Some(ReasoningEffort::High),
model_reasoning_summary: ReasoningSummary::Detailed,
model_supports_reasoning_summaries: None,
model_info_overrides: HashMap::new(),
model_verbosity: Some(Verbosity::High),
personality: Some(Personality::Pragmatic),
chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(),
+12 -8
View File
@@ -134,17 +134,19 @@ impl ModelsManager {
}
// todo(aibrahim): look if we can tighten it to pub(crate)
/// Look up model metadata, applying remote overrides and config adjustments.
/// Look up model metadata, applying remote overrides and config adjustments, as well as client overrides.
pub async fn get_model_info(&self, model: &str, config: &Config) -> ModelInfo {
let remote = self
.find_remote_model_by_longest_prefix(model, config)
.await;
let model = if let Some(remote) = remote {
let model_info = if let Some(remote) = remote {
remote
} else {
model_info::model_info_from_slug(model)
};
model_info::with_config_overrides(model, config)
let model_info = model_info::with_model_info_patches(model_info, model, config);
model_info::with_config_overrides(model_info, config)
}
async fn find_remote_model_by_longest_prefix(
@@ -369,11 +371,13 @@ impl ModelsManager {
}
/// Build `ModelInfo` without consulting remote state or cache.
pub(crate) fn construct_model_info_offline_for_tests(
model: &str,
config: &Config,
) -> ModelInfo {
model_info::with_config_overrides(model_info::model_info_from_slug(model), config)
///
/// This follows the same override precedence as `get_model_info`:
/// per-model `model_info_overrides` are applied first, then top-level config overrides.
pub(crate) fn construct_model_info_offline_for_tests(model: &str, config: &Config) -> ModelInfo {
let model_info = model_info::model_info_from_slug(model);
let model_info = model_info::with_model_info_patches(model_info, model, config);
model_info::with_config_overrides(model_info, config)
}
}
@@ -19,6 +19,102 @@ const LOCAL_FRIENDLY_TEMPLATE: &str =
const LOCAL_PRAGMATIC_TEMPLATE: &str = "You are a deeply pragmatic, effective software engineer.";
const PERSONALITY_PLACEHOLDER: &str = "{{ personality }}";
pub(crate) fn with_model_info_patch(
mut model: ModelInfo,
model_key: &str,
config: &Config,
) -> ModelInfo {
let Some(model_info_patch) = config.model_info_overrides.get(model_key) else {
return model;
};
model.slug = model_key.to_string();
if let Some(display_name) = &model_info_patch.display_name {
model.display_name = display_name.clone();
}
if let Some(description) = &model_info_patch.description {
model.description = Some(description.clone());
}
if let Some(default_reasoning_level) = model_info_patch.default_reasoning_level {
model.default_reasoning_level = Some(default_reasoning_level);
}
if let Some(supported_reasoning_levels) = &model_info_patch.supported_reasoning_levels {
model.supported_reasoning_levels = supported_reasoning_levels.clone();
}
if let Some(shell_type) = model_info_patch.shell_type {
model.shell_type = shell_type;
}
if let Some(visibility) = model_info_patch.visibility {
model.visibility = visibility;
}
if let Some(supported_in_api) = model_info_patch.supported_in_api {
model.supported_in_api = supported_in_api;
}
if let Some(priority) = model_info_patch.priority {
model.priority = priority;
}
if let Some(upgrade) = &model_info_patch.upgrade {
model.upgrade = Some(upgrade.clone());
}
if let Some(base_instructions) = &model_info_patch.base_instructions {
model.base_instructions = base_instructions.clone();
}
if let Some(model_messages) = &model_info_patch.model_messages {
model.model_messages = Some(model_messages.clone());
}
if let Some(supports_reasoning_summaries) = model_info_patch.supports_reasoning_summaries {
model.supports_reasoning_summaries = supports_reasoning_summaries;
}
if let Some(support_verbosity) = model_info_patch.support_verbosity {
model.support_verbosity = support_verbosity;
}
if let Some(default_verbosity) = model_info_patch.default_verbosity {
model.default_verbosity = Some(default_verbosity);
}
if let Some(apply_patch_tool_type) = &model_info_patch.apply_patch_tool_type {
model.apply_patch_tool_type = Some(apply_patch_tool_type.clone());
}
if let Some(truncation_policy) = model_info_patch.truncation_policy {
model.truncation_policy = truncation_policy;
}
if let Some(supports_parallel_tool_calls) = model_info_patch.supports_parallel_tool_calls {
model.supports_parallel_tool_calls = supports_parallel_tool_calls;
}
if let Some(context_window) = model_info_patch.context_window {
model.context_window = Some(context_window);
}
if let Some(auto_compact_token_limit) = model_info_patch.auto_compact_token_limit {
model.auto_compact_token_limit = Some(auto_compact_token_limit);
}
if let Some(effective_context_window_percent) =
model_info_patch.effective_context_window_percent
{
model.effective_context_window_percent = effective_context_window_percent;
}
if let Some(experimental_supported_tools) = &model_info_patch.experimental_supported_tools {
model.experimental_supported_tools = experimental_supported_tools.clone();
}
if let Some(input_modalities) = &model_info_patch.input_modalities {
model.input_modalities = input_modalities.clone();
}
model
}
pub(crate) fn with_model_info_patches(
mut model_info: ModelInfo,
requested_model: &str,
config: &Config,
) -> ModelInfo {
let resolved_slug = model_info.slug.clone();
model_info = with_model_info_patch(model_info, &resolved_slug, config);
if requested_model != resolved_slug {
model_info = with_model_info_patch(model_info, requested_model, config);
}
model_info
}
pub(crate) fn with_config_overrides(mut model: ModelInfo, config: &Config) -> ModelInfo {
if let Some(supports_reasoning_summaries) = config.model_supports_reasoning_summaries {
model.supports_reasoning_summaries = supports_reasoning_summaries;