Enforce Fast and Ultra Fast policies independently (#51253)

## Why

Fast mode previously gated both Fast and Ultra Fast, preventing policies from allowing one while disabling the other.

## What changed

- Add the default-enabled `features.ultrafast_mode` flag and enforce it independently of `features.fast_mode` in service-tier selection, requests, and agent prewarming.
- Filter TUI service-tier commands and selections by the corresponding policy and model catalog support.
- Report `supportsIndependentSpeedModes: true` from `configRequirements/read` and update the protocol schemas and generated bindings. Retain the shared Fast gate when connecting to older servers that omit this capability.
- Preserve legacy cloud speed denials by translating `rbac-fast-mode` fragments with `fast_mode = false` into a denial of both modes, while retaining higher-priority enterprise overrides.

## Testing

Add coverage for all four Fast/Ultra Fast policy combinations in requirements responses and request routing, agent prewarm inheritance, TUI command visibility and saved selections, and legacy cloud denial normalization and override precedence.

GitOrigin-RevId: 147e08524c4b228ea9d46b25a9f8aa64c359e169
This commit is contained in:
dpatel-oai
2026-10-06 03:35:55 +00:00
committed by copyberry
parent 989c01a41a
commit 7ac954ea24
43 changed files with 462 additions and 73 deletions
@@ -10218,6 +10218,14 @@
}
],
"description": "Null if no requirements are configured (e.g. no requirements.toml/MDM entries)."
},
"supportsIndependentSpeedModes": {
"default": null,
"description": "Whether Fast and Ultra Fast requirements are enforced independently. Older servers omit this field and use Fast mode as a shared speed gate.",
"type": [
"boolean",
"null"
]
}
},
"title": "ConfigRequirementsReadResponse",
@@ -6054,6 +6054,14 @@
}
],
"description": "Null if no requirements are configured (e.g. no requirements.toml/MDM entries)."
},
"supportsIndependentSpeedModes": {
"default": null,
"description": "Whether Fast and Ultra Fast requirements are enforced independently. Older servers omit this field and use Fast mode as a shared speed gate.",
"type": [
"boolean",
"null"
]
}
},
"title": "ConfigRequirementsReadResponse",
@@ -1138,6 +1138,14 @@
}
],
"description": "Null if no requirements are configured (e.g. no requirements.toml/MDM entries)."
},
"supportsIndependentSpeedModes": {
"default": null,
"description": "Whether Fast and Ultra Fast requirements are enforced independently. Older servers omit this field and use Fast mode as a shared speed gate.",
"type": [
"boolean",
"null"
]
}
},
"title": "ConfigRequirementsReadResponse",
@@ -4,6 +4,11 @@
import type { ConfigRequirements } from "./ConfigRequirements";
export type ConfigRequirementsReadResponse = {
/**
* Whether Fast and Ultra Fast requirements are enforced independently.
* Older servers omit this field and use Fast mode as a shared speed gate.
*/
supportsIndependentSpeedModes: boolean | null,
/**
* Null if no requirements are configured (e.g. no requirements.toml/MDM entries).
*/
@@ -760,6 +760,10 @@ pub enum ResidencyRequirement {
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ConfigRequirementsReadResponse {
/// Whether Fast and Ultra Fast requirements are enforced independently.
/// Older servers omit this field and use Fast mode as a shared speed gate.
#[serde(default)]
pub supports_independent_speed_modes: Option<bool>,
/// Null if no requirements are configured (e.g. no requirements.toml/MDM entries).
#[experimental(nested)]
pub requirements: Option<ConfigRequirements>,
+6
View File
@@ -211,6 +211,12 @@ Codex home must support compressed rollout files, including shared histories.
## Managed model provider requirements
`configRequirements/read` reports `supportsIndependentSpeedModes: true` when
`features.fast_mode` and `features.ultrafast_mode` independently control Fast and
Ultra Fast. Older servers omit this field and use Fast mode as a shared speed
gate. Missing individual feature requirements impose no additional restriction;
the model catalog still determines which service tiers a model supports.
Existing threads retain their provider configuration. Input RPCs reject requests when managed
`model_provider` or `model_providers` requirements no longer match that configuration, or cannot
be loaded. This covers turn start/steer, review, compaction, manual queue start, and active goal
@@ -143,7 +143,10 @@ impl ConfigRequestProcessor {
self.thread_manager.auth_manager().allowed_login_methods(),
);
Ok(ConfigRequirementsReadResponse { requirements })
Ok(ConfigRequirementsReadResponse {
supports_independent_speed_modes: Some(true),
requirements,
})
}
pub(crate) async fn value_write(
@@ -60,7 +60,7 @@ async fn application_network_defaults_and_null_base_url_are_exposed() -> Result<
let mut server = start_server(&home).await?;
assert_eq!(
read_requirements(&mut server).await?,
json!({"requirements": null})
json!({"requirements": null, "supportsIndependentSpeedModes": true})
);
Ok(())
}
@@ -66,7 +66,11 @@ async fn config_requirements_read_preserves_webmcp_policy() -> Result<()> {
"{contents}",
);
} else {
assert_eq!(wire, json!({ "requirements": null }), "{contents}");
assert_eq!(
wire,
json!({ "requirements": null, "supportsIndependentSpeedModes": true }),
"{contents}"
);
}
}
Ok(())
@@ -63,7 +63,10 @@ async fn config_requirements_read_preserves_import_policy() -> Result<()> {
if let Some(requirements) = wire["requirements"].as_object() {
assert_eq!(requirements.get("inAppBrowser"), Some(&expected_wire));
} else {
assert_eq!(wire, json!({ "requirements": null }));
assert_eq!(
wire,
json!({ "requirements": null, "supportsIndependentSpeedModes": true })
);
}
let response: ConfigRequirementsReadResponse = serde_json::from_value(wire)?;
let actual = response
@@ -47,7 +47,7 @@ async fn config_requirements_read_preserves_unrestricted_default(config: &str) -
let (_home, mut server) = start_server(config, /*requirements*/ None).await?;
assert_eq!(
read_requirements(&mut server).await?,
json!({"requirements": null})
json!({"requirements": null, "supportsIndependentSpeedModes": true})
);
Ok(())
}
@@ -66,6 +66,46 @@ fn write_config(codex_home: &TempDir, contents: &str) -> Result<()> {
)?)
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn config_requirements_read_exposes_independent_speed_policy() -> Result<()> {
for (fast_enabled, ultrafast_enabled) in
[(false, false), (false, true), (true, false), (true, true)]
{
let codex_home = TempDir::new()?;
std::fs::write(
codex_home.path().join("requirements.toml"),
format!(
"[features]\nfast_mode = {fast_enabled}\nultrafast_mode = {ultrafast_enabled}\n"
),
)?;
let mut app_server = TestAppServer::builder()
.with_codex_home(codex_home.path())
.without_auto_env()
.build_initialized_with_timeout(DEFAULT_READ_TIMEOUT)
.await?;
let request_id = app_server.send_config_requirements_read_request().await?;
let response: ConfigRequirementsReadResponse =
timeout(DEFAULT_READ_TIMEOUT, app_server.read_response(request_id)).await??;
assert_eq!(
(
response.supports_independent_speed_modes,
response
.requirements
.expect("speed requirements")
.feature_requirements
),
(
Some(true),
Some(std::collections::BTreeMap::from([
("fast_mode".to_string(), fast_enabled),
("ultrafast_mode".to_string(), ultrafast_enabled),
]))
),
);
}
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn managed_auth_settings_are_exposed_enforced_and_read_only() -> Result<()> {
let codex_home = TempDir::new()?;
@@ -713,7 +713,10 @@ async fn config_reads_complete_alongside_skills_list_request() -> Result<()> {
assert!(config.layers.is_none());
assert_eq!(
requirements,
ConfigRequirementsReadResponse { requirements: None }
ConfigRequirementsReadResponse {
supports_independent_speed_modes: Some(true),
requirements: None,
}
);
assert!(!permission_profiles.data.is_empty());
@@ -62,6 +62,24 @@ impl CloudRequirementsTomlBundle {
.into_iter()
.rev()
.map(|fragment| {
// Older backends used this fragment to deny every accelerated tier.
// Preserve that meaning through mixed deployments and rollbacks.
if fragment.id == "rbac-fast-mode"
&& let Ok(mut contents) = toml::from_str::<toml::Value>(&fragment.contents)
&& let Some(features) = contents
.get_mut("features")
.and_then(toml::Value::as_table_mut)
&& features.get("fast_mode").and_then(toml::Value::as_bool) == Some(false)
{
features.insert("ultrafast_mode".to_string(), toml::Value::Boolean(false));
return RequirementsLayerEntry::from_toml_value(
RequirementSource::EnterpriseManaged {
id: fragment.id,
name: fragment.name,
},
contents,
);
}
RequirementsLayerEntry::from_toml(
RequirementSource::EnterpriseManaged {
id: fragment.id,
@@ -18,6 +18,77 @@ use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
use tempfile::tempdir;
#[test]
fn legacy_speed_denial_is_normalized_without_changing_other_fragments() {
for (id, contents, expected_ultrafast) in [
(
"rbac-fast-mode",
"[features]\nfast_mode = false",
Some(false),
),
("rbac-fast-mode", "[features]\nfast_mode = true", None),
("admin-layer", "[features]\nfast_mode = false", None),
] {
let requirements = compose_requirements(
CloudRequirementsTomlBundle {
enterprise_managed: vec![CloudRequirementsFragment {
id: id.to_string(),
name: id.to_string(),
contents: contents.to_string(),
}],
}
.into_layers(),
)
.expect("valid cloud requirements")
.expect("requirements layer")
.into_toml();
assert_eq!(
requirements
.feature_requirements
.expect("feature requirements")
.entries
.get("ultrafast_mode")
.copied(),
expected_ultrafast,
"fragment={id}",
);
}
}
#[test]
fn enterprise_speed_override_still_takes_precedence_over_legacy_baseline() {
let requirements = compose_requirements(
CloudRequirementsTomlBundle {
enterprise_managed: vec![
CloudRequirementsFragment {
id: "admin-layer".to_string(),
name: "Admin".to_string(),
contents: "[features]\nultrafast_mode = true".to_string(),
},
CloudRequirementsFragment {
id: "rbac-fast-mode".to_string(),
name: "Legacy RBAC".to_string(),
contents: "[features]\nfast_mode = false".to_string(),
},
],
}
.into_layers(),
)
.expect("valid cloud requirements")
.expect("requirements layers")
.into_toml();
assert_eq!(
requirements
.feature_requirements
.expect("feature requirements")
.entries,
std::collections::BTreeMap::from([
("fast_mode".to_string(), false),
("ultrafast_mode".to_string(), true),
]),
);
}
#[tokio::test]
async fn shared_future_runs_once() {
let counter = Arc::new(AtomicUsize::new(0));
+6
View File
@@ -1205,6 +1205,9 @@
"tui_app_server": {
"type": "boolean"
},
"ultrafast_mode": {
"type": "boolean"
},
"unavailable_dummy_tools": {
"type": "boolean"
},
@@ -7381,6 +7384,9 @@
"tui_app_server": {
"type": "boolean"
},
"ultrafast_mode": {
"type": "boolean"
},
"unavailable_dummy_tools": {
"type": "boolean"
},
+4
View File
@@ -206,6 +206,10 @@ mod role_overrides {
.enabled(Feature::FastMode)
.then(|| ServiceTier::Fast.request_value().to_string()),
Some(ServiceTier::Flex) => Some(ServiceTier::Flex.request_value().to_string()),
None if service_tier == "ultrafast" => next_config
.features
.enabled(Feature::UltrafastMode)
.then(|| service_tier.clone()),
None => Some(service_tier.clone()),
};
}
+3
View File
@@ -3989,6 +3989,9 @@ impl Config {
.enabled(Feature::FastMode)
.then(|| ServiceTier::Fast.request_value().to_string()),
Some(ServiceTier::Flex) => Some(ServiceTier::Flex.request_value().to_string()),
None if service_tier == "ultrafast" => features
.enabled(Feature::UltrafastMode)
.then_some(service_tier),
None => Some(service_tier),
}
});
+8 -8
View File
@@ -82,6 +82,7 @@ use codex_extension_api::PromptSlot;
use codex_extension_api::TurnContextContributionInput;
use codex_features::FEATURES;
use codex_features::Feature;
use codex_features::Features;
use codex_features::unstable_features_warning_event;
use codex_history::RolloutItem;
use codex_hooks::Hooks;
@@ -834,14 +835,13 @@ impl Session {
/*developer_instructions*/ None,
)
});
let fast_mode_enabled = config.features.enabled(Feature::FastMode);
let initial_service_tier_warning = unsupported_service_tier_warning(
config.service_tier.as_deref(),
fast_mode_enabled,
&config.features,
&model_info,
);
let service_tier =
get_service_tier(config.service_tier.clone(), fast_mode_enabled, &model_info);
get_service_tier(config.service_tier.clone(), &config.features, &model_info);
let storage_originator = AuthStorageOriginator::from_client_name(&originator);
let session_configuration = SessionConfiguration {
turn_extension_init,
@@ -1124,14 +1124,14 @@ pub(crate) fn new_submission_id() -> String {
pub(crate) fn get_service_tier(
configured_service_tier: Option<String>,
fast_mode_enabled: bool,
features: &Features,
model_info: &ModelInfo,
) -> Option<String> {
let service_tier = configured_service_tier?;
if service_tier == ServiceTier::Flex.request_value() {
return Some(service_tier);
}
if fast_mode_enabled
if features.service_tier_enabled(&service_tier)
&& (service_tier == SERVICE_TIER_DEFAULT_REQUEST_VALUE
|| model_info.supports_service_tier(&service_tier))
{
@@ -1142,11 +1142,11 @@ pub(crate) fn get_service_tier(
fn unsupported_service_tier_warning(
configured_service_tier: Option<&str>,
fast_mode_enabled: bool,
features: &Features,
model_info: &ModelInfo,
) -> Option<String> {
let service_tier = configured_service_tier.filter(|service_tier| {
fast_mode_enabled
features.service_tier_enabled(service_tier)
&& *service_tier != SERVICE_TIER_DEFAULT_REQUEST_VALUE
&& !model_info.supports_service_tier(service_tier)
})?;
@@ -3733,7 +3733,7 @@ impl Session {
let mut inherited_settings = ResolvedStepSettings::new(
Arc::new(selected),
Arc::clone(&settings.model_info),
self.features.enabled(Feature::FastMode),
&self.features,
);
inherited_settings.mcp_approvals_reviewer_override =
settings.mcp_approvals_reviewer_override;
@@ -40,7 +40,7 @@ async fn initial_replay_preserves_prewarmed_effort(history: InitialHistory) {
let prewarm_settings = ResolvedStepSettings::new(
Arc::new(selected.clone()),
Arc::clone(&model_info),
/*fast_mode_enabled*/ false,
&codex_features::Features::default(),
);
// Force the ordering where prewarm pins its request before initial replay finishes.
assert_eq!(
@@ -55,7 +55,7 @@ async fn initial_replay_preserves_prewarmed_effort(history: InitialHistory) {
let turn_settings = ResolvedStepSettings::new(
Arc::new(selected),
model_info,
/*fast_mode_enabled*/ false,
&codex_features::Features::default(),
);
assert_eq!(
session
+1 -1
View File
@@ -99,7 +99,7 @@ pub(super) async fn spawn_review_thread(
let step_settings = Arc::new(ResolvedStepSettings::new(
Arc::new(selected),
Arc::new(model_info.clone()),
review_features.enabled(Feature::FastMode),
&review_features,
));
per_turn_config.model = Some(model);
per_turn_config.model_reasoning_effort = reasoning_effort;
+1 -1
View File
@@ -347,7 +347,7 @@ async fn schedule_startup_prewarm_inner(
) {
crate::session::get_service_tier(
session.services.agent_control.service_tier(),
session.features().enabled(Feature::FastMode),
&session.features(),
&preconnect_model_info,
)
} else {
+1 -1
View File
@@ -412,7 +412,7 @@ impl Session {
&constraints,
self.services.models_manager.as_ref(),
&overrides,
self.features.enabled(Feature::FastMode),
&self.features,
)
.await
.map_err(|error| error.to_string())
@@ -1062,11 +1062,8 @@ async fn activation_must_match_the_retained_turn_authority(change: ManagedPolicy
model_info.slug = "step-settings-policy-destination".to_string();
let mut selected = current.selected().clone();
selected.collaboration_mode.settings.model = model_info.slug.clone();
let destination = ResolvedStepSettings::new(
Arc::new(selected),
Arc::new(model_info),
session.features.enabled(Feature::FastMode),
);
let destination =
ResolvedStepSettings::new(Arc::new(selected), Arc::new(model_info), &session.features);
let mut live = session.state.lock().await.session_configuration.clone();
live.original_config_do_not_use = Arc::clone(&prepared.config);
assert_eq!(
+6 -8
View File
@@ -4,6 +4,7 @@ use crate::config::Constrained;
use crate::config::ConstraintError;
use crate::config::ConstraintResult;
use codex_config::ConfigRequirements;
use codex_features::Features;
use codex_models_manager::ModelsManagerConfig;
use codex_models_manager::manager::ModelsManager;
use codex_otel::SessionTelemetry;
@@ -57,16 +58,13 @@ impl ResolvedStepSettings {
pub(super) fn new(
selected: Arc<StepSettings>,
model_info: Arc<ModelInfo>,
fast_mode_enabled: bool,
features: &Features,
) -> Self {
let reasoning_summary = selected
.reasoning_summary
.unwrap_or(model_info.default_reasoning_summary);
let service_tier = super::get_service_tier(
selected.service_tier.clone(),
fast_mode_enabled,
&model_info,
);
let service_tier =
super::get_service_tier(selected.service_tier.clone(), features, &model_info);
Self {
selected,
model_info,
@@ -129,7 +127,7 @@ impl ResolvedStepSettings {
constraints: &StepSettingsConstraints<'_>,
models_manager: &dyn ModelsManager,
overrides: &ModelInfoOverrides,
fast_mode_enabled: bool,
features: &Features,
) -> ConstraintResult<Self> {
let selected = self.selected.apply(update, constraints)?;
let model_info =
@@ -138,7 +136,7 @@ impl ResolvedStepSettings {
} else {
Arc::new(selected.resolve_model_info(models_manager, overrides).await)
};
let mut next = Self::new(Arc::new(selected), model_info, fast_mode_enabled);
let mut next = Self::new(Arc::new(selected), model_info, features);
next.mcp_approvals_reviewer_override = update
.approvals_reviewer
.or(self.mcp_approvals_reviewer_override);
+18 -16
View File
@@ -4703,7 +4703,7 @@ async fn turn_context_with_model_updates_model_fields() {
*settings = ResolvedStepSettings::new(
Arc::new(selected),
Arc::clone(&settings.model_info),
/*fast_mode_enabled*/ true,
&codex_features::Features::with_defaults(),
);
});
Arc::make_mut(&mut turn_context.config).service_tier =
@@ -4719,7 +4719,7 @@ async fn turn_context_with_model_updates_model_fields() {
let current = Arc::new(ResolvedStepSettings::new(
Arc::new(current_selection),
Arc::clone(turn_context.model_info()),
/*fast_mode_enabled*/ true,
&codex_features::Features::with_defaults(),
));
turn_context.next_step_settings.store(Arc::clone(&current));
let updated = turn_context
@@ -4968,7 +4968,7 @@ fn get_service_tier_does_not_use_model_default_when_absent_and_fast_mode_enabled
assert_eq!(
get_service_tier(
/*configured_service_tier*/ None,
/*fast_mode_enabled*/ true,
&codex_features::Features::with_defaults(),
&model_info,
),
None
@@ -4982,7 +4982,7 @@ fn get_service_tier_does_not_use_model_default_when_fast_mode_disabled() {
assert_eq!(
get_service_tier(
/*configured_service_tier*/ None,
/*fast_mode_enabled*/ false,
&codex_features::Features::default(),
&model_info,
),
None
@@ -4996,7 +4996,7 @@ fn get_service_tier_keeps_supported_explicit_tier() {
assert_eq!(
get_service_tier(
Some(ServiceTier::Fast.request_value().to_string()),
/*fast_mode_enabled*/ true,
&codex_features::Features::with_defaults(),
&model_info,
),
Some(ServiceTier::Fast.request_value().to_string())
@@ -5010,7 +5010,7 @@ fn get_service_tier_does_not_default_when_model_has_no_default() {
assert_eq!(
get_service_tier(
/*configured_service_tier*/ None,
/*fast_mode_enabled*/ true,
&codex_features::Features::with_defaults(),
&model_info,
),
None
@@ -5024,7 +5024,7 @@ fn get_service_tier_drops_unsupported_configured_tier_when_fast_mode_enabled() {
assert_eq!(
get_service_tier(
Some("unsupported".to_string()),
/*fast_mode_enabled*/ true,
&codex_features::Features::with_defaults(),
&model_info,
),
None
@@ -5032,7 +5032,7 @@ fn get_service_tier_drops_unsupported_configured_tier_when_fast_mode_enabled() {
assert_eq!(
get_service_tier(
Some(SERVICE_TIER_DEFAULT_REQUEST_VALUE.to_string()),
/*fast_mode_enabled*/ true,
&codex_features::Features::with_defaults(),
&model_info,
),
Some(SERVICE_TIER_DEFAULT_REQUEST_VALUE.to_string())
@@ -5043,10 +5043,12 @@ fn get_service_tier_drops_unsupported_configured_tier_when_fast_mode_enabled() {
fn get_service_tier_preserves_flex_without_catalog_support_or_fast_mode() {
let model_info = model_with_default_service_tier(/*default_service_tier*/ None);
for fast_mode_enabled in [false, true] {
let mut features = codex_features::Features::with_defaults();
features.set_enabled(Feature::FastMode, fast_mode_enabled);
assert_eq!(
get_service_tier(
Some(ServiceTier::Flex.request_value().to_string()),
fast_mode_enabled,
&features,
&model_info,
),
Some(ServiceTier::Flex.request_value().to_string())
@@ -5061,7 +5063,7 @@ fn get_service_tier_ignores_non_flex_tiers_when_fast_mode_disabled() {
assert_eq!(
get_service_tier(
Some(ServiceTier::Fast.request_value().to_string()),
/*fast_mode_enabled*/ false,
&codex_features::Features::default(),
&model_info,
),
None
@@ -5069,7 +5071,7 @@ fn get_service_tier_ignores_non_flex_tiers_when_fast_mode_disabled() {
assert_eq!(
get_service_tier(
Some(SERVICE_TIER_DEFAULT_REQUEST_VALUE.to_string()),
/*fast_mode_enabled*/ false,
&codex_features::Features::default(),
&model_info,
),
None
@@ -5077,7 +5079,7 @@ fn get_service_tier_ignores_non_flex_tiers_when_fast_mode_disabled() {
assert_eq!(
get_service_tier(
Some("unsupported".to_string()),
/*fast_mode_enabled*/ false,
&codex_features::Features::default(),
&model_info,
),
None
@@ -5085,7 +5087,7 @@ fn get_service_tier_ignores_non_flex_tiers_when_fast_mode_disabled() {
assert_eq!(
get_service_tier(
/*configured_service_tier*/ None,
/*fast_mode_enabled*/ false,
&codex_features::Features::default(),
&model_info,
),
None
@@ -7036,7 +7038,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
Arc::new(super::step_settings::ResolvedStepSettings::new(
Arc::clone(&session_configuration.step_settings),
Arc::new(model_info),
config.features.enabled(Feature::FastMode),
&config.features,
)),
&models_manager,
/*network*/ None,
@@ -9302,7 +9304,7 @@ where
Arc::new(super::step_settings::ResolvedStepSettings::new(
Arc::clone(&session_configuration.step_settings),
Arc::new(model_info),
config.features.enabled(Feature::FastMode),
&config.features,
)),
&models_manager,
/*network*/ None,
@@ -11326,7 +11328,7 @@ async fn build_initial_context_uses_retained_step_after_model_change() {
.store(Arc::new(ResolvedStepSettings::new(
Arc::new(selected_b),
Arc::new(model_b),
/*fast_mode_enabled*/ false,
&codex_features::Features::default(),
)));
let step_b = session
.capture_step_context(Arc::clone(&turn_context), &CancellationToken::new())
+2 -2
View File
@@ -822,7 +822,7 @@ impl TurnContext {
let step_settings = Arc::new(ResolvedStepSettings::new(
Arc::new(selected),
model_info,
config.features.enabled(Feature::FastMode),
&config.features,
));
config.service_tier = step_settings.service_tier.clone();
let session_telemetry = step_settings.telemetry(&self.session_telemetry);
@@ -1395,7 +1395,7 @@ impl Session {
let step_settings = Arc::new(ResolvedStepSettings::new(
Arc::clone(&session_configuration.step_settings),
Arc::new(model_info),
self.features.enabled(Feature::FastMode),
&self.features,
));
let mut turn_context: TurnContext = Self::make_turn_context(
self.thread_id(),
+18 -6
View File
@@ -27,11 +27,15 @@ use std::time::Duration;
const WS_V2_BETA_HEADER_VALUE: &str = "responses_websockets=2026-02-06";
#[test_case::test_case(true; "fast mode enabled")]
#[test_case::test_case(false; "fast mode disabled")]
#[test_case::test_case("priority", true, false; "fast mode enabled")]
#[test_case::test_case("priority", false, true; "fast mode disabled")]
#[test_case::test_case("ultrafast", false, true; "ultrafast mode enabled")]
#[test_case::test_case("ultrafast", true, false; "ultrafast mode disabled")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn spawned_agent_prewarm_handshake_inherits_effective_root_service_tier(
tier: &'static str,
fast_mode_enabled: bool,
ultrafast_mode_enabled: bool,
) -> Result<()> {
skip_if_no_network!(Ok(()));
@@ -63,7 +67,11 @@ async fn spawned_agent_prewarm_handshake_inherits_effective_root_service_tier(
],
])
.await;
let tier = ServiceTier::Fast.request_value();
let tier_enabled = if tier == "ultrafast" {
ultrafast_mode_enabled
} else {
fast_mode_enabled
};
let test = test_codex()
.with_model("gpt-5.4")
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
@@ -89,7 +97,11 @@ async fn spawned_agent_prewarm_handshake_inherits_effective_root_service_tier(
.disable(Feature::FastMode)
.expect("fast mode");
}
config.service_tier = Some(ServiceTier::Fast.request_value().to_string());
config
.features
.set_enabled(Feature::UltrafastMode, ultrafast_mode_enabled)
.expect("configure Ultra Fast mode");
config.service_tier = Some(tier.to_string());
})
.build_with_websocket_server(&server)
.await?;
@@ -122,7 +134,7 @@ async fn spawned_agent_prewarm_handshake_inherits_effective_root_service_tier(
"child startup should connect before its first turn"
);
let handshakes = server.handshakes();
let expected_hint = if fast_mode_enabled {
let expected_hint = if tier_enabled {
format!("model=gpt-5.4;tier={tier}")
} else {
"model=gpt-5.4".to_string()
@@ -140,7 +152,7 @@ async fn spawned_agent_prewarm_handshake_inherits_effective_root_service_tier(
assert_eq!(child_warmup["generate"], false);
assert_eq!(
child_warmup["service_tier"].as_str(),
fast_mode_enabled.then_some(tier)
tier_enabled.then_some(tier)
);
assert_eq!(server.handshakes().len(), 2);
child.shutdown_and_wait().await?;
+57
View File
@@ -107,6 +107,63 @@ const INSTALLATION_ID_FILENAME: &str = "installation_id";
const TEST_WINDOW_ID: &str = "test-thread:0";
const TEST_INSTALLATION_ID: &str = "11111111-1111-4111-8111-111111111111";
#[test_case::test_case(false, false)]
#[test_case::test_case(false, true)]
#[test_case::test_case(true, false)]
#[test_case::test_case(true, true)]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn responses_request_enforces_independent_speed_requirements(
fast_enabled: bool,
ultrafast_enabled: bool,
) -> anyhow::Result<()> {
skip_if_no_network!(Ok(()));
for (tier, enabled) in [("priority", fast_enabled), ("ultrafast", ultrafast_enabled)] {
for configure_at_start in [false, true] {
let server = start_mock_server().await;
let response_mock = mount_sse_once(&server, sse(vec![ev_completed("done")])).await;
let test = test_codex()
.with_model("gpt-5.4")
.with_model_info_override("gpt-5.4", move |model| {
model.service_tiers = vec![codex_protocol::openai_models::ModelServiceTier {
id: tier.to_string(),
name: tier.to_string(),
description: String::new(),
}];
})
.with_config(move |config| {
config
.features
.set_enabled(Feature::FastMode, fast_enabled)
.expect("configure Fast mode");
config
.features
.set_enabled(Feature::UltrafastMode, ultrafast_enabled)
.expect("configure Ultra Fast mode");
config.service_tier = configure_at_start.then(|| tier.to_string());
})
.build_with_auto_env(&server)
.await?;
if !configure_at_start {
core_test_support::submit_thread_settings(
&test.codex,
ThreadSettingsOverrides {
service_tier: Some(Some(tier.to_string())),
..Default::default()
},
)
.await?;
}
test.submit_turn("hello").await?;
assert_eq!(
response_mock.single_request().body_json()["service_tier"].as_str(),
enabled.then_some(tier),
"tier={tier}, configure_at_start={configure_at_start}",
);
}
}
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn responses_request_preserves_flex_without_catalog_support_or_fast_mode()
-> anyhow::Result<()> {
+17
View File
@@ -401,6 +401,8 @@ pub enum Feature {
Artifact,
/// Enable Fast mode selection in the TUI and request layer.
FastMode,
/// Enable Ultra Fast mode independently of Fast mode.
UltrafastMode,
/// Enable explicitly requested model changes for later step captures.
StepModelSwitching,
/// Enable voice conversations in the TUI.
@@ -552,6 +554,15 @@ impl Features {
self.enabled.contains(&f)
}
/// Whether a routing tier is enabled, independently of model catalog support.
pub fn service_tier_enabled(&self, service_tier: &str) -> bool {
match service_tier {
"flex" => true,
"ultrafast" => self.enabled(Feature::UltrafastMode),
_ => self.enabled(Feature::FastMode),
}
}
/// Returns whether persistent execution is enabled for the selected effort.
pub fn persistent_execution_enabled(&self, reasoning_effort: Option<&ReasoningEffort>) -> bool {
reasoning_effort == Some(&ReasoningEffort::Persistent)
@@ -1892,6 +1903,12 @@ pub const FEATURES: &[FeatureSpec] = &[
stage: Stage::Stable,
default_enabled: true,
},
FeatureSpec {
id: Feature::UltrafastMode,
key: "ultrafast_mode",
stage: Stage::Stable,
default_enabled: true,
},
FeatureSpec {
id: Feature::StepModelSwitching,
key: "step_model_switching",
+2 -1
View File
@@ -1081,7 +1081,8 @@ impl App {
);
if server_model_cleared
&& config.model.is_none()
&& config.features.enabled(Feature::FastMode)
&& (config.features.enabled(Feature::FastMode)
|| config.features.enabled(Feature::UltrafastMode))
{
// Bootstrap's fallback model may be seeded from the client. Resolve tiers
// against the server catalog when config/read cleared the model.
@@ -188,6 +188,8 @@ async fn luna_reserve_recovery_restores_task_and_pending_turn_after_fresh_backen
configure_reserve_catalog(&mut app);
app.chat_widget
.set_feature_enabled(Feature::FastMode, /*enabled*/ false);
app.chat_widget
.set_feature_enabled(Feature::UltrafastMode, /*enabled*/ false);
app.chat_widget
.set_reasoning_effort(Some(ReasoningEffortConfig::High));
let original_mode = app.chat_widget.effective_collaboration_mode();
+9 -5
View File
@@ -718,15 +718,19 @@ impl AppServerSession {
},
async { Ok(crate::collaboration_modes::list(self.request_handle()).await) },
)?;
self.managed_new_thread_defaults = requirements
.requirements
.and_then(|requirements| requirements.models)
.and_then(|models| models.new_thread);
let available_models = models
let mut available_models = models
.data
.into_iter()
.map(model_preset_from_api_model)
.collect::<Vec<_>>();
crate::service_tier_resolution::constrain_server_service_tiers(
&mut available_models,
&requirements,
);
self.managed_new_thread_defaults = requirements
.requirements
.and_then(|requirements| requirements.models)
.and_then(|models| models.new_thread);
let default_model = config
.model
.clone()
+4 -3
View File
@@ -66,10 +66,10 @@ impl ChatWidget {
}
pub(super) fn sync_service_tier_commands(&mut self) {
let commands = self.current_model_service_tier_commands();
self.bottom_pane
.set_service_tier_commands_enabled(self.fast_mode_enabled());
self.bottom_pane
.set_service_tier_commands(self.current_model_service_tier_commands());
.set_service_tier_commands_enabled(!commands.is_empty());
self.bottom_pane.set_service_tier_commands(commands);
}
pub(super) fn current_model_service_tier_commands(&self) -> Vec<ServiceTierCommand> {
@@ -85,6 +85,7 @@ impl ChatWidget {
preset
.service_tiers
.into_iter()
.filter(|tier| self.config.features.service_tier_enabled(&tier.id))
.map(|tier| ServiceTierCommand {
id: tier.id,
name: tier.name.to_lowercase(),
+1 -1
View File
@@ -116,7 +116,7 @@ impl ChatWidget {
self.set_daybreak_enabled(self.daybreak_enabled);
self.refresh_status_surfaces();
}
if feature == Feature::FastMode {
if matches!(feature, Feature::FastMode | Feature::UltrafastMode) {
self.refresh_effective_service_tier();
self.sync_service_tier_commands();
}
@@ -1241,7 +1241,7 @@ impl ChatWidget {
plugins_command_enabled: self.config.features.enabled(Feature::Plugins),
token_activity_command_enabled: self.has_codex_backend_auth,
goal_command_enabled: self.config.features.enabled(Feature::Goals),
service_tier_commands_enabled: self.fast_mode_enabled(),
service_tier_commands_enabled: !self.current_model_service_tier_commands().is_empty(),
daybreak_command_description: self.daybreak_command_description(),
voice_command_enabled: self.realtime_conversation_available_for_thread,
worktrees_enabled: self.config.features.enabled(Feature::Worktrees)
@@ -106,6 +106,56 @@ fn next_copy_selection(
selection
}
#[tokio::test]
async fn service_tier_commands_and_saved_selection_respect_independent_speed_policy() {
for (fast_enabled, ultrafast_enabled) in [(false, true), (true, false)] {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.5")).await;
let mut preset = get_available_model(&chat, "gpt-5.5");
preset.service_tiers = ["priority", "ultrafast"]
.into_iter()
.map(|tier| codex_protocol::openai_models::ModelServiceTier {
id: tier.to_string(),
name: tier.to_string(),
description: format!("{tier} processing"),
})
.collect();
preset.default_service_tier = Some("ultrafast".to_string());
chat.model_catalog = std::sync::Arc::new(ModelCatalog::new(vec![preset]));
chat.set_feature_enabled(Feature::FastMode, fast_enabled);
chat.set_feature_enabled(Feature::UltrafastMode, ultrafast_enabled);
chat.set_service_tier(Some("ultrafast".to_string()));
assert_eq!(
chat.current_service_tier(),
ultrafast_enabled.then_some("ultrafast")
);
assert_eq!(
chat.current_model_service_tier_commands()
.into_iter()
.map(|tier| tier.id)
.collect::<Vec<_>>(),
vec![
if ultrafast_enabled {
"ultrafast"
} else {
"priority"
}
.to_string()
],
);
chat.bottom_pane
.set_composer_text("/ultra".to_string(), Vec::new(), Vec::new());
let snapshot_name = if ultrafast_enabled {
"ultrafast_command_allowed"
} else {
"ultrafast_command_denied"
};
insta::assert_snapshot!(
snapshot_name,
normalize_snapshot_paths(render_bottom_popup(&chat, /*width*/ 80))
);
}
}
#[tokio::test]
async fn service_tier_commands_lowercase_catalog_names() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.5")).await;
@@ -0,0 +1,9 @@
---
source: tui/src/chatwidget/tests/slash_commands.rs
expression: "render_bottom_popup(&chat, 80)"
---
› /ultrafast ultrafast processing
› /ultra
GPT-5.5 default · /tmp/project
@@ -0,0 +1,7 @@
---
source: tui/src/chatwidget/tests/slash_commands.rs
expression: "render_bottom_popup(&chat, 80)"
---
› /ultra
GPT-5.5 default · /tmp/project
+37 -6
View File
@@ -4,6 +4,38 @@ use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
use codex_protocol::config_types::ServiceTier;
use codex_protocol::openai_models::ModelPreset;
pub(crate) fn constrain_server_service_tiers(
models: &mut [ModelPreset],
requirements: &codex_app_server_protocol::ConfigRequirementsReadResponse,
) {
let Some(features) = requirements
.requirements
.as_ref()
.and_then(|requirements| requirements.feature_requirements.as_ref())
else {
return;
};
let fast_enabled = features.get("fast_mode") != Some(&false);
// Older servers enforce Ultra Fast through their shared Fast mode gate.
let ultrafast_enabled = features.get("ultrafast_mode") != Some(&false)
&& (requirements.supports_independent_speed_modes == Some(true) || fast_enabled);
let tier_enabled = |tier: &str| match tier {
"flex" => true,
"ultrafast" => ultrafast_enabled,
_ => fast_enabled,
};
for model in models {
model.service_tiers.retain(|tier| tier_enabled(&tier.id));
if model
.default_service_tier
.as_deref()
.is_some_and(|tier| !tier_enabled(tier))
{
model.default_service_tier = None;
}
}
}
pub(crate) fn configured_service_tier(
config: &Config,
notices: &codex_config::types::Notice,
@@ -24,12 +56,8 @@ pub(crate) fn effective_service_tier(
if configured.as_deref() == Some(ServiceTier::Flex.request_value()) {
return configured;
}
if !config.features.enabled(Feature::FastMode) {
return None;
}
let Some(preset) = models.iter().find(|preset| preset.model == model) else {
return configured;
return configured.filter(|tier| config.features.service_tier_enabled(tier));
};
match configured.as_deref() {
@@ -41,6 +69,7 @@ pub(crate) fn effective_service_tier(
.clone()
.filter(|service_tier| model_supports_service_tier(preset, service_tier)),
}
.filter(|tier| config.features.service_tier_enabled(tier))
}
pub(crate) fn service_tier_update_for_core(
@@ -54,7 +83,9 @@ pub(crate) fn service_tier_update_for_core(
return Some(Some(service_tier));
}
if !config.features.enabled(Feature::FastMode) {
if !config.features.enabled(Feature::FastMode)
&& !config.features.enabled(Feature::UltrafastMode)
{
return None;
}
@@ -11840,6 +11840,13 @@ class ConfigRequirementsReadResponse(BaseModel):
description="Null if no requirements are configured (e.g. no requirements.toml/MDM entries)."
),
] = None
supports_independent_speed_modes: Annotated[
bool | None,
Field(
alias="supportsIndependentSpeedModes",
description="Whether Fast and Ultra Fast requirements are enforced independently. Older servers omit this field and use Fast mode as a shared speed gate.",
),
] = None
class ExternalAgentConfigDetectResponse(BaseModel):