mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[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:
@@ -711,6 +711,8 @@ impl ModelClient {
|
||||
prompt.output_schema_strict,
|
||||
);
|
||||
let prompt_cache_key = Some(self.state.thread_id.to_string());
|
||||
let service_tier =
|
||||
service_tier.filter(|service_tier| model_info.supports_service_tier(service_tier));
|
||||
let request = ResponsesApiRequest {
|
||||
model: model_info.slug.clone(),
|
||||
instructions: instructions.clone(),
|
||||
|
||||
@@ -7,7 +7,6 @@ use codex_config::types::SessionPickerViewMode;
|
||||
use codex_config::types::ToolSuggestDisabledTool;
|
||||
use codex_features::FEATURES;
|
||||
use codex_protocol::config_types::Personality;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::config_types::TrustLevel;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use std::collections::BTreeMap;
|
||||
@@ -33,7 +32,7 @@ pub enum ConfigEdit {
|
||||
effort: Option<ReasoningEffort>,
|
||||
},
|
||||
/// Update the service tier preference for future turns.
|
||||
SetServiceTier { service_tier: Option<ServiceTier> },
|
||||
SetServiceTier { service_tier: Option<String> },
|
||||
/// Update the active (or default) model personality.
|
||||
SetModelPersonality { personality: Option<Personality> },
|
||||
/// Toggle the acknowledgement flag under `[notice]`.
|
||||
@@ -536,7 +535,9 @@ impl ConfigDocument {
|
||||
}),
|
||||
ConfigEdit::SetServiceTier { service_tier } => Ok(self.write_profile_value(
|
||||
&["service_tier"],
|
||||
service_tier.map(|service_tier| value(service_tier.to_string())),
|
||||
service_tier
|
||||
.as_ref()
|
||||
.map(|service_tier| value(service_tier.clone())),
|
||||
)),
|
||||
ConfigEdit::SetModelPersonality { personality } => Ok(self.write_profile_value(
|
||||
&["personality"],
|
||||
@@ -1114,7 +1115,7 @@ impl ConfigEditsBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_service_tier(mut self, service_tier: Option<ServiceTier>) -> Self {
|
||||
pub fn set_service_tier(mut self, service_tier: Option<String>) -> Self {
|
||||
self.edits.push(ConfigEdit::SetServiceTier { service_tier });
|
||||
self
|
||||
}
|
||||
|
||||
@@ -521,6 +521,10 @@ impl Session {
|
||||
&per_turn_config.agent_roles,
|
||||
));
|
||||
|
||||
let mut per_turn_config = per_turn_config;
|
||||
per_turn_config.service_tier = per_turn_config
|
||||
.service_tier
|
||||
.filter(|service_tier| model_info.supports_service_tier(service_tier));
|
||||
let per_turn_config = Arc::new(per_turn_config);
|
||||
let turn_metadata_state = Arc::new(TurnMetadataState::new(
|
||||
session_id.to_string(),
|
||||
|
||||
@@ -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(()));
|
||||
|
||||
Reference in New Issue
Block a user