1use std::collections::HashMap;
2
3use derive_builder::Builder;
4use futures_util::{StreamExt, stream::BoxStream};
5use reqwest::Client as HttpClient;
6use serde::{Deserialize, Deserializer, Serialize, de};
7use serde_json::Value;
8
9use crate::{
10 error::OpenRouterError,
11 strip_option_map_setter, strip_option_vec_setter,
12 transport::{
13 request as transport_request, response as transport_response, sse::response_lines,
14 },
15 types::{
16 OpenRouterExperimentalMetadata, ProviderPreferences, ReasoningConfig, ResponseFormat, Role,
17 completion::CompletionsResponse,
18 },
19 utils::parse_sse_frames,
20};
21
22#[derive(Serialize, Deserialize, Debug, Clone)]
24#[non_exhaustive]
25pub struct ImageUrl {
26 pub url: String,
28 #[serde(skip_serializing_if = "Option::is_none")]
30 pub detail: Option<String>,
31}
32
33impl ImageUrl {
34 pub fn new(url: impl Into<String>) -> Self {
35 Self {
36 url: url.into(),
37 detail: None,
38 }
39 }
40
41 pub fn with_detail(url: impl Into<String>, detail: impl Into<String>) -> Self {
42 Self {
43 url: url.into(),
44 detail: Some(detail.into()),
45 }
46 }
47}
48
49#[derive(Serialize, Deserialize, Debug, Clone)]
51#[non_exhaustive]
52pub struct InputAudio {
53 pub data: String,
55 pub format: String,
57}
58
59impl InputAudio {
60 pub fn new(data: impl Into<String>, format: impl Into<String>) -> Self {
61 Self {
62 data: data.into(),
63 format: format.into(),
64 }
65 }
66}
67
68#[derive(Serialize, Deserialize, Debug, Clone)]
70#[non_exhaustive]
71pub struct VideoUrl {
72 pub url: String,
74}
75
76impl VideoUrl {
77 pub fn new(url: impl Into<String>) -> Self {
78 Self { url: url.into() }
79 }
80}
81
82#[derive(Serialize, Deserialize, Debug, Clone, Default)]
84#[non_exhaustive]
85pub struct FileInput {
86 #[serde(skip_serializing_if = "Option::is_none")]
88 pub file_data: Option<String>,
89 #[serde(skip_serializing_if = "Option::is_none")]
91 pub file_id: Option<String>,
92 #[serde(skip_serializing_if = "Option::is_none")]
94 pub filename: Option<String>,
95}
96
97impl FileInput {
98 pub fn from_data(file_data: impl Into<String>) -> Self {
99 Self {
100 file_data: Some(file_data.into()),
101 file_id: None,
102 filename: None,
103 }
104 }
105
106 pub fn from_id(file_id: impl Into<String>) -> Self {
107 Self {
108 file_data: None,
109 file_id: Some(file_id.into()),
110 filename: None,
111 }
112 }
113
114 pub fn filename(mut self, filename: impl Into<String>) -> Self {
115 self.filename = Some(filename.into());
116 self
117 }
118}
119
120#[derive(Serialize, Deserialize, Debug, Clone)]
122#[non_exhaustive]
123#[serde(rename_all = "lowercase")]
124pub enum CacheControlType {
125 Ephemeral,
126}
127
128#[derive(Serialize, Deserialize, Debug, Clone)]
130#[non_exhaustive]
131pub struct CacheControl {
132 #[serde(rename = "type")]
133 pub kind: CacheControlType,
134 #[serde(skip_serializing_if = "Option::is_none")]
135 pub ttl: Option<String>,
136}
137
138impl CacheControl {
139 pub fn ephemeral() -> Self {
141 Self {
142 kind: CacheControlType::Ephemeral,
143 ttl: None,
144 }
145 }
146
147 pub fn ephemeral_with_ttl(ttl: impl Into<String>) -> Self {
149 Self {
150 kind: CacheControlType::Ephemeral,
151 ttl: Some(ttl.into()),
152 }
153 }
154}
155
156#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
158#[non_exhaustive]
159#[serde(rename_all = "lowercase")]
160pub enum PromptCacheMode {
161 Explicit,
162}
163
164#[derive(Serialize, Deserialize, Debug, Clone)]
166#[non_exhaustive]
167pub struct PromptCacheBreakpoint {
168 pub mode: PromptCacheMode,
169}
170
171impl PromptCacheBreakpoint {
172 pub fn explicit() -> Self {
173 Self {
174 mode: PromptCacheMode::Explicit,
175 }
176 }
177}
178
179#[derive(Serialize, Deserialize, Debug, Clone)]
181#[non_exhaustive]
182pub struct PromptCacheOptions {
183 pub mode: PromptCacheMode,
184 #[serde(skip_serializing_if = "Option::is_none")]
185 pub ttl: Option<String>,
186}
187
188impl PromptCacheOptions {
189 pub fn explicit() -> Self {
190 Self {
191 mode: PromptCacheMode::Explicit,
192 ttl: None,
193 }
194 }
195
196 pub fn explicit_with_ttl(ttl: impl Into<String>) -> Self {
197 Self {
198 mode: PromptCacheMode::Explicit,
199 ttl: Some(ttl.into()),
200 }
201 }
202}
203
204#[derive(Serialize, Deserialize, Debug, Clone)]
206#[non_exhaustive]
207#[serde(tag = "type", rename_all = "snake_case")]
208pub enum ContentPart {
209 Text {
211 text: String,
212 #[serde(skip_serializing_if = "Option::is_none")]
213 cache_control: Option<CacheControl>,
214 #[serde(skip_serializing_if = "Option::is_none")]
215 prompt_cache_breakpoint: Option<PromptCacheBreakpoint>,
216 },
217 ImageUrl { image_url: ImageUrl },
219 InputAudio { input_audio: InputAudio },
221 VideoUrl { video_url: VideoUrl },
223 InputVideo { video_url: VideoUrl },
225 File { file: FileInput },
227}
228
229impl ContentPart {
230 pub fn text(text: impl Into<String>) -> Self {
231 Self::Text {
232 text: text.into(),
233 cache_control: None,
234 prompt_cache_breakpoint: None,
235 }
236 }
237
238 pub fn text_with_cache_control(text: impl Into<String>, cache_control: CacheControl) -> Self {
239 Self::Text {
240 text: text.into(),
241 cache_control: Some(cache_control),
242 prompt_cache_breakpoint: None,
243 }
244 }
245
246 pub fn text_with_prompt_cache_breakpoint(
247 text: impl Into<String>,
248 breakpoint: PromptCacheBreakpoint,
249 ) -> Self {
250 Self::Text {
251 text: text.into(),
252 cache_control: None,
253 prompt_cache_breakpoint: Some(breakpoint),
254 }
255 }
256
257 pub fn cache_breakpoint_text(text: impl Into<String>) -> Self {
258 Self::text_with_prompt_cache_breakpoint(text, PromptCacheBreakpoint::explicit())
259 }
260
261 pub fn cacheable_text(text: impl Into<String>) -> Self {
262 Self::text_with_cache_control(text, CacheControl::ephemeral())
263 }
264
265 pub fn cacheable_text_with_ttl(text: impl Into<String>, ttl: impl Into<String>) -> Self {
266 Self::text_with_cache_control(text, CacheControl::ephemeral_with_ttl(ttl))
267 }
268
269 pub fn image_url(url: impl Into<String>) -> Self {
270 Self::ImageUrl {
271 image_url: ImageUrl::new(url),
272 }
273 }
274
275 pub fn image_url_with_detail(url: impl Into<String>, detail: impl Into<String>) -> Self {
276 Self::ImageUrl {
277 image_url: ImageUrl::with_detail(url, detail),
278 }
279 }
280
281 pub fn input_audio(data: impl Into<String>, format: impl Into<String>) -> Self {
282 Self::InputAudio {
283 input_audio: InputAudio::new(data, format),
284 }
285 }
286
287 pub fn video_url(url: impl Into<String>) -> Self {
288 Self::VideoUrl {
289 video_url: VideoUrl::new(url),
290 }
291 }
292
293 pub fn input_video(url: impl Into<String>) -> Self {
294 Self::InputVideo {
295 video_url: VideoUrl::new(url),
296 }
297 }
298
299 pub fn file_data(file_data: impl Into<String>) -> Self {
300 Self::File {
301 file: FileInput::from_data(file_data),
302 }
303 }
304
305 pub fn file_data_with_filename(
306 file_data: impl Into<String>,
307 filename: impl Into<String>,
308 ) -> Self {
309 Self::File {
310 file: FileInput::from_data(file_data).filename(filename),
311 }
312 }
313
314 pub fn file_id(file_id: impl Into<String>) -> Self {
315 Self::File {
316 file: FileInput::from_id(file_id),
317 }
318 }
319
320 pub fn file_id_with_filename(file_id: impl Into<String>, filename: impl Into<String>) -> Self {
321 Self::File {
322 file: FileInput::from_id(file_id).filename(filename),
323 }
324 }
325}
326
327#[derive(Serialize, Deserialize, Debug, Clone)]
329#[non_exhaustive]
330#[serde(untagged)]
331pub enum Content {
332 Text(String),
334 Parts(Vec<ContentPart>),
336}
337
338impl From<String> for Content {
339 fn from(s: String) -> Self {
340 Self::Text(s)
341 }
342}
343
344impl From<&str> for Content {
345 fn from(s: &str) -> Self {
346 Self::Text(s.to_string())
347 }
348}
349
350impl From<Vec<ContentPart>> for Content {
351 fn from(parts: Vec<ContentPart>) -> Self {
352 Self::Parts(parts)
353 }
354}
355
356#[derive(Serialize, Deserialize, Debug, Clone)]
358#[non_exhaustive]
359pub struct PredictionContentText {
360 #[serde(rename = "type")]
361 pub content_type: String,
362 pub text: String,
363}
364
365impl PredictionContentText {
366 pub fn new(text: impl Into<String>) -> Self {
367 Self {
368 content_type: "text".to_string(),
369 text: text.into(),
370 }
371 }
372}
373
374#[derive(Serialize, Deserialize, Debug, Clone)]
376#[non_exhaustive]
377#[serde(untagged)]
378pub enum PredictionContent {
379 Text(String),
380 Parts(Vec<PredictionContentText>),
381}
382
383#[derive(Serialize, Deserialize, Debug, Clone)]
385#[non_exhaustive]
386pub struct Prediction {
387 #[serde(rename = "type")]
388 pub prediction_type: String,
389 pub content: PredictionContent,
390}
391
392impl Prediction {
393 pub fn text(text: impl Into<String>) -> Self {
394 Self {
395 prediction_type: "content".to_string(),
396 content: PredictionContent::Text(text.into()),
397 }
398 }
399
400 pub fn parts(parts: Vec<PredictionContentText>) -> Self {
401 Self {
402 prediction_type: "content".to_string(),
403 content: PredictionContent::Parts(parts),
404 }
405 }
406}
407
408#[derive(Serialize, Deserialize, Debug, Clone)]
409#[non_exhaustive]
410pub struct Message {
411 pub role: Role,
412 pub content: Content,
413 #[serde(skip_serializing_if = "Option::is_none")]
415 pub model: Option<String>,
416 #[serde(skip_serializing_if = "Option::is_none")]
418 pub name: Option<String>,
419 #[serde(skip_serializing_if = "Option::is_none")]
421 pub tool_call_id: Option<String>,
422 #[serde(skip_serializing_if = "Option::is_none")]
424 pub tool_calls: Option<Vec<crate::types::ToolCall>>,
425}
426
427impl Message {
428 pub fn new(role: Role, content: impl Into<Content>) -> Self {
429 Self {
430 role,
431 content: content.into(),
432 model: None,
433 name: None,
434 tool_call_id: None,
435 tool_calls: None,
436 }
437 }
438
439 pub fn with_parts(role: Role, parts: Vec<ContentPart>) -> Self {
441 Self {
442 role,
443 content: Content::Parts(parts),
444 model: None,
445 name: None,
446 tool_call_id: None,
447 tool_calls: None,
448 }
449 }
450
451 pub fn tool_response(tool_call_id: &str, content: impl Into<Content>) -> Self {
453 Self {
454 role: Role::Tool,
455 content: content.into(),
456 model: None,
457 name: None,
458 tool_call_id: Some(tool_call_id.to_string()),
459 tool_calls: None,
460 }
461 }
462
463 pub fn tool_response_named(
465 tool_call_id: &str,
466 tool_name: &str,
467 content: impl Into<Content>,
468 ) -> Self {
469 Self {
470 role: Role::Tool,
471 content: content.into(),
472 model: None,
473 name: Some(tool_name.to_string()),
474 tool_call_id: Some(tool_call_id.to_string()),
475 tool_calls: None,
476 }
477 }
478
479 pub fn named(role: Role, name: &str, content: impl Into<Content>) -> Self {
481 Self {
482 role,
483 content: content.into(),
484 model: None,
485 name: Some(name.to_string()),
486 tool_call_id: None,
487 tool_calls: None,
488 }
489 }
490
491 pub fn assistant_with_tool_calls(
493 content: impl Into<Content>,
494 tool_calls: Vec<crate::types::ToolCall>,
495 ) -> Self {
496 Self {
497 role: Role::Assistant,
498 content: content.into(),
499 model: None,
500 name: None,
501 tool_call_id: None,
502 tool_calls: Some(tool_calls),
503 }
504 }
505
506 pub fn with_model(mut self, model: impl Into<String>) -> Self {
507 self.model = Some(model.into());
508 self
509 }
510}
511
512#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
514#[non_exhaustive]
515#[serde(rename_all = "lowercase")]
516pub enum Modality {
517 Text,
518 Image,
519 Audio,
520}
521
522#[derive(Serialize, Deserialize, Debug, Clone, Default)]
524#[non_exhaustive]
525pub struct DebugOptions {
526 #[serde(skip_serializing_if = "Option::is_none")]
527 pub echo_upstream_body: Option<bool>,
528}
529
530#[derive(Serialize, Deserialize, Debug, Clone, Default)]
532#[non_exhaustive]
533pub struct StreamOptions {
534 #[serde(skip_serializing_if = "Option::is_none")]
535 pub include_usage: Option<bool>,
536}
537
538#[derive(Serialize, Deserialize, Debug, Clone, Default)]
540#[non_exhaustive]
541pub struct TraceOptions {
542 #[serde(skip_serializing_if = "Option::is_none")]
543 pub trace_id: Option<String>,
544 #[serde(skip_serializing_if = "Option::is_none")]
545 pub trace_name: Option<String>,
546 #[serde(skip_serializing_if = "Option::is_none")]
547 pub span_name: Option<String>,
548 #[serde(skip_serializing_if = "Option::is_none")]
549 pub generation_name: Option<String>,
550 #[serde(skip_serializing_if = "Option::is_none")]
551 pub parent_span_id: Option<String>,
552 #[serde(flatten)]
553 pub extra: HashMap<String, Value>,
554}
555
556#[derive(Serialize, Deserialize, Debug, Clone, Default)]
558#[non_exhaustive]
559pub struct Plugin {
560 pub id: String,
561 #[serde(flatten)]
562 pub config: HashMap<String, Value>,
563}
564
565impl Plugin {
566 pub fn new(id: impl Into<String>) -> Self {
567 Self {
568 id: id.into(),
569 config: HashMap::new(),
570 }
571 }
572
573 pub fn option(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
574 self.config.insert(key.into(), value.into());
575 self
576 }
577}
578
579#[derive(Serialize, Deserialize, Debug, Clone)]
581#[non_exhaustive]
582#[serde(untagged)]
583pub enum StopSequence {
584 Single(String),
585 Multiple(Vec<String>),
586}
587
588impl From<String> for StopSequence {
589 fn from(value: String) -> Self {
590 Self::Single(value)
591 }
592}
593
594impl From<&str> for StopSequence {
595 fn from(value: &str) -> Self {
596 Self::Single(value.to_string())
597 }
598}
599
600impl From<Vec<String>> for StopSequence {
601 fn from(value: Vec<String>) -> Self {
602 Self::Multiple(value)
603 }
604}
605
606#[derive(Debug, Clone, Builder)]
607#[builder(build_fn(error = "OpenRouterError"))]
608#[non_exhaustive]
609pub struct ChatCompletionRequest {
610 #[builder(setter(into))]
611 model: String,
612
613 messages: Vec<Message>,
614
615 #[builder(setter(skip), default)]
616 stream: Option<bool>,
617
618 #[builder(setter(strip_option), default)]
619 experimental_metadata: Option<OpenRouterExperimentalMetadata>,
620
621 #[builder(setter(strip_option), default)]
622 max_tokens: Option<u32>,
623
624 #[builder(setter(strip_option), default)]
625 max_completion_tokens: Option<u32>,
626
627 #[builder(setter(strip_option), default)]
628 temperature: Option<f64>,
629
630 #[builder(setter(strip_option), default)]
631 seed: Option<u32>,
632
633 #[builder(setter(strip_option), default)]
634 top_p: Option<f64>,
635
636 #[builder(setter(strip_option), default)]
637 top_k: Option<u32>,
638
639 #[builder(setter(strip_option), default)]
640 frequency_penalty: Option<f64>,
641
642 #[builder(setter(strip_option), default)]
643 presence_penalty: Option<f64>,
644
645 #[builder(setter(strip_option), default)]
646 repetition_penalty: Option<f64>,
647
648 #[builder(setter(custom), default)]
649 logit_bias: Option<HashMap<String, f64>>,
650
651 #[builder(setter(strip_option), default)]
652 logprobs: Option<bool>,
653
654 #[builder(setter(strip_option), default)]
655 top_logprobs: Option<u32>,
656
657 #[builder(setter(strip_option), default)]
658 min_p: Option<f64>,
659
660 #[builder(setter(strip_option), default)]
661 top_a: Option<f64>,
662
663 #[builder(setter(custom), default)]
664 transforms: Option<Vec<String>>,
665
666 #[builder(setter(custom), default)]
667 models: Option<Vec<String>>,
668
669 #[builder(setter(into, strip_option), default)]
670 route: Option<String>,
671
672 #[builder(setter(into, strip_option), default)]
673 user: Option<String>,
674
675 #[builder(setter(into, strip_option), default)]
676 session_id: Option<String>,
677
678 #[builder(setter(strip_option), default)]
679 cache_control: Option<CacheControl>,
680
681 #[builder(setter(into, strip_option), default)]
682 prompt_cache_key: Option<String>,
683
684 #[builder(setter(strip_option), default)]
685 prompt_cache_options: Option<PromptCacheOptions>,
686
687 #[builder(setter(strip_option), default)]
688 prediction: Option<Prediction>,
689
690 #[builder(setter(strip_option), default)]
691 trace: Option<TraceOptions>,
692
693 #[builder(setter(strip_option), default)]
694 provider: Option<ProviderPreferences>,
695
696 #[builder(setter(custom), default)]
697 metadata: Option<HashMap<String, String>>,
698
699 #[builder(setter(custom), default)]
700 plugins: Option<Vec<Plugin>>,
701
702 #[builder(setter(custom), default)]
703 modalities: Option<Vec<Modality>>,
704
705 #[builder(setter(custom), default)]
706 image_config: Option<HashMap<String, Value>>,
707
708 #[builder(setter(strip_option), default)]
709 response_format: Option<ResponseFormat>,
710
711 #[builder(setter(strip_option), default)]
712 reasoning: Option<ReasoningConfig>,
713
714 #[builder(setter(strip_option), default)]
715 include_reasoning: Option<bool>,
716
717 #[builder(setter(into, strip_option), default)]
718 stop: Option<StopSequence>,
719
720 #[builder(setter(strip_option), default)]
721 stream_options: Option<StreamOptions>,
722
723 #[builder(setter(strip_option), default)]
724 debug: Option<DebugOptions>,
725
726 #[builder(setter(custom), default)]
727 tools: Option<Vec<crate::types::Tool>>,
728
729 #[builder(setter(custom), default)]
730 server_tools: Option<Vec<crate::types::ServerTool>>,
731
732 #[builder(setter(strip_option), default)]
733 tool_choice: Option<crate::types::ToolChoice>,
734
735 #[builder(setter(strip_option), default)]
736 parallel_tool_calls: Option<bool>,
737}
738
739#[derive(Deserialize)]
740struct ChatCompletionRequestWire {
741 model: String,
742 messages: Vec<Message>,
743 stream: Option<bool>,
744 #[serde(skip)]
745 experimental_metadata: Option<OpenRouterExperimentalMetadata>,
746 max_tokens: Option<u32>,
747 max_completion_tokens: Option<u32>,
748 temperature: Option<f64>,
749 seed: Option<u32>,
750 top_p: Option<f64>,
751 top_k: Option<u32>,
752 frequency_penalty: Option<f64>,
753 presence_penalty: Option<f64>,
754 repetition_penalty: Option<f64>,
755 logit_bias: Option<HashMap<String, f64>>,
756 logprobs: Option<bool>,
757 top_logprobs: Option<u32>,
758 min_p: Option<f64>,
759 top_a: Option<f64>,
760 transforms: Option<Vec<String>>,
761 models: Option<Vec<String>>,
762 route: Option<String>,
763 user: Option<String>,
764 session_id: Option<String>,
765 cache_control: Option<CacheControl>,
766 prompt_cache_key: Option<String>,
767 prompt_cache_options: Option<PromptCacheOptions>,
768 prediction: Option<Prediction>,
769 trace: Option<TraceOptions>,
770 provider: Option<ProviderPreferences>,
771 metadata: Option<HashMap<String, String>>,
772 plugins: Option<Vec<Plugin>>,
773 modalities: Option<Vec<Modality>>,
774 image_config: Option<HashMap<String, Value>>,
775 response_format: Option<ResponseFormat>,
776 reasoning: Option<ReasoningConfig>,
777 include_reasoning: Option<bool>,
778 stop: Option<StopSequence>,
779 stream_options: Option<StreamOptions>,
780 debug: Option<DebugOptions>,
781 tools: Option<Vec<Value>>,
782 tool_choice: Option<crate::types::ToolChoice>,
783 parallel_tool_calls: Option<bool>,
784}
785
786type SplitChatTools = (
787 Option<Vec<crate::types::Tool>>,
788 Option<Vec<crate::types::ServerTool>>,
789);
790
791impl<'de> Deserialize<'de> for ChatCompletionRequest {
792 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
793 where
794 D: Deserializer<'de>,
795 {
796 let wire = ChatCompletionRequestWire::deserialize(deserializer)?;
797 let (tools, server_tools) = split_chat_request_tools(wire.tools)
798 .map_err(|error| de::Error::custom(format!("invalid chat tools entry: {error}")))?;
799
800 Ok(Self {
801 model: wire.model,
802 messages: wire.messages,
803 stream: wire.stream,
804 experimental_metadata: wire.experimental_metadata,
805 max_tokens: wire.max_tokens,
806 max_completion_tokens: wire.max_completion_tokens,
807 temperature: wire.temperature,
808 seed: wire.seed,
809 top_p: wire.top_p,
810 top_k: wire.top_k,
811 frequency_penalty: wire.frequency_penalty,
812 presence_penalty: wire.presence_penalty,
813 repetition_penalty: wire.repetition_penalty,
814 logit_bias: wire.logit_bias,
815 logprobs: wire.logprobs,
816 top_logprobs: wire.top_logprobs,
817 min_p: wire.min_p,
818 top_a: wire.top_a,
819 transforms: wire.transforms,
820 models: wire.models,
821 route: wire.route,
822 user: wire.user,
823 session_id: wire.session_id,
824 cache_control: wire.cache_control,
825 prompt_cache_key: wire.prompt_cache_key,
826 prompt_cache_options: wire.prompt_cache_options,
827 prediction: wire.prediction,
828 trace: wire.trace,
829 provider: wire.provider,
830 metadata: wire.metadata,
831 plugins: wire.plugins,
832 modalities: wire.modalities,
833 image_config: wire.image_config,
834 response_format: wire.response_format,
835 reasoning: wire.reasoning,
836 include_reasoning: wire.include_reasoning,
837 stop: wire.stop,
838 stream_options: wire.stream_options,
839 debug: wire.debug,
840 tools,
841 server_tools,
842 tool_choice: wire.tool_choice,
843 parallel_tool_calls: wire.parallel_tool_calls,
844 })
845 }
846}
847
848fn split_chat_request_tools(
849 tools: Option<Vec<Value>>,
850) -> Result<SplitChatTools, serde_json::Error> {
851 let Some(values) = tools else {
852 return Ok((None, None));
853 };
854 let was_empty = values.is_empty();
855 let mut function_tools = Vec::new();
856 let mut server_tools = Vec::new();
857
858 for value in values {
859 let is_function_tool = value.get("function").is_some()
860 || value
861 .get("type")
862 .and_then(Value::as_str)
863 .is_some_and(|tool_type| tool_type == "function");
864 if is_function_tool {
865 function_tools.push(serde_json::from_value(value)?);
866 } else if crate::types::ServerTool::is_server_tool_value(&value) {
867 server_tools.push(serde_json::from_value(value)?);
868 } else {
869 function_tools.push(serde_json::from_value(value)?);
870 }
871 }
872
873 let tools = if function_tools.is_empty() && !was_empty {
874 None
875 } else {
876 Some(function_tools)
877 };
878 let server_tools = if server_tools.is_empty() {
879 None
880 } else {
881 Some(server_tools)
882 };
883
884 Ok((tools, server_tools))
885}
886
887fn insert_json_field<T, E>(
888 map: &mut serde_json::Map<String, Value>,
889 key: &str,
890 value: &T,
891) -> Result<(), E>
892where
893 T: Serialize,
894 E: serde::ser::Error,
895{
896 map.insert(
897 key.to_string(),
898 serde_json::to_value(value).map_err(E::custom)?,
899 );
900 Ok(())
901}
902
903fn insert_json_option<T, E>(
904 map: &mut serde_json::Map<String, Value>,
905 key: &str,
906 value: &Option<T>,
907) -> Result<(), E>
908where
909 T: Serialize,
910 E: serde::ser::Error,
911{
912 if let Some(value) = value {
913 insert_json_field::<T, E>(map, key, value)?;
914 }
915 Ok(())
916}
917
918impl Serialize for ChatCompletionRequest {
919 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
920 where
921 S: serde::Serializer,
922 {
923 let mut map = serde_json::Map::new();
924
925 insert_json_field::<_, S::Error>(&mut map, "model", &self.model)?;
926 insert_json_field::<_, S::Error>(&mut map, "messages", &self.messages)?;
927 insert_json_option::<_, S::Error>(&mut map, "stream", &self.stream)?;
928 insert_json_option::<_, S::Error>(&mut map, "max_tokens", &self.max_tokens)?;
929 insert_json_option::<_, S::Error>(
930 &mut map,
931 "max_completion_tokens",
932 &self.max_completion_tokens,
933 )?;
934 insert_json_option::<_, S::Error>(&mut map, "temperature", &self.temperature)?;
935 insert_json_option::<_, S::Error>(&mut map, "seed", &self.seed)?;
936 insert_json_option::<_, S::Error>(&mut map, "top_p", &self.top_p)?;
937 insert_json_option::<_, S::Error>(&mut map, "top_k", &self.top_k)?;
938 insert_json_option::<_, S::Error>(&mut map, "frequency_penalty", &self.frequency_penalty)?;
939 insert_json_option::<_, S::Error>(&mut map, "presence_penalty", &self.presence_penalty)?;
940 insert_json_option::<_, S::Error>(
941 &mut map,
942 "repetition_penalty",
943 &self.repetition_penalty,
944 )?;
945 insert_json_option::<_, S::Error>(&mut map, "logit_bias", &self.logit_bias)?;
946 insert_json_option::<_, S::Error>(&mut map, "logprobs", &self.logprobs)?;
947 insert_json_option::<_, S::Error>(&mut map, "top_logprobs", &self.top_logprobs)?;
948 insert_json_option::<_, S::Error>(&mut map, "min_p", &self.min_p)?;
949 insert_json_option::<_, S::Error>(&mut map, "top_a", &self.top_a)?;
950 insert_json_option::<_, S::Error>(&mut map, "transforms", &self.transforms)?;
951 insert_json_option::<_, S::Error>(&mut map, "models", &self.models)?;
952 insert_json_option::<_, S::Error>(&mut map, "route", &self.route)?;
953 insert_json_option::<_, S::Error>(&mut map, "user", &self.user)?;
954 insert_json_option::<_, S::Error>(&mut map, "session_id", &self.session_id)?;
955 insert_json_option::<_, S::Error>(&mut map, "cache_control", &self.cache_control)?;
956 insert_json_option::<_, S::Error>(&mut map, "prompt_cache_key", &self.prompt_cache_key)?;
957 insert_json_option::<_, S::Error>(
958 &mut map,
959 "prompt_cache_options",
960 &self.prompt_cache_options,
961 )?;
962 insert_json_option::<_, S::Error>(&mut map, "prediction", &self.prediction)?;
963 insert_json_option::<_, S::Error>(&mut map, "trace", &self.trace)?;
964 insert_json_option::<_, S::Error>(&mut map, "provider", &self.provider)?;
965 insert_json_option::<_, S::Error>(&mut map, "metadata", &self.metadata)?;
966 insert_json_option::<_, S::Error>(&mut map, "plugins", &self.plugins)?;
967 insert_json_option::<_, S::Error>(&mut map, "modalities", &self.modalities)?;
968 insert_json_option::<_, S::Error>(&mut map, "image_config", &self.image_config)?;
969 insert_json_option::<_, S::Error>(&mut map, "response_format", &self.response_format)?;
970 insert_json_option::<_, S::Error>(&mut map, "reasoning", &self.reasoning)?;
971 insert_json_option::<_, S::Error>(&mut map, "include_reasoning", &self.include_reasoning)?;
972 insert_json_option::<_, S::Error>(&mut map, "stop", &self.stop)?;
973 insert_json_option::<_, S::Error>(&mut map, "stream_options", &self.stream_options)?;
974 insert_json_option::<_, S::Error>(&mut map, "debug", &self.debug)?;
975
976 let function_tools = self.tools.as_deref().unwrap_or_default();
977 let server_tools = self.server_tools.as_deref().unwrap_or_default();
978 if self.tools.is_some() || self.server_tools.is_some() {
979 let mut tools = Vec::with_capacity(function_tools.len() + server_tools.len());
980 for tool in function_tools {
981 tools.push(serde_json::to_value(tool).map_err(serde::ser::Error::custom)?);
982 }
983 for tool in server_tools {
984 tools.push(serde_json::to_value(tool).map_err(serde::ser::Error::custom)?);
985 }
986 map.insert("tools".to_string(), Value::Array(tools));
987 }
988
989 insert_json_option::<_, S::Error>(&mut map, "tool_choice", &self.tool_choice)?;
990 insert_json_option::<_, S::Error>(
991 &mut map,
992 "parallel_tool_calls",
993 &self.parallel_tool_calls,
994 )?;
995
996 Value::Object(map).serialize(serializer)
997 }
998}
999
1000impl ChatCompletionRequestBuilder {
1001 strip_option_vec_setter!(models, String);
1002 strip_option_map_setter!(logit_bias, String, f64);
1003 strip_option_vec_setter!(transforms, String);
1004 strip_option_map_setter!(metadata, String, String);
1005 strip_option_map_setter!(image_config, String, Value);
1006 strip_option_vec_setter!(plugins, Plugin);
1007 strip_option_vec_setter!(modalities, Modality);
1008 strip_option_vec_setter!(tools, crate::types::Tool);
1009 strip_option_vec_setter!(server_tools, crate::types::ServerTool);
1010
1011 pub fn enable_reasoning(&mut self) -> &mut Self {
1013 use crate::types::ReasoningConfig;
1014 self.reasoning = Some(Some(ReasoningConfig::enabled()));
1015 self
1016 }
1017
1018 pub fn reasoning_effort(&mut self, effort: crate::types::Effort) -> &mut Self {
1020 use crate::types::ReasoningConfig;
1021 self.reasoning = Some(Some(ReasoningConfig::with_effort(effort)));
1022 self
1023 }
1024
1025 pub fn reasoning_max_tokens(&mut self, max_tokens: u32) -> &mut Self {
1027 use crate::types::ReasoningConfig;
1028 self.reasoning = Some(Some(ReasoningConfig::with_max_tokens(max_tokens)));
1029 self
1030 }
1031
1032 pub fn exclude_reasoning(&mut self) -> &mut Self {
1034 use crate::types::ReasoningConfig;
1035 self.reasoning = Some(Some(ReasoningConfig::excluded()));
1036 self
1037 }
1038
1039 pub fn tool(&mut self, tool: crate::types::Tool) -> &mut Self {
1041 if let Some(Some(ref mut existing_tools)) = self.tools {
1042 existing_tools.push(tool);
1043 } else {
1044 self.tools = Some(Some(vec![tool]));
1045 }
1046 self
1047 }
1048
1049 pub fn server_tool(&mut self, tool: crate::types::ServerTool) -> &mut Self {
1051 if let Some(Some(ref mut existing_tools)) = self.server_tools {
1052 existing_tools.push(tool);
1053 } else {
1054 self.server_tools = Some(Some(vec![tool]));
1055 }
1056 self
1057 }
1058
1059 pub fn tool_choice_auto(&mut self) -> &mut Self {
1061 self.tool_choice = Some(Some(crate::types::ToolChoice::auto()));
1062 self
1063 }
1064
1065 pub fn tool_choice_none(&mut self) -> &mut Self {
1067 self.tool_choice = Some(Some(crate::types::ToolChoice::none()));
1068 self
1069 }
1070
1071 pub fn tool_choice_required(&mut self) -> &mut Self {
1073 self.tool_choice = Some(Some(crate::types::ToolChoice::required()));
1074 self
1075 }
1076
1077 pub fn force_tool(&mut self, tool_name: &str) -> &mut Self {
1079 self.tool_choice = Some(Some(crate::types::ToolChoice::force_tool(tool_name)));
1080 self
1081 }
1082
1083 pub fn force_server_tool(&mut self, tool_type: impl Into<String>) -> &mut Self {
1085 self.tool_choice = Some(Some(crate::types::ToolChoice::force_server_tool(tool_type)));
1086 self
1087 }
1088
1089 pub fn typed_tool<T: crate::types::TypedTool>(&mut self) -> &mut Self {
1121 let tool = T::create_tool();
1122 self.tool(tool)
1123 }
1124
1125 pub fn typed_tools_batch(&mut self, tools: &[crate::types::Tool]) -> &mut Self {
1162 for tool in tools {
1163 self.tool(tool.clone());
1164 }
1165 self
1166 }
1167
1168 pub fn force_typed_tool<T: crate::types::TypedTool>(&mut self) -> &mut Self {
1196 let tool_name = T::name();
1197 let tool = T::create_tool();
1198 self.tool(tool);
1199 self.force_tool(tool_name);
1200 self
1201 }
1202}
1203
1204impl ChatCompletionRequest {
1205 pub fn builder() -> ChatCompletionRequestBuilder {
1206 ChatCompletionRequestBuilder::default()
1207 }
1208
1209 pub fn new(model: &str, messages: Vec<Message>) -> Self {
1210 Self::builder()
1211 .model(model)
1212 .messages(messages)
1213 .build()
1214 .expect("Failed to build ChatCompletionRequest")
1215 }
1216
1217 pub fn tools(&self) -> Option<&[crate::types::Tool]> {
1219 self.tools.as_deref()
1220 }
1221
1222 pub fn server_tools(&self) -> Option<&[crate::types::ServerTool]> {
1224 self.server_tools.as_deref()
1225 }
1226
1227 pub(crate) fn requires_openrouter_files_tool_header(&self) -> bool {
1228 self.server_tools
1229 .as_deref()
1230 .is_some_and(|tools| tools.iter().any(crate::types::ServerTool::is_files_tool))
1231 }
1232
1233 pub fn tool_choice(&self) -> Option<&crate::types::ToolChoice> {
1235 self.tool_choice.as_ref()
1236 }
1237
1238 pub fn parallel_tool_calls(&self) -> Option<bool> {
1240 self.parallel_tool_calls
1241 }
1242
1243 pub fn messages(&self) -> &[Message] {
1245 &self.messages
1246 }
1247
1248 fn stream(&self, stream: bool) -> Self {
1249 let mut req = self.clone();
1250 req.stream = Some(stream);
1251 req
1252 }
1253
1254 pub fn experimental_metadata(&self) -> Option<OpenRouterExperimentalMetadata> {
1255 self.experimental_metadata
1256 }
1257}
1258
1259pub async fn send_chat_completion(
1273 base_url: &str,
1274 api_key: &str,
1275 x_title: &Option<String>,
1276 http_referer: &Option<String>,
1277 app_categories: &Option<Vec<String>>,
1278 request: &ChatCompletionRequest,
1279) -> Result<CompletionsResponse, OpenRouterError> {
1280 let http_client = crate::transport::new_client()?;
1281 send_chat_completion_with_client(
1282 &http_client,
1283 base_url,
1284 api_key,
1285 x_title,
1286 http_referer,
1287 app_categories,
1288 request,
1289 )
1290 .await
1291}
1292
1293pub(crate) async fn send_chat_completion_with_client(
1294 http_client: &HttpClient,
1295 base_url: &str,
1296 api_key: &str,
1297 x_title: &Option<String>,
1298 http_referer: &Option<String>,
1299 app_categories: &Option<Vec<String>>,
1300 request: &ChatCompletionRequest,
1301) -> Result<CompletionsResponse, OpenRouterError> {
1302 let url = format!("{base_url}/chat/completions");
1303
1304 let request = request.stream(false);
1306
1307 let request_builder = transport_request::with_experimental_metadata_header(
1308 transport_request::with_client_request_headers(
1309 transport_request::post(http_client, &url),
1310 api_key,
1311 x_title,
1312 http_referer,
1313 app_categories,
1314 )?,
1315 &request.experimental_metadata,
1316 );
1317 let request_builder = transport_request::with_openrouter_files_tool_header(
1318 request_builder,
1319 request.requires_openrouter_files_tool_header(),
1320 );
1321
1322 let response = request_builder.json(&request).send().await?;
1323
1324 if response.status().is_success() {
1325 transport_response::parse_json_response(response, "chat completion").await
1326 } else {
1327 transport_response::handle_error(response).await?;
1328 unreachable!()
1329 }
1330}
1331
1332pub async fn stream_chat_completion(
1344 base_url: &str,
1345 api_key: &str,
1346 x_title: &Option<String>,
1347 http_referer: &Option<String>,
1348 app_categories: &Option<Vec<String>>,
1349 request: &ChatCompletionRequest,
1350) -> Result<BoxStream<'static, Result<CompletionsResponse, OpenRouterError>>, OpenRouterError> {
1351 let http_client = crate::transport::new_client()?;
1352 stream_chat_completion_with_client(
1353 &http_client,
1354 base_url,
1355 api_key,
1356 x_title,
1357 http_referer,
1358 app_categories,
1359 request,
1360 )
1361 .await
1362}
1363
1364pub(crate) async fn stream_chat_completion_with_client(
1365 http_client: &HttpClient,
1366 base_url: &str,
1367 api_key: &str,
1368 x_title: &Option<String>,
1369 http_referer: &Option<String>,
1370 app_categories: &Option<Vec<String>>,
1371 request: &ChatCompletionRequest,
1372) -> Result<BoxStream<'static, Result<CompletionsResponse, OpenRouterError>>, OpenRouterError> {
1373 let url = format!("{base_url}/chat/completions");
1374
1375 let request = request.stream(true);
1377
1378 let request_builder = transport_request::with_experimental_metadata_header(
1379 transport_request::with_client_request_headers(
1380 transport_request::post(http_client, &url),
1381 api_key,
1382 x_title,
1383 http_referer,
1384 app_categories,
1385 )?,
1386 &request.experimental_metadata,
1387 );
1388 let request_builder = transport_request::with_openrouter_files_tool_header(
1389 request_builder,
1390 request.requires_openrouter_files_tool_header(),
1391 );
1392
1393 let response = request_builder.json(&request).send().await?;
1394
1395 if response.status().is_success() {
1396 let lines = parse_sse_frames(response_lines(response))
1397 .filter_map(async |line| match line {
1398 Ok(frame) if frame.data == "[DONE]" => None,
1399 Ok(frame) => Some(
1400 serde_json::from_str::<CompletionsResponse>(&frame.data)
1401 .map_err(OpenRouterError::Serialization),
1402 ),
1403 Err(error) => Some(Err(error)),
1404 })
1405 .boxed();
1406
1407 Ok(lines)
1408 } else {
1409 transport_response::handle_error(response).await?;
1410 unreachable!()
1411 }
1412}