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