1use serde::{Deserialize, Serialize};
16
17use crate::capabilities::{CapabilityRequirements, StructuredOutputCapability};
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
21#[serde(rename_all = "snake_case")]
22#[non_exhaustive]
23pub enum ModelPurpose {
24 OfflineEvaluate,
26 Segment,
28 Coverage,
30 TakeUp,
32 Route,
34 Locate,
36 Extract,
38 Verify,
40 QuestionFrame,
42 CrossCheck,
44 Respects,
46 Investigate,
48 Acknowledge,
50 Answer,
52 Review,
54 Progress,
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
61#[serde(rename_all = "snake_case")]
62pub enum SafetyMode {
63 #[default]
66 Default,
67 UnsafeExperimental,
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
75#[serde(rename_all = "snake_case")]
76pub enum LogDetail {
77 Metadata,
79 Redacted,
81 Full,
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
87pub struct LoggingPolicy {
88 pub prompt: LogDetail,
90 pub output: LogDetail,
92 pub retain_raw_bodies: bool,
95}
96
97impl LoggingPolicy {
98 pub const METADATA_ONLY: Self = Self {
100 prompt: LogDetail::Metadata,
101 output: LogDetail::Metadata,
102 retain_raw_bodies: false,
103 };
104
105 pub const REDACTED_OUTPUT: Self = Self {
107 prompt: LogDetail::Metadata,
108 output: LogDetail::Redacted,
109 retain_raw_bodies: false,
110 };
111
112 pub const FULL: Self = Self {
114 prompt: LogDetail::Full,
115 output: LogDetail::Full,
116 retain_raw_bodies: false,
117 };
118
119 #[must_use]
121 pub const fn logs_content(&self) -> bool {
122 !matches!(self.prompt, LogDetail::Metadata) || !matches!(self.output, LogDetail::Metadata)
123 }
124}
125
126pub const MUTATION_SAFE_STRUCTURED_OUTPUT: [StructuredOutputCapability; 3] = [
128 StructuredOutputCapability::NativeJsonSchema,
129 StructuredOutputCapability::NativeFunctionSchema,
130 StructuredOutputCapability::GrammarConstrained,
131];
132
133pub const READ_ONLY_STRUCTURED_OUTPUT: [StructuredOutputCapability; 4] = [
135 StructuredOutputCapability::NativeJsonSchema,
136 StructuredOutputCapability::NativeFunctionSchema,
137 StructuredOutputCapability::GrammarConstrained,
138 StructuredOutputCapability::JsonObject,
139];
140
141impl ModelPurpose {
142 pub const ALL: [Self; 16] = [
144 Self::OfflineEvaluate,
145 Self::Segment,
146 Self::Coverage,
147 Self::TakeUp,
148 Self::Route,
149 Self::Locate,
150 Self::Extract,
151 Self::Verify,
152 Self::QuestionFrame,
153 Self::CrossCheck,
154 Self::Respects,
155 Self::Investigate,
156 Self::Acknowledge,
157 Self::Answer,
158 Self::Review,
159 Self::Progress,
160 ];
161
162 #[must_use]
164 pub const fn is_understanding(self) -> bool {
165 matches!(
166 self,
167 Self::Segment
168 | Self::Coverage
169 | Self::TakeUp
170 | Self::Route
171 | Self::Locate
172 | Self::Extract
173 | Self::Verify
174 | Self::QuestionFrame
175 | Self::CrossCheck
176 | Self::Respects
177 | Self::Investigate
178 )
179 }
180
181 #[must_use]
183 pub const fn as_str(self) -> &'static str {
184 match self {
185 Self::OfflineEvaluate => "offline_evaluate",
186 Self::Segment => "segment",
187 Self::Coverage => "coverage",
188 Self::TakeUp => "take_up",
189 Self::Route => "route",
190 Self::Locate => "locate",
191 Self::Extract => "extract",
192 Self::Verify => "verify",
193 Self::QuestionFrame => "question_frame",
194 Self::CrossCheck => "cross_check",
195 Self::Respects => "respects",
196 Self::Investigate => "investigate",
197 Self::Acknowledge => "acknowledge",
198 Self::Answer => "answer",
199 Self::Review => "review",
200 Self::Progress => "progress",
201 }
202 }
203
204 #[must_use]
207 pub const fn is_critical(self) -> bool {
208 self.is_understanding()
209 }
210
211 #[must_use]
213 pub fn requirements(self) -> CapabilityRequirements {
214 self.requirements_in(SafetyMode::Default)
215 }
216
217 #[must_use]
227 pub fn requirements_in(self, mode: SafetyMode) -> CapabilityRequirements {
228 let structured_output: Vec<StructuredOutputCapability> = match (self, mode) {
229 (_, SafetyMode::UnsafeExperimental) | (Self::OfflineEvaluate, _) => Vec::new(),
230 (
231 Self::Investigate
232 | Self::Acknowledge
233 | Self::Answer
234 | Self::Review
235 | Self::Progress,
236 SafetyMode::Default,
237 ) => READ_ONLY_STRUCTURED_OUTPUT.to_vec(),
238 (_, SafetyMode::Default) => MUTATION_SAFE_STRUCTURED_OUTPUT.to_vec(),
239 };
240 CapabilityRequirements {
241 structured_output,
242 needs_tools: false,
243 needs_streaming: false,
244 min_context_tokens: None,
245 needs_vision: false,
246 needs_documents: false,
247 }
248 }
249
250 #[must_use]
258 pub const fn logging_policy(self) -> LoggingPolicy {
259 match self {
260 Self::Segment
261 | Self::Coverage
262 | Self::TakeUp
263 | Self::Route
264 | Self::Locate
265 | Self::Extract
266 | Self::Verify
267 | Self::QuestionFrame
268 | Self::CrossCheck
269 | Self::Respects
270 | Self::Investigate => LoggingPolicy::REDACTED_OUTPUT,
271 Self::Acknowledge | Self::Answer | Self::Review | Self::Progress => {
272 LoggingPolicy::METADATA_ONLY
273 }
274 Self::OfflineEvaluate => LoggingPolicy::FULL,
275 }
276 }
277}
278
279impl std::fmt::Display for ModelPurpose {
280 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
281 f.write_str(self.as_str())
282 }
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288 use crate::capabilities::ProviderCapabilities;
289
290 fn caps(structured: StructuredOutputCapability) -> ProviderCapabilities {
291 ProviderCapabilities::minimal().with_structured_output(structured)
292 }
293
294 #[test]
295 fn understanding_rejects_prompt_only_json_object_and_none() {
296 let requirements = ModelPurpose::Extract.requirements();
297 for unsafe_transport in [
298 StructuredOutputCapability::PromptOnly,
299 StructuredOutputCapability::JsonObject,
300 StructuredOutputCapability::None,
301 ] {
302 assert!(requirements.satisfied_by(&caps(unsafe_transport)).is_err());
303 }
304 for safe in MUTATION_SAFE_STRUCTURED_OUTPUT {
305 assert!(requirements.satisfied_by(&caps(safe)).is_ok());
306 }
307 }
308
309 #[test]
310 fn unsafe_experimental_is_an_explicit_opt_in() {
311 let requirements = ModelPurpose::Extract.requirements_in(SafetyMode::UnsafeExperimental);
312 assert!(requirements.structured_output.is_empty());
313 assert!(
314 requirements
315 .satisfied_by(&caps(StructuredOutputCapability::PromptOnly))
316 .is_ok()
317 );
318 }
319
320 #[test]
321 fn a_read_only_task_accepts_json_object_but_not_prompt_only() {
322 for purpose in [ModelPurpose::Investigate, ModelPurpose::Acknowledge] {
323 let requirements = purpose.requirements();
324 assert_eq!(
325 requirements.structured_output,
326 READ_ONLY_STRUCTURED_OUTPUT.to_vec()
327 );
328 assert!(
329 requirements
330 .satisfied_by(&caps(StructuredOutputCapability::PromptOnly))
331 .is_err()
332 );
333 }
334 }
335
336 #[test]
337 fn narration_logs_metadata_only_and_is_not_critical() {
338 assert!(!ModelPurpose::Acknowledge.logging_policy().logs_content());
339 assert!(!ModelPurpose::Acknowledge.is_critical());
340 assert!(ModelPurpose::Extract.is_critical());
341 assert!(
342 ModelPurpose::OfflineEvaluate
343 .requirements()
344 .structured_output
345 .is_empty()
346 );
347 }
348
349 #[test]
350 fn labels_are_unique_and_never_retain_raw_bodies() {
351 let mut labels: Vec<&str> = ModelPurpose::ALL.iter().map(|p| p.as_str()).collect();
352 labels.sort_unstable();
353 labels.dedup();
354 assert_eq!(labels.len(), ModelPurpose::ALL.len());
355 for purpose in ModelPurpose::ALL {
356 assert!(!purpose.logging_policy().retain_raw_bodies);
357 let json = serde_json::to_string(&purpose).unwrap();
358 assert_eq!(json, format!("\"{}\"", purpose.as_str()));
359 }
360 }
361}