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#[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#[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#[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
368pub 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(¶ms)).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(¶ms)).await
408}
409
410pub 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
444pub 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
481pub 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}