mirror of
https://github.com/openai/codex.git
synced 2026-10-19 17:28:46 +00:00
Keep active-turn model settings stable across updates (#38785)
## Why Thread settings can change while a turn is still active, including between sampling requests. Those updates should apply to the next turn instead of changing the model configuration partway through the current turn. ## What changed - Snapshot the model, reasoning settings, service tier, approval settings, and model-attributed telemetry in `StepContext`. - Build prompts, model requests, world state, tracing, and startup prewarms from that step-scoped snapshot. - Share immutable model metadata between turn and step contexts. ## Testing Added an integration test that pauses an active turn, updates its thread settings, and verifies that all requests in the active turn retain the original settings while the next turn uses the updates. GitOrigin-RevId: c51352218e463dcc9b7338566d16742292c49fbf
This commit is contained in:
committed by
copyberry
parent
c4941302c7
commit
00f6a8a60e
@@ -1121,10 +1121,12 @@ fn guardian_approval_request_to_json_renders_mcp_tool_call_shape() -> serde_json
|
||||
async fn build_guardian_prompt_items_explains_node_repl_review_scope() -> anyhow::Result<()> {
|
||||
let (session, mut turn) =
|
||||
guardian_test_session_and_turn_with_base_url("http://localhost").await;
|
||||
Arc::get_mut(&mut turn)
|
||||
.expect("turn should be uniquely owned")
|
||||
.model_info
|
||||
.node_repl_auto_review_required = true;
|
||||
Arc::make_mut(
|
||||
&mut Arc::get_mut(&mut turn)
|
||||
.expect("turn should be uniquely owned")
|
||||
.model_info,
|
||||
)
|
||||
.node_repl_auto_review_required = true;
|
||||
seed_guardian_parent_history(&session, &turn).await;
|
||||
let context = GuardianReviewContext::from(&turn);
|
||||
|
||||
@@ -1700,7 +1702,7 @@ async fn guardian_request_model_for_auto_review(
|
||||
match catalog {
|
||||
GuardianTestCatalog::Bundled => {}
|
||||
GuardianTestCatalog::ParentOnly => {
|
||||
let parent_model = turn.model_info.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),
|
||||
@@ -1714,10 +1716,12 @@ async fn guardian_request_model_for_auto_review(
|
||||
.models_manager = Arc::new(models_manager);
|
||||
}
|
||||
}
|
||||
Arc::get_mut(&mut turn)
|
||||
.expect("turn should be unique")
|
||||
.model_info
|
||||
.auto_review_model_override = auto_review_model_override;
|
||||
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();
|
||||
let preferred_model = turn.provider.approval_review_preferred_model().to_string();
|
||||
let parent_turn_id = turn.sub_id.clone();
|
||||
@@ -1942,7 +1946,8 @@ 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());
|
||||
turn.model_info.auto_review_model_override = Some("codex-auto-review".to_string());
|
||||
Arc::make_mut(&mut turn.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;
|
||||
@@ -2186,7 +2191,8 @@ 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");
|
||||
turn_mut.model_info.auto_review_model_override = Some("codex-auto-review".to_string());
|
||||
Arc::make_mut(&mut turn_mut.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;
|
||||
|
||||
@@ -3387,7 +3393,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.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")
|
||||
|
||||
@@ -101,14 +101,9 @@ pub(crate) async fn build_prompt_input_from_session(
|
||||
let prompt_input = sess
|
||||
.clone_history()
|
||||
.await
|
||||
.for_prompt(&turn_context.model_info.input_modalities);
|
||||
.for_prompt(&step_context.model_info.input_modalities);
|
||||
let base_instructions = sess.get_base_instructions().await;
|
||||
let prompt = build_prompt(
|
||||
prompt_input,
|
||||
step_context.tool_router.as_ref(),
|
||||
turn_context.as_ref(),
|
||||
base_instructions,
|
||||
);
|
||||
let prompt = build_prompt(prompt_input, step_context.as_ref(), base_instructions);
|
||||
|
||||
Ok(prompt.input)
|
||||
}
|
||||
|
||||
@@ -3196,6 +3196,13 @@ 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(),
|
||||
turn: turn_context,
|
||||
environments,
|
||||
selected_capability_roots,
|
||||
|
||||
@@ -138,7 +138,7 @@ 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: model_info.clone(),
|
||||
model_info: Arc::new(model_info.clone()),
|
||||
session_telemetry: session_telemetry_for_context,
|
||||
provider: provider_for_context,
|
||||
reasoning_effort,
|
||||
|
||||
@@ -7,10 +7,32 @@ 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,
|
||||
/// Telemetry context tagged with this sampling request's model.
|
||||
pub(crate) session_telemetry: SessionTelemetry,
|
||||
pub(crate) environments: TurnEnvironmentSnapshot,
|
||||
/// Capability roots bound to ready environments in this exact step.
|
||||
pub(crate) selected_capability_roots: Vec<ResolvedSelectedCapabilityRoot>,
|
||||
|
||||
@@ -210,6 +210,13 @@ impl StepContext {
|
||||
pub(crate) fn for_test(turn: Arc<TurnContext>) -> Arc<Self> {
|
||||
let environments = turn.environments.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,
|
||||
session_telemetry: turn.session_telemetry.clone(),
|
||||
turn: Arc::clone(&turn),
|
||||
environments,
|
||||
selected_capability_roots: Vec::new(),
|
||||
@@ -2746,8 +2753,8 @@ async fn recompute_token_usage_updates_model_context_window() {
|
||||
}));
|
||||
}
|
||||
|
||||
turn_context.model_info.context_window = Some(128_000);
|
||||
turn_context.model_info.effective_context_window_percent = 100;
|
||||
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;
|
||||
|
||||
session.recompute_token_usage(&turn_context).await;
|
||||
|
||||
@@ -4435,7 +4442,7 @@ async fn turn_context_with_model_updates_model_fields() {
|
||||
|
||||
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, expected_model_info);
|
||||
assert_eq!(updated.model_info.as_ref(), &expected_model_info);
|
||||
assert_eq!(
|
||||
updated.reasoning_effort,
|
||||
Some(ReasoningEffortConfig::Medium)
|
||||
@@ -9247,8 +9254,8 @@ 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());
|
||||
turn_context.model_info.context_window = Some(100);
|
||||
turn_context.model_info.effective_context_window_percent = 50;
|
||||
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;
|
||||
turn_context
|
||||
.extension_data
|
||||
.insert(TurnContextExtensionTestState {
|
||||
@@ -9274,8 +9281,8 @@ 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());
|
||||
turn_context.model_info.context_window = Some(200);
|
||||
turn_context.model_info.effective_context_window_percent = 25;
|
||||
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;
|
||||
turn_context
|
||||
.extension_data
|
||||
.insert(TurnContextExtensionTestState {
|
||||
|
||||
@@ -96,7 +96,7 @@ async fn request_permissions_routes_to_guardian_when_reviewer_is_enabled() {
|
||||
.await;
|
||||
|
||||
let (mut session, mut turn_context_raw) = make_session_and_context().await;
|
||||
turn_context_raw.model_info.node_repl_auto_review_required = true;
|
||||
Arc::make_mut(&mut turn_context_raw.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
|
||||
|
||||
@@ -350,7 +350,7 @@ pub(crate) async fn run_turn(
|
||||
let sampling_request_input: Vec<ResponseItem> = async {
|
||||
sess.clone_history()
|
||||
.await
|
||||
.for_prompt(&turn_context.model_info.input_modalities)
|
||||
.for_prompt(&step_context.model_info.input_modalities)
|
||||
}
|
||||
.instrument(trace_span!("run_turn.prepare_sampling_request_input"))
|
||||
.await;
|
||||
@@ -1293,13 +1293,13 @@ pub(super) fn collect_explicit_app_ids_from_skill_items(
|
||||
#[instrument(level = "trace", skip_all)]
|
||||
pub(crate) fn build_prompt(
|
||||
input: Vec<ResponseItem>,
|
||||
router: &ToolRouter,
|
||||
turn_context: &TurnContext,
|
||||
step_context: &StepContext,
|
||||
base_instructions: BaseInstructions,
|
||||
) -> Prompt {
|
||||
let turn_context = &step_context.turn;
|
||||
Prompt {
|
||||
input,
|
||||
tools: router.model_visible_specs(),
|
||||
tools: step_context.tool_router.model_visible_specs(),
|
||||
parallel_tool_calls: true,
|
||||
base_instructions,
|
||||
output_schema: turn_context.final_output_json_schema.clone(),
|
||||
@@ -1315,7 +1315,7 @@ pub(crate) fn build_prompt(
|
||||
skip_all,
|
||||
fields(
|
||||
turn_id = %step_context.turn.sub_id,
|
||||
model = %step_context.turn.model_info.slug,
|
||||
model = %step_context.model_info.slug,
|
||||
cwd = %step_context.turn.cwd.display()
|
||||
)
|
||||
)]
|
||||
@@ -1330,8 +1330,6 @@ async fn run_sampling_request(
|
||||
cancellation_token: CancellationToken,
|
||||
) -> CodexResult<(SamplingRequestResult, Vec<ResponseItem>)> {
|
||||
let turn_context = Arc::clone(&step_context.turn);
|
||||
let router = Arc::clone(&step_context.tool_router);
|
||||
|
||||
let base_instructions = sess.get_base_instructions().await;
|
||||
|
||||
let tool_runtime = ToolCallRuntime::new(
|
||||
@@ -1355,7 +1353,7 @@ async fn run_sampling_request(
|
||||
} else {
|
||||
sess.clone_history()
|
||||
.await
|
||||
.for_prompt(&turn_context.model_info.input_modalities)
|
||||
.for_prompt(&step_context.model_info.input_modalities)
|
||||
};
|
||||
let mut prompt_input = prompt_input;
|
||||
if let Some(executed_tool_calls) = sess.services.executed_tool_calls.as_ref()
|
||||
@@ -1366,14 +1364,13 @@ async fn run_sampling_request(
|
||||
}
|
||||
let prompt = build_prompt(
|
||||
prompt_input,
|
||||
router.as_ref(),
|
||||
turn_context.as_ref(),
|
||||
step_context.as_ref(),
|
||||
base_instructions.clone(),
|
||||
);
|
||||
let err = match try_run_sampling_request(
|
||||
tool_runtime.clone(),
|
||||
Arc::clone(&sess),
|
||||
Arc::clone(&turn_context),
|
||||
Arc::clone(&step_context),
|
||||
Arc::clone(&turn_store),
|
||||
client_session,
|
||||
responses_metadata,
|
||||
@@ -2144,14 +2141,14 @@ fn assign_missing_streamed_response_item_id(
|
||||
#[instrument(level = "trace",
|
||||
skip_all,
|
||||
fields(
|
||||
turn_id = %turn_context.sub_id,
|
||||
model = %turn_context.model_info.slug
|
||||
turn_id = %step_context.turn.sub_id,
|
||||
model = %step_context.model_info.slug
|
||||
)
|
||||
)]
|
||||
async fn try_run_sampling_request(
|
||||
tool_runtime: ToolCallRuntime,
|
||||
sess: Arc<Session>,
|
||||
turn_context: Arc<TurnContext>,
|
||||
step_context: Arc<StepContext>,
|
||||
turn_store: Arc<codex_extension_api::ExtensionData>,
|
||||
client_session: &mut ModelClientSession,
|
||||
responses_metadata: &CodexResponsesMetadata,
|
||||
@@ -2159,17 +2156,18 @@ async fn try_run_sampling_request(
|
||||
prompt: &Prompt,
|
||||
cancellation_token: CancellationToken,
|
||||
) -> CodexResult<SamplingRequestResult> {
|
||||
let turn_context = Arc::clone(&step_context.turn);
|
||||
feedback_tags!(
|
||||
model = turn_context.model_info.slug.clone(),
|
||||
model = step_context.model_info.slug.clone(),
|
||||
approval_policy = turn_context.approval_policy(),
|
||||
sandbox_policy = &turn_context.sandbox_policy(),
|
||||
effort = turn_context.reasoning_effort,
|
||||
effort = step_context.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(),
|
||||
turn_context.model_info.slug.as_str(),
|
||||
step_context.model_info.slug.as_str(),
|
||||
turn_context.provider.info().name.as_str(),
|
||||
);
|
||||
let sampling_timing_guard = turn_context.turn_timing_state.begin_sampling();
|
||||
@@ -2181,11 +2179,11 @@ async fn try_run_sampling_request(
|
||||
let mut stream = client_session
|
||||
.stream(
|
||||
prompt,
|
||||
&turn_context.model_info,
|
||||
&turn_context.session_telemetry,
|
||||
turn_context.reasoning_effort.clone(),
|
||||
turn_context.reasoning_summary,
|
||||
turn_context.config.service_tier.clone(),
|
||||
&step_context.model_info,
|
||||
&step_context.session_telemetry,
|
||||
step_context.reasoning_effort.clone(),
|
||||
step_context.reasoning_summary,
|
||||
step_context.service_tier.clone(),
|
||||
responses_metadata,
|
||||
&inference_trace,
|
||||
)
|
||||
@@ -2205,7 +2203,12 @@ async fn try_run_sampling_request(
|
||||
let mut should_emit_token_count = false;
|
||||
const MAX_ANALYTICS_TOOL_CALL_IDS_PER_RESPONSE: usize = 256;
|
||||
let mut analytics_tool_call_ids = Vec::new();
|
||||
let reasoning_effort = turn_context.effective_reasoning_effort_for_tracing();
|
||||
let reasoning_effort = step_context
|
||||
.reasoning_effort
|
||||
.clone()
|
||||
.or_else(|| step_context.model_info.default_reasoning_level.clone())
|
||||
.map(|effort| effort.to_string())
|
||||
.unwrap_or_else(|| "default".to_string());
|
||||
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));
|
||||
|
||||
@@ -138,12 +138,18 @@ pub struct TurnContext {
|
||||
pub(crate) trace_id: Option<String>,
|
||||
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.
|
||||
pub config: Arc<Config>,
|
||||
pub(crate) auth_manager: Option<Arc<AuthManager>>,
|
||||
pub(crate) model_info: ModelInfo,
|
||||
/// Legacy turn model; step-scoped execution should use `StepContext::model_info`.
|
||||
pub(crate) model_info: Arc<ModelInfo>,
|
||||
/// 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,
|
||||
@@ -223,6 +229,8 @@ impl TurnContext {
|
||||
.await
|
||||
}
|
||||
|
||||
/// Approval policy admitted with the turn; step-scoped actions should use
|
||||
/// `StepContext::approval_policy` instead.
|
||||
pub(crate) fn approval_policy(&self) -> AskForApproval {
|
||||
self.config.permissions.approval_policy.value()
|
||||
}
|
||||
@@ -352,6 +360,7 @@ impl TurnContext {
|
||||
config.http_client_factory(),
|
||||
)
|
||||
.await;
|
||||
let model_info = Arc::new(model_info);
|
||||
|
||||
Self {
|
||||
sub_id: self.sub_id.clone(),
|
||||
@@ -360,7 +369,7 @@ impl TurnContext {
|
||||
code_mode_available: self.code_mode_available,
|
||||
config: Arc::new(config),
|
||||
auth_manager: self.auth_manager.clone(),
|
||||
model_info: model_info.clone(),
|
||||
model_info: Arc::clone(&model_info),
|
||||
session_telemetry: self
|
||||
.session_telemetry
|
||||
.clone()
|
||||
@@ -643,7 +652,7 @@ impl Session {
|
||||
code_mode_available: true,
|
||||
config: per_turn_config,
|
||||
auth_manager,
|
||||
model_info,
|
||||
model_info: Arc::new(model_info),
|
||||
session_telemetry: session_telemetry_for_context,
|
||||
provider,
|
||||
reasoning_effort,
|
||||
|
||||
@@ -168,9 +168,9 @@ impl Session {
|
||||
let model_messages = turn_context.model_info.model_messages.as_ref();
|
||||
world_state.add_section(PermissionsState::new(
|
||||
&permission_profile,
|
||||
turn_context.approval_policy(),
|
||||
step_context.approval_policy,
|
||||
ApprovalPromptContext::new(
|
||||
turn_context.config.approvals_reviewer,
|
||||
step_context.approvals_reviewer,
|
||||
model_messages.and_then(|messages| messages.approvals.as_ref()),
|
||||
model_messages.and_then(|messages| messages.permissions.as_ref()),
|
||||
),
|
||||
|
||||
@@ -282,7 +282,6 @@ async fn schedule_startup_prewarm_inner(
|
||||
&startup_cancellation_token,
|
||||
)
|
||||
.await?;
|
||||
let startup_router = Arc::clone(&step_context.tool_router);
|
||||
startup_turn_context.session_telemetry.record_startup_phase(
|
||||
"startup_prewarm_build_tools",
|
||||
built_tools_started_at.elapsed(),
|
||||
@@ -291,8 +290,7 @@ async fn schedule_startup_prewarm_inner(
|
||||
let build_prompt_started_at = Instant::now();
|
||||
let startup_prompt = build_prompt(
|
||||
Vec::new(),
|
||||
startup_router.as_ref(),
|
||||
startup_turn_context.as_ref(),
|
||||
step_context.as_ref(),
|
||||
BaseInstructions {
|
||||
text: base_instructions,
|
||||
provenance: None,
|
||||
@@ -316,11 +314,11 @@ async fn schedule_startup_prewarm_inner(
|
||||
client_session
|
||||
.prewarm_websocket(
|
||||
&startup_prompt,
|
||||
&startup_turn_context.model_info,
|
||||
&startup_turn_context.session_telemetry,
|
||||
startup_turn_context.reasoning_effort.clone(),
|
||||
startup_turn_context.reasoning_summary,
|
||||
startup_turn_context.config.service_tier.clone(),
|
||||
&step_context.model_info,
|
||||
&step_context.session_telemetry,
|
||||
step_context.reasoning_effort.clone(),
|
||||
step_context.reasoning_summary,
|
||||
step_context.service_tier.clone(),
|
||||
&responses_metadata,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -664,7 +664,7 @@ async fn shell_family_registers_visible_unified_exec_and_hidden_legacy_shell() {
|
||||
let plan = probe(|turn| {
|
||||
set_features(turn, &[Feature::ShellTool, Feature::UnifiedExec]);
|
||||
set_feature(turn, Feature::ShellZshFork, /*enabled*/ false);
|
||||
turn.model_info.shell_type = ConfigShellToolType::ShellCommand;
|
||||
Arc::make_mut(&mut turn.model_info).shell_type = ConfigShellToolType::ShellCommand;
|
||||
})
|
||||
.await;
|
||||
|
||||
@@ -687,7 +687,7 @@ async fn login_shell_parameter_follows_selected_environment() {
|
||||
set_feature(turn, Feature::ShellTool, /*enabled*/ true);
|
||||
set_feature(turn, Feature::UnifiedExec, tool_name == "exec_command");
|
||||
set_feature(turn, Feature::ShellZshFork, /*enabled*/ false);
|
||||
turn.model_info.shell_type = ConfigShellToolType::ShellCommand;
|
||||
Arc::make_mut(&mut turn.model_info).shell_type = ConfigShellToolType::ShellCommand;
|
||||
update_config(turn, |config| {
|
||||
config.permissions.allow_login_shell = !allow_login_shell;
|
||||
});
|
||||
@@ -744,7 +744,7 @@ async fn shell_command_is_not_registered_without_a_single_local_environment() {
|
||||
let remote_environment = probe(|turn| {
|
||||
set_feature(turn, Feature::ShellTool, /*enabled*/ true);
|
||||
set_feature(turn, Feature::UnifiedExec, /*enabled*/ false);
|
||||
turn.model_info.shell_type = ConfigShellToolType::ShellCommand;
|
||||
Arc::make_mut(&mut turn.model_info).shell_type = ConfigShellToolType::ShellCommand;
|
||||
|
||||
let TurnEnvironmentState::Ready(environment) = turn
|
||||
.environments
|
||||
@@ -769,7 +769,7 @@ async fn shell_command_is_not_registered_without_a_single_local_environment() {
|
||||
let multiple_local_environments = probe(|turn| {
|
||||
set_feature(turn, Feature::ShellTool, /*enabled*/ true);
|
||||
set_feature(turn, Feature::UnifiedExec, /*enabled*/ false);
|
||||
turn.model_info.shell_type = ConfigShellToolType::ShellCommand;
|
||||
Arc::make_mut(&mut turn.model_info).shell_type = ConfigShellToolType::ShellCommand;
|
||||
duplicate_primary_environment(turn);
|
||||
})
|
||||
.await;
|
||||
@@ -817,7 +817,7 @@ async fn shell_zsh_fork_stays_standalone_until_unified_exec_composition_is_enabl
|
||||
set_features(turn, &[Feature::ShellTool, Feature::UnifiedExec]);
|
||||
set_feature(turn, Feature::ShellZshFork, /*enabled*/ true);
|
||||
set_feature(turn, Feature::UnifiedExecZshFork, /*enabled*/ false);
|
||||
turn.model_info.shell_type = ConfigShellToolType::ShellCommand;
|
||||
Arc::make_mut(&mut turn.model_info).shell_type = ConfigShellToolType::ShellCommand;
|
||||
})
|
||||
.await;
|
||||
|
||||
@@ -836,7 +836,7 @@ async fn shell_zsh_fork_stays_standalone_until_unified_exec_composition_is_enabl
|
||||
Feature::UnifiedExecZshFork,
|
||||
],
|
||||
);
|
||||
turn.model_info.shell_type = ConfigShellToolType::ShellCommand;
|
||||
Arc::make_mut(&mut turn.model_info).shell_type = ConfigShellToolType::ShellCommand;
|
||||
})
|
||||
.await;
|
||||
|
||||
@@ -949,7 +949,8 @@ 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);
|
||||
turn.model_info.apply_patch_tool_type = Some(ApplyPatchToolType::Freeform);
|
||||
Arc::make_mut(&mut turn.model_info).apply_patch_tool_type =
|
||||
Some(ApplyPatchToolType::Freeform);
|
||||
})
|
||||
.await;
|
||||
no_environment.assert_visible_lacks(&[
|
||||
@@ -972,7 +973,8 @@ async fn environment_count_controls_environment_backed_tools() {
|
||||
set_feature(turn, Feature::ShellTool, /*enabled*/ true);
|
||||
set_feature(turn, Feature::UnifiedExec, /*enabled*/ true);
|
||||
set_feature(turn, Feature::RequestPermissionsTool, /*enabled*/ true);
|
||||
turn.model_info.apply_patch_tool_type = Some(ApplyPatchToolType::Freeform);
|
||||
Arc::make_mut(&mut turn.model_info).apply_patch_tool_type =
|
||||
Some(ApplyPatchToolType::Freeform);
|
||||
})
|
||||
.await;
|
||||
multiple_environments.assert_visible_contains(&[
|
||||
@@ -1000,7 +1002,7 @@ async fn environment_count_controls_environment_backed_tools() {
|
||||
async fn environment_tools_follow_the_step_context() {
|
||||
let (_session, mut turn) = make_session_and_context().await;
|
||||
set_feature(&mut turn, Feature::UnifiedExec, /*enabled*/ true);
|
||||
turn.model_info.apply_patch_tool_type = Some(ApplyPatchToolType::Freeform);
|
||||
Arc::make_mut(&mut turn.model_info).apply_patch_tool_type = Some(ApplyPatchToolType::Freeform);
|
||||
|
||||
let environments = turn.environments.clone();
|
||||
turn.environments.environments.clear();
|
||||
@@ -1130,7 +1132,7 @@ async fn mcp_and_tool_search_follow_direct_and_deferred_tool_exposure() {
|
||||
|
||||
let missing_model_capability = probe_with(
|
||||
|turn| {
|
||||
turn.model_info.supports_search_tool = false;
|
||||
Arc::make_mut(&mut turn.model_info).supports_search_tool = false;
|
||||
},
|
||||
searchable_mcp(),
|
||||
)
|
||||
@@ -1139,7 +1141,7 @@ 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);
|
||||
turn.model_info.supports_search_tool = true;
|
||||
Arc::make_mut(&mut turn.model_info).supports_search_tool = true;
|
||||
})
|
||||
.await;
|
||||
missing_deferred_tools.assert_visible_lacks(&["tool_search"]);
|
||||
@@ -1151,7 +1153,7 @@ async fn mcp_and_tool_search_follow_direct_and_deferred_tool_exposure() {
|
||||
|
||||
let bedrock_namespace_capability = probe_with(
|
||||
|turn| {
|
||||
turn.model_info.supports_search_tool = true;
|
||||
Arc::make_mut(&mut turn.model_info).supports_search_tool = true;
|
||||
use_bedrock_provider(turn);
|
||||
},
|
||||
searchable_mcp(),
|
||||
@@ -1161,7 +1163,7 @@ async fn mcp_and_tool_search_follow_direct_and_deferred_tool_exposure() {
|
||||
|
||||
let enabled = probe_with(
|
||||
|turn| {
|
||||
turn.model_info.supports_search_tool = true;
|
||||
Arc::make_mut(&mut turn.model_info).supports_search_tool = true;
|
||||
},
|
||||
searchable_mcp(),
|
||||
)
|
||||
@@ -1175,7 +1177,7 @@ 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);
|
||||
turn.model_info.supports_search_tool = true;
|
||||
Arc::make_mut(&mut turn.model_info).supports_search_tool = true;
|
||||
},
|
||||
ToolPlanInputs {
|
||||
tool_runtimes: vec![
|
||||
@@ -1227,8 +1229,8 @@ 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);
|
||||
turn.model_info.supports_search_tool = true;
|
||||
turn.model_info.use_responses_lite = true;
|
||||
Arc::make_mut(&mut turn.model_info).supports_search_tool = true;
|
||||
Arc::make_mut(&mut turn.model_info).use_responses_lite = true;
|
||||
metadata_state = Some(Arc::clone(&turn.turn_metadata_state));
|
||||
},
|
||||
ToolPlanInputs {
|
||||
@@ -1302,7 +1304,7 @@ 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;
|
||||
});
|
||||
turn.model_info.use_responses_lite = true;
|
||||
Arc::make_mut(&mut turn.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(),
|
||||
@@ -1357,7 +1359,7 @@ async fn unified_tool_runtimes_preserve_source_order_and_collision_priority() {
|
||||
|turn| {
|
||||
set_features(turn, &[Feature::ShellTool, Feature::UnifiedExec]);
|
||||
set_feature(turn, Feature::ShellZshFork, /*enabled*/ false);
|
||||
turn.model_info.shell_type = ConfigShellToolType::ShellCommand;
|
||||
Arc::make_mut(&mut turn.model_info).shell_type = ConfigShellToolType::ShellCommand;
|
||||
},
|
||||
ToolPlanInputs {
|
||||
tool_runtimes: vec![mcp_runtime(
|
||||
@@ -1553,7 +1555,7 @@ async fn strict_tool_collisions_reject_external_and_synthetic_duplicates() {
|
||||
if code_mode_enabled {
|
||||
set_feature(&mut turn, Feature::CodeMode, /*enabled*/ true);
|
||||
}
|
||||
turn.model_info.supports_search_tool = search_enabled;
|
||||
Arc::make_mut(&mut turn.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(
|
||||
@@ -1711,8 +1713,8 @@ async fn code_mode_uses_the_first_normalized_tool_identity() {
|
||||
if code_mode_only {
|
||||
set_feature(turn, Feature::CodeModeOnly, /*enabled*/ true);
|
||||
}
|
||||
turn.model_info.supports_search_tool = true;
|
||||
turn.model_info.use_responses_lite = true;
|
||||
Arc::make_mut(&mut turn.model_info).supports_search_tool = true;
|
||||
Arc::make_mut(&mut turn.model_info).use_responses_lite = true;
|
||||
metadata_state = Some(Arc::clone(&turn.turn_metadata_state));
|
||||
},
|
||||
ToolPlanInputs {
|
||||
@@ -1795,7 +1797,7 @@ 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| {
|
||||
turn.model_info.supports_search_tool = true;
|
||||
Arc::make_mut(&mut turn.model_info).supports_search_tool = true;
|
||||
},
|
||||
ToolPlanInputs {
|
||||
extension_tool_executors: vec![Arc::new(DeferredExtensionTool)],
|
||||
@@ -1815,7 +1817,7 @@ async fn tool_search_cache_rebuilds_when_deferred_sources_change() {
|
||||
let cache = ToolSearchHandlerCache::default();
|
||||
|
||||
let (_session, mut first_turn) = make_session_and_context().await;
|
||||
first_turn.model_info.supports_search_tool = true;
|
||||
Arc::make_mut(&mut first_turn.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(
|
||||
@@ -1836,7 +1838,7 @@ 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;
|
||||
second_turn.model_info.supports_search_tool = true;
|
||||
Arc::make_mut(&mut second_turn.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(
|
||||
@@ -1883,7 +1885,7 @@ 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;
|
||||
turn.model_info.supports_search_tool = true;
|
||||
Arc::make_mut(&mut turn.model_info).supports_search_tool = true;
|
||||
set_feature(
|
||||
&mut turn,
|
||||
Feature::DeferredToolWorldState,
|
||||
@@ -1996,7 +1998,7 @@ async fn request_plugin_install_requires_all_discovery_features() {
|
||||
async fn request_plugin_install_stays_visible_without_tool_search() {
|
||||
let plan = probe_with(
|
||||
|turn| {
|
||||
turn.model_info.supports_search_tool = false;
|
||||
Arc::make_mut(&mut turn.model_info).supports_search_tool = false;
|
||||
set_features(
|
||||
turn,
|
||||
&[Feature::ToolSuggest, Feature::Apps, Feature::Plugins],
|
||||
@@ -2132,7 +2134,7 @@ async fn code_mode_only_exposes_configured_dynamic_namespace_directly() {
|
||||
let plan = probe_with(
|
||||
|turn| {
|
||||
set_features(turn, &[Feature::CodeMode, Feature::CodeModeOnly]);
|
||||
turn.model_info.supports_search_tool = true;
|
||||
Arc::make_mut(&mut turn.model_info).supports_search_tool = true;
|
||||
update_config(turn, |config| {
|
||||
config.code_mode.direct_only_tool_namespaces = vec!["direct_only".to_string()];
|
||||
});
|
||||
@@ -2196,7 +2198,7 @@ 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);
|
||||
turn.model_info.supports_search_tool = true;
|
||||
Arc::make_mut(&mut turn.model_info).supports_search_tool = true;
|
||||
update_config(turn, |config| {
|
||||
config.code_mode.excluded_tool_namespaces = vec!["excluded".to_string()];
|
||||
});
|
||||
@@ -2450,7 +2452,7 @@ 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]);
|
||||
turn.model_info.tool_mode = Some(ToolMode::Direct);
|
||||
Arc::make_mut(&mut turn.model_info).tool_mode = Some(ToolMode::Direct);
|
||||
})
|
||||
.await;
|
||||
direct.assert_visible_lacks(&[
|
||||
@@ -2462,7 +2464,7 @@ 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| {
|
||||
turn.model_info.supports_search_tool = true;
|
||||
Arc::make_mut(&mut turn.model_info).supports_search_tool = true;
|
||||
set_feature(turn, Feature::Collab, /*enabled*/ true);
|
||||
set_feature(turn, Feature::MultiAgentV2, /*enabled*/ false);
|
||||
})
|
||||
@@ -2605,8 +2607,8 @@ 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);
|
||||
turn.model_info.slug = model.to_string();
|
||||
turn.model_info.multi_agent_version = model_multi_agent_version;
|
||||
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;
|
||||
turn.session_source = SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id: ThreadId::new(),
|
||||
depth: 1,
|
||||
@@ -2721,7 +2723,7 @@ async fn hosted_web_search_and_standalone_image_generation_follow_runtime_gates(
|
||||
let image_generation = probe_with(
|
||||
|turn| {
|
||||
use_chatgpt_auth(turn);
|
||||
turn.model_info.input_modalities = vec![InputModality::Image];
|
||||
Arc::make_mut(&mut turn.model_info).input_modalities = vec![InputModality::Image];
|
||||
},
|
||||
ToolPlanInputs {
|
||||
extension_tool_executors: vec![image_generation_tool.clone()],
|
||||
@@ -2735,7 +2737,7 @@ async fn hosted_web_search_and_standalone_image_generation_follow_runtime_gates(
|
||||
|turn| {
|
||||
use_chatgpt_auth(turn);
|
||||
set_feature(turn, Feature::ImageGeneration, /*enabled*/ false);
|
||||
turn.model_info.input_modalities = vec![InputModality::Image];
|
||||
Arc::make_mut(&mut turn.model_info).input_modalities = vec![InputModality::Image];
|
||||
},
|
||||
ToolPlanInputs {
|
||||
extension_tool_executors: vec![image_generation_tool.clone()],
|
||||
@@ -2748,7 +2750,7 @@ async fn hosted_web_search_and_standalone_image_generation_follow_runtime_gates(
|
||||
let text_only_model = probe_with(
|
||||
|turn| {
|
||||
use_chatgpt_auth(turn);
|
||||
turn.model_info.input_modalities = vec![];
|
||||
Arc::make_mut(&mut turn.model_info).input_modalities = vec![];
|
||||
},
|
||||
ToolPlanInputs {
|
||||
extension_tool_executors: vec![image_generation_tool.clone()],
|
||||
@@ -2761,7 +2763,7 @@ async fn hosted_web_search_and_standalone_image_generation_follow_runtime_gates(
|
||||
let unsupported_provider = probe_with(
|
||||
|turn| {
|
||||
use_bedrock_provider(turn);
|
||||
turn.model_info.input_modalities = vec![InputModality::Image];
|
||||
Arc::make_mut(&mut turn.model_info).input_modalities = vec![InputModality::Image];
|
||||
},
|
||||
ToolPlanInputs {
|
||||
extension_tool_executors: vec![image_generation_tool],
|
||||
@@ -2773,7 +2775,7 @@ 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);
|
||||
turn.model_info.web_search_tool_type = WebSearchToolType::TextAndImage;
|
||||
Arc::make_mut(&mut turn.model_info).web_search_tool_type = WebSearchToolType::TextAndImage;
|
||||
})
|
||||
.await;
|
||||
assert_eq!(
|
||||
@@ -2792,7 +2794,7 @@ 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);
|
||||
turn.model_info.input_modalities = vec![InputModality::Image];
|
||||
Arc::make_mut(&mut turn.model_info).input_modalities = vec![InputModality::Image];
|
||||
})
|
||||
.await;
|
||||
assert_eq!(
|
||||
@@ -2869,7 +2871,7 @@ async fn hosted_web_search_and_standalone_image_generation_follow_runtime_gates(
|
||||
|
||||
let bedrock_cached_web_search = probe(|turn| {
|
||||
use_bedrock_provider(turn);
|
||||
turn.model_info.web_search_tool_type = WebSearchToolType::Text;
|
||||
Arc::make_mut(&mut turn.model_info).web_search_tool_type = WebSearchToolType::Text;
|
||||
})
|
||||
.await;
|
||||
assert_eq!(
|
||||
@@ -2888,7 +2890,7 @@ 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);
|
||||
turn.model_info.web_search_tool_type = WebSearchToolType::Text;
|
||||
Arc::make_mut(&mut turn.model_info).web_search_tool_type = WebSearchToolType::Text;
|
||||
})
|
||||
.await;
|
||||
bedrock_with_standalone_web_search.assert_visible_contains(&["web_search"]);
|
||||
|
||||
@@ -3,12 +3,14 @@ use codex_config::types::Personality;
|
||||
use codex_core::CodexThread;
|
||||
use codex_core::ForkSnapshot;
|
||||
use codex_core::TurnInputRequest;
|
||||
use codex_core::config::Constrained;
|
||||
use codex_features::Feature;
|
||||
use codex_history::RolloutItem;
|
||||
use codex_history::RolloutLine;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_models_manager::bundled_models_response;
|
||||
use codex_models_manager::manager::RefreshStrategy;
|
||||
use codex_protocol::config_types::ApprovalsReviewer;
|
||||
use codex_protocol::config_types::CollaborationMode;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::config_types::ReasoningSummary;
|
||||
@@ -31,8 +33,13 @@ use codex_protocol::protocol::AskForApproval;
|
||||
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::ev_assistant_message;
|
||||
use core_test_support::responses::ev_completed;
|
||||
use core_test_support::responses::ev_completed_with_tokens;
|
||||
use core_test_support::responses::ev_function_call;
|
||||
use core_test_support::responses::ev_image_generation_call;
|
||||
use core_test_support::responses::ev_response_created;
|
||||
use core_test_support::responses::mount_models_once;
|
||||
@@ -47,7 +54,10 @@ use core_test_support::test_codex::local_selections;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
use core_test_support::test_codex::turn_permission_fields;
|
||||
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;
|
||||
use test_case::test_case;
|
||||
use wiremock::MockServer;
|
||||
|
||||
@@ -514,6 +524,150 @@ async fn model_and_personality_change_only_appends_model_instructions() -> Resul
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn settings_update_during_active_turn_applies_to_next_turn_only() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let response_mock = mount_sse_sequence(
|
||||
&server,
|
||||
vec![
|
||||
sse(vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_function_call(
|
||||
"pause-turn",
|
||||
"request_user_input",
|
||||
&json!({
|
||||
"questions": [{
|
||||
"id": "continue",
|
||||
"header": "Continue",
|
||||
"question": "Continue after settings update?",
|
||||
"options": [{
|
||||
"label": "Yes (Recommended)",
|
||||
"description": "Continue the current turn."
|
||||
}, {
|
||||
"label": "No",
|
||||
"description": "Stop the current turn."
|
||||
}]
|
||||
}]
|
||||
})
|
||||
.to_string(),
|
||||
),
|
||||
ev_completed("resp-1"),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_response_created("resp-2"),
|
||||
ev_assistant_message("msg-2", "first turn done"),
|
||||
ev_completed("resp-2"),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_response_created("resp-3"),
|
||||
ev_assistant_message("msg-3", "second turn done"),
|
||||
ev_completed("resp-3"),
|
||||
]),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
let mut builder = test_codex().with_model("gpt-5.2").with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::DefaultModeRequestUserInput)
|
||||
.expect("test config should allow feature update");
|
||||
config.model_reasoning_effort = Some(ReasoningEffort::Low);
|
||||
config.model_reasoning_summary = Some(ReasoningSummary::Concise);
|
||||
config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
|
||||
config.approvals_reviewer = ApprovalsReviewer::User;
|
||||
});
|
||||
let test = builder.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 request = wait_for_event_match(&test.codex, |event| match event {
|
||||
EventMsg::RequestUserInput(request) => Some(request.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.await;
|
||||
|
||||
core_test_support::submit_thread_settings(
|
||||
&test.codex,
|
||||
ThreadSettingsOverrides {
|
||||
model: Some("gpt-5.4".to_string()),
|
||||
effort: Some(Some(ReasoningEffort::High)),
|
||||
summary: Some(ReasoningSummary::Detailed),
|
||||
service_tier: Some(Some(ServiceTier::Fast.request_value().to_string())),
|
||||
approval_policy: Some(AskForApproval::Never),
|
||||
approvals_reviewer: Some(ApprovalsReviewer::AutoReview),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
test.codex
|
||||
.submit(Op::UserInputAnswer {
|
||||
id: request.turn_id,
|
||||
response: RequestUserInputResponse {
|
||||
answers: HashMap::from([(
|
||||
"continue".to_string(),
|
||||
RequestUserInputAnswer {
|
||||
answers: vec!["Yes (Recommended)".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 request_settings = requests
|
||||
.iter()
|
||||
.map(|request| {
|
||||
let body = request.body_json();
|
||||
json!({
|
||||
"model": body["model"],
|
||||
"reasoning": body["reasoning"],
|
||||
"service_tier": body.get("service_tier"),
|
||||
"approval_policy_never": request
|
||||
.message_input_texts("developer")
|
||||
.iter()
|
||||
.any(|text| text.contains("Approval policy is currently never")),
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
request_settings,
|
||||
vec![
|
||||
json!({
|
||||
"model": "gpt-5.2",
|
||||
"reasoning": { "effort": "low", "summary": "concise" },
|
||||
"service_tier": null,
|
||||
"approval_policy_never": false,
|
||||
}),
|
||||
json!({
|
||||
"model": "gpt-5.2",
|
||||
"reasoning": { "effort": "low", "summary": "concise" },
|
||||
"service_tier": null,
|
||||
"approval_policy_never": false,
|
||||
}),
|
||||
json!({
|
||||
"model": "gpt-5.4",
|
||||
"reasoning": { "effort": "high", "summary": "detailed" },
|
||||
"service_tier": "priority",
|
||||
"approval_policy_never": true,
|
||||
}),
|
||||
]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn service_tier_change_is_applied_on_next_http_turn() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
Reference in New Issue
Block a user