Skip to main content

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}