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(crate) const NONE: &str = "none";
14    pub(crate) 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    pub(crate) const ALLOWED_LEVELS: &[&str] = &[MINIMAL, LOW, MEDIUM, HIGH, XHIGH, MAX];
21    pub const LABEL_LOW: &str = "Low";
22    pub const LABEL_MEDIUM: &str = "Medium";
23    pub const LABEL_HIGH: &str = "High";
24    pub const DESCRIPTION_LOW: &str = "Fast responses with lightweight reasoning.";
25    pub const DESCRIPTION_MEDIUM: &str = "Balanced depth and speed. (Note: Mapped to high on some models)";
26    pub const DESCRIPTION_HIGH: &str = "Deep reasoning for complex problems.";
27}
28
29/// Supported reasoning effort levels configured via vtcode.toml
30/// These map to different provider-specific parameters:
31/// - For Gemini 3 Pro: Maps to thinking_level (low, high) - medium coming soon
32/// - For other models: Maps to provider-specific reasoning parameters
33#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
35#[serde(rename_all = "lowercase")]
36#[derive(Default)]
37pub enum ReasoningEffortLevel {
38    /// No reasoning configuration - for models that don't support configurable reasoning
39    None,
40    /// Minimal reasoning effort - maps to low thinking level for Gemini 3 Pro
41    Minimal,
42    /// Low reasoning effort - maps to low thinking level for Gemini 3 Pro
43    Low,
44    /// Medium reasoning effort - Note: Not fully available for Gemini 3 Pro yet, defaults to high
45    #[default]
46    Medium,
47    /// High reasoning effort - maps to high thinking level for Gemini 3 Pro
48    High,
49    /// Extra high reasoning effort - for GPT-5.6/6-Astra, Claude adaptive,
50    /// Grok-4.6+, and Muse Spark long-running tasks
51    XHigh,
52    /// Maximum reasoning effort - for GPT-5.6/6-Astra, Claude adaptive,
53    /// DeepSeek-V4, Kimi-K3, and GLM-5.x; aliased elsewhere
54    Max,
55    /// Forward-compatible catch-all for unrecognized effort level values
56    Unknown,
57}
58
59impl ReasoningEffortLevel {
60    /// Return the textual representation expected by downstream APIs
61    pub fn as_str(self) -> &'static str {
62        match self {
63            Self::None => constants::NONE,
64            Self::Minimal => constants::MINIMAL,
65            Self::Low => constants::LOW,
66            Self::Medium => constants::MEDIUM,
67            Self::High => constants::HIGH,
68            Self::XHigh => constants::XHIGH,
69            Self::Max => constants::MAX,
70            Self::Unknown => "unknown",
71        }
72    }
73
74    /// Attempt to parse an effort level from user configuration input
75    pub fn parse(value: &str) -> Option<Self> {
76        let normalized = value.trim();
77        if normalized.eq_ignore_ascii_case(constants::NONE) {
78            Some(Self::None)
79        } else if normalized.eq_ignore_ascii_case(constants::MINIMAL) {
80            Some(Self::Minimal)
81        } else if normalized.eq_ignore_ascii_case(constants::LOW) {
82            Some(Self::Low)
83        } else if normalized.eq_ignore_ascii_case(constants::MEDIUM) {
84            Some(Self::Medium)
85        } else if normalized.eq_ignore_ascii_case(constants::HIGH) {
86            Some(Self::High)
87        } else if normalized.eq_ignore_ascii_case(constants::XHIGH) {
88            Some(Self::XHigh)
89        } else if normalized.eq_ignore_ascii_case(constants::MAX) {
90            Some(Self::Max)
91        } else {
92            None
93        }
94    }
95
96    /// Enumerate the allowed configuration values for validation and messaging
97    pub fn allowed_values() -> &'static [&'static str] {
98        constants::ALLOWED_LEVELS
99    }
100}
101
102impl fmt::Display for ReasoningEffortLevel {
103    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104        f.write_str(self.as_str())
105    }
106}
107
108impl<'de> Deserialize<'de> for ReasoningEffortLevel {
109    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
110    where
111        D: Deserializer<'de>,
112    {
113        let raw = String::deserialize(deserializer)?;
114        if let Some(parsed) = Self::parse(&raw) {
115            Ok(parsed)
116        } else {
117            Ok(Self::Unknown)
118        }
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    #[test]
127    fn test_reasoning_effort_parse_and_allowed_values_include_max() {
128        assert_eq!(ReasoningEffortLevel::parse("max"), Some(ReasoningEffortLevel::Max));
129        assert_eq!(ReasoningEffortLevel::Max.as_str(), "max");
130        assert!(ReasoningEffortLevel::allowed_values().contains(&"max"));
131    }
132}