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(default, skip_serializing_if = "Option::is_none")]
50    pub alias_target: Option<ModelAliasTarget>,
51    #[serde(flatten)]
52    pub extra: HashMap<String, Value>,
53}
54
55/// Concrete model targeted by a model alias.
56#[derive(Serialize, Deserialize, Debug, Clone)]
57#[non_exhaustive]
58pub struct ModelAliasTarget {
59    pub slug: String,
60    pub name: String,
61}
62
63#[derive(Serialize, Deserialize, Debug, Clone)]
64#[non_exhaustive]
65pub struct Architecture {
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub modality: Option<String>,
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub tokenizer: Option<String>,
70    #[serde(default, skip_serializing_if = "Option::is_none")]
71    pub instruct_type: Option<String>,
72    #[serde(default, skip_serializing_if = "Option::is_none")]
73    pub input_modalities: Option<Vec<String>>,
74    #[serde(default, skip_serializing_if = "Option::is_none")]
75    pub output_modalities: Option<Vec<String>>,
76    #[serde(flatten)]
77    pub extra: HashMap<String, Value>,
78}
79
80#[derive(Serialize, Deserialize, Debug, Clone)]
81#[non_exhaustive]
82pub struct TopProvider {
83    pub context_length: Option<f64>,
84    pub max_completion_tokens: Option<f64>,
85    pub is_moderated: bool,
86    #[serde(flatten)]
87    pub extra: HashMap<String, Value>,
88}
89
90#[derive(Serialize, Deserialize, Debug, Clone)]
91#[non_exhaustive]
92pub struct Pricing {
93    pub prompt: String,
94    pub completion: String,
95    pub image: Option<String>,
96    pub request: Option<String>,
97    pub input_cache_read: Option<String>,
98    pub input_cache_write: Option<String>,
99    pub web_search: Option<String>,
100    pub internal_reasoning: Option<String>,
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub overrides: Option<Vec<PricingOverride>>,
103    #[serde(flatten)]
104    pub extra: HashMap<String, Value>,
105}
106
107/// Conditional override applied to base model pricing.
108#[derive(Serialize, Deserialize, Debug, Clone)]
109#[non_exhaustive]
110pub struct PricingOverride {
111    #[serde(skip_serializing_if = "Option::is_none")]
112    pub audio: Option<String>,
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub completion: Option<String>,
115    #[serde(skip_serializing_if = "Option::is_none")]
116    pub input_audio_cache: Option<String>,
117    #[serde(skip_serializing_if = "Option::is_none")]
118    pub input_cache_read: Option<String>,
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub input_cache_write: Option<String>,
121    #[serde(skip_serializing_if = "Option::is_none")]
122    pub input_cache_write_1h: Option<String>,
123    #[serde(skip_serializing_if = "Option::is_none")]
124    pub min_prompt_tokens: Option<f64>,
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub prompt: Option<String>,
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub utc_end: Option<f64>,
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub utc_start: Option<f64>,
131}
132
133#[derive(Serialize, Deserialize, Debug, Clone)]
134#[non_exhaustive]
135pub struct ModelLinks {
136    pub details: String,
137    #[serde(flatten)]
138    pub extra: HashMap<String, Value>,
139}
140
141#[derive(Serialize, Deserialize, Debug, Clone)]
142#[non_exhaustive]
143pub struct AABenchmarkEntry {
144    pub intelligence_index: Option<f64>,
145    pub coding_index: Option<f64>,
146    pub agentic_index: Option<f64>,
147    #[serde(flatten)]
148    pub extra: HashMap<String, Value>,
149}
150
151#[derive(Serialize, Deserialize, Debug, Clone)]
152#[non_exhaustive]
153pub struct DABenchmarkEntry {
154    pub arena: String,
155    pub category: String,
156    pub elo: f64,
157    pub win_rate: f64,
158    pub rank: u64,
159    #[serde(flatten)]
160    pub extra: HashMap<String, Value>,
161}
162
163#[derive(Serialize, Deserialize, Debug, Clone)]
164#[non_exhaustive]
165pub struct ModelBenchmarks {
166    #[serde(default, skip_serializing_if = "Option::is_none")]
167    pub artificial_analysis: Option<AABenchmarkEntry>,
168    #[serde(default)]
169    pub design_arena: Vec<DABenchmarkEntry>,
170    #[serde(flatten)]
171    pub extra: HashMap<String, Value>,
172}
173
174#[derive(Serialize, Deserialize, Debug, Clone)]
175#[non_exhaustive]
176pub struct ModelReasoning {
177    #[serde(default, skip_serializing_if = "Option::is_none")]
178    pub default_effort: Option<Effort>,
179    #[serde(default, skip_serializing_if = "Option::is_none")]
180    pub default_enabled: Option<bool>,
181    pub mandatory: bool,
182    #[serde(default, skip_serializing_if = "Option::is_none")]
183    pub supported_efforts: Option<Vec<Effort>>,
184    #[serde(default, skip_serializing_if = "Option::is_none")]
185    pub supports_max_tokens: Option<bool>,
186    #[serde(flatten)]
187    pub extra: HashMap<String, Value>,
188}
189
190#[derive(Serialize, Deserialize, Debug, Clone)]
191#[non_exhaustive]
192pub struct Endpoint {
193    pub name: String,
194    pub context_length: f64,
195    pub pricing: EndpointPricing,
196    pub provider_name: String,
197    pub supported_parameters: Vec<String>,
198    pub quantization: Option<String>,
199    pub max_completion_tokens: Option<f64>,
200    pub max_prompt_tokens: Option<f64>,
201    pub status: Option<serde_json::Value>,
202}
203
204#[derive(Serialize, Deserialize, Debug, Clone)]
205#[non_exhaustive]
206pub struct EndpointPricing {
207    #[serde(skip_serializing_if = "Option::is_none")]
208    pub request: Option<String>,
209    #[serde(skip_serializing_if = "Option::is_none")]
210    pub image: Option<String>,
211    pub prompt: String,
212    pub completion: String,
213    #[serde(skip_serializing_if = "Option::is_none")]
214    pub overrides: Option<Vec<PricingOverride>>,
215}
216
217#[derive(Serialize, Deserialize, Debug, Clone)]
218#[non_exhaustive]
219pub struct EndpointData {
220    pub id: String,
221    pub name: String,
222    pub created: f64,
223    pub description: String,
224    pub architecture: EndpointArchitecture,
225    pub endpoints: Vec<Endpoint>,
226}
227
228#[derive(Serialize, Deserialize, Debug, Clone)]
229#[non_exhaustive]
230pub struct EndpointArchitecture {
231    pub tokenizer: Option<String>,
232    pub instruct_type: Option<String>,
233    pub modality: Option<String>,
234}
235
236/// Extended query parameters for `GET /models`.
237#[derive(Serialize, Deserialize, Debug, Clone, Default, Builder)]
238#[builder(build_fn(error = "OpenRouterError"))]
239#[non_exhaustive]
240pub struct ListModelsParams {
241    #[builder(setter(strip_option), default)]
242    #[serde(skip_serializing_if = "Option::is_none")]
243    pub category: Option<ModelCategory>,
244    #[builder(setter(strip_option), default)]
245    #[serde(skip_serializing_if = "Option::is_none")]
246    pub supported_parameters: Option<SupportedParameters>,
247    #[builder(setter(into, strip_option), default)]
248    #[serde(skip_serializing_if = "Option::is_none")]
249    pub output_modalities: Option<String>,
250    #[builder(setter(into, strip_option), default)]
251    #[serde(skip_serializing_if = "Option::is_none")]
252    pub sort: Option<String>,
253    #[builder(setter(into, strip_option), default)]
254    #[serde(skip_serializing_if = "Option::is_none")]
255    pub q: Option<String>,
256    #[builder(setter(into, strip_option), default)]
257    #[serde(skip_serializing_if = "Option::is_none")]
258    pub input_modalities: Option<String>,
259    #[builder(setter(strip_option), default)]
260    #[serde(skip_serializing_if = "Option::is_none")]
261    pub context: Option<u32>,
262    #[builder(setter(strip_option), default)]
263    #[serde(skip_serializing_if = "Option::is_none")]
264    pub min_price: Option<f64>,
265    #[builder(setter(strip_option), default)]
266    #[serde(skip_serializing_if = "Option::is_none")]
267    pub max_price: Option<f64>,
268    #[builder(setter(strip_option), default)]
269    #[serde(skip_serializing_if = "Option::is_none")]
270    pub min_output_price: Option<f64>,
271    #[builder(setter(strip_option), default)]
272    #[serde(skip_serializing_if = "Option::is_none")]
273    pub max_output_price: Option<f64>,
274    #[builder(setter(strip_option), default)]
275    #[serde(skip_serializing_if = "Option::is_none")]
276    pub min_age_days: Option<u32>,
277    #[builder(setter(strip_option), default)]
278    #[serde(skip_serializing_if = "Option::is_none")]
279    pub max_age_days: Option<u32>,
280    #[builder(setter(strip_option), default)]
281    #[serde(skip_serializing_if = "Option::is_none")]
282    pub min_intelligence_index: Option<f64>,
283    #[builder(setter(strip_option), default)]
284    #[serde(skip_serializing_if = "Option::is_none")]
285    pub max_intelligence_index: Option<f64>,
286    #[builder(setter(strip_option), default)]
287    #[serde(skip_serializing_if = "Option::is_none")]
288    pub min_coding_index: Option<f64>,
289    #[builder(setter(strip_option), default)]
290    #[serde(skip_serializing_if = "Option::is_none")]
291    pub max_coding_index: Option<f64>,
292    #[builder(setter(strip_option), default)]
293    #[serde(skip_serializing_if = "Option::is_none")]
294    pub min_agentic_index: Option<f64>,
295    #[builder(setter(strip_option), default)]
296    #[serde(skip_serializing_if = "Option::is_none")]
297    pub max_agentic_index: Option<f64>,
298    #[builder(setter(strip_option), default)]
299    #[serde(skip_serializing_if = "Option::is_none")]
300    pub min_tool_success_rate: Option<f64>,
301    #[builder(setter(strip_option), default)]
302    #[serde(skip_serializing_if = "Option::is_none")]
303    pub max_tool_success_rate: Option<f64>,
304    #[builder(setter(into, strip_option), default)]
305    #[serde(skip_serializing_if = "Option::is_none")]
306    pub arch: Option<String>,
307    #[builder(setter(into, strip_option), default)]
308    #[serde(skip_serializing_if = "Option::is_none")]
309    pub model_authors: Option<String>,
310    #[builder(setter(into, strip_option), default)]
311    #[serde(skip_serializing_if = "Option::is_none")]
312    pub providers: Option<String>,
313    #[builder(setter(strip_option), default)]
314    #[serde(skip_serializing_if = "Option::is_none")]
315    pub distillable: Option<bool>,
316    #[builder(setter(strip_option), default)]
317    #[serde(skip_serializing_if = "Option::is_none")]
318    pub zdr: Option<bool>,
319    #[builder(setter(into, strip_option), default)]
320    #[serde(skip_serializing_if = "Option::is_none")]
321    pub region: Option<String>,
322    #[builder(setter(strip_option), default)]
323    #[serde(skip_serializing_if = "Option::is_none")]
324    pub limit: Option<u32>,
325    #[builder(setter(strip_option), default)]
326    #[serde(skip_serializing_if = "Option::is_none")]
327    pub offset: Option<u32>,
328}
329
330impl ListModelsParams {
331    pub fn builder() -> ListModelsParamsBuilder {
332        ListModelsParamsBuilder::default()
333    }
334
335    fn is_empty(&self) -> bool {
336        self.category.is_none()
337            && self.supported_parameters.is_none()
338            && self.output_modalities.is_none()
339            && self.sort.is_none()
340            && self.q.is_none()
341            && self.input_modalities.is_none()
342            && self.context.is_none()
343            && self.min_price.is_none()
344            && self.max_price.is_none()
345            && self.min_output_price.is_none()
346            && self.max_output_price.is_none()
347            && self.min_age_days.is_none()
348            && self.max_age_days.is_none()
349            && self.min_intelligence_index.is_none()
350            && self.max_intelligence_index.is_none()
351            && self.min_coding_index.is_none()
352            && self.max_coding_index.is_none()
353            && self.min_agentic_index.is_none()
354            && self.max_agentic_index.is_none()
355            && self.min_tool_success_rate.is_none()
356            && self.max_tool_success_rate.is_none()
357            && self.arch.is_none()
358            && self.model_authors.is_none()
359            && self.providers.is_none()
360            && self.distillable.is_none()
361            && self.zdr.is_none()
362            && self.region.is_none()
363            && self.limit.is_none()
364            && self.offset.is_none()
365    }
366}
367
368/// Returns a list of models available through the API
369///
370/// # Arguments
371///
372/// * `base_url` - The base URL of the OpenRouter API.
373/// * `api_key` - The API key for authentication.
374/// * `category` - Optional category filter for the models.
375/// * `supported_parameters` - Optional supported-parameter filter for the models.
376///
377/// # Returns
378///
379/// * `Result<Vec<Model>, OpenRouterError>` - A list of models or an error.
380pub async fn list_models(
381    base_url: &str,
382    api_key: &str,
383    category: Option<ModelCategory>,
384    supported_parameters: Option<SupportedParameters>,
385) -> Result<Vec<Model>, OpenRouterError> {
386    let http_client = crate::transport::new_client()?;
387    let params = ListModelsParams {
388        category,
389        supported_parameters,
390        ..Default::default()
391    };
392    list_models_with_params_and_client(&http_client, base_url, api_key, Some(&params)).await
393}
394
395pub(crate) async fn list_models_with_client(
396    http_client: &HttpClient,
397    base_url: &str,
398    api_key: &str,
399    category: Option<ModelCategory>,
400    supported_parameters: Option<SupportedParameters>,
401) -> Result<Vec<Model>, OpenRouterError> {
402    let params = ListModelsParams {
403        category,
404        supported_parameters,
405        ..Default::default()
406    };
407    list_models_with_params_and_client(http_client, base_url, api_key, Some(&params)).await
408}
409
410/// Returns a list of models using the full upstream filter surface.
411pub async fn list_models_with_params(
412    base_url: &str,
413    api_key: &str,
414    params: Option<&ListModelsParams>,
415) -> Result<Vec<Model>, OpenRouterError> {
416    let http_client = crate::transport::new_client()?;
417    list_models_with_params_and_client(&http_client, base_url, api_key, params).await
418}
419
420pub(crate) async fn list_models_with_params_and_client(
421    http_client: &HttpClient,
422    base_url: &str,
423    api_key: &str,
424    params: Option<&ListModelsParams>,
425) -> Result<Vec<Model>, OpenRouterError> {
426    let url = format!("{base_url}/models");
427    let req =
428        transport_request::with_bearer_auth(transport_request::get(http_client, &url), api_key);
429    let response = match params {
430        Some(params) if !params.is_empty() => req.query(params).send().await?,
431        _ => req.send().await?,
432    };
433
434    if response.status().is_success() {
435        let model_list_response: ApiResponse<_> =
436            transport_response::parse_json_response(response, "model list").await?;
437        Ok(model_list_response.data)
438    } else {
439        transport_response::handle_error(response).await?;
440        unreachable!()
441    }
442}
443
444/// Returns metadata about a specific model.
445pub async fn get_model(
446    base_url: &str,
447    api_key: &str,
448    author: &str,
449    slug: &str,
450) -> Result<Model, OpenRouterError> {
451    let http_client = crate::transport::new_client()?;
452    get_model_with_client(&http_client, base_url, api_key, author, slug).await
453}
454
455pub(crate) async fn get_model_with_client(
456    http_client: &HttpClient,
457    base_url: &str,
458    api_key: &str,
459    author: &str,
460    slug: &str,
461) -> Result<Model, OpenRouterError> {
462    let encoded_author = encode(author);
463    let encoded_slug = encode(slug);
464    let url = format!("{base_url}/model/{encoded_author}/{encoded_slug}");
465
466    let response =
467        transport_request::with_bearer_auth(transport_request::get(http_client, &url), api_key)
468            .send()
469            .await?;
470
471    if response.status().is_success() {
472        let model_response: ApiResponse<_> =
473            transport_response::parse_json_response(response, "model").await?;
474        Ok(model_response.data)
475    } else {
476        transport_response::handle_error(response).await?;
477        unreachable!()
478    }
479}
480
481/// Returns details about the endpoints for a specific model
482///
483/// # Arguments
484///
485/// * `base_url` - The base URL of the OpenRouter API.
486/// * `api_key` - The API key for authentication.
487/// * `author` - The author of the model.
488/// * `slug` - The slug identifier for the model.
489///
490/// # Returns
491///
492/// * `Result<EndpointData, OpenRouterError>` - The endpoint data or an error.
493pub async fn list_model_endpoints(
494    base_url: &str,
495    api_key: &str,
496    author: &str,
497    slug: &str,
498) -> Result<EndpointData, OpenRouterError> {
499    let http_client = crate::transport::new_client()?;
500    list_model_endpoints_with_client(&http_client, base_url, api_key, author, slug).await
501}
502
503pub(crate) async fn list_model_endpoints_with_client(
504    http_client: &HttpClient,
505    base_url: &str,
506    api_key: &str,
507    author: &str,
508    slug: &str,
509) -> Result<EndpointData, OpenRouterError> {
510    let encoded_author = encode(author);
511    let encoded_slug = encode(slug);
512    let url = format!("{base_url}/models/{encoded_author}/{encoded_slug}/endpoints");
513
514    let response =
515        transport_request::with_bearer_auth(transport_request::get(http_client, &url), api_key)
516            .send()
517            .await?;
518
519    if response.status().is_success() {
520        let endpoint_list_response: ApiResponse<_> =
521            transport_response::parse_json_response(response, "model endpoint list").await?;
522        Ok(endpoint_list_response.data)
523    } else {
524        transport_response::handle_error(response).await?;
525        unreachable!()
526    }
527}