mirror of
https://github.com/openai/codex.git
synced 2026-11-05 16:29:31 +00: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:
1 parent
47f1d7b40b
commit
7c0e54bf59
24 files changed
+919
-412
No files matched your search
+13
-107
@@ -150,7 +150,6 @@ use codex_protocol::config_types::CollaborationMode;
|
||||
use codex_protocol::config_types::CollaborationModeMask;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::config_types::Personality;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::config_types::Settings;
|
||||
#[cfg(target_os = "windows")]
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
@@ -344,6 +343,7 @@ use self::plan_implementation::PLAN_IMPLEMENTATION_TITLE;
|
||||
mod realtime;
|
||||
use self::realtime::RealtimeConversationUiState;
|
||||
mod reasoning_shortcuts;
|
||||
mod service_tiers;
|
||||
mod side;
|
||||
mod status_surfaces;
|
||||
use self::status_surfaces::CachedProjectRootName;
|
||||
@@ -761,7 +761,7 @@ pub(crate) struct ChatWidget {
|
||||
config: Config,
|
||||
raw_output_mode: bool,
|
||||
/// Runtime value resolved by core. `config.service_tier` remains the explicit user choice.
|
||||
effective_service_tier: Option<ServiceTier>,
|
||||
effective_service_tier: Option<String>,
|
||||
/// The unmasked collaboration mode settings (always Default mode).
|
||||
///
|
||||
/// Masks are applied on top of this base mode to derive the effective mode.
|
||||
@@ -2072,10 +2072,7 @@ impl ChatWidget {
|
||||
self.current_rollout_path = session.rollout_path.clone();
|
||||
self.current_cwd = Some(session.cwd.to_path_buf());
|
||||
self.config.cwd = session.cwd.clone();
|
||||
self.effective_service_tier = session
|
||||
.service_tier
|
||||
.as_deref()
|
||||
.and_then(ServiceTier::from_request_value);
|
||||
self.effective_service_tier = session.service_tier.clone();
|
||||
if let Err(err) = self
|
||||
.config
|
||||
.permissions
|
||||
@@ -2116,15 +2113,15 @@ impl ChatWidget {
|
||||
}
|
||||
self.refresh_model_display();
|
||||
self.refresh_status_surfaces();
|
||||
self.sync_fast_command_enabled();
|
||||
self.sync_service_tier_commands();
|
||||
self.sync_personality_command_enabled();
|
||||
self.sync_plugins_command_enabled();
|
||||
self.sync_goal_command_enabled();
|
||||
self.refresh_plugin_mentions();
|
||||
if display == SessionConfiguredDisplay::Normal {
|
||||
let startup_tooltip_override = self.startup_tooltip_override.take();
|
||||
let show_fast_status =
|
||||
self.should_show_fast_status(&model_for_header, self.effective_service_tier);
|
||||
let show_fast_status = self
|
||||
.should_show_fast_status(&model_for_header, self.effective_service_tier.as_deref());
|
||||
let session_info_cell = history_cell::new_session_info(
|
||||
&self.config,
|
||||
&model_for_header,
|
||||
@@ -4893,10 +4890,7 @@ impl ChatWidget {
|
||||
let active_cell = Some(Self::placeholder_session_header_cell(&config));
|
||||
|
||||
let current_cwd = Some(config.cwd.to_path_buf());
|
||||
let effective_service_tier = config
|
||||
.service_tier
|
||||
.as_deref()
|
||||
.and_then(ServiceTier::from_request_value);
|
||||
let effective_service_tier = config.service_tier.clone();
|
||||
let current_terminal_info = terminal_info();
|
||||
let runtime_keymap = RuntimeKeymap::from_config(&config.tui_keymap).ok();
|
||||
let default_keymap = RuntimeKeymap::defaults();
|
||||
@@ -5088,7 +5082,7 @@ impl ChatWidget {
|
||||
widget
|
||||
.bottom_pane
|
||||
.set_collaboration_modes_enabled(/*enabled*/ true);
|
||||
widget.sync_fast_command_enabled();
|
||||
widget.sync_service_tier_commands();
|
||||
widget.sync_personality_command_enabled();
|
||||
widget.sync_plugins_command_enabled();
|
||||
widget.sync_goal_command_enabled();
|
||||
@@ -5328,6 +5322,9 @@ impl ChatWidget {
|
||||
InputResult::Command(cmd) => {
|
||||
self.handle_slash_command_dispatch(cmd);
|
||||
}
|
||||
InputResult::ServiceTierCommand(command) => {
|
||||
self.handle_service_tier_command_dispatch(command);
|
||||
}
|
||||
InputResult::CommandWithArgs(cmd, args, text_elements) => {
|
||||
self.handle_slash_command_with_args_dispatch(cmd, args, text_elements);
|
||||
}
|
||||
@@ -9193,7 +9190,7 @@ impl ChatWidget {
|
||||
}
|
||||
}
|
||||
if feature == Feature::FastMode {
|
||||
self.sync_fast_command_enabled();
|
||||
self.sync_service_tier_commands();
|
||||
}
|
||||
if feature == Feature::Personality {
|
||||
self.sync_personality_command_enabled();
|
||||
@@ -9309,28 +9306,6 @@ impl ChatWidget {
|
||||
self.config.personality = Some(personality);
|
||||
}
|
||||
|
||||
/// Set Fast mode in the widget's config copy.
|
||||
pub(crate) fn set_service_tier(&mut self, service_tier: Option<ServiceTier>) {
|
||||
self.config.service_tier =
|
||||
service_tier.map(|service_tier| service_tier.request_value().to_string());
|
||||
self.effective_service_tier = service_tier;
|
||||
}
|
||||
|
||||
pub(crate) fn current_service_tier(&self) -> Option<ServiceTier> {
|
||||
self.effective_service_tier
|
||||
}
|
||||
|
||||
pub(crate) fn configured_service_tier(&self) -> Option<ServiceTier> {
|
||||
self.config
|
||||
.service_tier
|
||||
.as_deref()
|
||||
.and_then(ServiceTier::from_request_value)
|
||||
}
|
||||
|
||||
pub(crate) fn fast_default_opt_out(&self) -> Option<bool> {
|
||||
self.config.notices.fast_default_opt_out
|
||||
}
|
||||
|
||||
pub(crate) fn status_account_display(&self) -> Option<&StatusAccountDisplay> {
|
||||
self.status_account_display.as_ref()
|
||||
}
|
||||
@@ -9365,26 +9340,6 @@ impl ChatWidget {
|
||||
.set_connectors_enabled(self.connectors_enabled());
|
||||
}
|
||||
|
||||
pub(crate) fn should_show_fast_status(
|
||||
&self,
|
||||
model: &str,
|
||||
service_tier: Option<ServiceTier>,
|
||||
) -> bool {
|
||||
self.model_supports_fast_mode(model)
|
||||
&& matches!(service_tier, Some(ServiceTier::Fast))
|
||||
&& self.has_chatgpt_account
|
||||
}
|
||||
|
||||
fn fast_mode_enabled(&self) -> bool {
|
||||
self.config.features.enabled(Feature::FastMode)
|
||||
}
|
||||
|
||||
pub(crate) fn can_toggle_fast_mode_from_keybinding(&self) -> bool {
|
||||
self.fast_mode_enabled()
|
||||
&& !self.is_user_turn_pending_or_running()
|
||||
&& self.bottom_pane.no_modal_or_popup_active()
|
||||
}
|
||||
|
||||
pub(crate) fn set_realtime_audio_device(
|
||||
&mut self,
|
||||
kind: RealtimeAudioDeviceKind,
|
||||
@@ -9416,38 +9371,6 @@ impl ChatWidget {
|
||||
self.refresh_model_dependent_surfaces();
|
||||
}
|
||||
|
||||
fn set_service_tier_selection(&mut self, service_tier: Option<ServiceTier>) {
|
||||
if service_tier.is_none() {
|
||||
self.config.notices.fast_default_opt_out = Some(true);
|
||||
}
|
||||
self.set_service_tier(service_tier);
|
||||
self.app_event_tx
|
||||
.send(AppEvent::CodexOp(AppCommand::override_turn_context(
|
||||
/*cwd*/ None,
|
||||
/*approval_policy*/ None,
|
||||
/*approvals_reviewer*/ None,
|
||||
/*permission_profile*/ None,
|
||||
/*windows_sandbox_level*/ None,
|
||||
/*model*/ None,
|
||||
/*effort*/ None,
|
||||
/*summary*/ None,
|
||||
Some(service_tier.map(|service_tier| service_tier.request_value().to_string())),
|
||||
/*collaboration_mode*/ None,
|
||||
/*personality*/ None,
|
||||
)));
|
||||
self.app_event_tx
|
||||
.send(AppEvent::PersistServiceTierSelection { service_tier });
|
||||
}
|
||||
|
||||
pub(crate) fn toggle_fast_mode_from_ui(&mut self) {
|
||||
let next_tier = if matches!(self.current_service_tier(), Some(ServiceTier::Fast)) {
|
||||
None
|
||||
} else {
|
||||
Some(ServiceTier::Fast)
|
||||
};
|
||||
self.set_service_tier_selection(next_tier);
|
||||
}
|
||||
|
||||
pub(crate) fn current_model(&self) -> &str {
|
||||
if !self.collaboration_modes_enabled() {
|
||||
return self.current_collaboration_mode.model();
|
||||
@@ -9474,11 +9397,6 @@ impl ChatWidget {
|
||||
.unwrap_or_else(|| "System default".to_string())
|
||||
}
|
||||
|
||||
fn sync_fast_command_enabled(&mut self) {
|
||||
self.bottom_pane
|
||||
.set_fast_command_enabled(self.fast_mode_enabled());
|
||||
}
|
||||
|
||||
fn sync_personality_command_enabled(&mut self) {
|
||||
self.bottom_pane
|
||||
.set_personality_command_enabled(self.config.features.enabled(Feature::Personality));
|
||||
@@ -9508,19 +9426,6 @@ impl ChatWidget {
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn model_supports_fast_mode(&self, model: &str) -> bool {
|
||||
self.model_catalog
|
||||
.try_list_models()
|
||||
.ok()
|
||||
.and_then(|models| {
|
||||
models
|
||||
.into_iter()
|
||||
.find(|preset| preset.model == model)
|
||||
.map(|preset| preset.supports_fast_mode())
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Return whether the effective model currently advertises image-input support.
|
||||
///
|
||||
/// We intentionally default to `true` when model metadata cannot be read so transient catalog
|
||||
@@ -9660,6 +9565,7 @@ impl ChatWidget {
|
||||
self.session_header.set_model(effective.model());
|
||||
// Keep composer paste affordances aligned with the currently effective model.
|
||||
self.sync_image_paste_enabled();
|
||||
self.sync_service_tier_commands();
|
||||
self.refresh_terminal_title();
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user