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`
This commit is contained in:
Shijie Rao authored and GitHub committed 2026-05-20 15:57:50 -07:00
1 parent 0e9d222178
commit 370b13afc9
54 files changed
+682 -192

No files matched your search

+76
View File
@@ -17,6 +17,7 @@ use ts_rs::TS;
use crate::config_types::Personality;
use crate::config_types::ReasoningSummary;
use crate::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
use crate::config_types::ServiceTier;
use crate::config_types::Verbosity;
@@ -147,6 +148,9 @@ pub struct ModelPreset {
/// Service tiers this model can run with.
#[serde(default)]
pub service_tiers: Vec<ModelServiceTier>,
/// Catalog default service tier id for this model.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_service_tier: Option<String>,
/// Whether this is the default model for new users.
pub is_default: bool,
/// recommended upgrade model
@@ -270,6 +274,8 @@ pub struct ModelInfo {
pub additional_speed_tiers: Vec<String>,
#[serde(default)]
pub service_tiers: Vec<ModelServiceTier>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_service_tier: Option<String>,
pub availability_nux: Option<ModelAvailabilityNux>,
pub upgrade: Option<ModelInfoUpgrade>,
pub base_instructions: String,
@@ -455,6 +461,7 @@ impl From<ModelInfo> for ModelPreset {
supports_personality,
additional_speed_tiers: info.additional_speed_tiers,
service_tiers: info.service_tiers,
default_service_tier: info.default_service_tier,
is_default: false, // default is the highest priority available model
upgrade: info.upgrade.as_ref().map(|upgrade| ModelUpgrade {
id: upgrade.model.clone(),
@@ -493,6 +500,13 @@ impl ModelInfo {
.iter()
.any(|tier| tier.id == service_tier)
}
pub fn service_tier_for_request(&self, service_tier: Option<String>) -> Option<String> {
service_tier.filter(|service_tier| {
service_tier != SERVICE_TIER_DEFAULT_REQUEST_VALUE
&& self.supports_service_tier(service_tier)
})
}
}
impl ModelPreset {
@@ -576,6 +590,7 @@ mod tests {
priority: 1,
additional_speed_tiers: Vec::new(),
service_tiers: Vec::new(),
default_service_tier: None,
availability_nux: None,
upgrade: None,
base_instructions: "base".to_string(),
@@ -846,6 +861,7 @@ mod tests {
message: "Try Spark.".to_string(),
}),
additional_speed_tiers: vec![SPEED_TIER_FAST.to_string()],
default_service_tier: Some(ServiceTier::Fast.request_value().to_string()),
service_tiers: Vec::new(),
..test_model(/*spec*/ None)
});
@@ -857,6 +873,10 @@ mod tests {
})
);
assert!(preset.supports_fast_mode());
assert_eq!(
preset.default_service_tier,
Some(ServiceTier::Fast.request_value().to_string())
);
}
#[test]
@@ -872,4 +892,60 @@ mod tests {
assert!(preset.supports_fast_mode());
}
#[test]
fn service_tier_for_request_omits_explicit_default_tier() {
let model = ModelInfo {
default_service_tier: Some(ServiceTier::Fast.request_value().to_string()),
service_tiers: vec![ModelServiceTier {
id: ServiceTier::Fast.request_value().to_string(),
name: "Fast".to_string(),
description: "Priority processing.".to_string(),
}],
..test_model(/*spec*/ None)
};
assert_eq!(
model.service_tier_for_request(Some(SERVICE_TIER_DEFAULT_REQUEST_VALUE.to_string())),
None
);
}
#[test]
fn service_tier_for_request_filters_unsupported_tiers() {
let model = ModelInfo {
default_service_tier: Some(ServiceTier::Fast.request_value().to_string()),
service_tiers: vec![ModelServiceTier {
id: ServiceTier::Fast.request_value().to_string(),
name: "Fast".to_string(),
description: "Priority processing.".to_string(),
}],
..test_model(/*spec*/ None)
};
assert_eq!(
model.service_tier_for_request(Some(ServiceTier::Fast.request_value().to_string())),
Some(ServiceTier::Fast.request_value().to_string())
);
assert_eq!(
model.service_tier_for_request(Some("unsupported".to_string())),
None
);
assert_eq!(model.service_tier_for_request(/*service_tier*/ None), None);
}
#[test]
fn service_tier_for_request_does_not_apply_catalog_default() {
let model = ModelInfo {
default_service_tier: Some(ServiceTier::Fast.request_value().to_string()),
service_tiers: vec![ModelServiceTier {
id: ServiceTier::Fast.request_value().to_string(),
name: "Fast".to_string(),
description: "Priority processing.".to_string(),
}],
..test_model(/*spec*/ None)
};
assert_eq!(model.service_tier_for_request(/*service_tier*/ None), None);
}
}