Files
codex/codex-rs/models-manager/src/model_info.rs
Shijie Rao 370b13afc9 Honor client-resolved service tier defaults (#23537)
## Why

Model catalog responses can now advertise a nullable
`default_service_tier` for each model. Codex needs to preserve three
distinct states all the way from config/app-server inputs to inference:

- no explicit service tier, so the client may apply the current model
catalog default when FastMode is enabled
- explicit `default`, meaning the user intentionally wants standard
routing
- explicit catalog tier ids such as `priority`, `flex`, or future tiers

Keeping those states distinct prevents the UI from showing one tier
while core sends another, especially after model switches or app-server
`thread/start` / `turn/start` updates.

## What Changed

- Plumbed `default_service_tier` through model catalog protocol types,
app-server model responses, generated schemas, model cache fixtures, and
provider/model-manager conversions.
- Added the request-only `default` service tier sentinel and normalized
legacy config spelling so `fast` in `config.toml` still materializes as
the runtime/request id `priority`.
- Moved catalog default resolution to the TUI/client side, including
recomputing the effective service tier when model/FastMode-dependent
surfaces change.
- Updated app-server thread lifecycle config construction so
`serviceTier: null` preserves explicit standard-routing intent by
mapping to `default` instead of internal `None`.
- Kept core responsible for validating explicit tiers against the
current model and stripping `default` before `/v1/responses`, without
applying catalog defaults itself.

## Validation

- `CARGO_INCREMENTAL=0 cargo build -p codex-cli`
- `CARGO_INCREMENTAL=0 cargo test -p codex-app-server model_list`
- `cargo test -p codex-tui service_tier`
- `cargo test -p codex-protocol service_tier_for_request`
- `cargo test -p codex-core get_service_tier`
- `RUST_MIN_STACK=8388608 CARGO_INCREMENTAL=0 cargo test -p codex-core
service_tier`
2026-05-20 15:57:50 -07:00

124 lines
5.1 KiB
Rust

use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::openai_models::ConfigShellToolType;
use codex_protocol::openai_models::ModelInfo;
use codex_protocol::openai_models::ModelInstructionsVariables;
use codex_protocol::openai_models::ModelMessages;
use codex_protocol::openai_models::ModelVisibility;
use codex_protocol::openai_models::TruncationMode;
use codex_protocol::openai_models::TruncationPolicyConfig;
use codex_protocol::openai_models::WebSearchToolType;
use codex_protocol::openai_models::default_input_modalities;
use crate::config::ModelsManagerConfig;
use codex_utils_output_truncation::approx_bytes_for_tokens;
use tracing::warn;
pub const BASE_INSTRUCTIONS: &str = include_str!("../prompt.md");
const DEFAULT_PERSONALITY_HEADER: &str = "You are Codex, a coding agent based on GPT-5. You and the user share the same workspace and collaborate to achieve the user's goals.";
const LOCAL_FRIENDLY_TEMPLATE: &str =
"You optimize for team morale and being a supportive teammate as much as code quality.";
const LOCAL_PRAGMATIC_TEMPLATE: &str = "You are a deeply pragmatic, effective software engineer.";
const PERSONALITY_PLACEHOLDER: &str = "{{ personality }}";
pub fn with_config_overrides(mut model: ModelInfo, config: &ModelsManagerConfig) -> ModelInfo {
if let Some(supports_reasoning_summaries) = config.model_supports_reasoning_summaries
&& supports_reasoning_summaries
{
model.supports_reasoning_summaries = true;
}
if let Some(context_window) = config.model_context_window {
model.context_window = Some(
model
.max_context_window
.map_or(context_window, |max_context_window| {
context_window.min(max_context_window)
}),
);
}
if let Some(auto_compact_token_limit) = config.model_auto_compact_token_limit {
model.auto_compact_token_limit = Some(auto_compact_token_limit);
}
if let Some(token_limit) = config.tool_output_token_limit {
model.truncation_policy = match model.truncation_policy.mode {
TruncationMode::Bytes => {
let byte_limit =
i64::try_from(approx_bytes_for_tokens(token_limit)).unwrap_or(i64::MAX);
TruncationPolicyConfig::bytes(byte_limit)
}
TruncationMode::Tokens => {
let limit = i64::try_from(token_limit).unwrap_or(i64::MAX);
TruncationPolicyConfig::tokens(limit)
}
};
}
if let Some(base_instructions) = &config.base_instructions {
model.base_instructions = base_instructions.clone();
model.model_messages = None;
} else if !config.personality_enabled {
model.model_messages = None;
}
model
}
/// Build a minimal fallback model descriptor for missing/unknown slugs.
pub fn model_info_from_slug(slug: &str) -> ModelInfo {
warn!("Unknown model {slug} is used. This will use fallback model metadata.");
ModelInfo {
slug: slug.to_string(),
display_name: slug.to_string(),
description: None,
default_reasoning_level: None,
supported_reasoning_levels: Vec::new(),
shell_type: ConfigShellToolType::Default,
visibility: ModelVisibility::None,
supported_in_api: true,
priority: 99,
additional_speed_tiers: Vec::new(),
service_tiers: Vec::new(),
default_service_tier: None,
availability_nux: None,
upgrade: None,
base_instructions: BASE_INSTRUCTIONS.to_string(),
model_messages: local_personality_messages_for_slug(slug),
supports_reasoning_summaries: false,
default_reasoning_summary: ReasoningSummary::Auto,
support_verbosity: false,
default_verbosity: None,
apply_patch_tool_type: None,
web_search_tool_type: WebSearchToolType::Text,
truncation_policy: TruncationPolicyConfig::bytes(/*limit*/ 10_000),
supports_parallel_tool_calls: false,
supports_image_detail_original: false,
context_window: Some(272_000),
max_context_window: Some(272_000),
auto_compact_token_limit: None,
effective_context_window_percent: 95,
experimental_supported_tools: Vec::new(),
input_modalities: default_input_modalities(),
used_fallback_model_metadata: true, // this is the fallback model metadata
supports_search_tool: false,
}
}
fn local_personality_messages_for_slug(slug: &str) -> Option<ModelMessages> {
match slug {
"gpt-5.2-codex" | "exp-codex-personality" => Some(ModelMessages {
instructions_template: Some(format!(
"{DEFAULT_PERSONALITY_HEADER}\n\n{PERSONALITY_PLACEHOLDER}\n\n{BASE_INSTRUCTIONS}"
)),
instructions_variables: Some(ModelInstructionsVariables {
personality_default: Some(String::new()),
personality_friendly: Some(LOCAL_FRIENDLY_TEMPLATE.to_string()),
personality_pragmatic: Some(LOCAL_PRAGMATIC_TEMPLATE.to_string()),
}),
}),
_ => None,
}
}
#[cfg(test)]
#[path = "model_info_tests.rs"]
mod tests;