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 authored and copyberry committed 2026-08-25 17:41:22 +00:00
1 parent ba6cf9c692
commit 68301fa45f
68 files changed
+1393 -516

No files matched your search

+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,