Skip to main content

openrouter_rs/api/
chat.rs

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/// Image URL with optional detail level for vision models.
23#[derive(Serialize, Deserialize, Debug, Clone)]
24#[non_exhaustive]
25pub struct ImageUrl {
26    /// URL of the image (can be a web URL or base64 data URI)
27    pub url: String,
28    /// Detail level: "auto", "low", or "high"
29    #[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/// Audio input payload for multimodal requests.
50#[derive(Serialize, Deserialize, Debug, Clone)]
51#[non_exhaustive]
52pub struct InputAudio {
53    /// Base64-encoded audio data.
54    pub data: String,
55    /// Audio format (e.g. wav, mp3, flac).
56    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/// Video URL payload for multimodal requests.
69#[derive(Serialize, Deserialize, Debug, Clone)]
70#[non_exhaustive]
71pub struct VideoUrl {
72    /// URL of the input video.
73    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/// File payload for multimodal requests.
83#[derive(Serialize, Deserialize, Debug, Clone, Default)]
84#[non_exhaustive]
85pub struct FileInput {
86    /// File content as URL or data URL.
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub file_data: Option<String>,
89    /// File id for previously uploaded files.
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub file_id: Option<String>,
92    /// Optional filename metadata.
93    #[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/// Cache control type for prompt caching breakpoints.
121#[derive(Serialize, Deserialize, Debug, Clone)]
122#[non_exhaustive]
123#[serde(rename_all = "lowercase")]
124pub enum CacheControlType {
125    Ephemeral,
126}
127
128/// Cache control settings for text content parts.
129#[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    /// Create cache control using default ephemeral TTL.
140    pub fn ephemeral() -> Self {
141        Self {
142            kind: CacheControlType::Ephemeral,
143            ttl: None,
144        }
145    }
146
147    /// Create cache control with explicit TTL (e.g. "1h").
148    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/// Explicit prompt-cache mode used by OpenAI-style cache controls.
157#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
158#[non_exhaustive]
159#[serde(rename_all = "lowercase")]
160pub enum PromptCacheMode {
161    Explicit,
162}
163
164/// Marks an explicit prompt-cache boundary on a text content part.
165#[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/// Request-level controls for explicit prompt caching.
180#[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/// A content part in a multi-modal message.
205#[derive(Serialize, Deserialize, Debug, Clone)]
206#[non_exhaustive]
207#[serde(tag = "type", rename_all = "snake_case")]
208pub enum ContentPart {
209    /// Text content
210    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    /// Image URL content
218    ImageUrl { image_url: ImageUrl },
219    /// Audio input content
220    InputAudio { input_audio: InputAudio },
221    /// Video URL content
222    VideoUrl { video_url: VideoUrl },
223    /// Legacy video input content
224    InputVideo { video_url: VideoUrl },
225    /// File content
226    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/// Message content - either a simple string or multi-part content.
328#[derive(Serialize, Deserialize, Debug, Clone)]
329#[non_exhaustive]
330#[serde(untagged)]
331pub enum Content {
332    /// Simple text content
333    Text(String),
334    /// Multi-part content (text, images, etc.)
335    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/// One text part in a static predicted output.
357#[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/// Static predicted output content.
375#[derive(Serialize, Deserialize, Debug, Clone)]
376#[non_exhaustive]
377#[serde(untagged)]
378pub enum PredictionContent {
379    Text(String),
380    Parts(Vec<PredictionContentText>),
381}
382
383/// Static predicted output used to reduce latency when most output is known.
384#[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    /// Model that generated an assistant message.
414    #[serde(skip_serializing_if = "Option::is_none")]
415    pub model: Option<String>,
416    /// Optional name for tool messages or function calls
417    #[serde(skip_serializing_if = "Option::is_none")]
418    pub name: Option<String>,
419    /// Tool call ID for tool response messages
420    #[serde(skip_serializing_if = "Option::is_none")]
421    pub tool_call_id: Option<String>,
422    /// Tool calls made by assistant
423    #[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    /// Create a message with multi-part content (text and images).
440    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    /// Create a tool response message
452    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    /// Create a tool response message with a specific tool name
464    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    /// Create a message with a specific name
480    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    /// Create an assistant message with tool calls
492    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/// Output modality for chat responses.
513#[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/// Streaming debug options.
523#[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/// Streaming configuration options.
531#[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/// Trace metadata used for observability.
539#[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/// Plugin configuration payload.
557#[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/// Stop sequence configuration.
580#[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    /// Enable reasoning with default settings (medium effort)
1012    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    /// Set reasoning effort level
1019    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    /// Set reasoning max tokens
1026    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    /// Exclude reasoning from response (use reasoning internally but don't return it)
1033    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    /// Add a single tool to the request
1040    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    /// Add a single OpenRouter server tool to the request.
1050    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    /// Set tool choice to auto (model chooses whether to use tools)
1060    pub fn tool_choice_auto(&mut self) -> &mut Self {
1061        self.tool_choice = Some(Some(crate::types::ToolChoice::auto()));
1062        self
1063    }
1064
1065    /// Set tool choice to none (model will not use tools)
1066    pub fn tool_choice_none(&mut self) -> &mut Self {
1067        self.tool_choice = Some(Some(crate::types::ToolChoice::none()));
1068        self
1069    }
1070
1071    /// Set tool choice to required (model must use tools)
1072    pub fn tool_choice_required(&mut self) -> &mut Self {
1073        self.tool_choice = Some(Some(crate::types::ToolChoice::required()));
1074        self
1075    }
1076
1077    /// Force the model to use a specific tool
1078    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    /// Force the model to use a specific OpenRouter server tool.
1084    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    /// Add a typed tool to the request
1090    ///
1091    /// This method allows adding strongly-typed tools using the TypedTool trait.
1092    /// The tool's JSON Schema is automatically generated from the Rust type.
1093    ///
1094    /// # Examples
1095    ///
1096    /// ```rust
1097    /// use openrouter_rs::api::chat::{ChatCompletionRequest, Message};
1098    /// use openrouter_rs::types::Role;
1099    /// use openrouter_rs::types::typed_tool::TypedTool;
1100    /// use serde::{Deserialize, Serialize};
1101    /// use schemars::JsonSchema;
1102    ///
1103    /// #[derive(Serialize, Deserialize, JsonSchema)]
1104    /// struct WeatherParams {
1105    ///     location: String,
1106    /// }
1107    ///
1108    /// impl TypedTool for WeatherParams {
1109    ///     fn name() -> &'static str { "get_weather" }
1110    ///     fn description() -> &'static str { "Get weather for location" }
1111    /// }
1112    ///
1113    /// let request = ChatCompletionRequest::builder()
1114    ///     .model("anthropic/claude-sonnet-4")
1115    ///     .messages(vec![Message::new(Role::User, "What is the weather in Paris?")])
1116    ///     .typed_tool::<WeatherParams>()
1117    ///     .build()?;
1118    /// # Ok::<(), Box<dyn std::error::Error>>(())
1119    /// ```
1120    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    /// Add multiple typed tools to the request
1126    ///
1127    /// This is a convenience method for adding multiple typed tools at once.
1128    /// Each tool type must implement the TypedTool trait.
1129    ///
1130    /// # Examples
1131    ///
1132    /// ```rust
1133    /// # use openrouter_rs::api::chat::{ChatCompletionRequest, Message};
1134    /// # use openrouter_rs::types::Role;
1135    /// # use openrouter_rs::types::typed_tool::TypedTool;
1136    /// # use serde::{Deserialize, Serialize};
1137    /// # use schemars::JsonSchema;
1138    /// # #[derive(Serialize, Deserialize, JsonSchema)]
1139    /// # struct WeatherParams { location: String }
1140    /// # impl TypedTool for WeatherParams {
1141    /// #     fn name() -> &'static str { "get_weather" }
1142    /// #     fn description() -> &'static str { "Get weather" }
1143    /// # }
1144    /// # #[derive(Serialize, Deserialize, JsonSchema)]
1145    /// # struct CalculatorParams { a: f64, b: f64 }
1146    /// # impl TypedTool for CalculatorParams {
1147    /// #     fn name() -> &'static str { "calculator" }
1148    /// #     fn description() -> &'static str { "Calculate" }
1149    /// # }
1150    ///
1151    /// let request = ChatCompletionRequest::builder()
1152    ///     .model("anthropic/claude-sonnet-4")
1153    ///     .messages(vec![Message::new(Role::User, "Need weather and calculator help")])
1154    ///     .typed_tools_batch(&[
1155    ///         WeatherParams::create_tool(),
1156    ///         CalculatorParams::create_tool(),
1157    ///     ])
1158    ///     .build()?;
1159    /// # Ok::<(), Box<dyn std::error::Error>>(())
1160    /// ```
1161    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    /// Force the model to use a specific typed tool
1169    ///
1170    /// This method combines the typed tool functionality with tool choice forcing.
1171    /// The specified typed tool will be added to the tools list and forced as the choice.
1172    ///
1173    /// # Examples
1174    ///
1175    /// ```rust
1176    /// # use openrouter_rs::api::chat::{ChatCompletionRequest, Message};
1177    /// # use openrouter_rs::types::Role;
1178    /// # use openrouter_rs::types::typed_tool::TypedTool;
1179    /// # use serde::{Deserialize, Serialize};
1180    /// # use schemars::JsonSchema;
1181    /// # #[derive(Serialize, Deserialize, JsonSchema)]
1182    /// # struct WeatherParams { location: String }
1183    /// # impl TypedTool for WeatherParams {
1184    /// #     fn name() -> &'static str { "get_weather" }
1185    /// #     fn description() -> &'static str { "Get weather" }
1186    /// # }
1187    ///
1188    /// let request = ChatCompletionRequest::builder()
1189    ///     .model("anthropic/claude-sonnet-4")
1190    ///     .messages(vec![Message::new(Role::User, "What is the weather in Paris?")])
1191    ///     .force_typed_tool::<WeatherParams>()
1192    ///     .build()?;
1193    /// # Ok::<(), Box<dyn std::error::Error>>(())
1194    /// ```
1195    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    /// Get the tools defined in this request
1218    pub fn tools(&self) -> Option<&[crate::types::Tool]> {
1219        self.tools.as_deref()
1220    }
1221
1222    /// Get the OpenRouter server tools defined in this request.
1223    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    /// Get the tool choice setting
1234    pub fn tool_choice(&self) -> Option<&crate::types::ToolChoice> {
1235        self.tool_choice.as_ref()
1236    }
1237
1238    /// Get the parallel tool calls setting
1239    pub fn parallel_tool_calls(&self) -> Option<bool> {
1240        self.parallel_tool_calls
1241    }
1242
1243    /// Get the messages in this request
1244    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
1259/// Send a chat completion request to a selected model.
1260///
1261/// # Arguments
1262///
1263/// * `base_url` - The base URL for the OpenRouter API.
1264/// * `api_key` - The API key for authentication.
1265/// * `x_title` - The name of the site for the request.
1266/// * `http_referer` - The URL of the site for the request.
1267/// * `request` - The chat completion request containing the model and messages.
1268///
1269/// # Returns
1270///
1271/// * `Result<CompletionsResponse, OpenRouterError>` - The response from the chat completion request.
1272pub 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    // Ensure that the request is not streaming to get a single response
1305    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
1332/// Stream chat completion events from a selected model.
1333///
1334/// # Arguments
1335///
1336/// * `base_url` - The base URL for the OpenRouter API.
1337/// * `api_key` - The API key for authentication.
1338/// * `request` - The chat completion request containing the model and messages.
1339///
1340/// # Returns
1341///
1342/// * `Result<BoxStream<'static, Result<CompletionsResponse, OpenRouterError>>, OpenRouterError>` - A stream of chat completion events or an error.
1343pub 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    // Ensure that the request is streaming to get a continuous response
1376    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}