mirror of
https://github.com/openai/codex.git
synced 2026-10-19 17:28:46 +00:00
Preserve configured Flex tiers without catalog or fast-mode support (#46230)
## Why An explicit `service_tier = "flex"` could be dropped when fast mode was disabled or the model catalog did not advertise Flex, even though Flex is an API request option. ## What changed - Preserve configured `flex` through core session settings, TUI tier resolution, and request construction regardless of fast-mode or catalog support. - Omit `service_tier` from Amazon Bedrock requests, including with custom catalogs, because Bedrock only supports the implicit default tier. ## Testing Add regression coverage for Flex configured at startup or through thread settings, connected thread startup, and TUI turn submission. Update review coverage to expect Flex with fast mode disabled, and verify that both Bedrock providers omit `service_tier`. GitOrigin-RevId: 98f2761bdd780603839ebfec75cb983a5d55e554
This commit is contained in:
@@ -932,7 +932,12 @@ impl ModelClient {
|
||||
prompt.output_schema_strict,
|
||||
);
|
||||
let prompt_cache_key = Some(self.prompt_cache_key(responses_metadata));
|
||||
let service_tier = model_info.service_tier_for_request(service_tier);
|
||||
let service_tier = if self.state.provider.info().is_amazon_bedrock() {
|
||||
// Bedrock only supports the implicit default tier, including with custom catalogs.
|
||||
None
|
||||
} else {
|
||||
model_info.service_tier_for_request(service_tier)
|
||||
};
|
||||
let request = ResponsesApiRequest {
|
||||
model: model_info.slug.clone(),
|
||||
instructions,
|
||||
|
||||
@@ -107,6 +107,7 @@ use codex_protocol::approvals::NetworkPolicyRuleAction;
|
||||
use codex_protocol::config_types::ApprovalsReviewer;
|
||||
use codex_protocol::config_types::AutoCompactTokenLimitScope;
|
||||
use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::config_types::WebSearchMode;
|
||||
use codex_protocol::dynamic_tools::DynamicToolResponse;
|
||||
use codex_protocol::dynamic_tools::DynamicToolSpec;
|
||||
@@ -1076,13 +1077,17 @@ fn get_service_tier(
|
||||
fast_mode_enabled: bool,
|
||||
model_info: &ModelInfo,
|
||||
) -> Option<String> {
|
||||
if !fast_mode_enabled {
|
||||
return None;
|
||||
let service_tier = configured_service_tier?;
|
||||
if service_tier == ServiceTier::Flex.request_value() {
|
||||
return Some(service_tier);
|
||||
}
|
||||
configured_service_tier.filter(|service_tier| {
|
||||
service_tier == SERVICE_TIER_DEFAULT_REQUEST_VALUE
|
||||
|| model_info.supports_service_tier(service_tier)
|
||||
})
|
||||
if fast_mode_enabled
|
||||
&& (service_tier == SERVICE_TIER_DEFAULT_REQUEST_VALUE
|
||||
|| model_info.supports_service_tier(&service_tier))
|
||||
{
|
||||
return Some(service_tier);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn unsupported_service_tier_warning(
|
||||
|
||||
@@ -4626,14 +4626,6 @@ fn get_service_tier_drops_unsupported_configured_tier_when_fast_mode_enabled() {
|
||||
),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
get_service_tier(
|
||||
Some(ServiceTier::Flex.request_value().to_string()),
|
||||
/*fast_mode_enabled*/ true,
|
||||
&model_info,
|
||||
),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
get_service_tier(
|
||||
Some(SERVICE_TIER_DEFAULT_REQUEST_VALUE.to_string()),
|
||||
@@ -4645,7 +4637,22 @@ fn get_service_tier_drops_unsupported_configured_tier_when_fast_mode_enabled() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_service_tier_ignores_configured_tier_when_fast_mode_disabled() {
|
||||
fn get_service_tier_preserves_flex_without_catalog_support_or_fast_mode() {
|
||||
let model_info = model_with_default_service_tier(/*default_service_tier*/ None);
|
||||
for fast_mode_enabled in [false, true] {
|
||||
assert_eq!(
|
||||
get_service_tier(
|
||||
Some(ServiceTier::Flex.request_value().to_string()),
|
||||
fast_mode_enabled,
|
||||
&model_info,
|
||||
),
|
||||
Some(ServiceTier::Flex.request_value().to_string())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_service_tier_ignores_non_flex_tiers_when_fast_mode_disabled() {
|
||||
let model_info = model_with_default_service_tier(Some(ServiceTier::Fast.request_value()));
|
||||
|
||||
assert_eq!(
|
||||
|
||||
@@ -18,8 +18,10 @@ use codex_login::AuthKeyringBackendKind;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_login::auth::AgentIdentityAuthPolicy;
|
||||
use codex_login::auth::BedrockApiKeyAuth;
|
||||
use codex_login::default_client::originator;
|
||||
use codex_model_provider_info::AMAZON_BEDROCK_PROVIDER_ID;
|
||||
use codex_model_provider_info::AMAZON_BEDROCK_RUNTIME_PROVIDER_ID;
|
||||
use codex_model_provider_info::ModelProviderInfo;
|
||||
use codex_model_provider_info::WireApi;
|
||||
use codex_model_provider_info::built_in_model_providers;
|
||||
@@ -105,6 +107,96 @@ const INSTALLATION_ID_FILENAME: &str = "installation_id";
|
||||
const TEST_WINDOW_ID: &str = "test-thread:0";
|
||||
const TEST_INSTALLATION_ID: &str = "11111111-1111-4111-8111-111111111111";
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn responses_request_preserves_flex_without_catalog_support_or_fast_mode()
|
||||
-> anyhow::Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
for configure_at_start in [false, true] {
|
||||
let server = start_mock_server().await;
|
||||
let response_mock = mount_sse_once(&server, sse(vec![ev_completed("done")])).await;
|
||||
let test = test_codex()
|
||||
.with_model("gpt-5.4")
|
||||
.with_model_info_override("gpt-5.4", |model| model.service_tiers.clear())
|
||||
.with_config(move |config| {
|
||||
config
|
||||
.features
|
||||
.disable(Feature::FastMode)
|
||||
.expect("disable FastMode");
|
||||
config.service_tier = configure_at_start.then(|| "flex".to_string());
|
||||
})
|
||||
.build_with_auto_env(&server)
|
||||
.await?;
|
||||
if !configure_at_start {
|
||||
core_test_support::submit_thread_settings(
|
||||
&test.codex,
|
||||
ThreadSettingsOverrides {
|
||||
service_tier: Some(Some("flex".to_string())),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
test.codex
|
||||
.start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}]))
|
||||
.await?;
|
||||
wait_for_event(&test.codex, |event| {
|
||||
matches!(event, EventMsg::TurnComplete(_))
|
||||
})
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
response_mock.single_request().body_json()["service_tier"],
|
||||
json!("flex"),
|
||||
"configure_at_start={configure_at_start}",
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn responses_request_uses_implicit_default_tier_for_bedrock() -> anyhow::Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
for provider_id in [
|
||||
AMAZON_BEDROCK_PROVIDER_ID,
|
||||
AMAZON_BEDROCK_RUNTIME_PROVIDER_ID,
|
||||
] {
|
||||
let server = start_mock_server().await;
|
||||
let response_mock = mount_sse_once(&server, sse(vec![ev_completed("done")])).await;
|
||||
let test = test_codex()
|
||||
.with_auth(CodexAuth::BedrockApiKey(BedrockApiKeyAuth {
|
||||
api_key: "dummy".to_string(),
|
||||
region: "us-east-1".to_string(),
|
||||
}))
|
||||
.with_model("gpt-5.4")
|
||||
.with_model_info_override("gpt-5.4", |model| model.service_tiers.clear())
|
||||
.with_config(move |config| {
|
||||
let base_url = config.model_provider.base_url.clone();
|
||||
config.model_provider_id = provider_id.to_string();
|
||||
config.model_provider = built_in_model_providers(/*openai_base_url*/ None)
|
||||
.remove(provider_id)
|
||||
.expect("built-in Bedrock provider");
|
||||
config.model_provider.base_url = base_url;
|
||||
config.service_tier = Some("flex".to_string());
|
||||
})
|
||||
.build_with_auto_env(&server)
|
||||
.await?;
|
||||
test.submit_turn("hello").await?;
|
||||
|
||||
assert_eq!(
|
||||
response_mock
|
||||
.single_request()
|
||||
.body_json()
|
||||
.get("service_tier"),
|
||||
None,
|
||||
"provider={provider_id}",
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn rollout_response_item(item: ResponseItem) -> RolloutItem {
|
||||
RolloutItem::ResponseItem(item.into())
|
||||
}
|
||||
|
||||
@@ -717,7 +717,7 @@ async fn unsupported_configured_service_tier_warns_at_session_start() -> Result<
|
||||
let mut builder = test_codex()
|
||||
.with_model(model_slug)
|
||||
.with_config(move |config| {
|
||||
config.service_tier = Some(ServiceTier::Flex.request_value().to_string());
|
||||
config.service_tier = Some(ServiceTier::Fast.request_value().to_string());
|
||||
config.model_catalog = Some(ModelsResponse {
|
||||
models: vec![model],
|
||||
});
|
||||
@@ -737,7 +737,7 @@ async fn unsupported_configured_service_tier_warns_at_session_start() -> Result<
|
||||
};
|
||||
assert_eq!(
|
||||
warning.message,
|
||||
"Configured service tier `flex` is not advertised as supported for model `test-no-tier-model` and will be omitted from requests."
|
||||
"Configured service tier `priority` is not advertised as supported for model `test-no-tier-model` and will be omitted from requests."
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -806,7 +806,7 @@ async fn review_uses_updated_turn_permissions_and_approval_policy() {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn review_omits_retained_tier_when_fast_mode_disabled() -> anyhow::Result<()> {
|
||||
async fn review_preserves_flex_tier_when_fast_mode_disabled() -> anyhow::Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
let (server, request_log) =
|
||||
start_responses_server_with_sse(completed_sse(), /*expected_requests*/ 1).await;
|
||||
@@ -859,7 +859,7 @@ async fn review_omits_retained_tier_when_fast_mode_disabled() -> anyhow::Result<
|
||||
);
|
||||
assert_eq!(
|
||||
request_log.single_request().body_json().get("service_tier"),
|
||||
None
|
||||
Some(&serde_json::json!("flex"))
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -867,9 +867,12 @@ impl ModelPreset {
|
||||
|
||||
impl ModelInfo {
|
||||
pub fn supports_service_tier(&self, service_tier: &str) -> bool {
|
||||
self.service_tiers
|
||||
.iter()
|
||||
.any(|tier| tier.id == service_tier)
|
||||
// Flex is an API request option, even when the Codex catalog does not advertise it.
|
||||
service_tier == ServiceTier::Flex.request_value()
|
||||
|| self
|
||||
.service_tiers
|
||||
.iter()
|
||||
.any(|tier| tier.id == service_tier)
|
||||
}
|
||||
|
||||
pub fn service_tier_for_request(&self, service_tier: Option<String>) -> Option<String> {
|
||||
@@ -1839,6 +1842,19 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn service_tier_for_request_preserves_flex_without_catalog_support() {
|
||||
let model = ModelInfo {
|
||||
service_tiers: Vec::new(),
|
||||
..test_model(/*spec*/ None)
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
model.service_tier_for_request(Some(ServiceTier::Flex.request_value().to_string())),
|
||||
Some(ServiceTier::Flex.request_value().to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn service_tier_for_request_filters_unsupported_tiers() {
|
||||
let model = ModelInfo {
|
||||
|
||||
@@ -3473,6 +3473,40 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn connected_thread_start_preserves_flex_without_catalog_support() -> Result<()> {
|
||||
for fast_mode_enabled in [false, true] {
|
||||
let codex_home = tempfile::tempdir()?;
|
||||
let server_config = build_config(&codex_home).await;
|
||||
let mut config = server_config.clone();
|
||||
config.model = Some("gpt-5.5".to_string());
|
||||
config.service_tier = Some(ServiceTier::Flex.request_value().to_string());
|
||||
config
|
||||
.features
|
||||
.set_enabled(Feature::FastMode, fast_mode_enabled)?;
|
||||
let mut app_server =
|
||||
crate::start_embedded_app_server_for_picker(&server_config).await?;
|
||||
app_server.thread_params_mode = ThreadParamsMode::Remote;
|
||||
let mut preset = crate::test_support::TEST_MODEL_PRESETS
|
||||
.iter()
|
||||
.find(|preset| preset.model == "gpt-5.5")
|
||||
.expect("gpt-5.5 test preset")
|
||||
.clone();
|
||||
preset.service_tiers.clear();
|
||||
preset.default_service_tier = None;
|
||||
app_server.available_models = vec![preset];
|
||||
|
||||
let started = app_server.start_thread(&config).await?;
|
||||
|
||||
assert_eq!(
|
||||
started.session.service_tier,
|
||||
Some(ServiceTier::Flex.request_value().to_string())
|
||||
);
|
||||
app_server.shutdown().await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn persisted_resume_does_not_forward_implicit_service_tier() -> Result<()> {
|
||||
let codex_home = tempfile::tempdir().expect("tempdir");
|
||||
|
||||
@@ -3319,6 +3319,39 @@ async fn user_turn_carries_service_tier_after_fast_toggle() {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn user_turn_preserves_flex_without_catalog_support() {
|
||||
for fast_mode_enabled in [false, true] {
|
||||
let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
|
||||
chat.thread_id = Some(ThreadId::new());
|
||||
set_chatgpt_auth(&mut chat);
|
||||
let mut models = chat.model_catalog.try_list_models().expect("test catalog");
|
||||
for model in &mut models {
|
||||
model.service_tiers.clear();
|
||||
model.default_service_tier = None;
|
||||
}
|
||||
chat.model_catalog = std::sync::Arc::new(ModelCatalog::new(models));
|
||||
chat.set_feature_enabled(Feature::FastMode, fast_mode_enabled);
|
||||
chat.set_service_tier(Some(ServiceTier::Flex.request_value().to_string()));
|
||||
|
||||
assert_eq!(
|
||||
chat.current_service_tier(),
|
||||
Some(ServiceTier::Flex.request_value())
|
||||
);
|
||||
chat.bottom_pane
|
||||
.set_composer_text("hello".to_string(), Vec::new(), Vec::new());
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
|
||||
|
||||
let Op::UserTurn { service_tier, .. } = next_submit_op(&mut op_rx) else {
|
||||
panic!("expected a user turn");
|
||||
};
|
||||
assert_eq!(
|
||||
service_tier,
|
||||
Some(Some(ServiceTier::Flex.request_value().to_string()))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn model_switch_recomputes_catalog_default_service_tier() {
|
||||
let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(Some("gpt-5.2")).await;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::legacy_core::config::Config;
|
||||
use codex_features::Feature;
|
||||
use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::openai_models::ModelPreset;
|
||||
|
||||
pub(crate) fn configured_service_tier(
|
||||
@@ -19,11 +20,14 @@ pub(crate) fn effective_service_tier(
|
||||
model: &str,
|
||||
models: &[ModelPreset],
|
||||
) -> Option<String> {
|
||||
let configured = configured_service_tier(config, notices);
|
||||
if configured.as_deref() == Some(ServiceTier::Flex.request_value()) {
|
||||
return configured;
|
||||
}
|
||||
if !config.features.enabled(Feature::FastMode) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let configured = configured_service_tier(config, notices);
|
||||
let Some(preset) = models.iter().find(|preset| preset.model == model) else {
|
||||
return configured;
|
||||
};
|
||||
@@ -45,15 +49,15 @@ pub(crate) fn service_tier_update_for_core(
|
||||
model: &str,
|
||||
models: &[ModelPreset],
|
||||
) -> Option<Option<String>> {
|
||||
if !config.features.enabled(Feature::FastMode) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let effective = effective_service_tier(config, notices, model, models);
|
||||
if let Some(service_tier) = effective {
|
||||
return Some(Some(service_tier));
|
||||
}
|
||||
|
||||
if !config.features.enabled(Feature::FastMode) {
|
||||
return None;
|
||||
}
|
||||
|
||||
if !models.iter().any(|preset| preset.model == model) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user