use std::collections::HashMap;
use derive_builder::Builder;
use futures_util::{StreamExt, stream::BoxStream};
use reqwest::Client as HttpClient;
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use crate::{
api::chat::{CacheControl, DebugOptions, Plugin, TraceOptions},
error::OpenRouterError,
strip_option_map_setter, strip_option_vec_setter,
transport::{
request as transport_request, response as transport_response, sse::response_lines,
},
types::{OpenRouterExperimentalMetadata, ProviderPreferences},
utils::parse_sse_frames,
};
#[derive(Serialize, Deserialize, Debug, Clone, Builder)]
#[builder(build_fn(error = "OpenRouterError"))]
#[non_exhaustive]
pub struct ResponsesRequest {
#[builder(setter(strip_option), default)]
#[serde(skip_serializing_if = "Option::is_none")]
input: Option<Value>,
#[builder(setter(into, strip_option), default)]
#[serde(skip_serializing_if = "Option::is_none")]
instructions: Option<String>,
#[builder(setter(custom), default)]
#[serde(skip_serializing_if = "Option::is_none")]
metadata: Option<HashMap<String, String>>,
#[builder(setter(custom), default)]
#[serde(skip_serializing_if = "Option::is_none")]
tools: Option<Vec<Value>>,
#[builder(setter(strip_option), default)]
#[serde(skip_serializing_if = "Option::is_none")]
tool_choice: Option<Value>,
#[builder(setter(strip_option), default)]
#[serde(skip_serializing_if = "Option::is_none")]
parallel_tool_calls: Option<bool>,
#[builder(setter(into, strip_option), default)]
#[serde(skip_serializing_if = "Option::is_none")]
model: Option<String>,
#[builder(setter(custom), default)]
#[serde(skip_serializing_if = "Option::is_none")]
models: Option<Vec<String>>,
#[builder(setter(strip_option), default)]
#[serde(skip_serializing_if = "Option::is_none")]
text: Option<Value>,
#[builder(setter(strip_option), default)]
#[serde(skip_serializing_if = "Option::is_none")]
reasoning: Option<Value>,
#[builder(setter(strip_option), default)]
#[serde(skip_serializing_if = "Option::is_none")]
max_output_tokens: Option<u32>,
#[builder(setter(strip_option), default)]
#[serde(skip_serializing_if = "Option::is_none")]
temperature: Option<f64>,
#[builder(setter(strip_option), default)]
#[serde(skip_serializing_if = "Option::is_none")]
top_p: Option<f64>,
#[builder(setter(strip_option), default)]
#[serde(skip_serializing_if = "Option::is_none")]
top_logprobs: Option<u32>,
#[builder(setter(strip_option), default)]
#[serde(skip_serializing_if = "Option::is_none")]
max_tool_calls: Option<u32>,
#[builder(setter(strip_option), default)]
#[serde(skip_serializing_if = "Option::is_none")]
presence_penalty: Option<f64>,
#[builder(setter(strip_option), default)]
#[serde(skip_serializing_if = "Option::is_none")]
frequency_penalty: Option<f64>,
#[builder(setter(strip_option), default)]
#[serde(skip_serializing_if = "Option::is_none")]
top_k: Option<f64>,
#[builder(setter(custom), default)]
#[serde(skip_serializing_if = "Option::is_none")]
image_config: Option<HashMap<String, Value>>,
#[builder(setter(custom), default)]
#[serde(skip_serializing_if = "Option::is_none")]
modalities: Option<Vec<String>>,
#[builder(setter(into, strip_option), default)]
#[serde(skip_serializing_if = "Option::is_none")]
prompt_cache_key: Option<String>,
#[builder(setter(into, strip_option), default)]
#[serde(skip_serializing_if = "Option::is_none")]
previous_response_id: Option<String>,
#[builder(setter(strip_option), default)]
#[serde(skip_serializing_if = "Option::is_none")]
prompt: Option<Value>,
#[builder(setter(custom), default)]
#[serde(skip_serializing_if = "Option::is_none")]
include: Option<Vec<String>>,
#[builder(setter(strip_option), default)]
#[serde(skip_serializing_if = "Option::is_none")]
background: Option<bool>,
#[builder(setter(into, strip_option), default)]
#[serde(skip_serializing_if = "Option::is_none")]
safety_identifier: Option<String>,
#[builder(setter(strip_option), default)]
#[serde(skip_serializing_if = "Option::is_none")]
store: Option<bool>,
#[builder(setter(into, strip_option), default)]
#[serde(skip_serializing_if = "Option::is_none")]
service_tier: Option<String>,
#[builder(setter(into, strip_option), default)]
#[serde(skip_serializing_if = "Option::is_none")]
truncation: Option<String>,
#[builder(setter(skip), default)]
#[serde(skip_serializing_if = "Option::is_none")]
stream: Option<bool>,
#[builder(setter(strip_option), default)]
#[serde(skip)]
experimental_metadata: Option<OpenRouterExperimentalMetadata>,
#[builder(setter(strip_option), default)]
#[serde(skip_serializing_if = "Option::is_none")]
provider: Option<ProviderPreferences>,
#[builder(setter(custom), default)]
#[serde(skip_serializing_if = "Option::is_none")]
plugins: Option<Vec<Plugin>>,
#[builder(setter(into, strip_option), default)]
#[serde(skip_serializing_if = "Option::is_none")]
route: Option<String>,
#[builder(setter(into, strip_option), default)]
#[serde(skip_serializing_if = "Option::is_none")]
user: Option<String>,
#[builder(setter(into, strip_option), default)]
#[serde(skip_serializing_if = "Option::is_none")]
session_id: Option<String>,
#[builder(setter(strip_option), default)]
#[serde(skip_serializing_if = "Option::is_none")]
cache_control: Option<CacheControl>,
#[builder(setter(strip_option), default)]
#[serde(skip_serializing_if = "Option::is_none")]
trace: Option<TraceOptions>,
#[builder(setter(strip_option), default)]
#[serde(skip_serializing_if = "Option::is_none")]
debug: Option<DebugOptions>,
}
impl ResponsesRequestBuilder {
strip_option_map_setter!(metadata, String, String);
strip_option_vec_setter!(tools, Value);
strip_option_vec_setter!(models, String);
strip_option_map_setter!(image_config, String, Value);
strip_option_vec_setter!(modalities, String);
strip_option_vec_setter!(include, String);
strip_option_vec_setter!(plugins, Plugin);
pub fn server_tool(&mut self, tool: crate::types::ServerTool) -> &mut Self {
if let Some(Some(ref mut existing_tools)) = self.tools {
existing_tools.push(Value::from(tool));
} else {
self.tools = Some(Some(vec![Value::from(tool)]));
}
self
}
pub fn server_tools<T>(&mut self, tools: T) -> &mut Self
where
T: IntoIterator<Item = crate::types::ServerTool>,
{
for tool in tools {
self.server_tool(tool);
}
self
}
pub fn force_server_tool(&mut self, tool_type: impl Into<String>) -> &mut Self {
self.tool_choice = Some(Some(responses_server_tool_choice(tool_type.into())));
self
}
}
fn responses_server_tool_choice(tool_type: String) -> Value {
match tool_type.as_str() {
"web_search_preview" | "web_search_preview_2025_03_11" | "apply_patch" | "shell" => {
json!({"type": tool_type})
}
_ => json!({
"type": "allowed_tools",
"mode": "required",
"tools": [{"type": tool_type}]
}),
}
}
impl ResponsesRequest {
pub fn builder() -> ResponsesRequestBuilder {
ResponsesRequestBuilder::default()
}
pub fn new(model: impl Into<String>, input: Value) -> Self {
Self::builder()
.model(model.into())
.input(input)
.build()
.expect("Failed to build ResponsesRequest")
}
fn stream(&self, stream: bool) -> Self {
let mut req = self.clone();
req.stream = Some(stream);
req
}
pub fn experimental_metadata(&self) -> Option<OpenRouterExperimentalMetadata> {
self.experimental_metadata
}
pub(crate) fn requires_openrouter_files_tool_header(&self) -> bool {
self.tools.as_deref().is_some_and(|tools| {
tools
.iter()
.any(crate::types::ServerTool::is_files_tool_value)
})
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[non_exhaustive]
pub struct ResponsesResponse {
pub id: Option<String>,
#[serde(rename = "object")]
pub object_type: Option<String>,
pub created_at: Option<u64>,
pub model: Option<String>,
pub status: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output: Option<Vec<Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub usage: Option<Value>,
#[serde(flatten)]
pub extra: HashMap<String, Value>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[non_exhaustive]
pub struct ResponsesStreamEvent {
#[serde(rename = "type")]
pub event_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub sequence_number: Option<u64>,
#[serde(flatten)]
pub data: HashMap<String, Value>,
}
pub async fn create_response(
base_url: &str,
api_key: &str,
x_title: &Option<String>,
http_referer: &Option<String>,
app_categories: &Option<Vec<String>>,
request: &ResponsesRequest,
) -> Result<ResponsesResponse, OpenRouterError> {
let http_client = crate::transport::new_client()?;
create_response_with_client(
&http_client,
base_url,
api_key,
x_title,
http_referer,
app_categories,
request,
)
.await
}
pub(crate) async fn create_response_with_client(
http_client: &HttpClient,
base_url: &str,
api_key: &str,
x_title: &Option<String>,
http_referer: &Option<String>,
app_categories: &Option<Vec<String>>,
request: &ResponsesRequest,
) -> Result<ResponsesResponse, OpenRouterError> {
let url = format!("{base_url}/responses");
let request = request.stream(false);
let request_builder = transport_request::with_experimental_metadata_header(
transport_request::with_client_request_headers(
transport_request::post(http_client, &url),
api_key,
x_title,
http_referer,
app_categories,
)?,
&request.experimental_metadata,
);
let request_builder = transport_request::with_openrouter_files_tool_header(
request_builder,
request.requires_openrouter_files_tool_header(),
);
let response = request_builder.json(&request).send().await?;
if response.status().is_success() {
let response_data: ResponsesResponse =
transport_response::parse_json_response(response, "responses API").await?;
Ok(response_data)
} else {
transport_response::handle_error(response).await?;
unreachable!()
}
}
pub async fn stream_response(
base_url: &str,
api_key: &str,
x_title: &Option<String>,
http_referer: &Option<String>,
app_categories: &Option<Vec<String>>,
request: &ResponsesRequest,
) -> Result<BoxStream<'static, Result<ResponsesStreamEvent, OpenRouterError>>, OpenRouterError> {
let http_client = crate::transport::new_client()?;
stream_response_with_client(
&http_client,
base_url,
api_key,
x_title,
http_referer,
app_categories,
request,
)
.await
}
pub(crate) async fn stream_response_with_client(
http_client: &HttpClient,
base_url: &str,
api_key: &str,
x_title: &Option<String>,
http_referer: &Option<String>,
app_categories: &Option<Vec<String>>,
request: &ResponsesRequest,
) -> Result<BoxStream<'static, Result<ResponsesStreamEvent, OpenRouterError>>, OpenRouterError> {
let url = format!("{base_url}/responses");
let request = request.stream(true);
let request_builder = transport_request::with_experimental_metadata_header(
transport_request::with_client_request_headers(
transport_request::post(http_client, &url),
api_key,
x_title,
http_referer,
app_categories,
)?,
&request.experimental_metadata,
);
let request_builder = transport_request::with_openrouter_files_tool_header(
request_builder,
request.requires_openrouter_files_tool_header(),
);
let response = request_builder.json(&request).send().await?;
if response.status().is_success() {
let lines = parse_sse_frames(response_lines(response))
.filter_map(async |line| match line {
Ok(frame) if frame.data == "[DONE]" => None,
Ok(frame) => Some(
serde_json::from_str::<ResponsesStreamEvent>(&frame.data)
.map_err(OpenRouterError::Serialization),
),
Err(error) => Some(Err(error)),
})
.boxed();
Ok(lines)
} else {
transport_response::handle_error(response).await?;
unreachable!()
}
}