mirror of
https://github.com/openai/codex.git
synced 2026-10-25 07:55:34 +00:00
Add a turn-scoped service tier override (#40616)
## What changed - Add `serviceTierForTurn` to `turn/start` so a newly started turn can override the service tier without changing the thread's saved tier. - Treat `"default"` as standard speed and omitted or `null` values as inheriting the thread tier. - Keep `serviceTier` authoritative for subsequent turns when both fields are supplied, and ignore the turn-scoped override when steering an active turn. ## Testing - Extend the app-server turn-start test to verify both the one-turn `"default"` override and inheritance on the following turn. GitOrigin-RevId: 4c2c4db21d8d31617a7a6e6df65bf54834e72383
This commit is contained in:
@@ -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": [
|
||||
{
|
||||
|
||||
+7
@@ -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": [
|
||||
{
|
||||
|
||||
+7
@@ -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": [
|
||||
{
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -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, /**
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -131,6 +131,11 @@ pub struct TurnStartParams {
|
||||
)]
|
||||
#[ts(optional = nullable)]
|
||||
pub service_tier: Option<Option<String>>,
|
||||
/// 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<String>,
|
||||
/// Override the reasoning effort for this turn and subsequent turns.
|
||||
#[ts(optional = nullable)]
|
||||
pub effort: Option<ReasoningEffort>,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
|
||||
@@ -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(),
|
||||
}),
|
||||
|
||||
@@ -602,6 +602,7 @@ pub(crate) struct SessionSettingsUpdate {
|
||||
pub(crate) collaboration_mode: Option<CollaborationMode>,
|
||||
pub(crate) reasoning_summary: Option<ReasoningSummaryConfig>,
|
||||
pub(crate) service_tier: Option<Option<String>>,
|
||||
pub(crate) service_tier_for_turn: Option<String>,
|
||||
pub(crate) final_output_json_schema: Option<Option<Value>>,
|
||||
pub(crate) personality: Option<Personality>,
|
||||
pub(crate) app_server_client_name: Option<String>,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -98,12 +98,14 @@ impl PreparedTurnInputSettings {
|
||||
) -> CodexResult<Arc<TurnContext>> {
|
||||
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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<Value>,
|
||||
/// Service tier for a new turn, without changing the thread's preference.
|
||||
pub service_tier: Option<String>,
|
||||
/// Parent turn lineage recorded if this request starts a new turn.
|
||||
pub parent_turn_id: Option<String>,
|
||||
/// Causal root turn lineage recorded if this request starts a new turn.
|
||||
|
||||
@@ -1214,6 +1214,7 @@ impl AppServerSession {
|
||||
permissions,
|
||||
model: Some(model),
|
||||
service_tier,
|
||||
service_tier_for_turn: None,
|
||||
effort,
|
||||
summary,
|
||||
personality,
|
||||
|
||||
Reference in New Issue
Block a user