use std::collections::BTreeMap;
use serde::Serialize;
use serde_json::{Map, Value};
use crate::completion::provider_options::reply_field;
use crate::completion::{ExtensionOptions, ProviderExtension, ReplyExtras};
use crate::message::Api;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct OpenRouterExt;
impl ProviderExtension for OpenRouterExt {
const PROVIDER: &'static str = super::PROVIDER_NAME;
type Options = OpenRouterOptions;
type Extras = OpenRouterExtras;
}
#[non_exhaustive]
#[derive(Clone, Debug, Default, PartialEq, Serialize)]
pub struct OpenRouterOptions {
#[serde(rename = "*")]
pub shared: OpenRouterShared,
}
#[non_exhaustive]
#[derive(Clone, Debug, Default, PartialEq, Serialize)]
pub struct OpenRouterShared {
#[serde(skip_serializing_if = "Option::is_none")]
pub provider: Option<ProviderPreferences>,
#[serde(skip_serializing_if = "Option::is_none")]
pub models: Option<ModelFallbacks>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub plugins: Vec<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub metadata: BTreeMap<String, String>,
#[serde(skip_serializing_if = "ReasoningExtra::is_empty")]
pub reasoning: ReasoningExtra,
#[serde(skip_serializing_if = "Option::is_none")]
pub top_k: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min_p: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub top_a: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub repetition_penalty: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub user: Option<String>,
}
#[non_exhaustive]
#[derive(Clone, Debug, Default, PartialEq, Serialize)]
pub struct ReasoningExtra {
#[serde(skip_serializing_if = "Option::is_none")]
pub exclude: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<ReasoningSummary>,
}
impl ReasoningExtra {
fn is_empty(&self) -> bool {
self.exclude.is_none() && self.summary.is_none()
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum ReasoningSummary {
Auto,
Concise,
Detailed,
}
impl OpenRouterOptions {
pub fn new() -> Self {
Self::default()
}
pub fn provider(mut self, preferences: ProviderPreferences) -> Self {
self.shared.provider = Some(preferences);
self
}
pub fn models(mut self, models: ModelFallbacks) -> Self {
self.shared.models = Some(models);
self
}
pub fn plugin(mut self, plugin: Value) -> Self {
self.shared.plugins.push(plugin);
self
}
pub fn session_id(mut self, id: impl Into<String>) -> Self {
self.shared.session_id = Some(id.into());
self
}
pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.shared.metadata.insert(key.into(), value.into());
self
}
pub fn reasoning_exclude(mut self, exclude: bool) -> Self {
self.shared.reasoning.exclude = Some(exclude);
self
}
pub fn reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
self.shared.reasoning.summary = Some(summary);
self
}
pub fn top_k(mut self, k: u32) -> Self {
self.shared.top_k = Some(k);
self
}
pub fn min_p(mut self, p: f64) -> Self {
self.shared.min_p = Some(p);
self
}
pub fn top_a(mut self, a: f64) -> Self {
self.shared.top_a = Some(a);
self
}
pub fn repetition_penalty(mut self, penalty: f64) -> Self {
self.shared.repetition_penalty = Some(penalty);
self
}
pub fn user(mut self, user: impl Into<String>) -> Self {
self.shared.user = Some(user.into());
self
}
}
impl ExtensionOptions for OpenRouterOptions {
type Ext = OpenRouterExt;
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
#[serde(transparent)]
pub struct ModelFallbacks(Vec<String>);
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
#[error("model fallbacks need at least one model")]
pub struct EmptyFallbacks;
impl ModelFallbacks {
pub fn new(
models: impl IntoIterator<Item = impl Into<String>>,
) -> Result<Self, EmptyFallbacks> {
let models: Vec<String> = models.into_iter().map(Into::into).collect();
if models.is_empty() {
return Err(EmptyFallbacks);
}
Ok(Self(models))
}
pub fn models(&self) -> &[String] {
&self.0
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum DataCollection {
Allow,
Deny,
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Quantization {
Int4,
Int8,
Fp16,
Bf16,
Fp32,
Fp8,
Unknown,
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum ProviderSortStrategy {
Price,
Throughput,
Latency,
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum SortPartition {
Model,
None,
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct ProviderSortConfig {
pub by: ProviderSortStrategy,
#[serde(skip_serializing_if = "Option::is_none")]
pub partition: Option<SortPartition>,
}
impl ProviderSortConfig {
pub fn new(by: ProviderSortStrategy) -> Self {
Self {
by,
partition: None,
}
}
pub fn partition(mut self, partition: SortPartition) -> Self {
self.partition = Some(partition);
self
}
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(untagged)]
pub enum ProviderSort {
Simple(ProviderSortStrategy),
Complex(ProviderSortConfig),
}
impl From<ProviderSortStrategy> for ProviderSort {
fn from(strategy: ProviderSortStrategy) -> Self {
Self::Simple(strategy)
}
}
impl From<ProviderSortConfig> for ProviderSort {
fn from(config: ProviderSortConfig) -> Self {
Self::Complex(config)
}
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(untagged)]
pub enum ThroughputThreshold {
Simple(f64),
Percentile(PercentileThresholds),
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(untagged)]
pub enum LatencyThreshold {
Simple(f64),
Percentile(PercentileThresholds),
}
#[non_exhaustive]
#[derive(Clone, Debug, Default, PartialEq, Serialize)]
pub struct PercentileThresholds {
#[serde(skip_serializing_if = "Option::is_none")]
pub p50: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub p75: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub p90: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub p99: Option<f64>,
}
impl PercentileThresholds {
pub fn new() -> Self {
Self::default()
}
pub fn p50(mut self, value: f64) -> Self {
self.p50 = Some(value);
self
}
pub fn p75(mut self, value: f64) -> Self {
self.p75 = Some(value);
self
}
pub fn p90(mut self, value: f64) -> Self {
self.p90 = Some(value);
self
}
pub fn p99(mut self, value: f64) -> Self {
self.p99 = Some(value);
self
}
}
#[non_exhaustive]
#[derive(Clone, Debug, Default, PartialEq, Serialize)]
pub struct MaxPrice {
#[serde(skip_serializing_if = "Option::is_none")]
pub prompt: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub completion: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub request: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub image: Option<f64>,
}
impl MaxPrice {
pub fn new() -> Self {
Self::default()
}
pub fn prompt(mut self, price: f64) -> Self {
self.prompt = Some(price);
self
}
pub fn completion(mut self, price: f64) -> Self {
self.completion = Some(price);
self
}
pub fn request(mut self, price: f64) -> Self {
self.request = Some(price);
self
}
pub fn image(mut self, price: f64) -> Self {
self.image = Some(price);
self
}
}
#[non_exhaustive]
#[derive(Clone, Debug, Default, PartialEq, Serialize)]
pub struct ProviderPreferences {
#[serde(skip_serializing_if = "Option::is_none")]
pub order: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub only: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ignore: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allow_fallbacks: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub require_parameters: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data_collection: Option<DataCollection>,
#[serde(skip_serializing_if = "Option::is_none")]
pub zdr: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sort: Option<ProviderSort>,
#[serde(skip_serializing_if = "Option::is_none")]
pub preferred_min_throughput: Option<ThroughputThreshold>,
#[serde(skip_serializing_if = "Option::is_none")]
pub preferred_max_latency: Option<LatencyThreshold>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_price: Option<MaxPrice>,
#[serde(skip_serializing_if = "Option::is_none")]
pub quantizations: Option<Vec<Quantization>>,
}
fn strings(items: impl IntoIterator<Item = impl Into<String>>) -> Option<Vec<String>> {
Some(items.into_iter().map(Into::into).collect())
}
impl ProviderPreferences {
pub fn new() -> Self {
Self::default()
}
pub fn order(mut self, providers: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.order = strings(providers);
self
}
pub fn only(mut self, providers: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.only = strings(providers);
self
}
pub fn ignore(mut self, providers: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.ignore = strings(providers);
self
}
pub fn allow_fallbacks(mut self, allow: bool) -> Self {
self.allow_fallbacks = Some(allow);
self
}
pub fn require_parameters(mut self, require: bool) -> Self {
self.require_parameters = Some(require);
self
}
pub fn data_collection(mut self, policy: DataCollection) -> Self {
self.data_collection = Some(policy);
self
}
pub fn zdr(mut self, enable: bool) -> Self {
self.zdr = Some(enable);
self
}
pub fn sort(mut self, sort: impl Into<ProviderSort>) -> Self {
self.sort = Some(sort.into());
self
}
pub fn preferred_min_throughput(mut self, threshold: ThroughputThreshold) -> Self {
self.preferred_min_throughput = Some(threshold);
self
}
pub fn preferred_max_latency(mut self, threshold: LatencyThreshold) -> Self {
self.preferred_max_latency = Some(threshold);
self
}
pub fn max_price(mut self, price: MaxPrice) -> Self {
self.max_price = Some(price);
self
}
pub fn quantizations(mut self, quantizations: impl IntoIterator<Item = Quantization>) -> Self {
self.quantizations = Some(quantizations.into_iter().collect());
self
}
pub fn cheapest(self) -> Self {
self.sort(ProviderSortStrategy::Price)
}
}
#[non_exhaustive]
#[derive(Clone, Debug, Default, PartialEq)]
pub struct OpenRouterExtras {
pub provider: Option<String>,
pub native_finish_reason: Option<String>,
pub service_tier: Option<String>,
pub system_fingerprint: Option<String>,
pub cost: Option<f64>,
pub cost_details: Option<Map<String, Value>>,
pub is_byok: Option<bool>,
pub prompt_tokens_details: Option<Value>,
pub server_tool_use_details: Option<Value>,
pub openrouter_metadata: Option<Value>,
pub annotations: Option<Vec<Value>>,
}
impl ReplyExtras for OpenRouterExtras {
fn from_reply(api: &Api, raw: &Value) -> Result<Self, serde_json::Error> {
if api.as_str() == "openai.responses" {
return Ok(Self {
provider: reply_field(raw, "/provider")?,
service_tier: reply_field(raw, "/service_tier")?,
cost: reply_field(raw, "/usage/cost")?,
cost_details: reply_field(raw, "/usage/cost_details")?,
is_byok: reply_field(raw, "/usage/is_byok")?,
prompt_tokens_details: reply_field(raw, "/usage/input_tokens_details")?,
..Self::default()
});
}
Ok(Self {
provider: reply_field(raw, "/provider")?,
native_finish_reason: reply_field(raw, "/choices/0/native_finish_reason")?,
service_tier: reply_field(raw, "/service_tier")?,
system_fingerprint: reply_field(raw, "/system_fingerprint")?,
cost: reply_field(raw, "/usage/cost")?,
cost_details: reply_field(raw, "/usage/cost_details")?,
is_byok: reply_field(raw, "/usage/is_byok")?,
prompt_tokens_details: reply_field(raw, "/usage/prompt_tokens_details")?,
server_tool_use_details: reply_field(raw, "/usage/server_tool_use_details")?,
openrouter_metadata: reply_field(raw, "/openrouter_metadata")?,
annotations: reply_field(raw, "/choices/0/message/annotations")?,
})
}
}
#[cfg(test)]
mod tests;