mirror of
https://github.com/openai/codex.git
synced 2026-10-18 17:18:52 +00:00
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:
+8
@@ -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",
|
||||
|
||||
+8
@@ -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",
|
||||
|
||||
+8
@@ -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",
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
+5
@@ -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>,
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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()),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(¤t));
|
||||
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())
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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<()> {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
|
||||
+9
@@ -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
|
||||
+7
@@ -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
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user