openai_tools/moderations/request.rs
1//! OpenAI Moderations API Request Module
2//!
3//! This module provides the functionality to interact with the OpenAI Moderations API.
4//! It allows you to classify text inputs to determine if they violate content policies.
5//!
6//! # Key Features
7//!
8//! - **Single Text Moderation**: Check a single text string
9//! - **Batch Moderation**: Check multiple texts at once
10//! - **Model Selection**: Choose between omni-moderation and text-moderation models
11//!
12//! # Quick Start
13//!
14//! ```rust,no_run
15//! use openai_tools::moderations::request::Moderations;
16//!
17//! #[tokio::main]
18//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
19//! let moderations = Moderations::new()?;
20//!
21//! // Check a text for policy violations
22//! let response = moderations.moderate_text("Hello, world!", None).await?;
23//! if response.results[0].flagged {
24//! println!("Content was flagged!");
25//! } else {
26//! println!("Content is safe");
27//! }
28//!
29//! Ok(())
30//! }
31//! ```
32
33use crate::common::auth::AuthProvider;
34use crate::common::client::create_http_client;
35use crate::common::errors::{ErrorResponse, OpenAIToolError, Result};
36use crate::moderations::response::ModerationResponse;
37use serde::{Deserialize, Serialize};
38use std::time::Duration;
39
40/// Default API path for Moderations
41const MODERATIONS_PATH: &str = "moderations";
42
43/// Moderation model options.
44///
45/// The model to use for content moderation. Newer omni-moderation models
46/// support more categorization options and multi-modal inputs.
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
48#[non_exhaustive]
49pub enum ModerationModel {
50 /// Latest omni-moderation model with multi-modal support
51 #[serde(rename = "omni-moderation-latest")]
52 #[default]
53 OmniModerationLatest,
54 /// Legacy text-only moderation model
55 #[serde(rename = "text-moderation-latest")]
56 TextModerationLatest,
57}
58
59impl ModerationModel {
60 /// Returns the model identifier string.
61 pub fn as_str(&self) -> &'static str {
62 match self {
63 Self::OmniModerationLatest => "omni-moderation-latest",
64 Self::TextModerationLatest => "text-moderation-latest",
65 }
66 }
67}
68
69impl std::fmt::Display for ModerationModel {
70 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 write!(f, "{}", self.as_str())
72 }
73}
74
75/// Request payload for moderation endpoint.
76#[derive(Debug, Clone, Serialize, Deserialize)]
77struct ModerationRequest {
78 /// The input to classify
79 input: ModerationInput,
80 /// The model to use for classification
81 #[serde(skip_serializing_if = "Option::is_none")]
82 model: Option<String>,
83}
84
85/// Input types for moderation.
86#[derive(Debug, Clone, Serialize, Deserialize)]
87#[serde(untagged)]
88enum ModerationInput {
89 /// Single text string
90 Single(String),
91 /// Multiple text strings
92 Multiple(Vec<String>),
93}
94
95/// Client for interacting with the OpenAI Moderations API.
96///
97/// This struct provides methods to classify text content for potential
98/// policy violations. Use [`Moderations::new()`] to create a new instance.
99///
100/// # Example
101///
102/// ```rust,no_run
103/// use openai_tools::moderations::request::{Moderations, ModerationModel};
104///
105/// #[tokio::main]
106/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
107/// let moderations = Moderations::new()?;
108///
109/// // Check content with a specific model
110/// let response = moderations
111/// .moderate_text("Some text to check", Some(ModerationModel::OmniModerationLatest))
112/// .await?;
113///
114/// for result in &response.results {
115/// println!("Flagged: {}", result.flagged);
116/// }
117///
118/// Ok(())
119/// }
120/// ```
121pub struct Moderations {
122 /// Authentication provider (OpenAI or Azure)
123 auth: AuthProvider,
124 /// Optional request timeout duration
125 timeout: Option<Duration>,
126}
127
128impl Moderations {
129 /// Creates a new Moderations client for OpenAI API.
130 ///
131 /// Initializes the client by loading the OpenAI API key from
132 /// the environment variable `OPENAI_API_KEY`. Supports `.env` file loading
133 /// via dotenvy.
134 ///
135 /// # Returns
136 ///
137 /// * `Ok(Moderations)` - A new Moderations client ready for use
138 /// * `Err(OpenAIToolError)` - If the API key is not found in the environment
139 ///
140 /// # Example
141 ///
142 /// ```rust,no_run
143 /// use openai_tools::moderations::request::Moderations;
144 ///
145 /// let moderations = Moderations::new().expect("API key should be set");
146 /// ```
147 pub fn new() -> Result<Self> {
148 let auth = AuthProvider::openai_from_env()?;
149 Ok(Self { auth, timeout: None })
150 }
151
152 /// Creates a new Moderations client with a custom authentication provider
153 pub fn with_auth(auth: AuthProvider) -> Self {
154 Self { auth, timeout: None }
155 }
156
157 /// Creates a new Moderations client for Azure OpenAI API
158 pub fn azure() -> Result<Self> {
159 let auth = AuthProvider::azure_from_env()?;
160 Ok(Self { auth, timeout: None })
161 }
162
163 /// Creates a new Moderations client by auto-detecting the provider
164 pub fn detect_provider() -> Result<Self> {
165 let auth = AuthProvider::from_env()?;
166 Ok(Self { auth, timeout: None })
167 }
168
169 /// Creates a new Moderations client with URL-based provider detection
170 pub fn with_url<S: Into<String>>(base_url: S, api_key: S) -> Self {
171 let auth = AuthProvider::from_url_with_key(base_url, api_key);
172 Self { auth, timeout: None }
173 }
174
175 /// Creates a new Moderations client from URL using environment variables
176 pub fn from_url<S: Into<String>>(url: S) -> Result<Self> {
177 let auth = AuthProvider::from_url(url)?;
178 Ok(Self { auth, timeout: None })
179 }
180
181 /// Returns the authentication provider
182 pub fn auth(&self) -> &AuthProvider {
183 &self.auth
184 }
185
186 /// Sets the request timeout duration.
187 ///
188 /// # Arguments
189 ///
190 /// * `timeout` - The maximum time to wait for a response
191 ///
192 /// # Returns
193 ///
194 /// A mutable reference to self for method chaining
195 pub fn timeout(&mut self, timeout: Duration) -> &mut Self {
196 self.timeout = Some(timeout);
197 self
198 }
199
200 /// Creates the HTTP client with default headers.
201 fn create_client(&self) -> Result<(request::Client, request::header::HeaderMap)> {
202 let client = create_http_client(self.timeout)?;
203 let mut headers = request::header::HeaderMap::new();
204 self.auth.apply_headers(&mut headers)?;
205 headers.insert("Content-Type", request::header::HeaderValue::from_static("application/json"));
206 headers.insert("User-Agent", request::header::HeaderValue::from_static("openai-tools-rust"));
207 Ok((client, headers))
208 }
209
210 /// Moderates a single text string.
211 ///
212 /// Classifies the input text to determine if it violates OpenAI's content policy.
213 ///
214 /// # Arguments
215 ///
216 /// * `text` - The text content to moderate
217 /// * `model` - Optional model to use (defaults to `omni-moderation-latest`)
218 ///
219 /// # Returns
220 ///
221 /// * `Ok(ModerationResponse)` - The moderation results
222 /// * `Err(OpenAIToolError)` - If the request fails or response parsing fails
223 ///
224 /// # Example
225 ///
226 /// ```rust,no_run
227 /// use openai_tools::moderations::request::Moderations;
228 ///
229 /// #[tokio::main]
230 /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
231 /// let moderations = Moderations::new()?;
232 /// let response = moderations.moderate_text("Hello, world!", None).await?;
233 ///
234 /// let result = &response.results[0];
235 /// if result.flagged {
236 /// println!("Content was flagged!");
237 /// println!("Hate score: {}", result.category_scores.hate);
238 /// }
239 /// Ok(())
240 /// }
241 /// ```
242 pub async fn moderate_text(&self, text: &str, model: Option<ModerationModel>) -> Result<ModerationResponse> {
243 let request_body = ModerationRequest { input: ModerationInput::Single(text.to_string()), model: model.map(|m| m.as_str().to_string()) };
244
245 self.send_request(&request_body).await
246 }
247
248 /// Moderates multiple text strings.
249 ///
250 /// Classifies multiple input texts in a single request.
251 ///
252 /// # Arguments
253 ///
254 /// * `texts` - Vector of text strings to moderate
255 /// * `model` - Optional model to use (defaults to `omni-moderation-latest`)
256 ///
257 /// # Returns
258 ///
259 /// * `Ok(ModerationResponse)` - The moderation results (one result per input)
260 /// * `Err(OpenAIToolError)` - If the request fails or response parsing fails
261 ///
262 /// # Example
263 ///
264 /// ```rust,no_run
265 /// use openai_tools::moderations::request::Moderations;
266 ///
267 /// #[tokio::main]
268 /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
269 /// let moderations = Moderations::new()?;
270 /// let texts = vec![
271 /// "First text to check".to_string(),
272 /// "Second text to check".to_string(),
273 /// ];
274 /// let response = moderations.moderate_texts(texts, None).await?;
275 ///
276 /// for (i, result) in response.results.iter().enumerate() {
277 /// println!("Text {}: flagged = {}", i + 1, result.flagged);
278 /// }
279 /// Ok(())
280 /// }
281 /// ```
282 pub async fn moderate_texts(&self, texts: Vec<String>, model: Option<ModerationModel>) -> Result<ModerationResponse> {
283 let request_body = ModerationRequest { input: ModerationInput::Multiple(texts), model: model.map(|m| m.as_str().to_string()) };
284
285 self.send_request(&request_body).await
286 }
287
288 /// Sends the moderation request to the API.
289 async fn send_request(&self, request_body: &ModerationRequest) -> Result<ModerationResponse> {
290 let (client, headers) = self.create_client()?;
291
292 let body = serde_json::to_string(request_body).map_err(OpenAIToolError::SerdeJsonError)?;
293
294 let url = self.auth.endpoint(MODERATIONS_PATH);
295 let response = client.post(&url).headers(headers).body(body).send().await.map_err(OpenAIToolError::RequestError)?;
296
297 let status = response.status();
298 let content = response.text().await.map_err(OpenAIToolError::RequestError)?;
299
300 if cfg!(test) {
301 tracing::info!("Response content: {}", content);
302 }
303
304 if !status.is_success() {
305 if let Ok(error_resp) = serde_json::from_str::<ErrorResponse>(&content) {
306 return Err(OpenAIToolError::Error(error_resp.error.message.unwrap_or_default()));
307 }
308 return Err(OpenAIToolError::Error(format!("API error ({}): {}", status, content)));
309 }
310
311 serde_json::from_str::<ModerationResponse>(&content).map_err(OpenAIToolError::SerdeJsonError)
312 }
313}