1use std::collections::BTreeMap;
22
23use serde::Serialize;
24use serde_json::{Map, Value};
25
26use crate::completion::provider_options::reply_field;
27use crate::completion::{ExtensionOptions, ProviderExtension, ReplyExtras};
28use crate::message::Api;
29
30#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
32pub struct OpenRouterExt;
33
34impl ProviderExtension for OpenRouterExt {
35 const PROVIDER: &'static str = super::PROVIDER_NAME;
36 type Options = OpenRouterOptions;
37 type Extras = OpenRouterExtras;
38}
39
40#[non_exhaustive]
42#[derive(Clone, Debug, Default, PartialEq, Serialize)]
43pub struct OpenRouterOptions {
44 #[serde(rename = "*")]
46 pub shared: OpenRouterShared,
47}
48
49#[non_exhaustive]
51#[derive(Clone, Debug, Default, PartialEq, Serialize)]
52pub struct OpenRouterShared {
53 #[serde(skip_serializing_if = "Option::is_none")]
55 pub provider: Option<ProviderPreferences>,
56 #[serde(skip_serializing_if = "Option::is_none")]
58 pub models: Option<ModelFallbacks>,
59 #[serde(skip_serializing_if = "Vec::is_empty")]
61 pub plugins: Vec<Value>,
62 #[serde(skip_serializing_if = "Option::is_none")]
64 pub session_id: Option<String>,
65 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
67 pub metadata: BTreeMap<String, String>,
68 #[serde(skip_serializing_if = "ReasoningExtra::is_empty")]
70 pub reasoning: ReasoningExtra,
71 #[serde(skip_serializing_if = "Option::is_none")]
73 pub top_k: Option<u32>,
74 #[serde(skip_serializing_if = "Option::is_none")]
76 pub min_p: Option<f64>,
77 #[serde(skip_serializing_if = "Option::is_none")]
79 pub top_a: Option<f64>,
80 #[serde(skip_serializing_if = "Option::is_none")]
82 pub repetition_penalty: Option<f64>,
83 #[serde(skip_serializing_if = "Option::is_none")]
85 pub user: Option<String>,
86}
87
88#[non_exhaustive]
91#[derive(Clone, Debug, Default, PartialEq, Serialize)]
92pub struct ReasoningExtra {
93 #[serde(skip_serializing_if = "Option::is_none")]
95 pub exclude: Option<bool>,
96 #[serde(skip_serializing_if = "Option::is_none")]
98 pub summary: Option<ReasoningSummary>,
99}
100
101impl ReasoningExtra {
102 fn is_empty(&self) -> bool {
103 self.exclude.is_none() && self.summary.is_none()
104 }
105}
106
107#[non_exhaustive]
109#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
110#[serde(rename_all = "lowercase")]
111pub enum ReasoningSummary {
112 Auto,
114 Concise,
116 Detailed,
118}
119
120impl OpenRouterOptions {
121 pub fn new() -> Self {
123 Self::default()
124 }
125
126 pub fn provider(mut self, preferences: ProviderPreferences) -> Self {
128 self.shared.provider = Some(preferences);
129 self
130 }
131
132 pub fn models(mut self, models: ModelFallbacks) -> Self {
134 self.shared.models = Some(models);
135 self
136 }
137
138 pub fn plugin(mut self, plugin: Value) -> Self {
140 self.shared.plugins.push(plugin);
141 self
142 }
143
144 pub fn session_id(mut self, id: impl Into<String>) -> Self {
146 self.shared.session_id = Some(id.into());
147 self
148 }
149
150 pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
152 self.shared.metadata.insert(key.into(), value.into());
153 self
154 }
155
156 pub fn reasoning_exclude(mut self, exclude: bool) -> Self {
158 self.shared.reasoning.exclude = Some(exclude);
159 self
160 }
161
162 pub fn reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
164 self.shared.reasoning.summary = Some(summary);
165 self
166 }
167
168 pub fn top_k(mut self, k: u32) -> Self {
170 self.shared.top_k = Some(k);
171 self
172 }
173
174 pub fn min_p(mut self, p: f64) -> Self {
176 self.shared.min_p = Some(p);
177 self
178 }
179
180 pub fn top_a(mut self, a: f64) -> Self {
182 self.shared.top_a = Some(a);
183 self
184 }
185
186 pub fn repetition_penalty(mut self, penalty: f64) -> Self {
188 self.shared.repetition_penalty = Some(penalty);
189 self
190 }
191
192 pub fn user(mut self, user: impl Into<String>) -> Self {
194 self.shared.user = Some(user.into());
195 self
196 }
197}
198
199impl ExtensionOptions for OpenRouterOptions {
200 type Ext = OpenRouterExt;
201}
202
203#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
205#[serde(transparent)]
206pub struct ModelFallbacks(Vec<String>);
207
208#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
210#[error("model fallbacks need at least one model")]
211pub struct EmptyFallbacks;
212
213impl ModelFallbacks {
214 pub fn new(
220 models: impl IntoIterator<Item = impl Into<String>>,
221 ) -> Result<Self, EmptyFallbacks> {
222 let models: Vec<String> = models.into_iter().map(Into::into).collect();
223 if models.is_empty() {
224 return Err(EmptyFallbacks);
225 }
226 Ok(Self(models))
227 }
228
229 pub fn models(&self) -> &[String] {
231 &self.0
232 }
233}
234
235#[non_exhaustive]
237#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
238#[serde(rename_all = "lowercase")]
239pub enum DataCollection {
240 Allow,
242 Deny,
244}
245
246#[non_exhaustive]
248#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
249#[serde(rename_all = "lowercase")]
250pub enum Quantization {
251 Int4,
253 Int8,
255 Fp16,
257 Bf16,
259 Fp32,
261 Fp8,
263 Unknown,
265}
266
267#[non_exhaustive]
269#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
270#[serde(rename_all = "lowercase")]
271pub enum ProviderSortStrategy {
272 Price,
274 Throughput,
276 Latency,
278}
279
280#[non_exhaustive]
282#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
283#[serde(rename_all = "lowercase")]
284pub enum SortPartition {
285 Model,
287 None,
289}
290
291#[non_exhaustive]
293#[derive(Clone, Debug, PartialEq, Serialize)]
294pub struct ProviderSortConfig {
295 pub by: ProviderSortStrategy,
297 #[serde(skip_serializing_if = "Option::is_none")]
299 pub partition: Option<SortPartition>,
300}
301
302impl ProviderSortConfig {
303 pub fn new(by: ProviderSortStrategy) -> Self {
305 Self {
306 by,
307 partition: None,
308 }
309 }
310
311 pub fn partition(mut self, partition: SortPartition) -> Self {
313 self.partition = Some(partition);
314 self
315 }
316}
317
318#[non_exhaustive]
320#[derive(Clone, Debug, PartialEq, Serialize)]
321#[serde(untagged)]
322pub enum ProviderSort {
323 Simple(ProviderSortStrategy),
325 Complex(ProviderSortConfig),
327}
328
329impl From<ProviderSortStrategy> for ProviderSort {
330 fn from(strategy: ProviderSortStrategy) -> Self {
331 Self::Simple(strategy)
332 }
333}
334
335impl From<ProviderSortConfig> for ProviderSort {
336 fn from(config: ProviderSortConfig) -> Self {
337 Self::Complex(config)
338 }
339}
340
341#[non_exhaustive]
344#[derive(Clone, Debug, PartialEq, Serialize)]
345#[serde(untagged)]
346pub enum ThroughputThreshold {
347 Simple(f64),
349 Percentile(PercentileThresholds),
351}
352
353#[non_exhaustive]
356#[derive(Clone, Debug, PartialEq, Serialize)]
357#[serde(untagged)]
358pub enum LatencyThreshold {
359 Simple(f64),
361 Percentile(PercentileThresholds),
363}
364
365#[non_exhaustive]
367#[derive(Clone, Debug, Default, PartialEq, Serialize)]
368pub struct PercentileThresholds {
369 #[serde(skip_serializing_if = "Option::is_none")]
371 pub p50: Option<f64>,
372 #[serde(skip_serializing_if = "Option::is_none")]
374 pub p75: Option<f64>,
375 #[serde(skip_serializing_if = "Option::is_none")]
377 pub p90: Option<f64>,
378 #[serde(skip_serializing_if = "Option::is_none")]
380 pub p99: Option<f64>,
381}
382
383impl PercentileThresholds {
384 pub fn new() -> Self {
386 Self::default()
387 }
388
389 pub fn p50(mut self, value: f64) -> Self {
391 self.p50 = Some(value);
392 self
393 }
394
395 pub fn p75(mut self, value: f64) -> Self {
397 self.p75 = Some(value);
398 self
399 }
400
401 pub fn p90(mut self, value: f64) -> Self {
403 self.p90 = Some(value);
404 self
405 }
406
407 pub fn p99(mut self, value: f64) -> Self {
409 self.p99 = Some(value);
410 self
411 }
412}
413
414#[non_exhaustive]
417#[derive(Clone, Debug, Default, PartialEq, Serialize)]
418pub struct MaxPrice {
419 #[serde(skip_serializing_if = "Option::is_none")]
421 pub prompt: Option<f64>,
422 #[serde(skip_serializing_if = "Option::is_none")]
424 pub completion: Option<f64>,
425 #[serde(skip_serializing_if = "Option::is_none")]
427 pub request: Option<f64>,
428 #[serde(skip_serializing_if = "Option::is_none")]
430 pub image: Option<f64>,
431}
432
433impl MaxPrice {
434 pub fn new() -> Self {
436 Self::default()
437 }
438
439 pub fn prompt(mut self, price: f64) -> Self {
441 self.prompt = Some(price);
442 self
443 }
444
445 pub fn completion(mut self, price: f64) -> Self {
447 self.completion = Some(price);
448 self
449 }
450
451 pub fn request(mut self, price: f64) -> Self {
453 self.request = Some(price);
454 self
455 }
456
457 pub fn image(mut self, price: f64) -> Self {
459 self.image = Some(price);
460 self
461 }
462}
463
464#[non_exhaustive]
468#[derive(Clone, Debug, Default, PartialEq, Serialize)]
469pub struct ProviderPreferences {
470 #[serde(skip_serializing_if = "Option::is_none")]
472 pub order: Option<Vec<String>>,
473 #[serde(skip_serializing_if = "Option::is_none")]
475 pub only: Option<Vec<String>>,
476 #[serde(skip_serializing_if = "Option::is_none")]
478 pub ignore: Option<Vec<String>>,
479 #[serde(skip_serializing_if = "Option::is_none")]
481 pub allow_fallbacks: Option<bool>,
482 #[serde(skip_serializing_if = "Option::is_none")]
484 pub require_parameters: Option<bool>,
485 #[serde(skip_serializing_if = "Option::is_none")]
487 pub data_collection: Option<DataCollection>,
488 #[serde(skip_serializing_if = "Option::is_none")]
490 pub zdr: Option<bool>,
491 #[serde(skip_serializing_if = "Option::is_none")]
493 pub sort: Option<ProviderSort>,
494 #[serde(skip_serializing_if = "Option::is_none")]
496 pub preferred_min_throughput: Option<ThroughputThreshold>,
497 #[serde(skip_serializing_if = "Option::is_none")]
499 pub preferred_max_latency: Option<LatencyThreshold>,
500 #[serde(skip_serializing_if = "Option::is_none")]
502 pub max_price: Option<MaxPrice>,
503 #[serde(skip_serializing_if = "Option::is_none")]
505 pub quantizations: Option<Vec<Quantization>>,
506}
507
508fn strings(items: impl IntoIterator<Item = impl Into<String>>) -> Option<Vec<String>> {
509 Some(items.into_iter().map(Into::into).collect())
510}
511
512impl ProviderPreferences {
513 pub fn new() -> Self {
515 Self::default()
516 }
517
518 pub fn order(mut self, providers: impl IntoIterator<Item = impl Into<String>>) -> Self {
520 self.order = strings(providers);
521 self
522 }
523
524 pub fn only(mut self, providers: impl IntoIterator<Item = impl Into<String>>) -> Self {
526 self.only = strings(providers);
527 self
528 }
529
530 pub fn ignore(mut self, providers: impl IntoIterator<Item = impl Into<String>>) -> Self {
532 self.ignore = strings(providers);
533 self
534 }
535
536 pub fn allow_fallbacks(mut self, allow: bool) -> Self {
538 self.allow_fallbacks = Some(allow);
539 self
540 }
541
542 pub fn require_parameters(mut self, require: bool) -> Self {
544 self.require_parameters = Some(require);
545 self
546 }
547
548 pub fn data_collection(mut self, policy: DataCollection) -> Self {
550 self.data_collection = Some(policy);
551 self
552 }
553
554 pub fn zdr(mut self, enable: bool) -> Self {
556 self.zdr = Some(enable);
557 self
558 }
559
560 pub fn sort(mut self, sort: impl Into<ProviderSort>) -> Self {
562 self.sort = Some(sort.into());
563 self
564 }
565
566 pub fn preferred_min_throughput(mut self, threshold: ThroughputThreshold) -> Self {
568 self.preferred_min_throughput = Some(threshold);
569 self
570 }
571
572 pub fn preferred_max_latency(mut self, threshold: LatencyThreshold) -> Self {
574 self.preferred_max_latency = Some(threshold);
575 self
576 }
577
578 pub fn max_price(mut self, price: MaxPrice) -> Self {
580 self.max_price = Some(price);
581 self
582 }
583
584 pub fn quantizations(mut self, quantizations: impl IntoIterator<Item = Quantization>) -> Self {
586 self.quantizations = Some(quantizations.into_iter().collect());
587 self
588 }
589
590 pub fn cheapest(self) -> Self {
592 self.sort(ProviderSortStrategy::Price)
593 }
594}
595
596#[non_exhaustive]
598#[derive(Clone, Debug, Default, PartialEq)]
599pub struct OpenRouterExtras {
600 pub provider: Option<String>,
603 pub native_finish_reason: Option<String>,
605 pub service_tier: Option<String>,
607 pub system_fingerprint: Option<String>,
609 pub cost: Option<f64>,
611 pub cost_details: Option<Map<String, Value>>,
614 pub is_byok: Option<bool>,
617 pub prompt_tokens_details: Option<Value>,
620 pub server_tool_use_details: Option<Value>,
622 pub openrouter_metadata: Option<Value>,
624 pub annotations: Option<Vec<Value>>,
626}
627
628impl ReplyExtras for OpenRouterExtras {
629 fn from_reply(api: &Api, raw: &Value) -> Result<Self, serde_json::Error> {
630 if api.as_str() == "openai.responses" {
631 return Ok(Self {
632 provider: reply_field(raw, "/provider")?,
633 service_tier: reply_field(raw, "/service_tier")?,
634 cost: reply_field(raw, "/usage/cost")?,
635 cost_details: reply_field(raw, "/usage/cost_details")?,
636 is_byok: reply_field(raw, "/usage/is_byok")?,
637 prompt_tokens_details: reply_field(raw, "/usage/input_tokens_details")?,
638 ..Self::default()
639 });
640 }
641 Ok(Self {
642 provider: reply_field(raw, "/provider")?,
643 native_finish_reason: reply_field(raw, "/choices/0/native_finish_reason")?,
644 service_tier: reply_field(raw, "/service_tier")?,
645 system_fingerprint: reply_field(raw, "/system_fingerprint")?,
646 cost: reply_field(raw, "/usage/cost")?,
647 cost_details: reply_field(raw, "/usage/cost_details")?,
648 is_byok: reply_field(raw, "/usage/is_byok")?,
649 prompt_tokens_details: reply_field(raw, "/usage/prompt_tokens_details")?,
650 server_tool_use_details: reply_field(raw, "/usage/server_tool_use_details")?,
651 openrouter_metadata: reply_field(raw, "/openrouter_metadata")?,
652 annotations: reply_field(raw, "/choices/0/message/annotations")?,
653 })
654 }
655}
656
657#[cfg(test)]
658mod tests;