Skip to content

Commit cf36c68

Browse files
authored
[codex] Use managed defaults for TUI threads (#30147)
## Why #29683 exposes managed defaults for new-thread model settings through `configRequirements/read` without applying them server-wide. The TUI is an app-server client, so it should explicitly consume those defaults when it creates a fresh thread. This lets plain `codex` start on the managed model while preserving the existing ability to change model settings within the thread. ## What changed - Read `requirements.models.newThread` during TUI app-server bootstrap. - Apply the managed model, reasoning effort, and service tier to the initial fresh thread and subsequent `/new` or `/clear` threads. - Keep explicit launch overrides above the managed defaults. - Normalize the managed `fast` service tier to the `priority` request value. - Leave resumed and forked threads unchanged. The application logic lives in a small TUI-only module; app-server `thread/start` behavior remains unchanged for other clients. ## User experience - Plain `codex` starts with the managed new-thread settings. - A user can still change settings with `/model` or the existing service-tier controls. - Starting another fresh thread reapplies the managed defaults. - Explicit launch choices such as `codex -m <model>` continue to win. ## Validation - `just test -p codex-tui managed_new_thread_defaults` - `just fix -p codex-tui` Depends on #29683.
1 parent 79a8ffd commit cf36c68

6 files changed

Lines changed: 205 additions & 2 deletions

File tree

codex-rs/tui/src/app.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ use crate::legacy_core::config::ConfigBuilder;
5151
use crate::legacy_core::config::ConfigOverrides;
5252
use crate::legacy_core::config::PermissionProfileSnapshot;
5353
use crate::legacy_core::config::edit::ConfigEditsBuilder;
54+
use crate::managed_new_thread_defaults::apply_managed_new_thread_defaults;
5455
use crate::model_catalog::ModelCatalog;
5556
use crate::model_migration::ModelMigrationOutcome;
5657
use crate::model_migration::migration_copy_for_models;
@@ -794,7 +795,18 @@ impl App {
794795
None => app_server.bootstrap(&config).await?,
795796
};
796797
let bootstrap_ms = bootstrap.duration.as_millis();
797-
let mut model = bootstrap.default_model;
798+
if matches!(
799+
&session_selection,
800+
SessionSelection::StartFresh | SessionSelection::Exit
801+
) {
802+
apply_managed_new_thread_defaults(
803+
&mut config,
804+
app_server.managed_new_thread_defaults(),
805+
&cli_kv_overrides,
806+
&harness_overrides,
807+
);
808+
}
809+
let mut model = config.model.clone().unwrap_or(bootstrap.default_model);
798810
let available_models = bootstrap.available_models;
799811
let remote_connection = crate::status::remote_connection::remote_connection_status_value(
800812
&app_server_target,

codex-rs/tui/src/app/session_lifecycle.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -539,7 +539,13 @@ impl App {
539539
self.refresh_in_memory_config_from_disk_best_effort("starting a new thread")
540540
.await;
541541
let model = self.chat_widget.current_model().to_string();
542-
let config = self.fresh_session_config();
542+
let mut config = self.fresh_session_config();
543+
apply_managed_new_thread_defaults(
544+
&mut config,
545+
app_server.managed_new_thread_defaults(),
546+
&self.cli_kv_overrides,
547+
&self.harness_overrides,
548+
);
543549
let summary = session_summary(
544550
self.chat_widget.token_usage(),
545551
self.chat_widget.thread_id(),

codex-rs/tui/src/app_server_session.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ use codex_app_server_protocol::AskForApproval;
2424
use codex_app_server_protocol::AuthMode;
2525
use codex_app_server_protocol::ClientRequest;
2626
use codex_app_server_protocol::ConfigBatchWriteParams;
27+
use codex_app_server_protocol::ConfigRequirementsReadResponse;
2728
use codex_app_server_protocol::ConfigWriteResponse;
2829
use codex_app_server_protocol::ExternalAgentConfigDetectParams;
2930
use codex_app_server_protocol::ExternalAgentConfigDetectResponse;
@@ -39,6 +40,7 @@ use codex_app_server_protocol::MemoryResetResponse;
3940
use codex_app_server_protocol::Model as ApiModel;
4041
use codex_app_server_protocol::ModelListParams;
4142
use codex_app_server_protocol::ModelListResponse;
43+
use codex_app_server_protocol::NewThreadModelDefaults;
4244
use codex_app_server_protocol::RateLimitSnapshot;
4345
use codex_app_server_protocol::RequestId;
4446
use codex_app_server_protocol::ReviewDelivery;
@@ -178,6 +180,7 @@ pub(crate) struct AppServerSession {
178180
thread_settings_update_supported: bool,
179181
default_model: Option<String>,
180182
available_models: Vec<ModelPreset>,
183+
managed_new_thread_defaults: Option<NewThreadModelDefaults>,
181184
external_agent_config_import_completion_pending: AtomicBool,
182185
}
183186

@@ -222,6 +225,7 @@ impl AppServerSession {
222225
thread_settings_update_supported: true,
223226
default_model: None,
224227
available_models: Vec::new(),
228+
managed_new_thread_defaults: None,
225229
external_agent_config_import_completion_pending: AtomicBool::new(false),
226230
}
227231
}
@@ -260,6 +264,21 @@ impl AppServerSession {
260264
pub(crate) async fn bootstrap(&mut self, config: &Config) -> Result<AppServerBootstrap> {
261265
let started_at = Instant::now();
262266
let account = self.read_account().await?;
267+
let requirements_request_id = self.next_request_id();
268+
let requirements: ConfigRequirementsReadResponse = self
269+
.client
270+
.request_typed(ClientRequest::ConfigRequirementsRead {
271+
request_id: requirements_request_id,
272+
params: None,
273+
})
274+
.await
275+
.map_err(|err| {
276+
bootstrap_request_error("configRequirements/read failed during TUI bootstrap", err)
277+
})?;
278+
self.managed_new_thread_defaults = requirements
279+
.requirements
280+
.and_then(|requirements| requirements.models)
281+
.and_then(|models| models.new_thread);
263282
let model_request_id = self.next_request_id();
264283
let models: ModelListResponse = self
265284
.client
@@ -350,6 +369,10 @@ impl AppServerSession {
350369
})
351370
}
352371

372+
pub(crate) fn managed_new_thread_defaults(&self) -> Option<&NewThreadModelDefaults> {
373+
self.managed_new_thread_defaults.as_ref()
374+
}
375+
353376
/// Fetches the current account info without refreshing the auth token.
354377
///
355378
/// Used by both `bootstrap` (to populate the initial UI) and `get_login_status`

codex-rs/tui/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ mod line_truncation;
137137
pub(crate) mod live_wrap;
138138
pub use live_wrap::RowBuilder;
139139
mod local_chatgpt_auth;
140+
mod managed_new_thread_defaults;
140141
mod markdown;
141142
mod markdown_render;
142143
mod markdown_stream;
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
use crate::legacy_core::config::Config;
2+
use crate::legacy_core::config::ConfigOverrides;
3+
use codex_app_server_protocol::NewThreadModelDefaults;
4+
use codex_protocol::config_types::ServiceTier;
5+
use toml::Value as TomlValue;
6+
7+
pub(crate) fn apply_managed_new_thread_defaults(
8+
config: &mut Config,
9+
defaults: Option<&NewThreadModelDefaults>,
10+
cli_kv_overrides: &[(String, TomlValue)],
11+
harness_overrides: &ConfigOverrides,
12+
) {
13+
let Some(defaults) = defaults else {
14+
return;
15+
};
16+
// Managed values are defaults rather than enforcement. Preserve explicit launch choices from
17+
// dedicated flags such as `-m` (`harness_overrides`) and generic `-c key=value` settings
18+
// (`cli_kv_overrides`), then fill only the fields that were not selected for this invocation.
19+
// Model and reasoning effort are a compatibility-sensitive pair, so an explicit override of
20+
// either opts out of both managed values. For example, `codex -m gpt-5.4` keeps that model and
21+
// its existing/default effort, while `-c model_reasoning_effort=low` does not switch to the
22+
// managed model. Service tier remains independent and is resolved against the selected model
23+
// before the thread starts.
24+
let has_cli_override = |key: &str| cli_kv_overrides.iter().any(|(path, _value)| path == key);
25+
let has_explicit_model_settings = harness_overrides.model.is_some()
26+
|| has_cli_override("model")
27+
|| has_cli_override("model_reasoning_effort");
28+
29+
if !has_explicit_model_settings && let Some(model) = defaults.model.as_ref() {
30+
config.model = Some(model.clone());
31+
}
32+
if !has_explicit_model_settings
33+
&& let Some(reasoning_effort) = defaults.model_reasoning_effort.as_ref()
34+
{
35+
config.model_reasoning_effort = Some(reasoning_effort.clone());
36+
}
37+
if harness_overrides.service_tier.is_none()
38+
&& !has_cli_override("service_tier")
39+
&& let Some(service_tier) = defaults.service_tier.as_ref()
40+
{
41+
config.service_tier = Some(
42+
ServiceTier::from_request_value(service_tier)
43+
.map(|tier| tier.request_value().to_string())
44+
.unwrap_or_else(|| service_tier.clone()),
45+
);
46+
}
47+
}
48+
49+
#[cfg(test)]
50+
#[path = "managed_new_thread_defaults_tests.rs"]
51+
mod tests;
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
use super::*;
2+
use crate::legacy_core::config::ConfigBuilder;
3+
use codex_protocol::openai_models::ReasoningEffort;
4+
use pretty_assertions::assert_eq;
5+
6+
async fn test_config() -> Config {
7+
let codex_home = tempfile::tempdir().expect("tempdir").keep();
8+
ConfigBuilder::default()
9+
.codex_home(codex_home)
10+
.build()
11+
.await
12+
.expect("config")
13+
}
14+
15+
fn defaults() -> NewThreadModelDefaults {
16+
NewThreadModelDefaults {
17+
model: Some("managed-model".to_string()),
18+
model_reasoning_effort: Some(ReasoningEffort::High),
19+
service_tier: Some("fast".to_string()),
20+
}
21+
}
22+
23+
#[tokio::test]
24+
async fn applies_managed_defaults_to_a_new_thread_config() {
25+
let mut actual = test_config().await;
26+
actual.model = Some("configured-model".to_string());
27+
actual.model_reasoning_effort = Some(ReasoningEffort::Low);
28+
actual.service_tier = Some("flex".to_string());
29+
let mut expected = actual.clone();
30+
expected.model = Some("managed-model".to_string());
31+
expected.model_reasoning_effort = Some(ReasoningEffort::High);
32+
expected.service_tier = Some(ServiceTier::Fast.request_value().to_string());
33+
34+
apply_managed_new_thread_defaults(
35+
&mut actual,
36+
Some(&defaults()),
37+
&[],
38+
&ConfigOverrides::default(),
39+
);
40+
41+
assert_eq!(actual, expected);
42+
}
43+
44+
#[tokio::test]
45+
async fn explicit_model_skips_managed_model_and_reasoning_effort() {
46+
let mut actual = test_config().await;
47+
actual.model = Some("explicit-model".to_string());
48+
actual.model_reasoning_effort = None;
49+
actual.service_tier = Some("flex".to_string());
50+
let mut expected = actual.clone();
51+
expected.service_tier = Some(ServiceTier::Fast.request_value().to_string());
52+
let harness_overrides = ConfigOverrides {
53+
model: Some("explicit-model".to_string()),
54+
..ConfigOverrides::default()
55+
};
56+
57+
apply_managed_new_thread_defaults(&mut actual, Some(&defaults()), &[], &harness_overrides);
58+
59+
assert_eq!(actual, expected);
60+
}
61+
62+
#[tokio::test]
63+
async fn explicit_reasoning_effort_skips_managed_model_and_reasoning_effort() {
64+
let mut actual = test_config().await;
65+
actual.model = Some("configured-model".to_string());
66+
actual.model_reasoning_effort = Some(ReasoningEffort::Low);
67+
actual.service_tier = Some("flex".to_string());
68+
let mut expected = actual.clone();
69+
expected.service_tier = Some(ServiceTier::Fast.request_value().to_string());
70+
let cli_kv_overrides = vec![(
71+
"model_reasoning_effort".to_string(),
72+
TomlValue::String("low".to_string()),
73+
)];
74+
75+
apply_managed_new_thread_defaults(
76+
&mut actual,
77+
Some(&defaults()),
78+
&cli_kv_overrides,
79+
&ConfigOverrides::default(),
80+
);
81+
82+
assert_eq!(actual, expected);
83+
}
84+
85+
#[tokio::test]
86+
async fn explicit_launch_overrides_take_precedence() {
87+
let mut actual = test_config().await;
88+
actual.model = Some("explicit-model".to_string());
89+
actual.model_reasoning_effort = Some(ReasoningEffort::Low);
90+
actual.service_tier = Some("flex".to_string());
91+
let expected = actual.clone();
92+
let cli_kv_overrides = vec![(
93+
"model_reasoning_effort".to_string(),
94+
TomlValue::String("low".to_string()),
95+
)];
96+
let harness_overrides = ConfigOverrides {
97+
model: Some("explicit-model".to_string()),
98+
service_tier: Some(Some("flex".to_string())),
99+
..ConfigOverrides::default()
100+
};
101+
102+
apply_managed_new_thread_defaults(
103+
&mut actual,
104+
Some(&defaults()),
105+
&cli_kv_overrides,
106+
&harness_overrides,
107+
);
108+
109+
assert_eq!(actual, expected);
110+
}

0 commit comments

Comments
 (0)