use serde::{Deserialize, Deserializer, Serialize};
use std::fmt;
pub mod constants {
pub const NONE: &str = "none";
pub const MINIMAL: &str = "minimal";
pub const LOW: &str = "low";
pub const MEDIUM: &str = "medium";
pub const HIGH: &str = "high";
pub const XHIGH: &str = "xhigh";
pub const MAX: &str = "max";
pub const PARSEABLE_LEVELS: &[&str] = &[NONE, MINIMAL, LOW, MEDIUM, HIGH, XHIGH, MAX];
pub const ALLOWED_LEVELS: &[&str] = match PARSEABLE_LEVELS.split_first() {
Some((_none, levels)) => levels,
None => &[],
};
pub const LABEL_LOW: &str = "Low";
pub const LABEL_MEDIUM: &str = "Medium";
pub const LABEL_HIGH: &str = "High";
pub const DESCRIPTION_LOW: &str = "Fast responses with lightweight reasoning.";
pub const DESCRIPTION_MEDIUM: &str = "Balanced depth and speed. (Note: Mapped to high on some models)";
pub const DESCRIPTION_HIGH: &str = "Deep reasoning for complex problems.";
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
#[derive(Default)]
pub enum ReasoningEffortLevel {
None,
Minimal,
Low,
#[default]
Medium,
High,
XHigh,
Max,
Unknown,
}
impl ReasoningEffortLevel {
pub fn as_str(self) -> &'static str {
match self {
Self::None => constants::NONE,
Self::Minimal => constants::MINIMAL,
Self::Low => constants::LOW,
Self::Medium => constants::MEDIUM,
Self::High => constants::HIGH,
Self::XHigh => constants::XHIGH,
Self::Max => constants::MAX,
Self::Unknown => "unknown",
}
}
pub fn parse(value: &str) -> Option<Self> {
let normalized = value.trim();
if normalized.eq_ignore_ascii_case(constants::NONE) {
Some(Self::None)
} else if normalized.eq_ignore_ascii_case(constants::MINIMAL) {
Some(Self::Minimal)
} else if normalized.eq_ignore_ascii_case(constants::LOW) {
Some(Self::Low)
} else if normalized.eq_ignore_ascii_case(constants::MEDIUM) {
Some(Self::Medium)
} else if normalized.eq_ignore_ascii_case(constants::HIGH) {
Some(Self::High)
} else if normalized.eq_ignore_ascii_case(constants::XHIGH) {
Some(Self::XHigh)
} else if normalized.eq_ignore_ascii_case(constants::MAX) {
Some(Self::Max)
} else {
None
}
}
pub fn allowed_values() -> &'static [&'static str] {
constants::ALLOWED_LEVELS
}
}
impl fmt::Display for ReasoningEffortLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for ReasoningEffortLevel {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let raw = String::deserialize(deserializer)?;
if let Some(parsed) = Self::parse(&raw) {
Ok(parsed)
} else {
Ok(Self::Unknown)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_reasoning_effort_parse_and_allowed_values_include_max() {
assert_eq!(ReasoningEffortLevel::parse("max"), Some(ReasoningEffortLevel::Max));
assert_eq!(ReasoningEffortLevel::Max.as_str(), "max");
assert!(ReasoningEffortLevel::allowed_values().contains(&"max"));
}
fn named_levels() -> Vec<ReasoningEffortLevel> {
use ReasoningEffortLevel::*;
let levels = vec![None, Minimal, Low, Medium, High, XHigh, Max];
for level in &levels {
match level {
None | Minimal | Low | Medium | High | XHigh | Max | Unknown => {}
}
}
levels
}
#[test]
fn parseable_levels_match_the_parser_in_both_directions() {
for value in constants::PARSEABLE_LEVELS {
let parsed = ReasoningEffortLevel::parse(value).unwrap_or_else(|| panic!("{value} must parse"));
assert_eq!(parsed.as_str(), *value);
}
let named = named_levels();
assert_eq!(named.len(), constants::PARSEABLE_LEVELS.len());
for level in named {
assert!(constants::PARSEABLE_LEVELS.contains(&level.as_str()), "{level} missing from PARSEABLE_LEVELS");
}
assert_eq!(ReasoningEffortLevel::parse("unknown"), None);
}
#[test]
fn allowed_levels_are_parseable_levels_without_none() {
assert_eq!(constants::PARSEABLE_LEVELS[0], constants::NONE);
assert_eq!(constants::ALLOWED_LEVELS, &constants::PARSEABLE_LEVELS[1..]);
}
}