fix(tui): normalize Codex reasoning effort labels
This commit is contained in:
+3
-1
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user