1use serde::{Deserialize, Serialize};
12use std::collections::HashSet;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
20pub enum ServingBackend {
21 Realizar,
23 Ollama,
24 LlamaCpp,
25 Llamafile,
26 Candle,
27 Vllm,
28 Tgi,
29 LocalAI,
30
31 HuggingFace,
33 Together,
34 Replicate,
35 Anyscale,
36 Modal,
37 Fireworks,
38 Groq,
39 OpenAI,
40 Anthropic,
41 AzureOpenAI,
42 AwsBedrock,
43 GoogleVertex,
44
45 AwsLambda,
47 CloudflareWorkers,
48}
49
50impl ServingBackend {
51 #[must_use]
53 pub const fn is_local(&self) -> bool {
54 matches!(
55 self,
56 Self::Realizar
57 | Self::Ollama
58 | Self::LlamaCpp
59 | Self::Llamafile
60 | Self::Candle
61 | Self::Vllm
62 | Self::Tgi
63 | Self::LocalAI
64 )
65 }
66
67 #[must_use]
69 pub const fn is_remote(&self) -> bool {
70 !self.is_local()
71 }
72
73 #[must_use]
75 pub const fn api_host(&self) -> Option<&'static str> {
76 match self {
77 Self::HuggingFace => Some("api-inference.huggingface.co"),
78 Self::Together => Some("api.together.xyz"),
79 Self::Replicate => Some("api.replicate.com"),
80 Self::Anyscale => Some("api.anyscale.com"),
81 Self::Modal => Some("api.modal.com"),
82 Self::Fireworks => Some("api.fireworks.ai"),
83 Self::Groq => Some("api.groq.com"),
84 Self::OpenAI => Some("api.openai.com"),
85 Self::Anthropic => Some("api.anthropic.com"),
86 Self::AzureOpenAI => Some("openai.azure.com"),
87 Self::AwsBedrock => Some("bedrock-runtime.amazonaws.com"),
88 Self::GoogleVertex => Some("aiplatform.googleapis.com"),
89 Self::AwsLambda => Some("lambda.amazonaws.com"),
90 Self::CloudflareWorkers => Some("workers.cloudflare.com"),
91 _ => None,
92 }
93 }
94
95 #[must_use]
97 pub const fn is_serverless(&self) -> bool {
98 matches!(self, Self::AwsLambda | Self::CloudflareWorkers | Self::Modal)
99 }
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
108pub enum PrivacyTier {
109 Sovereign,
111 Private,
113 #[default]
115 Standard,
116}
117
118impl PrivacyTier {
119 #[must_use]
121 pub fn allows(&self, backend: ServingBackend) -> bool {
122 match self {
123 Self::Sovereign => backend.is_local(),
124 Self::Private => {
125 backend.is_local()
126 || matches!(
127 backend,
128 ServingBackend::AzureOpenAI
129 | ServingBackend::AwsBedrock
130 | ServingBackend::GoogleVertex
131 | ServingBackend::AwsLambda )
133 }
134 Self::Standard => true,
135 }
136 }
137
138 #[must_use]
140 pub fn blocked_hosts(&self) -> Vec<&'static str> {
141 match self {
142 Self::Sovereign => {
143 vec![
145 "api-inference.huggingface.co",
146 "api.together.xyz",
147 "api.replicate.com",
148 "api.anyscale.com",
149 "api.modal.com",
150 "api.fireworks.ai",
151 "api.groq.com",
152 "api.openai.com",
153 "api.anthropic.com",
154 "openai.azure.com",
155 "bedrock-runtime.amazonaws.com",
156 "aiplatform.googleapis.com",
157 "lambda.amazonaws.com",
158 "workers.cloudflare.com",
159 ]
160 }
161 Self::Private => {
162 vec![
164 "api-inference.huggingface.co",
165 "api.together.xyz",
166 "api.replicate.com",
167 "api.anyscale.com",
168 "api.modal.com",
169 "api.fireworks.ai",
170 "api.groq.com",
171 "api.openai.com",
172 "api.anthropic.com",
173 "workers.cloudflare.com", ]
175 }
176 Self::Standard => vec![],
177 }
178 }
179}
180
181#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
187pub enum LatencyTier {
188 RealTime,
190 #[default]
192 Interactive,
193 Batch,
195}
196
197#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
199pub enum ThroughputTier {
200 #[default]
202 Low,
203 Medium,
205 High,
207}
208
209#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
211pub enum CostTier {
212 Frugal,
214 #[default]
216 Balanced,
217 Premium,
219}
220
221#[derive(Debug, Clone, Default, Serialize, Deserialize)]
227pub struct BackendSelector {
228 pub privacy: PrivacyTier,
229 pub latency: LatencyTier,
230 pub throughput: ThroughputTier,
231 pub cost: CostTier,
232 pub disabled: HashSet<ServingBackend>,
234 pub preferred: Vec<ServingBackend>,
236}
237
238impl BackendSelector {
239 #[must_use]
241 pub fn new() -> Self {
242 Self::default()
243 }
244
245 #[must_use]
247 pub fn with_privacy(mut self, tier: PrivacyTier) -> Self {
248 self.privacy = tier;
249 self
250 }
251
252 #[must_use]
254 pub fn with_latency(mut self, tier: LatencyTier) -> Self {
255 self.latency = tier;
256 self
257 }
258
259 #[must_use]
261 pub fn with_throughput(mut self, tier: ThroughputTier) -> Self {
262 self.throughput = tier;
263 self
264 }
265
266 #[must_use]
268 pub fn with_cost(mut self, tier: CostTier) -> Self {
269 self.cost = tier;
270 self
271 }
272
273 #[must_use]
275 pub fn disable(mut self, backend: ServingBackend) -> Self {
276 self.disabled.insert(backend);
277 self
278 }
279
280 #[must_use]
282 pub fn prefer(mut self, backend: ServingBackend) -> Self {
283 self.preferred.push(backend);
284 self
285 }
286
287 #[must_use]
289 pub fn recommend(&self) -> Vec<ServingBackend> {
290 let mut candidates: Vec<ServingBackend> = Vec::new();
291
292 for backend in &self.preferred {
294 if self.is_valid(*backend) {
295 candidates.push(*backend);
296 }
297 }
298
299 let tier_backends = self.get_tier_backends();
301 for backend in tier_backends {
302 if self.is_valid(backend) && !candidates.contains(&backend) {
303 candidates.push(backend);
304 }
305 }
306
307 candidates
308 }
309
310 #[must_use]
312 pub fn is_valid(&self, backend: ServingBackend) -> bool {
313 !self.disabled.contains(&backend) && self.privacy.allows(backend)
314 }
315
316 pub fn validate(&self, backend: ServingBackend) -> Result<(), PrivacyViolation> {
320 if self.disabled.contains(&backend) {
321 return Err(PrivacyViolation::BackendDisabled(backend));
322 }
323
324 if !self.privacy.allows(backend) {
325 return Err(PrivacyViolation::TierViolation { backend, tier: self.privacy });
326 }
327
328 Ok(())
329 }
330
331 fn get_tier_backends(&self) -> Vec<ServingBackend> {
332 match (self.latency, self.privacy) {
333 (LatencyTier::RealTime, PrivacyTier::Sovereign) => {
335 vec![ServingBackend::Realizar, ServingBackend::LlamaCpp]
336 }
337 (LatencyTier::RealTime, _) => {
339 vec![ServingBackend::Groq, ServingBackend::Realizar, ServingBackend::Fireworks]
340 }
341 (LatencyTier::Interactive, PrivacyTier::Sovereign) => {
343 vec![ServingBackend::Realizar, ServingBackend::Ollama, ServingBackend::LlamaCpp]
344 }
345 (LatencyTier::Interactive, PrivacyTier::Private) => {
347 vec![
348 ServingBackend::Realizar,
349 ServingBackend::Ollama,
350 ServingBackend::AzureOpenAI,
351 ServingBackend::AwsBedrock,
352 ServingBackend::AwsLambda,
353 ]
354 }
355 (LatencyTier::Interactive, PrivacyTier::Standard) => {
357 vec![
358 ServingBackend::Realizar,
359 ServingBackend::Groq,
360 ServingBackend::Together,
361 ServingBackend::Fireworks,
362 ]
363 }
364 (LatencyTier::Batch, PrivacyTier::Sovereign) => {
366 vec![ServingBackend::Realizar, ServingBackend::Ollama]
367 }
368 (LatencyTier::Batch, PrivacyTier::Private) => {
370 vec![
371 ServingBackend::AwsLambda,
372 ServingBackend::Realizar,
373 ServingBackend::AwsBedrock,
374 ]
375 }
376 (LatencyTier::Batch, PrivacyTier::Standard) => {
378 vec![
379 ServingBackend::AwsLambda,
380 ServingBackend::Together,
381 ServingBackend::HuggingFace,
382 ServingBackend::Replicate,
383 ]
384 }
385 }
386 }
387}
388
389#[derive(Debug, Clone, PartialEq, Eq)]
391pub enum PrivacyViolation {
392 BackendDisabled(ServingBackend),
394 TierViolation { backend: ServingBackend, tier: PrivacyTier },
396}
397
398impl std::fmt::Display for PrivacyViolation {
399 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
400 match self {
401 Self::BackendDisabled(b) => write!(f, "Backend {:?} is disabled", b),
402 Self::TierViolation { backend, tier } => {
403 write!(f, "Backend {:?} violates {:?} privacy tier", backend, tier)
404 }
405 }
406 }
407}
408
409impl std::error::Error for PrivacyViolation {}
410
411#[cfg(test)]
416#[allow(non_snake_case)]
417mod tests {
418 use super::*;
419
420 #[test]
425 fn test_SERVE_BKD_001_local_backends() {
426 assert!(ServingBackend::Realizar.is_local());
427 assert!(ServingBackend::Ollama.is_local());
428 assert!(ServingBackend::LlamaCpp.is_local());
429 assert!(ServingBackend::Llamafile.is_local());
430 assert!(ServingBackend::Candle.is_local());
431 assert!(ServingBackend::Vllm.is_local());
432 assert!(ServingBackend::Tgi.is_local());
433 assert!(ServingBackend::LocalAI.is_local());
434 }
435
436 #[test]
437 fn test_SERVE_BKD_001_remote_backends() {
438 assert!(ServingBackend::OpenAI.is_remote());
439 assert!(ServingBackend::Anthropic.is_remote());
440 assert!(ServingBackend::Together.is_remote());
441 assert!(ServingBackend::Groq.is_remote());
442 assert!(ServingBackend::HuggingFace.is_remote());
443 }
444
445 #[test]
446 fn test_SERVE_BKD_001_api_hosts() {
447 assert_eq!(ServingBackend::OpenAI.api_host(), Some("api.openai.com"));
448 assert_eq!(ServingBackend::Anthropic.api_host(), Some("api.anthropic.com"));
449 assert_eq!(ServingBackend::Realizar.api_host(), None);
450 }
451
452 #[test]
457 fn test_SERVE_BKD_002_sovereign_blocks_remote() {
458 let tier = PrivacyTier::Sovereign;
459 assert!(tier.allows(ServingBackend::Realizar));
460 assert!(tier.allows(ServingBackend::Ollama));
461 assert!(!tier.allows(ServingBackend::OpenAI));
462 assert!(!tier.allows(ServingBackend::Anthropic));
463 assert!(!tier.allows(ServingBackend::AzureOpenAI));
464 }
465
466 #[test]
467 fn test_SERVE_BKD_002_private_allows_enterprise() {
468 let tier = PrivacyTier::Private;
469 assert!(tier.allows(ServingBackend::Realizar));
470 assert!(tier.allows(ServingBackend::AzureOpenAI));
471 assert!(tier.allows(ServingBackend::AwsBedrock));
472 assert!(tier.allows(ServingBackend::GoogleVertex));
473 assert!(!tier.allows(ServingBackend::OpenAI));
474 assert!(!tier.allows(ServingBackend::Together));
475 }
476
477 #[test]
478 fn test_SERVE_BKD_002_standard_allows_all() {
479 let tier = PrivacyTier::Standard;
480 assert!(tier.allows(ServingBackend::Realizar));
481 assert!(tier.allows(ServingBackend::OpenAI));
482 assert!(tier.allows(ServingBackend::Together));
483 }
484
485 #[test]
486 fn test_SERVE_BKD_002_sovereign_blocked_hosts() {
487 let hosts = PrivacyTier::Sovereign.blocked_hosts();
488 assert!(hosts.contains(&"api.openai.com"));
489 assert!(hosts.contains(&"api.anthropic.com"));
490 assert!(hosts.contains(&"api.together.xyz"));
491 assert!(hosts.contains(&"lambda.amazonaws.com"));
492 assert!(hosts.contains(&"workers.cloudflare.com"));
493 assert_eq!(hosts.len(), 14);
494 }
495
496 #[test]
497 fn test_SERVE_BKD_002_standard_no_blocked_hosts() {
498 let hosts = PrivacyTier::Standard.blocked_hosts();
499 assert!(hosts.is_empty());
500 }
501
502 #[test]
507 fn test_SERVE_BKD_003_default_selector() {
508 let selector = BackendSelector::new();
509 assert_eq!(selector.privacy, PrivacyTier::Standard);
510 assert_eq!(selector.latency, LatencyTier::Interactive);
511 }
512
513 #[test]
514 fn test_SERVE_BKD_003_sovereign_recommend() {
515 let selector = BackendSelector::new().with_privacy(PrivacyTier::Sovereign);
516 let backends = selector.recommend();
517 for backend in &backends {
519 assert!(backend.is_local(), "{:?} should be local", backend);
520 }
521 }
522
523 #[test]
524 fn test_SERVE_BKD_003_realtime_recommend() {
525 let selector = BackendSelector::new().with_latency(LatencyTier::RealTime);
526 let backends = selector.recommend();
527 assert!(backends.contains(&ServingBackend::Groq));
529 }
530
531 #[test]
532 fn test_SERVE_BKD_003_disabled_backend() {
533 let selector = BackendSelector::new().disable(ServingBackend::OpenAI);
534 assert!(!selector.is_valid(ServingBackend::OpenAI));
535 assert!(selector.is_valid(ServingBackend::Anthropic));
536 }
537
538 #[test]
539 fn test_SERVE_BKD_003_preferred_backend() {
540 let selector = BackendSelector::new().prefer(ServingBackend::Anthropic);
541 let backends = selector.recommend();
542 assert_eq!(backends[0], ServingBackend::Anthropic);
543 }
544
545 #[test]
550 fn test_SERVE_BKD_004_validate_sovereign_blocks_openai() {
551 let selector = BackendSelector::new().with_privacy(PrivacyTier::Sovereign);
552 let result = selector.validate(ServingBackend::OpenAI);
553 assert!(result.is_err());
554 assert!(matches!(result.unwrap_err(), PrivacyViolation::TierViolation { .. }));
555 }
556
557 #[test]
558 fn test_SERVE_BKD_004_validate_sovereign_allows_local() {
559 let selector = BackendSelector::new().with_privacy(PrivacyTier::Sovereign);
560 assert!(selector.validate(ServingBackend::Realizar).is_ok());
561 assert!(selector.validate(ServingBackend::Ollama).is_ok());
562 }
563
564 #[test]
565 fn test_SERVE_BKD_004_validate_disabled() {
566 let selector = BackendSelector::new().disable(ServingBackend::Together);
567 let result = selector.validate(ServingBackend::Together);
568 assert!(result.is_err());
569 assert!(matches!(result.unwrap_err(), PrivacyViolation::BackendDisabled(_)));
570 }
571
572 #[test]
573 fn test_SERVE_BKD_004_privacy_violation_display() {
574 let err = PrivacyViolation::TierViolation {
575 backend: ServingBackend::OpenAI,
576 tier: PrivacyTier::Sovereign,
577 };
578 assert!(err.to_string().contains("OpenAI"));
579 assert!(err.to_string().contains("Sovereign"));
580 }
581
582 #[test]
587 fn test_SERVE_BKD_005_builder_chain() {
588 let selector = BackendSelector::new()
589 .with_privacy(PrivacyTier::Private)
590 .with_latency(LatencyTier::RealTime)
591 .with_throughput(ThroughputTier::High)
592 .with_cost(CostTier::Premium)
593 .prefer(ServingBackend::AzureOpenAI)
594 .disable(ServingBackend::AwsBedrock);
595
596 assert_eq!(selector.privacy, PrivacyTier::Private);
597 assert_eq!(selector.latency, LatencyTier::RealTime);
598 assert_eq!(selector.throughput, ThroughputTier::High);
599 assert_eq!(selector.cost, CostTier::Premium);
600 assert!(selector.preferred.contains(&ServingBackend::AzureOpenAI));
601 assert!(selector.disabled.contains(&ServingBackend::AwsBedrock));
602 }
603
604 #[test]
609 fn test_SERVE_BKD_006_empty_recommend() {
610 let selector = BackendSelector::new()
612 .with_privacy(PrivacyTier::Sovereign)
613 .with_latency(LatencyTier::Interactive)
614 .disable(ServingBackend::Realizar)
615 .disable(ServingBackend::Ollama)
616 .disable(ServingBackend::LlamaCpp);
617 let backends = selector.recommend();
618 assert!(backends.is_empty());
621 }
622
623 #[test]
624 fn test_SERVE_BKD_006_batch_tier_prefers_cheap() {
625 let selector = BackendSelector::new()
626 .with_latency(LatencyTier::Batch)
627 .with_privacy(PrivacyTier::Standard);
628 let backends = selector.recommend();
629 assert!(backends.contains(&ServingBackend::AwsLambda));
631 }
632
633 #[test]
638 fn test_SERVE_BKD_007_lambda_is_serverless() {
639 assert!(ServingBackend::AwsLambda.is_serverless());
640 assert!(ServingBackend::CloudflareWorkers.is_serverless());
641 assert!(ServingBackend::Modal.is_serverless());
642 assert!(!ServingBackend::Realizar.is_serverless());
643 assert!(!ServingBackend::OpenAI.is_serverless());
644 }
645
646 #[test]
647 fn test_SERVE_BKD_007_lambda_api_host() {
648 assert_eq!(ServingBackend::AwsLambda.api_host(), Some("lambda.amazonaws.com"));
649 assert_eq!(ServingBackend::CloudflareWorkers.api_host(), Some("workers.cloudflare.com"));
650 }
651
652 #[test]
653 fn test_SERVE_BKD_007_lambda_privacy_tier() {
654 assert!(PrivacyTier::Private.allows(ServingBackend::AwsLambda));
656 assert!(PrivacyTier::Standard.allows(ServingBackend::AwsLambda));
657 assert!(!PrivacyTier::Sovereign.allows(ServingBackend::AwsLambda));
659 }
660
661 #[test]
662 fn test_SERVE_BKD_007_batch_private_includes_lambda() {
663 let selector = BackendSelector::new()
664 .with_latency(LatencyTier::Batch)
665 .with_privacy(PrivacyTier::Private);
666 let backends = selector.recommend();
667 assert!(backends.contains(&ServingBackend::AwsLambda));
669 }
670
671 #[test]
672 fn test_SERVE_BKD_007_interactive_private_includes_lambda() {
673 let selector = BackendSelector::new()
674 .with_latency(LatencyTier::Interactive)
675 .with_privacy(PrivacyTier::Private);
676 let backends = selector.recommend();
677 assert!(backends.contains(&ServingBackend::AwsLambda));
679 }
680
681 #[test]
682 fn test_SERVE_BKD_007_lambda_is_remote() {
683 assert!(ServingBackend::AwsLambda.is_remote());
684 assert!(ServingBackend::CloudflareWorkers.is_remote());
685 }
686
687 #[test]
688 fn test_SERVE_BKD_007_cloudflare_blocked_in_private() {
689 assert!(!PrivacyTier::Private.allows(ServingBackend::CloudflareWorkers));
691 }
692
693 #[test]
694 fn test_SERVE_BKD_007_private_blocked_hosts_includes_cloudflare() {
695 let hosts = PrivacyTier::Private.blocked_hosts();
696 assert!(hosts.contains(&"workers.cloudflare.com"));
697 assert!(!hosts.contains(&"lambda.amazonaws.com"));
699 }
700}