mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-11 01:51:34 -10:00
refactor(tui): move reasoning preferences out of UI app module
Move ReasoningEffort and EffectiveReasoningEffort (with all impls) from tui::app::types into the crate-level reasoning_preference module so engine, client routing, settings, and tools resolve effort without depending on presentation types. Repoint every consumer and drop the temporary re-export shim in tui/app/types.rs; tui::app no longer re-exports either type. Verification (real output): - cargo check -p codewhale-tui --all-targets --locked: pass, 0 warnings - cargo test -p codewhale-tui --lib --locked reasoning: test result: ok. 229 passed; 0 failed; 0 ignored; 0 measured; 12368 filtered out
This commit is contained in:
1 parent
798d3d6647
commit
76f8d0044d
37 files changed
+588
-574
No files matched your search
@@ -4,7 +4,7 @@
|
||||
//! [`select`] before each turn-level request to pick the actual tier
|
||||
//! based on the current message.
|
||||
|
||||
use crate::tui::app::ReasoningEffort;
|
||||
use crate::reasoning_preference::ReasoningEffort;
|
||||
|
||||
/// Choose a concrete `ReasoningEffort` tier for the next API request.
|
||||
///
|
||||
|
||||
@@ -917,8 +917,8 @@ fn tool_to_responses_function(tool: &Tool) -> Value {
|
||||
}
|
||||
|
||||
fn codex_responses_reasoning_effort(raw: &str) -> Option<&'static str> {
|
||||
crate::tui::app::ReasoningEffort::parse_strict(raw)
|
||||
.unwrap_or(crate::tui::app::ReasoningEffort::Medium)
|
||||
crate::reasoning_preference::ReasoningEffort::parse_strict(raw)
|
||||
.unwrap_or(crate::reasoning_preference::ReasoningEffort::Medium)
|
||||
.api_value_for_provider(ApiProvider::OpenaiCodex)
|
||||
}
|
||||
|
||||
|
||||
@@ -540,7 +540,7 @@ fn codex_reasoning_effort_uses_responses_labels() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn codex_selected_effort_reaches_preview_wire_and_restored_receipt_unchanged() {
|
||||
use crate::tui::app::{EffectiveReasoningEffort, ReasoningEffort};
|
||||
use crate::reasoning_preference::{EffectiveReasoningEffort, ReasoningEffort};
|
||||
use crate::work_graph::WorkActivityEvent;
|
||||
|
||||
let server = MockServer::start().await;
|
||||
|
||||
@@ -70,7 +70,8 @@ use crate::dependencies::ExternalTool as _;
|
||||
use crate::localization::{MessageId, tr};
|
||||
use crate::network_policy::NetworkPolicy;
|
||||
use crate::pricing::CostCurrency;
|
||||
use crate::tui::app::{App, ReasoningEffort};
|
||||
use crate::reasoning_preference::ReasoningEffort;
|
||||
use crate::tui::app::App;
|
||||
use crate::tui::history::HistoryCell;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -17,11 +17,9 @@ use crate::config_persistence::{
|
||||
persist_tui_integer_key, persist_unset_root_key,
|
||||
};
|
||||
use crate::localization::{MessageId, resolve_locale, tr};
|
||||
use crate::reasoning_preference::ReasoningEffort;
|
||||
use crate::settings::Settings;
|
||||
use crate::tui::app::{
|
||||
App, AppAction, AppMode, OnboardingState, ReasoningEffort, ScreenMode, SettingSelection,
|
||||
VimMode,
|
||||
};
|
||||
use crate::tui::app::{App, AppAction, AppMode, OnboardingState, ScreenMode, SettingSelection, VimMode};
|
||||
use anyhow::Result;
|
||||
use codewhale_execpolicy::ApprovalMode;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::config::{
|
||||
};
|
||||
use crate::localization::{Locale, MessageId, tr};
|
||||
#[cfg(test)]
|
||||
use crate::tui::app::ReasoningEffort;
|
||||
use crate::reasoning_preference::ReasoningEffort;
|
||||
use crate::tui::app::{App, AppAction, AppMode};
|
||||
use crate::tui::views::{HelpView, ModalKind, SubAgentsView, subagent_view_agents};
|
||||
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
|
||||
use crate::commands::{CommandResult, traits::CommandInfo};
|
||||
use crate::localization::{MessageId, tr};
|
||||
use crate::tui::app::{App, ReasoningEffort};
|
||||
use crate::reasoning_preference::ReasoningEffort;
|
||||
use crate::tui::app::App;
|
||||
|
||||
pub const EFFORT_INFO: CommandInfo = CommandInfo {
|
||||
name: "effort",
|
||||
|
||||
@@ -752,7 +752,7 @@ mod tests {
|
||||
),
|
||||
crate::config::DEEPSEEK_ALIAS_REPLACEMENT
|
||||
);
|
||||
app.reasoning_effort = crate::tui::app::ReasoningEffort::Max;
|
||||
app.reasoning_effort = crate::reasoning_preference::ReasoningEffort::Max;
|
||||
app.reasoning_effort_preference = None;
|
||||
app.apply_provider_switch_reasoning_effort(
|
||||
provider,
|
||||
@@ -762,9 +762,9 @@ mod tests {
|
||||
assert_eq!(
|
||||
app.reasoning_effort,
|
||||
if alias == "deepseek-chat" {
|
||||
crate::tui::app::ReasoningEffort::Off
|
||||
crate::reasoning_preference::ReasoningEffort::Off
|
||||
} else {
|
||||
crate::tui::app::ReasoningEffort::High
|
||||
crate::reasoning_preference::ReasoningEffort::High
|
||||
},
|
||||
"{provider:?} {alias}"
|
||||
);
|
||||
@@ -805,7 +805,7 @@ mod tests {
|
||||
),
|
||||
"deepseek-reasoner"
|
||||
);
|
||||
app.reasoning_effort = crate::tui::app::ReasoningEffort::Max;
|
||||
app.reasoning_effort = crate::reasoning_preference::ReasoningEffort::Max;
|
||||
app.reasoning_effort_preference = None;
|
||||
app.apply_provider_switch_reasoning_effort(
|
||||
provider,
|
||||
@@ -814,11 +814,11 @@ mod tests {
|
||||
);
|
||||
assert_eq!(
|
||||
app.reasoning_effort,
|
||||
crate::tui::app::ReasoningEffort::Max,
|
||||
crate::reasoning_preference::ReasoningEffort::Max,
|
||||
"custom endpoint owns alias semantics"
|
||||
);
|
||||
|
||||
app.reasoning_effort_preference = Some(crate::tui::app::ReasoningEffort::Max);
|
||||
app.reasoning_effort_preference = Some(crate::reasoning_preference::ReasoningEffort::Max);
|
||||
app.apply_provider_switch_reasoning_effort(
|
||||
provider,
|
||||
crate::config::DEFAULT_DEEPSEEK_BASE_URL,
|
||||
@@ -826,7 +826,7 @@ mod tests {
|
||||
);
|
||||
assert_eq!(
|
||||
app.reasoning_effort,
|
||||
crate::tui::app::ReasoningEffort::Max,
|
||||
crate::reasoning_preference::ReasoningEffort::Max,
|
||||
"explicit effort must beat compatibility inference"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -566,7 +566,7 @@ fn cache_inspect_json_keys_auto_replay_to_the_last_concrete_route() {
|
||||
let mut app = create_test_app();
|
||||
app.model = "auto".to_string();
|
||||
app.auto_model = true;
|
||||
app.reasoning_effort = crate::tui::app::ReasoningEffort::Off;
|
||||
app.reasoning_effort = crate::reasoning_preference::ReasoningEffort::Off;
|
||||
app.last_effective_provider = Some(crate::config::ApiProvider::OpenaiCodex);
|
||||
app.last_effective_provider_identity =
|
||||
Some(crate::config::ApiProvider::OpenaiCodex.as_str().to_string());
|
||||
|
||||
@@ -11,9 +11,10 @@ use tempfile::TempDir;
|
||||
use crate::commands::CommandResult;
|
||||
use crate::config::Config;
|
||||
use crate::models::Role;
|
||||
use crate::reasoning_preference::ReasoningEffort;
|
||||
use crate::session_manager::create_saved_session_with_id_and_mode;
|
||||
use crate::test_support::EnvVarGuard;
|
||||
use crate::tui::app::{App, AppAction, AppMode, ReasoningEffort, TuiOptions, TurnCacheRecord};
|
||||
use crate::tui::app::{App, AppAction, AppMode, TuiOptions, TurnCacheRecord};
|
||||
use crate::tui::history::HistoryCell;
|
||||
|
||||
fn dispatch_lifecycle(app: &mut App, name: &str, arg: Option<&str>) -> CommandResult {
|
||||
@@ -123,7 +124,7 @@ fn save_preserves_latest_auto_route_receipt() {
|
||||
app.last_effective_model = Some(crate::config::ZAI_GLM_5_TURBO_MODEL.to_string());
|
||||
app.last_auto_route_receipt = Some(receipt.clone());
|
||||
app.last_effective_reasoning_effort =
|
||||
Some(crate::tui::app::EffectiveReasoningEffort::ThinkingEnabledGranularityUnavailable);
|
||||
Some(crate::reasoning_preference::EffectiveReasoningEffort::ThinkingEnabledGranularityUnavailable);
|
||||
|
||||
let result = save(&mut app, Some(save_path.to_str().unwrap()));
|
||||
|
||||
@@ -635,7 +636,7 @@ fn load_auto_model_session_defers_model_restore_to_event_loop() {
|
||||
saved_app.set_model_selection("auto".to_string());
|
||||
saved_app.last_effective_model = Some("deepseek-v4-flash".to_string());
|
||||
saved_app.last_effective_reasoning_effort = Some(
|
||||
crate::tui::app::EffectiveReasoningEffort::Tier(ReasoningEffort::Low),
|
||||
crate::reasoning_preference::EffectiveReasoningEffort::Tier(ReasoningEffort::Low),
|
||||
);
|
||||
let save_path = tmpdir.path().join("auto_model.json");
|
||||
save(&mut saved_app, Some(save_path.to_str().unwrap()));
|
||||
|
||||
@@ -761,9 +761,9 @@ async fn plan_for(
|
||||
model,
|
||||
auto_model,
|
||||
if auto_model {
|
||||
crate::tui::app::ReasoningEffort::Auto
|
||||
crate::reasoning_preference::ReasoningEffort::Auto
|
||||
} else {
|
||||
crate::tui::app::ReasoningEffort::High
|
||||
crate::reasoning_preference::ReasoningEffort::High
|
||||
},
|
||||
prompt,
|
||||
)
|
||||
@@ -779,7 +779,7 @@ async fn plan_with_reasoning(
|
||||
provider: ApiProvider,
|
||||
model: &str,
|
||||
auto_model: bool,
|
||||
reasoning_effort: crate::tui::app::ReasoningEffort,
|
||||
reasoning_effort: crate::reasoning_preference::ReasoningEffort,
|
||||
prompt: &str,
|
||||
) -> crate::turn_route_plan::PlannedTurnRoute {
|
||||
crate::turn_route_plan::plan_turn_route(crate::turn_route_plan::TurnRoutePlanRequest {
|
||||
@@ -1334,7 +1334,7 @@ struct MatrixRoute {
|
||||
provider_key: &'static str,
|
||||
base_url: &'static str,
|
||||
model: &'static str,
|
||||
requested_reasoning: crate::tui::app::ReasoningEffort,
|
||||
requested_reasoning: crate::reasoning_preference::ReasoningEffort,
|
||||
requested_reasoning_label: &'static str,
|
||||
/// Reasoning-control keys the manifest must report, in receipt order.
|
||||
expect_control_keys: &'static [&'static str],
|
||||
@@ -1354,7 +1354,7 @@ fn glm_5_2_zai_coding() -> MatrixRoute {
|
||||
provider_key: "zai",
|
||||
base_url: crate::config::DEFAULT_ZAI_BASE_URL,
|
||||
model: crate::config::ZAI_GLM_5_2_MODEL,
|
||||
requested_reasoning: crate::tui::app::ReasoningEffort::High,
|
||||
requested_reasoning: crate::reasoning_preference::ReasoningEffort::High,
|
||||
requested_reasoning_label: "high",
|
||||
expect_control_keys: &["reasoning_effort", "thinking"],
|
||||
expect_wire_effort: Some("high"),
|
||||
@@ -1371,7 +1371,7 @@ fn glm_5_turbo_zai() -> MatrixRoute {
|
||||
provider_key: "zai",
|
||||
base_url: crate::config::DEFAULT_ZAI_BASE_URL,
|
||||
model: crate::config::ZAI_GLM_5_TURBO_MODEL,
|
||||
requested_reasoning: crate::tui::app::ReasoningEffort::High,
|
||||
requested_reasoning: crate::reasoning_preference::ReasoningEffort::High,
|
||||
requested_reasoning_label: "high",
|
||||
// No invented granularity: the toggle ships, the tier does not.
|
||||
expect_control_keys: &["thinking"],
|
||||
@@ -1390,7 +1390,7 @@ fn kimi_k3_moonshot_direct() -> MatrixRoute {
|
||||
base_url: crate::config::DEFAULT_MOONSHOT_BASE_URL,
|
||||
model: crate::config::MOONSHOT_KIMI_K3_MODEL,
|
||||
// The visible normalization: `off` is not a tier this route has.
|
||||
requested_reasoning: crate::tui::app::ReasoningEffort::Off,
|
||||
requested_reasoning: crate::reasoning_preference::ReasoningEffort::Off,
|
||||
requested_reasoning_label: "off",
|
||||
expect_control_keys: &["reasoning_effort"],
|
||||
expect_wire_effort: Some("low"),
|
||||
@@ -1407,7 +1407,7 @@ fn k3_kimi_code() -> MatrixRoute {
|
||||
provider_key: "moonshot",
|
||||
base_url: crate::config::DEFAULT_KIMI_CODE_BASE_URL,
|
||||
model: crate::config::KIMI_CODE_K3_MODEL,
|
||||
requested_reasoning: crate::tui::app::ReasoningEffort::Off,
|
||||
requested_reasoning: crate::reasoning_preference::ReasoningEffort::Off,
|
||||
requested_reasoning_label: "off",
|
||||
expect_control_keys: &["thinking"],
|
||||
expect_wire_effort: Some("low"),
|
||||
@@ -1424,7 +1424,7 @@ fn minimax_m3() -> MatrixRoute {
|
||||
provider_key: "minimax",
|
||||
base_url: crate::config::DEFAULT_MINIMAX_BASE_URL,
|
||||
model: crate::config::DEFAULT_MINIMAX_MODEL,
|
||||
requested_reasoning: crate::tui::app::ReasoningEffort::High,
|
||||
requested_reasoning: crate::reasoning_preference::ReasoningEffort::High,
|
||||
requested_reasoning_label: "high",
|
||||
expect_control_keys: &["thinking", "reasoning_split"],
|
||||
expect_wire_effort: None,
|
||||
|
||||
@@ -55,7 +55,7 @@ use super::role::{
|
||||
use crate::config::{ApiProvider, Config};
|
||||
use crate::llm_client::LlmClient;
|
||||
use crate::models::Role;
|
||||
use crate::tui::app::ReasoningEffort;
|
||||
use crate::reasoning_preference::ReasoningEffort;
|
||||
|
||||
/// Where exact Fleet definitions and Reasoning Router profiles are looked up,
|
||||
/// labelled so an identity can be qualified (`workspace/glm-pair`) instead of
|
||||
|
||||
@@ -8,7 +8,7 @@ use std::path::{Path, PathBuf};
|
||||
use anyhow::{Context, Result, anyhow, bail};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::tui::app::ReasoningEffort;
|
||||
use crate::reasoning_preference::ReasoningEffort;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
pub use codewhale_config::{
|
||||
|
||||
+17
-16
@@ -98,6 +98,7 @@ mod provider_catalog_live;
|
||||
mod provider_lake;
|
||||
mod provider_readiness;
|
||||
mod purge;
|
||||
pub mod reasoning_preference;
|
||||
mod regex_cache;
|
||||
mod remote_control;
|
||||
mod remote_setup;
|
||||
@@ -11198,7 +11199,7 @@ async fn run_interactive_with_notice(
|
||||
struct CliAutoRoute {
|
||||
provider: crate::config::ApiProvider,
|
||||
model: String,
|
||||
reasoning_effort: Option<crate::tui::app::ReasoningEffort>,
|
||||
reasoning_effort: Option<crate::reasoning_preference::ReasoningEffort>,
|
||||
/// Whether the runtime should continue resolving reasoning per prompt.
|
||||
///
|
||||
/// This is independent from `auto_model`: an Auto model can carry a fixed
|
||||
@@ -11210,7 +11211,7 @@ struct CliAutoRoute {
|
||||
fn cli_reasoning_effort_value(
|
||||
config: &Config,
|
||||
model: &str,
|
||||
effort: crate::tui::app::ReasoningEffort,
|
||||
effort: crate::reasoning_preference::ReasoningEffort,
|
||||
) -> Option<String> {
|
||||
effort
|
||||
.api_value_for_route(config.api_provider(), &config.deepseek_base_url(), model)
|
||||
@@ -11220,10 +11221,10 @@ fn cli_reasoning_effort_value(
|
||||
fn cli_reasoning_effort_value_for_prompt(
|
||||
config: &Config,
|
||||
model: &str,
|
||||
effort: crate::tui::app::ReasoningEffort,
|
||||
effort: crate::reasoning_preference::ReasoningEffort,
|
||||
prompt: &str,
|
||||
) -> Option<String> {
|
||||
let resolved = if effort == crate::tui::app::ReasoningEffort::Auto {
|
||||
let resolved = if effort == crate::reasoning_preference::ReasoningEffort::Auto {
|
||||
crate::auto_reasoning::select(false, prompt)
|
||||
} else {
|
||||
effort
|
||||
@@ -11242,7 +11243,7 @@ fn normalize_cli_reasoning_effort(value: &str) -> Result<Option<String>> {
|
||||
) {
|
||||
return Ok(None);
|
||||
}
|
||||
crate::tui::app::ReasoningEffort::parse_strict(trimmed)
|
||||
crate::reasoning_preference::ReasoningEffort::parse_strict(trimmed)
|
||||
.map(|effort| Some(effort.as_setting().to_string()))
|
||||
.map_err(anyhow::Error::msg)
|
||||
}
|
||||
@@ -11272,7 +11273,7 @@ async fn resolve_cli_auto_route(
|
||||
let preference = config
|
||||
.reasoning_effort()
|
||||
.filter(|_| config.reasoning_effort_is_explicit())
|
||||
.map(crate::tui::app::ReasoningEffort::from_setting);
|
||||
.map(crate::reasoning_preference::ReasoningEffort::from_setting);
|
||||
let (reasoning_effort, auto_controls_reasoning) =
|
||||
model_routing::resolve_auto_model_reasoning(preference, selection.reasoning_effort);
|
||||
Ok(CliAutoRoute {
|
||||
@@ -11287,7 +11288,7 @@ async fn resolve_cli_auto_route(
|
||||
{
|
||||
let auto_controls_reasoning = matches!(
|
||||
selection.reasoning_effort,
|
||||
Some(crate::tui::app::ReasoningEffort::Auto)
|
||||
Some(crate::reasoning_preference::ReasoningEffort::Auto)
|
||||
);
|
||||
return Ok(CliAutoRoute {
|
||||
provider: selection.provider,
|
||||
@@ -11321,13 +11322,13 @@ async fn resolve_cli_auto_route(
|
||||
// 30+ second SSE idle timeouts on trivial prompts.
|
||||
let reasoning_effort = config
|
||||
.reasoning_effort()
|
||||
.map(crate::tui::app::ReasoningEffort::from_setting);
|
||||
.map(crate::reasoning_preference::ReasoningEffort::from_setting);
|
||||
Ok(CliAutoRoute {
|
||||
provider: config.api_provider(),
|
||||
model: model.to_string(),
|
||||
auto_controls_reasoning: matches!(
|
||||
reasoning_effort,
|
||||
Some(crate::tui::app::ReasoningEffort::Auto)
|
||||
Some(crate::reasoning_preference::ReasoningEffort::Auto)
|
||||
),
|
||||
reasoning_effort,
|
||||
auto_model: false,
|
||||
@@ -11344,13 +11345,13 @@ async fn resolve_cli_exec_route(
|
||||
if force_configured_route && !model.trim().eq_ignore_ascii_case("auto") {
|
||||
let reasoning_effort = config
|
||||
.reasoning_effort()
|
||||
.map(crate::tui::app::ReasoningEffort::from_setting);
|
||||
.map(crate::reasoning_preference::ReasoningEffort::from_setting);
|
||||
return Ok(CliAutoRoute {
|
||||
provider: config.api_provider(),
|
||||
model: model.to_string(),
|
||||
auto_controls_reasoning: matches!(
|
||||
reasoning_effort,
|
||||
Some(crate::tui::app::ReasoningEffort::Auto)
|
||||
Some(crate::reasoning_preference::ReasoningEffort::Auto)
|
||||
),
|
||||
reasoning_effort,
|
||||
auto_model: false,
|
||||
@@ -15131,7 +15132,7 @@ reasoning = "high"
|
||||
assert!(route.auto_model);
|
||||
assert_eq!(
|
||||
route.reasoning_effort,
|
||||
Some(crate::tui::app::ReasoningEffort::Low)
|
||||
Some(crate::reasoning_preference::ReasoningEffort::Low)
|
||||
);
|
||||
assert!(
|
||||
!route.auto_controls_reasoning,
|
||||
@@ -16784,7 +16785,7 @@ api_key = "test-only-key"
|
||||
cli_reasoning_effort_value_for_prompt(
|
||||
&config,
|
||||
crate::config::KIMI_CODE_K3_MODEL,
|
||||
crate::tui::app::ReasoningEffort::Auto,
|
||||
crate::reasoning_preference::ReasoningEffort::Auto,
|
||||
prompt,
|
||||
)
|
||||
.as_deref(),
|
||||
@@ -16797,7 +16798,7 @@ api_key = "test-only-key"
|
||||
cli_reasoning_effort_value_for_prompt(
|
||||
&config,
|
||||
crate::config::KIMI_CODE_K3_MODEL,
|
||||
crate::tui::app::ReasoningEffort::Off,
|
||||
crate::reasoning_preference::ReasoningEffort::Off,
|
||||
"debug must not override an explicit effort",
|
||||
)
|
||||
.as_deref(),
|
||||
@@ -16808,7 +16809,7 @@ api_key = "test-only-key"
|
||||
|
||||
#[test]
|
||||
fn cli_route_tracks_auto_reasoning_independently_from_auto_model() {
|
||||
use crate::tui::app::ReasoningEffort;
|
||||
use crate::reasoning_preference::ReasoningEffort;
|
||||
|
||||
let fixed_model_auto_reasoning = CliAutoRoute {
|
||||
provider: crate::config::ApiProvider::Deepseek,
|
||||
@@ -16861,7 +16862,7 @@ api_key = "test-only-key"
|
||||
let resolved = cli_reasoning_effort_value_for_prompt(
|
||||
&config,
|
||||
crate::config::ZAI_GLM_5_2_MODEL,
|
||||
crate::tui::app::ReasoningEffort::Auto,
|
||||
crate::reasoning_preference::ReasoningEffort::Auto,
|
||||
"debug this failing integration test",
|
||||
)
|
||||
.expect("Auto must resolve to a concrete tier");
|
||||
|
||||
@@ -17,7 +17,7 @@ use crate::llm_client::LlmClient;
|
||||
use crate::model_inventory::ModelInventory;
|
||||
use crate::models::Role;
|
||||
use crate::models::{ContentBlock, Message, MessageRequest, MessageResponse, SystemPrompt};
|
||||
use crate::tui::app::ReasoningEffort;
|
||||
use crate::reasoning_preference::ReasoningEffort;
|
||||
|
||||
/// Big/cheap model pair the auto-router may choose between for the active
|
||||
/// provider (#3018).
|
||||
|
||||
@@ -0,0 +1,476 @@
|
||||
//! Reasoning preferences and effective tiers (#5316).
|
||||
//!
|
||||
//! Separated from TUI app state so engine, client routing, settings, and
|
||||
//! tools can resolve and normalize reasoning effort without depending on
|
||||
//! presentation types.
|
||||
|
||||
use crate::config::ApiProvider;
|
||||
use crate::work_graph::ReasoningEffortTier;
|
||||
|
||||
/// Reasoning-effort tier, mirrored across DeepSeek and Codex effort pickers.
|
||||
///
|
||||
/// The config file accepts every supported string value for forward-compat with
|
||||
/// providers that expose the full spectrum; DeepSeek currently collapses
|
||||
/// `Low`/`Medium` → `high`. OpenAI Codex normalizes inherited DeepSeek-only
|
||||
/// `Off` to `Low` and keeps `XHigh`, `Max`, and `Ultra` distinct at the
|
||||
/// provider boundary. The default keyboard cycler walks the three DeepSeek-distinct
|
||||
/// tiers: `Off` → `High` → `Max` → `Off`; provider-aware callers should use
|
||||
/// [`ReasoningEffort::cycle_next_in`] with the route's effort list. Auto
|
||||
/// routing has no concrete provider yet, so
|
||||
/// [`ReasoningEffort::cycle_next_for_auto_model`] retains the full
|
||||
/// provider-neutral preference vocabulary until dispatch.
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ReasoningEffort {
|
||||
Off,
|
||||
Minimal,
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
XHigh,
|
||||
Ultra,
|
||||
Auto,
|
||||
#[default]
|
||||
Max,
|
||||
}
|
||||
|
||||
/// Provider-effective reasoning state used by durable receipts and visible
|
||||
/// requested-to-effective labels.
|
||||
///
|
||||
/// Some routes, notably first-party GLM-5-Turbo, support a thinking toggle but
|
||||
/// publish no effort tiers. Keeping that state distinct prevents a requested
|
||||
/// `max` from being displayed or persisted as an effective `max` claim.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum EffectiveReasoningEffort {
|
||||
Tier(ReasoningEffort),
|
||||
ThinkingEnabledGranularityUnavailable,
|
||||
Unavailable,
|
||||
}
|
||||
|
||||
impl EffectiveReasoningEffort {
|
||||
/// Reconstruct a safe request tier for cache replay and inspection.
|
||||
///
|
||||
/// Routes with an enabled-but-untiered receipt collapse every non-Off
|
||||
/// request to the same wire toggle, so High is the canonical value that
|
||||
/// keeps reasoning enabled without claiming a granular effective tier.
|
||||
#[must_use]
|
||||
pub const fn request_tier_for_replay(self) -> Option<ReasoningEffort> {
|
||||
match self {
|
||||
Self::Tier(tier) => Some(tier),
|
||||
Self::ThinkingEnabledGranularityUnavailable => Some(ReasoningEffort::High),
|
||||
Self::Unavailable => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EffectiveReasoningEffort> for ReasoningEffortTier {
|
||||
fn from(value: EffectiveReasoningEffort) -> Self {
|
||||
match value {
|
||||
EffectiveReasoningEffort::Tier(tier) => tier.into(),
|
||||
EffectiveReasoningEffort::ThinkingEnabledGranularityUnavailable => {
|
||||
Self::ThinkingEnabledGranularityUnavailable
|
||||
}
|
||||
EffectiveReasoningEffort::Unavailable => Self::Unavailable,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ReasoningEffortTier> for EffectiveReasoningEffort {
|
||||
fn from(value: ReasoningEffortTier) -> Self {
|
||||
match value {
|
||||
ReasoningEffortTier::Off => Self::Tier(ReasoningEffort::Off),
|
||||
ReasoningEffortTier::Minimal => Self::Tier(ReasoningEffort::Minimal),
|
||||
ReasoningEffortTier::Low => Self::Tier(ReasoningEffort::Low),
|
||||
ReasoningEffortTier::Medium => Self::Tier(ReasoningEffort::Medium),
|
||||
ReasoningEffortTier::High => Self::Tier(ReasoningEffort::High),
|
||||
ReasoningEffortTier::XHigh => Self::Tier(ReasoningEffort::XHigh),
|
||||
ReasoningEffortTier::Ultra => Self::Tier(ReasoningEffort::Ultra),
|
||||
ReasoningEffortTier::Auto => Self::Tier(ReasoningEffort::Auto),
|
||||
ReasoningEffortTier::Max => Self::Tier(ReasoningEffort::Max),
|
||||
ReasoningEffortTier::ThinkingEnabledGranularityUnavailable => {
|
||||
Self::ThinkingEnabledGranularityUnavailable
|
||||
}
|
||||
ReasoningEffortTier::Unavailable => Self::Unavailable,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ReasoningEffort> for ReasoningEffortTier {
|
||||
fn from(value: ReasoningEffort) -> Self {
|
||||
match value {
|
||||
ReasoningEffort::Off => Self::Off,
|
||||
ReasoningEffort::Minimal => Self::Minimal,
|
||||
ReasoningEffort::Low => Self::Low,
|
||||
ReasoningEffort::Medium => Self::Medium,
|
||||
ReasoningEffort::High => Self::High,
|
||||
ReasoningEffort::XHigh => Self::XHigh,
|
||||
ReasoningEffort::Ultra => Self::Ultra,
|
||||
ReasoningEffort::Auto => Self::Auto,
|
||||
ReasoningEffort::Max => Self::Max,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ReasoningEffort {
|
||||
/// Parse an operator-supplied effort value.
|
||||
///
|
||||
/// This is deliberately the one canonical spelling table for every
|
||||
/// human-facing route. Callers that read an old persisted config may use
|
||||
/// [`Self::from_setting`] for its compatibility fallback, but a new CLI,
|
||||
/// settings, or tool input must reject an unknown value instead of quietly
|
||||
/// turning it into `max`.
|
||||
pub fn parse_strict(value: &str) -> Result<Self, String> {
|
||||
let trimmed = value.trim();
|
||||
match trimmed.to_ascii_lowercase().as_str() {
|
||||
"off" | "disabled" | "none" | "false" => Ok(Self::Off),
|
||||
"low" | "minimum" | "minimal" | "light" => Ok(Self::Low),
|
||||
"medium" | "mid" => Ok(Self::Medium),
|
||||
"high" => Ok(Self::High),
|
||||
"xhigh" => Ok(Self::XHigh),
|
||||
"auto" | "automatic" => Ok(Self::Auto),
|
||||
"ultra" | "ultracode" => Ok(Self::Ultra),
|
||||
"max" | "maximum" => Ok(Self::Max),
|
||||
_ => Err(format!(
|
||||
"Unrecognized reasoning effort {trimmed:?}. Expected: auto, off, low, medium, high, xhigh, max, or ultra."
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a persisted config-file string into an effort tier. Unknown
|
||||
/// legacy values fall back to the default (`Max`) so an old malformed
|
||||
/// settings file never prevents startup. New user input should use
|
||||
/// [`Self::parse_strict`] instead.
|
||||
#[must_use]
|
||||
pub fn from_setting(value: &str) -> Self {
|
||||
Self::parse_strict(value).unwrap_or_default()
|
||||
}
|
||||
|
||||
#[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 {
|
||||
match self {
|
||||
Self::Off => "off",
|
||||
Self::Minimal => "minimal",
|
||||
Self::Low => "low",
|
||||
Self::Medium => "medium",
|
||||
Self::High => "high",
|
||||
Self::XHigh => "xhigh",
|
||||
Self::Ultra => "ultra",
|
||||
Self::Auto => "auto",
|
||||
Self::Max => "max",
|
||||
}
|
||||
}
|
||||
|
||||
/// Short label for the header chip.
|
||||
#[must_use]
|
||||
pub fn short_label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Off => "off",
|
||||
Self::Minimal => "minimal",
|
||||
Self::Low => "low",
|
||||
Self::Medium => "med",
|
||||
Self::High => "high",
|
||||
Self::XHigh => "xhigh",
|
||||
Self::Ultra => "ultra",
|
||||
Self::Auto => "auto",
|
||||
Self::Max => "max",
|
||||
}
|
||||
}
|
||||
|
||||
/// Provider-facing label for user-visible surfaces.
|
||||
#[must_use]
|
||||
pub fn display_label_for_provider(self, provider: ApiProvider) -> &'static str {
|
||||
match (provider, self.normalize_for_provider(provider)) {
|
||||
(ApiProvider::OpenaiCodex, Self::Minimal) => "low",
|
||||
(ApiProvider::OpenaiCodex, Self::Low) => "low",
|
||||
(ApiProvider::OpenaiCodex, Self::Medium) => "medium",
|
||||
(ApiProvider::OpenaiCodex, Self::High) => "high",
|
||||
// `xhigh`, `max` and `ultra` are three distinct rungs the Codex
|
||||
// roster publishes per model; collapsing them onto "xhigh" dates
|
||||
// from when xhigh was the ceiling and made the top tiers
|
||||
// unreachable and indistinguishable.
|
||||
(ApiProvider::OpenaiCodex, Self::XHigh) => "xhigh",
|
||||
(ApiProvider::OpenaiCodex, Self::Ultra) => "ultra",
|
||||
(ApiProvider::OpenaiCodex, Self::Max) => "max",
|
||||
(ApiProvider::Xai, Self::XHigh) => "xhigh",
|
||||
(_, effort) => effort.short_label(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Value forwarded to the engine/client. `None` means "provider default"
|
||||
/// (for `Off` we still emit `"off"` so the client can inject
|
||||
/// `thinking = {"type": "disabled"}`).
|
||||
#[must_use]
|
||||
pub fn api_value(self) -> Option<&'static str> {
|
||||
Some(self.as_setting())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn normalize_for_provider(self, provider: ApiProvider) -> Self {
|
||||
if provider != ApiProvider::OpenaiCodex {
|
||||
return self;
|
||||
}
|
||||
match self {
|
||||
Self::Off => Self::Low,
|
||||
Self::Auto => Self::Medium,
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve an effort against the exact provider route that will receive
|
||||
/// the request. Both K3 routes are always-thinking, so `off` becomes the
|
||||
/// lowest supported tier. The Kimi Code membership route otherwise keeps
|
||||
/// its low/high/max mapping; direct Moonshot K3 additionally maps `medium`
|
||||
/// to `high`. First-party DeepSeek routes keep `low` (the wire documents
|
||||
/// low/high/max) while rounding `medium` up to `high`. Models that publish
|
||||
/// a Models.dev `reasoning_options` effort list keep that vocabulary
|
||||
/// instead of the historic Low/Medium collapse. Generic Moonshot and
|
||||
/// every other non-Codex route retain the historic high coercion.
|
||||
/// This intentionally does not change [`Self::normalize_for_provider`],
|
||||
/// whose generic wire semantics are used by older callers that do not yet
|
||||
/// have a route receipt.
|
||||
#[must_use]
|
||||
pub fn normalize_for_route(
|
||||
self,
|
||||
provider: ApiProvider,
|
||||
base_url: &str,
|
||||
wire_model: &str,
|
||||
) -> Self {
|
||||
let normalized = self.normalize_for_provider(provider);
|
||||
if crate::config::is_exact_kimi_code_k3_route(provider, base_url, wire_model) {
|
||||
return match normalized {
|
||||
Self::Off => Self::Low,
|
||||
other => other,
|
||||
};
|
||||
}
|
||||
if crate::config::is_exact_direct_moonshot_k3_route(provider, base_url, wire_model) {
|
||||
return match normalized {
|
||||
Self::Off => Self::Low,
|
||||
Self::Medium => Self::High,
|
||||
other => other,
|
||||
};
|
||||
}
|
||||
if provider == ApiProvider::OpenaiCodex {
|
||||
return normalized;
|
||||
}
|
||||
// First-party DeepSeek routes document `reasoning_effort` low/high/max
|
||||
// on the wire (no medium), so `low` is a real, cheaper tier there and
|
||||
// must reach the wire as low; `medium` rounds up to high because the
|
||||
// dialect has no such value (#52).
|
||||
if matches!(provider, ApiProvider::Deepseek | ApiProvider::DeepseekCN) {
|
||||
return match normalized {
|
||||
Self::Low => Self::Low,
|
||||
Self::Medium => Self::High,
|
||||
other => other,
|
||||
};
|
||||
}
|
||||
// Ollama's current OpenAI-compatible Chat Completions contract
|
||||
// documents the complete none/low/medium/high/max ladder. Keep every
|
||||
// real tier distinct for normal turns; only Codewhale-only synonyms
|
||||
// are folded onto the nearest documented spelling.
|
||||
if provider == ApiProvider::OllamaCloud {
|
||||
return match normalized {
|
||||
Self::Minimal => Self::Low,
|
||||
Self::XHigh | Self::Ultra => Self::Max,
|
||||
other => other,
|
||||
};
|
||||
}
|
||||
if let Some(values) = Self::catalog_effort_values(provider, wire_model) {
|
||||
return Self::clamp_to_catalog_efforts(normalized, provider, wire_model, &values);
|
||||
}
|
||||
match normalized {
|
||||
Self::Low | Self::Medium => Self::High,
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn catalog_default(provider: ApiProvider, wire_model: &str) -> Option<Self> {
|
||||
let offering = crate::provider_lake::catalog_offering_for_model(provider, wire_model)?;
|
||||
offering.reasoning_options.iter().find_map(|option| {
|
||||
option
|
||||
.get("type")
|
||||
.and_then(|value| value.as_str())
|
||||
.filter(|kind| kind.eq_ignore_ascii_case("effort"))?;
|
||||
option
|
||||
.get("default")
|
||||
.and_then(|value| value.as_str())
|
||||
.and_then(Self::from_catalog_token)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn catalog_effort_values(provider: ApiProvider, wire_model: &str) -> Option<Vec<Self>> {
|
||||
let offering = crate::provider_lake::catalog_offering_for_model(provider, wire_model)?;
|
||||
let mut efforts = Vec::new();
|
||||
for option in &offering.reasoning_options {
|
||||
if !option
|
||||
.get("type")
|
||||
.and_then(|value| value.as_str())
|
||||
.is_some_and(|kind| kind.eq_ignore_ascii_case("effort"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let Some(values) = option.get("values").and_then(|value| value.as_array()) else {
|
||||
continue;
|
||||
};
|
||||
for value in values {
|
||||
if let Some(effort) = value.as_str().and_then(Self::from_catalog_token)
|
||||
&& !efforts.contains(&effort)
|
||||
{
|
||||
efforts.push(effort);
|
||||
}
|
||||
}
|
||||
}
|
||||
(!efforts.is_empty()).then_some(efforts)
|
||||
}
|
||||
|
||||
pub fn from_catalog_token(raw: &str) -> Option<Self> {
|
||||
match raw.trim().to_ascii_lowercase().as_str() {
|
||||
"off" | "disabled" | "none" | "false" => Some(Self::Off),
|
||||
"minimal" | "minimum" => Some(Self::Minimal),
|
||||
"low" | "light" => Some(Self::Low),
|
||||
"medium" | "mid" => Some(Self::Medium),
|
||||
"high" => Some(Self::High),
|
||||
"xhigh" => Some(Self::XHigh),
|
||||
"ultra" | "ultracode" => Some(Self::Ultra),
|
||||
"max" | "maximum" => Some(Self::Max),
|
||||
"auto" | "automatic" | "adaptive" => Some(Self::Auto),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn clamp_to_catalog_efforts(
|
||||
normalized: Self,
|
||||
provider: ApiProvider,
|
||||
wire_model: &str,
|
||||
values: &[Self],
|
||||
) -> Self {
|
||||
if matches!(normalized, Self::Auto) || values.contains(&normalized) {
|
||||
return normalized;
|
||||
}
|
||||
let aliased = match normalized {
|
||||
Self::Minimal if values.contains(&Self::Low) => Self::Low,
|
||||
Self::Max | Self::Ultra if values.contains(&Self::XHigh) => Self::XHigh,
|
||||
Self::Off => Self::catalog_default(provider, wire_model).unwrap_or(Self::High),
|
||||
other => other,
|
||||
};
|
||||
if values.contains(&aliased) {
|
||||
aliased
|
||||
} else {
|
||||
Self::catalog_default(provider, wire_model).unwrap_or(Self::High)
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn api_value_for_provider(self, provider: ApiProvider) -> Option<&'static str> {
|
||||
if provider != ApiProvider::OpenaiCodex {
|
||||
return self.api_value();
|
||||
}
|
||||
Some(match self.normalize_for_provider(provider) {
|
||||
Self::Minimal => "low",
|
||||
Self::Low => "low",
|
||||
Self::Medium => "medium",
|
||||
Self::High => "high",
|
||||
Self::XHigh => "xhigh",
|
||||
Self::Ultra => "ultra",
|
||||
Self::Max => "max",
|
||||
Self::Off => "low",
|
||||
Self::Auto => "medium",
|
||||
})
|
||||
}
|
||||
|
||||
/// Provider-facing value after exact-route normalization.
|
||||
#[must_use]
|
||||
pub fn api_value_for_route(
|
||||
self,
|
||||
provider: ApiProvider,
|
||||
base_url: &str,
|
||||
wire_model: &str,
|
||||
) -> Option<&'static str> {
|
||||
self.normalize_for_route(provider, base_url, wire_model)
|
||||
.api_value_for_provider(provider)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn as_setting_for_provider(self, provider: ApiProvider) -> &'static str {
|
||||
self.api_value_for_provider(provider)
|
||||
.unwrap_or_else(|| self.as_setting())
|
||||
}
|
||||
|
||||
/// Persist the canonical setting after exact-route normalization.
|
||||
#[must_use]
|
||||
pub fn as_setting_for_route(
|
||||
self,
|
||||
provider: ApiProvider,
|
||||
base_url: &str,
|
||||
wire_model: &str,
|
||||
) -> &'static str {
|
||||
self.normalize_for_route(provider, base_url, wire_model)
|
||||
.as_setting_for_provider(provider)
|
||||
}
|
||||
|
||||
/// Cycle through the three behaviorally distinct tiers.
|
||||
#[must_use]
|
||||
pub fn cycle_next(self) -> Self {
|
||||
match self {
|
||||
Self::Off => Self::High,
|
||||
Self::Auto => Self::Off,
|
||||
Self::Minimal | Self::Low | Self::Medium | Self::High | Self::XHigh | Self::Ultra => {
|
||||
Self::Max
|
||||
}
|
||||
Self::Max => Self::Off,
|
||||
}
|
||||
}
|
||||
|
||||
/// Advance through an exact-route effort list. Unknown current values
|
||||
/// enter at the first listed tier so a persisted `max` on an `xhigh`
|
||||
/// ladder, or `off` on an always-thinking model, still moves.
|
||||
#[must_use]
|
||||
pub fn cycle_next_in(self, efforts: &[Self]) -> Self {
|
||||
if efforts.is_empty() {
|
||||
return self.cycle_next();
|
||||
}
|
||||
if let Some(index) = self.index_in(efforts) {
|
||||
return efforts[(index + 1) % efforts.len()];
|
||||
}
|
||||
efforts[0]
|
||||
}
|
||||
|
||||
fn index_in(self, efforts: &[Self]) -> Option<usize> {
|
||||
efforts
|
||||
.iter()
|
||||
.position(|&effort| effort == self)
|
||||
.or_else(|| {
|
||||
let aliases: &[Self] = match self {
|
||||
Self::Max | Self::Ultra => &[Self::XHigh],
|
||||
Self::XHigh => &[Self::Max],
|
||||
Self::Minimal => &[Self::Low],
|
||||
Self::Low => &[Self::Minimal],
|
||||
_ => return None,
|
||||
};
|
||||
aliases
|
||||
.iter()
|
||||
.find_map(|alias| efforts.iter().position(|&effort| effort == *alias))
|
||||
})
|
||||
}
|
||||
|
||||
/// Cycle the unresolved auto-model preference without applying any
|
||||
/// provider's normalization rules prematurely.
|
||||
#[must_use]
|
||||
pub fn cycle_next_for_auto_model(self) -> Self {
|
||||
match self {
|
||||
Self::Auto => Self::Off,
|
||||
Self::Off => Self::Minimal,
|
||||
Self::Minimal => Self::Low,
|
||||
Self::Low => Self::Medium,
|
||||
Self::Medium => Self::High,
|
||||
Self::High => Self::XHigh,
|
||||
Self::XHigh => Self::Ultra,
|
||||
Self::Ultra => Self::Max,
|
||||
Self::Max => Self::Auto,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@
|
||||
//! ## Reasoning-effort enum reuse
|
||||
//!
|
||||
//! [`RequestTuning::reasoning_effort`] reuses the canonical
|
||||
//! [`crate::tui::app::ReasoningEffort`] enum rather than defining a local
|
||||
//! [`crate::reasoning_preference::ReasoningEffort`] enum rather than defining a local
|
||||
//! `Off/Low/Medium/High` copy. That enum is the single source of truth for the
|
||||
//! effort tiers across the DeepSeek and Codex effort pickers, it is already
|
||||
//! imported by sibling top-level modules (`auto_reasoning`, `model_routing`),
|
||||
@@ -19,7 +19,7 @@
|
||||
//! Defining a parallel local enum here would duplicate that surface and risk
|
||||
//! drift, so we import the existing type.
|
||||
//!
|
||||
use crate::tui::app::ReasoningEffort;
|
||||
use crate::reasoning_preference::ReasoningEffort;
|
||||
|
||||
/// Optional request-tuning knobs a caller may attach to a model request.
|
||||
///
|
||||
|
||||
@@ -6381,7 +6381,7 @@ fn provider_model_entry_for_api(
|
||||
provider: ApiProvider,
|
||||
model: String,
|
||||
) -> ProviderModelEntry {
|
||||
use crate::tui::app::ReasoningEffort;
|
||||
use crate::reasoning_preference::ReasoningEffort;
|
||||
use codewhale_config::route::CapabilityState;
|
||||
|
||||
let mut entry = ProviderModelEntry {
|
||||
|
||||
@@ -1236,7 +1236,7 @@ impl RuntimeChatPrompt {
|
||||
return Err("Runtime relay turns must use Chat mode.".to_string());
|
||||
}
|
||||
if let Some(reasoning) = self.reasoning_effort.as_deref()
|
||||
&& crate::tui::app::ReasoningEffort::parse_strict(reasoning).is_err()
|
||||
&& crate::reasoning_preference::ReasoningEffort::parse_strict(reasoning).is_err()
|
||||
{
|
||||
return Err("The Runtime Chat reasoning effort is invalid.".to_string());
|
||||
}
|
||||
|
||||
@@ -3080,8 +3080,10 @@ pub struct StartTurnRequest {
|
||||
pub environment_id: Option<String>,
|
||||
}
|
||||
|
||||
fn parse_runtime_reasoning_effort(value: &str) -> Result<crate::tui::app::ReasoningEffort> {
|
||||
crate::tui::app::ReasoningEffort::parse_strict(value).map_err(anyhow::Error::msg)
|
||||
fn parse_runtime_reasoning_effort(
|
||||
value: &str,
|
||||
) -> Result<crate::reasoning_preference::ReasoningEffort> {
|
||||
crate::reasoning_preference::ReasoningEffort::parse_strict(value).map_err(anyhow::Error::msg)
|
||||
}
|
||||
|
||||
fn canonical_runtime_reasoning_effort(value: Option<&str>) -> Result<Option<String>> {
|
||||
@@ -3175,7 +3177,7 @@ fn runtime_turn_request_fingerprint(
|
||||
prompt: &str,
|
||||
input_summary: Option<&str>,
|
||||
requested_model: &str,
|
||||
reasoning_effort: Option<crate::tui::app::ReasoningEffort>,
|
||||
reasoning_effort: Option<crate::reasoning_preference::ReasoningEffort>,
|
||||
allowed_tools: Option<&[String]>,
|
||||
policy: RuntimePolicyProjection,
|
||||
allow_shell: bool,
|
||||
@@ -8865,7 +8867,7 @@ impl RuntimeThreadManager {
|
||||
let cfg_snapshot = self.config.read().clone();
|
||||
let configured_reasoning_preference = cfg_snapshot
|
||||
.reasoning_effort()
|
||||
.map(crate::tui::app::ReasoningEffort::from_setting);
|
||||
.map(crate::reasoning_preference::ReasoningEffort::from_setting);
|
||||
// Runtime API precedence is explicit and stable: a turn override wins
|
||||
// over its persisted thread default, which wins over normal config.
|
||||
let reasoning_preference = turn_reasoning_preference
|
||||
@@ -9016,10 +9018,10 @@ impl RuntimeThreadManager {
|
||||
)?;
|
||||
let auto_controls_reasoning = matches!(
|
||||
reasoning_preference,
|
||||
Some(crate::tui::app::ReasoningEffort::Auto)
|
||||
Some(crate::reasoning_preference::ReasoningEffort::Auto)
|
||||
);
|
||||
let selected_reasoning = reasoning_preference.map(|effort| {
|
||||
if effort == crate::tui::app::ReasoningEffort::Auto {
|
||||
if effort == crate::reasoning_preference::ReasoningEffort::Auto {
|
||||
crate::auto_reasoning::select(false, &prompt)
|
||||
} else {
|
||||
effort
|
||||
|
||||
@@ -1823,7 +1823,7 @@ fn runtime_turn_operation_keys_are_bounded_scoped_and_fingerprint_execution_poli
|
||||
"same prompt",
|
||||
None,
|
||||
"deepseek-v4-pro",
|
||||
Some(crate::tui::app::ReasoningEffort::High),
|
||||
Some(crate::reasoning_preference::ReasoningEffort::High),
|
||||
Some(&tools),
|
||||
policy,
|
||||
false,
|
||||
@@ -1843,7 +1843,7 @@ fn runtime_turn_operation_keys_are_bounded_scoped_and_fingerprint_execution_poli
|
||||
"same prompt",
|
||||
None,
|
||||
"deepseek-v4-pro",
|
||||
Some(crate::tui::app::ReasoningEffort::High),
|
||||
Some(crate::reasoning_preference::ReasoningEffort::High),
|
||||
Some(&tools),
|
||||
policy,
|
||||
false,
|
||||
@@ -1858,7 +1858,7 @@ fn runtime_turn_operation_keys_are_bounded_scoped_and_fingerprint_execution_poli
|
||||
"different prompt",
|
||||
None,
|
||||
"deepseek-v4-pro",
|
||||
Some(crate::tui::app::ReasoningEffort::High),
|
||||
Some(crate::reasoning_preference::ReasoningEffort::High),
|
||||
Some(&tools),
|
||||
policy,
|
||||
false,
|
||||
@@ -1873,7 +1873,7 @@ fn runtime_turn_operation_keys_are_bounded_scoped_and_fingerprint_execution_poli
|
||||
"same prompt",
|
||||
None,
|
||||
"different-model",
|
||||
Some(crate::tui::app::ReasoningEffort::High),
|
||||
Some(crate::reasoning_preference::ReasoningEffort::High),
|
||||
Some(&tools),
|
||||
policy,
|
||||
false,
|
||||
@@ -1888,7 +1888,7 @@ fn runtime_turn_operation_keys_are_bounded_scoped_and_fingerprint_execution_poli
|
||||
"same prompt",
|
||||
None,
|
||||
"deepseek-v4-pro",
|
||||
Some(crate::tui::app::ReasoningEffort::Medium),
|
||||
Some(crate::reasoning_preference::ReasoningEffort::Medium),
|
||||
Some(&tools),
|
||||
policy,
|
||||
false,
|
||||
@@ -1903,7 +1903,7 @@ fn runtime_turn_operation_keys_are_bounded_scoped_and_fingerprint_execution_poli
|
||||
"same prompt",
|
||||
None,
|
||||
"deepseek-v4-pro",
|
||||
Some(crate::tui::app::ReasoningEffort::High),
|
||||
Some(crate::reasoning_preference::ReasoningEffort::High),
|
||||
Some(&[]),
|
||||
policy,
|
||||
false,
|
||||
@@ -1918,7 +1918,7 @@ fn runtime_turn_operation_keys_are_bounded_scoped_and_fingerprint_execution_poli
|
||||
"same prompt",
|
||||
None,
|
||||
"deepseek-v4-pro",
|
||||
Some(crate::tui::app::ReasoningEffort::High),
|
||||
Some(crate::reasoning_preference::ReasoningEffort::High),
|
||||
Some(&tools),
|
||||
RuntimePolicyProjection::from_persisted("operate", Some("full_access"), true),
|
||||
true,
|
||||
|
||||
@@ -14,7 +14,7 @@ use serde::{Deserialize, Serialize};
|
||||
use crate::config::{expand_path, normalize_model_name};
|
||||
use crate::localization::normalize_configured_locale;
|
||||
use crate::palette::{normalize_hex_rgb_color, normalize_theme_setting};
|
||||
use crate::tui::app::ReasoningEffort;
|
||||
use crate::reasoning_preference::ReasoningEffort;
|
||||
use codewhale_config::resolve::Layer;
|
||||
|
||||
const SETTINGS_FILE_NAME: &str = "settings.toml";
|
||||
|
||||
@@ -54,6 +54,7 @@ use crate::models::{
|
||||
ContentBlock, Message, MessageRequest, MessageResponse, SystemPrompt, Tool, Usage,
|
||||
is_incomplete_stop_reason, is_output_limit_stop_reason, stop_reason_detail,
|
||||
};
|
||||
use crate::reasoning_preference::ReasoningEffort;
|
||||
use crate::request_tuning::RequestTuning;
|
||||
use crate::tools::canonical_action::{
|
||||
CANONICAL_ACTION_ALIASES, canonical_action_alias, is_action_family,
|
||||
@@ -69,7 +70,6 @@ use crate::tools::spec::{
|
||||
use crate::tools::todo::SharedTodoList;
|
||||
#[cfg(test)]
|
||||
use crate::tools::todo::TodoList;
|
||||
use crate::tui::app::ReasoningEffort;
|
||||
use crate::utils::spawn_supervised;
|
||||
use crate::work_graph::{
|
||||
EvidenceKind, EvidenceRef, OperationIntent, OperationOwnerSnapshot, OwnerState,
|
||||
|
||||
@@ -48,7 +48,7 @@ use crate::dependencies::ExternalTool;
|
||||
use crate::features::Feature;
|
||||
use crate::llm_client::LlmClient;
|
||||
use crate::models::{ContentBlock, Message, MessageRequest, SystemPrompt, Usage};
|
||||
use crate::tui::app::ReasoningEffort;
|
||||
use crate::reasoning_preference::ReasoningEffort;
|
||||
use crate::utils::truncate_with_ellipsis;
|
||||
|
||||
use super::spec::{
|
||||
|
||||
@@ -26,6 +26,7 @@ use crate::localization::{Locale, MessageId, resolve_locale, tr};
|
||||
use crate::models::{Message, SystemPrompt, Tool, Usage};
|
||||
use crate::palette::{self, UiTheme};
|
||||
use crate::pricing::{CostCurrency, CostEstimate};
|
||||
use crate::reasoning_preference::{EffectiveReasoningEffort, ReasoningEffort};
|
||||
use crate::session_manager::{SessionContextReference, SessionMetadata, SessionWorkState};
|
||||
use crate::settings::{InlineDiffMode, Settings};
|
||||
use crate::tools::plan::{PlanState, SharedPlanState, new_shared_plan_state};
|
||||
@@ -69,13 +70,12 @@ pub(crate) enum RedactionGateNotice {
|
||||
}
|
||||
pub use types::{
|
||||
AppAction, AppMode, AppModeUi, AutomationAction, ComposerDensity, ComposerSubmitAction,
|
||||
ComposerSubmitChord, InitialInput, McpUiAction, QueuedMessage, ReasoningEffort, ScreenMode,
|
||||
SettingSelection, ShellJobAction, SubmitDisposition, TaskPanelEntry, TaskPanelEntryKind,
|
||||
ToolCollapseMode, ToolDetailRecord, TranscriptSpacing, TuiOptions, VimMode,
|
||||
ComposerSubmitChord, InitialInput, McpUiAction, QueuedMessage, ScreenMode, SettingSelection,
|
||||
ShellJobAction, SubmitDisposition, TaskPanelEntry, TaskPanelEntryKind, ToolCollapseMode,
|
||||
ToolDetailRecord, TranscriptSpacing, TuiOptions, VimMode,
|
||||
};
|
||||
pub(crate) use types::{
|
||||
CacheReplayTarget, EffectiveReasoningEffort, GoalControlIntent, PendingGoalControl,
|
||||
WORKFLOW_DRAFT_INSTRUCTION_PREFIX,
|
||||
CacheReplayTarget, GoalControlIntent, PendingGoalControl, WORKFLOW_DRAFT_INSTRUCTION_PREFIX,
|
||||
};
|
||||
|
||||
// === Types ===
|
||||
|
||||
@@ -83,45 +83,6 @@ impl AppModeUi for AppMode {
|
||||
}
|
||||
}
|
||||
|
||||
/// Reasoning-effort tier, mirrored across DeepSeek and Codex effort pickers.
|
||||
///
|
||||
/// The config file accepts every supported string value for forward-compat with
|
||||
/// providers that expose the full spectrum; DeepSeek currently collapses
|
||||
/// `Low`/`Medium` → `high`. OpenAI Codex normalizes inherited DeepSeek-only
|
||||
/// `Off` to `Low` and keeps `XHigh`, `Max`, and `Ultra` distinct at the
|
||||
/// provider boundary. The default keyboard cycler walks the three DeepSeek-distinct
|
||||
/// tiers: `Off` → `High` → `Max` → `Off`; provider-aware callers should use
|
||||
/// [`ReasoningEffort::cycle_next_in`] with the route's effort list. Auto
|
||||
/// routing has no concrete provider yet, so
|
||||
/// [`ReasoningEffort::cycle_next_for_auto_model`] retains the full
|
||||
/// provider-neutral preference vocabulary until dispatch.
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ReasoningEffort {
|
||||
Off,
|
||||
Minimal,
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
XHigh,
|
||||
Ultra,
|
||||
Auto,
|
||||
#[default]
|
||||
Max,
|
||||
}
|
||||
|
||||
/// Provider-effective reasoning state used by durable receipts and visible
|
||||
/// requested-to-effective labels.
|
||||
///
|
||||
/// Some routes, notably first-party GLM-5-Turbo, support a thinking toggle but
|
||||
/// publish no effort tiers. Keeping that state distinct prevents a requested
|
||||
/// `max` from being displayed or persisted as an effective `max` claim.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum EffectiveReasoningEffort {
|
||||
Tier(ReasoningEffort),
|
||||
ThinkingEnabledGranularityUnavailable,
|
||||
Unavailable,
|
||||
}
|
||||
|
||||
/// Exact provider/model route whose prompt can be inspected or replayed.
|
||||
///
|
||||
/// Auto-model sessions keep `model == "auto"` as the user's selection, so
|
||||
@@ -139,439 +100,6 @@ pub(crate) struct CacheReplayTarget {
|
||||
pub(crate) base_url: Option<String>,
|
||||
}
|
||||
|
||||
impl EffectiveReasoningEffort {
|
||||
/// Reconstruct a safe request tier for cache replay and inspection.
|
||||
///
|
||||
/// Routes with an enabled-but-untiered receipt collapse every non-Off
|
||||
/// request to the same wire toggle, so High is the canonical value that
|
||||
/// keeps reasoning enabled without claiming a granular effective tier.
|
||||
#[must_use]
|
||||
pub(crate) const fn request_tier_for_replay(self) -> Option<ReasoningEffort> {
|
||||
match self {
|
||||
Self::Tier(tier) => Some(tier),
|
||||
Self::ThinkingEnabledGranularityUnavailable => Some(ReasoningEffort::High),
|
||||
Self::Unavailable => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EffectiveReasoningEffort> for crate::work_graph::ReasoningEffortTier {
|
||||
fn from(value: EffectiveReasoningEffort) -> Self {
|
||||
match value {
|
||||
EffectiveReasoningEffort::Tier(tier) => tier.into(),
|
||||
EffectiveReasoningEffort::ThinkingEnabledGranularityUnavailable => {
|
||||
Self::ThinkingEnabledGranularityUnavailable
|
||||
}
|
||||
EffectiveReasoningEffort::Unavailable => Self::Unavailable,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::work_graph::ReasoningEffortTier> for EffectiveReasoningEffort {
|
||||
fn from(value: crate::work_graph::ReasoningEffortTier) -> Self {
|
||||
use crate::work_graph::ReasoningEffortTier as Tier;
|
||||
match value {
|
||||
Tier::Off => Self::Tier(ReasoningEffort::Off),
|
||||
Tier::Minimal => Self::Tier(ReasoningEffort::Minimal),
|
||||
Tier::Low => Self::Tier(ReasoningEffort::Low),
|
||||
Tier::Medium => Self::Tier(ReasoningEffort::Medium),
|
||||
Tier::High => Self::Tier(ReasoningEffort::High),
|
||||
Tier::XHigh => Self::Tier(ReasoningEffort::XHigh),
|
||||
Tier::Ultra => Self::Tier(ReasoningEffort::Ultra),
|
||||
Tier::Auto => Self::Tier(ReasoningEffort::Auto),
|
||||
Tier::Max => Self::Tier(ReasoningEffort::Max),
|
||||
Tier::ThinkingEnabledGranularityUnavailable => {
|
||||
Self::ThinkingEnabledGranularityUnavailable
|
||||
}
|
||||
Tier::Unavailable => Self::Unavailable,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ReasoningEffort> for crate::work_graph::ReasoningEffortTier {
|
||||
fn from(value: ReasoningEffort) -> Self {
|
||||
match value {
|
||||
ReasoningEffort::Off => Self::Off,
|
||||
ReasoningEffort::Minimal => Self::Minimal,
|
||||
ReasoningEffort::Low => Self::Low,
|
||||
ReasoningEffort::Medium => Self::Medium,
|
||||
ReasoningEffort::High => Self::High,
|
||||
ReasoningEffort::XHigh => Self::XHigh,
|
||||
ReasoningEffort::Ultra => Self::Ultra,
|
||||
ReasoningEffort::Auto => Self::Auto,
|
||||
ReasoningEffort::Max => Self::Max,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ReasoningEffort {
|
||||
/// Parse an operator-supplied effort value.
|
||||
///
|
||||
/// This is deliberately the one canonical spelling table for every
|
||||
/// human-facing route. Callers that read an old persisted config may use
|
||||
/// [`Self::from_setting`] for its compatibility fallback, but a new CLI,
|
||||
/// settings, or tool input must reject an unknown value instead of quietly
|
||||
/// turning it into `max`.
|
||||
pub fn parse_strict(value: &str) -> Result<Self, String> {
|
||||
let trimmed = value.trim();
|
||||
match trimmed.to_ascii_lowercase().as_str() {
|
||||
"off" | "disabled" | "none" | "false" => Ok(Self::Off),
|
||||
"low" | "minimum" | "minimal" | "light" => Ok(Self::Low),
|
||||
"medium" | "mid" => Ok(Self::Medium),
|
||||
"high" => Ok(Self::High),
|
||||
"xhigh" => Ok(Self::XHigh),
|
||||
"auto" | "automatic" => Ok(Self::Auto),
|
||||
"ultra" | "ultracode" => Ok(Self::Ultra),
|
||||
"max" | "maximum" => Ok(Self::Max),
|
||||
_ => Err(format!(
|
||||
"Unrecognized reasoning effort {trimmed:?}. Expected: auto, off, low, medium, high, xhigh, max, or ultra."
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a persisted config-file string into an effort tier. Unknown
|
||||
/// legacy values fall back to the default (`Max`) so an old malformed
|
||||
/// settings file never prevents startup. New user input should use
|
||||
/// [`Self::parse_strict`] instead.
|
||||
#[must_use]
|
||||
pub fn from_setting(value: &str) -> Self {
|
||||
Self::parse_strict(value).unwrap_or_default()
|
||||
}
|
||||
|
||||
#[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 {
|
||||
match self {
|
||||
Self::Off => "off",
|
||||
Self::Minimal => "minimal",
|
||||
Self::Low => "low",
|
||||
Self::Medium => "medium",
|
||||
Self::High => "high",
|
||||
Self::XHigh => "xhigh",
|
||||
Self::Ultra => "ultra",
|
||||
Self::Auto => "auto",
|
||||
Self::Max => "max",
|
||||
}
|
||||
}
|
||||
|
||||
/// Short label for the header chip.
|
||||
#[must_use]
|
||||
pub fn short_label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Off => "off",
|
||||
Self::Minimal => "minimal",
|
||||
Self::Low => "low",
|
||||
Self::Medium => "med",
|
||||
Self::High => "high",
|
||||
Self::XHigh => "xhigh",
|
||||
Self::Ultra => "ultra",
|
||||
Self::Auto => "auto",
|
||||
Self::Max => "max",
|
||||
}
|
||||
}
|
||||
|
||||
/// Provider-facing label for user-visible surfaces.
|
||||
#[must_use]
|
||||
pub fn display_label_for_provider(self, provider: ApiProvider) -> &'static str {
|
||||
match (provider, self.normalize_for_provider(provider)) {
|
||||
(ApiProvider::OpenaiCodex, Self::Minimal) => "low",
|
||||
(ApiProvider::OpenaiCodex, Self::Low) => "low",
|
||||
(ApiProvider::OpenaiCodex, Self::Medium) => "medium",
|
||||
(ApiProvider::OpenaiCodex, Self::High) => "high",
|
||||
// `xhigh`, `max` and `ultra` are three distinct rungs the Codex
|
||||
// roster publishes per model; collapsing them onto "xhigh" dates
|
||||
// from when xhigh was the ceiling and made the top tiers
|
||||
// unreachable and indistinguishable.
|
||||
(ApiProvider::OpenaiCodex, Self::XHigh) => "xhigh",
|
||||
(ApiProvider::OpenaiCodex, Self::Ultra) => "ultra",
|
||||
(ApiProvider::OpenaiCodex, Self::Max) => "max",
|
||||
(ApiProvider::Xai, Self::XHigh) => "xhigh",
|
||||
(_, effort) => effort.short_label(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Value forwarded to the engine/client. `None` means "provider default"
|
||||
/// (for `Off` we still emit `"off"` so the client can inject
|
||||
/// `thinking = {"type": "disabled"}`).
|
||||
#[must_use]
|
||||
pub fn api_value(self) -> Option<&'static str> {
|
||||
Some(self.as_setting())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn normalize_for_provider(self, provider: ApiProvider) -> Self {
|
||||
if provider != ApiProvider::OpenaiCodex {
|
||||
return self;
|
||||
}
|
||||
match self {
|
||||
Self::Off => Self::Low,
|
||||
Self::Auto => Self::Medium,
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve an effort against the exact provider route that will receive
|
||||
/// the request. Both K3 routes are always-thinking, so `off` becomes the
|
||||
/// lowest supported tier. The Kimi Code membership route otherwise keeps
|
||||
/// its low/high/max mapping; direct Moonshot K3 additionally maps `medium`
|
||||
/// to `high`. First-party DeepSeek routes keep `low` (the wire documents
|
||||
/// low/high/max) while rounding `medium` up to `high`. Models that publish
|
||||
/// a Models.dev `reasoning_options` effort list keep that vocabulary
|
||||
/// instead of the historic Low/Medium collapse. Generic Moonshot and
|
||||
/// every other non-Codex route retain the historic high coercion.
|
||||
/// This intentionally does not change [`Self::normalize_for_provider`],
|
||||
/// whose generic wire semantics are used by older callers that do not yet
|
||||
/// have a route receipt.
|
||||
#[must_use]
|
||||
pub fn normalize_for_route(
|
||||
self,
|
||||
provider: ApiProvider,
|
||||
base_url: &str,
|
||||
wire_model: &str,
|
||||
) -> Self {
|
||||
let normalized = self.normalize_for_provider(provider);
|
||||
if crate::config::is_exact_kimi_code_k3_route(provider, base_url, wire_model) {
|
||||
return match normalized {
|
||||
Self::Off => Self::Low,
|
||||
other => other,
|
||||
};
|
||||
}
|
||||
if crate::config::is_exact_direct_moonshot_k3_route(provider, base_url, wire_model) {
|
||||
return match normalized {
|
||||
Self::Off => Self::Low,
|
||||
Self::Medium => Self::High,
|
||||
other => other,
|
||||
};
|
||||
}
|
||||
if provider == ApiProvider::OpenaiCodex {
|
||||
return normalized;
|
||||
}
|
||||
// First-party DeepSeek routes document `reasoning_effort` low/high/max
|
||||
// on the wire (no medium), so `low` is a real, cheaper tier there and
|
||||
// must reach the wire as low; `medium` rounds up to high because the
|
||||
// dialect has no such value (#52).
|
||||
if matches!(provider, ApiProvider::Deepseek | ApiProvider::DeepseekCN) {
|
||||
return match normalized {
|
||||
Self::Low => Self::Low,
|
||||
Self::Medium => Self::High,
|
||||
other => other,
|
||||
};
|
||||
}
|
||||
// Ollama's current OpenAI-compatible Chat Completions contract
|
||||
// documents the complete none/low/medium/high/max ladder. Keep every
|
||||
// real tier distinct for normal turns; only Codewhale-only synonyms
|
||||
// are folded onto the nearest documented spelling.
|
||||
if provider == ApiProvider::OllamaCloud {
|
||||
return match normalized {
|
||||
Self::Minimal => Self::Low,
|
||||
Self::XHigh | Self::Ultra => Self::Max,
|
||||
other => other,
|
||||
};
|
||||
}
|
||||
if let Some(values) = Self::catalog_effort_values(provider, wire_model) {
|
||||
return Self::clamp_to_catalog_efforts(normalized, provider, wire_model, &values);
|
||||
}
|
||||
match normalized {
|
||||
Self::Low | Self::Medium => Self::High,
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn catalog_default(provider: ApiProvider, wire_model: &str) -> Option<Self> {
|
||||
let offering = crate::provider_lake::catalog_offering_for_model(provider, wire_model)?;
|
||||
offering.reasoning_options.iter().find_map(|option| {
|
||||
option
|
||||
.get("type")
|
||||
.and_then(|value| value.as_str())
|
||||
.filter(|kind| kind.eq_ignore_ascii_case("effort"))?;
|
||||
option
|
||||
.get("default")
|
||||
.and_then(|value| value.as_str())
|
||||
.and_then(Self::from_catalog_token)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn catalog_effort_values(
|
||||
provider: ApiProvider,
|
||||
wire_model: &str,
|
||||
) -> Option<Vec<Self>> {
|
||||
let offering = crate::provider_lake::catalog_offering_for_model(provider, wire_model)?;
|
||||
let mut efforts = Vec::new();
|
||||
for option in &offering.reasoning_options {
|
||||
if !option
|
||||
.get("type")
|
||||
.and_then(|value| value.as_str())
|
||||
.is_some_and(|kind| kind.eq_ignore_ascii_case("effort"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let Some(values) = option.get("values").and_then(|value| value.as_array()) else {
|
||||
continue;
|
||||
};
|
||||
for value in values {
|
||||
if let Some(effort) = value.as_str().and_then(Self::from_catalog_token)
|
||||
&& !efforts.contains(&effort)
|
||||
{
|
||||
efforts.push(effort);
|
||||
}
|
||||
}
|
||||
}
|
||||
(!efforts.is_empty()).then_some(efforts)
|
||||
}
|
||||
|
||||
pub(crate) fn from_catalog_token(raw: &str) -> Option<Self> {
|
||||
match raw.trim().to_ascii_lowercase().as_str() {
|
||||
"off" | "disabled" | "none" | "false" => Some(Self::Off),
|
||||
"minimal" | "minimum" => Some(Self::Minimal),
|
||||
"low" | "light" => Some(Self::Low),
|
||||
"medium" | "mid" => Some(Self::Medium),
|
||||
"high" => Some(Self::High),
|
||||
"xhigh" => Some(Self::XHigh),
|
||||
"ultra" | "ultracode" => Some(Self::Ultra),
|
||||
"max" | "maximum" => Some(Self::Max),
|
||||
"auto" | "automatic" | "adaptive" => Some(Self::Auto),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn clamp_to_catalog_efforts(
|
||||
normalized: Self,
|
||||
provider: ApiProvider,
|
||||
wire_model: &str,
|
||||
values: &[Self],
|
||||
) -> Self {
|
||||
if matches!(normalized, Self::Auto) || values.contains(&normalized) {
|
||||
return normalized;
|
||||
}
|
||||
let aliased = match normalized {
|
||||
Self::Minimal if values.contains(&Self::Low) => Self::Low,
|
||||
Self::Max | Self::Ultra if values.contains(&Self::XHigh) => Self::XHigh,
|
||||
Self::Off => Self::catalog_default(provider, wire_model).unwrap_or(Self::High),
|
||||
other => other,
|
||||
};
|
||||
if values.contains(&aliased) {
|
||||
aliased
|
||||
} else {
|
||||
Self::catalog_default(provider, wire_model).unwrap_or(Self::High)
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn api_value_for_provider(self, provider: ApiProvider) -> Option<&'static str> {
|
||||
if provider != ApiProvider::OpenaiCodex {
|
||||
return self.api_value();
|
||||
}
|
||||
Some(match self.normalize_for_provider(provider) {
|
||||
Self::Minimal => "low",
|
||||
Self::Low => "low",
|
||||
Self::Medium => "medium",
|
||||
Self::High => "high",
|
||||
Self::XHigh => "xhigh",
|
||||
Self::Ultra => "ultra",
|
||||
Self::Max => "max",
|
||||
Self::Off => "low",
|
||||
Self::Auto => "medium",
|
||||
})
|
||||
}
|
||||
|
||||
/// Provider-facing value after exact-route normalization.
|
||||
#[must_use]
|
||||
pub fn api_value_for_route(
|
||||
self,
|
||||
provider: ApiProvider,
|
||||
base_url: &str,
|
||||
wire_model: &str,
|
||||
) -> Option<&'static str> {
|
||||
self.normalize_for_route(provider, base_url, wire_model)
|
||||
.api_value_for_provider(provider)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn as_setting_for_provider(self, provider: ApiProvider) -> &'static str {
|
||||
self.api_value_for_provider(provider)
|
||||
.unwrap_or_else(|| self.as_setting())
|
||||
}
|
||||
|
||||
/// Persist the canonical setting after exact-route normalization.
|
||||
#[must_use]
|
||||
pub fn as_setting_for_route(
|
||||
self,
|
||||
provider: ApiProvider,
|
||||
base_url: &str,
|
||||
wire_model: &str,
|
||||
) -> &'static str {
|
||||
self.normalize_for_route(provider, base_url, wire_model)
|
||||
.as_setting_for_provider(provider)
|
||||
}
|
||||
|
||||
/// Cycle through the three behaviorally distinct tiers.
|
||||
#[must_use]
|
||||
pub fn cycle_next(self) -> Self {
|
||||
match self {
|
||||
Self::Off => Self::High,
|
||||
Self::Auto => Self::Off,
|
||||
Self::Minimal | Self::Low | Self::Medium | Self::High | Self::XHigh | Self::Ultra => {
|
||||
Self::Max
|
||||
}
|
||||
Self::Max => Self::Off,
|
||||
}
|
||||
}
|
||||
|
||||
/// Advance through an exact-route effort list. Unknown current values
|
||||
/// enter at the first listed tier so a persisted `max` on an `xhigh`
|
||||
/// ladder, or `off` on an always-thinking model, still moves.
|
||||
#[must_use]
|
||||
pub fn cycle_next_in(self, efforts: &[Self]) -> Self {
|
||||
if efforts.is_empty() {
|
||||
return self.cycle_next();
|
||||
}
|
||||
if let Some(index) = self.index_in(efforts) {
|
||||
return efforts[(index + 1) % efforts.len()];
|
||||
}
|
||||
efforts[0]
|
||||
}
|
||||
|
||||
fn index_in(self, efforts: &[Self]) -> Option<usize> {
|
||||
efforts
|
||||
.iter()
|
||||
.position(|&effort| effort == self)
|
||||
.or_else(|| {
|
||||
let aliases: &[Self] = match self {
|
||||
Self::Max | Self::Ultra => &[Self::XHigh],
|
||||
Self::XHigh => &[Self::Max],
|
||||
Self::Minimal => &[Self::Low],
|
||||
Self::Low => &[Self::Minimal],
|
||||
_ => return None,
|
||||
};
|
||||
aliases
|
||||
.iter()
|
||||
.find_map(|alias| efforts.iter().position(|&effort| effort == *alias))
|
||||
})
|
||||
}
|
||||
|
||||
/// Cycle the unresolved auto-model preference without applying any
|
||||
/// provider's normalization rules prematurely.
|
||||
#[must_use]
|
||||
pub fn cycle_next_for_auto_model(self) -> Self {
|
||||
match self {
|
||||
Self::Auto => Self::Off,
|
||||
Self::Off => Self::Minimal,
|
||||
Self::Minimal => Self::Low,
|
||||
Self::Low => Self::Medium,
|
||||
Self::Medium => Self::High,
|
||||
Self::High => Self::XHigh,
|
||||
Self::XHigh => Self::Ultra,
|
||||
Self::Ultra => Self::Max,
|
||||
Self::Max => Self::Auto,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ComposerDensity {
|
||||
Compact,
|
||||
|
||||
@@ -975,7 +975,7 @@ impl HotbarAction for AppHotbarAction {
|
||||
AppHotbarKind::SessionCompact => app.is_compacting || app.manual_compaction_queued,
|
||||
AppHotbarKind::Mode(mode) => app.mode == mode,
|
||||
AppHotbarKind::ReasoningCycle => {
|
||||
app.reasoning_effort != crate::tui::app::ReasoningEffort::Off
|
||||
app.reasoning_effort != crate::reasoning_preference::ReasoningEffort::Off
|
||||
}
|
||||
AppHotbarKind::SidebarToggle => {
|
||||
app.work_surface.placement != crate::tui::work_surface::WorkSurfacePlacement::Off
|
||||
@@ -1439,7 +1439,8 @@ mod tests {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::config::{ApiProvider, Config};
|
||||
use crate::tui::app::{ReasoningEffort, TuiOptions};
|
||||
use crate::reasoning_preference::ReasoningEffort;
|
||||
use crate::tui::app::TuiOptions;
|
||||
use crate::tui::views::ModalKind;
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -39,8 +39,9 @@ use crate::palette;
|
||||
use crate::provider_lake::{
|
||||
catalog_offering_for_model, catalog_offering_for_model_identity, configured_providers,
|
||||
};
|
||||
use crate::reasoning_preference::ReasoningEffort;
|
||||
use crate::settings::PinnedModel;
|
||||
use crate::tui::app::{App, ReasoningEffort};
|
||||
use crate::tui::app::App;
|
||||
use crate::tui::menu_style;
|
||||
use crate::tui::views::{
|
||||
ActionHint, ListDetailLayout, ModalKind, ModalView, ViewAction, ViewEvent, render_modal_footer,
|
||||
|
||||
@@ -48,7 +48,7 @@ use crate::provider_readiness::{
|
||||
CredentialState, ProviderReadinessSnapshot, ProviderRouteIdentity, ResolvedProviderReadiness,
|
||||
credential_state_for_provider, route_identity_for_model,
|
||||
};
|
||||
use crate::tui::app::ReasoningEffort;
|
||||
use crate::reasoning_preference::ReasoningEffort;
|
||||
use crate::tui::menu_style;
|
||||
use crate::tui::views::{
|
||||
ActionHint, EmptyState, ListDetailLayout, ModalKind, ModalView, ViewAction, ViewEvent,
|
||||
|
||||
@@ -70,15 +70,15 @@ pub fn visible_slash_menu_entries(app: &App, limit: usize) -> Vec<SlashMenuEntry
|
||||
effort_entries.push(SlashMenuEntry {
|
||||
name: format!("/effort {}", label),
|
||||
description: match eff {
|
||||
crate::tui::app::ReasoningEffort::Auto => "choose per turn".into(),
|
||||
crate::tui::app::ReasoningEffort::Off => "no extra reasoning".into(),
|
||||
crate::tui::app::ReasoningEffort::Minimal => "minimal reasoning".into(),
|
||||
crate::tui::app::ReasoningEffort::Low => "lighter reasoning".into(),
|
||||
crate::tui::app::ReasoningEffort::Medium => "balanced reasoning".into(),
|
||||
crate::tui::app::ReasoningEffort::High => "deeper reasoning".into(),
|
||||
crate::tui::app::ReasoningEffort::XHigh => "extra-high reasoning".into(),
|
||||
crate::tui::app::ReasoningEffort::Ultra => "ultra reasoning".into(),
|
||||
crate::tui::app::ReasoningEffort::Max => "maximum reasoning".into(),
|
||||
crate::reasoning_preference::ReasoningEffort::Auto => "choose per turn".into(),
|
||||
crate::reasoning_preference::ReasoningEffort::Off => "no extra reasoning".into(),
|
||||
crate::reasoning_preference::ReasoningEffort::Minimal => "minimal reasoning".into(),
|
||||
crate::reasoning_preference::ReasoningEffort::Low => "lighter reasoning".into(),
|
||||
crate::reasoning_preference::ReasoningEffort::Medium => "balanced reasoning".into(),
|
||||
crate::reasoning_preference::ReasoningEffort::High => "deeper reasoning".into(),
|
||||
crate::reasoning_preference::ReasoningEffort::XHigh => "extra-high reasoning".into(),
|
||||
crate::reasoning_preference::ReasoningEffort::Ultra => "ultra reasoning".into(),
|
||||
crate::reasoning_preference::ReasoningEffort::Max => "maximum reasoning".into(),
|
||||
},
|
||||
is_skill: false,
|
||||
alias_hint: None,
|
||||
|
||||
@@ -131,15 +131,17 @@ use crate::tui::views::subagent_view_agents;
|
||||
use crate::tui::vim_mode;
|
||||
use crate::tui::workspace_context;
|
||||
|
||||
use crate::reasoning_preference::{EffectiveReasoningEffort, ReasoningEffort};
|
||||
|
||||
use super::key_actions;
|
||||
|
||||
use super::app::{
|
||||
ActiveCompaction, ActiveTurnMetadata, AgentCurrentActivity, App, AppAction, AppMode,
|
||||
ComposerSubmitAction, ComposerSubmitChord, EffectiveReasoningEffort, GoalControlIntent,
|
||||
OnboardingState, PendingGoalControl, PendingProviderSwitch, QueuedMessage, ReasoningEffort,
|
||||
RedactionGateNotice, ScreenMode, StatusToast, StatusToastLevel, SubmitDisposition,
|
||||
TaskPanelEntry, TaskPanelEntryKind, ToolEvidence, TuiOptions, bound_agent_activity_text,
|
||||
is_stop_word, looks_like_slash_command_input, shell_command_from_bang_input,
|
||||
ComposerSubmitAction, ComposerSubmitChord, GoalControlIntent, OnboardingState,
|
||||
PendingGoalControl, PendingProviderSwitch, QueuedMessage, RedactionGateNotice, ScreenMode,
|
||||
StatusToast, StatusToastLevel, SubmitDisposition, TaskPanelEntry, TaskPanelEntryKind,
|
||||
ToolEvidence, TuiOptions, bound_agent_activity_text, is_stop_word,
|
||||
looks_like_slash_command_input, shell_command_from_bang_input,
|
||||
};
|
||||
use super::approval::{
|
||||
ApprovalMode, ApprovalRequest, ApprovalView, ElevationRequest, ElevationView, ReviewDecision,
|
||||
|
||||
@@ -874,9 +874,9 @@ pub(crate) async fn apply_model_picker_choice(
|
||||
model: String,
|
||||
target_provider: Option<ApiProvider>,
|
||||
target_provider_id: Option<String>,
|
||||
effort: crate::tui::app::ReasoningEffort,
|
||||
effort: crate::reasoning_preference::ReasoningEffort,
|
||||
previous_model: String,
|
||||
previous_effort: crate::tui::app::ReasoningEffort,
|
||||
previous_effort: crate::reasoning_preference::ReasoningEffort,
|
||||
save_as_startup_default: bool,
|
||||
) {
|
||||
if app.reject_setting_change_while_busy(
|
||||
|
||||
@@ -2460,7 +2460,7 @@ mod tests {
|
||||
app.auto_model = false;
|
||||
app.active_route_base_url = "https://gateway.example/v1".to_string();
|
||||
app.model = "vendor-model-x".to_string();
|
||||
app.reasoning_effort = crate::tui::app::ReasoningEffort::High;
|
||||
app.reasoning_effort = crate::reasoning_preference::ReasoningEffort::High;
|
||||
assert_eq!(
|
||||
app.reasoning_effort_display_label(),
|
||||
"high→effective unavailable",
|
||||
|
||||
@@ -653,7 +653,7 @@ fn statusline_full_frame_custom_cost_preserves_evidence_and_width_shedding() {
|
||||
app.set_provider_identity(ApiProvider::Custom, "my-gateway");
|
||||
app.active_route_base_url = "https://gateway.example/v1".to_string();
|
||||
app.model = "vendor-model-x".to_string();
|
||||
app.reasoning_effort = crate::tui::app::ReasoningEffort::High;
|
||||
app.reasoning_effort = crate::reasoning_preference::ReasoningEffort::High;
|
||||
app.billing_presentation = BillingPresentation::Unknown;
|
||||
app.session.cost_coverage_unknown_legacy = true;
|
||||
app.status_items = vec![StatusItem::ContextPercent, StatusItem::Cost];
|
||||
|
||||
@@ -18,8 +18,9 @@ use crate::config::{
|
||||
ProviderConfig, ProvidersConfig,
|
||||
};
|
||||
use crate::core::engine::mock_engine_handle;
|
||||
use crate::reasoning_preference::ReasoningEffort;
|
||||
use crate::tui::active_cell::ActiveCell;
|
||||
use crate::tui::app::{ReasoningEffort, ToolDetailRecord};
|
||||
use crate::tui::app::ToolDetailRecord;
|
||||
use crate::tui::file_mention::{
|
||||
apply_mention_menu_selection, find_file_mention_completions, partial_file_mention_at_cursor,
|
||||
try_autocomplete_file_mention, user_request_with_file_mentions, visible_mention_menu_entries,
|
||||
|
||||
@@ -748,9 +748,9 @@ pub enum ViewEvent {
|
||||
/// Exact named custom route key when the selected provider enum is
|
||||
/// `Custom`; built-in routes leave this unset.
|
||||
provider_id: Option<String>,
|
||||
effort: crate::tui::app::ReasoningEffort,
|
||||
effort: crate::reasoning_preference::ReasoningEffort,
|
||||
previous_model: String,
|
||||
previous_effort: crate::tui::app::ReasoningEffort,
|
||||
previous_effort: crate::reasoning_preference::ReasoningEffort,
|
||||
save_as_startup_default: bool,
|
||||
},
|
||||
/// Emitted by the `/model` picker on Esc so the next open can restore
|
||||
@@ -2082,7 +2082,7 @@ impl ConfigView {
|
||||
value: settings.reasoning_effort.as_deref().map_or_else(
|
||||
|| tr(app.ui_locale, MessageId::ConfigDefaultReasoning).to_string(),
|
||||
|value| {
|
||||
crate::tui::app::ReasoningEffort::from_setting_for_provider(
|
||||
crate::reasoning_preference::ReasoningEffort::from_setting_for_provider(
|
||||
value,
|
||||
app.api_provider,
|
||||
)
|
||||
|
||||
@@ -19,10 +19,11 @@
|
||||
|
||||
use crate::compaction::CompactionConfig;
|
||||
use crate::config::{ApiProvider, Config, ProviderIdentity};
|
||||
use crate::reasoning_preference::ReasoningEffort;
|
||||
use crate::route_runtime::{
|
||||
ResolvedRuntimeRoute, resolve_runtime_route, resolve_runtime_route_for_identity,
|
||||
};
|
||||
use crate::tui::app::{AppMode, ReasoningEffort};
|
||||
use crate::tui::app::AppMode;
|
||||
|
||||
/// Everything the shared turn-route planner needs.
|
||||
///
|
||||
|
||||
Reference in new issue
Block a user