openrouter_provider/
lib.rs

1//! Type definitions for the [OpenRouter Provider API](https://openrouter.ai/docs/guides/for-providers).
2
3use serde::{Deserialize, Serialize};
4
5/// Response from the list models endpoint.
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
7pub struct ListModelsResponse {
8    pub data: Vec<Model>,
9}
10
11/// A model available from the provider.
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
13pub struct Model {
14    /// e.g., "anthropic/claude-sonnet-4"
15    pub id: String,
16    pub name: String,
17    /// Unix timestamp.
18    pub created: i64,
19    pub input_modalities: Vec<InputModality>,
20    pub output_modalities: Vec<OutputModality>,
21    pub quantization: Quantization,
22    /// Max input tokens.
23    pub context_length: u64,
24    /// Max output tokens.
25    pub max_output_length: u64,
26    pub pricing: Pricing,
27    pub supported_sampling_parameters: Vec<SamplingParameter>,
28    pub supported_features: Vec<Feature>,
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub openrouter: Option<OpenRouterInfo>,
31    /// Required for Hugging Face models.
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub hugging_face_id: Option<String>,
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub description: Option<String>,
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub datacenters: Option<Vec<Datacenter>>,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
41pub struct OpenRouterInfo {
42    /// e.g., "anthropic/claude-sonnet-4"
43    pub slug: String,
44}
45
46/// USD pricing as strings to prevent floating-point errors.
47#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
48pub struct Pricing {
49    pub prompt: String,
50    pub completion: String,
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub image: Option<String>,
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub request: Option<String>,
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub input_cache_read: Option<String>,
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub input_cache_write: Option<String>,
59}
60
61impl Pricing {
62    pub fn new(prompt: impl Into<String>, completion: impl Into<String>) -> Self {
63        Self {
64            prompt: prompt.into(),
65            completion: completion.into(),
66            image: None,
67            request: None,
68            input_cache_read: None,
69            input_cache_write: None,
70        }
71    }
72}
73
74#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
75#[serde(rename_all = "snake_case")]
76pub enum InputModality {
77    Text,
78    File,
79    Image,
80    Audio,
81    Video,
82}
83
84#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
85#[serde(rename_all = "snake_case")]
86pub enum OutputModality {
87    Text,
88    Image,
89}
90
91#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
92#[serde(rename_all = "snake_case")]
93pub enum Quantization {
94    Int4,
95    Int8,
96    Fp4,
97    Fp6,
98    Fp8,
99    Fp16,
100    Bf16,
101    Fp32,
102}
103
104#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
105#[serde(rename_all = "snake_case")]
106pub enum SamplingParameter {
107    Temperature,
108    TopP,
109    TopK,
110    RepetitionPenalty,
111    FrequencyPenalty,
112    PresencePenalty,
113    Stop,
114    Seed,
115}
116
117#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
118#[serde(rename_all = "snake_case")]
119pub enum Feature {
120    Tools,
121    JsonMode,
122    StructuredOutputs,
123    WebSearch,
124    Reasoning,
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
128pub struct Datacenter {
129    /// ISO 3166-1 alpha-2 code (e.g., "US", "DE").
130    pub country_code: String,
131}
132
133impl Datacenter {
134    pub fn new(country_code: impl Into<String>) -> Self {
135        Self {
136            country_code: country_code.into(),
137        }
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[test]
146    fn test_round_trip() {
147        let original = ListModelsResponse {
148            data: vec![Model {
149                id: "org/model".to_string(),
150                name: "Model".to_string(),
151                created: 1700000000,
152                input_modalities: vec![InputModality::Text, InputModality::Image],
153                output_modalities: vec![OutputModality::Text],
154                quantization: Quantization::Fp16,
155                context_length: 128000,
156                max_output_length: 4096,
157                pricing: Pricing::new("0.001", "0.002"),
158                supported_sampling_parameters: vec![SamplingParameter::Temperature],
159                supported_features: vec![Feature::Tools],
160                openrouter: None,
161                hugging_face_id: None,
162                description: Some("Test".to_string()),
163                datacenters: Some(vec![Datacenter::new("US")]),
164            }],
165        };
166        let json = serde_json::to_string(&original).unwrap();
167        let parsed: ListModelsResponse = serde_json::from_str(&json).unwrap();
168        assert_eq!(original, parsed);
169    }
170
171    #[test]
172    fn test_quantization_serialization() {
173        assert_eq!(
174            serde_json::to_string(&Quantization::Int4).unwrap(),
175            "\"int4\""
176        );
177        assert_eq!(
178            serde_json::to_string(&Quantization::Bf16).unwrap(),
179            "\"bf16\""
180        );
181        assert_eq!(
182            serde_json::to_string(&Quantization::Fp32).unwrap(),
183            "\"fp32\""
184        );
185    }
186
187    #[test]
188    fn test_feature_serialization() {
189        assert_eq!(
190            serde_json::to_string(&Feature::JsonMode).unwrap(),
191            "\"json_mode\""
192        );
193        assert_eq!(
194            serde_json::to_string(&Feature::StructuredOutputs).unwrap(),
195            "\"structured_outputs\""
196        );
197    }
198
199    #[test]
200    fn test_sampling_parameter_serialization() {
201        assert_eq!(
202            serde_json::to_string(&SamplingParameter::TopP).unwrap(),
203            "\"top_p\""
204        );
205        assert_eq!(
206            serde_json::to_string(&SamplingParameter::RepetitionPenalty).unwrap(),
207            "\"repetition_penalty\""
208        );
209    }
210}