Skip to main content

openrouter_rs/api/
messages.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, json};
8
9use crate::{
10    api::chat::{CacheControl, Plugin, TraceOptions},
11    error::OpenRouterError,
12    strip_option_vec_setter,
13    transport::{
14        request as transport_request, response as transport_response, sse::response_lines,
15    },
16    types::{AnthropicCacheCreation, OpenRouterExperimentalMetadata, ProviderPreferences},
17    utils::parse_sse_frames,
18};
19
20/// Role for Anthropic-compatible messages.
21#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
22#[non_exhaustive]
23#[serde(rename_all = "lowercase")]
24pub enum AnthropicRole {
25    User,
26    Assistant,
27    System,
28}
29
30/// Text block for `system` prompts.
31#[derive(Serialize, Deserialize, Debug, Clone)]
32#[non_exhaustive]
33pub struct AnthropicSystemTextBlock {
34    #[serde(rename = "type")]
35    pub block_type: AnthropicSystemTextBlockType,
36    pub text: String,
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub citations: Option<Vec<Value>>,
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub cache_control: Option<CacheControl>,
41    #[serde(flatten)]
42    pub extra: HashMap<String, Value>,
43}
44
45impl AnthropicSystemTextBlock {
46    pub fn text(text: impl Into<String>) -> Self {
47        Self {
48            block_type: AnthropicSystemTextBlockType::Text,
49            text: text.into(),
50            citations: None,
51            cache_control: None,
52            extra: HashMap::new(),
53        }
54    }
55}
56
57/// Block type for Anthropic system text blocks.
58#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
59#[non_exhaustive]
60#[serde(rename_all = "snake_case")]
61pub enum AnthropicSystemTextBlockType {
62    Text,
63}
64
65/// System prompt format for Anthropic messages.
66#[derive(Serialize, Deserialize, Debug, Clone)]
67#[non_exhaustive]
68#[serde(untagged)]
69pub enum AnthropicSystemPrompt {
70    Text(String),
71    Blocks(Vec<AnthropicSystemTextBlock>),
72}
73
74/// Message content for Anthropic messages.
75#[derive(Serialize, Deserialize, Debug, Clone)]
76#[non_exhaustive]
77#[serde(untagged)]
78pub enum AnthropicMessageContent {
79    Text(String),
80    Parts(Vec<AnthropicContentPart>),
81}
82
83impl From<String> for AnthropicMessageContent {
84    fn from(value: String) -> Self {
85        Self::Text(value)
86    }
87}
88
89impl From<&str> for AnthropicMessageContent {
90    fn from(value: &str) -> Self {
91        Self::Text(value.to_string())
92    }
93}
94
95impl From<Vec<AnthropicContentPart>> for AnthropicMessageContent {
96    fn from(value: Vec<AnthropicContentPart>) -> Self {
97        Self::Parts(value)
98    }
99}
100
101/// Multi-modal content part for Anthropic-compatible messages.
102#[derive(Serialize, Deserialize, Debug, Clone)]
103#[non_exhaustive]
104#[serde(tag = "type", rename_all = "snake_case")]
105pub enum AnthropicContentPart {
106    Text {
107        text: String,
108        #[serde(skip_serializing_if = "Option::is_none")]
109        citations: Option<Vec<Value>>,
110        #[serde(skip_serializing_if = "Option::is_none")]
111        cache_control: Option<CacheControl>,
112    },
113    Image {
114        source: Value,
115        #[serde(skip_serializing_if = "Option::is_none")]
116        cache_control: Option<CacheControl>,
117    },
118    Document {
119        source: Value,
120        #[serde(skip_serializing_if = "Option::is_none")]
121        title: Option<String>,
122        #[serde(skip_serializing_if = "Option::is_none")]
123        context: Option<String>,
124        #[serde(skip_serializing_if = "Option::is_none")]
125        citations: Option<Vec<Value>>,
126        #[serde(skip_serializing_if = "Option::is_none")]
127        cache_control: Option<CacheControl>,
128    },
129    ToolUse {
130        id: String,
131        name: String,
132        #[serde(skip_serializing_if = "Option::is_none")]
133        input: Option<Value>,
134        #[serde(skip_serializing_if = "Option::is_none")]
135        cache_control: Option<CacheControl>,
136    },
137    ToolResult {
138        tool_use_id: String,
139        #[serde(skip_serializing_if = "Option::is_none")]
140        content: Option<AnthropicMessageContent>,
141        #[serde(skip_serializing_if = "Option::is_none")]
142        is_error: Option<bool>,
143        #[serde(skip_serializing_if = "Option::is_none")]
144        cache_control: Option<CacheControl>,
145    },
146    Thinking {
147        thinking: String,
148        signature: String,
149    },
150    RedactedThinking {
151        data: String,
152    },
153    ServerToolUse {
154        id: String,
155        name: String,
156        #[serde(skip_serializing_if = "Option::is_none")]
157        input: Option<Value>,
158        #[serde(skip_serializing_if = "Option::is_none")]
159        cache_control: Option<CacheControl>,
160    },
161    WebSearchToolResult {
162        tool_use_id: String,
163        content: Value,
164        #[serde(skip_serializing_if = "Option::is_none")]
165        cache_control: Option<CacheControl>,
166    },
167    SearchResult {
168        source: String,
169        title: String,
170        content: Value,
171        #[serde(skip_serializing_if = "Option::is_none")]
172        citations: Option<Vec<Value>>,
173        #[serde(skip_serializing_if = "Option::is_none")]
174        cache_control: Option<CacheControl>,
175    },
176    Compaction {
177        content: Option<String>,
178        #[serde(skip_serializing_if = "Option::is_none")]
179        encrypted_content: Option<String>,
180    },
181    AdvisorToolResult {
182        tool_use_id: String,
183        content: Value,
184    },
185    ToolReference {
186        tool_name: String,
187    },
188    ToolAddition {
189        tool: Value,
190        #[serde(skip_serializing_if = "Option::is_none")]
191        cache_control: Option<CacheControl>,
192    },
193    ToolRemoval {
194        tool: Value,
195        #[serde(skip_serializing_if = "Option::is_none")]
196        cache_control: Option<CacheControl>,
197    },
198}
199
200impl AnthropicContentPart {
201    pub fn text(text: impl Into<String>) -> Self {
202        Self::Text {
203            text: text.into(),
204            citations: None,
205            cache_control: None,
206        }
207    }
208
209    pub fn image_url(url: impl Into<String>) -> Self {
210        Self::Image {
211            source: json!({
212                "type": "url",
213                "url": url.into()
214            }),
215            cache_control: None,
216        }
217    }
218
219    pub fn image_base64(media_type: impl Into<String>, data: impl Into<String>) -> Self {
220        Self::Image {
221            source: json!({
222                "type": "base64",
223                "media_type": media_type.into(),
224                "data": data.into()
225            }),
226            cache_control: None,
227        }
228    }
229
230    pub fn document_url(url: impl Into<String>) -> Self {
231        Self::Document {
232            source: json!({
233                "type": "url",
234                "url": url.into()
235            }),
236            title: None,
237            context: None,
238            citations: None,
239            cache_control: None,
240        }
241    }
242
243    pub fn document_file_id(file_id: impl Into<String>) -> Self {
244        Self::Document {
245            source: json!({
246                "type": "file",
247                "file_id": file_id.into()
248            }),
249            title: None,
250            context: None,
251            citations: None,
252            cache_control: None,
253        }
254    }
255
256    pub fn tool_use(
257        id: impl Into<String>,
258        name: impl Into<String>,
259        input: impl Into<Value>,
260    ) -> Self {
261        Self::ToolUse {
262            id: id.into(),
263            name: name.into(),
264            input: Some(input.into()),
265            cache_control: None,
266        }
267    }
268
269    pub fn tool_result(
270        tool_use_id: impl Into<String>,
271        content: impl Into<AnthropicMessageContent>,
272    ) -> Self {
273        Self::ToolResult {
274            tool_use_id: tool_use_id.into(),
275            content: Some(content.into()),
276            is_error: None,
277            cache_control: None,
278        }
279    }
280}
281
282/// A user/assistant message in Anthropic format.
283#[derive(Serialize, Deserialize, Debug, Clone)]
284#[non_exhaustive]
285pub struct AnthropicMessage {
286    pub role: AnthropicRole,
287    pub content: AnthropicMessageContent,
288}
289
290impl AnthropicMessage {
291    pub fn new(role: AnthropicRole, content: impl Into<AnthropicMessageContent>) -> Self {
292        Self {
293            role,
294            content: content.into(),
295        }
296    }
297
298    pub fn user(content: impl Into<AnthropicMessageContent>) -> Self {
299        Self::new(AnthropicRole::User, content)
300    }
301
302    pub fn assistant(content: impl Into<AnthropicMessageContent>) -> Self {
303        Self::new(AnthropicRole::Assistant, content)
304    }
305
306    pub fn system(content: impl Into<AnthropicMessageContent>) -> Self {
307        Self::new(AnthropicRole::System, content)
308    }
309
310    pub fn with_parts(role: AnthropicRole, parts: Vec<AnthropicContentPart>) -> Self {
311        Self {
312            role,
313            content: AnthropicMessageContent::Parts(parts),
314        }
315    }
316}
317
318/// Anthropic metadata payload.
319#[derive(Serialize, Deserialize, Debug, Clone, Default)]
320#[non_exhaustive]
321pub struct AnthropicMessagesMetadata {
322    #[serde(skip_serializing_if = "Option::is_none")]
323    pub user_id: Option<String>,
324    #[serde(flatten)]
325    pub extra: HashMap<String, Value>,
326}
327
328impl AnthropicMessagesMetadata {
329    pub fn with_user_id(user_id: impl Into<String>) -> Self {
330        Self {
331            user_id: Some(user_id.into()),
332            extra: HashMap::new(),
333        }
334    }
335}
336
337/// Tool definition for Anthropic-compatible messages.
338#[derive(Serialize, Deserialize, Debug, Clone)]
339#[non_exhaustive]
340pub struct AnthropicTool {
341    pub name: String,
342    #[serde(skip_serializing_if = "Option::is_none")]
343    pub description: Option<String>,
344    #[serde(skip_serializing_if = "Option::is_none")]
345    pub input_schema: Option<Value>,
346    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
347    pub tool_type: Option<String>,
348    #[serde(skip_serializing_if = "Option::is_none")]
349    pub cache_control: Option<CacheControl>,
350    #[serde(flatten)]
351    pub extra: HashMap<String, Value>,
352}
353
354impl AnthropicTool {
355    pub fn custom(
356        name: impl Into<String>,
357        description: impl Into<String>,
358        input_schema: impl Into<Value>,
359    ) -> Self {
360        Self {
361            name: name.into(),
362            description: Some(description.into()),
363            input_schema: Some(input_schema.into()),
364            tool_type: Some("custom".to_string()),
365            cache_control: None,
366            extra: HashMap::new(),
367        }
368    }
369
370    pub fn hosted(tool_type: impl Into<String>, name: impl Into<String>) -> Self {
371        Self {
372            name: name.into(),
373            description: None,
374            input_schema: None,
375            tool_type: Some(tool_type.into()),
376            cache_control: None,
377            extra: HashMap::new(),
378        }
379    }
380
381    pub fn option(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
382        self.extra.insert(key.into(), value.into());
383        self
384    }
385}
386
387/// Tool choice policy for Anthropic-compatible messages.
388#[derive(Serialize, Deserialize, Debug, Clone)]
389#[non_exhaustive]
390#[serde(tag = "type", rename_all = "snake_case")]
391pub enum AnthropicToolChoice {
392    Auto {
393        #[serde(skip_serializing_if = "Option::is_none")]
394        disable_parallel_tool_use: Option<bool>,
395    },
396    Any {
397        #[serde(skip_serializing_if = "Option::is_none")]
398        disable_parallel_tool_use: Option<bool>,
399    },
400    None,
401    Tool {
402        name: String,
403        #[serde(skip_serializing_if = "Option::is_none")]
404        disable_parallel_tool_use: Option<bool>,
405    },
406}
407
408impl AnthropicToolChoice {
409    pub fn auto() -> Self {
410        Self::Auto {
411            disable_parallel_tool_use: None,
412        }
413    }
414
415    pub fn any() -> Self {
416        Self::Any {
417            disable_parallel_tool_use: None,
418        }
419    }
420
421    pub fn none() -> Self {
422        Self::None
423    }
424
425    pub fn tool(name: impl Into<String>) -> Self {
426        Self::Tool {
427            name: name.into(),
428            disable_parallel_tool_use: None,
429        }
430    }
431}
432
433/// Thinking control for Anthropic-compatible messages.
434#[derive(Serialize, Deserialize, Debug, Clone)]
435#[non_exhaustive]
436#[serde(tag = "type", rename_all = "snake_case")]
437pub enum AnthropicThinking {
438    Enabled { budget_tokens: u32 },
439    Disabled,
440    Adaptive,
441}
442
443impl AnthropicThinking {
444    pub fn enabled(budget_tokens: u32) -> Self {
445        Self::Enabled { budget_tokens }
446    }
447
448    pub fn disabled() -> Self {
449        Self::Disabled
450    }
451
452    pub fn adaptive() -> Self {
453        Self::Adaptive
454    }
455}
456
457/// Output effort level for Anthropic output configuration.
458#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
459#[non_exhaustive]
460#[serde(rename_all = "lowercase")]
461pub enum AnthropicOutputEffort {
462    Low,
463    Medium,
464    High,
465    Max,
466}
467
468/// Output config for Anthropic messages.
469#[derive(Serialize, Deserialize, Debug, Clone, Default)]
470#[non_exhaustive]
471pub struct AnthropicOutputConfig {
472    #[serde(skip_serializing_if = "Option::is_none")]
473    pub effort: Option<AnthropicOutputEffort>,
474}
475
476impl AnthropicOutputConfig {
477    pub fn with_effort(effort: AnthropicOutputEffort) -> Self {
478        Self {
479            effort: Some(effort),
480        }
481    }
482}
483
484/// Request body for `POST /messages`.
485#[derive(Debug, Clone, Builder)]
486#[builder(build_fn(error = "OpenRouterError"))]
487#[non_exhaustive]
488pub struct AnthropicMessagesRequest {
489    #[builder(setter(into))]
490    model: String,
491
492    max_tokens: u32,
493
494    messages: Vec<AnthropicMessage>,
495
496    #[builder(setter(strip_option), default)]
497    system: Option<AnthropicSystemPrompt>,
498
499    #[builder(setter(strip_option), default)]
500    metadata: Option<AnthropicMessagesMetadata>,
501
502    #[builder(setter(custom), default)]
503    stop_sequences: Option<Vec<String>>,
504
505    #[builder(setter(skip), default)]
506    stream: Option<bool>,
507
508    #[builder(setter(strip_option), default)]
509    experimental_metadata: Option<OpenRouterExperimentalMetadata>,
510
511    #[builder(setter(strip_option), default)]
512    temperature: Option<f64>,
513
514    #[builder(setter(strip_option), default)]
515    top_p: Option<f64>,
516
517    #[builder(setter(strip_option), default)]
518    top_k: Option<u32>,
519
520    #[builder(setter(custom), default)]
521    tools: Option<Vec<AnthropicTool>>,
522
523    #[builder(setter(custom), default)]
524    server_tools: Option<Vec<crate::types::ServerTool>>,
525
526    #[builder(setter(strip_option), default)]
527    tool_choice: Option<AnthropicToolChoice>,
528
529    #[builder(setter(strip_option), default)]
530    thinking: Option<AnthropicThinking>,
531
532    #[builder(setter(into, strip_option), default)]
533    service_tier: Option<String>,
534
535    #[builder(setter(strip_option), default)]
536    provider: Option<ProviderPreferences>,
537
538    #[builder(setter(custom), default)]
539    plugins: Option<Vec<Plugin>>,
540
541    #[builder(setter(into, strip_option), default)]
542    route: Option<String>,
543
544    #[builder(setter(into, strip_option), default)]
545    user: Option<String>,
546
547    #[builder(setter(into, strip_option), default)]
548    session_id: Option<String>,
549
550    #[builder(setter(strip_option), default)]
551    trace: Option<TraceOptions>,
552
553    #[builder(setter(custom), default)]
554    models: Option<Vec<String>>,
555
556    #[builder(setter(strip_option), default)]
557    output_config: Option<AnthropicOutputConfig>,
558}
559
560#[derive(Deserialize)]
561struct AnthropicMessagesRequestWire {
562    model: String,
563    max_tokens: u32,
564    messages: Vec<AnthropicMessage>,
565    system: Option<AnthropicSystemPrompt>,
566    metadata: Option<AnthropicMessagesMetadata>,
567    stop_sequences: Option<Vec<String>>,
568    stream: Option<bool>,
569    #[serde(skip)]
570    experimental_metadata: Option<OpenRouterExperimentalMetadata>,
571    temperature: Option<f64>,
572    top_p: Option<f64>,
573    top_k: Option<u32>,
574    tools: Option<Vec<Value>>,
575    tool_choice: Option<AnthropicToolChoice>,
576    thinking: Option<AnthropicThinking>,
577    service_tier: Option<String>,
578    provider: Option<ProviderPreferences>,
579    plugins: Option<Vec<Plugin>>,
580    route: Option<String>,
581    user: Option<String>,
582    session_id: Option<String>,
583    trace: Option<TraceOptions>,
584    models: Option<Vec<String>>,
585    output_config: Option<AnthropicOutputConfig>,
586}
587
588type SplitAnthropicMessageTools = (
589    Option<Vec<AnthropicTool>>,
590    Option<Vec<crate::types::ServerTool>>,
591);
592
593impl<'de> Deserialize<'de> for AnthropicMessagesRequest {
594    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
595    where
596        D: Deserializer<'de>,
597    {
598        let wire = AnthropicMessagesRequestWire::deserialize(deserializer)?;
599        let (tools, server_tools) = split_anthropic_message_tools(wire.tools).map_err(|error| {
600            de::Error::custom(format!("invalid Anthropic messages tools entry: {error}"))
601        })?;
602
603        Ok(Self {
604            model: wire.model,
605            max_tokens: wire.max_tokens,
606            messages: wire.messages,
607            system: wire.system,
608            metadata: wire.metadata,
609            stop_sequences: wire.stop_sequences,
610            stream: wire.stream,
611            experimental_metadata: wire.experimental_metadata,
612            temperature: wire.temperature,
613            top_p: wire.top_p,
614            top_k: wire.top_k,
615            tools,
616            server_tools,
617            tool_choice: wire.tool_choice,
618            thinking: wire.thinking,
619            service_tier: wire.service_tier,
620            provider: wire.provider,
621            plugins: wire.plugins,
622            route: wire.route,
623            user: wire.user,
624            session_id: wire.session_id,
625            trace: wire.trace,
626            models: wire.models,
627            output_config: wire.output_config,
628        })
629    }
630}
631
632fn split_anthropic_message_tools(
633    tools: Option<Vec<Value>>,
634) -> Result<SplitAnthropicMessageTools, serde_json::Error> {
635    let Some(values) = tools else {
636        return Ok((None, None));
637    };
638    let was_empty = values.is_empty();
639    let mut anthropic_tools = Vec::new();
640    let mut server_tools = Vec::new();
641
642    for value in values {
643        if value.get("name").is_some() {
644            anthropic_tools.push(serde_json::from_value(value)?);
645        } else if crate::types::ServerTool::is_server_tool_value(&value) {
646            server_tools.push(serde_json::from_value(value)?);
647        } else {
648            anthropic_tools.push(serde_json::from_value(value)?);
649        }
650    }
651
652    let tools = if anthropic_tools.is_empty() && !was_empty {
653        None
654    } else {
655        Some(anthropic_tools)
656    };
657    let server_tools = if server_tools.is_empty() {
658        None
659    } else {
660        Some(server_tools)
661    };
662
663    Ok((tools, server_tools))
664}
665
666fn insert_json_field<T, E>(
667    map: &mut serde_json::Map<String, Value>,
668    key: &str,
669    value: &T,
670) -> Result<(), E>
671where
672    T: Serialize,
673    E: serde::ser::Error,
674{
675    map.insert(
676        key.to_string(),
677        serde_json::to_value(value).map_err(E::custom)?,
678    );
679    Ok(())
680}
681
682fn insert_json_option<T, E>(
683    map: &mut serde_json::Map<String, Value>,
684    key: &str,
685    value: &Option<T>,
686) -> Result<(), E>
687where
688    T: Serialize,
689    E: serde::ser::Error,
690{
691    if let Some(value) = value {
692        insert_json_field::<T, E>(map, key, value)?;
693    }
694    Ok(())
695}
696
697impl Serialize for AnthropicMessagesRequest {
698    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
699    where
700        S: serde::Serializer,
701    {
702        let mut map = serde_json::Map::new();
703
704        insert_json_field::<_, S::Error>(&mut map, "model", &self.model)?;
705        insert_json_field::<_, S::Error>(&mut map, "max_tokens", &self.max_tokens)?;
706        insert_json_field::<_, S::Error>(&mut map, "messages", &self.messages)?;
707        insert_json_option::<_, S::Error>(&mut map, "system", &self.system)?;
708        insert_json_option::<_, S::Error>(&mut map, "metadata", &self.metadata)?;
709        insert_json_option::<_, S::Error>(&mut map, "stop_sequences", &self.stop_sequences)?;
710        insert_json_option::<_, S::Error>(&mut map, "stream", &self.stream)?;
711        insert_json_option::<_, S::Error>(&mut map, "temperature", &self.temperature)?;
712        insert_json_option::<_, S::Error>(&mut map, "top_p", &self.top_p)?;
713        insert_json_option::<_, S::Error>(&mut map, "top_k", &self.top_k)?;
714
715        let anthropic_tools = self.tools.as_deref().unwrap_or_default();
716        let server_tools = self.server_tools.as_deref().unwrap_or_default();
717        if self.tools.is_some() || self.server_tools.is_some() {
718            let mut tools = Vec::with_capacity(anthropic_tools.len() + server_tools.len());
719            for tool in anthropic_tools {
720                tools.push(serde_json::to_value(tool).map_err(serde::ser::Error::custom)?);
721            }
722            for tool in server_tools {
723                tools.push(serde_json::to_value(tool).map_err(serde::ser::Error::custom)?);
724            }
725            map.insert("tools".to_string(), Value::Array(tools));
726        }
727
728        insert_json_option::<_, S::Error>(&mut map, "tool_choice", &self.tool_choice)?;
729        insert_json_option::<_, S::Error>(&mut map, "thinking", &self.thinking)?;
730        insert_json_option::<_, S::Error>(&mut map, "service_tier", &self.service_tier)?;
731        insert_json_option::<_, S::Error>(&mut map, "provider", &self.provider)?;
732        insert_json_option::<_, S::Error>(&mut map, "plugins", &self.plugins)?;
733        insert_json_option::<_, S::Error>(&mut map, "route", &self.route)?;
734        insert_json_option::<_, S::Error>(&mut map, "user", &self.user)?;
735        insert_json_option::<_, S::Error>(&mut map, "session_id", &self.session_id)?;
736        insert_json_option::<_, S::Error>(&mut map, "trace", &self.trace)?;
737        insert_json_option::<_, S::Error>(&mut map, "models", &self.models)?;
738        insert_json_option::<_, S::Error>(&mut map, "output_config", &self.output_config)?;
739
740        Value::Object(map).serialize(serializer)
741    }
742}
743
744impl AnthropicMessagesRequestBuilder {
745    strip_option_vec_setter!(stop_sequences, String);
746    strip_option_vec_setter!(tools, AnthropicTool);
747    strip_option_vec_setter!(server_tools, crate::types::ServerTool);
748    strip_option_vec_setter!(plugins, Plugin);
749    strip_option_vec_setter!(models, String);
750
751    pub fn tool(&mut self, tool: AnthropicTool) -> &mut Self {
752        if let Some(Some(ref mut existing_tools)) = self.tools {
753            existing_tools.push(tool);
754        } else {
755            self.tools = Some(Some(vec![tool]));
756        }
757        self
758    }
759
760    pub fn server_tool(&mut self, tool: crate::types::ServerTool) -> &mut Self {
761        if let Some(Some(ref mut existing_tools)) = self.server_tools {
762            existing_tools.push(tool);
763        } else {
764            self.server_tools = Some(Some(vec![tool]));
765        }
766        self
767    }
768
769    pub fn add_message(&mut self, message: AnthropicMessage) -> &mut Self {
770        if let Some(ref mut messages) = self.messages {
771            messages.push(message);
772        } else {
773            self.messages = Some(vec![message]);
774        }
775        self
776    }
777
778    pub fn thinking_enabled(&mut self, budget_tokens: u32) -> &mut Self {
779        self.thinking = Some(Some(AnthropicThinking::enabled(budget_tokens)));
780        self
781    }
782}
783
784impl AnthropicMessagesRequest {
785    pub fn builder() -> AnthropicMessagesRequestBuilder {
786        AnthropicMessagesRequestBuilder::default()
787    }
788
789    pub fn new(model: impl Into<String>, max_tokens: u32, messages: Vec<AnthropicMessage>) -> Self {
790        Self::builder()
791            .model(model.into())
792            .max_tokens(max_tokens)
793            .messages(messages)
794            .build()
795            .expect("Failed to build AnthropicMessagesRequest")
796    }
797
798    pub fn messages(&self) -> &[AnthropicMessage] {
799        &self.messages
800    }
801
802    pub fn tools(&self) -> Option<&[AnthropicTool]> {
803        self.tools.as_deref()
804    }
805
806    pub fn server_tools(&self) -> Option<&[crate::types::ServerTool]> {
807        self.server_tools.as_deref()
808    }
809
810    pub(crate) fn requires_openrouter_files_tool_header(&self) -> bool {
811        self.server_tools
812            .as_deref()
813            .is_some_and(|tools| tools.iter().any(crate::types::ServerTool::is_files_tool))
814    }
815
816    fn stream(&self, stream: bool) -> Self {
817        let mut req = self.clone();
818        req.stream = Some(stream);
819        req
820    }
821
822    pub fn experimental_metadata(&self) -> Option<OpenRouterExperimentalMetadata> {
823        self.experimental_metadata
824    }
825}
826
827/// Usage object in Anthropic messages response.
828#[derive(Serialize, Deserialize, Debug, Clone, Default)]
829#[non_exhaustive]
830pub struct AnthropicMessagesUsage {
831    #[serde(skip_serializing_if = "Option::is_none")]
832    pub input_tokens: Option<u64>,
833    #[serde(skip_serializing_if = "Option::is_none")]
834    pub output_tokens: Option<u64>,
835    #[serde(skip_serializing_if = "Option::is_none")]
836    pub cache_creation_input_tokens: Option<u64>,
837    #[serde(skip_serializing_if = "Option::is_none")]
838    pub cache_read_input_tokens: Option<u64>,
839    #[serde(skip_serializing_if = "Option::is_none")]
840    pub cache_creation: Option<AnthropicCacheCreation>,
841    #[serde(skip_serializing_if = "Option::is_none")]
842    pub service_tier: Option<String>,
843    #[serde(flatten)]
844    pub extra: HashMap<String, Value>,
845}
846
847/// Non-streaming response payload returned by `POST /messages`.
848#[derive(Serialize, Deserialize, Debug, Clone, Default)]
849#[non_exhaustive]
850pub struct AnthropicMessagesResponse {
851    #[serde(skip_serializing_if = "Option::is_none")]
852    pub id: Option<String>,
853    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
854    pub object_type: Option<String>,
855    #[serde(skip_serializing_if = "Option::is_none")]
856    pub role: Option<String>,
857    #[serde(default, skip_serializing_if = "Vec::is_empty")]
858    pub content: Vec<AnthropicContentPart>,
859    #[serde(skip_serializing_if = "Option::is_none")]
860    pub model: Option<String>,
861    #[serde(skip_serializing_if = "Option::is_none")]
862    pub stop_reason: Option<String>,
863    #[serde(skip_serializing_if = "Option::is_none")]
864    pub stop_sequence: Option<String>,
865    #[serde(skip_serializing_if = "Option::is_none")]
866    pub usage: Option<AnthropicMessagesUsage>,
867    #[serde(flatten)]
868    pub extra: HashMap<String, Value>,
869}
870
871/// Streaming data event payload for `POST /messages` when `stream=true`.
872#[derive(Serialize, Deserialize, Debug, Clone)]
873#[non_exhaustive]
874#[serde(tag = "type", rename_all = "snake_case")]
875pub enum AnthropicMessagesStreamEvent {
876    MessageStart {
877        message: Box<AnthropicMessagesResponse>,
878    },
879    MessageDelta {
880        delta: Value,
881        usage: Value,
882    },
883    MessageStop {
884        #[serde(default, skip_serializing_if = "Option::is_none")]
885        openrouter_metadata: Option<Value>,
886        #[serde(flatten)]
887        extra: HashMap<String, Value>,
888    },
889    ContentBlockStart {
890        index: u32,
891        content_block: Box<AnthropicContentPart>,
892    },
893    ContentBlockDelta {
894        index: u32,
895        delta: Value,
896    },
897    ContentBlockStop {
898        index: u32,
899    },
900    Ping,
901    Error {
902        error: Value,
903    },
904}
905
906impl AnthropicMessagesStreamEvent {
907    pub fn event_type(&self) -> &'static str {
908        match self {
909            Self::MessageStart { .. } => "message_start",
910            Self::MessageDelta { .. } => "message_delta",
911            Self::MessageStop { .. } => "message_stop",
912            Self::ContentBlockStart { .. } => "content_block_start",
913            Self::ContentBlockDelta { .. } => "content_block_delta",
914            Self::ContentBlockStop { .. } => "content_block_stop",
915            Self::Ping => "ping",
916            Self::Error { .. } => "error",
917        }
918    }
919}
920
921/// Streaming SSE envelope returned by `POST /messages`.
922#[derive(Serialize, Deserialize, Debug, Clone)]
923#[non_exhaustive]
924pub struct AnthropicMessagesSseEvent {
925    pub event: String,
926    pub data: AnthropicMessagesStreamEvent,
927}
928
929/// Send a non-streaming request to the Anthropic-compatible Messages API.
930pub async fn create_message(
931    base_url: &str,
932    api_key: &str,
933    x_title: &Option<String>,
934    http_referer: &Option<String>,
935    app_categories: &Option<Vec<String>>,
936    request: &AnthropicMessagesRequest,
937) -> Result<AnthropicMessagesResponse, OpenRouterError> {
938    let http_client = crate::transport::new_client()?;
939    create_message_with_client(
940        &http_client,
941        base_url,
942        api_key,
943        x_title,
944        http_referer,
945        app_categories,
946        request,
947    )
948    .await
949}
950
951pub(crate) async fn create_message_with_client(
952    http_client: &HttpClient,
953    base_url: &str,
954    api_key: &str,
955    x_title: &Option<String>,
956    http_referer: &Option<String>,
957    app_categories: &Option<Vec<String>>,
958    request: &AnthropicMessagesRequest,
959) -> Result<AnthropicMessagesResponse, OpenRouterError> {
960    let url = format!("{base_url}/messages");
961    let request = request.stream(false);
962
963    let request_builder = transport_request::with_experimental_metadata_header(
964        transport_request::with_client_request_headers(
965            transport_request::post(http_client, &url),
966            api_key,
967            x_title,
968            http_referer,
969            app_categories,
970        )?,
971        &request.experimental_metadata,
972    );
973    let request_builder = transport_request::with_openrouter_files_tool_header(
974        request_builder,
975        request.requires_openrouter_files_tool_header(),
976    );
977
978    let response = request_builder.json(&request).send().await?;
979
980    if response.status().is_success() {
981        let response_data: AnthropicMessagesResponse =
982            transport_response::parse_json_response(response, "messages API").await?;
983        Ok(response_data)
984    } else {
985        transport_response::handle_error(response).await?;
986        unreachable!()
987    }
988}
989
990/// Send a streaming request to the Anthropic-compatible Messages API.
991pub async fn stream_messages(
992    base_url: &str,
993    api_key: &str,
994    x_title: &Option<String>,
995    http_referer: &Option<String>,
996    app_categories: &Option<Vec<String>>,
997    request: &AnthropicMessagesRequest,
998) -> Result<BoxStream<'static, Result<AnthropicMessagesSseEvent, OpenRouterError>>, OpenRouterError>
999{
1000    let http_client = crate::transport::new_client()?;
1001    stream_messages_with_client(
1002        &http_client,
1003        base_url,
1004        api_key,
1005        x_title,
1006        http_referer,
1007        app_categories,
1008        request,
1009    )
1010    .await
1011}
1012
1013pub(crate) async fn stream_messages_with_client(
1014    http_client: &HttpClient,
1015    base_url: &str,
1016    api_key: &str,
1017    x_title: &Option<String>,
1018    http_referer: &Option<String>,
1019    app_categories: &Option<Vec<String>>,
1020    request: &AnthropicMessagesRequest,
1021) -> Result<BoxStream<'static, Result<AnthropicMessagesSseEvent, OpenRouterError>>, OpenRouterError>
1022{
1023    let url = format!("{base_url}/messages");
1024    let request = request.stream(true);
1025
1026    let request_builder = transport_request::with_experimental_metadata_header(
1027        transport_request::with_client_request_headers(
1028            transport_request::post(http_client, &url),
1029            api_key,
1030            x_title,
1031            http_referer,
1032            app_categories,
1033        )?,
1034        &request.experimental_metadata,
1035    );
1036    let request_builder = transport_request::with_openrouter_files_tool_header(
1037        request_builder,
1038        request.requires_openrouter_files_tool_header(),
1039    );
1040
1041    let response = request_builder.json(&request).send().await?;
1042
1043    if response.status().is_success() {
1044        let stream = parse_sse_frames(response_lines(response))
1045            .filter_map(async |frame| match frame {
1046                Ok(frame) if frame.data == "[DONE]" => None,
1047                Ok(frame) => Some(
1048                    serde_json::from_str::<AnthropicMessagesStreamEvent>(&frame.data)
1049                        .map_err(OpenRouterError::Serialization)
1050                        .map(|payload| AnthropicMessagesSseEvent {
1051                            event: frame
1052                                .event
1053                                .unwrap_or_else(|| payload.event_type().to_string()),
1054                            data: payload,
1055                        }),
1056                ),
1057                Err(error) => Some(Err(error)),
1058            })
1059            .boxed();
1060
1061        Ok(stream)
1062    } else {
1063        transport_response::handle_error(response).await?;
1064        unreachable!()
1065    }
1066}