1use std::collections::HashMap;
2
3use derive_builder::Builder;
4use futures_util::{AsyncBufReadExt, StreamExt, stream::BoxStream};
5use serde::{Deserialize, Serialize};
6use surf::http::headers::AUTHORIZATION;
7
8use crate::{
9 error::OpenRouterError,
10 strip_option_map_setter, strip_option_vec_setter,
11 types::{
12 ProviderPreferences, ReasoningConfig, ResponseFormat, Role, completion::CompletionsResponse,
13 },
14 utils::handle_error,
15};
16
17#[derive(Serialize, Deserialize, Debug, Clone)]
19pub struct ImageUrl {
20 pub url: String,
22 #[serde(skip_serializing_if = "Option::is_none")]
24 pub detail: Option<String>,
25}
26
27impl ImageUrl {
28 pub fn new(url: impl Into<String>) -> Self {
29 Self {
30 url: url.into(),
31 detail: None,
32 }
33 }
34
35 pub fn with_detail(url: impl Into<String>, detail: impl Into<String>) -> Self {
36 Self {
37 url: url.into(),
38 detail: Some(detail.into()),
39 }
40 }
41}
42
43#[derive(Serialize, Deserialize, Debug, Clone)]
45#[serde(tag = "type", rename_all = "snake_case")]
46pub enum ContentPart {
47 Text { text: String },
49 ImageUrl { image_url: ImageUrl },
51}
52
53impl ContentPart {
54 pub fn text(text: impl Into<String>) -> Self {
55 Self::Text { text: text.into() }
56 }
57
58 pub fn image_url(url: impl Into<String>) -> Self {
59 Self::ImageUrl {
60 image_url: ImageUrl::new(url),
61 }
62 }
63
64 pub fn image_url_with_detail(url: impl Into<String>, detail: impl Into<String>) -> Self {
65 Self::ImageUrl {
66 image_url: ImageUrl::with_detail(url, detail),
67 }
68 }
69}
70
71#[derive(Serialize, Deserialize, Debug, Clone)]
73#[serde(untagged)]
74pub enum Content {
75 Text(String),
77 Parts(Vec<ContentPart>),
79}
80
81impl From<String> for Content {
82 fn from(s: String) -> Self {
83 Self::Text(s)
84 }
85}
86
87impl From<&str> for Content {
88 fn from(s: &str) -> Self {
89 Self::Text(s.to_string())
90 }
91}
92
93impl From<Vec<ContentPart>> for Content {
94 fn from(parts: Vec<ContentPart>) -> Self {
95 Self::Parts(parts)
96 }
97}
98
99#[derive(Serialize, Deserialize, Debug, Clone)]
100pub struct Message {
101 pub role: Role,
102 pub content: Content,
103 #[serde(skip_serializing_if = "Option::is_none")]
105 pub name: Option<String>,
106 #[serde(skip_serializing_if = "Option::is_none")]
108 pub tool_call_id: Option<String>,
109 #[serde(skip_serializing_if = "Option::is_none")]
111 pub tool_calls: Option<Vec<crate::types::ToolCall>>,
112}
113
114impl Message {
115 pub fn new(role: Role, content: impl Into<Content>) -> Self {
116 Self {
117 role,
118 content: content.into(),
119 name: None,
120 tool_call_id: None,
121 tool_calls: None,
122 }
123 }
124
125 pub fn with_parts(role: Role, parts: Vec<ContentPart>) -> Self {
127 Self {
128 role,
129 content: Content::Parts(parts),
130 name: None,
131 tool_call_id: None,
132 tool_calls: None,
133 }
134 }
135
136 pub fn tool_response(tool_call_id: &str, content: impl Into<Content>) -> Self {
138 Self {
139 role: Role::Tool,
140 content: content.into(),
141 name: None,
142 tool_call_id: Some(tool_call_id.to_string()),
143 tool_calls: None,
144 }
145 }
146
147 pub fn tool_response_named(tool_call_id: &str, tool_name: &str, content: impl Into<Content>) -> Self {
149 Self {
150 role: Role::Tool,
151 content: content.into(),
152 name: Some(tool_name.to_string()),
153 tool_call_id: Some(tool_call_id.to_string()),
154 tool_calls: None,
155 }
156 }
157
158 pub fn named(role: Role, name: &str, content: impl Into<Content>) -> Self {
160 Self {
161 role,
162 content: content.into(),
163 name: Some(name.to_string()),
164 tool_call_id: None,
165 tool_calls: None,
166 }
167 }
168
169 pub fn assistant_with_tool_calls(content: impl Into<Content>, tool_calls: Vec<crate::types::ToolCall>) -> Self {
171 Self {
172 role: Role::Assistant,
173 content: content.into(),
174 name: None,
175 tool_call_id: None,
176 tool_calls: Some(tool_calls),
177 }
178 }
179}
180
181#[derive(Serialize, Deserialize, Debug, Clone, Builder)]
182#[builder(build_fn(error = "OpenRouterError"))]
183pub struct ChatCompletionRequest {
184 #[builder(setter(into))]
185 model: String,
186
187 messages: Vec<Message>,
188
189 #[builder(setter(skip), default)]
190 #[serde(skip_serializing_if = "Option::is_none")]
191 stream: Option<bool>,
192
193 #[builder(setter(strip_option), default)]
194 #[serde(skip_serializing_if = "Option::is_none")]
195 max_tokens: Option<u32>,
196
197 #[builder(setter(strip_option), default)]
198 #[serde(skip_serializing_if = "Option::is_none")]
199 temperature: Option<f64>,
200
201 #[builder(setter(strip_option), default)]
202 #[serde(skip_serializing_if = "Option::is_none")]
203 seed: Option<u32>,
204
205 #[builder(setter(strip_option), default)]
206 #[serde(skip_serializing_if = "Option::is_none")]
207 top_p: Option<f64>,
208
209 #[builder(setter(strip_option), default)]
210 #[serde(skip_serializing_if = "Option::is_none")]
211 top_k: Option<u32>,
212
213 #[builder(setter(strip_option), default)]
214 #[serde(skip_serializing_if = "Option::is_none")]
215 frequency_penalty: Option<f64>,
216
217 #[builder(setter(strip_option), default)]
218 #[serde(skip_serializing_if = "Option::is_none")]
219 presence_penalty: Option<f64>,
220
221 #[builder(setter(strip_option), default)]
222 #[serde(skip_serializing_if = "Option::is_none")]
223 repetition_penalty: Option<f64>,
224
225 #[builder(setter(custom), default)]
226 #[serde(skip_serializing_if = "Option::is_none")]
227 logit_bias: Option<HashMap<String, f64>>,
228
229 #[builder(setter(strip_option), default)]
230 #[serde(skip_serializing_if = "Option::is_none")]
231 top_logprobs: Option<u32>,
232
233 #[builder(setter(strip_option), default)]
234 #[serde(skip_serializing_if = "Option::is_none")]
235 min_p: Option<f64>,
236
237 #[builder(setter(strip_option), default)]
238 #[serde(skip_serializing_if = "Option::is_none")]
239 top_a: Option<f64>,
240
241 #[builder(setter(custom), default)]
242 #[serde(skip_serializing_if = "Option::is_none")]
243 transforms: Option<Vec<String>>,
244
245 #[builder(setter(custom), default)]
246 #[serde(skip_serializing_if = "Option::is_none")]
247 models: Option<Vec<String>>,
248
249 #[builder(setter(into, strip_option), default)]
250 #[serde(skip_serializing_if = "Option::is_none")]
251 route: Option<String>,
252
253 #[builder(setter(strip_option), default)]
254 #[serde(skip_serializing_if = "Option::is_none")]
255 provider: Option<ProviderPreferences>,
256
257 #[builder(setter(strip_option), default)]
258 #[serde(skip_serializing_if = "Option::is_none")]
259 response_format: Option<ResponseFormat>,
260
261 #[builder(setter(strip_option), default)]
262 #[serde(skip_serializing_if = "Option::is_none")]
263 reasoning: Option<ReasoningConfig>,
264
265 #[builder(setter(strip_option), default)]
266 #[serde(skip_serializing_if = "Option::is_none")]
267 include_reasoning: Option<bool>,
268
269 #[builder(setter(custom), default)]
270 #[serde(skip_serializing_if = "Option::is_none")]
271 tools: Option<Vec<crate::types::Tool>>,
272
273 #[builder(setter(strip_option), default)]
274 #[serde(skip_serializing_if = "Option::is_none")]
275 tool_choice: Option<crate::types::ToolChoice>,
276
277 #[builder(setter(strip_option), default)]
278 #[serde(skip_serializing_if = "Option::is_none")]
279 parallel_tool_calls: Option<bool>,
280}
281
282impl ChatCompletionRequestBuilder {
283 strip_option_vec_setter!(models, String);
284 strip_option_map_setter!(logit_bias, String, f64);
285 strip_option_vec_setter!(transforms, String);
286 strip_option_vec_setter!(tools, crate::types::Tool);
287
288 pub fn enable_reasoning(&mut self) -> &mut Self {
290 use crate::types::ReasoningConfig;
291 self.reasoning = Some(Some(ReasoningConfig::enabled()));
292 self
293 }
294
295 pub fn reasoning_effort(&mut self, effort: crate::types::Effort) -> &mut Self {
297 use crate::types::ReasoningConfig;
298 self.reasoning = Some(Some(ReasoningConfig::with_effort(effort)));
299 self
300 }
301
302 pub fn reasoning_max_tokens(&mut self, max_tokens: u32) -> &mut Self {
304 use crate::types::ReasoningConfig;
305 self.reasoning = Some(Some(ReasoningConfig::with_max_tokens(max_tokens)));
306 self
307 }
308
309 pub fn exclude_reasoning(&mut self) -> &mut Self {
311 use crate::types::ReasoningConfig;
312 self.reasoning = Some(Some(ReasoningConfig::excluded()));
313 self
314 }
315
316 pub fn tool(&mut self, tool: crate::types::Tool) -> &mut Self {
318 if let Some(Some(ref mut existing_tools)) = self.tools {
319 existing_tools.push(tool);
320 } else {
321 self.tools = Some(Some(vec![tool]));
322 }
323 self
324 }
325
326 pub fn tool_choice_auto(&mut self) -> &mut Self {
328 self.tool_choice = Some(Some(crate::types::ToolChoice::auto()));
329 self
330 }
331
332 pub fn tool_choice_none(&mut self) -> &mut Self {
334 self.tool_choice = Some(Some(crate::types::ToolChoice::none()));
335 self
336 }
337
338 pub fn tool_choice_required(&mut self) -> &mut Self {
340 self.tool_choice = Some(Some(crate::types::ToolChoice::required()));
341 self
342 }
343
344 pub fn force_tool(&mut self, tool_name: &str) -> &mut Self {
346 self.tool_choice = Some(Some(crate::types::ToolChoice::force_tool(tool_name)));
347 self
348 }
349
350 pub fn typed_tool<T: crate::types::TypedTool>(&mut self) -> &mut Self {
379 let tool = T::create_tool();
380 self.tool(tool)
381 }
382
383 pub fn typed_tools_batch(&mut self, tools: &[crate::types::Tool]) -> &mut Self {
417 for tool in tools {
418 self.tool(tool.clone());
419 }
420 self
421 }
422
423 pub fn force_typed_tool<T: crate::types::TypedTool>(&mut self) -> &mut Self {
448 let tool_name = T::name();
449 let tool = T::create_tool();
450 self.tool(tool);
451 self.force_tool(tool_name);
452 self
453 }
454}
455
456impl ChatCompletionRequest {
457 pub fn builder() -> ChatCompletionRequestBuilder {
458 ChatCompletionRequestBuilder::default()
459 }
460
461 pub fn new(model: &str, messages: Vec<Message>) -> Self {
462 Self::builder()
463 .model(model)
464 .messages(messages)
465 .build()
466 .expect("Failed to build ChatCompletionRequest")
467 }
468
469 pub fn tools(&self) -> Option<&Vec<crate::types::Tool>> {
471 self.tools.as_ref()
472 }
473
474 pub fn tool_choice(&self) -> Option<&crate::types::ToolChoice> {
476 self.tool_choice.as_ref()
477 }
478
479 pub fn parallel_tool_calls(&self) -> Option<bool> {
481 self.parallel_tool_calls
482 }
483
484 pub fn messages(&self) -> &Vec<Message> {
486 &self.messages
487 }
488
489 fn stream(&self, stream: bool) -> Self {
490 let mut req = self.clone();
491 req.stream = Some(stream);
492 req
493 }
494}
495
496pub async fn send_chat_completion(
510 base_url: &str,
511 api_key: &str,
512 x_title: &Option<String>,
513 http_referer: &Option<String>,
514 request: &ChatCompletionRequest,
515) -> Result<CompletionsResponse, OpenRouterError> {
516 let url = format!("{base_url}/chat/completions");
517
518 let request = request.stream(false);
520
521 let mut surf_req = surf::post(url)
522 .header(AUTHORIZATION, format!("Bearer {api_key}"))
523 .body_json(&request)?;
524
525 if let Some(x_title) = x_title {
526 surf_req = surf_req.header("X-Title", x_title);
527 }
528 if let Some(http_referer) = http_referer {
529 surf_req = surf_req.header("HTTP-Referer", http_referer);
530 }
531
532 let mut response = surf_req.await?;
533
534 if response.status().is_success() {
535 let body_text = response.body_string().await?;
536 let chat_response: CompletionsResponse = serde_json::from_str(&body_text)
537 .map_err(|e| {
538 eprintln!("Failed to deserialize response: {e}\nBody: {body_text}");
539 OpenRouterError::Serialization(e)
540 })?;
541 Ok(chat_response)
542 } else {
543 handle_error(response).await?;
544 unreachable!()
545 }
546}
547
548pub async fn stream_chat_completion(
560 base_url: &str,
561 api_key: &str,
562 request: &ChatCompletionRequest,
563) -> Result<BoxStream<'static, Result<CompletionsResponse, OpenRouterError>>, OpenRouterError> {
564 let url = format!("{base_url}/chat/completions");
565
566 let request = request.stream(true);
568
569 let response = surf::post(url)
570 .header(AUTHORIZATION, format!("Bearer {api_key}"))
571 .body_json(&request)?
572 .await?;
573
574 if response.status().is_success() {
575 let lines = response
576 .lines()
577 .filter_map(async |line| match line {
578 Ok(line) => line
579 .strip_prefix("data: ")
580 .filter(|line| *line != "[DONE]")
581 .map(serde_json::from_str::<CompletionsResponse>)
582 .map(|event| event.map_err(OpenRouterError::Serialization)),
583 Err(error) => Some(Err(OpenRouterError::Io(error))),
584 })
585 .boxed();
586
587 Ok(lines)
588 } else {
589 handle_error(response).await?;
590 unreachable!()
591 }
592}