[codex] Generalize service tier slash commands (#21745)

## Why

`/fast` was wired as a one-off slash command even though model metadata
now exposes service tiers as catalog data. That meant adding another
tier, such as a slower/cheaper tier, would require more hardcoded TUI
plumbing instead of letting the model catalog drive the available
commands.

This change makes service-tier commands data-driven: each advertised
`service_tiers` entry becomes a `/name` command using the catalog
description, while the request path sends the tier `id` only when the
selected model supports it.

## What Changed

- Removed the hardcoded `/fast` slash-command variant and introduced
dynamic service-tier command items in the composer and command popup.
- Added toggle behavior for service-tier commands: invoking `/name`
selects that tier, and invoking it again clears the selection.
- Preserved the existing Fast-mode keybinding/status affordances by
resolving the current model tier whose name is `fast`, while still
sending the tier request value such as `priority`.
- Persisted service-tier selections as raw request strings so non-fast
tiers can round-trip through config.
- Updated the Bedrock catalog entry to advertise fast support through
`service_tiers` with `id: "priority"` and `name: "fast"`.
- Added defensive filtering in core so unsupported selected service
tiers are omitted from `/responses` requests.

## Validation

- Added/updated coverage for dynamic service-tier slash command lookup,
popup descriptions, composer dispatch, TUI fast toggling, and
unsupported-tier omission in core request construction.
- Local tests were not run per request.

---------

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Ahmed Ibrahim
2026-05-08 20:09:51 +03:00
committed by GitHub
parent 47f1d7b40b
commit 7c0e54bf59
24 changed files with 919 additions and 412 deletions

View File

@@ -9,6 +9,7 @@ use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::ConfigShellToolType;
use codex_protocol::openai_models::InputModality;
use codex_protocol::openai_models::ModelInfo;
use codex_protocol::openai_models::ModelServiceTier;
use codex_protocol::openai_models::ModelVisibility;
use codex_protocol::openai_models::ModelsResponse;
use codex_protocol::openai_models::ReasoningEffort;
@@ -320,9 +321,28 @@ async fn flex_service_tier_is_applied_to_http_turn() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let model_slug = "test-flex-model";
let mut flex_model = test_model_info(
model_slug,
model_slug,
"supports flex tier",
default_input_modalities(),
);
flex_model.service_tiers = vec![ModelServiceTier {
id: ServiceTier::Flex.request_value().to_string(),
name: "flex".to_string(),
description: "Flexible processing.".to_string(),
}];
let resp_mock = mount_sse_once(&server, sse_completed("resp-1")).await;
let test = test_codex().build(&server).await?;
let mut builder = test_codex()
.with_model(model_slug)
.with_config(move |config| {
config.model_catalog = Some(ModelsResponse {
models: vec![flex_model],
});
});
let test = builder.build(&server).await?;
test.submit_turn_with_service_tier("flex turn", Some(ServiceTier::Flex))
.await?;
@@ -334,6 +354,39 @@ async fn flex_service_tier_is_applied_to_http_turn() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn unsupported_service_tier_is_omitted_from_http_turn() -> 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 resp_mock = mount_sse_once(&server, sse_completed("resp-1")).await;
let mut builder = test_codex()
.with_model(model_slug)
.with_config(move |config| {
config.model_catalog = Some(ModelsResponse {
models: vec![model],
});
});
let test = builder.build(&server).await?;
test.submit_turn_with_service_tier("fast turn", Some(ServiceTier::Fast))
.await?;
let request = resp_mock.single_request();
let body = request.body_json();
assert_eq!(body.get("service_tier"), None);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn model_change_from_image_to_text_strips_prior_image_content() -> Result<()> {
skip_if_no_network!(Ok(()));