use crate::{
client::OpenRouterClient,
error::{OpenRouterError, Result},
types::{Plugin, ProviderPreferences, Usage},
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum InputItem {
#[serde(rename = "message")]
Message {
role: InputRole,
content: InputContent,
},
#[serde(rename = "function_call")]
FunctionCall {
call_id: String,
name: String,
arguments: String,
id: String,
#[serde(skip_serializing_if = "Option::is_none")]
status: Option<String>,
},
#[serde(rename = "function_call_output")]
FunctionCallOutput {
#[serde(skip_serializing_if = "Option::is_none")]
id: Option<String>,
call_id: String,
output: String,
#[serde(skip_serializing_if = "Option::is_none")]
status: Option<String>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum InputRole {
User,
System,
Assistant,
Developer,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum InputContent {
String(String),
Array(Vec<InputContentPart>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum InputContentPart {
#[serde(rename = "input_text")]
Text { text: String },
#[serde(rename = "input_image")]
Image {
#[serde(skip_serializing_if = "Option::is_none")]
detail: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
image_url: Option<String>,
},
#[serde(rename = "input_file")]
File {
#[serde(skip_serializing_if = "Option::is_none")]
file_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
file_data: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
filename: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
file_url: Option<String>,
},
#[serde(rename = "input_audio")]
Audio {
input_audio: AudioInput,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AudioInput {
pub data: String,
pub format: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum OutputItem {
#[serde(rename = "message")]
Message {
id: String,
role: String,
content: Vec<OutputContent>,
status: String,
},
#[serde(rename = "reasoning")]
Reasoning {
id: String,
#[serde(skip_serializing_if = "Option::is_none")]
content: Option<Vec<ReasoningContent>>,
summary: Vec<ReasoningSummary>,
#[serde(skip_serializing_if = "Option::is_none")]
status: Option<String>,
},
#[serde(rename = "function_call")]
FunctionCall {
id: String,
name: String,
arguments: String,
call_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
status: Option<String>,
},
#[serde(rename = "web_search_call")]
WebSearchCall {
id: String,
status: String,
},
#[serde(rename = "file_search_call")]
FileSearchCall {
id: String,
queries: Vec<String>,
status: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum OutputContent {
#[serde(rename = "output_text")]
Text {
text: String,
#[serde(skip_serializing_if = "Option::is_none")]
annotations: Option<Vec<Value>>,
},
#[serde(rename = "refusal")]
Refusal { refusal: String },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum ReasoningContent {
#[serde(rename = "reasoning_text")]
Text { text: String },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum ReasoningSummary {
#[serde(rename = "summary_text")]
Text { text: String },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResponsesRequest {
pub input: Vec<InputItem>,
pub model: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub instructions: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<Vec<Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_choice: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parallel_tool_calls: Option<bool>,
#[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 max_output_tokens: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub presence_penalty: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub frequency_penalty: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning: Option<ReasoningConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub provider: Option<ProviderPreferences>,
#[serde(skip_serializing_if = "Option::is_none")]
pub plugins: Option<Vec<Plugin>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub user: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReasoningConfig {
pub effort: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_tokens: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResponsesResponse {
pub id: String,
pub object: String,
pub created_at: f64,
pub model: String,
pub status: String,
pub output: Vec<OutputItem>,
#[serde(skip_serializing_if = "Option::is_none")]
pub usage: Option<Usage>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output_text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<ResponseError>,
#[serde(skip_serializing_if = "Option::is_none")]
pub incomplete_details: Option<IncompleteDetails>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResponseError {
pub code: String,
pub message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IncompleteDetails {
pub reason: String,
}
pub struct ResponsesRequestBuilder {
request: ResponsesRequest,
}
impl ResponsesRequestBuilder {
pub fn new(model: impl Into<String>) -> Self {
Self {
request: ResponsesRequest {
input: Vec::new(),
model: model.into(),
instructions: None,
tools: None,
tool_choice: None,
parallel_tool_calls: None,
temperature: None,
top_p: None,
max_output_tokens: None,
presence_penalty: None,
frequency_penalty: None,
reasoning: None,
stream: None,
provider: None,
plugins: None,
user: None,
session_id: None,
},
}
}
pub fn user_message(mut self, content: impl Into<String>) -> Self {
self.request.input.push(InputItem::Message {
role: InputRole::User,
content: InputContent::String(content.into()),
});
self
}
pub fn system_message(mut self, content: impl Into<String>) -> Self {
self.request.input.push(InputItem::Message {
role: InputRole::System,
content: InputContent::String(content.into()),
});
self
}
pub fn assistant_message(mut self, content: impl Into<String>) -> Self {
self.request.input.push(InputItem::Message {
role: InputRole::Assistant,
content: InputContent::String(content.into()),
});
self
}
pub fn temperature(mut self, temp: f32) -> Self {
self.request.temperature = Some(temp);
self
}
pub fn reasoning(mut self, effort: impl Into<String>) -> Self {
self.request.reasoning = Some(ReasoningConfig {
effort: effort.into(),
summary: None,
max_tokens: None,
enabled: Some(true),
});
self
}
pub fn stream(mut self, stream: bool) -> Self {
self.request.stream = Some(stream);
self
}
pub fn build(self) -> ResponsesRequest {
self.request
}
}
impl OpenRouterClient {
pub async fn create_response(
&self,
request: ResponsesRequest,
) -> Result<ResponsesResponse> {
let url = format!("{}/responses", self.base_url);
let headers = self.build_headers()?;
let response = self
.client
.post(&url)
.headers(headers)
.json(&request)
.send()
.await
.map_err(OpenRouterError::HttpError)?;
let status = response.status();
if !status.is_success() {
let error_text = response.text().await.unwrap_or_default();
return Err(OpenRouterError::ApiError {
code: status.as_u16(),
message: error_text,
});
}
let result = response
.json::<ResponsesResponse>()
.await
.map_err(OpenRouterError::HttpError)?;
Ok(result)
}
}