mirror of
https://github.com/openai/codex.git
synced 2026-10-21 12:10:06 +00:00
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:
committed by
copyberry
parent
ba6cf9c692
commit
68301fa45f
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
|
||||
@@ -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!(
|
||||
¶ms.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(
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(),
|
||||
)))
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(¤t));
|
||||
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(
|
||||
¤t,
|
||||
&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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(¤t_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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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,
|
||||
));
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
)),
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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,
|
||||
}));
|
||||
|
||||
@@ -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"]);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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!();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
Reference in New Issue
Block a user