diff --git a/crates/codegen/kigi-sampling-types/src/types.rs b/crates/codegen/kigi-sampling-types/src/types.rs index 0fcbf21..fadf424 100644 --- a/crates/codegen/kigi-sampling-types/src/types.rs +++ b/crates/codegen/kigi-sampling-types/src/types.rs @@ -897,7 +897,7 @@ impl CompactionsRemaining { } /// Reasoning effort level. `None`/`Minimal` are omitted on the Anthropic Messages API. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize)] #[serde(rename_all = "lowercase")] pub enum ReasoningEffort { None, @@ -982,6 +982,16 @@ impl std::str::FromStr for ReasoningEffort { } } +impl<'de> serde::Deserialize<'de> for ReasoningEffort { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let raw = String::deserialize(deserializer)?; + raw.parse().map_err(serde::de::Error::custom) + } +} + /// Canonical wire parse; remapped menu ids need a model catalog. pub fn parse_canonical_effort_token(token: &str) -> Option { token.parse().ok() @@ -1849,6 +1859,24 @@ mod tests { assert!(serde_json::from_str::("\"BOGUS\"").is_err()); } + #[test] + fn reasoning_effort_deserializes_mixed_case() { + for (token, want) in [ + ("\"High\"", ReasoningEffort::High), + ("\"XHIGH\"", ReasoningEffort::Xhigh), + ("\"mAx\"", ReasoningEffort::Max), + ] { + let got: ReasoningEffort = serde_json::from_str(token).unwrap(); + assert_eq!(got, want, "deserialize {token}"); + } + let err = serde_json::from_str::("\"Highest\"").unwrap_err(); + assert!( + err.to_string().contains("invalid reasoning effort"), + "{err}" + ); + assert!(serde_json::from_str::("7").is_err()); + } + #[test] fn reasoning_effort_from_str_max_and_xhigh_are_distinct() { assert_eq!(