Snapshot resolved settings for each model step (#40651)

## Why

Thread settings can change while a turn is running, and delegated review or
compaction steps can select a different model. Each request needs a consistent
set of settings resolved against the model that will execute it.

## What changed

- Capture immutable resolved settings for each model step, including model
  metadata, reasoning options, service tier, approvals, and personality.
- Keep in-flight steps on their captured settings while applying thread updates
  to subsequent work.
- Resolve inherited reasoning summaries and filter service tiers against the
  selected model and feature configuration for review and compaction steps.

## Testing

Added coverage for settings updates during paused turns, model changes,
previous-model compaction, review-model defaults, and service-tier filtering.

GitOrigin-RevId: fca4b3b305379f1bf26a5cac75ee0dd1747ac1f6
This commit is contained in:
joeflorencio-openai
2026-08-25 17:41:22 +00:00
committed by copyberry
parent ba6cf9c692
commit 68301fa45f
68 changed files with 1393 additions and 516 deletions
+6 -6
View File
@@ -155,7 +155,7 @@ pub(crate) async fn run_compact_task(
trace_id: turn_context.trace_id.clone(),
started_at: turn_context.turn_timing_state.started_at_unix_secs().await,
model_context_window: turn_context.model_context_window(),
collaboration_mode_kind: turn_context.mode,
collaboration_mode_kind: turn_context.mode(),
});
sess.send_event(&turn_context, start_event).await;
run_compact_task_inner(
@@ -257,7 +257,7 @@ async fn run_compact_task_inner_impl(
let mut history = sess.clone_history().await;
history.record_items(
&[initial_input_for_turn.into()],
turn_context.model_info.truncation_policy.into(),
turn_context.model_info().truncation_policy.into(),
);
let max_retries = turn_context.provider.info().stream_max_retries();
@@ -277,7 +277,7 @@ async fn run_compact_task_inner_impl(
// Clone is required because of the loop
let turn_input = history
.clone()
.for_prompt(&turn_context.model_info.input_modalities);
.for_prompt(&turn_context.model_info().input_modalities);
let turn_input_len = turn_input.len();
let prompt = Prompt {
input: turn_input,
@@ -742,10 +742,10 @@ async fn drain_to_completed(
let mut stream = client_session
.stream(
prompt,
&turn_context.model_info,
turn_context.model_info(),
&turn_context.session_telemetry,
turn_context.reasoning_effort.clone(),
turn_context.reasoning_summary,
turn_context.reasoning_effort().cloned(),
turn_context.reasoning_summary(),
turn_context.config.service_tier.clone(),
responses_metadata,
// Rollout tracing currently models remote compaction only; local compaction streams
+5 -5
View File
@@ -90,7 +90,7 @@ pub(crate) async fn run_remote_compact_task(
trace_id: turn_context.trace_id.clone(),
started_at: turn_context.turn_timing_state.started_at_unix_secs().await,
model_context_window: turn_context.model_context_window(),
collaboration_mode_kind: turn_context.mode,
collaboration_mode_kind: turn_context.mode(),
});
sess.send_event(&turn_context, start_event).await;
@@ -206,7 +206,7 @@ async fn run_remote_compact_task_inner_impl(
let compaction_trace = sess.services.rollout_thread_trace.compaction_trace_context(
turn_context.sub_id.as_str(),
compaction_id.as_str(),
turn_context.model_info.slug.as_str(),
turn_context.model_info().slug.as_str(),
turn_context.provider.info().name.as_str(),
);
let compaction_item = TurnItem::ContextCompaction(context_compaction_item);
@@ -235,7 +235,7 @@ async fn run_remote_compact_task_inner_impl(
sess.services.rollout_thread_trace.compaction_trace_context(
fallback_turn_context.sub_id.as_str(),
compaction_id.as_str(),
fallback_turn_context.model_info.slug.as_str(),
fallback_turn_context.model_info().slug.as_str(),
fallback_turn_context.provider.info().name.as_str(),
);
let fallback_result = run_remote_compact_attempt(
@@ -249,8 +249,8 @@ async fn run_remote_compact_task_inner_impl(
.await;
record_model_fallback(
&sess.services.session_telemetry,
turn_context.model_info.slug.as_str(),
fallback_turn_context.model_info.slug.as_str(),
turn_context.model_info().slug.as_str(),
fallback_turn_context.model_info().slug.as_str(),
compaction_metadata.reason(),
compaction_metadata.implementation(),
fallback_result.as_ref().err(),
+4 -4
View File
@@ -58,7 +58,7 @@ pub(super) async fn run_remote_compact_attempt(
let trace_input_history = compaction_trace
.is_enabled()
.then(|| history.raw_items().cloned().collect());
let prompt_input = history.for_prompt(&turn_context.model_info.input_modalities);
let prompt_input = history.for_prompt(&turn_context.model_info().input_modalities);
let tool_router = &step_context.tool_router;
let prompt = Prompt {
input: prompt_input,
@@ -79,11 +79,11 @@ pub(super) async fn run_remote_compact_attempt(
.model_client
.compact_conversation_history(
&prompt,
&turn_context.model_info,
turn_context.model_info(),
turn_state,
CompactConversationRequestSettings {
effort: turn_context.reasoning_effort.clone(),
summary: turn_context.reasoning_summary,
effort: turn_context.reasoning_effort().cloned(),
summary: turn_context.reasoning_summary(),
service_tier: if sess.services.auth_manager.auth_mode() == Some(AuthMode::ApiKey) {
None
} else {
+8 -8
View File
@@ -118,7 +118,7 @@ pub(crate) async fn run_remote_compact_task(
trace_id: turn_context.trace_id.clone(),
started_at: turn_context.turn_timing_state.started_at_unix_secs().await,
model_context_window: turn_context.model_context_window(),
collaboration_mode_kind: turn_context.mode,
collaboration_mode_kind: turn_context.mode(),
});
sess.send_event(&turn_context, start_event).await;
@@ -234,7 +234,7 @@ async fn run_remote_compact_task_inner_impl(
let compaction_trace = sess.services.rollout_thread_trace.compaction_trace_context(
turn_context.sub_id.as_str(),
compaction_id.as_str(),
turn_context.model_info.slug.as_str(),
turn_context.model_info().slug.as_str(),
turn_context.provider.info().name.as_str(),
);
let compaction_item = TurnItem::ContextCompaction(context_compaction_item);
@@ -264,7 +264,7 @@ async fn run_remote_compact_task_inner_impl(
sess.services.rollout_thread_trace.compaction_trace_context(
fallback_turn_context.sub_id.as_str(),
compaction_id.as_str(),
fallback_turn_context.model_info.slug.as_str(),
fallback_turn_context.model_info().slug.as_str(),
fallback_turn_context.provider.info().name.as_str(),
);
let fallback_result = run_remote_compact_v2_attempt(
@@ -278,8 +278,8 @@ async fn run_remote_compact_task_inner_impl(
.await;
record_model_fallback(
&sess.services.session_telemetry,
turn_context.model_info.slug.as_str(),
fallback_turn_context.model_info.slug.as_str(),
turn_context.model_info().slug.as_str(),
fallback_turn_context.model_info().slug.as_str(),
compaction_metadata.reason(),
compaction_metadata.implementation(),
fallback_result.as_ref().err(),
@@ -380,10 +380,10 @@ async fn run_remote_compaction_request_v2(
let result = match client_session
.stream(
prompt,
&turn_context.model_info,
turn_context.model_info(),
&turn_context.session_telemetry,
turn_context.reasoning_effort.clone(),
turn_context.reasoning_summary,
turn_context.reasoning_effort().cloned(),
turn_context.reasoning_summary(),
turn_context.config.service_tier.clone(),
responses_metadata,
&InferenceTraceContext::disabled(),
@@ -69,7 +69,7 @@ pub(super) async fn run_remote_compact_v2_attempt(
.is_enabled()
.then(|| history.raw_items().cloned().collect());
let (mut input, prompt_input_metadata): (Vec<_>, Vec<_>) = history
.for_prompt_annotated(&turn_context.model_info.input_modalities)
.for_prompt_annotated(&turn_context.model_info().input_modalities)
.into_iter()
.map(|envelope| (envelope.item, envelope.metadata))
.unzip();
@@ -91,7 +91,7 @@ pub(super) async fn run_remote_compact_v2_attempt(
)
.await;
let trace_attempt = compaction_trace.start_attempt(&serde_json::json!({
"model": turn_context.model_info.slug.as_str(),
"model": turn_context.model_info().slug.as_str(),
"instructions": prompt.base_instructions.text.as_str(),
"input": &prompt.input,
"parallel_tool_calls": prompt.parallel_tool_calls,
+1 -1
View File
@@ -32,7 +32,7 @@ pub(crate) async fn run_manual_compact_task(
trace_id: turn_context.trace_id.clone(),
started_at: turn_context.turn_timing_state.started_at_unix_secs().await,
model_context_window: turn_context.model_context_window(),
collaboration_mode_kind: turn_context.mode,
collaboration_mode_kind: turn_context.mode(),
});
sess.send_event(&turn_context, start_event).await;
@@ -31,7 +31,7 @@ pub(crate) enum NodeReplReviewEvidenceMode {
pub(crate) fn node_repl_review_evidence_mode(turn: &TurnContext) -> NodeReplReviewEvidenceMode {
let features = &turn.config.features;
if turn.model_info.node_repl_auto_review_required
if turn.model_info().node_repl_auto_review_required
|| features.enabled(Feature::GuardianEnhancedNodeReplTranscripts)
&& features.enabled(Feature::GuardianNodeReplTranscriptImages)
{
+4 -2
View File
@@ -254,8 +254,10 @@ impl ContextManager {
// Estimate token usage using byte-based heuristics from the truncation helpers.
// This is a coarse lower bound, not a tokenizer-accurate count.
pub(crate) fn estimate_token_count(&self, turn_context: &TurnContext) -> Option<i64> {
let model_info = &turn_context.model_info;
let personality = turn_context.personality.or(turn_context.config.personality);
let model_info = &turn_context.model_info();
let personality = turn_context
.personality()
.or(turn_context.config.personality);
let base_instructions = BaseInstructions {
text: model_info.get_model_instructions(personality),
provenance: None,
+13 -13
View File
@@ -249,7 +249,7 @@ async fn record_guardian_non_denial(session: &Arc<Session>, turn_id: &str) {
}
async fn record_guardian_denial(session: &Arc<Session>, turn: &Arc<TurnContext>, turn_id: &str) {
let policy = if turn.model_info.model_specialty.as_deref() == Some(MODEL_SPECIALTY_CYBER) {
let policy = if turn.model_info().model_specialty.as_deref() == Some(MODEL_SPECIALTY_CYBER) {
GuardianRejectionCircuitBreakerPolicy::CyberModel
} else {
GuardianRejectionCircuitBreakerPolicy::Standard
@@ -335,7 +335,7 @@ async fn run_guardian_review(
.config
.config_layer_stack
.requirements()
.auto_review_required_for_model(&turn.model_info.slug)
.auto_review_required_for_model(&turn.model_info().slug)
|| turn.config.features.enabled(Feature::GuardianV2))
&& !requires_synchronous_review
&& options
@@ -722,7 +722,7 @@ async fn run_guardian_review(
assessment.rationale.trim()
};
let rejection_instructions = turn
.model_info
.model_info()
.model_messages
.as_ref()
.and_then(|messages| messages.auto_review.as_ref())
@@ -847,7 +847,7 @@ pub(super) async fn guardian_review_session_config(
fallback
}
};
let model_override = turn.model_info.auto_review_model_override.as_deref();
let model_override = turn.model_info().auto_review_model_override.as_deref();
let review_model_id = model_override.unwrap_or(default_review_model_id);
let review_model = available_models
.iter()
@@ -868,17 +868,17 @@ pub(super) async fn guardian_review_session_config(
(review_model_id.to_string(), reasoning_effort)
} else {
let reasoning_effort = preferred_reasoning_effort(
turn.model_info
turn.model_info()
.supported_reasoning_levels
.iter()
.any(|preset| preset.effort == codex_protocol::openai_models::ReasoningEffort::Low),
turn.reasoning_effort
.clone()
.or_else(|| turn.model_info.default_reasoning_level.clone()),
turn.reasoning_effort()
.or(turn.model_info().default_reasoning_level.as_ref())
.cloned(),
);
(
model_override
.unwrap_or(turn.model_info.slug.as_str())
.unwrap_or(turn.model_info().slug.as_str())
.to_string(),
reasoning_effort,
)
@@ -899,7 +899,7 @@ pub(super) async fn guardian_review_session_config(
guardian_reasoning_effort.clone(),
guardian_model_info.model_messages.as_ref(),
)?;
if turn.model_info.node_repl_auto_review_required {
if turn.model_info().node_repl_auto_review_required {
spawn_config
.features
.enable(Feature::RetainClientDeveloperMessages)
@@ -909,7 +909,7 @@ pub(super) async fn guardian_review_session_config(
)
})?;
}
if guardian_model != turn.model_info.slug {
if guardian_model != turn.model_info().slug {
spawn_config.model_context_window = None;
spawn_config.model_auto_compact_token_limit = None;
}
@@ -974,8 +974,8 @@ async fn run_guardian_review_session_before_deadline(
guardian_catalog_contains_auto_review: session_config.catalog_contains_auto_review,
guardian_review_model_overridden: session_config.model_overridden,
guardian_review_model_override: session_config.model_override,
reasoning_summary: turn.reasoning_summary,
personality: turn.personality,
reasoning_summary: turn.reasoning_summary(),
personality: turn.personality(),
external_cancel,
deadline,
}),
+7 -7
View File
@@ -438,7 +438,7 @@ impl GuardianReviewSessionManager {
.with_node_repl_policy_eligibility(
parent_context
.turn()
.model_info
.model_info()
.node_repl_auto_review_required,
);
let spawn_cancel_token = self.cancellation_token.child_token();
@@ -528,7 +528,7 @@ impl GuardianReviewSessionManager {
params
.parent_context
.turn()
.model_info
.model_info()
.node_repl_auto_review_required,
);
let mut spawned_trunk = false;
@@ -952,7 +952,7 @@ async fn run_review_on_session(
.sync_session_approved_hosts_to(&review_session.session.services.network_approval)
.await;
if params.parent_context.turn().model_info.node_repl_auto_review_required
if params.parent_context.turn().model_info().node_repl_auto_review_required
&& matches!(
&params.request,
GuardianApprovalRequest::McpToolCall { server, tool_name, .. }
@@ -1607,10 +1607,10 @@ mod tests {
async fn test_review_params() -> GuardianReviewSessionParams {
let (session, turn) = crate::session::tests::make_session_and_context().await;
let model = turn.model_info.slug.clone();
let reasoning_effort = turn.reasoning_effort.clone();
let reasoning_summary = turn.reasoning_summary;
let personality = turn.personality;
let model = turn.model_info().slug.clone();
let reasoning_effort = turn.reasoning_effort().cloned();
let reasoning_summary = turn.reasoning_summary();
let personality = turn.personality();
#[allow(deprecated)]
let cwd = turn.cwd.clone();
let spawn_config = build_guardian_review_session_config(
+23 -19
View File
@@ -14,6 +14,7 @@ use crate::guardian::prompt::guardian_policy_prompt_with_config_and_template;
use crate::guardian::review::guardian_review_session_config;
use crate::guardian::review::routes_approval_to_guardian_with_reviewer;
use crate::session::session::Session;
use crate::session::tests::update_turn_settings_for_test;
use crate::session::turn_context::TurnContext;
use crate::test_support;
use codex_analytics::GuardianApprovalRequestSource;
@@ -1185,12 +1186,10 @@ async fn build_guardian_prompt_items_keeps_required_node_repl_reviews_generic()
{
let (session, mut turn) =
guardian_test_session_and_turn_with_base_url("http://localhost").await;
Arc::make_mut(
&mut Arc::get_mut(&mut turn)
.expect("turn should be uniquely owned")
.model_info,
)
.node_repl_auto_review_required = true;
update_turn_settings_for_test(
Arc::get_mut(&mut turn).expect("turn should be uniquely owned"),
|settings| Arc::make_mut(&mut settings.model_info).node_repl_auto_review_required = true,
);
seed_guardian_parent_history(&session, &turn).await;
let context = GuardianReviewContext::from(&turn);
@@ -1810,7 +1809,7 @@ async fn guardian_request_model_for_auto_review(
match catalog {
GuardianTestCatalog::Bundled => {}
GuardianTestCatalog::ParentOnly => {
let parent_model = turn.model_info.as_ref().clone();
let parent_model = turn.model_info().as_ref().clone();
let auth_manager = Arc::clone(&session.services.auth_manager);
let models_manager = StaticModelsManager::new(
Some(auth_manager),
@@ -1824,13 +1823,14 @@ async fn guardian_request_model_for_auto_review(
.models_manager = Arc::new(models_manager);
}
}
Arc::make_mut(
&mut Arc::get_mut(&mut turn)
.expect("turn should be unique")
.model_info,
)
.auto_review_model_override = auto_review_model_override;
let parent_model = turn.model_info.slug.clone();
update_turn_settings_for_test(
Arc::get_mut(&mut turn).expect("turn should be unique"),
|settings| {
Arc::make_mut(&mut settings.model_info).auto_review_model_override =
auto_review_model_override
},
);
let parent_model = turn.model_info().slug.clone();
let preferred_model = turn.provider.approval_review_preferred_model().to_string();
let parent_turn_id = turn.sub_id.clone();
seed_guardian_parent_history(&session, &turn).await;
@@ -2055,8 +2055,10 @@ async fn guardian_review_request_layout_matches_model_visible_request_snapshot()
session.services.skills_service.clear_cache();
turn.config = Arc::clone(&config);
turn.provider = create_model_provider(config.model_provider.clone(), turn.auth_manager.clone());
Arc::make_mut(&mut turn.model_info).auto_review_model_override =
Some("codex-auto-review".to_string());
update_turn_settings_for_test(&mut turn, |settings| {
Arc::make_mut(&mut settings.model_info).auto_review_model_override =
Some("codex-auto-review".to_string());
});
let session = Arc::new(session);
let turn = Arc::new(turn);
seed_guardian_parent_history(&session, &turn).await;
@@ -2330,8 +2332,10 @@ async fn guardian_reuses_prompt_cache_key_and_appends_prior_reviews() -> anyhow:
.enable(Feature::GuardianReuseParentCompaction)
.expect("Guardian parent-compaction reuse should be configurable");
let turn_mut = Arc::get_mut(&mut turn).expect("turn should be unique");
Arc::make_mut(&mut turn_mut.model_info).auto_review_model_override =
Some("codex-auto-review".to_string());
update_turn_settings_for_test(turn_mut, |settings| {
Arc::make_mut(&mut settings.model_info).auto_review_model_override =
Some("codex-auto-review".to_string());
});
turn_mut.config = Arc::new(config);
seed_guardian_parent_history(&session, &turn).await;
@@ -3542,7 +3546,7 @@ async fn guardian_review_session_config_clears_context_overrides_for_distinct_ef
async fn guardian_review_session_config_preserves_context_overrides_for_same_effective_model() {
let server = start_mock_server().await;
let (mut session, mut turn) = guardian_test_session_and_turn(&server).await;
let parent_model = turn.model_info.as_ref().clone();
let parent_model = turn.model_info().as_ref().clone();
let auth_manager = Arc::clone(&session.services.auth_manager);
Arc::get_mut(&mut session)
.expect("session should be unique")
+13 -13
View File
@@ -145,7 +145,7 @@ pub(crate) async fn run_pending_session_start_hooks(
#[allow(deprecated)]
cwd: turn_context.cwd.clone(),
transcript_path: sess.hook_transcript_path().await,
model: turn_context.model_info.slug.clone(),
model: turn_context.model_info().slug.clone(),
permission_mode: hook_permission_mode(turn_context),
target,
};
@@ -187,7 +187,7 @@ pub(crate) async fn run_pre_tool_use_hooks(
#[allow(deprecated)]
cwd: turn_context.cwd.clone(),
transcript_path: sess.hook_transcript_path().await,
model: turn_context.model_info.slug.clone(),
model: turn_context.model_info().slug.clone(),
permission_mode: hook_permission_mode(turn_context),
tool_name: tool_name.name().to_string(),
matcher_aliases: tool_name.matcher_aliases().to_vec(),
@@ -248,7 +248,7 @@ pub(crate) async fn run_permission_request_hooks(
#[allow(deprecated)]
cwd: turn_context.cwd.to_path_buf(),
transcript_path: sess.hook_transcript_path().await,
model: turn_context.model_info.slug.clone(),
model: turn_context.model_info().slug.clone(),
permission_mode: hook_permission_mode(turn_context),
tool_name: payload.tool_name.name().to_string(),
matcher_aliases: payload.tool_name.matcher_aliases().to_vec(),
@@ -290,7 +290,7 @@ pub(crate) async fn run_post_tool_use_hooks(
#[allow(deprecated)]
cwd: turn_context.cwd.clone(),
transcript_path: sess.hook_transcript_path().await,
model: turn_context.model_info.slug.clone(),
model: turn_context.model_info().slug.clone(),
permission_mode: hook_permission_mode(turn_context),
tool_name,
matcher_aliases,
@@ -366,8 +366,8 @@ pub(crate) async fn run_turn_stop_hooks(
let request_metadata = turn_context
.turn_metadata_state
.current_meta_value_for_mcp_request(McpTurnMetadataContext {
model: step_context.model_info.slug.as_str(),
reasoning_effort: step_context.reasoning_effort.clone(),
model: step_context.settings.model_info.slug.as_str(),
reasoning_effort: step_context.settings.reasoning_effort().cloned(),
})
.map(|turn_metadata| {
serde_json::Map::from_iter([(
@@ -381,7 +381,7 @@ pub(crate) async fn run_turn_stop_hooks(
#[allow(deprecated)]
cwd: turn_context.cwd.clone(),
transcript_path,
model: turn_context.model_info.slug.clone(),
model: turn_context.model_info().slug.clone(),
permission_mode: hook_permission_mode(turn_context),
request_metadata,
stop_hook_active,
@@ -450,7 +450,7 @@ pub(crate) async fn run_turn_interrupt_hooks(sess: &Arc<Session>, turn_context:
#[allow(deprecated)]
cwd: turn_context.cwd.clone(),
transcript_path: sess.hook_transcript_path().await,
model: turn_context.model_info.slug.clone(),
model: turn_context.model_info().slug.clone(),
permission_mode: hook_permission_mode(turn_context),
};
if let Err(err) = sess.flush_rollout().await {
@@ -474,7 +474,7 @@ pub(crate) async fn run_pre_compact_hooks(
#[allow(deprecated)]
cwd: turn_context.cwd.clone(),
transcript_path: sess.hook_transcript_path().await,
model: turn_context.model_info.slug.clone(),
model: turn_context.model_info().slug.clone(),
trigger: compaction_trigger_label(trigger).to_string(),
};
let preview_runs = sess.hooks().preview_pre_compact(&request);
@@ -511,7 +511,7 @@ pub(crate) async fn run_post_compact_hooks(
#[allow(deprecated)]
cwd: turn_context.cwd.clone(),
transcript_path: sess.hook_transcript_path().await,
model: turn_context.model_info.slug.clone(),
model: turn_context.model_info().slug.clone(),
trigger: compaction_trigger_label(trigger).to_string(),
};
let preview_runs = sess.hooks().preview_post_compact(&request);
@@ -608,7 +608,7 @@ pub(crate) async fn inspect_pending_input(
#[allow(deprecated)]
cwd: turn_context.cwd.clone(),
transcript_path: sess.hook_transcript_path().await,
model: turn_context.model_info.slug.clone(),
model: turn_context.model_info().slug.clone(),
permission_mode: hook_permission_mode(turn_context),
prompt: UserMessageItem::new(content).message(),
};
@@ -845,7 +845,7 @@ fn hook_run_analytics_payload(
) -> (codex_analytics::TrackEventsContext, HookRunFact) {
(
build_track_events_context(
turn_context.model_info.slug.clone(),
turn_context.model_info().slug.clone(),
thread_id,
completed
.turn_id
@@ -1071,7 +1071,7 @@ mod tests {
assert_eq!(tracking.thread_id, "thread-123");
assert_eq!(tracking.turn_id, "turn-from-hook");
assert_eq!(tracking.model_slug, turn_context.model_info.slug);
assert_eq!(tracking.model_slug, turn_context.model_info().slug);
assert_eq!(hook.event_name, HookEventName::Stop);
assert_eq!(hook.handler_type, HookHandlerType::Command);
assert_eq!(hook.execution_mode, HookExecutionMode::Sync);
+6 -6
View File
@@ -287,7 +287,7 @@ pub(crate) async fn handle_mcp_tool_call(
&call_id,
invocation,
item_metadata.clone(),
crate::guardian::guardian_timeout_message(&turn_context.model_info),
crate::guardian::guardian_timeout_message(turn_context.model_info()),
/*already_started*/ true,
)
.await
@@ -449,7 +449,7 @@ async fn handle_approved_mcp_tool_call(
.map(|(connector_id, action_name)| HostedFileUploadContext {
connector_id: connector_id.clone(),
action_name: action_name.clone(),
model: turn_context.model_info.slug.clone(),
model: turn_context.model_info().slug.clone(),
});
let rewritten_arguments = rewrite_mcp_tool_arguments_for_openai_files(
sess,
@@ -491,7 +491,7 @@ async fn handle_approved_mcp_tool_call(
.await
.map_err(|error| format!("tool call error: {error:?}"))?;
let result = sanitize_mcp_tool_result_for_model(
&turn_context.model_info.input_modalities,
&turn_context.model_info().input_modalities,
Ok(result),
)?;
Ok(maybe_request_codex_apps_auth_elicitation(
@@ -1023,7 +1023,7 @@ async fn maybe_track_codex_app_used(
};
let tracking = build_track_events_context(
turn_context.model_info.slug.clone(),
turn_context.model_info().slug.clone(),
sess.thread_id.to_string(),
turn_context.sub_id.clone(),
turn_context.originator.clone(),
@@ -1193,7 +1193,7 @@ fn build_mcp_tool_call_request_meta(
if let Some(turn_metadata) = turn_context
.turn_metadata_state
.current_meta_value_for_mcp_request(McpTurnMetadataContext {
model: turn_context.model_info.slug.as_str(),
model: turn_context.model_info().slug.as_str(),
reasoning_effort: turn_context.effective_reasoning_effort(),
})
{
@@ -1317,7 +1317,7 @@ async fn maybe_request_mcp_tool_approval(
let approvals_reviewer = connectors::mcp_approvals_reviewer_from_layers(
&config.config_layer_stack,
config.approvals_reviewer,
Some(turn_context.model_info.slug.as_str()),
Some(turn_context.model_info().slug.as_str()),
&invocation.server,
metadata.connector_id.as_deref(),
);
+6 -6
View File
@@ -93,7 +93,7 @@ fn approval_config(turn_context: &TurnContext) -> codex_mcp::McpConfig {
fn mcp_turn_metadata_context(turn_context: &TurnContext) -> McpTurnMetadataContext<'_> {
McpTurnMetadataContext {
model: turn_context.model_info.slug.as_str(),
model: turn_context.model_info().slug.as_str(),
reasoning_effort: turn_context.effective_reasoning_effort(),
}
}
@@ -1038,15 +1038,15 @@ async fn mcp_tool_call_request_meta_includes_turn_metadata_for_custom_server() {
turn_metadata
.get("model")
.and_then(serde_json::Value::as_str),
Some(turn_context.model_info.slug.as_str())
Some(turn_context.model_info().slug.as_str())
);
assert_eq!(
turn_metadata["node_repl_auto_review_required"],
serde_json::Value::Bool(turn_context.model_info.node_repl_auto_review_required),
serde_json::Value::Bool(turn_context.model_info().node_repl_auto_review_required),
);
assert_eq!(
turn_metadata["node_repl_disabled"],
serde_json::Value::Bool(turn_context.model_info.node_repl_disabled),
serde_json::Value::Bool(turn_context.model_info().node_repl_disabled),
);
assert_eq!(
turn_metadata
@@ -2496,7 +2496,7 @@ async fn permission_request_hook_allows_mcp_tool_call() {
"turn_id": "turn_id",
"cwd": turn_cwd,
"transcript_path": null,
"model": turn_context.model_info.slug,
"model": turn_context.model_info().slug,
"permission_mode": "default",
"tool_name": "mcp__memory__create_entities",
"hook_event_name": "PermissionRequest",
@@ -2566,7 +2566,7 @@ async fn permission_request_hook_uses_hook_tool_name_without_metadata() {
"turn_id": "turn_id",
"cwd": turn_cwd,
"transcript_path": null,
"model": turn_context.model_info.slug,
"model": turn_context.model_info().slug,
"permission_mode": "default",
"tool_name": "mcp__memory__create_entities",
"hook_event_name": "PermissionRequest",
+1 -1
View File
@@ -101,7 +101,7 @@ pub(crate) async fn build_prompt_input_from_session(
let prompt_input = sess
.clone_history()
.await
.for_prompt(&step_context.model_info.input_modalities);
.for_prompt(&step_context.settings.model_info.input_modalities);
let base_instructions = sess.get_base_instructions().await;
let prompt = build_prompt(prompt_input, step_context.as_ref(), base_instructions);
+1 -1
View File
@@ -85,7 +85,7 @@ pub(crate) async fn handle_retryable_response_stream_error(
if retry_state.retries >= max_retries
&& client_session.try_switch_fallback_transport(
&turn_context.session_telemetry,
&turn_context.model_info,
turn_context.model_info(),
)
{
sess.send_event(
+2 -2
View File
@@ -31,7 +31,7 @@ pub(crate) async fn context_window_token_status(
match turn_context.config.model_auto_compact_token_limit_scope {
AutoCompactTokenLimitScope::Total => (
active_context_tokens,
turn_context.model_info.auto_compact_token_limit(),
turn_context.model_info().auto_compact_token_limit(),
None,
),
AutoCompactTokenLimitScope::BodyAfterPrefix => {
@@ -41,7 +41,7 @@ pub(crate) async fn context_window_token_status(
let scope_limit = turn_context
.config
.model_auto_compact_token_limit
.or_else(|| turn_context.model_info.auto_compact_token_limit());
.or_else(|| turn_context.model_info().auto_compact_token_limit());
(
active_context_tokens.saturating_sub(baseline),
scope_limit,
+3 -8
View File
@@ -119,12 +119,8 @@ pub async fn run_user_shell_command(sess: &Arc<Session>, sub_id: String, command
}
let turn_context = sess.new_default_turn_with_sub_id(sub_id).await;
sess.spawn_task(
Arc::clone(&turn_context),
Vec::new(),
UserShellCommandTask::new(command),
)
.await;
sess.spawn_task(turn_context, Vec::new(), UserShellCommandTask::new(command))
.await;
}
pub async fn resolve_elicitation(
@@ -243,8 +239,7 @@ pub async fn reload_user_config(sess: &Arc<Session>) {
pub async fn compact(sess: &Arc<Session>, sub_id: String) {
let turn_context = sess.new_default_turn_with_sub_id(sub_id).await;
sess.spawn_task(Arc::clone(&turn_context), Vec::new(), CompactTask)
.await;
sess.spawn_task(turn_context, Vec::new(), CompactTask).await;
}
pub async fn thread_rollback(sess: &Arc<Session>, sub_id: String, num_turns: u32) {
+4 -4
View File
@@ -766,7 +766,7 @@ async fn review_guardian_mcp_elicitation(
|| crate::connectors::mcp_approvals_reviewer_from_layers(
&mcp_config.config_layer_stack,
ApprovalsReviewer::AutoReview,
Some(turn_context.model_info.slug.as_str()),
Some(turn_context.model_info().slug.as_str()),
request.server_name.as_str(),
connector_id,
) != ApprovalsReviewer::AutoReview
@@ -806,7 +806,7 @@ async fn review_guardian_mcp_elicitation(
| ReviewDecision::Abort
)
.then(|| {
mcp_elicitation_response_from_guardian_decision(decision, &turn_context.model_info)
mcp_elicitation_response_from_guardian_decision(decision, turn_context.model_info())
}));
}
@@ -844,7 +844,7 @@ async fn review_guardian_mcp_elicitation(
let approvals_reviewer = crate::connectors::mcp_approvals_reviewer_from_layers(
&mcp_config.config_layer_stack,
mcp_config.approvals_reviewer,
Some(turn_context.model_info.slug.as_str()),
Some(turn_context.model_info().slug.as_str()),
request.server_name.as_str(),
elicitation_connector_id(&request.elicitation),
);
@@ -882,7 +882,7 @@ async fn review_guardian_mcp_elicitation(
.await;
Ok(Some(mcp_elicitation_response_from_guardian_decision(
decision,
&turn_context.model_info,
turn_context.model_info(),
)))
}
+15 -13
View File
@@ -1379,7 +1379,7 @@ impl Session {
.await;
// If resuming, warn when the last recorded model differs from the current one.
let curr: &str = turn_context.model_info.slug.as_str();
let curr: &str = turn_context.model_info().slug.as_str();
if let Some(prev) = previous_turn_settings
.as_ref()
.map(|settings| settings.model.as_str())
@@ -3069,7 +3069,7 @@ impl Session {
let mut items = items.to_vec();
let image_preparation_mode = if unified_image_budget_enabled(
&turn_context.config.features,
&turn_context.model_info,
turn_context.model_info(),
) {
ImagePreparationMode::UnifiedBudget
} else {
@@ -3202,7 +3202,7 @@ impl Session {
.note_recorded_items(&response_items);
state
.history
.record_annotated_items(&items, turn_context.model_info.truncation_policy.into());
.record_annotated_items(&items, turn_context.model_info().truncation_policy.into());
}
for image in image_preparations {
self.services
@@ -3286,6 +3286,10 @@ impl Session {
cancellation_token: &CancellationToken,
required_servers: &[String],
) -> CodexResult<Arc<StepContext>> {
// Capture once before asynchronous planning; all request consumers
// retain this immutable settings version even if the turn is updated.
let settings = turn_context.current_settings.load_full();
let session_telemetry = settings.telemetry(&turn_context.session_telemetry);
// Keep selections fixed for the turn while allowing their startup work to finish.
let environments = turn_context.environments.refresh_readiness();
self.services
@@ -3354,6 +3358,8 @@ impl Session {
});
extension_data.insert(selected_plugins.clone());
turn_context.extension_data.insert(selected_plugins);
// Tool planning still uses the admitted turn. Migrating it to the
// captured model is a separate step from diagnostic activation.
let tool_router = turn::built_tools(
self.as_ref(),
turn_context.as_ref(),
@@ -3365,13 +3371,8 @@ impl Session {
.or_cancel(cancellation_token)
.await??;
Ok(Arc::new(StepContext {
model_info: Arc::clone(&turn_context.model_info),
reasoning_effort: turn_context.reasoning_effort.clone(),
reasoning_summary: turn_context.reasoning_summary,
service_tier: turn_context.config.service_tier.clone(),
approval_policy: turn_context.approval_policy(),
approvals_reviewer: turn_context.config.approvals_reviewer,
session_telemetry: turn_context.session_telemetry.clone(),
settings,
session_telemetry,
turn: turn_context,
environments,
selected_capability_roots,
@@ -3399,7 +3400,7 @@ impl Session {
state.current_time_reminder.note_recorded_items(items);
state.record_items(
items.iter(),
turn_context.model_info.truncation_policy.into(),
turn_context.model_info().truncation_policy.into(),
);
}
self.persist_rollout_items(&[
@@ -3414,10 +3415,11 @@ impl Session {
async fn maybe_warn_on_server_model_mismatch(
self: &Arc<Self>,
turn_context: &Arc<TurnContext>,
step_context: &StepContext,
server_model: String,
) -> bool {
let requested_model = turn_context.model_info.slug.clone();
let turn_context = &step_context.turn;
let requested_model = step_context.settings.model_info.slug.clone();
let server_model_normalized = server_model.to_ascii_lowercase();
let requested_model_normalized = requested_model.to_ascii_lowercase();
if server_model_normalized == requested_model_normalized {
+2 -2
View File
@@ -73,7 +73,7 @@ pub(super) fn usage_hint_text(
}
let catalog = turn_context
.model_info
.model_info()
.model_messages
.as_ref()
.and_then(|messages| messages.multi_agent.as_ref())
@@ -148,7 +148,7 @@ pub(crate) fn effective_multi_agent_mode(turn_context: &TurnContext) -> Option<M
}
let catalog_mode = turn_context
.model_info
.model_info()
.model_messages
.as_ref()
.and_then(|messages| messages.multi_agent.as_ref())
+27 -25
View File
@@ -1,4 +1,6 @@
use super::step_settings::ResolvedStepSettings;
use super::*;
use arc_swap::ArcSwap;
use std::sync::atomic::AtomicBool;
/// Spawn a review thread using the given prompt.
@@ -12,7 +14,7 @@ pub(super) async fn spawn_review_thread(
let model = config
.review_model
.clone()
.unwrap_or_else(|| parent_turn_context.model_info.slug.clone());
.unwrap_or_else(|| parent_turn_context.model_info().slug.clone());
let available_models = sess
.services
.models_manager
@@ -43,15 +45,16 @@ pub(super) async fn spawn_review_thread(
let provider = parent_turn_context.provider.clone();
let auth_manager = parent_turn_context.auth_manager.clone();
let model_info = review_model_info.clone();
let mut selected = parent_turn_context.initial_settings.selected().clone();
let mut reasoning_effort = selected.collaboration_mode.reasoning_effort();
// Build per‑turn client with the requested model/family.
let mut per_turn_config = (*parent_turn_context.config).clone();
// Preserve configured overrides without carrying over the parent model's defaults.
per_turn_config.token_budget = config.token_budget.clone();
per_turn_config.model = Some(model.clone());
per_turn_config.features = review_features.clone();
if let Some(current_effort) = per_turn_config.model_reasoning_effort.as_ref()
&& review_model_info.slug != parent_turn_context.model_info.slug
if let Some(current_effort) = reasoning_effort.as_ref()
&& review_model_info.slug != parent_turn_context.model_info().slug
&& !review_model_info.used_fallback_model_metadata
&& !review_model_info
.supported_reasoning_levels
@@ -59,7 +62,7 @@ pub(super) async fn spawn_review_thread(
.any(|preset| &preset.effort == current_effort)
{
let supported_reasoning_levels = &review_model_info.supported_reasoning_levels;
per_turn_config.model_reasoning_effort = supported_reasoning_levels
reasoning_effort = supported_reasoning_levels
.get(supported_reasoning_levels.len().saturating_sub(1) / 2)
.map(|preset| preset.effort.clone())
.or_else(|| review_model_info.default_reasoning_level.clone());
@@ -74,17 +77,8 @@ pub(super) async fn spawn_review_thread(
);
}
let session_telemetry = parent_turn_context
.session_telemetry
.clone()
.with_model(model.as_str(), review_model_info.slug.as_str());
let auth_manager_for_context = auth_manager.clone();
let provider_for_context = provider.clone();
let session_telemetry_for_context = session_telemetry.clone();
let reasoning_effort = per_turn_config.model_reasoning_effort.clone();
let reasoning_summary = per_turn_config
.model_reasoning_summary
.unwrap_or(model_info.default_reasoning_summary);
let session_source = parent_turn_context.session_source.clone();
let (forked_from_thread_id, thread_source, service_tier) = {
let state = sess.state.lock().await;
@@ -99,12 +93,25 @@ pub(super) async fn spawn_review_thread(
.or_else(|| config.service_tier.clone()),
)
};
per_turn_config.service_tier = service_tier;
let auto_review_enabled = crate::guardian::routes_approval_policy_to_guardian(
per_turn_config.permissions.approval_policy.value(),
per_turn_config.approvals_reviewer,
);
// The original config may retain a requested tier unsupported by the parent
// model. Resolve it against the review model and the shared feature gate.
selected.collaboration_mode.settings.model = model.clone();
selected.collaboration_mode.settings.reasoning_effort = reasoning_effort.clone();
selected.service_tier = service_tier;
let step_settings = Arc::new(ResolvedStepSettings::new(
Arc::new(selected),
Arc::new(model_info.clone()),
review_features.enabled(Feature::FastMode),
));
per_turn_config.model = Some(model);
per_turn_config.model_reasoning_effort = reasoning_effort;
per_turn_config.service_tier = step_settings.service_tier.clone();
let session_telemetry_for_context =
step_settings.telemetry(&parent_turn_context.session_telemetry);
let per_turn_config = Arc::new(per_turn_config);
let review_turn_id = sub_id.to_string();
let turn_metadata_state = Arc::new(TurnMetadataState::new(
@@ -139,11 +146,10 @@ pub(super) async fn spawn_review_thread(
code_mode_available: parent_turn_context.code_mode_available,
config: per_turn_config,
auth_manager: auth_manager_for_context,
model_info: Arc::new(model_info.clone()),
initial_settings: Arc::clone(&step_settings),
current_settings: ArcSwap::from(step_settings),
session_telemetry: session_telemetry_for_context,
provider: provider_for_context,
reasoning_effort,
reasoning_summary,
session_source,
history_mode: parent_turn_context.history_mode,
parent_thread_id: parent_turn_context.parent_thread_id,
@@ -155,12 +161,7 @@ pub(super) async fn spawn_review_thread(
timezone: parent_turn_context.timezone.clone(),
app_server_client_name: parent_turn_context.app_server_client_name.clone(),
developer_instructions: None,
mode: parent_turn_context.mode,
collaboration_mode_developer_instructions: parent_turn_context
.collaboration_mode_developer_instructions
.clone(),
multi_agent_version: MultiAgentVersion::Disabled,
personality: parent_turn_context.personality,
network: parent_turn_context.network.clone(),
windows_sandbox_level: parent_turn_context.windows_sandbox_level,
#[allow(deprecated)]
@@ -191,7 +192,8 @@ pub(super) async fn spawn_review_thread(
// TODO(ccunningham): Review turns currently rely on `spawn_task` for TurnComplete but do not
// emit a parent TurnStarted. Consider giving review a full parent turn lifecycle
// (TurnStarted + TurnComplete) for consistency with other standalone tasks.
sess.spawn_task(tc.clone(), input, ReviewTask::new()).await;
sess.spawn_task(Arc::clone(&tc), input, ReviewTask::new())
.await;
// Announce entering review mode so UIs can switch modes.
let item = TurnItem::EnteredReviewMode(EnteredReviewModeItem {
@@ -331,14 +331,14 @@ impl Session {
RolloutItem::ResponseItem(response_item) => {
history.record_annotated_items(
std::slice::from_ref(response_item),
turn_context.model_info.truncation_policy.into(),
turn_context.model_info().truncation_policy.into(),
);
}
RolloutItem::InterAgentCommunication(communication) => {
let response_item = communication.to_model_input_item();
history.record_items(
std::iter::once(&response_item),
turn_context.model_info.truncation_policy.into(),
turn_context.model_info().truncation_policy.into(),
);
}
RolloutItem::InterAgentCommunicationMetadata { .. } => {}
@@ -241,12 +241,12 @@ async fn record_initial_history_resumed_bare_turn_context_does_not_hydrate_previ
file_system_sandbox_policy: None,
model: previous_model.to_string(),
comp_hash: None,
personality: turn_context.personality,
personality: turn_context.personality(),
collaboration_mode: Some(turn_context.collaboration_mode()),
multi_agent_version: None,
multi_agent_mode: None,
realtime_active: Some(turn_context.realtime_active),
effort: turn_context.reasoning_effort.clone(),
effort: turn_context.reasoning_effort().cloned(),
summary: codex_protocol::config_types::ReasoningSummary::Auto,
};
let rollout_items = vec![RolloutItem::TurnContext(previous_context_item)];
@@ -289,12 +289,12 @@ async fn record_initial_history_resumed_hydrates_previous_turn_settings_from_lif
file_system_sandbox_policy: None,
model: previous_model.to_string(),
comp_hash: Some("comp-hash-a".to_string()),
personality: turn_context.personality,
personality: turn_context.personality(),
collaboration_mode: Some(turn_context.collaboration_mode()),
multi_agent_version: None,
multi_agent_mode: None,
realtime_active: Some(turn_context.realtime_active),
effort: turn_context.reasoning_effort.clone(),
effort: turn_context.reasoning_effort().cloned(),
summary: codex_protocol::config_types::ReasoningSummary::Auto,
};
let turn_id = previous_context_item
@@ -464,7 +464,7 @@ async fn reconstruct_history_rollback_keeps_history_and_metadata_in_sync_for_com
assert_eq!(
reconstructed.previous_turn_settings,
Some(PreviousTurnSettings {
model: turn_context.model_info.slug.clone(),
model: turn_context.model_info().slug.clone(),
comp_hash: None,
realtime_active: Some(turn_context.realtime_active),
})
@@ -565,7 +565,7 @@ async fn reconstruct_history_rollback_keeps_history_and_metadata_in_sync_for_inc
assert_eq!(
reconstructed.previous_turn_settings,
Some(PreviousTurnSettings {
model: turn_context.model_info.slug.clone(),
model: turn_context.model_info().slug.clone(),
comp_hash: None,
realtime_active: Some(turn_context.realtime_active),
})
@@ -697,7 +697,7 @@ async fn reconstruct_history_rollback_skips_non_user_turns_for_history_and_metad
assert_eq!(
reconstructed.previous_turn_settings,
Some(PreviousTurnSettings {
model: turn_context.model_info.slug.clone(),
model: turn_context.model_info().slug.clone(),
comp_hash: None,
realtime_active: Some(turn_context.realtime_active),
})
@@ -802,7 +802,7 @@ async fn reconstruct_history_rollback_counts_inter_agent_assistant_turns() {
assert_eq!(
reconstructed.previous_turn_settings,
Some(PreviousTurnSettings {
model: turn_context.model_info.slug.clone(),
model: turn_context.model_info().slug.clone(),
comp_hash: None,
realtime_active: Some(turn_context.realtime_active),
})
@@ -1036,7 +1036,7 @@ async fn record_initial_history_resumed_rollback_drops_incomplete_user_turn_comp
assert_eq!(
session.previous_turn_settings().await,
Some(PreviousTurnSettings {
model: turn_context.model_info.slug.clone(),
model: turn_context.model_info().slug.clone(),
comp_hash: None,
realtime_active: Some(turn_context.realtime_active),
})
@@ -1360,12 +1360,12 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis
file_system_sandbox_policy: None,
model: previous_model.to_string(),
comp_hash: None,
personality: turn_context.personality,
personality: turn_context.personality(),
collaboration_mode: Some(turn_context.collaboration_mode()),
multi_agent_version: None,
multi_agent_mode: None,
realtime_active: Some(turn_context.realtime_active),
effort: turn_context.reasoning_effort.clone(),
effort: turn_context.reasoning_effort().cloned(),
summary: codex_protocol::config_types::ReasoningSummary::Auto,
};
let previous_turn_id = previous_context_item
@@ -1451,12 +1451,12 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis
file_system_sandbox_policy: None,
model: previous_model.to_string(),
comp_hash: None,
personality: turn_context.personality,
personality: turn_context.personality(),
collaboration_mode: Some(turn_context.collaboration_mode()),
multi_agent_version: None,
multi_agent_mode: None,
realtime_active: Some(turn_context.realtime_active),
effort: turn_context.reasoning_effort.clone(),
effort: turn_context.reasoning_effort().cloned(),
summary: codex_protocol::config_types::ReasoningSummary::Auto,
}))
.expect("serialize expected reference context item")
@@ -1484,12 +1484,12 @@ async fn record_initial_history_resumed_aborted_turn_without_id_clears_active_tu
file_system_sandbox_policy: None,
model: previous_model.to_string(),
comp_hash: None,
personality: turn_context.personality,
personality: turn_context.personality(),
collaboration_mode: Some(turn_context.collaboration_mode()),
multi_agent_version: None,
multi_agent_mode: None,
realtime_active: Some(turn_context.realtime_active),
effort: turn_context.reasoning_effort.clone(),
effort: turn_context.reasoning_effort().cloned(),
summary: codex_protocol::config_types::ReasoningSummary::Auto,
};
let previous_turn_id = previous_context_item
@@ -1616,12 +1616,12 @@ async fn record_initial_history_resumed_unmatched_abort_preserves_active_turn_fo
file_system_sandbox_policy: None,
model: current_model.to_string(),
comp_hash: None,
personality: turn_context.personality,
personality: turn_context.personality(),
collaboration_mode: Some(turn_context.collaboration_mode()),
multi_agent_version: None,
multi_agent_mode: None,
realtime_active: Some(turn_context.realtime_active),
effort: turn_context.reasoning_effort.clone(),
effort: turn_context.reasoning_effort().cloned(),
summary: codex_protocol::config_types::ReasoningSummary::Auto,
};
@@ -1744,12 +1744,12 @@ async fn record_initial_history_resumed_trailing_incomplete_turn_compaction_clea
file_system_sandbox_policy: None,
model: previous_model.to_string(),
comp_hash: None,
personality: turn_context.personality,
personality: turn_context.personality(),
collaboration_mode: Some(turn_context.collaboration_mode()),
multi_agent_version: None,
multi_agent_mode: None,
realtime_active: Some(turn_context.realtime_active),
effort: turn_context.reasoning_effort.clone(),
effort: turn_context.reasoning_effort().cloned(),
summary: codex_protocol::config_types::ReasoningSummary::Auto,
};
let previous_turn_id = previous_context_item
@@ -1882,7 +1882,7 @@ async fn record_initial_history_resumed_trailing_incomplete_turn_preserves_turn_
assert_eq!(
session.previous_turn_settings().await,
Some(PreviousTurnSettings {
model: turn_context.model_info.slug.clone(),
model: turn_context.model_info().slug.clone(),
comp_hash: None,
realtime_active: Some(turn_context.realtime_active),
})
@@ -1916,12 +1916,12 @@ async fn record_initial_history_resumed_replaced_incomplete_compacted_turn_clear
file_system_sandbox_policy: None,
model: previous_model.to_string(),
comp_hash: None,
personality: turn_context.personality,
personality: turn_context.personality(),
collaboration_mode: Some(turn_context.collaboration_mode()),
multi_agent_version: None,
multi_agent_mode: None,
realtime_active: Some(turn_context.realtime_active),
effort: turn_context.reasoning_effort.clone(),
effort: turn_context.reasoning_effort().cloned(),
summary: codex_protocol::config_types::ReasoningSummary::Auto,
};
let previous_turn_id = previous_context_item
+3 -19
View File
@@ -2,35 +2,19 @@ use std::sync::Arc;
use crate::agents_md::LoadedAgentsMd;
use crate::environment_selection::TurnEnvironmentSnapshot;
use crate::session::step_settings::ResolvedStepSettings;
use crate::session::turn_context::TurnContext;
use crate::tools::router::ToolRouter;
use codex_exec_server::ExecutorCapabilityDiscoverySnapshot;
use codex_exec_server::ResolvedSelectedCapabilityRoot;
use codex_mcp::McpBinding;
use codex_otel::SessionTelemetry;
use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::openai_models::ModelInfo;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::protocol::AskForApproval;
/// Request-scoped state that may change between model sampling requests.
pub(crate) struct StepContext {
pub(crate) turn: Arc<TurnContext>,
/// Concrete model and capabilities used by this sampling request.
pub(crate) model_info: Arc<ModelInfo>,
/// Reasoning effort selected for this sampling request.
pub(crate) reasoning_effort: Option<ReasoningEffort>,
/// Reasoning summary resolved for this sampling request.
pub(crate) reasoning_summary: ReasoningSummary,
/// Effective service tier supported by this sampling request's model.
pub(crate) service_tier: Option<String>,
/// Effective approval policy used by this sampling request's tool actions.
/// Model-specific Guardian requirements can change this during a model switch.
pub(crate) approval_policy: AskForApproval,
/// Effective reviewer selected for this sampling request's approvals.
/// Model-specific Guardian requirements can change this during a model switch.
pub(crate) approvals_reviewer: ApprovalsReviewer,
/// One immutable settings version captured before request preparation.
pub(crate) settings: Arc<ResolvedStepSettings>,
/// Telemetry context tagged with this sampling request's model.
pub(crate) session_telemetry: SessionTelemetry,
pub(crate) environments: TurnEnvironmentSnapshot,
+102 -2
View File
@@ -1,32 +1,117 @@
//! Configured inputs used to derive effective step settings.
//! Configured inputs retained independently of future thread settings.
use crate::config::Constrained;
use crate::config::ConstraintError;
use crate::config::ConstraintResult;
use codex_config::ConfigRequirements;
use codex_models_manager::ModelsManagerConfig;
use codex_models_manager::manager::ModelsManager;
use codex_otel::SessionTelemetry;
use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::Personality;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
use codex_protocol::config_types::ServiceTier;
use codex_protocol::openai_models::ModelInfo;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::protocol::AskForApproval;
use std::sync::Arc;
/// Model and execution settings selected for an individual model step within
/// a turn. A turn may contain several steps, each using its own captured settings.
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct StepSettings {
pub(crate) collaboration_mode: CollaborationMode,
/// `None` follows the pinned model's default.
pub(crate) reasoning_summary: Option<ReasoningSummary>,
/// Normalized requested tier. Startup retains initial-model filtering;
/// resolution filters the effective tier against the pinned model.
pub(crate) service_tier: Option<String>,
pub(crate) personality: Option<Personality>,
pub(crate) approval_policy: Constrained<AskForApproval>,
pub(crate) approvals_reviewer: ApprovalsReviewer,
}
/// Immutable configured choices, pinned model metadata, and effective request values.
///
/// One snapshot may produce several fresh `StepContext`s. Replacing the
/// snapshot does not change steps or actions that have already captured it.
#[derive(Clone, Debug)]
pub(crate) struct ResolvedStepSettings {
/// Inputs for later sparse patches. Unset defaults and unsupported requested
/// tiers must not be reconstructed from the effective values below.
selected: Arc<StepSettings>,
pub(crate) model_info: Arc<ModelInfo>,
/// Effective request summary: the configured value or the pinned model's default.
pub(crate) reasoning_summary: ReasoningSummary,
/// Tier sent on requests, filtered by feature and model support.
pub(crate) service_tier: Option<String>,
}
impl ResolvedStepSettings {
/// Derives request values from selected settings and pinned metadata.
pub(super) fn new(
selected: Arc<StepSettings>,
model_info: Arc<ModelInfo>,
fast_mode_enabled: bool,
) -> 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,
);
Self {
selected,
model_info,
reasoning_summary,
service_tier,
}
}
pub(crate) fn reasoning_effort(&self) -> Option<&ReasoningEffort> {
self.selected
.collaboration_mode
.settings
.reasoning_effort
.as_ref()
}
pub(crate) fn approval_policy(&self) -> AskForApproval {
self.selected.approval_policy.value()
}
pub(crate) fn approvals_reviewer(&self) -> ApprovalsReviewer {
self.selected.approvals_reviewer
}
/// Retained inputs for constructing a snapshot against different model metadata.
pub(super) fn selected(&self) -> &StepSettings {
&self.selected
}
pub(super) fn selected_collaboration_mode(&self) -> &CollaborationMode {
&self.selected.collaboration_mode
}
pub(super) fn personality(&self) -> Option<Personality> {
self.selected.personality
}
pub(super) fn telemetry(&self, base: &SessionTelemetry) -> SessionTelemetry {
base.clone().with_model(
self.selected.collaboration_mode.model(),
&self.model_info.slug,
)
}
}
/// Explicit startup overrides applied to catalog-derived model metadata.
/// Construct from `Config::to_models_manager_config()` so model-derived base
/// instructions are not mistaken for explicit overrides.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(crate) struct ModelInfoOverrides {
pub(crate) context_window: Option<i64>,
@@ -83,6 +168,8 @@ pub(crate) struct StepSettingsUpdate {
}
/// Constraints used when applying and validating a candidate settings version.
/// Future settings use the proposed environment; active settings use the
/// environment already admitted for that execution.
pub(crate) struct StepSettingsConstraints<'a> {
pub(crate) requirements: &'a ConfigRequirements,
pub(crate) guardian_approval_enabled: bool,
@@ -91,6 +178,19 @@ pub(crate) struct StepSettingsConstraints<'a> {
}
impl StepSettings {
/// Resolves the selected model using the session's explicit startup overrides.
pub(super) async fn resolve_model_info(
&self,
models_manager: &dyn ModelsManager,
overrides: &ModelInfoOverrides,
personality_enabled: bool,
) -> ModelInfo {
let config = overrides.models_manager_config(self.personality, personality_enabled);
models_manager
.get_model_info(self.collaboration_mode.model(), &config)
.await
}
/// Applies edits and validates the result against the supplied constraints.
/// Callers must supply the constraints of the proposed target environment.
pub(crate) fn apply(
@@ -182,4 +282,4 @@ impl StepSettings {
#[cfg(test)]
#[path = "step_settings_tests.rs"]
mod tests;
pub(super) mod tests;
@@ -1,5 +1,6 @@
use super::*;
use crate::config::PermissionProfileState;
use crate::config::test_config;
use crate::session::session::SessionConfiguration;
use crate::session::session::SessionSettingsUpdate;
use crate::session::tests::make_session_configuration_for_tests;
@@ -7,9 +8,17 @@ use codex_config::ConfigLayerStack;
use codex_config::RequirementSource;
use codex_config::Sourced;
use codex_features::Feature;
use codex_models_manager::manager::StaticModelsManager;
use codex_models_manager::model_info::model_info_from_slug;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Settings;
use codex_protocol::models::BaseInstructionsProvenance;
use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::AutoReviewMessages;
use codex_protocol::openai_models::GuardianV2ModelConfig;
use codex_protocol::openai_models::ModelInstructionsVariables;
use codex_protocol::openai_models::ModelsResponse;
use codex_protocol::openai_models::TruncationPolicyConfig;
use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxEntry;
@@ -22,6 +31,14 @@ use core_test_support::test_codex::local;
use pretty_assertions::assert_eq;
use std::collections::BTreeSet;
use std::sync::Arc;
use test_case::test_case;
pub(crate) fn update_selected_settings_for_test(
settings: &mut ResolvedStepSettings,
update: impl FnOnce(&mut StepSettings),
) {
update(Arc::make_mut(&mut settings.selected));
}
fn set_requirements(configuration: &mut SessionConfiguration, requirements: ConfigRequirements) {
let config = Arc::make_mut(&mut configuration.original_config_do_not_use);
@@ -383,3 +400,109 @@ fn collaboration_replacement_wins_and_effort_clear_remains_sparse() {
}
);
}
#[test_case(BaseInstructionsProvenance::Custom; "explicit instructions")]
#[test_case(BaseInstructionsProvenance::Model { model: "model-a".to_string() }; "model-derived instructions")]
#[tokio::test]
async fn model_resolution_preserves_startup_overrides_and_instruction_provenance(
provenance: BaseInstructionsProvenance,
) {
let configured_instructions = "explicit {{ personality }}";
let auto_review = AutoReviewMessages {
policy: Some("catalog review policy".to_string()),
policy_template: Some("catalog review template".to_string()),
rejection_instructions: None,
timeout_instructions: None,
};
let guardian_v2 = GuardianV2ModelConfig {
classifier_instructions: Some("catalog classifier".to_string()),
review_threshold_basis_points: Some(7_500),
..Default::default()
};
let mut model = model_info_from_slug("model-b");
model.context_window = Some(90_000);
model.max_context_window = Some(100_000);
model.auto_compact_token_limit = Some(80_000);
model.truncation_policy = TruncationPolicyConfig::tokens(/*limit*/ 1_000);
let messages = model
.model_messages
.as_mut()
.expect("test model should have instruction metadata");
messages.instructions_template =
Some("Catalog B.\n# Personality\n{{ personality }}\n# Rules\nKeep the rules.".to_string());
messages.instructions_variables = Some(ModelInstructionsVariables {
personality_default: Some("default".to_string()),
personality_friendly: Some("friendly".to_string()),
personality_pragmatic: Some("pragmatic".to_string()),
});
messages.auto_review = Some(auto_review);
messages.guardian_v2 = Some(guardian_v2);
let catalog = ModelsResponse {
models: vec![model],
};
let models_manager = StaticModelsManager::new(/*auth_manager*/ None, catalog.clone());
let mut config = test_config().await;
config.model = Some("model-a".to_string());
config.model_catalog = Some(catalog);
config.model_context_window = Some(160_000);
config.model_auto_compact_token_limit = Some(70_000);
config.tool_output_token_limit = Some(777);
config.base_instructions = Some(configured_instructions.to_string());
let explicit_instructions = matches!(&provenance, BaseInstructionsProvenance::Custom);
config.base_instructions_provenance = Some(provenance);
// Capture the same filtered explicit overrides that session startup owns.
let overrides = ModelInfoOverrides::from(config.to_models_manager_config());
for (personality, personality_enabled, catalog_instructions) in [
(
Personality::Friendly,
true,
"Catalog B.\n# Personality\nfriendly\n# Rules\nKeep the rules.",
),
(
Personality::None,
true,
"Catalog B.\n# Rules\nKeep the rules.",
),
(
Personality::Friendly,
false,
"Catalog B.\n# Personality\ndefault\n# Rules\nKeep the rules.",
),
] {
config.personality = Some(personality);
config
.features
.set_enabled(Feature::Personality, personality_enabled)
.expect("test config should allow personality changes");
let mut settings = configured_settings();
settings.collaboration_mode = settings.collaboration_mode.with_updates(
Some("model-b".to_string()),
/*effort*/ None,
/*developer_instructions*/ None,
);
settings.personality = config.personality;
let legacy = models_manager
.get_model_info("model-b", &config.to_models_manager_config())
.await;
let resolved = settings
.resolve_model_info(
&models_manager,
&overrides,
config.features.enabled(Feature::Personality),
)
.await;
assert_eq!(resolved, legacy);
assert_eq!(
resolved.get_model_instructions(settings.personality),
if explicit_instructions {
configured_instructions
} else {
catalog_instructions
},
);
}
}
+140 -42
View File
@@ -1,6 +1,8 @@
use super::mcp_refresh::McpRefresh;
use super::step_settings::ResolvedStepSettings;
use super::step_settings::StepSettings;
use super::step_settings::StepSettingsUpdate;
pub(crate) use super::step_settings::tests::update_selected_settings_for_test;
use super::turn_context::TurnEnvironment;
use super::*;
use crate::agents_md_manager::AgentsMdManager;
@@ -212,16 +214,31 @@ pub(crate) fn mcp_config_for_test(config: &crate::config::Config) -> Arc<codex_m
))
}
/// Updates both initial/current views before sharing a test turn context.
pub(crate) fn update_turn_settings_for_test(
turn: &mut TurnContext,
update: impl FnOnce(&mut super::step_settings::ResolvedStepSettings),
) {
let mut settings = turn.initial_settings.as_ref().clone();
update(&mut settings);
let settings = Arc::new(settings);
turn.initial_settings = Arc::clone(&settings);
turn.current_settings.store(settings);
}
impl StepContext {
pub(crate) fn for_test(turn: Arc<TurnContext>) -> Arc<Self> {
let environments = turn.environments.clone();
// Unit fixtures still customize the legacy Config directly.
// Production capture instead takes the turn's current snapshot.
let mut settings = turn.initial_settings.as_ref().clone();
update_selected_settings_for_test(&mut settings, |selected| {
selected.approval_policy = turn.config.permissions.approval_policy.clone();
selected.approvals_reviewer = turn.config.approvals_reviewer;
});
settings.service_tier = turn.config.service_tier.clone();
Arc::new(Self {
model_info: Arc::clone(&turn.model_info),
reasoning_effort: turn.reasoning_effort.clone(),
reasoning_summary: turn.reasoning_summary,
service_tier: turn.config.service_tier.clone(),
approval_policy: turn.approval_policy(),
approvals_reviewer: turn.config.approvals_reviewer,
settings: Arc::new(settings),
session_telemetry: turn.session_telemetry.clone(),
turn: Arc::clone(&turn),
environments,
@@ -496,7 +513,7 @@ async fn world_state_extension_metrics_follow_turn_model_switch() {
.session_telemetry
.clone()
.with_metrics(metrics.clone());
let next_model = if turn_context.model_info.slug == "gpt-5.4" {
let next_model = if turn_context.model_info().slug == "gpt-5.4" {
"gpt-5.2"
} else {
"gpt-5.4"
@@ -2890,8 +2907,10 @@ async fn recompute_token_usage_updates_model_context_window() {
}));
}
Arc::make_mut(&mut turn_context.model_info).context_window = Some(128_000);
Arc::make_mut(&mut turn_context.model_info).effective_context_window_percent = 100;
update_turn_settings_for_test(&mut turn_context, |settings| {
Arc::make_mut(&mut settings.model_info).context_window = Some(128_000);
Arc::make_mut(&mut settings.model_info).effective_context_window_percent = 100;
});
session.recompute_token_usage(&turn_context).await;
@@ -3655,12 +3674,12 @@ async fn record_initial_history_forked_hydrates_previous_turn_settings() {
file_system_sandbox_policy: None,
model: previous_model.to_string(),
comp_hash: None,
personality: turn_context.personality,
personality: turn_context.personality(),
collaboration_mode: Some(turn_context.collaboration_mode()),
multi_agent_version: None,
multi_agent_mode: None,
realtime_active: Some(turn_context.realtime_active),
effort: turn_context.reasoning_effort.clone(),
effort: turn_context.reasoning_effort().cloned(),
summary: codex_protocol::config_types::ReasoningSummary::Auto,
};
let turn_id = previous_context_item
@@ -3962,7 +3981,7 @@ async fn thread_rollback_recomputes_previous_turn_settings_and_reference_context
assert_eq!(
sess.previous_turn_settings().await,
Some(PreviousTurnSettings {
model: tc.model_info.slug.clone(),
model: tc.model_info().slug.clone(),
comp_hash: None,
realtime_active: Some(tc.realtime_active),
})
@@ -4585,7 +4604,7 @@ async fn includes_timed_out_message() {
};
let (_, turn_context) = make_session_and_context().await;
let out = format_exec_output_str(&exec, turn_context.model_info.truncation_policy.into());
let out = format_exec_output_str(&exec, turn_context.model_info().truncation_policy.into());
assert_eq!(
out,
@@ -4596,7 +4615,38 @@ async fn includes_timed_out_message() {
#[tokio::test]
async fn turn_context_with_model_updates_model_fields() {
let (session, mut turn_context) = make_session_and_context().await;
turn_context.reasoning_effort = Some(ReasoningEffortConfig::Minimal);
let config = Arc::make_mut(&mut turn_context.config);
config.features.enable(Feature::FastMode).unwrap();
config.model_reasoning_effort = Some(ReasoningEffortConfig::Minimal);
config.model_reasoning_summary = Some(ReasoningSummaryConfig::Detailed);
update_turn_settings_for_test(&mut turn_context, |settings| {
let mut selected = settings.selected().clone();
selected.collaboration_mode.settings.reasoning_effort =
Some(ReasoningEffortConfig::Minimal);
selected.reasoning_summary = Some(ReasoningSummaryConfig::Detailed);
selected.service_tier = Some(ServiceTier::Fast.request_value().to_string());
*settings = ResolvedStepSettings::new(
Arc::new(selected),
Arc::clone(&settings.model_info),
/*fast_mode_enabled*/ true,
);
});
Arc::make_mut(&mut turn_context.config).service_tier =
turn_context.initial_settings.service_tier.clone();
let captured = turn_context.current_settings.load_full();
let mut current_selection = captured.selected().clone();
current_selection.reasoning_summary = Some(ReasoningSummaryConfig::None);
current_selection.service_tier = None;
current_selection
.collaboration_mode
.settings
.reasoning_effort = Some(ReasoningEffortConfig::High);
let current = Arc::new(ResolvedStepSettings::new(
Arc::new(current_selection),
Arc::clone(turn_context.model_info()),
/*fast_mode_enabled*/ true,
));
turn_context.current_settings.store(Arc::clone(&current));
let updated = turn_context
.with_model("gpt-5.4".to_string(), &session.services.models_manager)
.await;
@@ -4609,12 +4659,48 @@ async fn turn_context_with_model_updates_model_fields() {
)
.await;
// Historical model selection starts from the frozen turn settings, not a
// later publication, and retains its selected summary/tier.
assert_eq!(
(
updated.reasoning_summary(),
updated.initial_settings.service_tier.as_deref()
),
(
ReasoningSummaryConfig::Detailed,
Some(ServiceTier::Fast.request_value())
),
);
assert_eq!(
(
updated.config.model_reasoning_summary,
updated.config.service_tier.as_deref()
),
(
Some(ReasoningSummaryConfig::Detailed),
Some(ServiceTier::Fast.request_value())
),
);
assert!(Arc::ptr_eq(&captured, &turn_context.initial_settings));
assert!(Arc::ptr_eq(
&current,
&turn_context.current_settings.load_full()
));
assert!(!Arc::ptr_eq(&captured, &updated.initial_settings));
assert!(Arc::ptr_eq(
&updated.initial_settings,
&updated.current_settings.load_full()
));
assert!(!Arc::ptr_eq(
&updated.current_settings.load_full(),
&turn_context.current_settings.load_full()
));
assert_eq!(updated.config.model.as_deref(), Some("gpt-5.4"));
assert_eq!(updated.collaboration_mode().model(), "gpt-5.4");
assert_eq!(updated.model_info.as_ref(), &expected_model_info);
assert_eq!(updated.model_info().as_ref(), &expected_model_info);
assert_eq!(
updated.reasoning_effort,
Some(ReasoningEffortConfig::Medium)
updated.reasoning_effort(),
Some(&ReasoningEffortConfig::Medium)
);
assert_eq!(
updated.collaboration_mode().reasoning_effort(),
@@ -6251,7 +6337,11 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
session.services.shell_zsh_path.as_ref(),
session.services.main_execve_wrapper_exe.as_ref(),
per_turn_config,
model_info,
Arc::new(super::step_settings::ResolvedStepSettings::new(
Arc::clone(&session_configuration.step_settings),
Arc::new(model_info),
config.features.enabled(Feature::FastMode),
)),
&models_manager,
/*network*/ None,
resolved_turn_environments,
@@ -8476,7 +8566,11 @@ where
session.services.shell_zsh_path.as_ref(),
session.services.main_execve_wrapper_exe.as_ref(),
per_turn_config,
model_info,
Arc::new(super::step_settings::ResolvedStepSettings::new(
Arc::clone(&session_configuration.step_settings),
Arc::new(model_info),
config.features.enabled(Feature::FastMode),
)),
&models_manager,
/*network*/ None,
resolved_turn_environments,
@@ -9137,7 +9231,7 @@ async fn record_context_updates_emits_environment_item_for_network_changes() {
let previous_context = Arc::new(previous_context);
let mut current_context = previous_context
.with_model(
previous_context.model_info.slug.clone(),
previous_context.model_info().slug.clone(),
&session.services.models_manager,
)
.await;
@@ -9193,7 +9287,7 @@ async fn record_context_updates_emits_environment_item_for_cwd_changes() {
let previous_context = Arc::new(previous_context);
let mut current_context = previous_context
.with_model(
previous_context.model_info.slug.clone(),
previous_context.model_info().slug.clone(),
&session.services.models_manager,
)
.await;
@@ -9250,7 +9344,7 @@ async fn record_context_updates_use_environment_permission_profile_and_workspace
let previous_context = Arc::new(previous_context);
let mut current_context = previous_context
.with_model(
previous_context.model_info.slug.clone(),
previous_context.model_info().slug.clone(),
&session.services.models_manager,
)
.await;
@@ -9304,7 +9398,7 @@ async fn record_context_updates_emits_environment_item_for_time_changes() {
let previous_context = Arc::new(previous_context);
let mut current_context = previous_context
.with_model(
previous_context.model_info.slug.clone(),
previous_context.model_info().slug.clone(),
&session.services.models_manager,
)
.await;
@@ -9328,7 +9422,7 @@ async fn record_context_updates_omits_environment_item_when_disabled() {
let previous_context = Arc::new(previous_context);
let mut current_context = previous_context
.with_model(
previous_context.model_info.slug.clone(),
previous_context.model_info().slug.clone(),
&session.services.models_manager,
)
.await;
@@ -9393,7 +9487,7 @@ async fn record_context_updates_emits_realtime_start_when_session_becomes_live()
let previous_context = Arc::new(previous_context);
let mut current_context = previous_context
.with_model(
previous_context.model_info.slug.clone(),
previous_context.model_info().slug.clone(),
&session.services.models_manager,
)
.await;
@@ -9417,7 +9511,7 @@ async fn record_context_updates_emits_realtime_end_when_session_stops_being_live
previous_context.realtime_active = true;
let mut current_context = previous_context
.with_model(
previous_context.model_info.slug.clone(),
previous_context.model_info().slug.clone(),
&session.services.models_manager,
)
.await;
@@ -9561,8 +9655,10 @@ async fn build_initial_context_includes_turn_context_fragments_from_extensions()
let mut builder = codex_extension_api::ExtensionRegistryBuilder::new();
builder.prompt_contributor(Arc::new(TurnContextExtensionTestContributor));
session.services.extensions = Arc::new(builder.build());
Arc::make_mut(&mut turn_context.model_info).context_window = Some(100);
Arc::make_mut(&mut turn_context.model_info).effective_context_window_percent = 50;
update_turn_settings_for_test(&mut turn_context, |settings| {
Arc::make_mut(&mut settings.model_info).context_window = Some(100);
Arc::make_mut(&mut settings.model_info).effective_context_window_percent = 50;
});
turn_context
.extension_data
.insert(TurnContextExtensionTestState {
@@ -9588,8 +9684,10 @@ async fn record_context_updates_includes_turn_context_fragments_on_steady_state_
let mut builder = codex_extension_api::ExtensionRegistryBuilder::new();
builder.prompt_contributor(Arc::new(TurnContextExtensionTestContributor));
session.services.extensions = Arc::new(builder.build());
Arc::make_mut(&mut turn_context.model_info).context_window = Some(200);
Arc::make_mut(&mut turn_context.model_info).effective_context_window_percent = 25;
update_turn_settings_for_test(&mut turn_context, |settings| {
Arc::make_mut(&mut settings.model_info).context_window = Some(200);
Arc::make_mut(&mut settings.model_info).effective_context_window_percent = 25;
});
turn_context
.extension_data
.insert(TurnContextExtensionTestState {
@@ -9770,7 +9868,7 @@ async fn build_initial_context_restates_realtime_start_when_reference_context_is
let (session, mut turn_context) = make_session_and_context().await;
turn_context.realtime_active = true;
let previous_turn_settings = PreviousTurnSettings {
model: turn_context.model_info.slug.clone(),
model: turn_context.model_info().slug.clone(),
comp_hash: None,
realtime_active: Some(true),
};
@@ -10121,7 +10219,7 @@ async fn build_initial_context_prepends_model_switch_message() {
async fn record_context_updates_and_set_reference_context_item_persists_full_reinjection_to_rollout()
{
let (mut session, previous_context) = make_session_and_context().await;
let next_model = if previous_context.model_info.slug == "gpt-5.4" {
let next_model = if previous_context.model_info().slug == "gpt-5.4" {
"gpt-5.2"
} else {
"gpt-5.4"
@@ -10150,7 +10248,7 @@ async fn record_context_updates_and_set_reference_context_item_persists_full_rei
session
.set_previous_turn_settings(Some(PreviousTurnSettings {
model: previous_context.model_info.slug.clone(),
model: previous_context.model_info().slug.clone(),
comp_hash: None,
realtime_active: Some(previous_context.realtime_active),
}))
@@ -11402,10 +11500,10 @@ async fn sample_rollout(
&& content.iter().any(|c| {
matches!(c, ContentItem::InputText { text } if text.contains("<personality_spec>"))
}))
}) && let Some(p) = reconstruction_turn.personality
}) && let Some(p) = reconstruction_turn.personality()
&& session.features.enabled(Feature::Personality)
&& let Some(personality_message) = reconstruction_turn
.model_info
.model_info()
.model_messages
.as_ref()
.and_then(|m| m.get_personality_message(Some(p)).filter(|s| !s.is_empty()))
@@ -11425,20 +11523,20 @@ async fn sample_rollout(
}
live_history.record_items(
initial_context.iter(),
reconstruction_turn.model_info.truncation_policy.into(),
reconstruction_turn.model_info().truncation_policy.into(),
);
let user1 = user_message("first user");
live_history.record_items(
std::iter::once(&user1),
reconstruction_turn.model_info.truncation_policy.into(),
reconstruction_turn.model_info().truncation_policy.into(),
);
rollout_items.push(RolloutItem::ResponseItem(user1.clone().into()));
let assistant1 = assistant_message("assistant reply one");
live_history.record_items(
std::iter::once(&assistant1),
reconstruction_turn.model_info.truncation_policy.into(),
reconstruction_turn.model_info().truncation_policy.into(),
);
rollout_items.push(RolloutItem::ResponseItem(assistant1.clone().into()));
@@ -11461,14 +11559,14 @@ async fn sample_rollout(
let user2 = user_message("second user");
live_history.record_items(
std::iter::once(&user2),
reconstruction_turn.model_info.truncation_policy.into(),
reconstruction_turn.model_info().truncation_policy.into(),
);
rollout_items.push(RolloutItem::ResponseItem(user2.clone().into()));
let assistant2 = assistant_message("assistant reply two");
live_history.record_items(
std::iter::once(&assistant2),
reconstruction_turn.model_info.truncation_policy.into(),
reconstruction_turn.model_info().truncation_policy.into(),
);
rollout_items.push(RolloutItem::ResponseItem(assistant2.clone().into()));
@@ -11491,14 +11589,14 @@ async fn sample_rollout(
let user3 = user_message("third user");
live_history.record_items(
std::iter::once(&user3),
reconstruction_turn.model_info.truncation_policy.into(),
reconstruction_turn.model_info().truncation_policy.into(),
);
rollout_items.push(RolloutItem::ResponseItem(user3.into()));
let assistant3 = assistant_message("assistant reply three");
live_history.record_items(
std::iter::once(&assistant3),
reconstruction_turn.model_info.truncation_policy.into(),
reconstruction_turn.model_info().truncation_policy.into(),
);
rollout_items.push(RolloutItem::ResponseItem(assistant3.into()));
@@ -5,6 +5,7 @@ use crate::guardian::GUARDIAN_REVIEWER_NAME;
use crate::plugins::plugins_manager_for_config;
use crate::sandboxing::SandboxPermissions;
use crate::session::step_context::StepContext;
use crate::session::tests::update_turn_settings_for_test;
use crate::test_support::models_manager_with_provider;
use crate::tools::context::ToolCallSource;
use crate::tools::context::ToolOutput;
@@ -97,7 +98,9 @@ async fn request_permissions_routes_to_guardian_when_reviewer_is_enabled() {
.await;
let (mut session, mut turn_context_raw) = make_session_and_context().await;
Arc::make_mut(&mut turn_context_raw.model_info).node_repl_auto_review_required = true;
update_turn_settings_for_test(&mut turn_context_raw, |settings| {
Arc::make_mut(&mut settings.model_info).node_repl_auto_review_required = true;
});
*session.active_turn.lock().await = Some(ActiveTurn::default());
Arc::make_mut(&mut turn_context_raw.config)
.permissions
+35 -31
View File
@@ -272,8 +272,8 @@ pub(crate) async fn run_turn(
sess.merge_connector_selection(explicitly_enabled_connectors.clone())
.await;
sess.set_previous_turn_settings(Some(PreviousTurnSettings {
model: turn_context.model_info.slug.clone(),
comp_hash: turn_context.model_info.comp_hash.clone(),
model: turn_context.model_info().slug.clone(),
comp_hash: turn_context.model_info().comp_hash.clone(),
realtime_active: Some(turn_context.realtime_active),
}))
.await;
@@ -372,7 +372,7 @@ pub(crate) async fn run_turn(
let sampling_request_input: Vec<ResponseItem> = async {
sess.clone_history()
.await
.for_prompt(&step_context.model_info.input_modalities)
.for_prompt(&step_context.settings.model_info.input_modalities)
}
.instrument(trace_span!("run_turn.prepare_sampling_request_input"))
.await;
@@ -782,7 +782,7 @@ async fn build_skills_and_plugins(
}
let tracking = build_track_events_context(
turn_context.model_info.slug.clone(),
turn_context.model_info().slug.clone(),
sess.thread_id.to_string(),
turn_context.sub_id.clone(),
turn_context.originator.clone(),
@@ -999,13 +999,13 @@ async fn track_turn_resolved_config_analytics(
submission_type: None,
ephemeral: thread_config.ephemeral,
session_source: thread_config.session_source,
model: turn_context.model_info.slug.clone(),
model: turn_context.model_info().slug.clone(),
model_provider: turn_context.config.model_provider_id.clone(),
permission_profile: turn_context.permission_profile(),
#[allow(deprecated)]
permission_profile_cwd: turn_context.cwd.to_path_buf(),
reasoning_effort: turn_context.reasoning_effort.clone(),
reasoning_summary: Some(turn_context.reasoning_summary),
reasoning_effort: turn_context.reasoning_effort().cloned(),
reasoning_summary: Some(turn_context.reasoning_summary()),
service_tier: turn_context
.config
.service_tier
@@ -1014,8 +1014,8 @@ async fn track_turn_resolved_config_analytics(
approval_policy: turn_context.approval_policy(),
approvals_reviewer: turn_context.config.approvals_reviewer,
sandbox_network_access: turn_context.network_sandbox_policy().is_enabled(),
collaboration_mode: turn_context.mode,
personality: turn_context.personality,
collaboration_mode: turn_context.mode(),
personality: turn_context.personality(),
workspace_kind: turn_context.turn_metadata_state.workspace_kind(),
is_first_turn,
});
@@ -1077,7 +1077,7 @@ async fn capture_current_model_fallback_step_context(
.is_some_and(codex_login::AuthManager::current_auth_uses_codex_backend);
if !uses_codex_backend
|| !turn_context.provider.info().is_openai()
|| previous_model == turn_context.model_info.slug
|| previous_model == turn_context.model_info().slug
{
return Ok(None);
}
@@ -1101,7 +1101,7 @@ async fn maybe_run_previous_model_inline_compact(
};
let should_compact_for_comp_hash_change = comp_hash_changed(
previous_turn_settings.comp_hash.as_deref(),
turn_context.model_info.comp_hash.as_deref(),
turn_context.model_info().comp_hash.as_deref(),
);
let previous_model = previous_turn_settings.model;
let previous_model_turn_context = Arc::new(
@@ -1147,7 +1147,7 @@ async fn maybe_run_previous_model_inline_compact(
{
AutoCompactTokenLimitScope::Total => {
let new_auto_compact_limit = turn_context
.model_info
.model_info()
.auto_compact_token_limit()
.unwrap_or(i64::MAX);
active_context_tokens > new_auto_compact_limit
@@ -1156,7 +1156,7 @@ async fn maybe_run_previous_model_inline_compact(
AutoCompactTokenLimitScope::BodyAfterPrefix => active_context_tokens >= new_context_window,
};
let should_run = previous_model_limit_reached
&& previous_model_turn_context.model_info.slug != turn_context.model_info.slug
&& previous_model_turn_context.model_info().slug != turn_context.model_info().slug
&& old_context_window > new_context_window;
if should_run {
let step_context = sess
@@ -1346,7 +1346,7 @@ pub(crate) fn build_prompt(
skip_all,
fields(
turn_id = %step_context.turn.sub_id,
model = %step_context.model_info.slug,
model = %step_context.settings.model_info.slug,
cwd = %step_context.turn.cwd.display()
)
)]
@@ -1384,7 +1384,7 @@ async fn run_sampling_request(
} else {
sess.clone_history()
.await
.for_prompt(&step_context.model_info.input_modalities)
.for_prompt(&step_context.settings.model_info.input_modalities)
};
let mut prompt_input = prompt_input;
if let Some(executed_tool_calls) = sess.services.executed_tool_calls.as_ref()
@@ -1500,7 +1500,7 @@ pub(crate) async fn prepare_tool_recommendations(
skip_all,
fields(
turn_id = %turn_context.sub_id,
model = %turn_context.model_info.slug,
model = %turn_context.model_info().slug,
apps_enabled = turn_context.apps_enabled()
)
)]
@@ -2186,7 +2186,7 @@ fn assign_missing_streamed_response_item_id(
skip_all,
fields(
turn_id = %step_context.turn.sub_id,
model = %step_context.model_info.slug
model = %step_context.settings.model_info.slug
)
)]
async fn try_run_sampling_request(
@@ -2202,16 +2202,16 @@ async fn try_run_sampling_request(
) -> CodexResult<SamplingRequestResult> {
let turn_context = Arc::clone(&step_context.turn);
feedback_tags!(
model = step_context.model_info.slug.clone(),
model = step_context.settings.model_info.slug.clone(),
approval_policy = turn_context.approval_policy(),
sandbox_policy = &turn_context.sandbox_policy(),
effort = step_context.reasoning_effort,
effort = step_context.settings.reasoning_effort(),
auth_mode = sess.services.auth_manager.auth_mode(),
features = sess.features.enabled_features(),
);
let inference_trace = sess.services.rollout_thread_trace.inference_trace_context(
turn_context.sub_id.as_str(),
step_context.model_info.slug.as_str(),
step_context.settings.model_info.slug.as_str(),
turn_context.provider.info().name.as_str(),
);
let sampling_timing_guard = turn_context.turn_timing_state.begin_sampling();
@@ -2223,11 +2223,11 @@ async fn try_run_sampling_request(
let mut stream = client_session
.stream(
prompt,
&step_context.model_info,
&step_context.settings.model_info,
&step_context.session_telemetry,
step_context.reasoning_effort.clone(),
step_context.reasoning_summary,
step_context.service_tier.clone(),
step_context.settings.reasoning_effort().cloned(),
step_context.settings.reasoning_summary,
step_context.settings.service_tier.clone(),
responses_metadata,
&inference_trace,
)
@@ -2248,12 +2248,16 @@ async fn try_run_sampling_request(
const MAX_ANALYTICS_TOOL_CALL_IDS_PER_RESPONSE: usize = 256;
let mut analytics_tool_call_ids = Vec::new();
let reasoning_effort = step_context
.reasoning_effort
.clone()
.or_else(|| step_context.model_info.default_reasoning_level.clone())
.map(|effort| effort.to_string())
.settings
.reasoning_effort()
.or(step_context
.settings
.model_info
.default_reasoning_level
.as_ref())
.map(std::string::ToString::to_string)
.unwrap_or_else(|| "default".to_string());
let plan_mode = turn_context.mode == ModeKind::Plan;
let plan_mode = turn_context.mode() == ModeKind::Plan;
let mut assistant_message_stream_parsers = AssistantMessageStreamParsers::new(plan_mode);
let mut plan_mode_state = plan_mode.then(|| PlanModeStreamState::new(&turn_context.sub_id));
let defer_streamed_turn_items_for_contributors =
@@ -2499,7 +2503,7 @@ async fn try_run_sampling_request(
.server_model_warning_emitted
.load(Ordering::Relaxed)
&& sess
.maybe_warn_on_server_model_mismatch(&turn_context, server_model)
.maybe_warn_on_server_model_mismatch(&step_context, server_model)
.await
{
turn_context
@@ -2524,7 +2528,7 @@ async fn try_run_sampling_request(
sess.send_event(
&turn_context,
EventMsg::SafetyBuffering(SafetyBufferingEvent {
model: turn_context.model_info.slug.clone(),
model: step_context.settings.model_info.slug.clone(),
use_cases: buffering.use_cases,
reasons: buffering.reasons,
show_buffering_ui: buffering.show_buffering_ui,
+121 -95
View File
@@ -1,9 +1,11 @@
use super::step_settings::ResolvedStepSettings;
use super::*;
use crate::environment_selection::EnvironmentConfigOrigin;
use crate::environment_selection::TurnEnvironmentSnapshot;
use crate::exec_policy::AllowPrefixRules;
use crate::shell_snapshot::ShellSnapshotFile;
use crate::tools::sandboxing::executor_windows_sandbox_level;
use arc_swap::ArcSwap;
use codex_core_plugins::PluginCommandAttribution;
use codex_core_plugins::ResolvedPluginMetricsOperation;
use codex_core_plugins::TrustedPluginRoots;
@@ -174,18 +176,17 @@ pub struct TurnContext {
pub(crate) realtime_active: bool,
pub(crate) code_mode_available: bool,
/// Turn-scoped configuration. Read step-specific settings such as service tier and
/// approvals reviewer from the corresponding `StepContext` fields instead.
/// approvals reviewer from the corresponding `StepContext` instead.
pub config: Arc<Config>,
pub(crate) auth_manager: Option<Arc<AuthManager>>,
/// Legacy turn model; step-scoped execution should use `StepContext::model_info`.
pub(crate) model_info: Arc<ModelInfo>,
/// Frozen settings used to construct this context. Legacy turn consumers
/// keep this view even when later steps use different settings.
pub(crate) initial_settings: Arc<ResolvedStepSettings>,
/// Snapshot for the next step; request consumers use their captured StepContext.
pub(super) current_settings: ArcSwap<ResolvedStepSettings>,
/// Turn-wide telemetry; model-attributed step work should use `StepContext::session_telemetry`.
pub(crate) session_telemetry: SessionTelemetry,
pub(crate) provider: SharedModelProvider,
/// Legacy turn effort; step-scoped execution should use `StepContext::reasoning_effort`.
pub(crate) reasoning_effort: Option<ReasoningEffortConfig>,
/// Legacy turn summary; step-scoped execution should use `StepContext::reasoning_summary`.
pub(crate) reasoning_summary: ReasoningSummaryConfig,
pub(crate) session_source: SessionSource,
pub(crate) history_mode: ThreadHistoryMode,
pub(crate) parent_thread_id: Option<ThreadId>,
@@ -200,10 +201,7 @@ pub struct TurnContext {
pub(crate) timezone: Option<String>,
pub(crate) app_server_client_name: Option<String>,
pub(crate) developer_instructions: Option<String>,
pub(crate) mode: ModeKind,
pub(crate) collaboration_mode_developer_instructions: Option<String>,
pub(crate) multi_agent_version: MultiAgentVersion,
pub(crate) personality: Option<Personality>,
pub(crate) network: Option<NetworkProxy>,
// TODO(anp): Reconcile this parallel turn snapshot with TurnEnvironment::sandbox_context
// so owner-provided environment settings govern the remaining sandbox decisions.
@@ -226,6 +224,46 @@ enum TurnMultiAgentRuntime {
}
impl TurnContext {
/// Legacy: returns the frozen initial-turn model metadata.
/// Step-scoped consumers should use their captured `StepContext::settings`.
pub(crate) fn model_info(&self) -> &Arc<ModelInfo> {
&self.initial_settings.model_info
}
/// Legacy: returns the frozen initial-turn reasoning-effort selection.
/// Step-scoped consumers should use their captured `StepContext::settings`.
pub(crate) fn reasoning_effort(&self) -> Option<&ReasoningEffortConfig> {
self.initial_settings.reasoning_effort()
}
/// Legacy: returns the frozen initial-turn effective reasoning summary.
/// Step-scoped consumers should use their captured `StepContext::settings`.
pub(crate) fn reasoning_summary(&self) -> ReasoningSummaryConfig {
self.initial_settings.reasoning_summary
}
/// Legacy: returns the frozen initial-turn collaboration mode kind.
/// Step-scoped consumers should use their captured `StepContext::settings`.
pub(crate) fn mode(&self) -> ModeKind {
self.initial_settings.selected_collaboration_mode().mode
}
/// Legacy: returns the frozen initial-turn personality.
/// Step-scoped consumers should use their captured `StepContext::settings`.
pub(crate) fn personality(&self) -> Option<Personality> {
self.initial_settings.personality()
}
/// Legacy: returns the frozen initial-turn collaboration-mode developer instructions.
/// Step-scoped consumers should use their captured `StepContext::settings`.
pub(crate) fn collaboration_mode_developer_instructions(&self) -> &Option<String> {
&self
.initial_settings
.selected_collaboration_mode()
.settings
.developer_instructions
}
pub(crate) fn skills_snapshot(&self) -> Arc<HostSkillsSnapshot> {
let Some(snapshot) = self.extension_data.get::<HostSkillsSnapshot>() else {
unreachable!("every turn has a host skills snapshot");
@@ -233,13 +271,15 @@ impl TurnContext {
snapshot
}
/// Legacy: returns the frozen initial-turn collaboration mode with the resolved model slug.
/// Step-scoped consumers should use their captured `StepContext::settings`.
pub(crate) fn collaboration_mode(&self) -> CollaborationMode {
CollaborationMode {
mode: self.mode,
mode: self.mode(),
settings: Settings {
model: self.model_info.slug.clone(),
reasoning_effort: self.reasoning_effort.clone(),
developer_instructions: self.collaboration_mode_developer_instructions.clone(),
model: self.model_info().slug.clone(),
reasoning_effort: self.reasoning_effort().cloned(),
developer_instructions: self.collaboration_mode_developer_instructions().clone(),
},
}
}
@@ -266,12 +306,14 @@ impl TurnContext {
.await
}
/// Approval policy admitted with the turn; step-scoped actions should use
/// `StepContext::approval_policy` instead.
/// Legacy: returns the frozen initial-turn approval policy.
/// Step-scoped consumers should use their captured `StepContext::settings`.
pub(crate) fn approval_policy(&self) -> AskForApproval {
self.config.permissions.approval_policy.value()
}
/// Legacy: returns the frozen initial-turn prefix-rule policy.
/// Step-scoped consumers should use their captured `StepContext::settings`.
pub(crate) fn allow_prefix_rules(&self) -> AllowPrefixRules {
let ignore_rules = self
.config
@@ -280,8 +322,9 @@ impl TurnContext {
.auto_review
.as_ref()
.and_then(|auto_review| auto_review.ignore_rules.as_ref())
.is_some_and(|models| models.contains(&self.model_info.slug));
if self.model_info.model_specialty.as_deref() == Some(MODEL_SPECIALTY_CYBER) || ignore_rules
.is_some_and(|models| models.contains(&self.model_info().slug));
if self.model_info().model_specialty.as_deref() == Some(MODEL_SPECIALTY_CYBER)
|| ignore_rules
{
AllowPrefixRules::IgnoreForCyberModel
} else {
@@ -354,21 +397,27 @@ impl TurnContext {
workspace_roots
}
/// Legacy: returns the frozen initial-turn reasoning effort, including the initial model default.
/// Step-scoped consumers should use their captured `StepContext::settings`.
pub(crate) fn effective_reasoning_effort(&self) -> Option<ReasoningEffortConfig> {
self.reasoning_effort
.clone()
.or_else(|| self.model_info.default_reasoning_level.clone())
self.reasoning_effort()
.or(self.model_info().default_reasoning_level.as_ref())
.cloned()
}
/// Legacy: returns the frozen initial-turn reasoning-effort label for tracing.
/// Step-scoped consumers should use their captured `StepContext::settings`.
pub(crate) fn effective_reasoning_effort_for_tracing(&self) -> String {
self.effective_reasoning_effort()
.map(|effort| effort.to_string())
.unwrap_or_else(|| "default".to_string())
}
/// Legacy: returns the frozen initial-turn model context window.
/// Step-scoped consumers should use their captured `StepContext::settings`.
pub(crate) fn model_context_window(&self) -> Option<i64> {
let effective_context_window_percent = self.model_info.effective_context_window_percent;
self.model_info
let effective_context_window_percent = self.model_info().effective_context_window_percent;
self.model_info()
.resolved_context_window()
.map(|context_window| {
context_window.saturating_mul(effective_context_window_percent) / 100
@@ -401,10 +450,9 @@ impl TurnContext {
.iter()
.map(|preset| preset.effort.clone())
.collect::<Vec<_>>();
let reasoning_effort = if let Some(current_reasoning_effort) = self.reasoning_effort.clone()
{
if supported_reasoning_levels.contains(&current_reasoning_effort) {
Some(current_reasoning_effort)
let reasoning_effort = if let Some(current_reasoning_effort) = self.reasoning_effort() {
if supported_reasoning_levels.contains(current_reasoning_effort) {
Some(current_reasoning_effort.clone())
} else {
supported_reasoning_levels
.get(supported_reasoning_levels.len().saturating_sub(1) / 2)
@@ -426,6 +474,19 @@ impl TurnContext {
)
.await;
let model_info = Arc::new(model_info);
let mut selected = self.initial_settings.selected().clone();
selected.collaboration_mode = selected.collaboration_mode.with_updates(
Some(model),
Some(reasoning_effort),
/*developer_instructions*/ None,
);
let step_settings = Arc::new(ResolvedStepSettings::new(
Arc::new(selected),
model_info,
config.features.enabled(Feature::FastMode),
));
config.service_tier = step_settings.service_tier.clone();
let session_telemetry = step_settings.telemetry(&self.session_telemetry);
Self {
sub_id: self.sub_id.clone(),
@@ -434,14 +495,10 @@ impl TurnContext {
code_mode_available: self.code_mode_available,
config: Arc::new(config),
auth_manager: self.auth_manager.clone(),
model_info: Arc::clone(&model_info),
session_telemetry: self
.session_telemetry
.clone()
.with_model(model.as_str(), model_info.slug.as_str()),
initial_settings: Arc::clone(&step_settings),
current_settings: ArcSwap::from(step_settings),
session_telemetry,
provider: self.provider.clone(),
reasoning_effort,
reasoning_summary: self.reasoning_summary,
session_source: self.session_source.clone(),
history_mode: self.history_mode,
parent_thread_id: self.parent_thread_id,
@@ -453,12 +510,7 @@ impl TurnContext {
timezone: self.timezone.clone(),
app_server_client_name: self.app_server_client_name.clone(),
developer_instructions: self.developer_instructions.clone(),
mode: self.mode,
collaboration_mode_developer_instructions: self
.collaboration_mode_developer_instructions
.clone(),
multi_agent_version: self.multi_agent_version,
personality: self.personality,
network: self.network.clone(),
windows_sandbox_level: self.windows_sandbox_level,
available_models,
@@ -517,14 +569,14 @@ impl TurnContext {
),
network: self.turn_context_network_item(),
file_system_sandbox_policy: self.non_legacy_file_system_sandbox_policy(),
model: self.model_info.slug.clone(),
comp_hash: self.model_info.comp_hash.clone(),
personality: self.personality,
model: self.model_info().slug.clone(),
comp_hash: self.model_info().comp_hash.clone(),
personality: self.personality(),
collaboration_mode: Some(self.collaboration_mode()),
multi_agent_version: Some(self.multi_agent_version),
multi_agent_mode: None,
realtime_active: Some(self.realtime_active),
effort: self.reasoning_effort.clone(),
effort: self.reasoning_effort().cloned(),
summary: ReasoningSummaryConfig::Auto,
}
}
@@ -642,7 +694,7 @@ impl Session {
shell_zsh_path: Option<&PathBuf>,
main_execve_wrapper_exe: Option<&PathBuf>,
per_turn_config: Config,
model_info: ModelInfo,
step_settings: Arc<ResolvedStepSettings>,
models_manager: &SharedModelsManager,
network: Option<NetworkProxy>,
environments: TurnEnvironmentSnapshot,
@@ -650,21 +702,9 @@ impl Session {
sub_id: String,
skills_snapshot: HostSkillsSnapshot,
) -> TurnContext {
let collaboration_mode = &session_configuration.step_settings.collaboration_mode;
let reasoning_effort = collaboration_mode.reasoning_effort();
let reasoning_summary = session_configuration
.step_settings
.reasoning_summary
.unwrap_or(model_info.default_reasoning_summary);
let session_telemetry = session_telemetry.clone().with_model(
session_configuration
.step_settings
.collaboration_mode
.model(),
model_info.slug.as_str(),
);
let model_info = &step_settings.model_info;
let session_telemetry_for_context = step_settings.telemetry(session_telemetry);
let session_source = session_configuration.session_source.clone();
let session_telemetry_for_context = session_telemetry;
let available_models = models_manager.try_list_models().unwrap_or_default();
let unified_exec_shell_mode = UnifiedExecShellMode::for_session(
per_turn_config.features.get(),
@@ -674,12 +714,8 @@ impl Session {
);
let mut per_turn_config = per_turn_config;
super::token_budget::apply_model_defaults(&mut per_turn_config, &model_info);
per_turn_config.service_tier = get_service_tier(
per_turn_config.service_tier,
per_turn_config.features.enabled(Feature::FastMode),
&model_info,
);
super::token_budget::apply_model_defaults(&mut per_turn_config, model_info);
per_turn_config.service_tier = step_settings.service_tier.clone();
let permission_profile = environments.permission_profile_or_else(|| {
per_turn_config.permissions.effective_permission_profile()
});
@@ -701,7 +737,7 @@ impl Session {
session_configuration.windows_sandbox_level,
network.is_some(),
auto_review_enabled,
&model_info,
model_info,
));
turn_metadata_state
.set_responses_api_metadata(per_turn_config.responses_api_metadata.clone());
@@ -715,11 +751,10 @@ impl Session {
code_mode_available: true,
config: per_turn_config,
auth_manager,
model_info: Arc::new(model_info),
initial_settings: Arc::clone(&step_settings),
current_settings: ArcSwap::from(step_settings),
session_telemetry: session_telemetry_for_context,
provider,
reasoning_effort,
reasoning_summary,
session_source,
history_mode: session_configuration.history_mode,
parent_thread_id: session_configuration.parent_thread_id,
@@ -731,13 +766,7 @@ impl Session {
timezone: Some(timezone),
app_server_client_name: session_configuration.app_server_client_name.clone(),
developer_instructions: session_configuration.developer_instructions.clone(),
mode: collaboration_mode.mode,
collaboration_mode_developer_instructions: collaboration_mode
.settings
.developer_instructions
.clone(),
multi_agent_version,
personality: session_configuration.step_settings.personality,
network,
windows_sandbox_level: session_configuration.windows_sandbox_level,
available_models,
@@ -758,13 +787,13 @@ impl Session {
sub_id: String,
updates: SessionSettingsUpdate,
) -> CodexResult<(Arc<TurnContext>, ThreadSettingsSnapshot)> {
let Some(turn) = self
let Some((turn_context, snapshot)) = self
.new_turn_with_sub_id_if(sub_id, updates, |_, _| true)
.await?
else {
unreachable!("unconditional turn construction must accept valid settings");
};
Ok(turn)
Ok((turn_context, snapshot))
}
/// Commits accepted settings atomically, then constructs the turn without holding
@@ -808,7 +837,7 @@ impl Session {
Ok(Some((turn_context, commit.snapshot)))
}
/// Builds a turn from the exact committed settings without starting a task.
/// Constructs a turn from the exact committed settings without starting a task.
async fn new_turn_from_configuration(
&self,
sub_id: String,
@@ -862,20 +891,12 @@ impl Session {
.map(TurnEnvironment::permission_profile)
.cloned()
.unwrap_or_else(|| session_configuration.permission_profile());
let model_info = self
.services
.models_manager
.get_model_info(
session_configuration
.step_settings
.collaboration_mode
.model(),
&session_configuration
.model_info_overrides
.models_manager_config(
session_configuration.step_settings.personality,
self.features.enabled(Feature::Personality),
),
let model_info = session_configuration
.step_settings
.resolve_model_info(
self.services.models_manager.as_ref(),
&session_configuration.model_info_overrides,
self.features.enabled(Feature::Personality),
)
.await;
self.services
@@ -924,6 +945,11 @@ impl Session {
.snapshot_for_config(&skills_input, fs)
.await
};
let step_settings = Arc::new(ResolvedStepSettings::new(
Arc::clone(&session_configuration.step_settings),
Arc::new(model_info),
self.features.enabled(Feature::FastMode),
));
let mut turn_context: TurnContext = Self::make_turn_context(
self.thread_id(),
self.session_id(),
@@ -936,7 +962,7 @@ impl Session {
self.services.shell_zsh_path.as_ref(),
self.services.main_execve_wrapper_exe.as_ref(),
per_turn_config,
model_info,
step_settings,
&self.services.models_manager,
self.services
.network_proxy
@@ -973,13 +999,13 @@ impl Session {
}
pub(crate) async fn maybe_emit_model_warnings_for_turn(&self, tc: &TurnContext) {
if tc.model_info.used_fallback_model_metadata {
if tc.model_info().used_fallback_model_metadata {
self.send_event(
tc,
EventMsg::Warning(WarningEvent {
message: format!(
"Model metadata for `{}` not found. Defaulting to fallback metadata; this can degrade performance and cause issues.",
tc.model_info.slug
tc.model_info().slug
),
}),
)
@@ -1002,7 +1028,7 @@ impl Session {
}
if let Some(message) =
unsupported_code_mode_warning(&tc.model_info, tc.config.features.get())
unsupported_code_mode_warning(tc.model_info(), tc.config.features.get())
{
self.send_event(tc, EventMsg::Warning(WarningEvent { message }))
.await;
+2 -2
View File
@@ -130,7 +130,7 @@ impl PreparedTurnInputSettings {
updates.final_output_json_schema = Some(final_output_json_schema);
updates.service_tier_for_turn = service_tier;
let turn = match kind {
let turn_context = match kind {
TurnStartKind::User | TurnStartKind::Recovery => Some(
session
.new_turn_with_sub_id(submission_id.clone(), updates)
@@ -144,7 +144,7 @@ impl PreparedTurnInputSettings {
.await?
}
};
let Some((turn_context, settings_snapshot)) = turn else {
let Some((turn_context, settings_snapshot)) = turn_context else {
return Ok(None);
};
if emit_thread_settings_applied {
+14 -1
View File
@@ -2,6 +2,7 @@ use super::*;
use crate::config::Constrained;
use crate::session::step_settings::StepSettingsUpdate;
use crate::session::tests::make_session_and_context_with_rx;
use crate::session::turn_context::TurnContext;
use crate::state::TaskKind;
use crate::tasks::SessionTask;
use crate::tasks::SessionTaskResult;
@@ -126,7 +127,7 @@ async fn accepted_input_applies_thread_settings() {
collaboration_mode: Some(CollaborationMode {
mode: ModeKind::Default,
settings: Settings {
model: turn_context.model_info.slug.clone(),
model: turn_context.model_info().slug.clone(),
reasoning_effort: config.model_reasoning_effort.clone(),
developer_instructions: None,
},
@@ -382,6 +383,14 @@ async fn prepared_user_updates_merge_with_settings_at_turn_start() {
);
assert_eq!(session.collaboration_mode().await, expected);
assert_eq!(turn_context.collaboration_mode(), expected);
assert_eq!(
turn_context.initial_settings.selected_collaboration_mode(),
&expected
);
assert!(Arc::ptr_eq(
&turn_context.initial_settings.model_info,
turn_context.model_info(),
));
}
}
@@ -432,6 +441,10 @@ async fn automatic_admission_uses_current_candidate_after_plan_preview() {
);
assert_eq!(session.collaboration_mode().await, expected);
assert_eq!(turn_context.collaboration_mode(), expected);
assert_eq!(
turn_context.initial_settings.selected_collaboration_mode(),
&expected
);
}
#[tokio::test]
+14 -14
View File
@@ -41,8 +41,8 @@ impl Session {
"building step world state"
);
let model_instructions = turn_context
.model_info
.get_model_instructions(turn_context.personality);
.model_info()
.get_model_instructions(turn_context.personality());
let (previous_model, previous_context, base_instructions) = {
let state = self.state.lock().await;
let base_instructions = state.session_configuration.base_instructions.clone();
@@ -65,7 +65,7 @@ impl Session {
base_instructions,
)
};
let personality_is_baked = turn_context.model_info.supports_personality()
let personality_is_baked = turn_context.model_info().supports_personality()
&& base_instructions == model_instructions;
let environment_subagents = if turn_context.config.include_environment_context {
self.services
@@ -77,22 +77,22 @@ impl Session {
};
let mut world_state = WorldState::default();
world_state.add_section(ModelInstructionsState::new(
&turn_context.model_info.slug,
&turn_context.model_info().slug,
previous_model.as_deref(),
model_instructions,
));
if self.features.enabled(Feature::Personality) {
let personality_instructions = turn_context.personality.and_then(|personality| {
let personality_instructions = turn_context.personality().and_then(|personality| {
turn_context
.model_info
.model_info()
.model_messages
.as_ref()
.and_then(|messages| messages.get_personality_message(Some(personality)))
.filter(|message| !message.is_empty())
});
world_state.add_section(PersonalityState::new(
&turn_context.model_info.slug,
turn_context.personality,
&turn_context.model_info().slug,
turn_context.personality(),
previous_context
.as_ref()
.map(|previous| previous.model.as_str())
@@ -156,12 +156,12 @@ impl Session {
let cwd = environment
.and_then(|environment| environment.cwd().to_abs_path().ok())
.unwrap_or_else(|| turn_context.cwd.clone());
let model_messages = turn_context.model_info.model_messages.as_ref();
let model_messages = turn_context.model_info().model_messages.as_ref();
world_state.add_section(PermissionsState::new(
&permission_profile,
step_context.approval_policy,
step_context.settings.approval_policy(),
ApprovalPromptContext::new(
step_context.approvals_reviewer,
step_context.settings.approvals_reviewer(),
model_messages.and_then(|messages| messages.approvals.as_ref()),
model_messages.and_then(|messages| messages.permissions.as_ref()),
),
@@ -183,7 +183,7 @@ impl Session {
world_state.add_section(CollaborationModeState::from_collaboration_mode(
&turn_context.collaboration_mode(),
turn_context
.model_info
.model_info()
.model_messages
.as_ref()
.and_then(|messages| messages.collaboration_modes.as_ref()),
@@ -227,12 +227,12 @@ impl Session {
false
};
let apps_usage_instructions_available =
apps_available && turn_context.model_info.include_apps_usage_instructions;
apps_available && turn_context.model_info().include_apps_usage_instructions;
world_state.add_section(AppsInstructionsState::new(
apps_usage_instructions_available,
));
let plugins_usage_instructions_available = step_context.mcp.plugins_available()
&& turn_context.model_info.include_plugin_usage_instructions;
&& turn_context.model_info().include_plugin_usage_instructions;
world_state.add_section(PluginsInstructionsState::new(
plugins_usage_instructions_available,
));
+4 -4
View File
@@ -314,11 +314,11 @@ async fn schedule_startup_prewarm_inner(
client_session
.prewarm_websocket(
&startup_prompt,
&step_context.model_info,
&step_context.settings.model_info,
&step_context.session_telemetry,
step_context.reasoning_effort.clone(),
step_context.reasoning_summary,
step_context.service_tier.clone(),
step_context.settings.reasoning_effort().cloned(),
step_context.settings.reasoning_summary,
step_context.settings.service_tier.clone(),
&responses_metadata,
)
.await?;
+1 -1
View File
@@ -150,7 +150,7 @@ pub(crate) async fn maybe_emit_implicit_skill_invocation(
.analytics_events_client
.track_skill_invocations(
build_track_events_context(
turn_context.model_info.slug.clone(),
turn_context.model_info().slug.clone(),
sess.thread_id.to_string(),
turn_context.sub_id.clone(),
turn_context.originator.clone(),
+2 -2
View File
@@ -100,7 +100,7 @@ pub(crate) async fn record_completed_response_item_with_finalized_facts(
|| {
completed_item_defers_mailbox_delivery_to_next_turn(
item,
turn_context.mode == ModeKind::Plan,
turn_context.mode() == ModeKind::Plan,
)
},
|facts| facts.defers_mailbox_delivery_to_next_turn,
@@ -292,7 +292,7 @@ pub(crate) async fn handle_output_item_done(
previously_active_item: Option<TurnItem>,
) -> Result<OutputItemResult> {
let mut output = OutputItemResult::default();
let plan_mode = ctx.turn_context.mode == ModeKind::Plan;
let plan_mode = ctx.turn_context.mode() == ModeKind::Plan;
match ToolRouter::build_tool_call(item.clone()) {
// The model emitted a tool call; log it, persist the item immediately, and queue the tool execution.
+1 -1
View File
@@ -375,7 +375,7 @@ impl Session {
otel.name = span_name,
thread.id = %self.thread_id,
turn.id = %turn_context.sub_id,
model = %turn_context.model_info.slug,
model = %turn_context.model_info().slug,
codex.turn.reasoning_effort = %reasoning_effort,
codex.turn.token_usage.input_tokens = field::Empty,
codex.turn.token_usage.cached_input_tokens = field::Empty,
+1 -1
View File
@@ -52,7 +52,7 @@ impl SessionTask for RegularTask {
trace_id: ctx.trace_id.clone(),
started_at: ctx.turn_timing_state.started_at_unix_secs().await,
model_context_window: ctx.model_context_window(),
collaboration_mode_kind: ctx.mode,
collaboration_mode_kind: ctx.mode(),
});
sess.send_event(ctx.as_ref(), event).await;
sess.set_server_reasoning_included(/*included*/ false).await;
+1 -1
View File
@@ -121,7 +121,7 @@ async fn start_review_conversation(
let model = config
.review_model
.clone()
.unwrap_or_else(|| ctx.model_info.slug.clone());
.unwrap_or_else(|| ctx.model_info().slug.clone());
sub_agent_config.model = Some(model);
(run_codex_thread_one_shot(
sub_agent_config,
+3 -3
View File
@@ -122,7 +122,7 @@ pub(crate) async fn execute_user_shell_command(
trace_id: turn_context.trace_id.clone(),
started_at: turn_context.turn_timing_state.started_at_unix_secs().await,
model_context_window: turn_context.model_context_window(),
collaboration_mode_kind: turn_context.mode,
collaboration_mode_kind: turn_context.mode(),
});
session.send_event(turn_context.as_ref(), event).await;
}
@@ -312,7 +312,7 @@ pub(crate) async fn execute_user_shell_command(
duration: Some(output.duration),
formatted_output: Some(format_exec_output_str(
&output,
turn_context.model_info.truncation_policy.into(),
turn_context.model_info().truncation_policy.into(),
)),
}),
)
@@ -353,7 +353,7 @@ pub(crate) async fn execute_user_shell_command(
duration: Some(exec_output.duration),
formatted_output: Some(format_exec_output_str(
&exec_output,
turn_context.model_info.truncation_policy.into(),
turn_context.model_info().truncation_policy.into(),
)),
}),
)
+4 -2
View File
@@ -1561,12 +1561,13 @@ async fn resume_and_fork_do_not_restore_thread_environments_from_rollout() {
)
.await
.expect("resume source thread");
let (resumed_turn, _) = resumed
let (prepared_turn, _) = resumed
.thread
.session
.new_turn_with_sub_id("resume-turn".to_string(), SessionSettingsUpdate::default())
.await
.expect("build resumed turn context");
let resumed_turn = prepared_turn;
assert_eq!(resumed_turn.environments.turn_environments().count(), 1);
assert_eq!(
resumed_turn
@@ -1595,12 +1596,13 @@ async fn resume_and_fork_do_not_restore_thread_environments_from_rollout() {
)
.await
.expect("fork source thread");
let (forked_turn, _) = forked
let (prepared_turn, _) = forked
.thread
.session
.new_turn_with_sub_id("fork-turn".to_string(), SessionSettingsUpdate::default())
.await
.expect("build forked turn context");
let forked_turn = prepared_turn;
assert_eq!(forked_turn.environments.turn_environments().count(), 1);
assert_eq!(
forked_turn
+1 -1
View File
@@ -497,7 +497,7 @@ impl Session {
_ => {}
}
}
resolution.into_tool_result(&ctx.review_context.turn().model_info)
resolution.into_tool_result(ctx.review_context.turn().model_info())
}
async fn request_reviewer_approval(
+1 -1
View File
@@ -277,7 +277,7 @@ pub(super) async fn handle_runtime_response(
}
fn sanitize_runtime_image_detail(turn: &TurnContext, items: &mut [FunctionCallOutputContentItem]) {
sanitize_image_detail_items(can_request_original_image_detail(&turn.model_info), items);
sanitize_image_detail_items(can_request_original_image_detail(turn.model_info()), items);
}
fn format_script_status(response: &RuntimeResponse) -> String {
+3 -3
View File
@@ -132,7 +132,7 @@ async fn emit_exec_command_begin(ctx: ToolEventCtx<'_>, exec_input: &ExecCommand
.analytics_events_client
.track_artifact_operation(
build_track_events_context(
ctx.turn.model_info.slug.clone(),
ctx.turn.model_info().slug.clone(),
ctx.session.thread_id.to_string(),
ctx.turn.sub_id.clone(),
ctx.turn.originator.clone(),
@@ -373,7 +373,7 @@ impl ToolEmitter {
output: &ExecToolCallOutput,
ctx: ToolEventCtx<'_>,
) -> String {
super::format_exec_output_for_model(output, ctx.turn.model_info.truncation_policy.into())
super::format_exec_output_for_model(output, ctx.turn.model_info().truncation_policy.into())
}
pub async fn finish(
@@ -521,7 +521,7 @@ async fn emit_exec_stage(
duration: output.duration,
formatted_output: format_exec_output_str(
&output,
ctx.turn.model_info.truncation_policy.into(),
ctx.turn.model_info().truncation_policy.into(),
),
status: if output.exit_code == 0 {
ExecCommandStatus::Completed
@@ -162,7 +162,7 @@ async fn to_extension_call(invocation: &ToolInvocation) -> ExtensionToolCall {
.turn
.turn_metadata_state
.current_meta_value_for_mcp_request(McpTurnMetadataContext {
model: invocation.turn.model_info.slug.as_str(),
model: invocation.turn.model_info().slug.as_str(),
reasoning_effort: invocation.turn.effective_reasoning_effort(),
})
.and_then(|metadata| to_ascii_json_string(&metadata).ok());
@@ -194,9 +194,9 @@ async fn to_extension_call(invocation: &ToolInvocation) -> ExtensionToolCall {
turn_id: invocation.turn.sub_id.clone(),
call_id: invocation.call_id.clone(),
tool_name: invocation.tool_name.clone(),
model: invocation.turn.model_info.slug.clone(),
model: invocation.turn.model_info().slug.clone(),
codex_turn_metadata,
truncation_policy: invocation.turn.model_info.truncation_policy.into(),
truncation_policy: invocation.turn.model_info().truncation_policy.into(),
source: extension_tool_call_source(invocation.source.clone()),
conversation_history,
turn_item_emitter: Arc::new(CoreTurnItemEmitter {
@@ -388,8 +388,8 @@ mod tests {
let weak_session = Arc::downgrade(&session);
let weak_turn = Arc::downgrade(&turn);
let turn_id = turn.sub_id.clone();
let model = turn.model_info.slug.clone();
let truncation_policy = turn.model_info.truncation_policy.into();
let model = turn.model_info().slug.clone();
let truncation_policy = turn.model_info().truncation_policy.into();
let expected_sandbox_cwds = turn
.environments
.turn_environments()
+2 -2
View File
@@ -208,8 +208,8 @@ impl McpHandler {
result: result.result,
tool_input: result.tool_input,
wall_time: started.elapsed(),
original_image_detail_supported: can_request_original_image_detail(&turn.model_info),
truncation_policy: turn.model_info.truncation_policy.into(),
original_image_detail_supported: can_request_original_image_detail(turn.model_info()),
truncation_policy: turn.model_info().truncation_policy.into(),
}))
}
}
@@ -290,7 +290,7 @@ where
emit_tool_call_begin(session, turn, call_id, invocation.clone()).await;
let start = Instant::now();
let result = operation.await.and_then(|payload| {
serialize_function_output(payload, turn.model_info.truncation_policy.into())
serialize_function_output(payload, turn.model_info().truncation_policy.into())
});
match result {
@@ -194,13 +194,13 @@ pub(crate) fn build_agent_resume_config(turn: &TurnContext) -> Result<Config, Fu
fn build_agent_shared_config(turn: &TurnContext) -> Result<Config, FunctionCallError> {
let base_config = turn.config.clone();
let mut config = (*base_config).clone();
config.model = Some(turn.model_info.slug.clone());
config.model = Some(turn.model_info().slug.clone());
config.model_provider = turn.provider.info().clone();
config.model_reasoning_effort = turn
.reasoning_effort
.clone()
.or_else(|| turn.model_info.default_reasoning_level.clone());
config.model_reasoning_summary = Some(turn.reasoning_summary);
.reasoning_effort()
.or(turn.model_info().default_reasoning_level.as_ref())
.cloned();
config.model_reasoning_summary = Some(turn.reasoning_summary());
config.developer_instructions = turn.developer_instructions.clone();
if turn.multi_agent_version == MultiAgentVersion::V2
&& let Some(developer_instructions) = turn
@@ -308,8 +308,8 @@ pub(crate) async fn apply_requested_spawn_agent_model_overrides(
if let Some(reasoning_effort) = requested_reasoning_effort {
validate_spawn_agent_reasoning_effort(
&turn.model_info.slug,
&turn.model_info.supported_reasoning_levels,
&turn.model_info().slug,
&turn.model_info().supported_reasoning_levels,
&reasoning_effort,
)?;
config.model_reasoning_effort = Some(reasoning_effort);
@@ -386,11 +386,9 @@ async fn multi_agent_v2_spawn_fork_turns_all_applies_agent_type_override() {
.features
.enable(Feature::MultiAgentV2)
.expect("test config should allow feature update");
let turn = TurnContext {
config: Arc::new(config),
multi_agent_version: codex_protocol::protocol::MultiAgentVersion::V2,
..turn
};
let mut turn = turn;
turn.config = Arc::new(config);
turn.multi_agent_version = codex_protocol::protocol::MultiAgentVersion::V2;
SpawnAgentHandlerV2::default()
.handle(invocation(
@@ -915,11 +913,9 @@ async fn multi_agent_v2_spawn_partial_fork_turns_allows_agent_type_override() {
.features
.enable(Feature::MultiAgentV2)
.expect("test config should allow feature update");
let turn = TurnContext {
config: Arc::new(config),
multi_agent_version: codex_protocol::protocol::MultiAgentVersion::V2,
..turn
};
let mut turn = turn;
turn.config = Arc::new(config);
turn.multi_agent_version = codex_protocol::protocol::MultiAgentVersion::V2;
let parent_provider_id = turn.config.model_provider_id.clone();
let output = SpawnAgentHandlerV2::default()
@@ -4534,7 +4530,7 @@ async fn build_agent_spawn_config_uses_turn_context_values() {
let base_instructions = BaseInstructions {
text: "base".to_string(),
provenance: Some(BaseInstructionsProvenance::Model {
model: turn.model_info.slug.clone(),
model: turn.model_info().slug.clone(),
}),
};
turn.developer_instructions = Some("dev".to_string());
@@ -4581,10 +4577,10 @@ async fn build_agent_spawn_config_uses_turn_context_values() {
let mut expected = (*turn.config).clone();
expected.base_instructions_provenance = base_instructions.provenance.clone();
expected.base_instructions = Some(base_instructions.text);
expected.model = Some(turn.model_info.slug.clone());
expected.model = Some(turn.model_info().slug.clone());
expected.model_provider = turn.provider.info().clone();
expected.model_reasoning_effort = turn.reasoning_effort.clone();
expected.model_reasoning_summary = Some(turn.reasoning_summary);
expected.model_reasoning_effort = turn.reasoning_effort().cloned();
expected.model_reasoning_summary = Some(turn.reasoning_summary());
expected.developer_instructions = turn.developer_instructions.clone();
#[allow(deprecated)]
{
@@ -4604,7 +4600,7 @@ async fn build_agent_resume_config_clears_base_instructions() {
let mut base_config = (*turn.config).clone();
base_config.base_instructions = Some("caller-base".to_string());
base_config.base_instructions_provenance = Some(BaseInstructionsProvenance::Model {
model: turn.model_info.slug.clone(),
model: turn.model_info().slug.clone(),
});
turn.config = Arc::new(base_config);
Arc::make_mut(&mut turn.config)
@@ -4634,10 +4630,10 @@ async fn build_agent_resume_config_clears_base_instructions() {
let mut expected = (*turn.config).clone();
expected.base_instructions = None;
expected.base_instructions_provenance = None;
expected.model = Some(turn.model_info.slug.clone());
expected.model = Some(turn.model_info().slug.clone());
expected.model_provider = turn.provider.info().clone();
expected.model_reasoning_effort = turn.reasoning_effort.clone();
expected.model_reasoning_summary = Some(turn.reasoning_summary);
expected.model_reasoning_effort = turn.reasoning_effort().cloned();
expected.model_reasoning_summary = Some(turn.reasoning_summary());
expected.developer_instructions = turn.developer_instructions.clone();
#[allow(deprecated)]
{
@@ -186,7 +186,7 @@ async fn handle_spawn_agent(
let multi_agent_v2_usage_hints =
if is_full_history_fork && turn.multi_agent_version == MultiAgentVersion::V2 {
let child_model_info = match config.model.as_deref() {
Some(model) if model != turn.model_info.slug => Some(
Some(model) if model != turn.model_info().slug => Some(
session
.services
.models_manager
@@ -197,7 +197,7 @@ async fn handle_spawn_agent(
};
let child_catalog = child_model_info
.as_ref()
.unwrap_or(&turn.model_info)
.unwrap_or(turn.model_info())
.model_messages
.as_ref()
.and_then(|messages| messages.multi_agent.as_ref())
+1 -1
View File
@@ -81,7 +81,7 @@ impl PlanHandler {
}
};
if turn.mode == ModeKind::Plan {
if turn.mode() == ModeKind::Plan {
return Err(FunctionCallError::RespondToModel(
"update_plan is a TODO/checklist tool and is not allowed in Plan mode".to_string(),
));
@@ -217,7 +217,7 @@ impl RequestPluginInstallHandler {
.analytics_events_client
.track_plugin_install_requested(
build_track_events_context(
turn.model_info.slug.clone(),
turn.model_info().slug.clone(),
session.thread_id.to_string(),
turn.sub_id.clone(),
turn.originator.clone(),
@@ -2,6 +2,8 @@ use super::*;
use crate::session::step_context::StepContext;
use crate::session::tests::make_session_and_context;
use crate::session::tests::make_session_and_context_with_rx;
use crate::session::tests::update_selected_settings_for_test;
use crate::session::tests::update_turn_settings_for_test;
use crate::state::ActiveTurn;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
@@ -224,9 +226,14 @@ async fn guardian_user_input_evidence_is_bounded() {
#[tokio::test]
async fn request_user_input_sets_blocking_from_turn_mode() {
let (session, mut turn, events) = make_session_and_context_with_rx().await;
Arc::get_mut(&mut turn)
.expect("turn context should be uniquely owned")
.mode = ModeKind::Plan;
update_turn_settings_for_test(
Arc::get_mut(&mut turn).expect("turn context should be uniquely owned"),
|settings| {
update_selected_settings_for_test(settings, |selected| {
selected.collaboration_mode.mode = ModeKind::Plan;
});
},
);
*session.active_turn.lock().await = Some(ActiveTurn::default());
let request = tokio::spawn({
@@ -345,7 +345,7 @@ impl ExecCommandHandler {
chunk_id: String::new(),
wall_time: std::time::Duration::ZERO,
raw_output: output.into_text().into_bytes(),
truncation_policy: turn.model_info.truncation_policy.into(),
truncation_policy: turn.model_info().truncation_policy.into(),
max_output_tokens,
process_id: None,
exit_code: None,
@@ -397,7 +397,7 @@ impl ExecCommandHandler {
chunk_id: generate_chunk_id(),
wall_time: output.duration,
raw_output: output_text.into_bytes(),
truncation_policy: turn.model_info.truncation_policy.into(),
truncation_policy: turn.model_info().truncation_policy.into(),
max_output_tokens,
// Sandbox denial is terminal, so there is no live
// process for write_stdin to resume.
@@ -78,7 +78,7 @@ impl WriteStdinHandler {
input: &args.chars,
yield_time_ms: args.yield_time_ms,
max_output_tokens: args.max_output_tokens,
truncation_policy: turn.model_info.truncation_policy.into(),
truncation_policy: turn.model_info().truncation_policy.into(),
interaction_event: Some(WriteStdinInteractionEvent {
session: &session,
turn: &turn,
@@ -93,7 +93,7 @@ impl ViewImageHandler {
) -> Result<Box<dyn crate::tools::context::ToolOutput>, FunctionCallError> {
if !invocation
.turn
.model_info
.model_info()
.input_modalities
.contains(&InputModality::Image)
{
@@ -182,7 +182,7 @@ impl ViewImageHandler {
FunctionCallError::RespondToModel(VIEW_IMAGE_INVALID_MESSAGE.to_string())
})?;
let can_request_original_detail = can_request_original_image_detail(&turn.model_info);
let can_request_original_detail = can_request_original_image_detail(turn.model_info());
let use_original_detail = self.options.unified_image_budget
|| can_request_original_detail && matches!(detail, Some(ViewImageDetail::Original));
let image_detail = if use_original_detail {
+1 -1
View File
@@ -65,7 +65,7 @@ pub(crate) fn tool_user_shell_type(
}
pub(crate) fn requested_tool_mode(turn_context: &TurnContext) -> ToolMode {
turn_context.model_info.tool_mode.unwrap_or_else(|| {
turn_context.model_info().tool_mode.unwrap_or_else(|| {
if turn_context.config.features.enabled(Feature::CodeModeOnly) {
ToolMode::CodeModeOnly
} else if turn_context.config.features.enabled(Feature::CodeMode) {
@@ -365,7 +365,7 @@ impl CoreShellActionProvider {
}
ReviewDecision::TimedOut => EscalationDecision::deny(Some(
crate::guardian::guardian_timeout_message(
&self.review_context.turn().model_info,
self.review_context.turn().model_info(),
),
)),
ReviewDecision::ApprovedMcpPolicyAmendment => {
+21 -18
View File
@@ -373,7 +373,7 @@ pub(crate) fn finalize_tool_router(
.config
.tool_registry
.turn_metadata_includes_tool_info
&& turn_context.model_info.use_responses_lite;
&& turn_context.model_info().use_responses_lite;
if turn_context.config.tool_registry.error_on_tool_collisions {
if let Some(tool_name) = registry.first_collision() {
@@ -547,7 +547,7 @@ fn hosted_model_tool_specs(
registered_extension_tool_names: &[ToolName],
) -> Vec<ToolSpec> {
// Responses Lite accepts schemas for client-executed tools, not hosted Responses tools.
if turn_context.model_info.use_responses_lite
if turn_context.model_info().use_responses_lite
|| crate::guardian::is_basic_session_source(&turn_context.session_source)
{
return Vec::new();
@@ -567,7 +567,7 @@ fn hosted_model_tool_specs(
if let Some(hosted_web_search_tool) = create_web_search_tool(WebSearchToolOptions {
web_search_mode,
web_search_config,
web_search_tool_type: turn_context.model_info.web_search_tool_type,
web_search_tool_type: turn_context.model_info().web_search_tool_type,
}) {
specs.push(hosted_web_search_tool);
}
@@ -575,7 +575,7 @@ fn hosted_model_tool_specs(
}
pub(crate) fn search_tool_enabled(turn_context: &TurnContext) -> bool {
turn_context.model_info.supports_search_tool && namespace_tools_enabled(turn_context)
turn_context.model_info().supports_search_tool && namespace_tools_enabled(turn_context)
}
pub(crate) fn tool_suggest_enabled(turn_context: &TurnContext) -> bool {
@@ -602,7 +602,7 @@ fn collab_tools_enabled(turn_context: &TurnContext) -> bool {
),
MultiAgentVersion::V2 => {
turn_context.session_source.get_agent_path().is_none()
|| turn_context.model_info.multi_agent_version == Some(MultiAgentVersion::V2)
|| turn_context.model_info().multi_agent_version == Some(MultiAgentVersion::V2)
}
}
}
@@ -633,7 +633,7 @@ fn image_generation_available(turn_context: &TurnContext) -> bool {
}
if !turn_context
.model_info
.model_info()
.input_modalities
.contains(&InputModality::Image)
{
@@ -794,8 +794,10 @@ fn register_code_mode_executors(
&namespace_descriptions,
turn_context.config.code_mode.default_exec_yield_time_ms,
tool_mode == ToolMode::CodeModeOnly,
if unified_image_budget_enabled(&turn_context.config.features, &turn_context.model_info)
{
if unified_image_budget_enabled(
&turn_context.config.features,
turn_context.model_info(),
) {
codex_code_mode::ImageDetailVisibility::Hidden
} else {
codex_code_mode::ImageDetailVisibility::Visible
@@ -908,7 +910,7 @@ fn add_core_tool_sources(context: &CoreToolPlanContext<'_>, registry: &mut ToolR
if turn_context.config.features.enabled(Feature::ShellTool)
&& turn_context.config.features.enabled(Feature::UnifiedExec)
&& !matches!(
turn_context.model_info.shell_type,
turn_context.model_info().shell_type,
ConfigShellToolType::Disabled
)
{
@@ -926,11 +928,11 @@ fn add_core_tool_sources(context: &CoreToolPlanContext<'_>, registry: &mut ToolR
if turn_context.config.features.enabled(Feature::ViewImage) {
registry.add(ViewImageHandler::new(ViewImageToolOptions {
can_request_original_image_detail: can_request_original_image_detail(
&turn_context.model_info,
turn_context.model_info(),
),
unified_image_budget: unified_image_budget_enabled(
&turn_context.config.features,
&turn_context.model_info,
turn_context.model_info(),
),
include_environment_id,
}));
@@ -948,7 +950,7 @@ fn add_core_tool_sources(context: &CoreToolPlanContext<'_>, registry: &mut ToolR
fn standalone_web_search_enabled(turn_context: &TurnContext) -> bool {
namespace_tools_enabled(turn_context)
&& turn_context.provider.capabilities().web_search
&& (turn_context.model_info.use_responses_lite
&& (turn_context.model_info().use_responses_lite
|| turn_context
.config
.features
@@ -975,7 +977,7 @@ fn add_shell_tools(context: &CoreToolPlanContext<'_>, registry: &mut ToolRegistr
|| !features.enabled(Feature::ShellTool)
|| !features.enabled(Feature::UnifiedExec)
|| matches!(
turn_context.model_info.shell_type,
turn_context.model_info().shell_type,
ConfigShellToolType::Disabled
)
{
@@ -1051,7 +1053,7 @@ fn add_core_utility_tools(context: &CoreToolPlanContext<'_>, registry: &mut Tool
if !turn_context.session_source.is_non_root_agent()
&& turn_context
.model_info
.model_info()
.experimental_supported_tools
.iter()
.any(|tool| tool == "send_user_message_async")
@@ -1096,14 +1098,15 @@ fn add_core_utility_tools(context: &CoreToolPlanContext<'_>, registry: &mut Tool
));
}
if environment_mode.has_environment() && turn_context.model_info.apply_patch_tool_type.is_some()
if environment_mode.has_environment()
&& turn_context.model_info().apply_patch_tool_type.is_some()
{
let include_environment_id = matches!(environment_mode, ToolEnvironmentMode::Multiple);
registry.add(ApplyPatchHandler::new(include_environment_id));
}
if turn_context
.model_info
.model_info()
.experimental_supported_tools
.iter()
.any(|tool| tool == "test_sync_tool")
@@ -1115,11 +1118,11 @@ fn add_core_utility_tools(context: &CoreToolPlanContext<'_>, registry: &mut Tool
let include_environment_id = matches!(environment_mode, ToolEnvironmentMode::Multiple);
registry.add(ViewImageHandler::new(ViewImageToolOptions {
can_request_original_image_detail: can_request_original_image_detail(
&turn_context.model_info,
turn_context.model_info(),
),
unified_image_budget: unified_image_budget_enabled(
&turn_context.config.features,
&turn_context.model_info,
turn_context.model_info(),
),
include_environment_id,
}));
+128 -43
View File
@@ -1,3 +1,4 @@
use crate::session::tests::update_turn_settings_for_test;
use std::collections::BTreeMap;
use std::sync::Arc;
@@ -532,7 +533,9 @@ async fn internal_guardian_sessions_respect_managed_shell_restrictions() {
)
.expect("managed shell restriction should be valid");
}
Arc::make_mut(&mut turn.model_info).shell_type = shell_type;
update_turn_settings_for_test(&mut turn, |settings| {
Arc::make_mut(&mut settings.model_info).shell_type = shell_type;
});
let turn = Arc::new(turn);
let step_context = StepContext::for_test(Arc::clone(&turn));
@@ -850,7 +853,9 @@ async fn shell_family_registers_only_unified_exec_tools() {
let plan = probe(|turn| {
set_features(turn, &[Feature::ShellTool]);
set_feature(turn, Feature::ShellZshFork, /*enabled*/ false);
Arc::make_mut(&mut turn.model_info).shell_type = ConfigShellToolType::UnifiedExec;
update_turn_settings_for_test(turn, |settings| {
Arc::make_mut(&mut settings.model_info).shell_type = ConfigShellToolType::UnifiedExec;
});
})
.await;
@@ -866,7 +871,10 @@ async fn login_shell_parameter_follows_selected_environment() {
let plan = probe(|turn| {
set_feature(turn, Feature::ShellTool, /*enabled*/ true);
set_feature(turn, Feature::ShellZshFork, /*enabled*/ false);
Arc::make_mut(&mut turn.model_info).shell_type = ConfigShellToolType::UnifiedExec;
update_turn_settings_for_test(turn, |settings| {
Arc::make_mut(&mut settings.model_info).shell_type =
ConfigShellToolType::UnifiedExec;
});
update_config(turn, |config| {
config.permissions.allow_login_shell = !allow_login_shell;
});
@@ -922,7 +930,9 @@ async fn login_shell_parameter_is_available_when_any_environment_allows_it() {
async fn disabling_shell_tools_disables_command_tools_for_all_environments() {
let remote_environment = probe(|turn| {
set_feature(turn, Feature::ShellTool, /*enabled*/ false);
Arc::make_mut(&mut turn.model_info).shell_type = ConfigShellToolType::UnifiedExec;
update_turn_settings_for_test(turn, |settings| {
Arc::make_mut(&mut settings.model_info).shell_type = ConfigShellToolType::UnifiedExec;
});
let TurnEnvironmentState::Ready(environment) = turn
.environments
@@ -946,7 +956,9 @@ async fn disabling_shell_tools_disables_command_tools_for_all_environments() {
let multiple_local_environments = probe(|turn| {
set_feature(turn, Feature::ShellTool, /*enabled*/ false);
Arc::make_mut(&mut turn.model_info).shell_type = ConfigShellToolType::UnifiedExec;
update_turn_settings_for_test(turn, |settings| {
Arc::make_mut(&mut settings.model_info).shell_type = ConfigShellToolType::UnifiedExec;
});
duplicate_primary_environment(turn);
})
.await;
@@ -988,7 +1000,9 @@ async fn shell_zsh_fork_keeps_unified_exec_available() {
set_features(turn, &[Feature::ShellTool]);
set_feature(turn, Feature::ShellZshFork, /*enabled*/ true);
set_feature(turn, Feature::UnifiedExecZshFork, /*enabled*/ false);
Arc::make_mut(&mut turn.model_info).shell_type = ConfigShellToolType::UnifiedExec;
update_turn_settings_for_test(turn, |settings| {
Arc::make_mut(&mut settings.model_info).shell_type = ConfigShellToolType::UnifiedExec;
});
})
.await;
@@ -1004,7 +1018,9 @@ async fn shell_zsh_fork_keeps_unified_exec_available() {
Feature::UnifiedExecZshFork,
],
);
Arc::make_mut(&mut turn.model_info).shell_type = ConfigShellToolType::UnifiedExec;
update_turn_settings_for_test(turn, |settings| {
Arc::make_mut(&mut settings.model_info).shell_type = ConfigShellToolType::UnifiedExec;
});
})
.await;
@@ -1116,8 +1132,10 @@ async fn environment_count_controls_environment_backed_tools() {
turn.environments.environments.clear();
set_feature(turn, Feature::ShellTool, /*enabled*/ true);
set_feature(turn, Feature::RequestPermissionsTool, /*enabled*/ true);
Arc::make_mut(&mut turn.model_info).apply_patch_tool_type =
Some(ApplyPatchToolType::Freeform);
update_turn_settings_for_test(turn, |settings| {
Arc::make_mut(&mut settings.model_info).apply_patch_tool_type =
Some(ApplyPatchToolType::Freeform);
});
})
.await;
no_environment.assert_visible_lacks(&[
@@ -1139,8 +1157,10 @@ async fn environment_count_controls_environment_backed_tools() {
duplicate_primary_environment(turn);
set_feature(turn, Feature::ShellTool, /*enabled*/ true);
set_feature(turn, Feature::RequestPermissionsTool, /*enabled*/ true);
Arc::make_mut(&mut turn.model_info).apply_patch_tool_type =
Some(ApplyPatchToolType::Freeform);
update_turn_settings_for_test(turn, |settings| {
Arc::make_mut(&mut settings.model_info).apply_patch_tool_type =
Some(ApplyPatchToolType::Freeform);
});
})
.await;
multiple_environments.assert_visible_contains(&[
@@ -1165,7 +1185,10 @@ async fn environment_count_controls_environment_backed_tools() {
#[tokio::test]
async fn environment_tools_follow_the_step_context() {
let (_session, mut turn) = make_session_and_context().await;
Arc::make_mut(&mut turn.model_info).apply_patch_tool_type = Some(ApplyPatchToolType::Freeform);
update_turn_settings_for_test(&mut turn, |settings| {
Arc::make_mut(&mut settings.model_info).apply_patch_tool_type =
Some(ApplyPatchToolType::Freeform);
});
let environments = turn.environments.clone();
turn.environments.environments.clear();
@@ -1295,7 +1318,9 @@ async fn mcp_and_tool_search_follow_direct_and_deferred_tool_exposure() {
let missing_model_capability = probe_with(
|turn| {
Arc::make_mut(&mut turn.model_info).supports_search_tool = false;
update_turn_settings_for_test(turn, |settings| {
Arc::make_mut(&mut settings.model_info).supports_search_tool = false;
});
},
searchable_mcp(),
)
@@ -1304,7 +1329,9 @@ async fn mcp_and_tool_search_follow_direct_and_deferred_tool_exposure() {
let missing_deferred_tools = probe(|turn| {
set_feature(turn, Feature::Collab, /*enabled*/ false);
Arc::make_mut(&mut turn.model_info).supports_search_tool = true;
update_turn_settings_for_test(turn, |settings| {
Arc::make_mut(&mut settings.model_info).supports_search_tool = true;
});
})
.await;
missing_deferred_tools.assert_visible_lacks(&["tool_search"]);
@@ -1316,7 +1343,9 @@ async fn mcp_and_tool_search_follow_direct_and_deferred_tool_exposure() {
let bedrock_namespace_capability = probe_with(
|turn| {
Arc::make_mut(&mut turn.model_info).supports_search_tool = true;
update_turn_settings_for_test(turn, |settings| {
Arc::make_mut(&mut settings.model_info).supports_search_tool = true;
});
use_bedrock_provider(turn);
},
searchable_mcp(),
@@ -1326,7 +1355,9 @@ async fn mcp_and_tool_search_follow_direct_and_deferred_tool_exposure() {
let enabled = probe_with(
|turn| {
Arc::make_mut(&mut turn.model_info).supports_search_tool = true;
update_turn_settings_for_test(turn, |settings| {
Arc::make_mut(&mut settings.model_info).supports_search_tool = true;
});
},
searchable_mcp(),
)
@@ -1340,7 +1371,9 @@ async fn mcp_and_tool_search_follow_direct_and_deferred_tool_exposure() {
let reserved_namespace = probe_with(
|turn| {
set_feature(turn, Feature::CodeMode, /*enabled*/ true);
Arc::make_mut(&mut turn.model_info).supports_search_tool = true;
update_turn_settings_for_test(turn, |settings| {
Arc::make_mut(&mut settings.model_info).supports_search_tool = true;
});
},
ToolPlanInputs {
tool_runtimes: vec![
@@ -1392,8 +1425,10 @@ async fn tool_namespaces_info_is_opt_in_and_tracks_mcp_exposure() {
config.tool_registry.turn_metadata_includes_tool_info = enabled;
});
set_feature(turn, Feature::CodeMode, /*enabled*/ true);
Arc::make_mut(&mut turn.model_info).supports_search_tool = true;
Arc::make_mut(&mut turn.model_info).use_responses_lite = true;
update_turn_settings_for_test(turn, |settings| {
Arc::make_mut(&mut settings.model_info).supports_search_tool = true;
Arc::make_mut(&mut settings.model_info).use_responses_lite = true;
});
metadata_state = Some(Arc::clone(&turn.turn_metadata_state));
},
ToolPlanInputs {
@@ -1467,7 +1502,9 @@ async fn strict_namespace_ownership_requires_tool_namespace_inventory_opt_in() {
config.tool_registry.error_on_tool_collisions = true;
config.tool_registry.turn_metadata_includes_tool_info = enabled;
});
Arc::make_mut(&mut turn.model_info).use_responses_lite = true;
update_turn_settings_for_test(&mut turn, |settings| {
Arc::make_mut(&mut settings.model_info).use_responses_lite = true;
});
let step_context = StepContext::for_test(Arc::new(turn));
let mut registry = build_core_tool_registry(
step_context.turn.as_ref(),
@@ -1522,7 +1559,10 @@ async fn unified_tool_runtimes_preserve_source_order_and_collision_priority() {
|turn| {
set_features(turn, &[Feature::ShellTool]);
set_feature(turn, Feature::ShellZshFork, /*enabled*/ false);
Arc::make_mut(&mut turn.model_info).shell_type = ConfigShellToolType::UnifiedExec;
update_turn_settings_for_test(turn, |settings| {
Arc::make_mut(&mut settings.model_info).shell_type =
ConfigShellToolType::UnifiedExec;
});
},
ToolPlanInputs {
tool_runtimes: vec![mcp_runtime(
@@ -1718,7 +1758,9 @@ async fn strict_tool_collisions_reject_external_and_synthetic_duplicates() {
if code_mode_enabled {
set_feature(&mut turn, Feature::CodeMode, /*enabled*/ true);
}
Arc::make_mut(&mut turn.model_info).supports_search_tool = search_enabled;
update_turn_settings_for_test(&mut turn, |settings| {
Arc::make_mut(&mut settings.model_info).supports_search_tool = search_enabled;
});
let turn = Arc::new(turn);
let step_context = StepContext::for_test(Arc::clone(&turn));
let mut registry = build_core_tool_registry(
@@ -1876,8 +1918,10 @@ async fn code_mode_uses_the_first_normalized_tool_identity() {
if code_mode_only {
set_feature(turn, Feature::CodeModeOnly, /*enabled*/ true);
}
Arc::make_mut(&mut turn.model_info).supports_search_tool = true;
Arc::make_mut(&mut turn.model_info).use_responses_lite = true;
update_turn_settings_for_test(turn, |settings| {
Arc::make_mut(&mut settings.model_info).supports_search_tool = true;
Arc::make_mut(&mut settings.model_info).use_responses_lite = true;
});
metadata_state = Some(Arc::clone(&turn.turn_metadata_state));
},
ToolPlanInputs {
@@ -1960,7 +2004,9 @@ async fn code_mode_uses_the_first_normalized_tool_identity() {
async fn deferred_extension_tools_are_discoverable_with_tool_search() {
let plan = probe_with(
|turn| {
Arc::make_mut(&mut turn.model_info).supports_search_tool = true;
update_turn_settings_for_test(turn, |settings| {
Arc::make_mut(&mut settings.model_info).supports_search_tool = true;
});
},
ToolPlanInputs {
extension_tool_executors: vec![Arc::new(DeferredExtensionTool)],
@@ -1980,7 +2026,9 @@ async fn tool_search_cache_rebuilds_when_deferred_sources_change() {
let cache = ToolSearchHandlerCache::default();
let (_session, mut first_turn) = make_session_and_context().await;
Arc::make_mut(&mut first_turn.model_info).supports_search_tool = true;
update_turn_settings_for_test(&mut first_turn, |settings| {
Arc::make_mut(&mut settings.model_info).supports_search_tool = true;
});
let first_turn = Arc::new(first_turn);
let first_step_context = StepContext::for_test(Arc::clone(&first_turn));
let mut first_registry = build_core_tool_registry(
@@ -2001,7 +2049,9 @@ async fn tool_search_cache_rebuilds_when_deferred_sources_change() {
let first_plan = ToolPlanProbe::from_router(first_router);
let (_session, mut second_turn) = make_session_and_context().await;
Arc::make_mut(&mut second_turn.model_info).supports_search_tool = true;
update_turn_settings_for_test(&mut second_turn, |settings| {
Arc::make_mut(&mut settings.model_info).supports_search_tool = true;
});
let second_turn = Arc::new(second_turn);
let second_step_context = StepContext::for_test(Arc::clone(&second_turn));
let mut second_registry = build_core_tool_registry(
@@ -2048,7 +2098,9 @@ async fn tool_search_cache_rebuilds_when_deferred_world_state_changes() {
for world_state_enabled in [false, true, false] {
let (_session, mut turn) = make_session_and_context().await;
Arc::make_mut(&mut turn.model_info).supports_search_tool = true;
update_turn_settings_for_test(&mut turn, |settings| {
Arc::make_mut(&mut settings.model_info).supports_search_tool = true;
});
set_feature(
&mut turn,
Feature::DeferredToolWorldState,
@@ -2161,7 +2213,9 @@ async fn request_plugin_install_requires_all_discovery_features() {
async fn request_plugin_install_stays_visible_without_tool_search() {
let plan = probe_with(
|turn| {
Arc::make_mut(&mut turn.model_info).supports_search_tool = false;
update_turn_settings_for_test(turn, |settings| {
Arc::make_mut(&mut settings.model_info).supports_search_tool = false;
});
set_features(
turn,
&[Feature::ToolSuggest, Feature::Apps, Feature::Plugins],
@@ -2297,7 +2351,9 @@ async fn code_mode_only_exposes_configured_dynamic_namespace_directly() {
let plan = probe_with(
|turn| {
set_features(turn, &[Feature::CodeMode, Feature::CodeModeOnly]);
Arc::make_mut(&mut turn.model_info).supports_search_tool = true;
update_turn_settings_for_test(turn, |settings| {
Arc::make_mut(&mut settings.model_info).supports_search_tool = true;
});
update_config(turn, |config| {
config.code_mode.direct_only_tool_namespaces = vec!["direct_only".to_string()];
});
@@ -2361,7 +2417,9 @@ async fn excluded_deferred_namespaces_do_not_enable_nested_tool_guidance() {
|turn| {
set_features(turn, &[Feature::CodeMode, Feature::CodeModeOnly]);
set_feature(turn, Feature::Collab, /*enabled*/ false);
Arc::make_mut(&mut turn.model_info).supports_search_tool = true;
update_turn_settings_for_test(turn, |settings| {
Arc::make_mut(&mut settings.model_info).supports_search_tool = true;
});
update_config(turn, |config| {
config.code_mode.excluded_tool_namespaces = vec!["excluded".to_string()];
});
@@ -2615,7 +2673,9 @@ async fn multi_agent_v2_can_disable_wait_agent() {
async fn tool_mode_selector_overrides_feature_flags() {
let direct = probe(|turn| {
set_features(turn, &[Feature::CodeMode, Feature::CodeModeOnly]);
Arc::make_mut(&mut turn.model_info).tool_mode = Some(ToolMode::Direct);
update_turn_settings_for_test(turn, |settings| {
Arc::make_mut(&mut settings.model_info).tool_mode = Some(ToolMode::Direct);
});
})
.await;
direct.assert_visible_lacks(&[
@@ -2627,7 +2687,9 @@ async fn tool_mode_selector_overrides_feature_flags() {
#[tokio::test]
async fn v1_multi_agent_tools_defer_when_tool_search_available() {
let plan = probe(|turn| {
Arc::make_mut(&mut turn.model_info).supports_search_tool = true;
update_turn_settings_for_test(turn, |settings| {
Arc::make_mut(&mut settings.model_info).supports_search_tool = true;
});
set_feature(turn, Feature::Collab, /*enabled*/ true);
set_feature(turn, Feature::MultiAgentV2, /*enabled*/ false);
})
@@ -2770,8 +2832,11 @@ async fn multi_agent_v2_bedrock_workers_only_delegate_when_model_supports_v2() {
config.multi_agent_v2.tool_namespace = Some("agents".to_string());
});
use_bedrock_provider(turn);
Arc::make_mut(&mut turn.model_info).slug = model.to_string();
Arc::make_mut(&mut turn.model_info).multi_agent_version = model_multi_agent_version;
update_turn_settings_for_test(turn, |settings| {
Arc::make_mut(&mut settings.model_info).slug = model.to_string();
Arc::make_mut(&mut settings.model_info).multi_agent_version =
model_multi_agent_version;
});
turn.session_source = SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
parent_thread_id: ThreadId::new(),
depth: 1,
@@ -2886,7 +2951,10 @@ async fn hosted_web_search_and_standalone_image_generation_follow_runtime_gates(
let image_generation = probe_with(
|turn| {
use_chatgpt_auth(turn);
Arc::make_mut(&mut turn.model_info).input_modalities = vec![InputModality::Image];
update_turn_settings_for_test(turn, |settings| {
Arc::make_mut(&mut settings.model_info).input_modalities =
vec![InputModality::Image];
});
},
ToolPlanInputs {
extension_tool_executors: vec![image_generation_tool.clone()],
@@ -2900,7 +2968,10 @@ async fn hosted_web_search_and_standalone_image_generation_follow_runtime_gates(
|turn| {
use_chatgpt_auth(turn);
set_feature(turn, Feature::ImageGeneration, /*enabled*/ false);
Arc::make_mut(&mut turn.model_info).input_modalities = vec![InputModality::Image];
update_turn_settings_for_test(turn, |settings| {
Arc::make_mut(&mut settings.model_info).input_modalities =
vec![InputModality::Image];
});
},
ToolPlanInputs {
extension_tool_executors: vec![image_generation_tool.clone()],
@@ -2913,7 +2984,9 @@ async fn hosted_web_search_and_standalone_image_generation_follow_runtime_gates(
let text_only_model = probe_with(
|turn| {
use_chatgpt_auth(turn);
Arc::make_mut(&mut turn.model_info).input_modalities = vec![];
update_turn_settings_for_test(turn, |settings| {
Arc::make_mut(&mut settings.model_info).input_modalities = vec![];
});
},
ToolPlanInputs {
extension_tool_executors: vec![image_generation_tool.clone()],
@@ -2926,7 +2999,10 @@ async fn hosted_web_search_and_standalone_image_generation_follow_runtime_gates(
let unsupported_provider = probe_with(
|turn| {
use_bedrock_provider(turn);
Arc::make_mut(&mut turn.model_info).input_modalities = vec![InputModality::Image];
update_turn_settings_for_test(turn, |settings| {
Arc::make_mut(&mut settings.model_info).input_modalities =
vec![InputModality::Image];
});
},
ToolPlanInputs {
extension_tool_executors: vec![image_generation_tool],
@@ -2938,7 +3014,10 @@ async fn hosted_web_search_and_standalone_image_generation_follow_runtime_gates(
let live_web_search = probe(|turn| {
set_web_search_mode(turn, WebSearchMode::Live);
Arc::make_mut(&mut turn.model_info).web_search_tool_type = WebSearchToolType::TextAndImage;
update_turn_settings_for_test(turn, |settings| {
Arc::make_mut(&mut settings.model_info).web_search_tool_type =
WebSearchToolType::TextAndImage;
});
})
.await;
assert_eq!(
@@ -2957,7 +3036,9 @@ async fn hosted_web_search_and_standalone_image_generation_follow_runtime_gates(
use_chatgpt_auth(turn);
set_features(turn, &[Feature::CodeModeOnly, Feature::MultiAgentV2]);
set_web_search_mode(turn, WebSearchMode::Live);
Arc::make_mut(&mut turn.model_info).input_modalities = vec![InputModality::Image];
update_turn_settings_for_test(turn, |settings| {
Arc::make_mut(&mut settings.model_info).input_modalities = vec![InputModality::Image];
});
})
.await;
assert_eq!(
@@ -3034,7 +3115,9 @@ async fn hosted_web_search_and_standalone_image_generation_follow_runtime_gates(
let bedrock_cached_web_search = probe(|turn| {
use_bedrock_provider(turn);
Arc::make_mut(&mut turn.model_info).web_search_tool_type = WebSearchToolType::Text;
update_turn_settings_for_test(turn, |settings| {
Arc::make_mut(&mut settings.model_info).web_search_tool_type = WebSearchToolType::Text;
});
})
.await;
assert_eq!(
@@ -3053,7 +3136,9 @@ async fn hosted_web_search_and_standalone_image_generation_follow_runtime_gates(
set_feature(turn, Feature::StandaloneWebSearch, /*enabled*/ true);
set_web_search_mode(turn, WebSearchMode::Cached);
use_bedrock_provider(turn);
Arc::make_mut(&mut turn.model_info).web_search_tool_type = WebSearchToolType::Text;
update_turn_settings_for_test(turn, |settings| {
Arc::make_mut(&mut settings.model_info).web_search_tool_type = WebSearchToolType::Text;
});
})
.await;
bedrock_with_standalone_web_search.assert_visible_contains(&["web_search"]);
+1 -1
View File
@@ -193,7 +193,7 @@ async fn exec_command_with_tty(
chunk_id: generate_chunk_id(),
wall_time,
raw_output: collected,
truncation_policy: turn.model_info.truncation_policy.into(),
truncation_policy: turn.model_info().truncation_policy.into(),
max_output_tokens: None,
process_id: response_process_id,
exit_code,
@@ -741,7 +741,7 @@ impl UnifiedExecProcessManager {
truncation_policy: context
.step_context
.turn
.model_info
.model_info()
.truncation_policy
.into(),
max_output_tokens: request.max_output_tokens,
+1 -1
View File
@@ -13,7 +13,7 @@ fn user_shell_command_fragment(
) -> UserShellCommand {
let output = format_exec_output_str(
exec_output,
turn_context.model_info.truncation_policy.into(),
turn_context.model_info().truncation_policy.into(),
);
UserShellCommand::new(command, exec_output.exit_code, exec_output.duration, output)
}
+94
View File
@@ -14,10 +14,13 @@ use codex_models_manager::bundled_models_response;
use codex_protocol::config_types::AutoCompactTokenLimitScope;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::config_types::ServiceTier;
use codex_protocol::config_types::Settings;
use codex_protocol::items::TurnItem;
use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::ModelInfo;
use codex_protocol::openai_models::ModelServiceTier;
use codex_protocol::openai_models::ModelsResponse;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
@@ -2280,6 +2283,97 @@ async fn pre_sampling_compact_runs_when_comp_hash_changes() {
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn previous_model_compaction_resolves_selected_settings() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let request_log = mount_sse_sequence(
&server,
vec![
sse(vec![
ev_assistant_message("m1", "before switch"),
ev_completed("r1"),
]),
sse(vec![
ev_assistant_message("m2", "COMPACTION_SUMMARY"),
ev_completed("r2"),
]),
responses::sse_completed("r3"),
],
)
.await;
let model_provider = non_openai_model_provider(&server);
let test = test_codex()
.with_model_info_override("gpt-5.4", |model| {
model.comp_hash = Some("hash-a".to_string());
model.default_reasoning_summary = ReasoningSummary::Detailed;
model.service_tiers = vec![ModelServiceTier {
id: ServiceTier::Fast.request_value().to_string(),
name: "Fast".to_string(),
description: "Priority processing".to_string(),
}];
})
.with_model_info_override("gpt-5.2", |model| {
model.comp_hash = Some("hash-b".to_string());
model.default_reasoning_summary = ReasoningSummary::Auto;
model.service_tiers.clear();
})
.with_model("gpt-5.4")
.with_config(move |config| {
config.model_provider = model_provider;
config.model_reasoning_summary = None;
config.service_tier = Some(ServiceTier::Fast.request_value().to_string());
config
.features
.enable(Feature::FastMode)
.expect("enable FastMode");
set_test_compact_prompt(config);
})
.build_with_auto_env(&server)
.await?;
test.submit_text_turn("before switch").await?;
test.codex
.start_or_steer_turn(
TurnInputRequest::user_input(vec![UserInput::Text {
text: "after switch".to_string(),
text_elements: Vec::new(),
}])
.with_thread_settings(ThreadSettingsOverrides {
model: Some("gpt-5.2".to_string()),
..Default::default()
}),
)
.await?;
wait_for_event(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_))
})
.await;
// The retained priority selection is filtered out for B, restored for
// compaction on A, and still omitted from the following B request.
let actual = request_log
.requests()
.iter()
.map(|request| {
let body = request.body_json();
json!([
body["model"],
body["reasoning"]["summary"],
body["service_tier"]
])
})
.collect::<Vec<_>>();
assert_eq!(
actual,
vec![
json!(["gpt-5.4", "detailed", "priority"]),
json!(["gpt-5.4", "detailed", "priority"]),
json!(["gpt-5.2", "auto", null]),
]
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn pre_sampling_compact_falls_back_from_retired_previous_model_after_rename() {
skip_if_no_network!();
+1
View File
@@ -153,6 +153,7 @@ mod skills;
mod skills_extension;
mod spawn_agent_description;
mod sqlite_state;
mod step_settings_snapshots;
mod stream_error_allows_next_turn;
mod stream_no_completed;
mod subagent_notifications;
+167 -5
View File
@@ -10,7 +10,12 @@ use codex_history::RolloutItem;
use codex_history::RolloutLine;
use codex_login::CodexAuth;
use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Personality;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::config_types::ServiceTier;
use codex_protocol::config_types::Settings;
use codex_protocol::items::TurnItem;
use codex_protocol::models::ContentItem;
use codex_protocol::models::PermissionProfile;
@@ -627,6 +632,10 @@ async fn review_uses_updated_turn_permissions_and_approval_policy() {
config.review_model = Some("gpt-5.4".to_string());
config.model_context_window = Some(128_000);
config.service_tier = Some(ServiceTier::Fast.request_value().to_string());
config
.features
.enable(Feature::FastMode)
.expect("enable FastMode");
config
.features
.enable(Feature::TokenBudget)
@@ -671,13 +680,22 @@ async fn review_uses_updated_turn_permissions_and_approval_policy() {
approval_policy: Some(AskForApproval::Never),
approvals_reviewer: Some(ApprovalsReviewer::User),
permission_profile: Some(PermissionProfile::Disabled),
effort: Some(Some(ReasoningEffort::XHigh)),
personality: Some(Personality::Friendly),
collaboration_mode: Some(CollaborationMode {
mode: ModeKind::Plan,
settings: Settings {
model: "gpt-5.2".to_string(),
reasoning_effort: Some(ReasoningEffort::XHigh),
developer_instructions: Some("Parent planning instructions".to_string()),
},
}),
..Default::default()
},
)
.await
.expect("updated thread permissions should be accepted");
let stored_settings = codex.thread_settings_snapshot().await;
codex
.submit(Op::Review {
review_request: ReviewRequest {
@@ -691,6 +709,7 @@ async fn review_uses_updated_turn_permissions_and_approval_policy() {
.expect("review should start");
wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await;
assert_eq!(codex.thread_settings_snapshot().await, stored_settings);
let request = request_log.single_request();
assert_eq!(request.body_json()["reasoning"]["effort"], "medium");
assert_eq!(
@@ -751,23 +770,166 @@ async fn review_uses_updated_turn_permissions_and_approval_policy() {
})
.expect("review rollout should contain session metadata");
assert_eq!(review_session_cwd, updated_cwd.as_path());
let review_approvals_reviewer = review_rollout
let review_context = review_rollout
.lines()
.filter_map(|line| {
let rollout_line: RolloutLine =
serde_json::from_str(line).expect("review rollout line should be valid");
match rollout_line.item {
RolloutItem::TurnContext(turn_context) => turn_context.approvals_reviewer,
RolloutItem::TurnContext(turn_context) => Some(turn_context),
_ => None,
}
})
.next_back();
assert_eq!(review_approvals_reviewer, Some(ApprovalsReviewer::User));
.next_back()
.expect("review rollout should contain turn context");
assert_eq!(
review_context.approvals_reviewer,
Some(ApprovalsReviewer::User)
);
assert_eq!(review_context.personality, Some(Personality::Friendly));
// The review delegate still starts in its own default mode, not the parent's Plan mode.
assert_eq!(
review_context.collaboration_mode,
Some(CollaborationMode {
mode: ModeKind::Default,
settings: Settings {
model: "gpt-5.4".to_string(),
reasoning_effort: Some(ReasoningEffort::Medium),
developer_instructions: None,
},
})
);
let _codex_home_guard = codex_home;
server.verify().await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn review_omits_retained_tier_when_fast_mode_disabled() -> anyhow::Result<()> {
skip_if_no_network!(Ok(()));
let (server, request_log) =
start_responses_server_with_sse(completed_sse(), /*expected_requests*/ 1).await;
let test = test_codex()
.with_model_info_override("gpt-5.4", |model| {
model.service_tiers = vec![ModelServiceTier {
id: ServiceTier::Flex.request_value().to_string(),
name: "Flex".to_string(),
description: "Flexible processing".to_string(),
}];
})
.with_config(|config| {
config
.features
.disable(Feature::FastMode)
.expect("disable FastMode");
})
.build_with_auto_env(&server)
.await?;
core_test_support::submit_thread_settings(
&test.codex,
ThreadSettingsOverrides {
service_tier: Some(Some(ServiceTier::Flex.request_value().to_string())),
..Default::default()
},
)
.await?;
test.codex
.submit(Op::Review {
review_request: ReviewRequest {
target: ReviewTarget::Custom {
instructions: "review the changes".to_string(),
},
user_facing_hint: None,
},
})
.await?;
wait_for_event(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_))
})
.await;
assert_eq!(
test.codex
.thread_settings_snapshot()
.await
.service_tier
.as_deref(),
Some("flex")
);
assert_eq!(
request_log.single_request().body_json().get("service_tier"),
None
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn review_resolves_inherited_summary_preferences() -> anyhow::Result<()> {
skip_if_no_network!(Ok(()));
let (server, request_log) =
start_responses_server_with_sse(completed_sse(), /*expected_requests*/ 2).await;
let test = test_codex()
.with_model_info_override("gpt-5.2", |model| {
model.default_reasoning_summary = ReasoningSummary::Auto;
})
.with_model_info_override("gpt-5.4", |model| {
model.default_reasoning_summary = ReasoningSummary::Detailed;
})
.with_model("gpt-5.2")
.with_config(|config| {
config.review_model = Some("gpt-5.4".to_string());
config.model_reasoning_summary = None;
})
.build_with_auto_env(&server)
.await?;
// First follow the review model's default, then use a preference updated on the thread.
for summary in [None, Some(ReasoningSummary::Concise)] {
if let Some(summary) = summary {
core_test_support::submit_thread_settings(
&test.codex,
ThreadSettingsOverrides {
summary: Some(summary),
..Default::default()
},
)
.await?;
}
let stored_settings = test.codex.thread_settings_snapshot().await;
test.codex
.submit(Op::Review {
review_request: ReviewRequest {
target: ReviewTarget::Custom {
instructions: "review the changes".to_string(),
},
user_facing_hint: None,
},
})
.await?;
wait_for_event(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_))
})
.await;
assert_eq!(test.codex.thread_settings_snapshot().await, stored_settings);
}
let actual = request_log
.requests()
.iter()
.map(|request| {
let body = request.body_json();
serde_json::json!([body["model"], body["reasoning"]["summary"]])
})
.collect::<Vec<_>>();
assert_eq!(
actual,
vec![
serde_json::json!(["gpt-5.4", "detailed"]),
serde_json::json!(["gpt-5.4", "concise"]),
]
);
Ok(())
}
/// Ensure that when a custom `review_model` is set in the config, the review
/// request uses that model (and not the main chat model).
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -0,0 +1,171 @@
use anyhow::Result;
use codex_core::TurnInputRequest;
use codex_features::Feature;
use codex_models_manager::bundled_models_response;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::config_types::ServiceTier;
use codex_protocol::openai_models::ModelsResponse;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::request_user_input::RequestUserInputAnswer;
use codex_protocol::request_user_input::RequestUserInputResponse;
use codex_protocol::user_input::UserInput;
use core_test_support::responses;
use core_test_support::skip_if_no_network;
use core_test_support::submit_thread_settings;
use core_test_support::test_codex::test_codex;
use core_test_support::wait_for_event;
use core_test_support::wait_for_event_match;
use pretty_assertions::assert_eq;
use serde_json::json;
use std::collections::HashMap;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn ordinary_settings_updates_keep_the_running_turn_snapshot() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = responses::start_mock_server().await;
let response_mock = responses::mount_sse_sequence(
&server,
vec![
responses::sse(vec![
responses::ev_response_created("response-1"),
responses::ev_function_call(
"pause",
"request_user_input",
&json!({ "questions": [{
"id": "continue",
"header": "Continue",
"question": "Continue after the settings update?",
"options": [
{ "label": "Yes", "description": "Continue the turn." },
{ "label": "No", "description": "Stop the turn." }
]
}] })
.to_string(),
),
responses::ev_completed("response-1"),
]),
responses::sse_completed("response-2"),
responses::sse_completed("response-3"),
],
)
.await;
let test = test_codex()
.with_model("snapshot-model-a")
.with_config(|config| {
// This regression exercises ordinary turn construction without
// enabling the diagnostic active-settings path.
config
.features
.enable(Feature::DefaultModeRequestUserInput)
.unwrap();
config.features.enable(Feature::FastMode).unwrap();
let template = bundled_models_response()
.unwrap()
.models
.into_iter()
.find(|model| model.slug == "gpt-5.4")
.unwrap();
config.model_catalog = Some(ModelsResponse {
models: ["snapshot-model-a", "snapshot-model-b"]
.into_iter()
.map(|slug| {
let mut model = template.clone();
model.slug = slug.to_string();
model.default_reasoning_summary = ReasoningSummary::Concise;
model
})
.collect(),
});
config.model_reasoning_effort = Some(ReasoningEffort::Low);
config.model_reasoning_summary = None;
config.service_tier = None;
})
.build_with_auto_env(&server)
.await?;
test.codex
.start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
text: "pause before continuing".into(),
text_elements: Vec::new(),
}]))
.await?;
let paused = wait_for_event_match(&test.codex, |event| match event {
EventMsg::RequestUserInput(request) => Some(request.clone()),
_ => None,
})
.await;
let mut expected = test.codex.thread_settings_snapshot().await;
submit_thread_settings(
&test.codex,
ThreadSettingsOverrides {
model: Some("snapshot-model-b".to_string()),
effort: Some(Some(ReasoningEffort::High)),
summary: Some(ReasoningSummary::Detailed),
service_tier: Some(Some(ServiceTier::Fast.request_value().to_string())),
..Default::default()
},
)
.await?;
expected.model = "snapshot-model-b".to_string();
expected.reasoning_effort = Some(ReasoningEffort::High);
expected.reasoning_summary = Some(ReasoningSummary::Detailed);
expected.service_tier = Some(ServiceTier::Fast.request_value().to_string());
expected.collaboration_mode = expected.collaboration_mode.with_updates(
Some(expected.model.clone()),
Some(expected.reasoning_effort.clone()),
/*developer_instructions*/ None,
);
assert_eq!(test.codex.thread_settings_snapshot().await, expected);
test.codex
.submit(Op::UserInputAnswer {
id: paused.turn_id.clone(),
response: RequestUserInputResponse {
answers: HashMap::from([(
"continue".to_string(),
RequestUserInputAnswer {
answers: vec!["Yes".to_string()],
},
)]),
},
})
.await?;
wait_for_event(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_))
})
.await;
test.submit_text_turn("start the next turn").await?;
let requests = response_mock.requests();
let actual = requests
.iter()
.map(|request| {
let body = request.body_json();
json!({
"model": body["model"],
"reasoning": body["reasoning"],
"service_tier": body.get("service_tier"),
})
})
.collect::<Vec<_>>();
let original = json!({
"model": "snapshot-model-a",
"reasoning": { "effort": "low", "summary": "concise" },
"service_tier": null,
});
assert_eq!(
actual,
vec![
original.clone(),
original,
json!({
"model": "snapshot-model-b",
"reasoning": { "effort": "high", "summary": "detailed" },
"service_tier": "priority",
})
]
);
Ok(())
}