use crate::adapter::AdapterError;
use crate::types::{OpenRouterArchitecture, OpenRouterTopProvider};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct OpenRouterCatalogModel {
pub id: String,
#[serde(default)]
pub canonical_slug: Option<String>,
#[serde(default)]
pub hugging_face_id: Option<String>,
#[serde(default)]
pub name: String,
#[serde(default)]
pub created: Option<f64>,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub context_length: Option<u32>,
#[serde(default)]
pub architecture: Option<OpenRouterArchitecture>,
#[serde(default)]
pub pricing: Option<OpenRouterCatalogPricing>,
#[serde(default)]
pub top_provider: Option<OpenRouterTopProvider>,
#[serde(default)]
pub supported_parameters: Vec<String>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct OpenRouterCatalogPricing {
#[serde(default)]
pub prompt: Option<String>,
#[serde(default)]
pub completion: Option<String>,
#[serde(default)]
pub request: Option<String>,
#[serde(default)]
pub image: Option<String>,
#[serde(default)]
pub web_search: Option<String>,
#[serde(default)]
pub internal_reasoning: Option<String>,
#[serde(default)]
pub input_cache_read: Option<String>,
#[serde(default)]
pub input_cache_write: Option<String>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct OpenRouterEndpoint {
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub provider_name: Option<String>,
#[serde(default)]
pub tag: Option<String>,
#[serde(default)]
pub context_length: Option<u32>,
#[serde(default)]
pub max_completion_tokens: Option<u32>,
#[serde(default)]
pub max_prompt_tokens: Option<u32>,
#[serde(default)]
pub quantization: Option<String>,
#[serde(default)]
pub status: Option<f64>,
#[serde(default)]
pub uptime_last_30m: Option<f64>,
#[serde(default)]
pub latency_last_30m: Option<f64>,
#[serde(default)]
pub throughput_last_30m: Option<f64>,
#[serde(default)]
pub pricing: Option<OpenRouterCatalogPricing>,
#[serde(default)]
pub supported_parameters: Vec<String>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct OpenRouterModelEndpoints {
#[serde(default)]
pub id: Option<String>,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub created: Option<f64>,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub architecture: Option<OpenRouterArchitecture>,
#[serde(default)]
pub endpoints: Vec<OpenRouterEndpoint>,
}
#[derive(Debug, Deserialize)]
struct OpenRouterCatalogResponse {
data: Vec<OpenRouterCatalogModel>,
}
#[derive(Debug, Deserialize)]
struct OpenRouterEndpointsResponse {
data: OpenRouterModelEndpoints,
}
pub fn pricing_from_catalog(pricing: &OpenRouterCatalogPricing) -> Option<crate::catalog::ModelPricing> {
let per_million = |raw: &Option<String>| -> Option<f64> { raw.as_ref().and_then(|value| value.parse::<f64>().ok()).map(|per_token| per_token * 1_000_000.0) };
let input = per_million(&pricing.prompt);
let output = per_million(&pricing.completion);
let cache_read = per_million(&pricing.input_cache_read);
let cache_write = per_million(&pricing.input_cache_write);
let reasoning = per_million(&pricing.internal_reasoning);
if input.is_none() && output.is_none() && cache_read.is_none() && cache_write.is_none() && reasoning.is_none() {
return None;
}
Some(crate::catalog::ModelPricing {
input: input.unwrap_or(0.0),
output: output.unwrap_or(0.0),
cache_read,
cache_write,
reasoning,
input_audio: None,
output_audio: None,
tiers: Vec::new(),
})
}
pub fn endpoint_url(base_url: &str, author: &str, slug: &str) -> String {
format!("{}/v1/models/{}/{}/endpoints", base_url.trim_end_matches('/'), author, slug)
}
pub async fn fetch_public_models(
base_url: &str,
api_key: Option<&str>,
extra_headers: &BTreeMap<String, String>,
timeout: Option<u64>,
) -> Result<Vec<OpenRouterCatalogModel>, AdapterError> {
let url = format!("{}/v1/models", base_url.trim_end_matches('/'));
let request = build_get(&url, api_key, extra_headers, timeout);
let resp: OpenRouterCatalogResponse = send_json(request, "public models").await?;
Ok(resp.data)
}
pub async fn fetch_user_models(
base_url: &str,
api_key: Option<&str>,
extra_headers: &BTreeMap<String, String>,
timeout: Option<u64>,
) -> Result<Vec<OpenRouterCatalogModel>, AdapterError> {
let url = format!("{}/v1/models/user", base_url.trim_end_matches('/'));
let request = build_get(&url, api_key, extra_headers, timeout);
let resp: OpenRouterCatalogResponse = send_json(request, "user models").await?;
Ok(resp.data)
}
pub async fn fetch_model_endpoints(
base_url: &str,
author: &str,
slug: &str,
api_key: Option<&str>,
extra_headers: &BTreeMap<String, String>,
timeout: Option<u64>,
) -> Result<OpenRouterModelEndpoints, AdapterError> {
let url = endpoint_url(base_url, author, slug);
let request = build_get(&url, api_key, extra_headers, timeout);
let resp: OpenRouterEndpointsResponse = send_json(request, "model endpoints").await?;
Ok(resp.data)
}
fn build_get(url: &str, api_key: Option<&str>, extra_headers: &BTreeMap<String, String>, timeout: Option<u64>) -> reqwest::RequestBuilder {
let client = reqwest::Client::new();
let mut request = client.get(url);
if let Some(timeout) = timeout {
request = request.timeout(std::time::Duration::from_millis(timeout));
}
if let Some(api_key) = api_key {
request = request.header("Authorization", format!("Bearer {}", api_key));
}
for (key, value) in extra_headers {
request = request.header(key, value);
}
request
}
async fn send_json<T: serde::de::DeserializeOwned>(request: reqwest::RequestBuilder, ctx: &str) -> Result<T, AdapterError> {
let resp = request.send().await.map_err(|e| AdapterError::Http(format!("Failed to fetch {ctx}: {e}")))?;
if !resp.status().is_success() {
let status = resp.status();
let text = resp.text().await.unwrap_or_else(|_| "Unknown error".to_string());
return Err(AdapterError::Provider {
code: status.as_u16().to_string(),
message: text,
});
}
resp.json::<T>().await.map_err(|e| AdapterError::Http(format!("Failed to parse {ctx} response: {e}")))
}