mirror of
https://github.com/openai/codex.git
synced 2026-11-09 20:50:33 +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
@@ -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()),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user