From 02170996e63aed49dfd95835be8c417a511df869 Mon Sep 17 00:00:00 2001 From: Shijie Rao Date: Thu, 23 Apr 2026 00:54:44 -0400 Subject: [PATCH] Default Fast service tier for eligible ChatGPT plans (#19053) ## Why Enterprise and business-like ChatGPT plans should get Codex's Fast service tier by default when the user or caller has not made an explicit service-tier choice. At the same time, callers need a durable way to choose standard routing without adding a new persisted `standard` service tier value. This keeps existing config compatibility while letting core own the managed default policy. ## What changed - Resolve the effective service tier in core at session creation: explicit `fast` or `flex` wins, explicit null/clear or `[notice].fast_default_opt_out = true` resolves to standard routing, and otherwise eligible ChatGPT plans resolve to Fast when FastMode is enabled. - Add `[notice].fast_default_opt_out` as the persisted opt-out marker for managed Fast defaults. - Treat app-server/TUI `service_tier: null` as an explicit standard/clear choice by preserving that intent through config loading. - Update TUI rendering to use core's effective service tier for startup and status surfaces while still keeping `config.service_tier` as the explicit configured choice. - Update `/fast off` to clear `service_tier`, persist the opt-out marker, and send explicit standard for subsequent turns. ## Verification - Added unit coverage for config override/notice handling, service-tier resolution, runtime null clearing, and `/fast off` turn propagation. - `cargo build -p codex-cli` Full test suite was not run locally per author request. --- codex-rs/config/src/profile_toml.rs | 1 + codex-rs/config/src/types.rs | 2 + codex-rs/core/config.schema.json | 11 +- codex-rs/core/src/config/config_tests.rs | 45 ++++++++ codex-rs/core/src/config/edit.rs | 13 +++ codex-rs/core/src/config/mod.rs | 18 +++- codex-rs/core/src/session/mod.rs | 33 +++++- codex-rs/core/src/session/tests.rs | 100 ++++++++++++++++++ codex-rs/tui/src/app/event_dispatch.rs | 22 ++-- codex-rs/tui/src/app/session_lifecycle.rs | 3 +- codex-rs/tui/src/chatwidget.rs | 23 +++- codex-rs/tui/src/chatwidget/slash_dispatch.rs | 14 +-- .../tui/src/chatwidget/status_surfaces.rs | 4 +- codex-rs/tui/src/chatwidget/tests/helpers.rs | 2 + .../src/chatwidget/tests/slash_commands.rs | 23 +++- codex-rs/tui/src/history_cell.rs | 8 +- 16 files changed, 289 insertions(+), 33 deletions(-) diff --git a/codex-rs/config/src/profile_toml.rs b/codex-rs/config/src/profile_toml.rs index 69215c044..642770ff7 100644 --- a/codex-rs/config/src/profile_toml.rs +++ b/codex-rs/config/src/profile_toml.rs @@ -23,6 +23,7 @@ use codex_protocol::protocol::AskForApproval; #[schemars(deny_unknown_fields)] pub struct ConfigProfile { pub model: Option, + /// Optional explicit service tier preference for new turns (`fast` or `flex`). pub service_tier: Option, /// The key in the `model_providers` map identifying the /// [`ModelProviderInfo`] to use. diff --git a/codex-rs/config/src/types.rs b/codex-rs/config/src/types.rs index 50a24db53..7413686a7 100644 --- a/codex-rs/config/src/types.rs +++ b/codex-rs/config/src/types.rs @@ -615,6 +615,8 @@ pub struct Notice { pub hide_full_access_warning: Option, /// Tracks whether the user has acknowledged the Windows world-writable directories warning. pub hide_world_writable_warning: Option, + /// Tracks whether the user opted out of Codex-managed fast defaults. + pub fast_default_opt_out: Option, /// Tracks whether the user opted out of the rate limit model switch reminder. pub hide_rate_limit_model_nudge: Option, /// Tracks whether the user has seen the model migration prompt diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index a3cf84af4..64aa3e5f1 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -631,7 +631,12 @@ "$ref": "#/definitions/SandboxMode" }, "service_tier": { - "$ref": "#/definitions/ServiceTier" + "allOf": [ + { + "$ref": "#/definitions/ServiceTier" + } + ], + "description": "Optional explicit service tier preference for new turns (`fast` or `flex`)." }, "tools": { "$ref": "#/definitions/ToolsToml" @@ -1397,6 +1402,10 @@ }, "description": "Tracks scopes where external config migration prompts should be suppressed." }, + "fast_default_opt_out": { + "description": "Tracks whether the user opted out of Codex-managed fast defaults.", + "type": "boolean" + }, "hide_full_access_warning": { "description": "Tracks whether the user has acknowledged the full access warning prompt.", "type": "boolean" diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index deaf4b2db..5450c5266 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -39,6 +39,7 @@ use codex_config::types::McpServerTransportConfig; use codex_config::types::MemoriesConfig; use codex_config::types::MemoriesToml; use codex_config::types::ModelAvailabilityNuxConfig; +use codex_config::types::Notice; use codex_config::types::NotificationCondition; use codex_config::types::NotificationMethod; use codex_config::types::Notifications; @@ -5298,6 +5299,50 @@ async fn metrics_exporter_defaults_to_statsig_when_missing() -> std::io::Result< Ok(()) } +#[tokio::test] +async fn explicit_null_service_tier_override_sets_fast_default_opt_out() -> std::io::Result<()> { + let fixture = create_test_fixture()?; + + let config = Config::load_from_base_config_with_overrides( + fixture.cfg.clone(), + ConfigOverrides { + cwd: Some(fixture.cwd_path()), + service_tier: Some(None), + ..Default::default() + }, + fixture.codex_home(), + ) + .await?; + + assert_eq!(config.service_tier, None); + assert_eq!(config.notices.fast_default_opt_out, Some(true)); + Ok(()) +} + +#[tokio::test] +async fn fast_default_opt_out_notice_config_is_respected() -> std::io::Result<()> { + let fixture = create_test_fixture()?; + let mut cfg = fixture.cfg.clone(); + cfg.notice = Some(Notice { + fast_default_opt_out: Some(true), + ..Default::default() + }); + + let config = Config::load_from_base_config_with_overrides( + cfg, + ConfigOverrides { + cwd: Some(fixture.cwd_path()), + ..Default::default() + }, + fixture.codex_home(), + ) + .await?; + + assert_eq!(config.service_tier, None); + assert_eq!(config.notices.fast_default_opt_out, Some(true)); + Ok(()) +} + #[tokio::test] async fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> { let fixture = create_test_fixture()?; diff --git a/codex-rs/core/src/config/edit.rs b/codex-rs/core/src/config/edit.rs index e7cf3651c..e49dc9dc0 100644 --- a/codex-rs/core/src/config/edit.rs +++ b/codex-rs/core/src/config/edit.rs @@ -37,6 +37,8 @@ pub enum ConfigEdit { SetNoticeHideFullAccessWarning(bool), /// Toggle the Windows world-writable directories warning acknowledgement flag. SetNoticeHideWorldWritableWarning(bool), + /// Toggle the opt-out marker for Codex-managed fast defaults. + SetNoticeFastDefaultOptOut(bool), /// Toggle the rate limit model nudge acknowledgement flag. SetNoticeHideRateLimitModelNudge(bool), /// Toggle the Windows onboarding acknowledgement flag. @@ -436,6 +438,11 @@ impl ConfigDocument { &[NOTICE_TABLE_KEY, "hide_world_writable_warning"], value(*acknowledged), )), + ConfigEdit::SetNoticeFastDefaultOptOut(opted_out) => Ok(self.write_value( + Scope::Global, + &[NOTICE_TABLE_KEY, "fast_default_opt_out"], + value(*opted_out), + )), ConfigEdit::SetNoticeHideRateLimitModelNudge(acknowledged) => Ok(self.write_value( Scope::Global, &[NOTICE_TABLE_KEY, "hide_rate_limit_model_nudge"], @@ -978,6 +985,12 @@ impl ConfigEditsBuilder { self } + pub fn set_fast_default_opt_out(mut self, opted_out: bool) -> Self { + self.edits + .push(ConfigEdit::SetNoticeFastDefaultOptOut(opted_out)); + self + } + pub fn set_hide_rate_limit_model_nudge(mut self, acknowledged: bool) -> Self { self.edits .push(ConfigEdit::SetNoticeHideRateLimitModelNudge(acknowledged)); diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 771c1a486..8d60307ec 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -2031,14 +2031,24 @@ impl Config { let forced_login_method = cfg.forced_login_method; let model = model.or(config_profile.model).or(cfg.model); - let service_tier = service_tier_override - .unwrap_or_else(|| config_profile.service_tier.or(cfg.service_tier)); + let mut notices = cfg.notice.unwrap_or_default(); + let service_tier = match service_tier_override { + Some(Some(service_tier)) => Some(service_tier), + Some(None) => { + // Preserve explicit standard/clear intent after the nested override + // collapses into `Config.service_tier = None`. + notices.fast_default_opt_out = Some(true); + None + } + None => config_profile.service_tier.or(cfg.service_tier), + }; let service_tier = match service_tier { Some(ServiceTier::Fast) if features.enabled(Feature::FastMode) => { Some(ServiceTier::Fast) } + Some(ServiceTier::Fast) => None, Some(ServiceTier::Flex) => Some(ServiceTier::Flex), - _ => None, + None => None, }; let compact_prompt = compact_prompt.or(cfg.compact_prompt).and_then(|value| { @@ -2414,7 +2424,7 @@ impl Config { active_profile: active_profile_name, active_project, windows_wsl_setup_acknowledged: cfg.windows_wsl_setup_acknowledged.unwrap_or(false), - notices: cfg.notice.unwrap_or_default(), + notices, check_for_update_on_startup, disable_paste_burst: cfg.disable_paste_burst.unwrap_or(false), analytics_enabled: config_profile diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index 902fc205d..360e5cef7 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -77,6 +77,7 @@ use codex_otel::current_span_w3c_trace_context; use codex_otel::set_parent_from_w3c_trace_context; use codex_protocol::ThreadId; use codex_protocol::ToolName; +use codex_protocol::account::PlanType as AccountPlanType; use codex_protocol::approvals::ElicitationRequestEvent; use codex_protocol::approvals::ExecPolicyAmendment; use codex_protocol::approvals::NetworkPolicyAmendment; @@ -600,11 +601,20 @@ impl Codex { developer_instructions: None, }, }; + let account_plan_type = auth_manager + .auth_cached() + .and_then(|auth| auth.account_plan_type()); + let service_tier = get_service_tier( + config.service_tier, + config.notices.fast_default_opt_out.unwrap_or(false), + account_plan_type, + config.features.enabled(Feature::FastMode), + ); let session_configuration = SessionConfiguration { provider: config.model_provider.clone(), collaboration_mode, model_reasoning_summary: config.model_reasoning_summary, - service_tier: config.service_tier, + service_tier, developer_instructions: config.developer_instructions.clone(), user_instructions, personality: config.personality, @@ -785,6 +795,27 @@ impl Codex { } } +fn get_service_tier( + configured_service_tier: Option, + fast_default_opt_out: bool, + account_plan_type: Option, + fast_mode_enabled: bool, +) -> Option { + if configured_service_tier.is_some() || fast_default_opt_out || !fast_mode_enabled { + return configured_service_tier; + } + + account_plan_type + .is_some_and(is_enterprise_default_service_tier_plan) + .then_some(ServiceTier::Fast) +} + +fn is_enterprise_default_service_tier_plan(plan_type: AccountPlanType) -> bool { + plan_type == AccountPlanType::Enterprise + || plan_type.is_business_like() + || plan_type.is_team_like() +} + #[cfg(test)] pub(crate) fn completed_session_loop_termination() -> SessionLoopTermination { futures::future::ready(()).boxed().shared() diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 9b1ddd6c7..4e47596b8 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -26,6 +26,8 @@ use codex_models_manager::bundled_models_response; use codex_models_manager::model_info; use codex_protocol::AgentPath; use codex_protocol::ThreadId; +use codex_protocol::account::PlanType as AccountPlanType; +use codex_protocol::config_types::ServiceTier; use codex_protocol::config_types::TrustLevel; use codex_protocol::exec_output::ExecToolCallOutput; use codex_protocol::models::FileSystemPermissions; @@ -2643,6 +2645,104 @@ fn session_telemetry( ) } +#[test] +fn get_service_tier_defaults_enterprise_accounts_to_fast() { + assert_eq!( + get_service_tier( + /*configured_service_tier*/ None, + /*fast_default_opt_out*/ false, + Some(AccountPlanType::Enterprise), + /*fast_mode_enabled*/ true, + ), + Some(ServiceTier::Fast) + ); + assert_eq!( + get_service_tier( + /*configured_service_tier*/ None, + /*fast_default_opt_out*/ false, + Some(AccountPlanType::EnterpriseCbpUsageBased), + /*fast_mode_enabled*/ true, + ), + Some(ServiceTier::Fast) + ); + assert_eq!( + get_service_tier( + /*configured_service_tier*/ None, + /*fast_default_opt_out*/ false, + Some(AccountPlanType::Business), + /*fast_mode_enabled*/ true, + ), + Some(ServiceTier::Fast) + ); + assert_eq!( + get_service_tier( + /*configured_service_tier*/ None, + /*fast_default_opt_out*/ false, + Some(AccountPlanType::Team), + /*fast_mode_enabled*/ true, + ), + Some(ServiceTier::Fast) + ); + assert_eq!( + get_service_tier( + /*configured_service_tier*/ None, + /*fast_default_opt_out*/ false, + Some(AccountPlanType::SelfServeBusinessUsageBased), + /*fast_mode_enabled*/ true, + ), + Some(ServiceTier::Fast) + ); +} + +#[test] +fn get_service_tier_respects_fast_default_opt_out() { + assert_eq!( + get_service_tier( + /*configured_service_tier*/ None, + /*fast_default_opt_out*/ true, + Some(AccountPlanType::Enterprise), + /*fast_mode_enabled*/ true, + ), + None + ); +} + +#[test] +fn get_service_tier_does_not_default_non_enterprise_or_disabled_fast_mode() { + assert_eq!( + get_service_tier( + /*configured_service_tier*/ None, + /*fast_default_opt_out*/ false, + Some(AccountPlanType::Pro), + /*fast_mode_enabled*/ true, + ), + None + ); + assert_eq!( + get_service_tier( + /*configured_service_tier*/ None, + /*fast_default_opt_out*/ false, + Some(AccountPlanType::Enterprise), + /*fast_mode_enabled*/ false, + ), + None + ); +} + +#[tokio::test] +async fn session_settings_null_service_tier_update_clears_service_tier() { + let session_configuration = make_session_configuration_for_tests().await; + + let updated = session_configuration + .apply(&SessionSettingsUpdate { + service_tier: Some(None), + ..Default::default() + }) + .expect("null service tier update should apply"); + + assert_eq!(updated.service_tier, None); +} + pub(crate) async fn make_session_configuration_for_tests() -> SessionConfiguration { let codex_home = tempfile::tempdir().expect("create temp dir"); let config = build_test_config(codex_home.path()).await; diff --git a/codex-rs/tui/src/app/event_dispatch.rs b/codex-rs/tui/src/app/event_dispatch.rs index d4bfa45e2..40f440814 100644 --- a/codex-rs/tui/src/app/event_dispatch.rs +++ b/codex-rs/tui/src/app/event_dispatch.rs @@ -1029,14 +1029,24 @@ impl App { AppEvent::PersistServiceTierSelection { service_tier } => { self.refresh_status_line(); let profile = self.active_profile.as_deref(); - match ConfigEditsBuilder::new(&self.config.codex_home) + self.config.service_tier = service_tier; + let mut edits = ConfigEditsBuilder::new(&self.config.codex_home) .with_profile(profile) - .set_service_tier(service_tier) - .apply() - .await - { + .set_service_tier(service_tier); + if service_tier.is_none() { + self.config.notices.fast_default_opt_out = Some(true); + edits = edits.set_fast_default_opt_out(/*opted_out*/ true); + } + match edits.apply().await { Ok(()) => { - let status = if service_tier.is_some() { "on" } else { "off" }; + let status = if matches!( + service_tier, + Some(codex_protocol::config_types::ServiceTier::Fast) + ) { + "on" + } else { + "off" + }; let mut message = format!("Fast mode set to {status}"); if let Some(profile) = profile { message.push_str(" for "); diff --git a/codex-rs/tui/src/app/session_lifecycle.rs b/codex-rs/tui/src/app/session_lifecycle.rs index 89d5e1d43..dddae35e0 100644 --- a/codex-rs/tui/src/app/session_lifecycle.rs +++ b/codex-rs/tui/src/app/session_lifecycle.rs @@ -622,7 +622,8 @@ impl App { pub(super) fn fresh_session_config(&self) -> Config { let mut config = self.config.clone(); - config.service_tier = self.chat_widget.current_service_tier(); + config.service_tier = self.chat_widget.configured_service_tier(); + config.notices.fast_default_opt_out = self.chat_widget.fast_default_opt_out(); config } pub(super) async fn resume_target_session( diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 412455cff..b1141e3c2 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -776,6 +776,8 @@ pub(crate) struct ChatWidget { /// where the overlay may briefly treat new tail content as already cached. active_cell_revision: u64, config: Config, + /// Runtime value resolved by core. `config.service_tier` remains the explicit user choice. + effective_service_tier: Option, /// The unmasked collaboration mode settings (always Default mode). /// /// Masks are applied on top of this base mode to derive the effective mode. @@ -2120,6 +2122,7 @@ impl ChatWidget { self.current_rollout_path = event.rollout_path.clone(); self.current_cwd = Some(event.cwd.to_path_buf()); self.config.cwd = event.cwd.clone(); + self.effective_service_tier = event.service_tier; if let Err(err) = self .config .permissions @@ -5106,6 +5109,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; let queued_message_edit_binding = queued_message_edit_binding_for_terminal(terminal_info()); let mut widget = Self { app_event_tx: app_event_tx.clone(), @@ -5124,6 +5128,7 @@ impl ChatWidget { active_cell, active_cell_revision: 0, config, + effective_service_tier, skills_all: Vec::new(), skills_initial_state: None, current_collaboration_mode, @@ -5942,7 +5947,11 @@ impl ChatWidget { .personality .filter(|_| self.config.features.enabled(Feature::Personality)) .filter(|_| self.current_model_supports_personality()); - let service_tier = Some(self.config.service_tier); + let service_tier = match self.config.service_tier { + Some(service_tier) => Some(Some(service_tier)), + None if self.config.notices.fast_default_opt_out == Some(true) => Some(None), + None => None, + }; let op = AppCommand::user_turn( items, self.config.cwd.to_path_buf(), @@ -9850,12 +9859,21 @@ impl ChatWidget { /// Set Fast mode in the widget's config copy. pub(crate) fn set_service_tier(&mut self, service_tier: Option) { self.config.service_tier = service_tier; + self.effective_service_tier = service_tier; } pub(crate) fn current_service_tier(&self) -> Option { + self.effective_service_tier + } + + pub(crate) fn configured_service_tier(&self) -> Option { self.config.service_tier } + pub(crate) fn fast_default_opt_out(&self) -> Option { + self.config.notices.fast_default_opt_out + } + pub(crate) fn status_account_display(&self) -> Option<&StatusAccountDisplay> { self.status_account_display.as_ref() } @@ -9932,6 +9950,9 @@ impl ChatWidget { } fn set_service_tier_selection(&mut self, service_tier: Option) { + 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( diff --git a/codex-rs/tui/src/chatwidget/slash_dispatch.rs b/codex-rs/tui/src/chatwidget/slash_dispatch.rs index 6e041b833..febd8aef4 100644 --- a/codex-rs/tui/src/chatwidget/slash_dispatch.rs +++ b/codex-rs/tui/src/chatwidget/slash_dispatch.rs @@ -172,7 +172,7 @@ impl ChatWidget { self.open_model_popup(); } SlashCommand::Fast => { - let next_tier = if matches!(self.config.service_tier, Some(ServiceTier::Fast)) { + let next_tier = if matches!(self.current_service_tier(), Some(ServiceTier::Fast)) { None } else { Some(ServiceTier::Fast) @@ -527,12 +527,12 @@ impl ChatWidget { "on" => self.set_service_tier_selection(Some(ServiceTier::Fast)), "off" => self.set_service_tier_selection(/*service_tier*/ None), "status" => { - let status = if matches!(self.config.service_tier, Some(ServiceTier::Fast)) - { - "on" - } else { - "off" - }; + let status = + if matches!(self.current_service_tier(), Some(ServiceTier::Fast)) { + "on" + } else { + "off" + }; self.add_info_message( format!("Fast mode is {status}."), /*hint*/ None, diff --git a/codex-rs/tui/src/chatwidget/status_surfaces.rs b/codex-rs/tui/src/chatwidget/status_surfaces.rs index a6a10495d..94ed21600 100644 --- a/codex-rs/tui/src/chatwidget/status_surfaces.rs +++ b/codex-rs/tui/src/chatwidget/status_surfaces.rs @@ -482,7 +482,7 @@ impl ChatWidget { )), StatusLineItem::SessionId => self.thread_id.map(|id| id.to_string()), StatusLineItem::FastMode => Some( - if matches!(self.config.service_tier, Some(ServiceTier::Fast)) { + if matches!(self.current_service_tier(), Some(ServiceTier::Fast)) { "Fast on".to_string() } else { "Fast off".to_string() @@ -603,7 +603,7 @@ impl ChatWidget { fn model_with_reasoning_display_name(&self) -> String { let label = Self::status_line_reasoning_effort_label(self.effective_reasoning_effort()); let fast_label = - if self.should_show_fast_status(self.current_model(), self.config.service_tier) { + if self.should_show_fast_status(self.current_model(), self.current_service_tier()) { " fast" } else { "" diff --git a/codex-rs/tui/src/chatwidget/tests/helpers.rs b/codex-rs/tui/src/chatwidget/tests/helpers.rs index 7b2f46aec..960ab992a 100644 --- a/codex-rs/tui/src/chatwidget/tests/helpers.rs +++ b/codex-rs/tui/src/chatwidget/tests/helpers.rs @@ -181,6 +181,7 @@ pub(super) async fn make_chatwidget_manual( }; let current_collaboration_mode = base_mode; let active_collaboration_mask = collaboration_modes::default_mask(model_catalog.as_ref()); + let effective_service_tier = cfg.service_tier; let mut widget = ChatWidget { app_event_tx, codex_op_target: super::CodexOpTarget::Direct(op_tx), @@ -188,6 +189,7 @@ pub(super) async fn make_chatwidget_manual( active_cell: None, active_cell_revision: 0, config: cfg, + effective_service_tier, current_collaboration_mode, active_collaboration_mask, has_chatgpt_account: false, diff --git a/codex-rs/tui/src/chatwidget/tests/slash_commands.rs b/codex-rs/tui/src/chatwidget/tests/slash_commands.rs index 457a0859e..85b868ce7 100644 --- a/codex-rs/tui/src/chatwidget/tests/slash_commands.rs +++ b/codex-rs/tui/src/chatwidget/tests/slash_commands.rs @@ -1550,7 +1550,7 @@ async fn queued_fast_slash_applies_before_next_queued_message() { } #[tokio::test] -async fn user_turn_clears_service_tier_after_fast_is_turned_off() { +async fn user_turn_sends_standard_override_after_fast_is_turned_off() { let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(Some("gpt-5.3-codex")).await; chat.thread_id = Some(ThreadId::new()); set_chatgpt_auth(&mut chat); @@ -1560,7 +1560,24 @@ async fn user_turn_clears_service_tier_after_fast_is_turned_off() { let _events = std::iter::from_fn(|| rx.try_recv().ok()).collect::>(); chat.dispatch_command_with_args(SlashCommand::Fast, "off".to_string(), Vec::new()); - let _events = std::iter::from_fn(|| rx.try_recv().ok()).collect::>(); + let events = std::iter::from_fn(|| rx.try_recv().ok()).collect::>(); + assert!( + events.iter().any(|event| matches!( + event, + AppEvent::CodexOp(Op::OverrideTurnContext { + service_tier: Some(None), + .. + }) + )), + "expected fast-mode off override app event; events: {events:?}" + ); + assert!( + events.iter().any(|event| matches!( + event, + AppEvent::PersistServiceTierSelection { service_tier: None } + )), + "expected fast-mode opt-out persistence app event; events: {events:?}" + ); chat.bottom_pane .set_composer_text("hello".to_string(), Vec::new(), Vec::new()); @@ -1571,7 +1588,7 @@ async fn user_turn_clears_service_tier_after_fast_is_turned_off() { service_tier: Some(None), .. } => {} - other => panic!("expected Op::UserTurn to clear service tier, got {other:?}"), + other => panic!("expected Op::UserTurn with standard service tier override, got {other:?}"), } } diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index 33a05776b..7fa32003e 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -50,7 +50,6 @@ use codex_config::types::McpServerTransportConfig; use codex_mcp::qualified_mcp_tool_name_prefix; use codex_otel::RuntimeMetricsSummary; use codex_protocol::account::PlanType; -use codex_protocol::config_types::ServiceTier; #[cfg(test)] use codex_protocol::mcp::Resource; #[cfg(test)] @@ -1241,12 +1240,7 @@ pub(crate) fn new_session_info( } else { if config.show_tooltips && let Some(tooltips) = tooltip_override - .or_else(|| { - tooltips::get_tooltip( - auth_plan, - matches!(config.service_tier, Some(ServiceTier::Fast)), - ) - }) + .or_else(|| tooltips::get_tooltip(auth_plan, show_fast_status)) .map(|tip| TooltipHistoryCell::new(tip, &config.cwd)) { parts.push(Box::new(tooltips));