Skip to main content

llm/providers/openrouter/
types.rs

1use async_openai::types::chat::{
2    ChatCompletionStreamOptions, ChatCompletionToolChoiceOption, ChatCompletionTools, ResponseFormat, StopConfiguration,
3};
4use serde::{Deserialize, Serialize};
5
6use crate::providers::openai_compatible::CompatibleChatRequest;
7use crate::providers::openai_compatible::types::CompatibleChatMessage;
8
9/// OpenRouter-specific usage configuration
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct OpenRouterUsage {
12    #[serde(rename = "include")]
13    pub include: bool,
14}
15
16/// Cache control marker for `OpenRouter` prompt caching.
17/// Enables automatic prefix caching and sticky routing.
18/// See: <https://openrouter.ai/docs/guides/best-practices/prompt-caching>
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct CacheControl {
21    #[serde(rename = "type")]
22    pub cache_type: String,
23}
24
25impl CacheControl {
26    pub fn ephemeral() -> Self {
27        Self { cache_type: "ephemeral".to_string() }
28    }
29}
30
31/// Custom request type for `OpenRouter` that includes the usage parameter
32///
33/// `OpenRouter` requires a specific `usage` parameter in the request body to enable
34/// token usage tracking. See: <https://openrouter.ai/docs/use-cases/usage-accounting>
35#[derive(Debug, Clone, Serialize)]
36pub struct OpenRouterChatRequest {
37    pub model: String,
38    pub messages: Vec<CompatibleChatMessage>,
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub stream: Option<bool>,
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub tools: Option<Vec<ChatCompletionTools>>,
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub tool_choice: Option<ChatCompletionToolChoiceOption>,
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub temperature: Option<f32>,
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub top_p: Option<f32>,
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub max_completion_tokens: Option<u32>,
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub stream_options: Option<ChatCompletionStreamOptions>,
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub usage: Option<OpenRouterUsage>,
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub presence_penalty: Option<f32>,
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub frequency_penalty: Option<f32>,
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub stop: Option<StopConfiguration>,
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub response_format: Option<ResponseFormat>,
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub reasoning_effort: Option<crate::ReasoningEffort>,
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub cache_control: Option<CacheControl>,
67}
68
69impl From<CompatibleChatRequest> for OpenRouterChatRequest {
70    fn from(request: CompatibleChatRequest) -> Self {
71        Self {
72            model: request.model,
73            messages: request.messages,
74            stream: request.stream,
75            tools: request.tools,
76            tool_choice: None,
77            temperature: None,
78            top_p: None,
79            max_completion_tokens: None,
80            stream_options: Some(ChatCompletionStreamOptions { include_usage: Some(true), include_obfuscation: None }),
81            usage: Some(OpenRouterUsage { include: true }),
82            presence_penalty: None,
83            frequency_penalty: None,
84            stop: None,
85            response_format: None,
86            reasoning_effort: None,
87            cache_control: Some(CacheControl::ephemeral()),
88        }
89    }
90}