Skip to main content

openrouter_rs/api/
discovery.rs

1use std::collections::HashMap;
2
3use derive_builder::Builder;
4use reqwest::Client as HttpClient;
5use serde::{Deserialize, Serialize};
6use urlencoding::encode;
7
8use crate::{
9    api::models::ModelReasoning,
10    error::OpenRouterError,
11    transport::{request as transport_request, response as transport_response},
12    types::ApiResponse,
13};
14
15/// Number-like value used by OpenRouter pricing fields.
16#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
17#[non_exhaustive]
18#[serde(untagged)]
19pub enum BigNumber {
20    String(String),
21    Number(f64),
22}
23
24/// Public provider metadata returned by `GET /providers`.
25#[derive(Serialize, Deserialize, Debug, Clone)]
26#[non_exhaustive]
27pub struct Provider {
28    pub name: String,
29    pub slug: String,
30    pub privacy_policy_url: Option<String>,
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub terms_of_service_url: Option<String>,
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub status_page_url: Option<String>,
35    #[serde(flatten)]
36    pub extra: HashMap<String, serde_json::Value>,
37}
38
39/// Model pricing payload returned by model discovery endpoints.
40#[derive(Serialize, Deserialize, Debug, Clone)]
41#[non_exhaustive]
42pub struct PublicPricing {
43    pub prompt: BigNumber,
44    pub completion: BigNumber,
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub request: Option<BigNumber>,
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub image: Option<BigNumber>,
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub image_token: Option<BigNumber>,
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub image_output: Option<BigNumber>,
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub audio: Option<BigNumber>,
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub audio_output: Option<BigNumber>,
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub input_audio_cache: Option<BigNumber>,
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub web_search: Option<BigNumber>,
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub internal_reasoning: Option<BigNumber>,
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub input_cache_read: Option<BigNumber>,
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub input_cache_write: Option<BigNumber>,
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub discount: Option<f64>,
69}
70
71/// Model architecture data in model discovery responses.
72#[derive(Serialize, Deserialize, Debug, Clone)]
73#[non_exhaustive]
74pub struct ModelArchitecture {
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub tokenizer: Option<String>,
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub instruct_type: Option<String>,
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub modality: Option<String>,
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub input_modalities: Option<Vec<String>>,
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub output_modalities: Option<Vec<String>>,
85}
86
87/// Top provider metadata in model discovery responses.
88#[derive(Serialize, Deserialize, Debug, Clone)]
89#[non_exhaustive]
90pub struct TopProviderInfo {
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub context_length: Option<f64>,
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub max_completion_tokens: Option<f64>,
95    pub is_moderated: bool,
96}
97
98/// Per-request token limits for a model.
99#[derive(Serialize, Deserialize, Debug, Clone)]
100#[non_exhaustive]
101pub struct PerRequestLimits {
102    pub prompt_tokens: f64,
103    pub completion_tokens: f64,
104}
105
106/// Model payload returned by `GET /models/user`.
107#[derive(Serialize, Deserialize, Debug, Clone)]
108#[non_exhaustive]
109pub struct UserModel {
110    pub id: String,
111    pub canonical_slug: String,
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub hugging_face_id: Option<String>,
114    pub name: String,
115    pub created: f64,
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub description: Option<String>,
118    pub pricing: PublicPricing,
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub context_length: Option<f64>,
121    pub architecture: ModelArchitecture,
122    pub top_provider: TopProviderInfo,
123    #[serde(skip_serializing_if = "Option::is_none")]
124    pub per_request_limits: Option<PerRequestLimits>,
125    #[serde(default)]
126    pub supported_parameters: Vec<String>,
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub supported_voices: Option<Vec<String>>,
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub default_parameters: Option<serde_json::Value>,
131    #[serde(skip_serializing_if = "Option::is_none")]
132    pub expiration_date: Option<String>,
133    #[serde(default, skip_serializing_if = "Option::is_none")]
134    pub reasoning: Option<ModelReasoning>,
135    #[serde(flatten)]
136    pub extra: HashMap<String, serde_json::Value>,
137}
138
139/// Count payload returned by `GET /models/count`.
140#[derive(Serialize, Deserialize, Debug, Clone)]
141#[non_exhaustive]
142pub struct ModelsCountData {
143    pub count: u64,
144}
145
146/// Percentile statistics payload used by endpoint throughput/latency.
147#[derive(Serialize, Deserialize, Debug, Clone)]
148#[non_exhaustive]
149pub struct PercentileStats {
150    pub p50: f64,
151    pub p75: f64,
152    pub p90: f64,
153    pub p99: f64,
154}
155
156/// Public endpoint payload returned by `GET /endpoints/zdr`.
157#[derive(Serialize, Deserialize, Debug, Clone)]
158#[non_exhaustive]
159pub struct PublicEndpoint {
160    pub name: String,
161    pub model_id: String,
162    pub model_name: String,
163    pub context_length: f64,
164    pub pricing: PublicPricing,
165    pub provider_name: String,
166    pub tag: String,
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub quantization: Option<String>,
169    #[serde(skip_serializing_if = "Option::is_none")]
170    pub max_completion_tokens: Option<f64>,
171    #[serde(skip_serializing_if = "Option::is_none")]
172    pub max_prompt_tokens: Option<f64>,
173    #[serde(default)]
174    pub supported_parameters: Vec<String>,
175    #[serde(skip_serializing_if = "Option::is_none")]
176    pub status: Option<i32>,
177    #[serde(skip_serializing_if = "Option::is_none")]
178    pub uptime_last_30m: Option<f64>,
179    pub supports_implicit_caching: bool,
180    #[serde(skip_serializing_if = "Option::is_none")]
181    pub latency_last_30m: Option<PercentileStats>,
182    #[serde(skip_serializing_if = "Option::is_none")]
183    pub throughput_last_30m: Option<PercentileStats>,
184    #[serde(flatten)]
185    pub extra: HashMap<String, serde_json::Value>,
186}
187
188/// Activity item payload returned by `GET /activity`.
189#[derive(Serialize, Deserialize, Debug, Clone)]
190#[non_exhaustive]
191pub struct ActivityItem {
192    pub date: String,
193    pub model: String,
194    pub model_permaslug: String,
195    pub endpoint_id: String,
196    pub provider_name: String,
197    pub usage: f64,
198    pub byok_usage_inference: f64,
199    pub requests: f64,
200    pub prompt_tokens: f64,
201    pub completion_tokens: f64,
202    pub reasoning_tokens: f64,
203    #[serde(flatten)]
204    pub extra: HashMap<String, serde_json::Value>,
205}
206
207/// One daily model-ranking row returned by `GET /datasets/rankings-daily`.
208#[derive(Serialize, Deserialize, Debug, Clone)]
209#[non_exhaustive]
210pub struct RankingsDailyItem {
211    pub date: String,
212    pub model_permaslug: String,
213    pub total_tokens: String,
214    #[serde(flatten)]
215    pub extra: HashMap<String, serde_json::Value>,
216}
217
218/// Metadata for a daily rankings dataset response.
219#[derive(Serialize, Deserialize, Debug, Clone)]
220#[non_exhaustive]
221pub struct RankingsDailyMeta {
222    pub as_of: String,
223    pub version: String,
224    pub start_date: String,
225    pub end_date: String,
226    #[serde(flatten)]
227    pub extra: HashMap<String, serde_json::Value>,
228}
229
230/// Daily token totals for top public models plus an aggregated `other` row.
231#[derive(Serialize, Deserialize, Debug, Clone)]
232#[non_exhaustive]
233pub struct RankingsDailyResponse {
234    pub data: Vec<RankingsDailyItem>,
235    pub meta: RankingsDailyMeta,
236}
237
238/// Query parameters for `GET /datasets/app-rankings`.
239#[derive(Serialize, Deserialize, Debug, Clone, Default, Builder)]
240#[builder(build_fn(error = "OpenRouterError"))]
241#[non_exhaustive]
242pub struct AppRankingsParams {
243    #[builder(setter(into, strip_option), default)]
244    #[serde(skip_serializing_if = "Option::is_none")]
245    pub category: Option<String>,
246    #[builder(setter(into, strip_option), default)]
247    #[serde(skip_serializing_if = "Option::is_none")]
248    pub subcategory: Option<String>,
249    #[builder(setter(into, strip_option), default)]
250    #[serde(skip_serializing_if = "Option::is_none")]
251    pub sort: Option<String>,
252    #[builder(setter(into, strip_option), default)]
253    #[serde(skip_serializing_if = "Option::is_none")]
254    pub start_date: Option<String>,
255    #[builder(setter(into, strip_option), default)]
256    #[serde(skip_serializing_if = "Option::is_none")]
257    pub end_date: Option<String>,
258    #[builder(setter(strip_option), default)]
259    #[serde(skip_serializing_if = "Option::is_none")]
260    pub limit: Option<u32>,
261    #[builder(setter(strip_option), default)]
262    #[serde(skip_serializing_if = "Option::is_none")]
263    pub offset: Option<u32>,
264}
265
266impl AppRankingsParams {
267    pub fn builder() -> AppRankingsParamsBuilder {
268        AppRankingsParamsBuilder::default()
269    }
270
271    fn is_empty(&self) -> bool {
272        self.category.is_none()
273            && self.subcategory.is_none()
274            && self.sort.is_none()
275            && self.start_date.is_none()
276            && self.end_date.is_none()
277            && self.limit.is_none()
278            && self.offset.is_none()
279    }
280}
281
282/// One application ranking row returned by `GET /datasets/app-rankings`.
283#[derive(Serialize, Deserialize, Debug, Clone)]
284#[non_exhaustive]
285pub struct AppRankingsItem {
286    pub rank: u64,
287    pub app_id: u64,
288    pub app_name: String,
289    pub total_tokens: String,
290    pub total_requests: u64,
291    #[serde(flatten)]
292    pub extra: HashMap<String, serde_json::Value>,
293}
294
295/// App rankings dataset response.
296#[derive(Serialize, Deserialize, Debug, Clone)]
297#[non_exhaustive]
298pub struct AppRankingsResponse {
299    pub data: Vec<AppRankingsItem>,
300    pub meta: RankingsDailyMeta,
301}
302
303/// Top model share for one task classification.
304#[derive(Serialize, Deserialize, Debug, Clone)]
305#[non_exhaustive]
306pub struct TaskClassificationModel {
307    pub id: String,
308    pub tag_usage_share: f64,
309    pub tag_token_share: f64,
310    #[serde(flatten)]
311    pub extra: HashMap<String, serde_json::Value>,
312}
313
314/// One task classification row returned by `GET /classifications/task`.
315#[derive(Serialize, Deserialize, Debug, Clone)]
316#[non_exhaustive]
317pub struct TaskClassificationItem {
318    pub tag: String,
319    pub display_name: String,
320    pub macro_category: String,
321    pub usage_share: f64,
322    pub token_share: f64,
323    pub category_usage_share: f64,
324    pub category_token_share: f64,
325    pub models: Vec<TaskClassificationModel>,
326    #[serde(flatten)]
327    pub extra: HashMap<String, serde_json::Value>,
328}
329
330/// Aggregate market-share data for one task macro-category.
331#[derive(Serialize, Deserialize, Debug, Clone)]
332#[non_exhaustive]
333pub struct TaskClassificationMacroCategory {
334    pub key: String,
335    pub label: String,
336    pub usage_share: f64,
337    pub token_share: f64,
338    #[serde(flatten)]
339    pub extra: HashMap<String, serde_json::Value>,
340}
341
342/// Data payload returned by `GET /classifications/task`.
343#[derive(Serialize, Deserialize, Debug, Clone)]
344#[non_exhaustive]
345pub struct TaskClassificationsData {
346    pub window_days: u64,
347    pub as_of: String,
348    pub classifications: Vec<TaskClassificationItem>,
349    pub macro_categories: Vec<TaskClassificationMacroCategory>,
350    #[serde(flatten)]
351    pub extra: HashMap<String, serde_json::Value>,
352}
353
354/// Task classification response returned by `GET /classifications/task`.
355#[derive(Serialize, Deserialize, Debug, Clone)]
356#[non_exhaustive]
357pub struct TaskClassificationsResponse {
358    pub data: TaskClassificationsData,
359}
360
361/// OpenRouter benchmark pricing payload.
362#[derive(Serialize, Deserialize, Debug, Clone)]
363#[non_exhaustive]
364pub struct BenchmarkPricing {
365    pub prompt: String,
366    pub completion: String,
367    #[serde(flatten)]
368    pub extra: HashMap<String, serde_json::Value>,
369}
370
371/// One Artificial Analysis benchmark row.
372#[derive(Serialize, Deserialize, Debug, Clone)]
373#[non_exhaustive]
374pub struct BenchmarksAAItem {
375    pub model_permaslug: String,
376    pub aa_name: String,
377    pub intelligence_index: Option<f64>,
378    pub coding_index: Option<f64>,
379    pub agentic_index: Option<f64>,
380    pub pricing: Option<BenchmarkPricing>,
381    #[serde(flatten)]
382    pub extra: HashMap<String, serde_json::Value>,
383}
384
385/// Metadata for Artificial Analysis benchmark rows.
386#[derive(Serialize, Deserialize, Debug, Clone)]
387#[non_exhaustive]
388pub struct BenchmarksAAMeta {
389    pub as_of: String,
390    pub version: String,
391    pub source: String,
392    pub source_url: String,
393    pub citation: String,
394    pub model_count: u64,
395    #[serde(flatten)]
396    pub extra: HashMap<String, serde_json::Value>,
397}
398
399/// Artificial Analysis benchmark dataset response.
400#[derive(Serialize, Deserialize, Debug, Clone)]
401#[non_exhaustive]
402pub struct BenchmarksAAResponse {
403    pub data: Vec<BenchmarksAAItem>,
404    pub meta: BenchmarksAAMeta,
405}
406
407/// Placement distribution from Design Arena tournament matches.
408#[derive(Serialize, Deserialize, Debug, Clone)]
409#[non_exhaustive]
410pub struct DesignArenaTournamentStats {
411    pub first_place: Option<u64>,
412    pub second_place: Option<u64>,
413    pub third_place: Option<u64>,
414    pub fourth_place: Option<u64>,
415    pub total: Option<u64>,
416    #[serde(flatten)]
417    pub extra: HashMap<String, serde_json::Value>,
418}
419
420/// One Design Arena benchmark row.
421#[derive(Serialize, Deserialize, Debug, Clone)]
422#[non_exhaustive]
423pub struct BenchmarksDAItem {
424    pub model_permaslug: String,
425    pub display_name: String,
426    pub arena: String,
427    pub category: String,
428    pub elo: f64,
429    pub win_rate: f64,
430    pub avg_generation_time_ms: Option<f64>,
431    pub tournament_stats: DesignArenaTournamentStats,
432    pub pricing: Option<BenchmarkPricing>,
433    #[serde(flatten)]
434    pub extra: HashMap<String, serde_json::Value>,
435}
436
437/// ELO bounds for a Design Arena response.
438#[derive(Serialize, Deserialize, Debug, Clone)]
439#[non_exhaustive]
440pub struct DesignArenaEloBounds {
441    pub min: f64,
442    pub max: f64,
443    #[serde(flatten)]
444    pub extra: HashMap<String, serde_json::Value>,
445}
446
447/// Metadata for Design Arena benchmark rows.
448#[derive(Serialize, Deserialize, Debug, Clone)]
449#[non_exhaustive]
450pub struct BenchmarksDAMeta {
451    pub as_of: String,
452    pub version: String,
453    pub source: String,
454    pub source_url: String,
455    pub citation: String,
456    pub model_count: u64,
457    pub arena: String,
458    pub category: Option<String>,
459    pub elo_bounds: DesignArenaEloBounds,
460    #[serde(flatten)]
461    pub extra: HashMap<String, serde_json::Value>,
462}
463
464/// Design Arena benchmark dataset response.
465#[derive(Serialize, Deserialize, Debug, Clone)]
466#[non_exhaustive]
467pub struct BenchmarksDAResponse {
468    pub data: Vec<BenchmarksDAItem>,
469    pub meta: BenchmarksDAMeta,
470}
471
472/// Query parameters for the unified benchmarks endpoint.
473#[derive(Serialize, Deserialize, Debug, Clone, Default, Builder)]
474#[builder(build_fn(error = "OpenRouterError"))]
475#[non_exhaustive]
476pub struct UnifiedBenchmarksParams {
477    #[builder(setter(into, strip_option), default)]
478    #[serde(skip_serializing_if = "Option::is_none")]
479    pub source: Option<String>,
480    #[builder(setter(into, strip_option), default)]
481    #[serde(skip_serializing_if = "Option::is_none")]
482    pub task_type: Option<String>,
483    #[builder(setter(into, strip_option), default)]
484    #[serde(skip_serializing_if = "Option::is_none")]
485    pub arena: Option<String>,
486    #[builder(setter(into, strip_option), default)]
487    #[serde(skip_serializing_if = "Option::is_none")]
488    pub category: Option<String>,
489    #[builder(setter(strip_option), default)]
490    #[serde(skip_serializing_if = "Option::is_none")]
491    pub max_results: Option<u32>,
492}
493
494impl UnifiedBenchmarksParams {
495    pub fn builder() -> UnifiedBenchmarksParamsBuilder {
496        UnifiedBenchmarksParamsBuilder::default()
497    }
498
499    pub fn artificial_analysis() -> Self {
500        Self {
501            source: Some("artificial-analysis".to_string()),
502            task_type: None,
503            arena: None,
504            category: None,
505            max_results: None,
506        }
507    }
508
509    pub fn design_arena() -> Self {
510        Self {
511            source: Some("design-arena".to_string()),
512            task_type: None,
513            arena: None,
514            category: None,
515            max_results: None,
516        }
517    }
518}
519
520/// One Artificial Analysis row returned by `GET /benchmarks`.
521#[derive(Serialize, Deserialize, Debug, Clone)]
522#[non_exhaustive]
523pub struct UnifiedBenchmarksAAItem {
524    pub source: String,
525    pub model_permaslug: String,
526    pub display_name: String,
527    pub intelligence_index: Option<f64>,
528    pub coding_index: Option<f64>,
529    pub agentic_index: Option<f64>,
530    pub pricing: Option<BenchmarkPricing>,
531    #[serde(flatten)]
532    pub extra: HashMap<String, serde_json::Value>,
533}
534
535/// One Design Arena row returned by `GET /benchmarks`.
536#[derive(Serialize, Deserialize, Debug, Clone)]
537#[non_exhaustive]
538pub struct UnifiedBenchmarksDAItem {
539    pub source: String,
540    pub model_permaslug: String,
541    pub display_name: String,
542    pub arena: String,
543    pub category: String,
544    pub elo: f64,
545    pub win_rate: f64,
546    pub avg_generation_time_ms: Option<f64>,
547    pub tournament_stats: DesignArenaTournamentStats,
548    pub pricing: Option<BenchmarkPricing>,
549    #[serde(flatten)]
550    pub extra: HashMap<String, serde_json::Value>,
551}
552
553/// One benchmark row returned by `GET /benchmarks`.
554#[derive(Serialize, Deserialize, Debug, Clone)]
555#[serde(untagged)]
556#[non_exhaustive]
557pub enum UnifiedBenchmarkItem {
558    DesignArena(UnifiedBenchmarksDAItem),
559    ArtificialAnalysis(UnifiedBenchmarksAAItem),
560    Other(HashMap<String, serde_json::Value>),
561}
562
563/// Metadata for the unified benchmarks endpoint.
564#[derive(Serialize, Deserialize, Debug, Clone)]
565#[non_exhaustive]
566pub struct UnifiedBenchmarksMeta {
567    pub as_of: String,
568    pub version: String,
569    pub source: Option<String>,
570    pub source_url: Option<String>,
571    pub citation: Option<String>,
572    pub model_count: u64,
573    pub task_type: Option<String>,
574    #[serde(flatten)]
575    pub extra: HashMap<String, serde_json::Value>,
576}
577
578/// Unified benchmark response returned by `GET /benchmarks`.
579#[derive(Serialize, Deserialize, Debug, Clone)]
580#[non_exhaustive]
581pub struct UnifiedBenchmarksResponse {
582    pub data: Vec<UnifiedBenchmarkItem>,
583    pub meta: UnifiedBenchmarksMeta,
584}
585
586/// List all providers (`GET /providers`).
587pub async fn list_providers(
588    base_url: &str,
589    api_key: &str,
590) -> Result<Vec<Provider>, OpenRouterError> {
591    let http_client = crate::transport::new_client()?;
592    list_providers_with_client(&http_client, base_url, api_key).await
593}
594
595pub(crate) async fn list_providers_with_client(
596    http_client: &HttpClient,
597    base_url: &str,
598    api_key: &str,
599) -> Result<Vec<Provider>, OpenRouterError> {
600    let url = format!("{base_url}/providers");
601    let response =
602        transport_request::with_bearer_auth(transport_request::get(http_client, &url), api_key)
603            .send()
604            .await?;
605
606    if response.status().is_success() {
607        let parsed: ApiResponse<Vec<Provider>> =
608            transport_response::parse_json_response(response, "provider list").await?;
609        Ok(parsed.data)
610    } else {
611        transport_response::handle_error(response).await?;
612        unreachable!()
613    }
614}
615
616/// List models filtered by user settings (`GET /models/user`).
617pub async fn list_models_for_user(
618    base_url: &str,
619    api_key: &str,
620) -> Result<Vec<UserModel>, OpenRouterError> {
621    let http_client = crate::transport::new_client()?;
622    list_models_for_user_with_client(&http_client, base_url, api_key).await
623}
624
625pub(crate) async fn list_models_for_user_with_client(
626    http_client: &HttpClient,
627    base_url: &str,
628    api_key: &str,
629) -> Result<Vec<UserModel>, OpenRouterError> {
630    let url = format!("{base_url}/models/user");
631    let response =
632        transport_request::with_bearer_auth(transport_request::get(http_client, &url), api_key)
633            .send()
634            .await?;
635
636    if response.status().is_success() {
637        let parsed: ApiResponse<Vec<UserModel>> =
638            transport_response::parse_json_response(response, "user model list").await?;
639        Ok(parsed.data)
640    } else {
641        transport_response::handle_error(response).await?;
642        unreachable!()
643    }
644}
645
646/// Count available models (`GET /models/count`).
647pub async fn count_models(
648    base_url: &str,
649    api_key: &str,
650) -> Result<ModelsCountData, OpenRouterError> {
651    let http_client = crate::transport::new_client()?;
652    count_models_with_client(&http_client, base_url, api_key).await
653}
654
655pub(crate) async fn count_models_with_client(
656    http_client: &HttpClient,
657    base_url: &str,
658    api_key: &str,
659) -> Result<ModelsCountData, OpenRouterError> {
660    let url = format!("{base_url}/models/count");
661    let response =
662        transport_request::with_bearer_auth(transport_request::get(http_client, &url), api_key)
663            .send()
664            .await?;
665
666    if response.status().is_success() {
667        let parsed: ApiResponse<ModelsCountData> =
668            transport_response::parse_json_response(response, "model count").await?;
669        Ok(parsed.data)
670    } else {
671        transport_response::handle_error(response).await?;
672        unreachable!()
673    }
674}
675
676/// Return daily token totals for top public models (`GET /datasets/rankings-daily`).
677pub async fn get_rankings_daily(
678    base_url: &str,
679    api_key: &str,
680    start_date: Option<&str>,
681    end_date: Option<&str>,
682) -> Result<RankingsDailyResponse, OpenRouterError> {
683    let http_client = crate::transport::new_client()?;
684    get_rankings_daily_with_client(&http_client, base_url, api_key, start_date, end_date).await
685}
686
687pub(crate) async fn get_rankings_daily_with_client(
688    http_client: &HttpClient,
689    base_url: &str,
690    api_key: &str,
691    start_date: Option<&str>,
692    end_date: Option<&str>,
693) -> Result<RankingsDailyResponse, OpenRouterError> {
694    #[derive(Serialize)]
695    struct RankingsDailyQuery<'a> {
696        #[serde(skip_serializing_if = "Option::is_none")]
697        start_date: Option<&'a str>,
698        #[serde(skip_serializing_if = "Option::is_none")]
699        end_date: Option<&'a str>,
700    }
701
702    let url = format!("{base_url}/datasets/rankings-daily");
703    let query = RankingsDailyQuery {
704        start_date,
705        end_date,
706    };
707    let req =
708        transport_request::with_bearer_auth(transport_request::get(http_client, &url), api_key);
709    let response = if query.start_date.is_none() && query.end_date.is_none() {
710        req.send().await?
711    } else {
712        req.query(&query).send().await?
713    };
714
715    if response.status().is_success() {
716        transport_response::parse_json_response(response, "rankings daily").await
717    } else {
718        transport_response::handle_error(response).await?;
719        unreachable!()
720    }
721}
722
723/// Return app rankings over a date window (`GET /datasets/app-rankings`).
724pub async fn get_app_rankings(
725    base_url: &str,
726    api_key: &str,
727    params: Option<&AppRankingsParams>,
728) -> Result<AppRankingsResponse, OpenRouterError> {
729    let http_client = crate::transport::new_client()?;
730    get_app_rankings_with_client(&http_client, base_url, api_key, params).await
731}
732
733pub(crate) async fn get_app_rankings_with_client(
734    http_client: &HttpClient,
735    base_url: &str,
736    api_key: &str,
737    params: Option<&AppRankingsParams>,
738) -> Result<AppRankingsResponse, OpenRouterError> {
739    let url = format!("{base_url}/datasets/app-rankings");
740    let req =
741        transport_request::with_bearer_auth(transport_request::get(http_client, &url), api_key);
742    let response = match params {
743        Some(params) if !params.is_empty() => req.query(params).send().await?,
744        _ => req.send().await?,
745    };
746
747    if response.status().is_success() {
748        transport_response::parse_json_response(response, "app rankings").await
749    } else {
750        transport_response::handle_error(response).await?;
751        unreachable!()
752    }
753}
754
755/// Return task classification market-share data (`GET /classifications/task`).
756pub async fn get_task_classifications(
757    base_url: &str,
758    api_key: &str,
759    window: Option<&str>,
760) -> Result<TaskClassificationsResponse, OpenRouterError> {
761    let http_client = crate::transport::new_client()?;
762    get_task_classifications_with_client(&http_client, base_url, api_key, window).await
763}
764
765pub(crate) async fn get_task_classifications_with_client(
766    http_client: &HttpClient,
767    base_url: &str,
768    api_key: &str,
769    window: Option<&str>,
770) -> Result<TaskClassificationsResponse, OpenRouterError> {
771    let url = format!("{base_url}/classifications/task");
772    let req =
773        transport_request::with_bearer_auth(transport_request::get(http_client, &url), api_key);
774    let response = match window {
775        Some(window) => req.query(&[("window", window)]).send().await?,
776        None => req.send().await?,
777    };
778
779    if response.status().is_success() {
780        transport_response::parse_json_response(response, "task classifications").await
781    } else {
782        transport_response::handle_error(response).await?;
783        unreachable!()
784    }
785}
786
787/// Return benchmark rows from a selected benchmark source (`GET /benchmarks`).
788pub async fn get_benchmarks(
789    base_url: &str,
790    api_key: &str,
791    params: &UnifiedBenchmarksParams,
792) -> Result<UnifiedBenchmarksResponse, OpenRouterError> {
793    let http_client = crate::transport::new_client()?;
794    get_benchmarks_with_client(&http_client, base_url, api_key, params).await
795}
796
797pub(crate) async fn get_benchmarks_with_client(
798    http_client: &HttpClient,
799    base_url: &str,
800    api_key: &str,
801    params: &UnifiedBenchmarksParams,
802) -> Result<UnifiedBenchmarksResponse, OpenRouterError> {
803    let url = format!("{base_url}/benchmarks");
804    let response =
805        transport_request::with_bearer_auth(transport_request::get(http_client, &url), api_key)
806            .query(params)
807            .send()
808            .await?;
809
810    if response.status().is_success() {
811        transport_response::parse_json_response(response, "benchmarks").await
812    } else {
813        transport_response::handle_error(response).await?;
814        unreachable!()
815    }
816}
817
818#[derive(Serialize)]
819struct BenchmarkMaxResultsQuery {
820    #[serde(skip_serializing_if = "Option::is_none")]
821    max_results: Option<u32>,
822}
823
824/// Return Artificial Analysis benchmark rows.
825#[deprecated(note = "use get_benchmarks with source `artificial-analysis`")]
826pub async fn get_benchmarks_artificial_analysis(
827    base_url: &str,
828    api_key: &str,
829    max_results: Option<u32>,
830) -> Result<BenchmarksAAResponse, OpenRouterError> {
831    let http_client = crate::transport::new_client()?;
832    get_benchmarks_artificial_analysis_with_client(&http_client, base_url, api_key, max_results)
833        .await
834}
835
836pub(crate) async fn get_benchmarks_artificial_analysis_with_client(
837    http_client: &HttpClient,
838    base_url: &str,
839    api_key: &str,
840    max_results: Option<u32>,
841) -> Result<BenchmarksAAResponse, OpenRouterError> {
842    let url = format!("{base_url}/datasets/benchmarks/artificial-analysis");
843    let query = BenchmarkMaxResultsQuery { max_results };
844    let req =
845        transport_request::with_bearer_auth(transport_request::get(http_client, &url), api_key);
846    let response = if query.max_results.is_none() {
847        req.send().await?
848    } else {
849        req.query(&query).send().await?
850    };
851
852    if response.status().is_success() {
853        transport_response::parse_json_response(response, "Artificial Analysis benchmarks").await
854    } else {
855        transport_response::handle_error(response).await?;
856        unreachable!()
857    }
858}
859
860#[derive(Serialize)]
861struct DesignArenaQuery<'a> {
862    #[serde(skip_serializing_if = "Option::is_none")]
863    arena: Option<&'a str>,
864    #[serde(skip_serializing_if = "Option::is_none")]
865    category: Option<&'a str>,
866    #[serde(skip_serializing_if = "Option::is_none")]
867    max_results: Option<u32>,
868}
869
870/// Return Design Arena benchmark rows.
871#[deprecated(note = "use get_benchmarks with source `design-arena`")]
872pub async fn get_benchmarks_design_arena(
873    base_url: &str,
874    api_key: &str,
875    arena: Option<&str>,
876    category: Option<&str>,
877    max_results: Option<u32>,
878) -> Result<BenchmarksDAResponse, OpenRouterError> {
879    let http_client = crate::transport::new_client()?;
880    get_benchmarks_design_arena_with_client(
881        &http_client,
882        base_url,
883        api_key,
884        arena,
885        category,
886        max_results,
887    )
888    .await
889}
890
891pub(crate) async fn get_benchmarks_design_arena_with_client(
892    http_client: &HttpClient,
893    base_url: &str,
894    api_key: &str,
895    arena: Option<&str>,
896    category: Option<&str>,
897    max_results: Option<u32>,
898) -> Result<BenchmarksDAResponse, OpenRouterError> {
899    let url = format!("{base_url}/datasets/benchmarks/design-arena");
900    let query = DesignArenaQuery {
901        arena,
902        category,
903        max_results,
904    };
905    let req =
906        transport_request::with_bearer_auth(transport_request::get(http_client, &url), api_key);
907    let response =
908        if query.arena.is_none() && query.category.is_none() && query.max_results.is_none() {
909            req.send().await?
910        } else {
911            req.query(&query).send().await?
912        };
913
914    if response.status().is_success() {
915        transport_response::parse_json_response(response, "Design Arena benchmarks").await
916    } else {
917        transport_response::handle_error(response).await?;
918        unreachable!()
919    }
920}
921
922/// List ZDR-compatible endpoints (`GET /endpoints/zdr`).
923pub async fn list_zdr_endpoints(
924    base_url: &str,
925    api_key: &str,
926) -> Result<Vec<PublicEndpoint>, OpenRouterError> {
927    let http_client = crate::transport::new_client()?;
928    list_zdr_endpoints_with_client(&http_client, base_url, api_key).await
929}
930
931pub(crate) async fn list_zdr_endpoints_with_client(
932    http_client: &HttpClient,
933    base_url: &str,
934    api_key: &str,
935) -> Result<Vec<PublicEndpoint>, OpenRouterError> {
936    let url = format!("{base_url}/endpoints/zdr");
937    let response =
938        transport_request::with_bearer_auth(transport_request::get(http_client, &url), api_key)
939            .send()
940            .await?;
941
942    if response.status().is_success() {
943        let parsed: ApiResponse<Vec<PublicEndpoint>> =
944            transport_response::parse_json_response(response, "ZDR endpoint list").await?;
945        Ok(parsed.data)
946    } else {
947        transport_response::handle_error(response).await?;
948        unreachable!()
949    }
950}
951
952/// Get endpoint-grouped activity (`GET /activity`).
953///
954/// `date` is optional and should be in `YYYY-MM-DD` format.
955pub async fn get_activity(
956    base_url: &str,
957    management_key: &str,
958    date: Option<&str>,
959) -> Result<Vec<ActivityItem>, OpenRouterError> {
960    let http_client = crate::transport::new_client()?;
961    get_activity_with_client(&http_client, base_url, management_key, date).await
962}
963
964pub(crate) async fn get_activity_with_client(
965    http_client: &HttpClient,
966    base_url: &str,
967    management_key: &str,
968    date: Option<&str>,
969) -> Result<Vec<ActivityItem>, OpenRouterError> {
970    let url = if let Some(date) = date {
971        format!("{base_url}/activity?date={}", encode(date))
972    } else {
973        format!("{base_url}/activity")
974    };
975
976    let response = transport_request::with_bearer_auth(
977        transport_request::get(http_client, &url),
978        management_key,
979    )
980    .send()
981    .await?;
982
983    if response.status().is_success() {
984        let parsed: ApiResponse<Vec<ActivityItem>> =
985            transport_response::parse_json_response(response, "activity list").await?;
986        Ok(parsed.data)
987    } else {
988        transport_response::handle_error(response).await?;
989        unreachable!()
990    }
991}