1use serde::{Deserialize, Serialize};
4use std::collections::BTreeSet;
5use std::fmt;
6use std::str::FromStr;
7
8#[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#[derive(Clone, Debug, Default, Eq, PartialEq)]
75pub enum ReasoningEffortSupport {
76 #[default]
77 Unknown,
78 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}