Skip to main content

openrouter_rs/api/
responses.rs

1use std::collections::HashMap;
2
3use derive_builder::Builder;
4use futures_util::{StreamExt, stream::BoxStream};
5use reqwest::Client as HttpClient;
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9use crate::{
10    api::chat::{CacheControl, DebugOptions, Plugin, TraceOptions},
11    error::OpenRouterError,
12    strip_option_map_setter, strip_option_vec_setter,
13    transport::{
14        request as transport_request, response as transport_response, sse::response_lines,
15    },
16    types::{OpenRouterExperimentalMetadata, ProviderPreferences},
17    utils::parse_sse_frames,
18};
19
20/// Request body for the OpenRouter Responses API (`POST /responses`).
21#[derive(Serialize, Deserialize, Debug, Clone, Builder)]
22#[builder(build_fn(error = "OpenRouterError"))]
23#[non_exhaustive]
24pub struct ResponsesRequest {
25    #[builder(setter(strip_option), default)]
26    #[serde(skip_serializing_if = "Option::is_none")]
27    input: Option<Value>,
28
29    #[builder(setter(into, strip_option), default)]
30    #[serde(skip_serializing_if = "Option::is_none")]
31    instructions: Option<String>,
32
33    #[builder(setter(custom), default)]
34    #[serde(skip_serializing_if = "Option::is_none")]
35    metadata: Option<HashMap<String, String>>,
36
37    #[builder(setter(custom), default)]
38    #[serde(skip_serializing_if = "Option::is_none")]
39    tools: Option<Vec<Value>>,
40
41    #[builder(setter(strip_option), default)]
42    #[serde(skip_serializing_if = "Option::is_none")]
43    tool_choice: Option<Value>,
44
45    #[builder(setter(strip_option), default)]
46    #[serde(skip_serializing_if = "Option::is_none")]
47    parallel_tool_calls: Option<bool>,
48
49    #[builder(setter(into, strip_option), default)]
50    #[serde(skip_serializing_if = "Option::is_none")]
51    model: Option<String>,
52
53    #[builder(setter(custom), default)]
54    #[serde(skip_serializing_if = "Option::is_none")]
55    models: Option<Vec<String>>,
56
57    #[builder(setter(strip_option), default)]
58    #[serde(skip_serializing_if = "Option::is_none")]
59    text: Option<Value>,
60
61    #[builder(setter(strip_option), default)]
62    #[serde(skip_serializing_if = "Option::is_none")]
63    reasoning: Option<Value>,
64
65    #[builder(setter(strip_option), default)]
66    #[serde(skip_serializing_if = "Option::is_none")]
67    max_output_tokens: Option<u32>,
68
69    #[builder(setter(strip_option), default)]
70    #[serde(skip_serializing_if = "Option::is_none")]
71    temperature: Option<f64>,
72
73    #[builder(setter(strip_option), default)]
74    #[serde(skip_serializing_if = "Option::is_none")]
75    top_p: Option<f64>,
76
77    #[builder(setter(strip_option), default)]
78    #[serde(skip_serializing_if = "Option::is_none")]
79    top_logprobs: Option<u32>,
80
81    #[builder(setter(strip_option), default)]
82    #[serde(skip_serializing_if = "Option::is_none")]
83    max_tool_calls: Option<u32>,
84
85    #[builder(setter(strip_option), default)]
86    #[serde(skip_serializing_if = "Option::is_none")]
87    presence_penalty: Option<f64>,
88
89    #[builder(setter(strip_option), default)]
90    #[serde(skip_serializing_if = "Option::is_none")]
91    frequency_penalty: Option<f64>,
92
93    #[builder(setter(strip_option), default)]
94    #[serde(skip_serializing_if = "Option::is_none")]
95    top_k: Option<f64>,
96
97    #[builder(setter(custom), default)]
98    #[serde(skip_serializing_if = "Option::is_none")]
99    image_config: Option<HashMap<String, Value>>,
100
101    #[builder(setter(custom), default)]
102    #[serde(skip_serializing_if = "Option::is_none")]
103    modalities: Option<Vec<String>>,
104
105    #[builder(setter(into, strip_option), default)]
106    #[serde(skip_serializing_if = "Option::is_none")]
107    prompt_cache_key: Option<String>,
108
109    #[builder(setter(into, strip_option), default)]
110    #[serde(skip_serializing_if = "Option::is_none")]
111    previous_response_id: Option<String>,
112
113    #[builder(setter(strip_option), default)]
114    #[serde(skip_serializing_if = "Option::is_none")]
115    prompt: Option<Value>,
116
117    #[builder(setter(custom), default)]
118    #[serde(skip_serializing_if = "Option::is_none")]
119    include: Option<Vec<String>>,
120
121    #[builder(setter(strip_option), default)]
122    #[serde(skip_serializing_if = "Option::is_none")]
123    background: Option<bool>,
124
125    #[builder(setter(into, strip_option), default)]
126    #[serde(skip_serializing_if = "Option::is_none")]
127    safety_identifier: Option<String>,
128
129    #[builder(setter(strip_option), default)]
130    #[serde(skip_serializing_if = "Option::is_none")]
131    store: Option<bool>,
132
133    #[builder(setter(into, strip_option), default)]
134    #[serde(skip_serializing_if = "Option::is_none")]
135    service_tier: Option<String>,
136
137    #[builder(setter(into, strip_option), default)]
138    #[serde(skip_serializing_if = "Option::is_none")]
139    truncation: Option<String>,
140
141    #[builder(setter(skip), default)]
142    #[serde(skip_serializing_if = "Option::is_none")]
143    stream: Option<bool>,
144
145    #[builder(setter(strip_option), default)]
146    #[serde(skip)]
147    experimental_metadata: Option<OpenRouterExperimentalMetadata>,
148
149    #[builder(setter(strip_option), default)]
150    #[serde(skip_serializing_if = "Option::is_none")]
151    provider: Option<ProviderPreferences>,
152
153    #[builder(setter(custom), default)]
154    #[serde(skip_serializing_if = "Option::is_none")]
155    plugins: Option<Vec<Plugin>>,
156
157    #[builder(setter(into, strip_option), default)]
158    #[serde(skip_serializing_if = "Option::is_none")]
159    route: Option<String>,
160
161    #[builder(setter(into, strip_option), default)]
162    #[serde(skip_serializing_if = "Option::is_none")]
163    user: Option<String>,
164
165    #[builder(setter(into, strip_option), default)]
166    #[serde(skip_serializing_if = "Option::is_none")]
167    session_id: Option<String>,
168
169    #[builder(setter(strip_option), default)]
170    #[serde(skip_serializing_if = "Option::is_none")]
171    cache_control: Option<CacheControl>,
172
173    #[builder(setter(strip_option), default)]
174    #[serde(skip_serializing_if = "Option::is_none")]
175    trace: Option<TraceOptions>,
176
177    #[builder(setter(strip_option), default)]
178    #[serde(skip_serializing_if = "Option::is_none")]
179    debug: Option<DebugOptions>,
180}
181
182impl ResponsesRequestBuilder {
183    strip_option_map_setter!(metadata, String, String);
184    strip_option_vec_setter!(tools, Value);
185    strip_option_vec_setter!(models, String);
186    strip_option_map_setter!(image_config, String, Value);
187    strip_option_vec_setter!(modalities, String);
188    strip_option_vec_setter!(include, String);
189    strip_option_vec_setter!(plugins, Plugin);
190}
191
192impl ResponsesRequest {
193    pub fn builder() -> ResponsesRequestBuilder {
194        ResponsesRequestBuilder::default()
195    }
196
197    pub fn new(model: impl Into<String>, input: Value) -> Self {
198        Self::builder()
199            .model(model.into())
200            .input(input)
201            .build()
202            .expect("Failed to build ResponsesRequest")
203    }
204
205    fn stream(&self, stream: bool) -> Self {
206        let mut req = self.clone();
207        req.stream = Some(stream);
208        req
209    }
210
211    pub fn experimental_metadata(&self) -> Option<OpenRouterExperimentalMetadata> {
212        self.experimental_metadata
213    }
214}
215
216/// Non-streaming response payload returned by `POST /responses`.
217#[derive(Serialize, Deserialize, Debug, Clone)]
218#[non_exhaustive]
219pub struct ResponsesResponse {
220    pub id: Option<String>,
221    #[serde(rename = "object")]
222    pub object_type: Option<String>,
223    pub created_at: Option<u64>,
224    pub model: Option<String>,
225    pub status: Option<String>,
226    #[serde(skip_serializing_if = "Option::is_none")]
227    pub output: Option<Vec<Value>>,
228    #[serde(skip_serializing_if = "Option::is_none")]
229    pub usage: Option<Value>,
230    #[serde(flatten)]
231    pub extra: HashMap<String, Value>,
232}
233
234/// Streaming event payload returned by `POST /responses` when `stream=true`.
235#[derive(Serialize, Deserialize, Debug, Clone)]
236#[non_exhaustive]
237pub struct ResponsesStreamEvent {
238    #[serde(rename = "type")]
239    pub event_type: String,
240    #[serde(skip_serializing_if = "Option::is_none")]
241    pub sequence_number: Option<u64>,
242    #[serde(flatten)]
243    pub data: HashMap<String, Value>,
244}
245
246/// Send a non-streaming request to the Responses API.
247pub async fn create_response(
248    base_url: &str,
249    api_key: &str,
250    x_title: &Option<String>,
251    http_referer: &Option<String>,
252    app_categories: &Option<Vec<String>>,
253    request: &ResponsesRequest,
254) -> Result<ResponsesResponse, OpenRouterError> {
255    let http_client = crate::transport::new_client()?;
256    create_response_with_client(
257        &http_client,
258        base_url,
259        api_key,
260        x_title,
261        http_referer,
262        app_categories,
263        request,
264    )
265    .await
266}
267
268pub(crate) async fn create_response_with_client(
269    http_client: &HttpClient,
270    base_url: &str,
271    api_key: &str,
272    x_title: &Option<String>,
273    http_referer: &Option<String>,
274    app_categories: &Option<Vec<String>>,
275    request: &ResponsesRequest,
276) -> Result<ResponsesResponse, OpenRouterError> {
277    let url = format!("{base_url}/responses");
278    let request = request.stream(false);
279
280    let response = transport_request::with_experimental_metadata_header(
281        transport_request::with_client_request_headers(
282            transport_request::post(http_client, &url),
283            api_key,
284            x_title,
285            http_referer,
286            app_categories,
287        )?,
288        &request.experimental_metadata,
289    )
290    .json(&request)
291    .send()
292    .await?;
293
294    if response.status().is_success() {
295        let response_data: ResponsesResponse =
296            transport_response::parse_json_response(response, "responses API").await?;
297        Ok(response_data)
298    } else {
299        transport_response::handle_error(response).await?;
300        unreachable!()
301    }
302}
303
304/// Send a streaming request to the Responses API.
305pub async fn stream_response(
306    base_url: &str,
307    api_key: &str,
308    x_title: &Option<String>,
309    http_referer: &Option<String>,
310    app_categories: &Option<Vec<String>>,
311    request: &ResponsesRequest,
312) -> Result<BoxStream<'static, Result<ResponsesStreamEvent, OpenRouterError>>, OpenRouterError> {
313    let http_client = crate::transport::new_client()?;
314    stream_response_with_client(
315        &http_client,
316        base_url,
317        api_key,
318        x_title,
319        http_referer,
320        app_categories,
321        request,
322    )
323    .await
324}
325
326pub(crate) async fn stream_response_with_client(
327    http_client: &HttpClient,
328    base_url: &str,
329    api_key: &str,
330    x_title: &Option<String>,
331    http_referer: &Option<String>,
332    app_categories: &Option<Vec<String>>,
333    request: &ResponsesRequest,
334) -> Result<BoxStream<'static, Result<ResponsesStreamEvent, OpenRouterError>>, OpenRouterError> {
335    let url = format!("{base_url}/responses");
336    let request = request.stream(true);
337
338    let response = transport_request::with_experimental_metadata_header(
339        transport_request::with_client_request_headers(
340            transport_request::post(http_client, &url),
341            api_key,
342            x_title,
343            http_referer,
344            app_categories,
345        )?,
346        &request.experimental_metadata,
347    )
348    .json(&request)
349    .send()
350    .await?;
351
352    if response.status().is_success() {
353        let lines = parse_sse_frames(response_lines(response))
354            .filter_map(async |line| match line {
355                Ok(frame) if frame.data == "[DONE]" => None,
356                Ok(frame) => Some(
357                    serde_json::from_str::<ResponsesStreamEvent>(&frame.data)
358                        .map_err(OpenRouterError::Serialization),
359                ),
360                Err(error) => Some(Err(error)),
361            })
362            .boxed();
363
364        Ok(lines)
365    } else {
366        transport_response::handle_error(response).await?;
367        unreachable!()
368    }
369}