Skip to main content

vtcode_commons/
reasoning.rs

1//! Reasoning effort level definitions shared across VT Code crates.
2//!
3//! This module provides the [`ReasoningEffortLevel`] enum and associated
4//! constants used for configuring model reasoning depth. These types live
5//! in `vtcode-commons` so that both `vtcode-config` and `vtcode-llm`
6//! can reference them without circular dependencies.
7
8use serde::{Deserialize, Deserializer, Serialize};
9use std::fmt;
10
11/// Reasoning effort level string constants.
12pub mod constants {
13    pub const NONE: &str = "none";
14    pub const MINIMAL: &str = "minimal";
15    pub const LOW: &str = "low";
16    pub const MEDIUM: &str = "medium";
17    pub const HIGH: &str = "high";
18    pub const XHIGH: &str = "xhigh";
19    pub const MAX: &str = "max";
20    /// Every value [`super::ReasoningEffortLevel::parse`] accepts, in
21    /// ascending order. This is the single source list: tool schemas that
22    /// accept a reasoning effort override use it verbatim.
23    pub const PARSEABLE_LEVELS: &[&str] = &[NONE, MINIMAL, LOW, MEDIUM, HIGH, XHIGH, MAX];
24    /// Effort levels offered for configuration and selection: every
25    /// [`PARSEABLE_LEVELS`] entry except the leading `none`, which means
26    /// "send no reasoning configuration" rather than an effort level.
27    pub const ALLOWED_LEVELS: &[&str] = match PARSEABLE_LEVELS.split_first() {
28        Some((_none, levels)) => levels,
29        None => &[],
30    };
31    pub const LABEL_LOW: &str = "Low";
32    pub const LABEL_MEDIUM: &str = "Medium";
33    pub const LABEL_HIGH: &str = "High";
34    pub const DESCRIPTION_LOW: &str = "Fast responses with lightweight reasoning.";
35    pub const DESCRIPTION_MEDIUM: &str = "Balanced depth and speed. (Note: Mapped to high on some models)";
36    pub const DESCRIPTION_HIGH: &str = "Deep reasoning for complex problems.";
37}
38
39/// Supported reasoning effort levels configured via vtcode.toml
40/// These map to different provider-specific parameters:
41/// - For Gemini 3 Pro: Maps to thinking_level (low, high) - medium coming soon
42/// - For other models: Maps to provider-specific reasoning parameters
43#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
45#[serde(rename_all = "lowercase")]
46#[derive(Default)]
47pub enum ReasoningEffortLevel {
48    /// No reasoning configuration - for models that don't support configurable reasoning
49    None,
50    /// Minimal reasoning effort - maps to low thinking level for Gemini 3 Pro
51    Minimal,
52    /// Low reasoning effort - maps to low thinking level for Gemini 3 Pro
53    Low,
54    /// Medium reasoning effort - Note: Not fully available for Gemini 3 Pro yet, defaults to high
55    #[default]
56    Medium,
57    /// High reasoning effort - maps to high thinking level for Gemini 3 Pro
58    High,
59    /// Extra high reasoning effort - for GPT-5.6/6-Astra, Claude adaptive,
60    /// Grok-4.6+, and Muse Spark long-running tasks
61    XHigh,
62    /// Maximum reasoning effort - for GPT-5.6/6-Astra, Claude adaptive,
63    /// DeepSeek-V4, Kimi-K3, and GLM-5.x; aliased elsewhere
64    Max,
65    /// Forward-compatible catch-all for unrecognized effort level values
66    Unknown,
67}
68
69impl ReasoningEffortLevel {
70    /// Return the textual representation expected by downstream APIs
71    pub fn as_str(self) -> &'static str {
72        match self {
73            Self::None => constants::NONE,
74            Self::Minimal => constants::MINIMAL,
75            Self::Low => constants::LOW,
76            Self::Medium => constants::MEDIUM,
77            Self::High => constants::HIGH,
78            Self::XHigh => constants::XHIGH,
79            Self::Max => constants::MAX,
80            Self::Unknown => "unknown",
81        }
82    }
83
84    /// Attempt to parse an effort level from user configuration input
85    pub fn parse(value: &str) -> Option<Self> {
86        let normalized = value.trim();
87        if normalized.eq_ignore_ascii_case(constants::NONE) {
88            Some(Self::None)
89        } else if normalized.eq_ignore_ascii_case(constants::MINIMAL) {
90            Some(Self::Minimal)
91        } else if normalized.eq_ignore_ascii_case(constants::LOW) {
92            Some(Self::Low)
93        } else if normalized.eq_ignore_ascii_case(constants::MEDIUM) {
94            Some(Self::Medium)
95        } else if normalized.eq_ignore_ascii_case(constants::HIGH) {
96            Some(Self::High)
97        } else if normalized.eq_ignore_ascii_case(constants::XHIGH) {
98            Some(Self::XHigh)
99        } else if normalized.eq_ignore_ascii_case(constants::MAX) {
100            Some(Self::Max)
101        } else {
102            None
103        }
104    }
105
106    /// Enumerate the allowed configuration values for validation and messaging
107    pub fn allowed_values() -> &'static [&'static str] {
108        constants::ALLOWED_LEVELS
109    }
110}
111
112impl fmt::Display for ReasoningEffortLevel {
113    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114        f.write_str(self.as_str())
115    }
116}
117
118impl<'de> Deserialize<'de> for ReasoningEffortLevel {
119    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
120    where
121        D: Deserializer<'de>,
122    {
123        let raw = String::deserialize(deserializer)?;
124        if let Some(parsed) = Self::parse(&raw) {
125            Ok(parsed)
126        } else {
127            Ok(Self::Unknown)
128        }
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    #[test]
137    fn test_reasoning_effort_parse_and_allowed_values_include_max() {
138        assert_eq!(ReasoningEffortLevel::parse("max"), Some(ReasoningEffortLevel::Max));
139        assert_eq!(ReasoningEffortLevel::Max.as_str(), "max");
140        assert!(ReasoningEffortLevel::allowed_values().contains(&"max"));
141    }
142
143    /// Every named level, with an exhaustive match so a new variant fails to
144    /// compile here until it is added to the level lists.
145    fn named_levels() -> Vec<ReasoningEffortLevel> {
146        use ReasoningEffortLevel::*;
147        let levels = vec![None, Minimal, Low, Medium, High, XHigh, Max];
148        for level in &levels {
149            match level {
150                None | Minimal | Low | Medium | High | XHigh | Max | Unknown => {}
151            }
152        }
153        levels
154    }
155
156    #[test]
157    fn parseable_levels_match_the_parser_in_both_directions() {
158        for value in constants::PARSEABLE_LEVELS {
159            let parsed = ReasoningEffortLevel::parse(value).unwrap_or_else(|| panic!("{value} must parse"));
160            assert_eq!(parsed.as_str(), *value);
161        }
162        let named = named_levels();
163        assert_eq!(named.len(), constants::PARSEABLE_LEVELS.len());
164        for level in named {
165            assert!(constants::PARSEABLE_LEVELS.contains(&level.as_str()), "{level} missing from PARSEABLE_LEVELS");
166        }
167        assert_eq!(ReasoningEffortLevel::parse("unknown"), None);
168    }
169
170    #[test]
171    fn allowed_levels_are_parseable_levels_without_none() {
172        assert_eq!(constants::PARSEABLE_LEVELS[0], constants::NONE);
173        assert_eq!(constants::ALLOWED_LEVELS, &constants::PARSEABLE_LEVELS[1..]);
174    }
175}