linger-openai-sdk 0.1.1

Rust-native async SDK for OpenAI APIs with typed requests, streaming, uploads, retries, and pluggable transports.
Documentation
use crate::error::LingerError;
use crate::RequestId;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::BTreeMap;

/// EN: Request body for `POST /v1/moderations`.
/// 中文:`POST /v1/moderations` 的请求体。
#[derive(Clone, Debug, Serialize, PartialEq)]
#[non_exhaustive]
pub struct CreateModerationRequest {
    /// EN: Optional moderation model id.
    /// 中文:可选的 moderation 模型 ID。
    #[serde(skip_serializing_if = "Option::is_none")]
    pub model: Option<String>,
    /// EN: Text, text list, or multimodal input to classify.
    /// 中文:要分类的文本、文本列表或多模态输入。
    pub input: ModerationInput,
    /// EN: Forward-compatible optional fields not yet covered by handwritten types.
    /// 中文:手写类型尚未覆盖的前向兼容可选字段。
    #[serde(flatten)]
    pub extra: BTreeMap<String, Value>,
}

impl CreateModerationRequest {
    /// EN: Starts building a moderation request.
    /// 中文:开始构建 moderation 请求。
    pub fn builder() -> CreateModerationRequestBuilder {
        CreateModerationRequestBuilder::default()
    }
}

/// EN: Builder for create-moderation requests.
/// 中文:创建 moderation 请求的构建器。
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct CreateModerationRequestBuilder {
    model: Option<String>,
    input: Option<ModerationInput>,
    extra: BTreeMap<String, Value>,
}

impl CreateModerationRequestBuilder {
    /// EN: Sets the optional moderation model id.
    /// 中文:设置可选的 moderation 模型 ID。
    pub fn model(mut self, model: impl Into<String>) -> Self {
        self.model = Some(model.into());
        self
    }

    /// EN: Sets the moderation input.
    /// 中文:设置 moderation 输入。
    pub fn input(mut self, input: impl Into<ModerationInput>) -> Self {
        self.input = Some(input.into());
        self
    }

    /// EN: Adds a forward-compatible JSON field.
    /// 中文:添加前向兼容的 JSON 字段。
    pub fn extra(mut self, name: impl Into<String>, value: Value) -> Self {
        self.extra.insert(name.into(), value);
        self
    }

    /// EN: Builds and validates the request.
    /// 中文:构建并校验请求。
    pub fn build(self) -> Result<CreateModerationRequest, LingerError> {
        if self
            .model
            .as_ref()
            .is_some_and(|value| value.trim().is_empty())
        {
            return Err(LingerError::invalid_config("model must not be empty"));
        }
        let input = self
            .input
            .ok_or_else(|| LingerError::invalid_config("input is required"))?;
        if input.is_empty() {
            return Err(LingerError::invalid_config("input must not be empty"));
        }
        Ok(CreateModerationRequest {
            model: self.model,
            input,
            extra: self.extra,
        })
    }
}

/// EN: Moderations API input value.
/// 中文:Moderations API 输入值。
#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
#[serde(untagged)]
#[non_exhaustive]
pub enum ModerationInput {
    /// EN: Single text input.
    /// 中文:单条文本输入。
    Text(String),
    /// EN: Multiple text inputs.
    /// 中文:多条文本输入。
    Texts(Vec<String>),
    /// EN: Multimodal input items.
    /// 中文:多模态输入项。
    Items(Vec<ModerationInputItem>),
}

impl ModerationInput {
    fn is_empty(&self) -> bool {
        match self {
            Self::Text(value) => value.is_empty(),
            Self::Texts(values) => values.is_empty() || values.iter().any(String::is_empty),
            Self::Items(items) => {
                items.is_empty() || items.iter().any(ModerationInputItem::is_empty)
            }
        }
    }
}

impl From<&str> for ModerationInput {
    fn from(value: &str) -> Self {
        Self::Text(value.to_string())
    }
}

impl From<String> for ModerationInput {
    fn from(value: String) -> Self {
        Self::Text(value)
    }
}

impl From<Vec<String>> for ModerationInput {
    fn from(value: Vec<String>) -> Self {
        Self::Texts(value)
    }
}

impl From<Vec<&str>> for ModerationInput {
    fn from(value: Vec<&str>) -> Self {
        Self::Texts(value.into_iter().map(str::to_string).collect())
    }
}

impl From<Vec<ModerationInputItem>> for ModerationInput {
    fn from(value: Vec<ModerationInputItem>) -> Self {
        Self::Items(value)
    }
}

/// EN: Multimodal moderation input item.
/// 中文:多模态 moderation 输入项。
#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
#[serde(tag = "type")]
#[non_exhaustive]
pub enum ModerationInputItem {
    /// EN: Text input item.
    /// 中文:文本输入项。
    #[serde(rename = "text")]
    Text {
        /// EN: Text content.
        /// 中文:文本内容。
        text: String,
    },
    /// EN: Image URL input item.
    /// 中文:图片 URL 输入项。
    #[serde(rename = "image_url")]
    ImageUrl {
        /// EN: Image URL object.
        /// 中文:图片 URL 对象。
        image_url: ModerationImageUrl,
    },
}

impl ModerationInputItem {
    /// EN: Creates a text input item.
    /// 中文:创建文本输入项。
    pub fn text(text: impl Into<String>) -> Self {
        Self::Text { text: text.into() }
    }

    /// EN: Creates an image URL input item.
    /// 中文:创建图片 URL 输入项。
    pub fn image_url(url: impl Into<String>) -> Self {
        Self::ImageUrl {
            image_url: ModerationImageUrl { url: url.into() },
        }
    }

    fn is_empty(&self) -> bool {
        match self {
            Self::Text { text } => text.is_empty(),
            Self::ImageUrl { image_url } => image_url.url.is_empty(),
        }
    }
}

/// EN: Image URL object used by moderation input items.
/// 中文:moderation 输入项使用的图片 URL 对象。
#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct ModerationImageUrl {
    /// EN: Publicly accessible image URL.
    /// 中文:可公开访问的图片 URL。
    pub url: String,
}

/// EN: Response object returned by the Moderations API.
/// 中文:Moderations API 返回的响应对象。
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[non_exhaustive]
pub struct ModerationCreateResponse {
    /// EN: Moderation response id.
    /// 中文:moderation 响应 ID。
    pub id: String,
    /// EN: Model used to classify the input.
    /// 中文:用于分类输入的模型。
    pub model: String,
    /// EN: Moderation results.
    /// 中文:moderation 结果。
    #[serde(default)]
    pub results: Vec<ModerationResult>,
    /// EN: OpenAI request id from response headers.
    /// 中文:响应头中的 OpenAI 请求 ID。
    #[serde(skip)]
    request_id: Option<RequestId>,
    /// EN: Additional fields preserved for forward compatibility.
    /// 中文:为前向兼容保留的额外字段。
    #[serde(flatten)]
    pub extra: BTreeMap<String, Value>,
}

impl ModerationCreateResponse {
    pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
        self.request_id = request_id;
        self
    }

    /// EN: Returns the OpenAI request id, when present.
    /// 中文:返回 OpenAI 请求 ID,如存在。
    pub fn request_id(&self) -> Option<&RequestId> {
        self.request_id.as_ref()
    }
}

/// EN: Single moderation result.
/// 中文:单个 moderation 结果。
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[non_exhaustive]
pub struct ModerationResult {
    /// EN: Whether the input was flagged by any moderation category.
    /// 中文:输入是否被任一 moderation 类别标记。
    pub flagged: bool,
    /// EN: Category decisions keyed by OpenAI category name.
    /// 中文:以 OpenAI 类别名为键的类别判定。
    #[serde(default)]
    pub categories: BTreeMap<String, bool>,
    /// EN: Category scores keyed by OpenAI category name.
    /// 中文:以 OpenAI 类别名为键的类别分数。
    #[serde(default)]
    pub category_scores: BTreeMap<String, f64>,
    /// EN: Input types considered for each category.
    /// 中文:每个类别适用的输入类型。
    #[serde(default)]
    pub category_applied_input_types: BTreeMap<String, Vec<String>>,
    /// EN: Additional fields preserved for forward compatibility.
    /// 中文:为前向兼容保留的额外字段。
    #[serde(flatten)]
    pub extra: BTreeMap<String, Value>,
}