1use crate::ModelConstraints;
3use serde::{Deserialize, Serialize};
4
5#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
7pub struct ModelCapabilities {
8 pub vision: bool,
9 pub tool_calling: bool,
10 pub structured_output: bool,
11}
12
13impl ModelConstraints {
14 pub fn validate(&self, capabilities: &ModelCapabilities) -> Result<(), &'static str> {
16 for (required, supported, name) in [
17 (self.vision, capabilities.vision, "vision"),
18 (self.tool_calling, capabilities.tool_calling, "tool_calling"),
19 (
20 self.structured_output,
21 capabilities.structured_output,
22 "structured_output",
23 ),
24 ] {
25 if required && !supported {
26 return Err(name);
27 }
28 }
29 Ok(())
30 }
31}
32
33#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
37#[serde(deny_unknown_fields)]
38pub struct GenerationParameters {
39 pub reasoning_effort: Option<String>,
40 pub temperature: Option<f64>,
41}
42
43impl GenerationParameters {
44 pub fn validate(&self) -> Result<(), &'static str> {
45 if let Some(effort) = self.reasoning_effort.as_deref() {
46 if !matches!(
47 effort,
48 "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max" | "ultra"
49 ) {
50 return Err("invalid reasoning_effort");
51 }
52 }
53 if self
54 .temperature
55 .is_some_and(|value| !value.is_finite() || value < 0.0)
56 {
57 return Err("temperature must be finite and nonnegative");
58 }
59 Ok(())
60 }
61}
62
63#[cfg(test)]
64mod tests {
65 use super::*;
66 #[test]
67 fn declarations_are_requirements_not_prohibitions() {
68 let supported = ModelCapabilities {
69 vision: true,
70 tool_calling: true,
71 structured_output: true,
72 };
73 assert!(ModelConstraints::default().validate(&supported).is_ok());
74 assert!(ModelConstraints::default()
75 .validate(&ModelCapabilities::default())
76 .is_ok());
77 for constraints in [
78 ModelConstraints {
79 vision: true,
80 ..Default::default()
81 },
82 ModelConstraints {
83 tool_calling: true,
84 ..Default::default()
85 },
86 ModelConstraints {
87 structured_output: true,
88 ..Default::default()
89 },
90 ] {
91 assert!(constraints.validate(&supported).is_ok());
92 assert!(constraints.validate(&ModelCapabilities::default()).is_err());
93 }
94 }
95 #[test]
96 fn retired_generation_control_is_not_silently_ignored() {
97 assert!(serde_json::from_value::<ModelConstraints>(serde_json::json!({
98 "vision": false, "tool_calling": false, "structured_output": false, "reasoning": "high"
99 })).is_err());
100 }
101 #[test]
102 fn validates_explicit_generation_settings() {
103 for effort in ["none", "low", "high", "max", "ultra"] {
104 assert!(GenerationParameters {
105 reasoning_effort: Some(effort.into()),
106 temperature: None
107 }
108 .validate()
109 .is_ok());
110 }
111 assert!(GenerationParameters {
112 reasoning_effort: Some("unknown".into()),
113 temperature: None
114 }
115 .validate()
116 .is_err());
117 for temperature in [-1.0, f64::NAN, f64::INFINITY] {
118 assert!(GenerationParameters {
119 temperature: Some(temperature),
120 ..Default::default()
121 }
122 .validate()
123 .is_err());
124 }
125 }
126}