Skip to main content

ferrum_types/
reasoning_controls.rs

1//! Standard request controls and model-declared reasoning effort support.
2
3use serde::{Deserialize, Serialize};
4use std::collections::BTreeSet;
5use std::fmt;
6use std::str::FromStr;
7
8/// Standard reasoning effort vocabulary. A request's omitted or null effort is
9/// represented by `Option::None`, separately from the explicit `"none"` value.
10#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize)]
11#[serde(rename_all = "lowercase")]
12pub enum ReasoningEffort {
13    None,
14    Minimal,
15    Low,
16    Medium,
17    High,
18    XHigh,
19    Max,
20}
21
22impl ReasoningEffort {
23    pub const fn as_str(self) -> &'static str {
24        match self {
25            Self::None => "none",
26            Self::Minimal => "minimal",
27            Self::Low => "low",
28            Self::Medium => "medium",
29            Self::High => "high",
30            Self::XHigh => "xhigh",
31            Self::Max => "max",
32        }
33    }
34}
35
36impl fmt::Display for ReasoningEffort {
37    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
38        formatter.write_str(self.as_str())
39    }
40}
41
42impl FromStr for ReasoningEffort {
43    type Err = String;
44
45    fn from_str(value: &str) -> Result<Self, Self::Err> {
46        match value {
47            "none" => Ok(Self::None),
48            "minimal" => Ok(Self::Minimal),
49            "low" => Ok(Self::Low),
50            "medium" => Ok(Self::Medium),
51            "high" => Ok(Self::High),
52            "xhigh" => Ok(Self::XHigh),
53            "max" => Ok(Self::Max),
54            _ => Err(format!(
55                "unsupported reasoning effort {value:?}; expected none, minimal, low, medium, high, xhigh, or max"
56            )),
57        }
58    }
59}
60
61impl<'de> Deserialize<'de> for ReasoningEffort {
62    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
63    where
64        D: serde::Deserializer<'de>,
65    {
66        let value = String::deserialize(deserializer)?;
67        value.parse().map_err(serde::de::Error::custom)
68    }
69}
70
71/// An explicit model contract, independent of its output parser or whether a
72/// template happens to interpolate an effort variable. Unknown support must not
73/// be treated as a declaration that a requested effort is unsupported.
74#[derive(Clone, Debug, Default, Eq, PartialEq)]
75pub enum ReasoningEffortSupport {
76    #[default]
77    Unknown,
78    /// Exhaustive supported values. An empty set explicitly supports no effort
79    /// controls; omission of a declaration is represented by `Unknown` instead.
80    Declared(BTreeSet<ReasoningEffort>),
81}
82
83impl ReasoningEffortSupport {
84    pub fn supports(&self, effort: ReasoningEffort) -> Option<bool> {
85        self.declared_efforts()
86            .map(|efforts| efforts.contains(&effort))
87    }
88
89    pub fn declared_efforts(&self) -> Option<&BTreeSet<ReasoningEffort>> {
90        match self {
91            Self::Unknown => None,
92            Self::Declared(efforts) => Some(efforts),
93        }
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn effort_wire_values_agree_with_text_parsing_and_display() {
103        for (wire, effort) in [
104            ("none", ReasoningEffort::None),
105            ("minimal", ReasoningEffort::Minimal),
106            ("low", ReasoningEffort::Low),
107            ("medium", ReasoningEffort::Medium),
108            ("high", ReasoningEffort::High),
109            ("xhigh", ReasoningEffort::XHigh),
110            ("max", ReasoningEffort::Max),
111        ] {
112            assert_eq!(wire.parse::<ReasoningEffort>().unwrap(), effort);
113            assert_eq!(effort.to_string(), wire);
114            let value = serde_json::Value::String(wire.to_owned());
115            assert_eq!(serde_json::to_value(effort).unwrap(), value);
116            assert_eq!(
117                serde_json::from_value::<ReasoningEffort>(value).unwrap(),
118                effort
119            );
120        }
121    }
122
123    #[test]
124    fn effort_rejects_nonstandard_values_and_nonstring_wire_types() {
125        for value in ["", "auto", "LOW", " high ", "unlimited"] {
126            assert!(value.parse::<ReasoningEffort>().is_err());
127            assert!(serde_json::from_value::<ReasoningEffort>(serde_json::json!(value)).is_err());
128        }
129        for value in [
130            serde_json::json!(false),
131            serde_json::json!(1),
132            serde_json::json!({}),
133            serde_json::json!({"high": null}),
134        ] {
135            assert!(serde_json::from_value::<ReasoningEffort>(value).is_err());
136        }
137    }
138
139    #[test]
140    fn optional_null_is_distinct_from_explicit_none() {
141        assert_eq!(
142            serde_json::from_value::<Option<ReasoningEffort>>(serde_json::Value::Null).unwrap(),
143            None
144        );
145        assert_eq!(
146            serde_json::from_value::<Option<ReasoningEffort>>(serde_json::json!("none")).unwrap(),
147            Some(ReasoningEffort::None)
148        );
149    }
150
151    #[test]
152    fn unknown_support_is_distinct_from_an_exhaustive_declaration() {
153        let unknown = ReasoningEffortSupport::default();
154        assert_eq!(unknown.supports(ReasoningEffort::None), None);
155        assert_eq!(unknown.supports(ReasoningEffort::High), None);
156        assert_eq!(unknown.declared_efforts(), None);
157
158        let declared = ReasoningEffortSupport::Declared(BTreeSet::from([ReasoningEffort::High]));
159        assert_eq!(declared.supports(ReasoningEffort::High), Some(true));
160        assert_eq!(declared.supports(ReasoningEffort::None), Some(false));
161        assert_eq!(
162            declared.declared_efforts(),
163            Some(&BTreeSet::from([ReasoningEffort::High]))
164        );
165        let unsupported = ReasoningEffortSupport::Declared(BTreeSet::new());
166        assert_eq!(unsupported.supports(ReasoningEffort::High), Some(false));
167    }
168}