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#[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
272pub 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(¶ms)).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(¶ms)).await
312}
313
314pub 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
348pub 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
385pub 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}