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;