feat: add gated ultra reasoning effort

This commit is contained in:
shijie-openai
2026-06-24 17:41:38 -07:00
parent 800529218a
commit e558cd0cf7
18 changed files with 282 additions and 21 deletions
+17 -6
View File
@@ -163,6 +163,13 @@ pub(crate) struct CompactConversationRequestSettings {
pub(crate) service_tier: Option<String>,
}
fn reasoning_effort_for_request(effort: ReasoningEffortConfig) -> ReasoningEffortConfig {
match effort {
ReasoningEffortConfig::Ultra => ReasoningEffortConfig::Custom("max".to_string()),
effort => effort,
}
}
fn session_telemetry_for_request(
session_telemetry: &SessionTelemetry,
request: &ResponsesApiRequest,
@@ -665,11 +672,13 @@ impl ModelClient {
let payload = ApiMemorySummarizeInput {
model: model_info.slug.clone(),
raw_memories,
reasoning: effort.map(|effort| Reasoning {
effort: Some(effort),
summary: None,
context: None,
}),
reasoning: effort
.map(reasoning_effort_for_request)
.map(|effort| Reasoning {
effort: Some(effort),
summary: None,
context: None,
}),
};
client
@@ -768,7 +777,9 @@ impl ModelClient {
) -> Option<Reasoning> {
if model_info.supports_reasoning_summaries {
Some(Reasoning {
effort: effort.or_else(|| model_info.default_reasoning_level.clone()),
effort: effort
.or_else(|| model_info.default_reasoning_level.clone())
.map(reasoning_effort_for_request),
summary: if summary == ReasoningSummaryConfig::None {
None
} else {
+15
View File
@@ -31,6 +31,7 @@ use codex_protocol::auth::AuthMode;
use codex_protocol::models::ContentItem;
use codex_protocol::models::ResponseItem;
use codex_protocol::openai_models::ModelInfo;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::protocol::InternalSessionSource;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::SubAgentSource;
@@ -154,6 +155,20 @@ fn test_session_telemetry() -> SessionTelemetry {
)
}
#[test]
fn ultra_reasoning_uses_max_for_requests() {
assert_eq!(
(
super::reasoning_effort_for_request(ReasoningEffort::Ultra),
super::reasoning_effort_for_request(ReasoningEffort::High),
),
(
ReasoningEffort::Custom("max".to_string()),
ReasoningEffort::High,
)
);
}
#[derive(Default)]
struct TagCollectorVisitor {
tags: BTreeMap<String, String>,
+1
View File
@@ -1461,6 +1461,7 @@ impl Config {
tool_output_token_limit: self.tool_output_token_limit,
base_instructions: self.base_instructions.clone(),
personality_enabled: self.features.enabled(Feature::Personality),
ultra_reasoning_enabled: self.features.enabled(Feature::MultiAgentMode),
model_supports_reasoning_summaries: self.model_supports_reasoning_summaries,
model_catalog: self.model_catalog.clone(),
}
+5 -1
View File
@@ -5,6 +5,7 @@ use codex_analytics::GuardianReviewFailureReason;
use codex_analytics::GuardianReviewTerminalStatus;
use codex_analytics::GuardianReviewTrackContext;
use codex_analytics::GuardianReviewedAction;
use codex_features::Feature;
use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::CodexErrorInfo;
@@ -684,11 +685,14 @@ pub(super) async fn guardian_review_session_config(
Some(network_proxy) => Some(network_proxy.proxy().current_cfg().await?),
None => None,
};
let available_models = session
let mut available_models = session
.services
.models_manager
.list_models(codex_models_manager::manager::RefreshStrategy::Offline)
.await;
if !turn.config.features.enabled(Feature::MultiAgentMode) {
codex_models_manager::model_presets::hide_ultra_reasoning_effort(&mut available_models);
}
let default_review_model_id = turn.provider.approval_review_preferred_model();
let preferred_reasoning_effort = |supports_low: bool, fallback| {
if supports_low {
+40
View File
@@ -6,6 +6,7 @@ use crate::config::ManagedFeatures;
use crate::config::NetworkProxySpec;
use crate::config::test_config;
use crate::guardian::approval_request::guardian_request_target_item_id;
use crate::guardian::review::guardian_review_session_config;
use crate::session::session::Session;
use crate::session::turn_context::TurnContext;
use crate::test_support;
@@ -36,6 +37,7 @@ use codex_protocol::models::PermissionProfile;
use codex_protocol::models::ResponseItem;
use codex_protocol::openai_models::ModelsResponse;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::openai_models::ReasoningEffortPreset;
use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxEntry;
@@ -1420,6 +1422,44 @@ enum GuardianTestCatalog {
ParentOnly,
}
#[tokio::test]
async fn guardian_review_does_not_select_ultra_when_feature_is_disabled() -> anyhow::Result<()> {
let server = start_mock_server().await;
let (mut session, turn) = guardian_test_session_and_turn(&server).await;
assert!(!turn.config.features.enabled(Feature::MultiAgentMode));
let mut review_model = turn.model_info.clone();
review_model.slug = turn.provider.approval_review_preferred_model().to_string();
review_model.default_reasoning_level = Some(ReasoningEffort::Ultra);
review_model.supported_reasoning_levels = vec![
ReasoningEffortPreset {
effort: ReasoningEffort::High,
description: "High".to_string(),
},
ReasoningEffortPreset {
effort: ReasoningEffort::Ultra,
description: "Ultra".to_string(),
},
];
let models_manager = StaticModelsManager::new(
Some(Arc::clone(&session.services.auth_manager)),
ModelsResponse {
models: vec![review_model],
},
);
Arc::get_mut(&mut session)
.expect("session should be unique")
.services
.models_manager = Arc::new(models_manager);
let guardian_config = guardian_review_session_config(&session, &turn).await?;
assert_eq!(
guardian_config.spawn_config.model_reasoning_effort,
Some(ReasoningEffort::High)
);
Ok(())
}
async fn guardian_request_model_for_auto_review(
auto_review_model_override: Option<String>,
catalog: GuardianTestCatalog,
+4 -1
View File
@@ -24,11 +24,14 @@ pub(super) async fn spawn_review_thread(
let _ = review_features.disable(Feature::WebSearchCached);
let _ = review_features.disable(Feature::Goals);
let review_web_search_mode = WebSearchMode::Disabled;
let available_models = sess
let mut available_models = sess
.services
.models_manager
.list_models(RefreshStrategy::OnlineIfUncached)
.await;
if !review_features.enabled(Feature::MultiAgentMode) {
codex_models_manager::model_presets::hide_ultra_reasoning_effort(&mut available_models);
}
let unified_exec_shell_mode = UnifiedExecShellMode::for_session(
codex_tools::unified_exec_feature_mode_for_features(review_features.get()),
crate::tools::tool_user_shell_type(sess.services.user_shell.as_ref()),
+9 -2
View File
@@ -6,6 +6,7 @@ use codex_core_skills::HostSkillsSnapshot;
use codex_file_system::FileSystemSandboxContext;
use codex_model_provider::SharedModelProvider;
use codex_model_provider::create_model_provider;
use codex_models_manager::model_presets::hide_ultra_reasoning_effort;
use codex_protocol::SessionId;
use codex_protocol::ThreadId;
use codex_protocol::models::AdditionalPermissionProfile;
@@ -246,9 +247,12 @@ impl TurnContext {
Some(reasoning_effort.clone()),
/*developer_instructions*/ None,
);
let available_models = models_manager
let mut available_models = models_manager
.list_models(RefreshStrategy::OnlineIfUncached)
.await;
if !config.features.enabled(Feature::MultiAgentMode) {
hide_ultra_reasoning_effort(&mut available_models);
}
Self {
sub_id: self.sub_id.clone(),
@@ -506,7 +510,10 @@ impl Session {
let auth_manager_for_context = auth_manager.clone();
let provider_for_context = create_model_provider(provider, auth_manager);
let session_telemetry_for_context = session_telemetry;
let available_models = models_manager.try_list_models().unwrap_or_default();
let mut available_models = models_manager.try_list_models().unwrap_or_default();
if !per_turn_config.features.enabled(Feature::MultiAgentMode) {
hide_ultra_reasoning_effort(&mut available_models);
}
let unified_exec_shell_mode = UnifiedExecShellMode::for_session(
codex_tools::unified_exec_feature_mode_for_features(per_turn_config.features.get()),
crate::tools::tool_user_shell_type(user_shell),
@@ -130,6 +130,10 @@ async fn spawn_agent_description_lists_visible_models_and_reasoning_efforts() ->
effort: ReasoningEffort::High,
description: "Deep dive".to_string(),
},
ReasoningEffortPreset {
effort: ReasoningEffort::Ultra,
description: "Maximum reasoning with proactive delegation".to_string(),
},
],
vec![ModelServiceTier {
id: "priority".to_string(),
@@ -167,6 +171,10 @@ async fn spawn_agent_description_lists_visible_models_and_reasoning_efforts() ->
.features
.enable(Feature::Collab)
.expect("test config should allow feature update");
config
.features
.enable(Feature::MultiAgentMode)
.expect("test config should allow feature update");
config.multi_agent_v2.hide_spawn_agent_metadata = false;
});
let test = builder.build(&server).await?;
@@ -200,7 +208,7 @@ async fn spawn_agent_description_lists_visible_models_and_reasoning_efforts() ->
"expected model override usage guidance in spawn_agent description: {description:?}"
);
assert!(
description.contains("Reasoning efforts: low, medium (default), high."),
description.contains("Reasoning efforts: low, medium (default), high, ultra."),
"expected default reasoning effort in spawn_agent description: {description:?}"
);
assert!(
+2 -2
View File
@@ -145,7 +145,7 @@ pub enum Feature {
Collab,
/// Enable task-path-based multi-agent routing.
MultiAgentV2,
/// Removed compatibility flag retained as a no-op.
/// Enable Ultra reasoning effort and its proactive multi-agent behavior.
MultiAgentMode,
/// Enable CSV-backed agent job tools.
SpawnCsv,
@@ -1036,7 +1036,7 @@ pub const FEATURES: &[FeatureSpec] = &[
FeatureSpec {
id: Feature::MultiAgentMode,
key: "multi_agent_mode",
stage: Stage::Removed,
stage: Stage::UnderDevelopment,
default_enabled: false,
},
FeatureSpec {
+1
View File
@@ -7,6 +7,7 @@ pub struct ModelsManagerConfig {
pub tool_output_token_limit: Option<usize>,
pub base_instructions: Option<String>,
pub personality_enabled: bool,
pub ultra_reasoning_enabled: bool,
pub model_supports_reasoning_summaries: Option<bool>,
pub model_catalog: Option<ModelsResponse>,
}
+12
View File
@@ -4,6 +4,7 @@ use codex_protocol::openai_models::ModelInfo;
use codex_protocol::openai_models::ModelInstructionsVariables;
use codex_protocol::openai_models::ModelMessages;
use codex_protocol::openai_models::ModelVisibility;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::openai_models::TruncationMode;
use codex_protocol::openai_models::TruncationPolicyConfig;
use codex_protocol::openai_models::WebSearchToolType;
@@ -21,6 +22,17 @@ const LOCAL_PRAGMATIC_TEMPLATE: &str = "You are a deeply pragmatic, effective so
const PERSONALITY_PLACEHOLDER: &str = "{{ personality }}";
pub fn with_config_overrides(mut model: ModelInfo, config: &ModelsManagerConfig) -> ModelInfo {
if !config.ultra_reasoning_enabled {
model
.supported_reasoning_levels
.retain(|preset| preset.effort != ReasoningEffort::Ultra);
if model.default_reasoning_level == Some(ReasoningEffort::Ultra) {
model.default_reasoning_level = model
.supported_reasoning_levels
.last()
.map(|preset| preset.effort.clone());
}
}
if let Some(supports_reasoning_summaries) = config.model_supports_reasoning_summaries
&& supports_reasoning_summaries
{
@@ -1,7 +1,42 @@
use super::*;
use crate::ModelsManagerConfig;
use codex_protocol::openai_models::ReasoningEffortPreset;
use pretty_assertions::assert_eq;
#[test]
fn ultra_reasoning_requires_feature() {
let mut model = model_info_from_slug("unknown-model");
model.default_reasoning_level = Some(ReasoningEffort::Ultra);
model.supported_reasoning_levels = vec![
ReasoningEffortPreset {
effort: ReasoningEffort::Low,
description: "Low".to_string(),
},
ReasoningEffortPreset {
effort: ReasoningEffort::Ultra,
description: "Ultra".to_string(),
},
];
let mut expected_without_ultra = model.clone();
expected_without_ultra.default_reasoning_level = Some(ReasoningEffort::Low);
expected_without_ultra.supported_reasoning_levels.pop();
assert_eq!(
with_config_overrides(model.clone(), &ModelsManagerConfig::default()),
expected_without_ultra
);
assert_eq!(
with_config_overrides(
model.clone(),
&ModelsManagerConfig {
ultra_reasoning_enabled: true,
..Default::default()
},
),
model
);
}
#[test]
fn reasoning_summaries_override_true_enables_support() {
let model = model_info_from_slug("unknown-model");
@@ -1,6 +1,25 @@
use codex_protocol::openai_models::ModelPreset;
use codex_protocol::openai_models::ReasoningEffort;
/// Legacy notice keys kept for config compatibility with older migration prompts.
///
/// Hardcoded model presets were removed; model listings are now derived from the active catalog.
pub const HIDE_GPT5_1_MIGRATION_PROMPT_CONFIG: &str = "hide_gpt5_1_migration_prompt";
pub const HIDE_GPT_5_1_CODEX_MAX_MIGRATION_PROMPT_CONFIG: &str =
"hide_gpt-5.1-codex-max_migration_prompt";
/// Removes the gated Ultra effort from model presets exposed to clients or tools.
pub fn hide_ultra_reasoning_effort(models: &mut [ModelPreset]) {
for model in models {
model
.supported_reasoning_efforts
.retain(|preset| preset.effort != ReasoningEffort::Ultra);
if model.default_reasoning_effort == ReasoningEffort::Ultra {
model.default_reasoning_effort = model
.supported_reasoning_efforts
.last()
.map(|preset| preset.effort.clone())
.unwrap_or(ReasoningEffort::None);
}
}
}
+8
View File
@@ -45,6 +45,7 @@ pub enum ReasoningEffort {
Medium,
High,
XHigh,
Ultra,
/// A model-defined effort value that this client does not know yet.
Custom(String),
}
@@ -59,6 +60,7 @@ impl ReasoningEffort {
Self::Medium => "medium",
Self::High => "high",
Self::XHigh => "xhigh",
Self::Ultra => "ultra",
Self::Custom(effort) => effort,
}
}
@@ -123,6 +125,7 @@ impl FromStr for ReasoningEffort {
"medium" => Ok(Self::Medium),
"high" => Ok(Self::High),
"xhigh" => Ok(Self::XHigh),
"ultra" => Ok(Self::Ultra),
"" => Err("reasoning_effort must not be empty".to_string()),
effort => Ok(Self::Custom(effort.to_string())),
}
@@ -701,20 +704,25 @@ mod tests {
let deserialized = from_str::<ReasoningEffort>(r#""max""#)
.expect("custom reasoning effort should deserialize");
let serialized = to_string(&custom).expect("custom reasoning effort should serialize");
let serialized_ultra = to_string(&ReasoningEffort::Ultra).expect("Ultra should serialize");
assert_eq!(
(
"high".parse(),
"ultra".parse(),
"max".parse(),
deserialized,
serialized,
serialized_ultra,
custom.to_string(),
),
(
Ok(ReasoningEffort::High),
Ok(ReasoningEffort::Ultra),
Ok(custom.clone()),
custom,
r#""max""#.to_string(),
r#""ultra""#.to_string(),
"max".to_string(),
)
);
+101 -5
View File
@@ -107,6 +107,7 @@ use codex_app_server_protocol::TurnStartResponse;
use codex_app_server_protocol::TurnSteerParams;
use codex_app_server_protocol::TurnSteerResponse;
use codex_app_server_protocol::UserInput;
use codex_features::Feature;
use codex_otel::TelemetryAuthMode;
use codex_protocol::ThreadId;
use codex_protocol::approvals::GuardianAssessmentEvent;
@@ -118,6 +119,7 @@ use codex_protocol::openai_models::ModelAvailabilityNux;
use codex_protocol::openai_models::ModelPreset;
use codex_protocol::openai_models::ModelServiceTier;
use codex_protocol::openai_models::ModelUpgrade;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::openai_models::ReasoningEffortPreset;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathUri;
@@ -278,7 +280,9 @@ impl AppServerSession {
let available_models = models
.data
.into_iter()
.map(model_preset_from_api_model)
.map(|model| {
model_preset_from_api_model(model, config.features.enabled(Feature::MultiAgentMode))
})
.collect::<Vec<_>>();
let default_model = config
.model
@@ -1189,7 +1193,7 @@ pub(crate) fn status_account_display_from_auth_mode(
}
}
fn model_preset_from_api_model(model: ApiModel) -> ModelPreset {
fn model_preset_from_api_model(model: ApiModel, ultra_reasoning_enabled: bool) -> ModelPreset {
let upgrade = model.upgrade.map(|upgrade_id| {
let upgrade_info = model.upgrade_info.clone();
ModelUpgrade {
@@ -1205,7 +1209,7 @@ fn model_preset_from_api_model(model: ApiModel) -> ModelPreset {
}
});
ModelPreset {
let mut preset = ModelPreset {
id: model.id,
model: model.model,
display_name: model.display_name,
@@ -1240,7 +1244,24 @@ fn model_preset_from_api_model(model: ApiModel) -> ModelPreset {
// `model/list` already returns models filtered for the active client/auth context.
supported_in_api: true,
input_modalities: model.input_modalities,
};
if ultra_reasoning_enabled {
preset
.supported_reasoning_efforts
.sort_by_key(|preset| preset.effort == ReasoningEffort::Ultra);
} else {
preset
.supported_reasoning_efforts
.retain(|preset| preset.effort != ReasoningEffort::Ultra);
if preset.default_reasoning_effort == ReasoningEffort::Ultra {
preset.default_reasoning_effort = preset
.supported_reasoning_efforts
.last()
.map(|preset| preset.effort.clone())
.unwrap_or(ReasoningEffort::None);
}
}
preset
}
fn approvals_reviewer_override_from_config(
@@ -1747,7 +1768,6 @@ mod tests {
use codex_app_server_protocol::ThreadStatus;
use codex_app_server_protocol::Turn;
use codex_app_server_protocol::TurnStatus;
use codex_features::Feature;
use codex_protocol::config_types::Personality;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::config_types::ServiceTier;
@@ -1756,7 +1776,6 @@ mod tests {
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_READ_ONLY;
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE;
use codex_protocol::models::ManagedFileSystemPermissions;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxEntry;
@@ -1793,6 +1812,83 @@ mod tests {
}
}
#[test]
fn model_preset_filters_ultra_for_tui_when_feature_is_disabled() {
let api_model = ApiModel {
id: "model-id".to_string(),
model: "model-slug".to_string(),
upgrade: None,
upgrade_info: None,
availability_nux: None,
display_name: "Model".to_string(),
description: "Description".to_string(),
hidden: false,
supported_reasoning_efforts: vec![
codex_app_server_protocol::ReasoningEffortOption {
reasoning_effort: ReasoningEffort::Ultra,
description: "Ultra".to_string(),
},
codex_app_server_protocol::ReasoningEffortOption {
reasoning_effort: ReasoningEffort::Low,
description: "Low".to_string(),
},
codex_app_server_protocol::ReasoningEffortOption {
reasoning_effort: ReasoningEffort::XHigh,
description: "Extra high".to_string(),
},
],
default_reasoning_effort: ReasoningEffort::Ultra,
input_modalities: codex_protocol::openai_models::default_input_modalities(),
supports_personality: false,
additional_speed_tiers: Vec::new(),
service_tiers: Vec::new(),
default_service_tier: None,
is_default: true,
};
let filtered =
model_preset_from_api_model(api_model.clone(), /*ultra_reasoning_enabled*/ false);
let unfiltered =
model_preset_from_api_model(api_model, /*ultra_reasoning_enabled*/ true);
assert_eq!(
(
filtered.default_reasoning_effort,
filtered.supported_reasoning_efforts,
unfiltered.default_reasoning_effort,
unfiltered.supported_reasoning_efforts,
),
(
ReasoningEffort::XHigh,
vec![
ReasoningEffortPreset {
effort: ReasoningEffort::Low,
description: "Low".to_string(),
},
ReasoningEffortPreset {
effort: ReasoningEffort::XHigh,
description: "Extra high".to_string(),
},
],
ReasoningEffort::Ultra,
vec![
ReasoningEffortPreset {
effort: ReasoningEffort::Low,
description: "Low".to_string(),
},
ReasoningEffortPreset {
effort: ReasoningEffort::XHigh,
description: "Extra high".to_string(),
},
ReasoningEffortPreset {
effort: ReasoningEffort::Ultra,
description: "Ultra".to_string(),
},
],
)
);
}
#[test]
fn app_server_rate_limit_snapshots_deduplicates_top_level_limit_from_map() {
let response = GetAccountRateLimitsResponse {
@@ -505,6 +505,7 @@ impl ChatWidget {
ReasoningEffortConfig::Medium => "Medium".to_string(),
ReasoningEffortConfig::High => "High".to_string(),
ReasoningEffortConfig::XHigh => "Extra high".to_string(),
ReasoningEffortConfig::Ultra => "Ultra".to_string(),
ReasoningEffortConfig::Custom(value) => value.clone(),
}
}
@@ -6,7 +6,7 @@ expression: popup
1. Low Fast responses with lighter reasoning
2. Medium (default) Balances speed and reasoning depth for everyday tasks
3. max Maximum available reasoning
3. Ultra Maximum reasoning with proactive delegation
› 4. High (current) Greater reasoning depth for complex problems
5. Extra high Extra high reasoning depth for complex problems
@@ -3018,8 +3018,8 @@ async fn model_reasoning_selection_popup_snapshot() {
preset.supported_reasoning_efforts.insert(
2,
ReasoningEffortPreset {
effort: ReasoningEffortConfig::Custom("max".to_string()),
description: "Maximum available reasoning".to_string(),
effort: ReasoningEffortConfig::Ultra,
description: "Maximum reasoning with proactive delegation".to_string(),
},
);
chat.open_reasoning_popup(preset);