fix(tui): normalize Codex reasoning effort labels

This commit is contained in:
Hunter B
2026-06-12 02:07:39 -07:00
parent 6511de3359
commit b0b7317688
6 changed files with 200 additions and 15 deletions
+3 -1
View File
@@ -73,7 +73,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
reconcile stale running fanout counts from manager snapshots.
- **OpenAI Codex reasoning tiers.** Switching from DeepSeek to `openai-codex`
now normalizes stale reasoning state into Responses-compatible
`low`/`medium`/`high`/`xhigh` tiers and reports Codex as a Responses payload
`low`/`medium`/`high`/`xhigh` tiers. Startup, `/config`, and the model
picker now display Codex labels instead of leaking DeepSeek
`off`/`max` names, while Codex still reports as a Responses payload
provider.
- **OpenAI Codex context metadata (#3070).** The `gpt-5.5` default and
CodeWhale aliases now use OpenAI's documented 1,050,000-token context window
+3 -1
View File
@@ -73,7 +73,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
reconcile stale running fanout counts from manager snapshots.
- **OpenAI Codex reasoning tiers.** Switching from DeepSeek to `openai-codex`
now normalizes stale reasoning state into Responses-compatible
`low`/`medium`/`high`/`xhigh` tiers and reports Codex as a Responses payload
`low`/`medium`/`high`/`xhigh` tiers. Startup, `/config`, and the model
picker now display Codex labels instead of leaking DeepSeek
`off`/`max` names, while Codex still reports as a Responses payload
provider.
- **OpenAI Codex context metadata (#3070).** The `gpt-5.5` default and
CodeWhale aliases now use OpenAI's documented 1,050,000-token context window
@@ -795,7 +795,9 @@ pub fn set_config_value(app: &mut App, key: &str, value: &str, persist: bool) ->
settings
.reasoning_effort
.as_deref()
.map_or_else(ReasoningEffort::default, ReasoningEffort::from_setting)
.map_or_else(ReasoningEffort::default, |value| {
ReasoningEffort::from_setting_for_provider(value, app.api_provider)
})
};
app.last_effective_reasoning_effort = None;
app.update_model_compaction_budget();
@@ -824,10 +826,14 @@ pub fn set_config_value(app: &mut App, key: &str, value: &str, persist: bool) ->
.background_color
.clone()
.unwrap_or_else(|| "default".to_string()),
"reasoning_effort" | "effort" => settings
.reasoning_effort
.clone()
.unwrap_or_else(|| "config/default".to_string()),
"reasoning_effort" | "effort" => settings.reasoning_effort.as_deref().map_or_else(
|| "config/default".to_string(),
|value| {
ReasoningEffort::from_setting_for_provider(value, app.api_provider)
.as_setting_for_provider(app.api_provider)
.to_string()
},
),
"composer_vim_mode" | "vim_mode" | "vim" => settings.composer_vim_mode.clone(),
_ => value.to_string(),
};
@@ -1359,6 +1365,35 @@ mod tests {
assert!(app.last_effective_reasoning_effort.is_none());
}
#[test]
fn config_reasoning_effort_uses_codex_provider_labels() {
let temp_root = env::temp_dir().join(format!(
"codewhale-tui-codex-effort-config-test-{}",
std::process::id()
));
fs::create_dir_all(&temp_root).unwrap();
let _guard = EnvGuard::new(&temp_root);
let mut app = create_test_app();
app.api_provider = ApiProvider::OpenaiCodex;
app.reasoning_effort = ReasoningEffort::High;
let result = set_config_value(&mut app, "reasoning_effort", "off", false);
assert_eq!(app.reasoning_effort, ReasoningEffort::Low);
assert_eq!(
result.message.as_deref(),
Some("reasoning_effort = low (session only, add --save to persist)")
);
let result = set_config_value(&mut app, "reasoning_effort", "xhigh", false);
assert_eq!(app.reasoning_effort, ReasoningEffort::Max);
assert_eq!(
result.message.as_deref(),
Some("reasoning_effort = xhigh (session only, add --save to persist)")
);
}
#[test]
fn config_model_accepts_future_deepseek_model_id() {
let mut app = create_test_app();
+44 -1
View File
@@ -210,6 +210,11 @@ impl ReasoningEffort {
}
}
#[must_use]
pub fn from_setting_for_provider(value: &str, provider: ApiProvider) -> Self {
Self::from_setting(value).normalize_for_provider(provider)
}
/// Canonical lowercase label used for config storage and UI hints.
#[must_use]
pub fn as_setting(self) -> &'static str {
@@ -2096,7 +2101,7 @@ impl App {
ReasoningEffort::Auto
} else {
configured_reasoning_effort.map_or_else(ReasoningEffort::default, |s| {
ReasoningEffort::from_setting(s)
ReasoningEffort::from_setting_for_provider(s, provider)
})
};
@@ -5528,6 +5533,44 @@ mod tests {
assert_eq!(app.reasoning_effort_display_label(), "low");
}
#[test]
fn app_new_normalizes_saved_codex_reasoning_effort() {
let _lock = lock_test_env();
let tmp = tempfile::TempDir::new().expect("tempdir");
let config_path = tmp.path().join("config.toml");
let _config_path = EnvVarGuard::set("DEEPSEEK_CONFIG_PATH", &config_path);
let _token = EnvVarGuard::set("OPENAI_CODEX_ACCESS_TOKEN", "test-codex-startup-token");
let config = Config {
provider: Some("openai-codex".to_string()),
providers: Some(ProvidersConfig {
openai_codex: ProviderConfig {
model: Some(crate::config::DEFAULT_OPENAI_CODEX_MODEL.to_string()),
..ProviderConfig::default()
},
..ProvidersConfig::default()
}),
..Config::default()
};
for (raw, expected, display) in [
("off", ReasoningEffort::Low, "low"),
("auto", ReasoningEffort::Medium, "medium"),
("max", ReasoningEffort::Max, "xhigh"),
] {
std::fs::write(
tmp.path().join("settings.toml"),
format!("reasoning_effort = \"{raw}\"\n"),
)
.expect("settings");
let app = App::new(test_options(false), &config);
assert_eq!(app.api_provider, ApiProvider::OpenaiCodex);
assert_eq!(app.reasoning_effort, expected, "raw setting {raw}");
assert_eq!(app.reasoning_effort_display_label(), display);
}
}
#[test]
fn settings_default_provider_auth_check_uses_provider_scoped_key() {
let _lock = lock_test_env();
+80 -1
View File
@@ -155,11 +155,23 @@ impl ModelPickerView {
)
}
fn select_effort_for_current_model(&mut self, effort: ReasoningEffort) {
let provider = self.resolved_provider().unwrap_or(self.initial_provider);
let model_is_auto = self.resolved_model().trim().eq_ignore_ascii_case("auto");
let normalized = normalize_picker_effort(effort, provider, model_is_auto);
self.selected_effort_idx = picker_efforts_for_provider(provider, model_is_auto)
.iter()
.position(|candidate| *candidate == normalized)
.unwrap_or_else(|| default_picker_effort_idx(provider, model_is_auto));
}
fn move_up(&mut self) -> bool {
match self.focus {
Pane::Model => {
if self.selected_model_idx > 0 {
let effort = self.resolved_effort();
self.selected_model_idx -= 1;
self.select_effort_for_current_model(effort);
return true;
}
}
@@ -178,7 +190,9 @@ impl ModelPickerView {
Pane::Model => {
let max = self.model_row_count().saturating_sub(1);
if self.selected_model_idx < max {
let effort = self.resolved_effort();
self.selected_model_idx += 1;
self.select_effort_for_current_model(effort);
return true;
}
}
@@ -503,7 +517,11 @@ impl ModalView for ModelPickerView {
}
KeyCode::Home => {
match self.focus {
Pane::Model => self.selected_model_idx = 0,
Pane::Model => {
let effort = self.resolved_effort();
self.selected_model_idx = 0;
self.select_effort_for_current_model(effort);
}
Pane::Effort => self.selected_effort_idx = 0,
}
ViewAction::None
@@ -511,7 +529,9 @@ impl ModalView for ModelPickerView {
KeyCode::End => {
match self.focus {
Pane::Model => {
let effort = self.resolved_effort();
self.selected_model_idx = self.model_row_count().saturating_sub(1);
self.select_effort_for_current_model(effort);
}
Pane::Effort => {
self.selected_effort_idx = self.current_efforts().len().saturating_sub(1);
@@ -835,6 +855,65 @@ mod tests {
assert_eq!(labels, vec!["low", "medium", "high", "xhigh"]);
}
#[test]
fn picker_remaps_deepseek_off_when_highlighting_saved_codex_model() {
let (mut app, _lock) = create_test_app();
app.api_provider = crate::config::ApiProvider::Deepseek;
app.model = "deepseek-v4-pro".to_string();
app.auto_model = false;
app.reasoning_effort = ReasoningEffort::Off;
app.provider_models
.insert("openai-codex".to_string(), "gpt-5.5".to_string());
let mut view = ModelPickerView::new(&app);
assert_eq!(view.resolved_effort(), ReasoningEffort::Off);
while view.resolved_provider() != Some(crate::config::ApiProvider::OpenaiCodex) {
assert!(
view.move_down(),
"saved Codex model row should be reachable"
);
}
assert_eq!(view.resolved_model(), "gpt-5.5");
assert_eq!(view.resolved_effort(), ReasoningEffort::Low);
assert_eq!(view.selected_effort_idx, 0);
let labels = view
.current_efforts()
.iter()
.map(|effort| {
effort.display_label_for_provider(crate::config::ApiProvider::OpenaiCodex)
})
.collect::<Vec<_>>();
assert_eq!(labels, vec!["low", "medium", "high", "xhigh"]);
}
#[test]
fn picker_remaps_deepseek_max_to_codex_xhigh_when_model_provider_changes() {
let (mut app, _lock) = create_test_app();
app.api_provider = crate::config::ApiProvider::Deepseek;
app.model = "deepseek-v4-pro".to_string();
app.auto_model = false;
app.reasoning_effort = ReasoningEffort::Max;
app.provider_models
.insert("openai-codex".to_string(), "gpt-5.5".to_string());
let mut view = ModelPickerView::new(&app);
while view.resolved_provider() != Some(crate::config::ApiProvider::OpenaiCodex) {
assert!(
view.move_down(),
"saved Codex model row should be reachable"
);
}
assert_eq!(view.resolved_effort(), ReasoningEffort::Max);
assert_eq!(
view.resolved_effort()
.display_label_for_provider(crate::config::ApiProvider::OpenaiCodex),
"xhigh"
);
}
#[test]
fn picker_preserves_unknown_model_via_custom_row() {
let (mut app, _lock) = create_test_app();
+30 -6
View File
@@ -498,11 +498,17 @@ impl ConfigView {
ConfigRow {
section: ConfigSection::Model,
key: "reasoning_effort".to_string(),
value: settings
.reasoning_effort
.as_deref()
.unwrap_or("(config/default)")
.to_string(),
value: settings.reasoning_effort.as_deref().map_or_else(
|| "(config/default)".to_string(),
|value| {
crate::tui::app::ReasoningEffort::from_setting_for_provider(
value,
app.api_provider,
)
.as_setting_for_provider(app.api_provider)
.to_string()
},
),
editable: true,
scope: ConfigScope::Saved,
},
@@ -1146,7 +1152,9 @@ fn config_hint_for_key(key: &str) -> &'static str {
"max_history" => "integer (0 allowed)",
"auto_compact_threshold_percent" => "10..=100",
"default_model" => "deepseek-v4-pro | deepseek-v4-flash | deepseek-* | none/default",
"reasoning_effort" => "auto | off | low | medium | high | max | xhigh | default",
"reasoning_effort" => {
"DeepSeek: auto/off/high/max; Codex: low/medium/high/xhigh; default clears saved value"
}
"mcp_config_path" => "path to mcp.json",
_ => "",
}
@@ -2455,6 +2463,22 @@ base_url = "https://api.xiaomimimo.com/v1"
}
}
#[test]
fn config_view_displays_saved_codex_reasoning_effort_label() {
let _guard = ConfigSettingsEnvGuard::new("reasoning_effort = \"max\"\n");
let mut app = create_test_app();
app.api_provider = crate::config::ApiProvider::OpenaiCodex;
let view = ConfigView::new_for_app(&app);
let row = view
.rows
.iter()
.find(|row| row.key == "reasoning_effort")
.expect("reasoning_effort row");
assert_eq!(row.value, "xhigh");
}
#[test]
fn config_view_filter_matches_group_and_rows() {
let mut view = create_config_view(Locale::En);