use std::{borrow::Cow, collections::HashMap};
use serde::{Deserialize, Serialize};
use serde_json::json;
#[derive(Clone, Serialize, Default, Debug, Deserialize, PartialEq)]
pub struct RequestBody {
pub model: String,
pub input: Vec<InputItem>,
#[serde(skip_serializing_if = "Option::is_none")]
pub previous_response_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include: Option<Vec<Includable>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub modalities: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_output_tokens: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub n: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub verbosity: Option<Verbosity>,
#[serde(skip_serializing_if = "Option::is_none")]
pub instructions: Option<Instructions>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prediction: Option<PredictionConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub audio: Option<AudioConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning: Option<ReasoningConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub temperature: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub top_p: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub logprobs: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub top_logprobs: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub frequency_penalty: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub presence_penalty: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub logit_bias: Option<HashMap<String, serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub user: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub seed: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop: Option<Stop>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream_options: Option<StreamOptions>,
#[serde(skip_serializing_if = "Option::is_none")]
pub response_format: Option<ResponseFormat>,
#[serde(skip_serializing_if = "Option::is_none")]
pub store: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<HashMap<String, serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parallel_tool_calls: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<Vec<Tool>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_choice: Option<ToolChoice>,
#[serde(skip_serializing_if = "Option::is_none")]
pub service_tier: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub web_search_options: Option<WebSearchOptions>,
#[serde(skip_serializing_if = "Option::is_none")]
pub openrouter_reasoning: Option<OpenRouterReasoning>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prompt_cache_key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub truncation: Option<Truncation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<serde_json::Value>,
}
impl RequestBody {
pub fn from_text(model: impl Into<String>, text: impl Into<String>) -> Self {
let input = vec![InputItem::text(Role::User, text)];
Self {
model: model.into(),
input,
..Default::default()
}
}
pub fn first_user_input(&self) -> Option<&InputItem> {
self.input.iter().find(|message| message.role == Role::User)
}
pub fn first_user_text(&self) -> Option<String> {
self.first_user_input()?.first_text()
}
}
#[derive(Clone, Serialize, Default, Debug, Deserialize, PartialEq)]
pub struct InputItem {
pub role: Role,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub content: Vec<ContentPart>,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
impl InputItem {
pub fn new(role: Role, content: Vec<ContentPart>) -> Self {
Self {
role,
content,
..Default::default()
}
}
pub fn text(role: Role, text: impl Into<String>) -> Self {
let content = vec![json!({
"type": "input_text",
"text": text.into()
})];
Self::new(role, content)
}
pub fn first_text(&self) -> Option<String> {
self.content.iter().find_map(|part| {
part.get("text")
.and_then(|value| value.as_str())
.map(|text| text.to_string())
})
}
}
pub type ContentPart = serde_json::Value;
#[derive(Clone, Serialize, Debug, Deserialize, PartialEq, Eq)]
pub enum Includable {
#[serde(rename = "file_search_call.results")]
FileSearchCallResults,
#[serde(rename = "message.input_image.image_url")]
MessageInputImageUrl,
#[serde(rename = "computer_call_output.output.image_url")]
ComputerCallOutputImageUrl,
}
#[derive(Clone, Copy, Serialize, Default, Debug, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Role {
System,
Developer,
#[default]
User,
Assistant,
Tool,
}
#[derive(Clone, Serialize, Debug, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum Instructions {
Text(String),
Content(Vec<ContentPart>),
}
#[derive(Clone, Serialize, Debug, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum Stop {
String(String),
Array(Vec<String>),
}
#[derive(Clone, Serialize, Debug, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Tool {
Function(FunctionTool),
FileSearch(FileSearchTool),
#[serde(rename = "web_search_preview")]
WebSearchPreview(WebSearchTool),
#[serde(rename = "web_search_preview_2025_03_11")]
WebSearchPreview20250311(WebSearchTool),
#[serde(rename = "computer_use_preview")]
ComputerUsePreview(ComputerUseTool),
}
impl Default for Tool {
fn default() -> Self {
Tool::Function(FunctionTool::default())
}
}
#[derive(Clone, Serialize, Default, Debug, Deserialize, PartialEq)]
pub struct FunctionTool {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<Cow<'static, str>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parameters: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub strict: Option<bool>,
}
#[derive(Clone, Serialize, Default, Debug, Deserialize, PartialEq)]
pub struct FileSearchTool {
pub vector_store_ids: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_num_results: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ranking_options: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub filters: Option<serde_json::Value>,
}
#[derive(Clone, Serialize, Default, Debug, Deserialize, PartialEq)]
pub struct WebSearchTool {
#[serde(skip_serializing_if = "Option::is_none")]
pub user_location: Option<WebSearchUserLocation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub search_context_size: Option<SearchContextSize>,
}
#[derive(Clone, Serialize, Debug, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum SearchContextSize {
Low,
Medium,
High,
}
#[derive(Clone, Serialize, Debug, Deserialize, PartialEq)]
pub struct ComputerUseTool {
pub environment: ComputerEnvironment,
pub display_width: u32,
pub display_height: u32,
}
#[derive(Clone, Serialize, Debug, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ComputerEnvironment {
Windows,
Mac,
Linux,
Ubuntu,
Browser,
}
#[derive(Clone, Serialize, Default, Debug, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum ToolType {
#[default]
Function,
FileSearch,
#[serde(rename = "web_search_preview")]
WebSearchPreview,
#[serde(rename = "computer_use_preview")]
ComputerUsePreview,
}
#[derive(Clone, Serialize, Default, Debug, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum ToolChoice {
#[default]
Auto,
None,
Tool(ToolChoiceFunction),
}
#[derive(Clone, Serialize, Default, Debug, Deserialize, PartialEq)]
pub struct ToolChoiceFunction {
pub r#type: ToolType,
pub function: FunctionName,
}
#[derive(Clone, Serialize, Default, Debug, Deserialize, PartialEq)]
pub struct FunctionName {
pub name: String,
}
#[derive(Debug, Deserialize, Default, Serialize, Clone, PartialEq)]
#[serde(rename_all = "snake_case")]
#[serde(tag = "type")]
pub enum ResponseFormat {
#[default]
Text,
JsonObject,
JsonSchema {
description: Option<String>,
properties: Option<serde_json::Value>,
name: String,
strict: Option<bool>,
},
}
#[derive(Debug, Deserialize, Default, Serialize, Clone, PartialEq)]
pub struct StreamOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub include_usage: Option<bool>,
}
#[derive(Debug, Deserialize, Default, Serialize, Clone, PartialEq)]
pub struct PredictionConfig {
pub text: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub logprobs: Option<Vec<f32>>,
}
#[derive(Debug, Deserialize, Default, Serialize, Clone, PartialEq)]
pub struct AudioConfig {
pub voice: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub speed: Option<f32>,
}
#[derive(Debug, Deserialize, Default, Serialize, Clone, PartialEq)]
pub struct WebSearchOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub search_context_size: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub user_location: Option<WebSearchUserLocation>,
}
#[derive(Debug, Deserialize, Default, Serialize, Clone, PartialEq)]
pub struct WebSearchUserLocation {
#[serde(default = "default_approximate_type")]
pub r#type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub city: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub country: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub region: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub timezone: Option<String>,
}
fn default_approximate_type() -> String {
"approximate".to_string()
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ReasoningEffort {
High,
Medium,
Low,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ReasoningSummary {
Auto,
Concise,
Detailed,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Truncation {
Auto,
Disabled,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Verbosity {
Low,
Medium,
High,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub struct OpenRouterReasoning {
pub effort: ReasoningEffort,
pub exclude: bool,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
pub struct ReasoningConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub effort: Option<ReasoningEffort>,
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<ReasoningSummary>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn create_simple_text_request() {
let request = RequestBody::from_text("gpt-4.1-mini", "Hello world");
assert_eq!(request.model, "gpt-4.1-mini");
assert_eq!(request.input.len(), 1);
let first = request.first_user_input().unwrap();
assert_eq!(first.role, Role::User);
assert_eq!(first.first_text().as_deref(), Some("Hello world"));
}
}