diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index f8a41e274e..ec5e3b67bd 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -5246,6 +5246,13 @@ "null" ] }, + "serviceTierForTurn": { + "description": "Override the service tier only when this request starts a new turn. Use \"default\" for standard speed. Omitted or null inherits the thread's tier. Does not change the thread's tier or a turn being steered.", + "type": [ + "string", + "null" + ] + }, "summary": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 5cb982f4c5..780cdf059b 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -23939,6 +23939,13 @@ "null" ] }, + "serviceTierForTurn": { + "description": "Override the service tier only when this request starts a new turn. Use \"default\" for standard speed. Omitted or null inherits the thread's tier. Does not change the thread's tier or a turn being steered.", + "type": [ + "string", + "null" + ] + }, "summary": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 08dd4e5ba9..79fed91877 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -21674,6 +21674,13 @@ "null" ] }, + "serviceTierForTurn": { + "description": "Override the service tier only when this request starts a new turn. Use \"default\" for standard speed. Omitted or null inherits the thread's tier. Does not change the thread's tier or a turn being steered.", + "type": [ + "string", + "null" + ] + }, "summary": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json index 477e57e43e..197bdeff8c 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json @@ -655,6 +655,13 @@ "null" ] }, + "serviceTierForTurn": { + "description": "Override the service tier only when this request starts a new turn. Use \"default\" for standard speed. Omitted or null inherits the thread's tier. Does not change the thread's tier or a turn being steered.", + "type": [ + "string", + "null" + ] + }, "summary": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst index d28f194bea..5397e9b489 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst differ diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst index cd8a109d13..a134b06aec 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst differ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts index afe1ac6d94..7bccf79043 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts @@ -30,6 +30,11 @@ model?: string | null, /** * Override the service tier for this turn and subsequent turns. */ serviceTier?: string | null | null, /** + * Override the service tier only when this request starts a new turn. + * Use "default" for standard speed. Omitted or null inherits the thread's tier. + * Does not change the thread's tier or a turn being steered. + */ +serviceTierForTurn?: string | null, /** * Override the reasoning effort for this turn and subsequent turns. */ effort?: ReasoningEffort | null, /** diff --git a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs index cf081d7c95..309196dd1c 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs @@ -4725,6 +4725,7 @@ fn turn_start_params_preserve_explicit_null_service_tier() { permissions: None, model: None, service_tier: None, + service_tier_for_turn: None, effort: None, summary: None, output_schema: None, diff --git a/codex-rs/app-server-protocol/src/protocol/v2/turn.rs b/codex-rs/app-server-protocol/src/protocol/v2/turn.rs index 0b2c092ec8..b92a1a2883 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/turn.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/turn.rs @@ -131,6 +131,11 @@ pub struct TurnStartParams { )] #[ts(optional = nullable)] pub service_tier: Option>, + /// Override the service tier only when this request starts a new turn. + /// Use "default" for standard speed. Omitted or null inherits the thread's tier. + /// Does not change the thread's tier or a turn being steered. + #[ts(optional = nullable)] + pub service_tier_for_turn: Option, /// Override the reasoning effort for this turn and subsequent turns. #[ts(optional = nullable)] pub effort: Option, diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 0bfad24875..68d2cb406c 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -903,6 +903,8 @@ The `audio` variant accepts data URLs. Other URL schemes are rejected. `localAud You can optionally specify config overrides on the new turn. If specified, these settings become the default for subsequent turns on the same thread. `outputSchema` applies only to the current turn. Experimental `environments` is turn-scoped: omit it to inherit the thread's sticky environments, pass `[]` to run the turn with no environments, or pass explicit environment ids to override the sticky selection for this turn only. +`serviceTierForTurn` overrides the tier only when the request starts a new turn, without changing the thread's saved tier. Use `"default"` for standard speed, or omit it (or pass `null`) to inherit the thread's tier. It is ignored when the request steers an active turn. The existing `serviceTier` field still changes the tier for subsequent turns, including when both fields are supplied. + `approvalsReviewer` accepts: - `"user"` — default. Review approval requests directly in the client. diff --git a/codex-rs/app-server/src/message_processor_tracing_tests.rs b/codex-rs/app-server/src/message_processor_tracing_tests.rs index cd9d22d426..f3246bb453 100644 --- a/codex-rs/app-server/src/message_processor_tracing_tests.rs +++ b/codex-rs/app-server/src/message_processor_tracing_tests.rs @@ -673,6 +673,7 @@ async fn turn_start_jsonrpc_span_parents_core_turn_spans() -> Result<()> { approvals_reviewer: None, model: None, service_tier: None, + service_tier_for_turn: None, effort: None, summary: None, personality: None, diff --git a/codex-rs/app-server/src/request_processors/turn_processor.rs b/codex-rs/app-server/src/request_processors/turn_processor.rs index 5dfd2c5701..a300d5a43a 100644 --- a/codex-rs/app-server/src/request_processors/turn_processor.rs +++ b/codex-rs/app-server/src/request_processors/turn_processor.rs @@ -540,6 +540,7 @@ impl TurnRequestProcessor { .with_thread_settings(thread_settings) .on_start(TurnStartOptions { final_output_json_schema: params.output_schema, + service_tier: params.service_tier_for_turn, ..Default::default() }) .with_additional_context(additional_context) diff --git a/codex-rs/app-server/tests/suite/v2/turn_start.rs b/codex-rs/app-server/tests/suite/v2/turn_start.rs index baaab3c776..d598d7befd 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -709,7 +709,7 @@ async fn turn_start_sends_service_tier_id_to_model_request() -> Result<()> { responses::ev_assistant_message("msg-1", "Done"), responses::ev_completed("resp-1"), ]); - let response_mock = responses::mount_sse_once(&server, body).await; + let response_mock = responses::mount_sse_once(&server, body.clone()).await; let codex_home = TempDir::new()?; MockResponsesConfig::new(&server.uri()).write(codex_home.path())?; @@ -736,7 +736,7 @@ async fn turn_start_sends_service_tier_id_to_model_request() -> Result<()> { .request(|request_id| ClientRequest::TurnStart { request_id, params: TurnStartParams { - thread_id: thread.id, + thread_id: thread.id.clone(), service_tier: Some(Some(service_tier_id.clone())), input: vec![V2UserInput::Text { text: "Hello".to_string(), @@ -757,6 +757,31 @@ async fn turn_start_sends_service_tier_id_to_model_request() -> Result<()> { json!(service_tier_id) ); + for (service_tier_for_turn, expected_service_tier) in [ + (Some("default".to_string()), None), + (None, Some(json!(service_tier_id))), + ] { + let response_mock = responses::mount_sse_once(&server, body.clone()).await; + mcp.start_turn_and_wait_for_completion(TurnStartParams { + thread_id: thread.id.clone(), + service_tier_for_turn, + input: vec![V2UserInput::Text { + text: "Hello again".to_string(), + text_elements: Vec::new(), + }], + ..Default::default() + }) + .await?; + assert_eq!( + response_mock + .single_request() + .body_json() + .get("service_tier") + .cloned(), + expected_service_tier + ); + } + Ok(()) } @@ -2759,6 +2784,7 @@ async fn turn_start_explicit_local_environment_updates_legacy_cwd_between_turns( effort: Some(ReasoningEffort::Medium), summary: Some(ReasoningSummary::Auto), service_tier: None, + service_tier_for_turn: None, personality: None, output_schema: None, collaboration_mode: None, @@ -2802,6 +2828,7 @@ async fn turn_start_explicit_local_environment_updates_legacy_cwd_between_turns( effort: Some(ReasoningEffort::Medium), summary: Some(ReasoningSummary::Auto), service_tier: None, + service_tier_for_turn: None, personality: None, output_schema: None, collaboration_mode: None, diff --git a/codex-rs/core/src/codex_delegate.rs b/codex-rs/core/src/codex_delegate.rs index 52a72b8970..b23d82b507 100644 --- a/codex-rs/core/src/codex_delegate.rs +++ b/codex-rs/core/src/codex_delegate.rs @@ -214,6 +214,7 @@ pub(crate) async fn run_codex_thread_one_shot( .submit_turn_input( TurnInputRequest::user_input(input).on_start(TurnStartOptions { final_output_json_schema, + service_tier: None, parent_turn_id: Some(parent_turn_id), root_turn_id, }), diff --git a/codex-rs/core/src/guardian/review_session.rs b/codex-rs/core/src/guardian/review_session.rs index bb3f92e1cf..563381973a 100644 --- a/codex-rs/core/src/guardian/review_session.rs +++ b/codex-rs/core/src/guardian/review_session.rs @@ -1179,6 +1179,7 @@ async fn run_review_on_session( }) .on_start(TurnStartOptions { final_output_json_schema: Some(params.schema.clone()), + service_tier: None, parent_turn_id: Some(parent_turn.sub_id.clone()), root_turn_id: parent_turn.turn_metadata_state.root_turn_id(), }), diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index 350187abeb..c074c5dde1 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -602,6 +602,7 @@ pub(crate) struct SessionSettingsUpdate { pub(crate) collaboration_mode: Option, pub(crate) reasoning_summary: Option, pub(crate) service_tier: Option>, + pub(crate) service_tier_for_turn: Option, pub(crate) final_output_json_schema: Option>, pub(crate) personality: Option, pub(crate) app_server_client_name: Option, diff --git a/codex-rs/core/src/session/turn_context.rs b/codex-rs/core/src/session/turn_context.rs index 5389c652bf..6d057fcfd3 100644 --- a/codex-rs/core/src/session/turn_context.rs +++ b/codex-rs/core/src/session/turn_context.rs @@ -788,7 +788,7 @@ impl Session { }; let ( - session_configuration, + mut session_configuration, mcp_inputs_changed, permission_profile_changed, previous_config, @@ -817,6 +817,10 @@ impl Session { self.refresh_managed_network_proxy_for_current_permission_profile() .await; } + // Apply the override only to the turn's copy, after persisting thread settings. + if let Some(service_tier) = updates.service_tier_for_turn { + session_configuration.service_tier = Some(service_tier); + } Ok(self .new_turn_from_configuration( sub_id, diff --git a/codex-rs/core/src/session/turn_input.rs b/codex-rs/core/src/session/turn_input.rs index f4985c33f1..1e8bc70340 100644 --- a/codex-rs/core/src/session/turn_input.rs +++ b/codex-rs/core/src/session/turn_input.rs @@ -98,12 +98,14 @@ impl PreparedTurnInputSettings { ) -> CodexResult> { let TurnStartOptions { final_output_json_schema, + service_tier, parent_turn_id, root_turn_id, } = self.start_options; let emit_thread_settings_applied = self.thread_settings_update.is_some(); let mut updates = self.thread_settings_update.unwrap_or_default(); updates.final_output_json_schema = Some(final_output_json_schema); + updates.service_tier_for_turn = service_tier; // new_turn_with_sub_id already emits an error event when settings are invalid. let turn_context = session diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index f0b573e3eb..efb59b4424 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -986,6 +986,7 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> { permissions: None, model: None, service_tier: None, + service_tier_for_turn: None, effort: default_effort, summary: None, personality: None, diff --git a/codex-rs/protocol/src/turn_input.rs b/codex-rs/protocol/src/turn_input.rs index e7b58a957a..2fe52a1a43 100644 --- a/codex-rs/protocol/src/turn_input.rs +++ b/codex-rs/protocol/src/turn_input.rs @@ -146,6 +146,8 @@ pub struct TurnStartOptions { /// Structured-output schema for a new turn. When steering, Core rejects /// the input if the active turn uses a different schema. pub final_output_json_schema: Option, + /// Service tier for a new turn, without changing the thread's preference. + pub service_tier: Option, /// Parent turn lineage recorded if this request starts a new turn. pub parent_turn_id: Option, /// Causal root turn lineage recorded if this request starts a new turn. diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index 1b89e841f3..5a60b3af58 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -1214,6 +1214,7 @@ impl AppServerSession { permissions, model: Some(model), service_tier, + service_tier_for_turn: None, effort, summary, personality,