Warn when configured service tiers are unsupported (#31284)

## Why

Codex currently omits a configured `service_tier` when the selected
model's catalog entry does not advertise support for it. That fallback
is silent, so users can unknowingly send requests at the default tier
instead. This makes cases such as #26604 difficult to diagnose.

## What changed

- Emit the shared core `Warning` event during session startup when a
configured service tier will be omitted because the initial model does
not advertise support for it.
- Do not warn on later model or service-tier changes, which keeps
warning emission stateless and avoids client-specific handling.
- Keep the existing request filtering behavior unchanged.

## Validation

- `just test -p codex-core unsupported_service_tier`
- `just test -p codex-core
unsupported_configured_service_tier_warns_at_session_start`

### Manual validation

- Launched the real TUI with the bundled catalog, where `gpt-5.5`
advertises the `priority` tier but not `flex`, and configured
`service_tier = "flex"`.
- Confirmed the unsupported-tier warning appeared exactly once during
startup.
- Submitted two turns through a local Responses API SSE stub; both
received mock replies and neither emitted another warning.
- Inspected both captured `/v1/responses` request bodies and confirmed
that neither contained a `service_tier` field.
- Repeated the two-turn TUI flow against the live Responses API; both
turns completed and the warning was not repeated.
This commit is contained in:
Eric Traut
2026-07-06 18:08:35 -07:00
committed by GitHub
parent 8a18312ee5
commit 45be435135
2 changed files with 70 additions and 3 deletions

View File

@@ -629,11 +629,14 @@ impl Codex {
developer_instructions: None,
},
};
let service_tier = get_service_tier(
config.service_tier.clone(),
config.features.enabled(Feature::FastMode),
let fast_mode_enabled = config.features.enabled(Feature::FastMode);
let initial_service_tier_warning = unsupported_service_tier_warning(
config.service_tier.as_deref(),
fast_mode_enabled,
&model_info,
);
let service_tier =
get_service_tier(config.service_tier.clone(), fast_mode_enabled, &model_info);
let session_configuration = SessionConfiguration {
provider: config.model_provider.clone(),
collaboration_mode,
@@ -706,6 +709,14 @@ impl Codex {
error!("Failed to create session: {e:#}");
map_session_init_error(&e, &config.codex_home)
})?;
if let Some(message) = initial_service_tier_warning {
session
.send_event_raw(Event {
id: INITIAL_SUBMIT_ID.to_owned(),
msg: EventMsg::Warning(WarningEvent { message }),
})
.await;
}
let thread_id = session.thread_id;
// This task will run until Op::Shutdown is received.
@@ -908,6 +919,22 @@ fn get_service_tier(
})
}
fn unsupported_service_tier_warning(
configured_service_tier: Option<&str>,
fast_mode_enabled: bool,
model_info: &ModelInfo,
) -> Option<String> {
let service_tier = configured_service_tier.filter(|service_tier| {
fast_mode_enabled
&& *service_tier != SERVICE_TIER_DEFAULT_REQUEST_VALUE
&& !model_info.supports_service_tier(service_tier)
})?;
Some(format!(
"Configured service tier `{service_tier}` is not advertised as supported for model `{}` and will be omitted from requests.",
model_info.slug
))
}
fn session_permission_profile_state_from_config(
config: &Config,
) -> CodexResult<PermissionProfileState> {

View File

@@ -387,6 +387,46 @@ async fn unsupported_service_tier_is_omitted_from_http_turn() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn unsupported_configured_service_tier_warns_at_session_start() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let model_slug = "test-no-tier-model";
let model = test_model_info(
model_slug,
model_slug,
"no service tiers",
default_input_modalities(),
);
let mut builder = test_codex()
.with_model(model_slug)
.with_config(move |config| {
config.service_tier = Some(ServiceTier::Flex.request_value().to_string());
config.model_catalog = Some(ModelsResponse {
models: vec![model],
});
});
let test = builder.build(&server).await?;
let warning = wait_for_event(&test.codex, |event| {
matches!(
event,
EventMsg::Warning(warning)
if warning.message.contains("will be omitted from requests")
)
})
.await;
let EventMsg::Warning(warning) = warning else {
unreachable!("wait_for_event matched a warning")
};
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."
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn default_service_tier_override_is_omitted_from_http_turn() -> Result<()> {
skip_if_no_network!(Ok(()));