openai4rs 0.1.8

A non-official Rust crate for calling the OpenAI service
Documentation
use super::types::{
    ChatCompletionMessageParam, ChatCompletionPredictionContentParam, ChatCompletionToolParam,
    Modality, ReasoningEffort, ToolChoice,
};
use crate::common::types::{Bodies, Headers, QueryParams, ServiceTier};
use derive_builder::Builder;
use serde::Serialize;
use std::collections::HashMap;
/// Parameters for creating model responses for chat conversations.
///
/// This struct represents the request parameters for the OpenAI chat completion API,
/// supporting text generation, vision, and audio capabilities. Parameters support
/// may vary depending on the model used, especially for newer reasoning models.
#[derive(Debug, Clone, Serialize, Builder)]
#[builder(
    name = "RequestParamsBuilder",
    derive(Debug),
    pattern = "owned",
    setter(strip_option)
)]
pub struct RequestParams<'a> {
    /// The ID of the model to use for generating responses, such as `gpt-4o` or `o1`.
    ///
    /// OpenAI provides multiple models with different capabilities,
    /// performance characteristics, and pricing.
    pub model: &'a str,

    /// The list of messages that make up the conversation so far.
    ///
    /// Depending on the model you're using, different message types (modalities)
    /// are supported, such as text, images, and audio.
    pub messages: &'a Vec<ChatCompletionMessageParam>,

    /// A number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far,
    /// decreasing the model's likelihood to repeat the same line verbatim.
    #[builder(default)]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub frequency_penalty: Option<f32>,

    /// Modifies the likelihood of specified tokens appearing in the completion.
    ///
    /// Accepts a JSON object that maps tokens (specified by their token ID in the tokenizer)
    /// to an associated bias value from -100 to 100. Mathematically, the bias is added to the logits generated by the model before sampling.
    #[builder(default)]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub logit_bias: Option<HashMap<String, i32>>,

    /// Whether to return log probabilities of the output tokens.
    ///
    /// If true, returns the log probabilities of each output token in the `content` of `message`.
    #[builder(default)]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub logprobs: Option<bool>,

    /// The type of output you want the model to generate.
    ///
    /// Most models are capable of generating text, which is the default: `["text"]`.
    /// The `gpt-4o-audio-preview` model can also generate audio. To request both
    /// text and audio responses, use: `["text", "audio"]`.
    #[builder(default)]
    #[serde(skip_serializing_if = "skip_if_option_vec_empty")]
    pub modalities: Option<Vec<Modality>>,

    /// An upper bound on the number of tokens that can be generated for a completion,
    ///
    /// including visible output tokens and reasoning tokens.
    #[builder(default)]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub max_completion_tokens: Option<i32>,

    /// The maximum number of tokens that can be generated in the chat completion.
    ///
    /// This value can be used to control the cost of text generated via the API.
    /// Applicable to the o1 series models.
    #[builder(default)]
    #[serde(skip_serializing_if = "Option::is_none")]
    #[deprecated(note = "Use `max_completion_tokens` instead")]
    pub max_tokens: Option<i32>,

    /// A set of up to 16 key-value pairs that can be attached to an object.
    ///
    /// This is useful for storing additional information about the object in a structured format.
    /// Keys have a maximum length of 64 characters, and values have a maximum length of 512 characters.
    #[builder(default)]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub metadata: Option<HashMap<String, String>>,

    /// Whether to enable parallel function calls during tool use.
    #[builder(default)]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub parallel_tool_calls: Option<bool>,

    /// A number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far,
    /// increasing the model's likelihood to talk about new topics.
    #[builder(default)]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub presence_penalty: Option<f32>,

    /// How many chat completion choices to generate for each input message.
    ///
    /// Note that you will be charged based on the total number of tokens generated across all choices.
    /// Keep `n` at `1` to minimize costs.
    #[builder(default)]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub n: Option<i32>,

    /// An alternative to sampling with temperature, called nucleus sampling.
    ///
    /// The model considers the results of the tokens with top_p probability mass.
    /// So 0.1 means only the tokens comprising the top 10% probability mass are considered.
    /// We generally recommend altering this parameter or `temperature` but not both.
    #[builder(default)]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub top_p: Option<f32>,

    /// If set to true, the model response data will be streamed to the client using server-sent events
    /// as it is generated.
    ///
    /// For more information on how to handle streaming events, see the streaming response guide.
    #[builder(default)]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub stream: Option<bool>,

    /// Up to 4 sequences where the API will stop generating further tokens.
    ///
    /// The returned text will not contain the stop sequence.
    #[builder(default)]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub send: Option<i32>,

    /// What sampling temperature to use, between 0 and 2.
    ///
    /// Higher values like 0.8 will make the output more random, while lower values like 0.2
    /// will make it more focused and deterministic. We generally recommend altering this parameter or `top_p`,
    /// but not both.
    #[builder(default)]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub temperature: Option<f32>,

    /// A unique identifier representing your end-user, which can help OpenAI
    /// monitor and detect abuse.
    #[builder(default)]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub user: Option<String>,

    /// An integer between 0 and 20 specifying the number of most likely tokens to return at each token position,
    /// each with an associated log probability.
    /// If this parameter is used, `logprobs` must be set to `true`.
    #[builder(default)]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub top_logprobs: Option<i32>,

    /// Static prediction output content, such as the content of a text file
    /// that is being regenerated.
    #[builder(default)]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub prediction: Option<ChatCompletionPredictionContentParam>,

    /// **o-series models only** - Limit the reasoning workload of the reasoning model.
    ///
    /// Currently supported values are `low`, `medium`, and `high`. Reducing the reasoning workload
    /// can speed up response times and reduce the number of tokens used for reasoning in the response.
    #[builder(default)]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub reasoning_effort: Option<ReasoningEffort>,

    /// Specifies the latency tier used to process the request.
    ///
    /// This parameter is relevant to customers subscribed to the scale tier service.
    /// - If set to 'auto' and the project has scale tier enabled, the system will
    ///   utilize scale tier credits until they are exhausted.
    /// - If set to 'default', the request will be processed using the default service
    ///   tier, which has a lower uptime SLA and no latency guarantees.
    #[builder(default)]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub service_tier: Option<ServiceTier>,

    /// A list of tools the model may call. Currently, only functions are supported as tools.
    ///
    /// Use this parameter to provide a list of functions that the model may generate JSON input for.
    /// Up to 128 functions are supported.
    #[builder(default)]
    #[serde(skip_serializing_if = "skip_if_option_vec_empty")]
    pub tools: Option<Vec<ChatCompletionToolParam>>,

    /// Controls which (if any) tool the model calls.
    ///
    /// - `none` means the model will not call any tools and instead generates a message.
    /// - `auto` means the model can choose between generating a message or calling one or more tools.
    /// - `required` means the model must call one or more tools.
    /// - Specifying a particular tool forces the model to call that tool.
    ///
    /// Defaults to `none` when no tools are present. Defaults to `auto` if tools are present.
    #[builder(default)]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tool_choice: Option<ToolChoice>,

    /// Send additional headers with the request.
    ///
    #[builder(default)]
    #[serde(skip_serializing)]
    pub extra_headers: Option<Headers>,

    /// Add additional query parameters to the request.
    ///
    #[builder(default)]
    #[serde(skip_serializing)]
    pub extra_query: Option<QueryParams>,

    /// Add additional JSON properties to the request.
    ///
    /// This field will not be serialized in the request body.
    #[builder(default)]
    #[serde(skip_serializing)]
    pub extra_body: Option<Bodies>,

    /// HTTP request retry count, overriding the client's global setting.
    ///
    /// This field will not be serialized in the request body.
    #[builder(default)]
    #[serde(skip_serializing)]
    pub retry_count: Option<u32>,

    /// HTTP request timeout in seconds, overriding the client's global setting.
    ///
    /// This field will not be serialized in the request body.
    #[builder(default)]
    #[serde(skip_serializing)]
    pub timeout_seconds: Option<u64>,

    /// HTTP request User-Agent, overriding the client's global setting.
    ///
    /// This field will not be serialized in the request body.
    #[builder(default)]
    #[serde(skip_serializing)]
    pub user_agent: Option<String>,
}

pub fn chat_request<'a>(
    model: &'a str,
    messages: &'a Vec<ChatCompletionMessageParam>,
) -> RequestParamsBuilder<'a> {
    RequestParamsBuilder::create_empty()
        .model(model)
        .messages(messages)
}

pub trait IntoRequestParams<'a> {
    fn into_request_params(self) -> RequestParams<'a>;
}

impl<'a> IntoRequestParams<'a> for RequestParams<'a> {
    fn into_request_params(self) -> RequestParams<'a> {
        self
    }
}

impl<'a> IntoRequestParams<'a> for RequestParamsBuilder<'a> {
    fn into_request_params(self) -> RequestParams<'a> {
        self.build().unwrap()
    }
}

impl RequestParamsBuilder<'_> {
    /// Adds an HTTP header to the request.
    /// This allows adding custom headers to the API request, such as authentication tokens or custom metadata.
    pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
        let headers_map = self
            .extra_headers
            .get_or_insert_with(|| Some(HashMap::new()))
            .get_or_insert_with(HashMap::new);
        headers_map.insert(key.into(), value.into());
        self
    }

    /// Adds a query parameter to the request.
    /// This allows adding custom query parameters to the API request URL, such as additional filtering or configuration options.
    pub fn query(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
        let query_map = self
            .extra_query
            .get_or_insert_with(|| Some(HashMap::new()))
            .get_or_insert_with(HashMap::new);
        query_map.insert(key.into(), value.into());
        self
    }

    /// Adds a field to the request body.
    /// This allows adding custom fields to the JSON request body, such as additional parameters not directly supported by the builder.
    pub fn body(mut self, key: impl Into<String>, value: impl Into<serde_json::Value>) -> Self {
        let body_map = self
            .extra_body
            .get_or_insert_with(|| Some(HashMap::new()))
            .get_or_insert_with(HashMap::new);
        body_map.insert(key.into(), value.into());
        self
    }
}

fn skip_if_option_vec_empty<T>(opt: &Option<Vec<T>>) -> bool
where
    T: std::fmt::Debug,
{
    opt.as_ref().is_none_or(Vec::is_empty)
}