add fast mode toggle (#13212)

- add a local Fast mode setting in codex-core (similar to how model id
is currently stored on disk locally)
- send `service_tier=priority` on requests when Fast is enabled
- add `/fast` in the TUI and persist it locally
- feature flag
This commit is contained in:
pash-openai
2026-03-02 20:29:33 -08:00
committed by GitHub
parent 56cc2c71f4
commit 2f5b01abd6
69 changed files with 929 additions and 127 deletions
+15
View File
@@ -63,6 +63,7 @@ use codex_otel::OtelManager;
use codex_protocol::ThreadId;
use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig;
use codex_protocol::config_types::ServiceTier;
use codex_protocol::config_types::Verbosity as VerbosityConfig;
use codex_protocol::models::ResponseItem;
use codex_protocol::openai_models::ModelInfo;
@@ -520,6 +521,7 @@ impl ModelClientSession {
model_info: &ModelInfo,
effort: Option<ReasoningEffortConfig>,
summary: ReasoningSummaryConfig,
service_tier: Option<ServiceTier>,
) -> Result<ResponsesApiRequest> {
let instructions = &prompt.base_instructions.text;
let input = prompt.get_formatted_input();
@@ -569,6 +571,10 @@ impl ModelClientSession {
store: provider.is_azure_responses_endpoint(),
stream: true,
include,
service_tier: match service_tier {
Some(ServiceTier::Fast) => Some("priority".to_string()),
_ => None,
},
prompt_cache_key,
text,
};
@@ -793,6 +799,7 @@ impl ModelClientSession {
otel_manager: &OtelManager,
effort: Option<ReasoningEffortConfig>,
summary: ReasoningSummaryConfig,
service_tier: Option<ServiceTier>,
turn_metadata_header: Option<&str>,
) -> Result<ResponseStream> {
if let Some(path) = &*CODEX_RS_SSE_FIXTURE {
@@ -823,6 +830,7 @@ impl ModelClientSession {
model_info,
effort,
summary,
service_tier,
)?;
let client = ApiResponsesClient::new(
transport,
@@ -858,6 +866,7 @@ impl ModelClientSession {
otel_manager: &OtelManager,
effort: Option<ReasoningEffortConfig>,
summary: ReasoningSummaryConfig,
service_tier: Option<ServiceTier>,
turn_metadata_header: Option<&str>,
warmup: bool,
) -> Result<WebsocketStreamOutcome> {
@@ -877,6 +886,7 @@ impl ModelClientSession {
model_info,
effort,
summary,
service_tier,
)?;
let mut ws_payload = ResponseCreateWsRequest {
client_metadata: build_ws_client_metadata(turn_metadata_header),
@@ -958,6 +968,7 @@ impl ModelClientSession {
otel_manager: &OtelManager,
effort: Option<ReasoningEffortConfig>,
summary: ReasoningSummaryConfig,
service_tier: Option<ServiceTier>,
turn_metadata_header: Option<&str>,
) -> Result<()> {
let Some(ws_version) = self.client.active_ws_version(model_info) else {
@@ -982,6 +993,7 @@ impl ModelClientSession {
otel_manager,
effort,
summary,
service_tier,
turn_metadata_header,
true,
)
@@ -1020,6 +1032,7 @@ impl ModelClientSession {
otel_manager: &OtelManager,
effort: Option<ReasoningEffortConfig>,
summary: ReasoningSummaryConfig,
service_tier: Option<ServiceTier>,
turn_metadata_header: Option<&str>,
) -> Result<ResponseStream> {
let wire_api = self.client.state.provider.wire_api;
@@ -1034,6 +1047,7 @@ impl ModelClientSession {
otel_manager,
effort,
summary,
service_tier,
turn_metadata_header,
false,
)
@@ -1052,6 +1066,7 @@ impl ModelClientSession {
otel_manager,
effort,
summary,
service_tier,
turn_metadata_header,
)
.await
+3
View File
@@ -255,6 +255,7 @@ mod tests {
stream: true,
include: vec![],
prompt_cache_key: None,
service_tier: None,
text: Some(TextControls {
verbosity: Some(OpenAiVerbosity::Low),
format: None,
@@ -296,6 +297,7 @@ mod tests {
stream: true,
include: vec![],
prompt_cache_key: None,
service_tier: None,
text: Some(text_controls),
};
@@ -332,6 +334,7 @@ mod tests {
stream: true,
include: vec![],
prompt_cache_key: None,
service_tier: None,
text: None,
};
+19
View File
@@ -290,6 +290,7 @@ use codex_otel::TelemetryAuthMode;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::Personality;
use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig;
use codex_protocol::config_types::ServiceTier;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::models::ContentItem;
use codex_protocol::models::DeveloperInstructions;
@@ -459,6 +460,7 @@ impl Codex {
provider: config.model_provider.clone(),
collaboration_mode,
model_reasoning_summary: config.model_reasoning_summary,
service_tier: config.service_tier,
developer_instructions: config.developer_instructions.clone(),
user_instructions,
personality: config.personality,
@@ -823,6 +825,7 @@ pub(crate) struct SessionConfiguration {
collaboration_mode: CollaborationMode,
model_reasoning_summary: Option<ReasoningSummaryConfig>,
service_tier: Option<ServiceTier>,
/// Developer instructions that supplement the base instructions.
developer_instructions: Option<String>,
@@ -897,6 +900,9 @@ impl SessionConfiguration {
if let Some(summary) = updates.reasoning_summary {
next_configuration.model_reasoning_summary = Some(summary);
}
if let Some(service_tier) = updates.service_tier {
next_configuration.service_tier = service_tier;
}
if let Some(personality) = updates.personality {
next_configuration.personality = Some(personality);
}
@@ -927,6 +933,7 @@ pub(crate) struct SessionSettingsUpdate {
pub(crate) windows_sandbox_level: Option<WindowsSandboxLevel>,
pub(crate) collaboration_mode: Option<CollaborationMode>,
pub(crate) reasoning_summary: Option<ReasoningSummaryConfig>,
pub(crate) service_tier: Option<Option<ServiceTier>>,
pub(crate) final_output_json_schema: Option<Option<Value>>,
pub(crate) personality: Option<Personality>,
pub(crate) app_server_client_name: Option<String>,
@@ -997,6 +1004,7 @@ impl Session {
per_turn_config.model_reasoning_effort =
session_configuration.collaboration_mode.reasoning_effort();
per_turn_config.model_reasoning_summary = session_configuration.model_reasoning_summary;
per_turn_config.service_tier = session_configuration.service_tier;
per_turn_config.personality = session_configuration.personality;
let resolved_web_search_mode = resolve_web_search_mode_for_turn(
&per_turn_config.web_search_mode,
@@ -3690,6 +3698,7 @@ async fn submission_loop(sess: Arc<Session>, config: Arc<Config>, rx_sub: Receiv
model,
effort,
summary,
service_tier,
collaboration_mode,
personality,
} => {
@@ -3713,6 +3722,7 @@ async fn submission_loop(sess: Arc<Session>, config: Arc<Config>, rx_sub: Receiv
windows_sandbox_level,
collaboration_mode: Some(collaboration_mode),
reasoning_summary: summary,
service_tier,
personality,
..Default::default()
},
@@ -3909,6 +3919,7 @@ mod handlers {
model,
effort,
summary,
service_tier,
final_output_json_schema,
items,
collaboration_mode,
@@ -3933,6 +3944,7 @@ mod handlers {
windows_sandbox_level: None,
collaboration_mode,
reasoning_summary: summary,
service_tier,
final_output_json_schema: Some(final_output_json_schema),
personality,
app_server_client_name: None,
@@ -6200,6 +6212,7 @@ async fn try_run_sampling_request(
&turn_context.otel_manager,
turn_context.reasoning_effort,
turn_context.reasoning_summary,
turn_context.config.service_tier,
turn_metadata_header,
)
.instrument(trace_span!("stream_request"))
@@ -7697,6 +7710,7 @@ mod tests {
model_reasoning_summary: config.model_reasoning_summary,
developer_instructions: config.developer_instructions.clone(),
user_instructions: config.user_instructions.clone(),
service_tier: None,
personality: config.personality,
base_instructions: config
.base_instructions
@@ -7791,6 +7805,7 @@ mod tests {
model_reasoning_summary: config.model_reasoning_summary,
developer_instructions: config.developer_instructions.clone(),
user_instructions: config.user_instructions.clone(),
service_tier: None,
personality: config.personality,
base_instructions: config
.base_instructions
@@ -8104,6 +8119,7 @@ mod tests {
model_reasoning_summary: config.model_reasoning_summary,
developer_instructions: config.developer_instructions.clone(),
user_instructions: config.user_instructions.clone(),
service_tier: None,
personality: config.personality,
base_instructions: config
.base_instructions
@@ -8159,6 +8175,7 @@ mod tests {
model_reasoning_summary: config.model_reasoning_summary,
developer_instructions: config.developer_instructions.clone(),
user_instructions: config.user_instructions.clone(),
service_tier: None,
personality: config.personality,
base_instructions: config
.base_instructions
@@ -8250,6 +8267,7 @@ mod tests {
model_reasoning_summary: config.model_reasoning_summary,
developer_instructions: config.developer_instructions.clone(),
user_instructions: config.user_instructions.clone(),
service_tier: None,
personality: config.personality,
base_instructions: config
.base_instructions
@@ -8418,6 +8436,7 @@ mod tests {
model_reasoning_summary: config.model_reasoning_summary,
developer_instructions: config.developer_instructions.clone(),
user_instructions: config.user_instructions.clone(),
service_tier: None,
personality: config.personality,
base_instructions: config
.base_instructions
+1
View File
@@ -402,6 +402,7 @@ async fn drain_to_completed(
&turn_context.otel_manager,
turn_context.reasoning_effort,
turn_context.reasoning_summary,
turn_context.config.service_tier,
turn_metadata_header,
)
.await?;
+12
View File
@@ -5,6 +5,7 @@ use crate::path_utils::write_atomically;
use anyhow::Context;
use codex_config::CONFIG_TOML_FILE;
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;
@@ -26,6 +27,8 @@ pub enum ConfigEdit {
model: Option<String>,
effort: Option<ReasoningEffort>,
},
/// Update the service tier preference for future turns.
SetServiceTier { service_tier: Option<ServiceTier> },
/// Update the active (or default) model personality.
SetModelPersonality { personality: Option<Personality> },
/// Toggle the acknowledgement flag under `[notice]`.
@@ -327,6 +330,10 @@ impl ConfigDocument {
);
mutated
}),
ConfigEdit::SetServiceTier { service_tier } => Ok(self.write_profile_value(
&["service_tier"],
service_tier.map(|service_tier| value(service_tier.to_string())),
)),
ConfigEdit::SetModelPersonality { personality } => Ok(self.write_profile_value(
&["personality"],
personality.map(|personality| value(personality.to_string())),
@@ -774,6 +781,11 @@ impl ConfigEditsBuilder {
self
}
pub fn set_service_tier(mut self, service_tier: Option<ServiceTier>) -> Self {
self.edits.push(ConfigEdit::SetServiceTier { service_tier });
self
}
pub fn set_personality(mut self, personality: Option<Personality>) -> Self {
self.edits
.push(ConfigEdit::SetModelPersonality { personality });
+47
View File
@@ -62,6 +62,7 @@ use codex_protocol::config_types::ForcedLoginMethod;
use codex_protocol::config_types::Personality;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::config_types::SandboxMode;
use codex_protocol::config_types::ServiceTier;
use codex_protocol::config_types::TrustLevel;
use codex_protocol::config_types::Verbosity;
use codex_protocol::config_types::WebSearchMode;
@@ -185,6 +186,9 @@ pub struct Config {
/// Optional override of model selection.
pub model: Option<String>,
/// Effective service tier preference for new turns.
pub service_tier: Option<ServiceTier>,
/// Model used specifically for review sessions.
pub review_model: Option<String>,
@@ -1184,6 +1188,9 @@ pub struct ConfigToml {
/// Optionally specify a personality for the model
pub personality: Option<Personality>,
/// Optional explicit service tier preference for new turns.
pub service_tier: Option<ServiceTier>,
/// Base URL for requests to ChatGPT (as opposed to the OpenAI API).
pub chatgpt_base_url: Option<String>,
@@ -1948,6 +1955,14 @@ impl Config {
let forced_login_method = cfg.forced_login_method;
let model = model.or(config_profile.model).or(cfg.model);
let service_tier = if features.enabled(Feature::FastMode) {
config_profile
.service_tier
.or(cfg.service_tier)
.filter(|tier| matches!(tier, ServiceTier::Fast))
} else {
None
};
let compact_prompt = compact_prompt.or(cfg.compact_prompt).and_then(|value| {
let trimmed = value.trim();
@@ -2094,6 +2109,7 @@ impl Config {
let config = Self {
model,
service_tier,
review_model,
model_context_window: cfg.model_context_window,
model_auto_compact_token_limit: cfg.model_auto_compact_token_limit,
@@ -4878,6 +4894,7 @@ model_verbosity = "high"
review_model: None,
model_context_window: None,
model_auto_compact_token_limit: None,
service_tier: None,
model_provider_id: "openai".to_string(),
model_provider: fixture.openai_provider.clone(),
permissions: Permissions {
@@ -5007,6 +5024,7 @@ model_verbosity = "high"
review_model: None,
model_context_window: None,
model_auto_compact_token_limit: None,
service_tier: None,
model_provider_id: "openai-custom".to_string(),
model_provider: fixture.openai_custom_provider.clone(),
permissions: Permissions {
@@ -5134,6 +5152,7 @@ model_verbosity = "high"
review_model: None,
model_context_window: None,
model_auto_compact_token_limit: None,
service_tier: None,
model_provider_id: "openai".to_string(),
model_provider: fixture.openai_provider.clone(),
permissions: Permissions {
@@ -5247,6 +5266,7 @@ model_verbosity = "high"
review_model: None,
model_context_window: None,
model_auto_compact_token_limit: None,
service_tier: None,
model_provider_id: "openai".to_string(),
model_provider: fixture.openai_provider.clone(),
permissions: Permissions {
@@ -5635,6 +5655,33 @@ trust_level = "untrusted"
Ok(())
}
#[test]
fn legacy_standard_service_tier_loads_as_default_none() -> anyhow::Result<()> {
let codex_home = TempDir::new()?;
let cfg = toml::from_str::<ConfigToml>(
r#"
service_tier = "standard"
[features]
fast_mode = true
"#,
)
.expect("TOML deserialization should succeed");
let config = Config::load_from_base_config_with_overrides(
cfg,
ConfigOverrides {
cwd: Some(codex_home.path().to_path_buf()),
..Default::default()
},
codex_home.path().to_path_buf(),
)?;
assert_eq!(config.service_tier, None);
Ok(())
}
#[test]
fn derive_sandbox_policy_falls_back_to_constraint_value_for_implicit_defaults()
-> anyhow::Result<()> {
+2
View File
@@ -8,6 +8,7 @@ use crate::config::types::WindowsToml;
use crate::protocol::AskForApproval;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::config_types::SandboxMode;
use codex_protocol::config_types::ServiceTier;
use codex_protocol::config_types::Verbosity;
use codex_protocol::config_types::WebSearchMode;
use codex_protocol::openai_models::ReasoningEffort;
@@ -18,6 +19,7 @@ use codex_protocol::openai_models::ReasoningEffort;
#[schemars(deny_unknown_fields)]
pub struct ConfigProfile {
pub model: Option<String>,
pub service_tier: Option<ServiceTier>,
/// The key in the `model_providers` map identifying the
/// [`ModelProviderInfo`] to use.
pub model_provider: Option<String>,
+1
View File
@@ -7,6 +7,7 @@ use crate::config_loader::RequirementSource;
pub use codex_protocol::config_types::AltScreenMode;
pub use codex_protocol::config_types::ModeKind;
pub use codex_protocol::config_types::Personality;
pub use codex_protocol::config_types::ServiceTier;
pub use codex_protocol::config_types::WebSearchMode;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::collections::BTreeMap;
+8
View File
@@ -145,6 +145,8 @@ pub enum Feature {
CollaborationModes,
/// Enable personality selection in the TUI.
Personality,
/// Enable Fast mode selection in the TUI and request layer.
FastMode,
/// Enable voice transcription in the TUI composer.
VoiceTranscription,
/// Enable experimental realtime voice conversation mode in the TUI.
@@ -660,6 +662,12 @@ pub const FEATURES: &[FeatureSpec] = &[
stage: Stage::Stable,
default_enabled: true,
},
FeatureSpec {
id: Feature::FastMode,
key: "fast_mode",
stage: Stage::UnderDevelopment,
default_enabled: false,
},
FeatureSpec {
id: Feature::VoiceTranscription,
key: "voice_transcription",
+4
View File
@@ -13,6 +13,7 @@ use crate::rollout::policy::should_persist_response_item_for_memories;
use codex_api::ResponseEvent;
use codex_otel::OtelManager;
use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig;
use codex_protocol::config_types::ServiceTier;
use codex_protocol::models::BaseInstructions;
use codex_protocol::models::ContentItem;
use codex_protocol::models::ResponseItem;
@@ -36,6 +37,7 @@ pub(in crate::memories) struct RequestContext {
pub(in crate::memories) otel_manager: OtelManager,
pub(in crate::memories) reasoning_effort: Option<ReasoningEffortConfig>,
pub(in crate::memories) reasoning_summary: ReasoningSummaryConfig,
pub(in crate::memories) service_tier: Option<ServiceTier>,
pub(in crate::memories) turn_metadata_header: Option<String>,
}
@@ -144,6 +146,7 @@ impl RequestContext {
otel_manager: turn_context.otel_manager.clone(),
reasoning_effort: Some(phase_one::REASONING_EFFORT),
reasoning_summary: turn_context.reasoning_summary,
service_tier: turn_context.config.service_tier,
}
}
}
@@ -322,6 +325,7 @@ mod job {
&stage_one_context.otel_manager,
stage_one_context.reasoning_effort,
stage_one_context.reasoning_summary,
stage_one_context.service_tier,
stage_one_context.turn_metadata_header.as_deref(),
)
.await?;
+1
View File
@@ -44,6 +44,7 @@ impl RegularTask {
&turn_context.otel_manager,
turn_context.reasoning_effort,
turn_context.reasoning_summary,
turn_context.config.service_tier,
turn_metadata_header.as_deref(),
)
.await?;