Skip to main content

rig_core/providers/openrouter/
completion.rs

1//! OpenRouter routing preferences, model identifiers, and typed completion metadata.
2//!
3//! ```
4//! use rig_core::providers::openrouter::ProviderPreferences;
5//! let params = ProviderPreferences::new().cheapest().to_json();
6//! assert!(params.get("provider").is_some());
7//! ```
8
9use serde::{Deserialize, Serialize};
10
11use crate::completion;
12use crate::providers::internal::openai_chat_completions_compatible::{
13    map_native_finish_reason, map_openai_finish_reason,
14};
15use crate::providers::openai::completion::Message;
16
17/// The `qwen/qwq-32b` model. Find more models at <https://openrouter.ai/models>.
18pub const QWEN_QWQ_32B: &str = "qwen/qwq-32b";
19/// The `anthropic/claude-3.7-sonnet` model. Find more models at <https://openrouter.ai/models>.
20pub const CLAUDE_3_7_SONNET: &str = "anthropic/claude-3.7-sonnet";
21/// The `perplexity/sonar-pro` model. Find more models at <https://openrouter.ai/models>.
22pub const PERPLEXITY_SONAR_PRO: &str = "perplexity/sonar-pro";
23/// The `google/gemini-2.0-flash-001` model. Find more models at <https://openrouter.ai/models>.
24pub const GEMINI_FLASH_2_0: &str = "google/gemini-2.0-flash-001";
25
26/// Data collection policy for providers.
27///
28/// Controls whether providers are allowed to collect and store request data.
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
30#[serde(rename_all = "lowercase")]
31pub enum DataCollection {
32    /// Allow providers that may collect data (default)
33    #[default]
34    Allow,
35    /// Restrict routing to providers that do not store user data non-transiently
36    Deny,
37}
38
39/// Model quantization levels supported by OpenRouter.
40///
41/// Restrict routing to providers serving a specific quantization level.
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
43#[serde(rename_all = "lowercase")]
44pub enum Quantization {
45    /// 4-bit integer quantization
46    #[serde(rename = "int4")]
47    Int4,
48    /// 8-bit integer quantization
49    #[serde(rename = "int8")]
50    Int8,
51    /// 16-bit floating point
52    #[serde(rename = "fp16")]
53    Fp16,
54    /// Brain floating point 16-bit
55    #[serde(rename = "bf16")]
56    Bf16,
57    /// 32-bit floating point (full precision)
58    #[serde(rename = "fp32")]
59    Fp32,
60    /// 8-bit floating point
61    #[serde(rename = "fp8")]
62    Fp8,
63    /// Unknown or custom quantization level
64    #[serde(rename = "unknown")]
65    Unknown,
66}
67
68/// Simple sorting strategy for providers.
69///
70/// Determines how providers should be prioritized when multiple are available.
71/// If you set `sort`, default load balancing is disabled and providers are tried
72/// deterministically in the resulting order.
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
74#[serde(rename_all = "lowercase")]
75pub enum ProviderSortStrategy {
76    /// Sort by price (cheapest first)
77    Price,
78    /// Sort by throughput (higher tokens/sec first)
79    Throughput,
80    /// Sort by latency (lower latency first)
81    Latency,
82}
83
84/// Partition strategy for multi-model requests.
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
86#[serde(rename_all = "lowercase")]
87pub enum SortPartition {
88    /// Sort providers within each model group (default)
89    Model,
90    /// Sort providers globally across all models
91    None,
92}
93
94/// Complex sorting configuration with partition support.
95///
96/// For multi-model requests, allows control over how providers are sorted.
97#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
98pub struct ProviderSortConfig {
99    /// Sorting strategy
100    pub by: ProviderSortStrategy,
101
102    /// Partition strategy (optional)
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub partition: Option<SortPartition>,
105}
106
107impl ProviderSortConfig {
108    /// Create a new sort config with the given strategy
109    pub fn new(by: ProviderSortStrategy) -> Self {
110        Self {
111            by,
112            partition: None,
113        }
114    }
115
116    /// Set partition strategy for multi-model requests
117    pub fn partition(mut self, partition: SortPartition) -> Self {
118        self.partition = Some(partition);
119        self
120    }
121}
122
123/// Sort configuration - can be a simple string or a complex object.
124///
125/// Use `ProviderSort::Simple` for basic sorting, or `ProviderSort::Complex`
126/// for multi-model requests with partition control.
127#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
128#[serde(untagged)]
129pub enum ProviderSort {
130    /// Simple sorting by a single strategy
131    Simple(ProviderSortStrategy),
132    /// Complex sorting with partition support
133    Complex(ProviderSortConfig),
134}
135
136impl From<ProviderSortStrategy> for ProviderSort {
137    fn from(strategy: ProviderSortStrategy) -> Self {
138        ProviderSort::Simple(strategy)
139    }
140}
141
142impl From<ProviderSortConfig> for ProviderSort {
143    fn from(config: ProviderSortConfig) -> Self {
144        ProviderSort::Complex(config)
145    }
146}
147
148/// Throughput threshold configuration with percentile support.
149///
150/// Endpoints not meeting the threshold are deprioritized (moved later), not excluded.
151#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
152#[serde(untagged)]
153pub enum ThroughputThreshold {
154    /// Simple threshold in tokens/sec
155    Simple(f64),
156    /// Percentile-based thresholds
157    Percentile(PercentileThresholds),
158}
159
160/// Latency threshold configuration with percentile support.
161///
162/// Endpoints not meeting the threshold are deprioritized, not excluded.
163#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
164#[serde(untagged)]
165pub enum LatencyThreshold {
166    /// Simple threshold in seconds
167    Simple(f64),
168    /// Percentile-based thresholds
169    Percentile(PercentileThresholds),
170}
171
172/// Percentile-based thresholds for throughput or latency.
173#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
174pub struct PercentileThresholds {
175    /// 50th percentile threshold
176    #[serde(skip_serializing_if = "Option::is_none")]
177    pub p50: Option<f64>,
178    /// 75th percentile threshold
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub p75: Option<f64>,
181    /// 90th percentile threshold
182    #[serde(skip_serializing_if = "Option::is_none")]
183    pub p90: Option<f64>,
184    /// 99th percentile threshold
185    #[serde(skip_serializing_if = "Option::is_none")]
186    pub p99: Option<f64>,
187}
188
189impl PercentileThresholds {
190    /// Create new empty percentile thresholds
191    pub fn new() -> Self {
192        Self::default()
193    }
194
195    /// Set p50 threshold
196    pub fn p50(mut self, value: f64) -> Self {
197        self.p50 = Some(value);
198        self
199    }
200
201    /// Set p75 threshold
202    pub fn p75(mut self, value: f64) -> Self {
203        self.p75 = Some(value);
204        self
205    }
206
207    /// Set p90 threshold
208    pub fn p90(mut self, value: f64) -> Self {
209        self.p90 = Some(value);
210        self
211    }
212
213    /// Set p99 threshold
214    pub fn p99(mut self, value: f64) -> Self {
215        self.p99 = Some(value);
216        self
217    }
218}
219
220/// Maximum price configuration for hard ceiling on costs.
221///
222/// If no eligible provider is at or under the ceiling, the request fails.
223/// Units are OpenRouter pricing units (e.g., dollars per million tokens).
224#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
225pub struct MaxPrice {
226    /// Maximum price per prompt token
227    #[serde(skip_serializing_if = "Option::is_none")]
228    pub prompt: Option<f64>,
229    /// Maximum price per completion token
230    #[serde(skip_serializing_if = "Option::is_none")]
231    pub completion: Option<f64>,
232    /// Maximum price per request
233    #[serde(skip_serializing_if = "Option::is_none")]
234    pub request: Option<f64>,
235    /// Maximum price per image
236    #[serde(skip_serializing_if = "Option::is_none")]
237    pub image: Option<f64>,
238}
239
240impl MaxPrice {
241    /// Create new empty max price config
242    pub fn new() -> Self {
243        Self::default()
244    }
245
246    /// Set maximum price per prompt token
247    pub fn prompt(mut self, price: f64) -> Self {
248        self.prompt = Some(price);
249        self
250    }
251
252    /// Set maximum price per completion token
253    pub fn completion(mut self, price: f64) -> Self {
254        self.completion = Some(price);
255        self
256    }
257
258    /// Set maximum price per request
259    pub fn request(mut self, price: f64) -> Self {
260        self.request = Some(price);
261        self
262    }
263
264    /// Set maximum price per image
265    pub fn image(mut self, price: f64) -> Self {
266        self.image = Some(price);
267        self
268    }
269}
270
271/// Request-side provider eligibility and ordering preferences. Unset fields are
272/// omitted, leaving gateway defaults in effect. Rig serializes these preferences
273/// without validating provider slugs or numeric limits.
274///
275/// See <https://openrouter.ai/docs/guides/routing/provider-selection>.
276///
277/// ```rust
278/// use rig_core::providers::openrouter::{ProviderPreferences, ProviderSortStrategy, Quantization};
279///
280/// let prefs = ProviderPreferences::new()
281///     .sort(ProviderSortStrategy::Throughput)
282///     .zdr(true)
283///     .quantizations([Quantization::Int8])
284///     .only(["anthropic", "openai"]);
285/// ```
286#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
287pub struct ProviderPreferences {
288    /// Try these provider slugs in the given order first.
289    /// If `allow_fallbacks: true`, OpenRouter may try other providers after this list is exhausted.
290    #[serde(skip_serializing_if = "Option::is_none")]
291    pub order: Option<Vec<String>>,
292
293    /// Hard allowlist. Only these provider slugs are eligible.
294    #[serde(skip_serializing_if = "Option::is_none")]
295    pub only: Option<Vec<String>>,
296
297    /// Blocklist. These provider slugs are never used.
298    #[serde(skip_serializing_if = "Option::is_none")]
299    pub ignore: Option<Vec<String>>,
300
301    /// If `false`, the router will not use any providers outside what your constraints permit.
302    /// Default is `true`.
303    #[serde(skip_serializing_if = "Option::is_none")]
304    pub allow_fallbacks: Option<bool>,
305
306    /// If `true`, only route to providers that support all parameters in your request.
307    ///
308    /// This is recommended for structured outputs so OpenRouter only selects
309    /// providers that support the generated `response_format` parameter.
310    /// Default is `false`.
311    #[serde(skip_serializing_if = "Option::is_none")]
312    pub require_parameters: Option<bool>,
313
314    /// Data collection policy. If [`DataCollection::Deny`], restrict routing to providers
315    /// that do not store user data non-transiently. Default is [`DataCollection::Allow`].
316    #[serde(skip_serializing_if = "Option::is_none")]
317    pub data_collection: Option<DataCollection>,
318
319    /// If `true`, restrict routing to Zero Data Retention endpoints only.
320    #[serde(skip_serializing_if = "Option::is_none")]
321    pub zdr: Option<bool>,
322
323    /// Sorting strategy. Affects ordering, not strict exclusion.
324    /// If set, default load balancing is disabled.
325    #[serde(skip_serializing_if = "Option::is_none")]
326    pub sort: Option<ProviderSort>,
327
328    /// Throughput threshold. Endpoints not meeting the threshold are deprioritized.
329    #[serde(skip_serializing_if = "Option::is_none")]
330    pub preferred_min_throughput: Option<ThroughputThreshold>,
331
332    /// Latency threshold. Endpoints not meeting the threshold are deprioritized.
333    #[serde(skip_serializing_if = "Option::is_none")]
334    pub preferred_max_latency: Option<LatencyThreshold>,
335
336    /// Hard price ceiling. If no provider is at or under, the request fails.
337    #[serde(skip_serializing_if = "Option::is_none")]
338    pub max_price: Option<MaxPrice>,
339
340    /// Restrict routing to providers serving specific quantization levels.
341    #[serde(skip_serializing_if = "Option::is_none")]
342    pub quantizations: Option<Vec<Quantization>>,
343}
344
345impl ProviderPreferences {
346    /// Create a new empty provider preferences struct
347    pub fn new() -> Self {
348        Self::default()
349    }
350
351    /// Try these provider slugs in the given order first.
352    ///
353    /// If `allow_fallbacks` is true (default), OpenRouter may try other providers
354    /// after this list is exhausted.
355    ///
356    /// ```rust
357    /// use rig_core::providers::openrouter::ProviderPreferences;
358    ///
359    /// let prefs = ProviderPreferences::new()
360    ///     .order(["anthropic", "openai"]);
361    /// ```
362    pub fn order(mut self, providers: impl IntoIterator<Item = impl Into<String>>) -> Self {
363        self.order = Some(
364            providers
365                .into_iter()
366                .map(std::convert::Into::into)
367                .collect(),
368        );
369        self
370    }
371
372    /// Restrict eligibility to these provider slugs.
373    ///
374    /// ```rust
375    /// use rig_core::providers::openrouter::ProviderPreferences;
376    ///
377    /// let prefs = ProviderPreferences::new()
378    ///     .only(["azure", "together"])
379    ///     .allow_fallbacks(false);
380    /// ```
381    pub fn only(mut self, providers: impl IntoIterator<Item = impl Into<String>>) -> Self {
382        self.only = Some(
383            providers
384                .into_iter()
385                .map(std::convert::Into::into)
386                .collect(),
387        );
388        self
389    }
390
391    /// Exclude these provider slugs.
392    ///
393    /// ```rust
394    /// use rig_core::providers::openrouter::ProviderPreferences;
395    ///
396    /// let prefs = ProviderPreferences::new()
397    ///     .ignore(["deepinfra"]);
398    /// ```
399    pub fn ignore(mut self, providers: impl IntoIterator<Item = impl Into<String>>) -> Self {
400        self.ignore = Some(
401            providers
402                .into_iter()
403                .map(std::convert::Into::into)
404                .collect(),
405        );
406        self
407    }
408
409    /// Control whether fallbacks are allowed.
410    ///
411    /// If `false`, the router will not use any providers outside what your constraints permit.
412    /// Default is `true`.
413    pub fn allow_fallbacks(mut self, allow: bool) -> Self {
414        self.allow_fallbacks = Some(allow);
415        self
416    }
417
418    /// If `true`, only route to providers that support all parameters in your request.
419    ///
420    /// Default is `false`, meaning providers may ignore unsupported parameters.
421    pub fn require_parameters(mut self, require: bool) -> Self {
422        self.require_parameters = Some(require);
423        self
424    }
425
426    /// Set data collection policy.
427    ///
428    /// If `Deny`, restrict routing to providers that do not store user data non-transiently.
429    pub fn data_collection(mut self, policy: DataCollection) -> Self {
430        self.data_collection = Some(policy);
431        self
432    }
433
434    /// Restrict routing to Zero Data Retention endpoints when enabled.
435    ///
436    /// ```rust
437    /// use rig_core::providers::openrouter::ProviderPreferences;
438    ///
439    /// let prefs = ProviderPreferences::new()
440    ///     .zdr(true);
441    /// ```
442    pub fn zdr(mut self, enable: bool) -> Self {
443        self.zdr = Some(enable);
444        self
445    }
446
447    /// Set the sorting strategy for providers.
448    ///
449    /// Disable default load balancing and try providers in the resulting order.
450    ///
451    /// ```rust
452    /// use rig_core::providers::openrouter::{ProviderPreferences, ProviderSortStrategy};
453    ///
454    /// let prefs = ProviderPreferences::new()
455    ///     .sort(ProviderSortStrategy::Latency);
456    /// ```
457    pub fn sort(mut self, sort: impl Into<ProviderSort>) -> Self {
458        self.sort = Some(sort.into());
459        self
460    }
461
462    /// Set preferred minimum throughput threshold.
463    ///
464    /// Endpoints below the threshold are deprioritized, not excluded.
465    ///
466    /// ```rust
467    /// use rig_core::providers::openrouter::{ProviderPreferences, ThroughputThreshold, PercentileThresholds};
468    ///
469    /// let prefs = ProviderPreferences::new()
470    ///     .preferred_min_throughput(ThroughputThreshold::Simple(50.0));
471    ///
472    /// let prefs = ProviderPreferences::new()
473    ///     .preferred_min_throughput(ThroughputThreshold::Percentile(
474    ///         PercentileThresholds::new().p90(50.0)
475    ///     ));
476    /// ```
477    pub fn preferred_min_throughput(mut self, threshold: ThroughputThreshold) -> Self {
478        self.preferred_min_throughput = Some(threshold);
479        self
480    }
481
482    /// Set preferred maximum latency threshold.
483    ///
484    /// Endpoints not meeting the threshold are deprioritized, not excluded.
485    pub fn preferred_max_latency(mut self, threshold: LatencyThreshold) -> Self {
486        self.preferred_max_latency = Some(threshold);
487        self
488    }
489
490    /// Set maximum price ceiling.
491    ///
492    /// If no eligible provider is at or under the ceiling, the request fails.
493    pub fn max_price(mut self, price: MaxPrice) -> Self {
494        self.max_price = Some(price);
495        self
496    }
497
498    /// Restrict routing to providers serving these quantization levels.
499    ///
500    /// ```rust
501    /// use rig_core::providers::openrouter::{ProviderPreferences, Quantization};
502    ///
503    /// let prefs = ProviderPreferences::new()
504    ///     .quantizations([Quantization::Int8, Quantization::Fp16]);
505    /// ```
506    pub fn quantizations(mut self, quantizations: impl IntoIterator<Item = Quantization>) -> Self {
507        self.quantizations = Some(quantizations.into_iter().collect());
508        self
509    }
510
511    /// Convenience: Sort by price (cheapest first)
512    pub fn cheapest(self) -> Self {
513        self.sort(ProviderSortStrategy::Price)
514    }
515
516    /// Convert to JSON value for use in additional_params
517    pub fn to_json(&self) -> serde_json::Value {
518        serde_json::json!({
519            "provider": self
520        })
521    }
522}
523
524fn deserialize_openrouter_choices_dropping_incomplete_tool_calls<'de, D>(
525    deserializer: D,
526) -> Result<Vec<Choice>, D::Error>
527where
528    D: serde::Deserializer<'de>,
529{
530    crate::providers::internal::openai_chat_completions_compatible::deserialize_choices_dropping_incomplete_tool_calls_when(
531        deserializer,
532        |choice| {
533            let normalized = choice
534                .get("finish_reason")
535                .and_then(serde_json::Value::as_str)
536                .filter(|reason| !reason.is_empty());
537            if let Some(reason) = normalized {
538                return matches!(map_openai_finish_reason(reason), completion::FinishReason::Length);
539            }
540
541            choice
542                .get("native_finish_reason")
543                .and_then(serde_json::Value::as_str)
544                .filter(|reason| !reason.is_empty())
545                .is_some_and(|reason| {
546                    matches!(map_native_finish_reason(reason), completion::FinishReason::Length)
547                })
548        },
549    )
550}
551
552/// Typed OpenRouter completion response.
553///
554/// For more information, see the
555/// [OpenRouter Chat Completions reference](https://openrouter.ai/docs/api/api-reference/chat/create-a-chat-completion).
556#[derive(Clone, Debug, Serialize, Deserialize)]
557pub struct CompletionResponse {
558    pub id: String,
559    pub object: String,
560    pub created: u64,
561    pub model: String,
562    #[serde(deserialize_with = "deserialize_openrouter_choices_dropping_incomplete_tool_calls")]
563    pub choices: Vec<Choice>,
564    pub system_fingerprint: Option<String>,
565    /// Upstream provider selected by OpenRouter for this response.
566    #[serde(default, skip_serializing_if = "Option::is_none")]
567    pub provider: Option<String>,
568    /// Service tier reported by the routed provider, when present.
569    #[serde(default, skip_serializing_if = "Option::is_none")]
570    pub service_tier: Option<String>,
571    pub usage: Option<Usage>,
572}
573
574#[derive(Clone, Debug, Deserialize, Serialize)]
575pub struct Choice {
576    pub index: usize,
577    pub native_finish_reason: Option<String>,
578    pub message: Message,
579    pub finish_reason: Option<String>,
580    /// Per-token probability metadata returned when `logprobs` is requested.
581    ///
582    /// Normalized completions intentionally omit provider-native
583    /// probabilities; they stay readable through `CompletionResponse::raw`.
584    #[serde(default, skip_serializing_if = "Option::is_none")]
585    pub logprobs: Option<serde_json::Value>,
586}
587
588#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize)]
589pub struct Usage {
590    pub prompt_tokens: usize,
591    #[serde(default)]
592    pub completion_tokens: usize,
593    pub total_tokens: usize,
594    #[serde(default)]
595    pub cost: f64,
596    /// Prompt-token cache breakdown, when reported.
597    #[serde(default, skip_serializing_if = "Option::is_none")]
598    pub prompt_tokens_details: Option<PromptTokensDetails>,
599    /// Completion-token breakdown, including the reasoning share when reported.
600    #[serde(default, skip_serializing_if = "Option::is_none")]
601    pub completion_tokens_details: Option<CompletionTokensDetails>,
602}
603
604/// Prompt-token breakdown reported by OpenRouter for cached requests.
605#[derive(Clone, Copy, Debug, Deserialize, Serialize, Default)]
606pub struct PromptTokensDetails {
607    /// Tokens served from cache (cache hit).
608    #[serde(default)]
609    pub cached_tokens: usize,
610    /// Tokens written to cache on this call (cache miss that populated the cache).
611    #[serde(default)]
612    pub cache_write_tokens: usize,
613}
614
615/// Completion-token breakdown containing the reported reasoning share.
616#[derive(Clone, Copy, Debug, Deserialize, Serialize, Default)]
617pub struct CompletionTokensDetails {
618    /// Tokens the upstream spent on hidden reasoning, counted inside
619    /// `completion_tokens`.
620    #[serde(default)]
621    pub reasoning_tokens: usize,
622}
623
624#[cfg(test)]
625mod tests;