Skip to main content

openrouter_rs/api/
models.rs

1use std::collections::HashMap;
2
3use derive_builder::Builder;
4use reqwest::Client as HttpClient;
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7use urlencoding::encode;
8
9use crate::{
10    error::OpenRouterError,
11    transport::{request as transport_request, response as transport_response},
12    types::{ApiResponse, Effort, ModelCategory, SupportedParameters},
13};
14
15#[derive(Serialize, Deserialize, Debug, Clone)]
16#[non_exhaustive]
17pub struct Model {
18    pub id: String,
19    #[serde(default, skip_serializing_if = "Option::is_none")]
20    pub canonical_slug: Option<String>,
21    #[serde(default, skip_serializing_if = "Option::is_none")]
22    pub hugging_face_id: Option<String>,
23    pub name: String,
24    pub created: f64,
25    #[serde(default)]
26    pub description: String,
27    #[serde(default, skip_serializing_if = "Option::is_none")]
28    pub context_length: Option<f64>,
29    pub architecture: Architecture,
30    pub top_provider: TopProvider,
31    pub pricing: Pricing,
32    pub per_request_limits: Option<HashMap<String, String>>,
33    #[serde(default)]
34    pub supported_parameters: Vec<String>,
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub supported_voices: Option<Vec<String>>,
37    #[serde(default, skip_serializing_if = "Option::is_none")]
38    pub default_parameters: Option<Value>,
39    #[serde(default, skip_serializing_if = "Option::is_none")]
40    pub expiration_date: Option<String>,
41    #[serde(default, skip_serializing_if = "Option::is_none")]
42    pub knowledge_cutoff: Option<String>,
43    #[serde(default, skip_serializing_if = "Option::is_none")]
44    pub links: Option<ModelLinks>,
45    #[serde(default, skip_serializing_if = "Option::is_none")]
46    pub benchmarks: Option<ModelBenchmarks>,
47    #[serde(default, skip_serializing_if = "Option::is_none")]
48    pub reasoning: Option<ModelReasoning>,
49    #[serde(flatten)]
50    pub extra: HashMap<String, Value>,
51}
52
53#[derive(Serialize, Deserialize, Debug, Clone)]
54#[non_exhaustive]
55pub struct Architecture {
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub modality: Option<String>,
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub tokenizer: Option<String>,
60    #[serde(default, skip_serializing_if = "Option::is_none")]
61    pub instruct_type: Option<String>,
62    #[serde(default, skip_serializing_if = "Option::is_none")]
63    pub input_modalities: Option<Vec<String>>,
64    #[serde(default, skip_serializing_if = "Option::is_none")]
65    pub output_modalities: Option<Vec<String>>,
66    #[serde(flatten)]
67    pub extra: HashMap<String, Value>,
68}
69
70#[derive(Serialize, Deserialize, Debug, Clone)]
71#[non_exhaustive]
72pub struct TopProvider {
73    pub context_length: Option<f64>,
74    pub max_completion_tokens: Option<f64>,
75    pub is_moderated: bool,
76    #[serde(flatten)]
77    pub extra: HashMap<String, Value>,
78}
79
80#[derive(Serialize, Deserialize, Debug, Clone)]
81#[non_exhaustive]
82pub struct Pricing {
83    pub prompt: String,
84    pub completion: String,
85    pub image: Option<String>,
86    pub request: Option<String>,
87    pub input_cache_read: Option<String>,
88    pub input_cache_write: Option<String>,
89    pub web_search: Option<String>,
90    pub internal_reasoning: Option<String>,
91    #[serde(flatten)]
92    pub extra: HashMap<String, Value>,
93}
94
95#[derive(Serialize, Deserialize, Debug, Clone)]
96#[non_exhaustive]
97pub struct ModelLinks {
98    pub details: String,
99    #[serde(flatten)]
100    pub extra: HashMap<String, Value>,
101}
102
103#[derive(Serialize, Deserialize, Debug, Clone)]
104#[non_exhaustive]
105pub struct AABenchmarkEntry {
106    pub intelligence_index: Option<f64>,
107    pub coding_index: Option<f64>,
108    pub agentic_index: Option<f64>,
109    #[serde(flatten)]
110    pub extra: HashMap<String, Value>,
111}
112
113#[derive(Serialize, Deserialize, Debug, Clone)]
114#[non_exhaustive]
115pub struct DABenchmarkEntry {
116    pub arena: String,
117    pub category: String,
118    pub elo: f64,
119    pub win_rate: f64,
120    pub rank: u64,
121    #[serde(flatten)]
122    pub extra: HashMap<String, Value>,
123}
124
125#[derive(Serialize, Deserialize, Debug, Clone)]
126#[non_exhaustive]
127pub struct ModelBenchmarks {
128    #[serde(default, skip_serializing_if = "Option::is_none")]
129    pub artificial_analysis: Option<AABenchmarkEntry>,
130    #[serde(default)]
131    pub design_arena: Vec<DABenchmarkEntry>,
132    #[serde(flatten)]
133    pub extra: HashMap<String, Value>,
134}
135
136#[derive(Serialize, Deserialize, Debug, Clone)]
137#[non_exhaustive]
138pub struct ModelReasoning {
139    #[serde(default, skip_serializing_if = "Option::is_none")]
140    pub default_effort: Option<Effort>,
141    #[serde(default, skip_serializing_if = "Option::is_none")]
142    pub default_enabled: Option<bool>,
143    pub mandatory: bool,
144    #[serde(default, skip_serializing_if = "Option::is_none")]
145    pub supported_efforts: Option<Vec<Effort>>,
146    #[serde(default, skip_serializing_if = "Option::is_none")]
147    pub supports_max_tokens: Option<bool>,
148    #[serde(flatten)]
149    pub extra: HashMap<String, Value>,
150}
151
152#[derive(Serialize, Deserialize, Debug, Clone)]
153#[non_exhaustive]
154pub struct Endpoint {
155    pub name: String,
156    pub context_length: f64,
157    pub pricing: EndpointPricing,
158    pub provider_name: String,
159    pub supported_parameters: Vec<String>,
160    pub quantization: Option<String>,
161    pub max_completion_tokens: Option<f64>,
162    pub max_prompt_tokens: Option<f64>,
163    pub status: Option<serde_json::Value>,
164}
165
166#[derive(Serialize, Deserialize, Debug, Clone)]
167#[non_exhaustive]
168pub struct EndpointPricing {
169    #[serde(skip_serializing_if = "Option::is_none")]
170    pub request: Option<String>,
171    #[serde(skip_serializing_if = "Option::is_none")]
172    pub image: Option<String>,
173    pub prompt: String,
174    pub completion: String,
175}
176
177#[derive(Serialize, Deserialize, Debug, Clone)]
178#[non_exhaustive]
179pub struct EndpointData {
180    pub id: String,
181    pub name: String,
182    pub created: f64,
183    pub description: String,
184    pub architecture: EndpointArchitecture,
185    pub endpoints: Vec<Endpoint>,
186}
187
188#[derive(Serialize, Deserialize, Debug, Clone)]
189#[non_exhaustive]
190pub struct EndpointArchitecture {
191    pub tokenizer: Option<String>,
192    pub instruct_type: Option<String>,
193    pub modality: Option<String>,
194}
195
196/// Extended query parameters for `GET /models`.
197#[derive(Serialize, Deserialize, Debug, Clone, Default, Builder)]
198#[builder(build_fn(error = "OpenRouterError"))]
199#[non_exhaustive]
200pub struct ListModelsParams {
201    #[builder(setter(strip_option), default)]
202    #[serde(skip_serializing_if = "Option::is_none")]
203    pub category: Option<ModelCategory>,
204    #[builder(setter(strip_option), default)]
205    #[serde(skip_serializing_if = "Option::is_none")]
206    pub supported_parameters: Option<SupportedParameters>,
207    #[builder(setter(into, strip_option), default)]
208    #[serde(skip_serializing_if = "Option::is_none")]
209    pub output_modalities: Option<String>,
210    #[builder(setter(into, strip_option), default)]
211    #[serde(skip_serializing_if = "Option::is_none")]
212    pub sort: Option<String>,
213    #[builder(setter(into, strip_option), default)]
214    #[serde(skip_serializing_if = "Option::is_none")]
215    pub q: Option<String>,
216    #[builder(setter(into, strip_option), default)]
217    #[serde(skip_serializing_if = "Option::is_none")]
218    pub input_modalities: Option<String>,
219    #[builder(setter(strip_option), default)]
220    #[serde(skip_serializing_if = "Option::is_none")]
221    pub context: Option<u32>,
222    #[builder(setter(strip_option), default)]
223    #[serde(skip_serializing_if = "Option::is_none")]
224    pub min_price: Option<f64>,
225    #[builder(setter(strip_option), default)]
226    #[serde(skip_serializing_if = "Option::is_none")]
227    pub max_price: Option<f64>,
228    #[builder(setter(into, strip_option), default)]
229    #[serde(skip_serializing_if = "Option::is_none")]
230    pub arch: Option<String>,
231    #[builder(setter(into, strip_option), default)]
232    #[serde(skip_serializing_if = "Option::is_none")]
233    pub model_authors: Option<String>,
234    #[builder(setter(into, strip_option), default)]
235    #[serde(skip_serializing_if = "Option::is_none")]
236    pub providers: Option<String>,
237    #[builder(setter(strip_option), default)]
238    #[serde(skip_serializing_if = "Option::is_none")]
239    pub distillable: Option<bool>,
240    #[builder(setter(strip_option), default)]
241    #[serde(skip_serializing_if = "Option::is_none")]
242    pub zdr: Option<bool>,
243    #[builder(setter(into, strip_option), default)]
244    #[serde(skip_serializing_if = "Option::is_none")]
245    pub region: Option<String>,
246}
247
248impl ListModelsParams {
249    pub fn builder() -> ListModelsParamsBuilder {
250        ListModelsParamsBuilder::default()
251    }
252
253    fn is_empty(&self) -> bool {
254        self.category.is_none()
255            && self.supported_parameters.is_none()
256            && self.output_modalities.is_none()
257            && self.sort.is_none()
258            && self.q.is_none()
259            && self.input_modalities.is_none()
260            && self.context.is_none()
261            && self.min_price.is_none()
262            && self.max_price.is_none()
263            && self.arch.is_none()
264            && self.model_authors.is_none()
265            && self.providers.is_none()
266            && self.distillable.is_none()
267            && self.zdr.is_none()
268            && self.region.is_none()
269    }
270}
271
272/// Returns a list of models available through the API
273///
274/// # Arguments
275///
276/// * `base_url` - The base URL of the OpenRouter API.
277/// * `api_key` - The API key for authentication.
278/// * `category` - Optional category filter for the models.
279/// * `supported_parameters` - Optional supported-parameter filter for the models.
280///
281/// # Returns
282///
283/// * `Result<Vec<Model>, OpenRouterError>` - A list of models or an error.
284pub async fn list_models(
285    base_url: &str,
286    api_key: &str,
287    category: Option<ModelCategory>,
288    supported_parameters: Option<SupportedParameters>,
289) -> Result<Vec<Model>, OpenRouterError> {
290    let http_client = crate::transport::new_client()?;
291    let params = ListModelsParams {
292        category,
293        supported_parameters,
294        ..Default::default()
295    };
296    list_models_with_params_and_client(&http_client, base_url, api_key, Some(&params)).await
297}
298
299pub(crate) async fn list_models_with_client(
300    http_client: &HttpClient,
301    base_url: &str,
302    api_key: &str,
303    category: Option<ModelCategory>,
304    supported_parameters: Option<SupportedParameters>,
305) -> Result<Vec<Model>, OpenRouterError> {
306    let params = ListModelsParams {
307        category,
308        supported_parameters,
309        ..Default::default()
310    };
311    list_models_with_params_and_client(http_client, base_url, api_key, Some(&params)).await
312}
313
314/// Returns a list of models using the full upstream filter surface.
315pub async fn list_models_with_params(
316    base_url: &str,
317    api_key: &str,
318    params: Option<&ListModelsParams>,
319) -> Result<Vec<Model>, OpenRouterError> {
320    let http_client = crate::transport::new_client()?;
321    list_models_with_params_and_client(&http_client, base_url, api_key, params).await
322}
323
324pub(crate) async fn list_models_with_params_and_client(
325    http_client: &HttpClient,
326    base_url: &str,
327    api_key: &str,
328    params: Option<&ListModelsParams>,
329) -> Result<Vec<Model>, OpenRouterError> {
330    let url = format!("{base_url}/models");
331    let req =
332        transport_request::with_bearer_auth(transport_request::get(http_client, &url), api_key);
333    let response = match params {
334        Some(params) if !params.is_empty() => req.query(params).send().await?,
335        _ => req.send().await?,
336    };
337
338    if response.status().is_success() {
339        let model_list_response: ApiResponse<_> =
340            transport_response::parse_json_response(response, "model list").await?;
341        Ok(model_list_response.data)
342    } else {
343        transport_response::handle_error(response).await?;
344        unreachable!()
345    }
346}
347
348/// Returns metadata about a specific model.
349pub async fn get_model(
350    base_url: &str,
351    api_key: &str,
352    author: &str,
353    slug: &str,
354) -> Result<Model, OpenRouterError> {
355    let http_client = crate::transport::new_client()?;
356    get_model_with_client(&http_client, base_url, api_key, author, slug).await
357}
358
359pub(crate) async fn get_model_with_client(
360    http_client: &HttpClient,
361    base_url: &str,
362    api_key: &str,
363    author: &str,
364    slug: &str,
365) -> Result<Model, OpenRouterError> {
366    let encoded_author = encode(author);
367    let encoded_slug = encode(slug);
368    let url = format!("{base_url}/model/{encoded_author}/{encoded_slug}");
369
370    let response =
371        transport_request::with_bearer_auth(transport_request::get(http_client, &url), api_key)
372            .send()
373            .await?;
374
375    if response.status().is_success() {
376        let model_response: ApiResponse<_> =
377            transport_response::parse_json_response(response, "model").await?;
378        Ok(model_response.data)
379    } else {
380        transport_response::handle_error(response).await?;
381        unreachable!()
382    }
383}
384
385/// Returns details about the endpoints for a specific model
386///
387/// # Arguments
388///
389/// * `base_url` - The base URL of the OpenRouter API.
390/// * `api_key` - The API key for authentication.
391/// * `author` - The author of the model.
392/// * `slug` - The slug identifier for the model.
393///
394/// # Returns
395///
396/// * `Result<EndpointData, OpenRouterError>` - The endpoint data or an error.
397pub async fn list_model_endpoints(
398    base_url: &str,
399    api_key: &str,
400    author: &str,
401    slug: &str,
402) -> Result<EndpointData, OpenRouterError> {
403    let http_client = crate::transport::new_client()?;
404    list_model_endpoints_with_client(&http_client, base_url, api_key, author, slug).await
405}
406
407pub(crate) async fn list_model_endpoints_with_client(
408    http_client: &HttpClient,
409    base_url: &str,
410    api_key: &str,
411    author: &str,
412    slug: &str,
413) -> Result<EndpointData, OpenRouterError> {
414    let encoded_author = encode(author);
415    let encoded_slug = encode(slug);
416    let url = format!("{base_url}/models/{encoded_author}/{encoded_slug}/endpoints");
417
418    let response =
419        transport_request::with_bearer_auth(transport_request::get(http_client, &url), api_key)
420            .send()
421            .await?;
422
423    if response.status().is_success() {
424        let endpoint_list_response: ApiResponse<_> =
425            transport_response::parse_json_response(response, "model endpoint list").await?;
426        Ok(endpoint_list_response.data)
427    } else {
428        transport_response::handle_error(response).await?;
429        unreachable!()
430    }
431}