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