apr_cli/federation/
policy.rs1use super::traits::*;
7use std::time::Duration;
8
9#[derive(Debug, Clone)]
15pub struct SelectionCriteria {
16 pub capability: Capability,
18 pub min_health: HealthState,
20 pub max_latency: Option<Duration>,
22 pub min_privacy: PrivacyLevel,
24 pub preferred_regions: Vec<RegionId>,
26 pub excluded_nodes: Vec<NodeId>,
28}
29
30impl Default for SelectionCriteria {
31 fn default() -> Self {
32 Self {
33 capability: Capability::Generate,
34 min_health: HealthState::Degraded,
35 max_latency: None,
36 min_privacy: PrivacyLevel::Public,
37 preferred_regions: vec![],
38 excluded_nodes: vec![],
39 }
40 }
41}
42
43pub struct LatencyPolicy {
52 pub weight: f64,
54 pub max_latency: Duration,
56}
57
58impl Default for LatencyPolicy {
59 fn default() -> Self {
60 Self {
61 weight: 1.0,
62 max_latency: Duration::from_secs(5),
63 }
64 }
65}
66
67impl RoutingPolicyTrait for LatencyPolicy {
68 fn score(&self, candidate: &RouteCandidate, _request: &InferenceRequest) -> f64 {
69 let latency_ms = candidate.target.estimated_latency.as_millis() as f64;
70 let max_ms = self.max_latency.as_millis() as f64;
71
72 if latency_ms >= max_ms {
73 return 0.0;
74 }
75
76 let score = 1.0 - (latency_ms / max_ms);
78 score * self.weight
79 }
80
81 fn is_eligible(&self, candidate: &RouteCandidate, _request: &InferenceRequest) -> bool {
82 candidate.target.estimated_latency <= self.max_latency
83 }
84
85 fn name(&self) -> &'static str {
86 "latency"
87 }
88}
89
90pub struct LocalityPolicy {
95 pub weight: f64,
97 pub same_region_boost: f64,
99 pub cross_region_penalty: f64,
101}
102
103impl Default for LocalityPolicy {
104 fn default() -> Self {
105 Self {
106 weight: 1.0,
107 same_region_boost: 0.3,
108 cross_region_penalty: 0.1,
109 }
110 }
111}
112
113impl RoutingPolicyTrait for LocalityPolicy {
114 fn score(&self, candidate: &RouteCandidate, _request: &InferenceRequest) -> f64 {
115 let base_score = 0.5;
117
118 let score = base_score + candidate.scores.locality_score * self.same_region_boost;
120 score * self.weight
121 }
122
123 fn is_eligible(&self, _candidate: &RouteCandidate, _request: &InferenceRequest) -> bool {
124 true }
126
127 fn name(&self) -> &'static str {
128 "locality"
129 }
130}
131
132#[derive(Default)]
136pub struct PrivacyPolicy {
137 pub region_privacy: std::collections::HashMap<RegionId, PrivacyLevel>,
139}
140
141impl PrivacyPolicy {
142 #[must_use]
144 pub fn with_region(mut self, region: RegionId, level: PrivacyLevel) -> Self {
145 self.region_privacy.insert(region, level);
146 self
147 }
148}
149
150impl RoutingPolicyTrait for PrivacyPolicy {
151 fn score(&self, _candidate: &RouteCandidate, _request: &InferenceRequest) -> f64 {
152 1.0 }
154
155 fn is_eligible(&self, candidate: &RouteCandidate, request: &InferenceRequest) -> bool {
156 let region_level = self
157 .region_privacy
158 .get(&candidate.target.region_id)
159 .copied()
160 .unwrap_or(PrivacyLevel::Internal);
162
163 region_level >= request.qos.privacy
165 }
166
167 fn name(&self) -> &'static str {
168 "privacy"
169 }
170}
171
172pub struct CostPolicy {
176 pub weight: f64,
178 pub region_costs: std::collections::HashMap<RegionId, f64>,
180}
181
182impl Default for CostPolicy {
183 fn default() -> Self {
184 Self {
185 weight: 1.0,
186 region_costs: std::collections::HashMap::new(),
187 }
188 }
189}
190
191impl CostPolicy {
192 #[must_use]
193 pub fn with_region_cost(mut self, region: RegionId, cost: f64) -> Self {
194 self.region_costs.insert(region, cost.clamp(0.0, 1.0));
195 self
196 }
197}
198
199impl RoutingPolicyTrait for CostPolicy {
200 fn score(&self, candidate: &RouteCandidate, request: &InferenceRequest) -> f64 {
201 let region_cost = self
202 .region_costs
203 .get(&candidate.target.region_id)
204 .copied()
205 .unwrap_or(0.5);
206
207 let cost_tolerance = request.qos.cost_tolerance as f64 / 100.0;
208
209 let score = if cost_tolerance > 0.5 {
212 candidate.scores.throughput_score
214 } else {
215 1.0 - region_cost
217 };
218
219 score * self.weight
220 }
221
222 fn is_eligible(&self, _candidate: &RouteCandidate, _request: &InferenceRequest) -> bool {
223 true
224 }
225
226 fn name(&self) -> &'static str {
227 "cost"
228 }
229}
230
231pub struct HealthPolicy {
235 pub weight: f64,
237 pub healthy_score: f64,
239 pub degraded_score: f64,
241}
242
243impl Default for HealthPolicy {
244 fn default() -> Self {
245 Self {
246 weight: 2.0, healthy_score: 1.0,
248 degraded_score: 0.3,
249 }
250 }
251}
252
253impl RoutingPolicyTrait for HealthPolicy {
254 fn score(&self, candidate: &RouteCandidate, _request: &InferenceRequest) -> f64 {
255 candidate.scores.health_score * self.weight
256 }
257
258 fn is_eligible(&self, candidate: &RouteCandidate, _request: &InferenceRequest) -> bool {
259 candidate.scores.health_score > 0.0
261 }
262
263 fn name(&self) -> &'static str {
264 "health"
265 }
266}
267
268pub struct CompositePolicy {
274 policies: Vec<Box<dyn RoutingPolicyTrait>>,
275}
276
277impl CompositePolicy {
278 pub fn new() -> Self {
279 Self { policies: vec![] }
280 }
281
282 #[must_use]
283 pub fn with_policy(mut self, policy: impl RoutingPolicyTrait + 'static) -> Self {
284 self.policies.push(Box::new(policy));
285 self
286 }
287
288 pub fn enterprise_default() -> Self {
290 Self::new()
291 .with_policy(HealthPolicy::default())
292 .with_policy(LatencyPolicy::default())
293 .with_policy(PrivacyPolicy::default())
294 .with_policy(LocalityPolicy::default())
295 .with_policy(CostPolicy::default())
296 }
297}
298
299impl Default for CompositePolicy {
300 fn default() -> Self {
301 Self::enterprise_default()
302 }
303}
304
305impl RoutingPolicyTrait for CompositePolicy {
306 fn score(&self, candidate: &RouteCandidate, request: &InferenceRequest) -> f64 {
307 if self.policies.is_empty() {
308 return 1.0;
309 }
310
311 let total: f64 = self
312 .policies
313 .iter()
314 .map(|p| p.score(candidate, request))
315 .sum();
316
317 total / self.policies.len() as f64
318 }
319
320 fn is_eligible(&self, candidate: &RouteCandidate, request: &InferenceRequest) -> bool {
321 self.policies
323 .iter()
324 .all(|p| p.is_eligible(candidate, request))
325 }
326
327 fn name(&self) -> &'static str {
328 "composite"
329 }
330}
331
332pub struct RoutingPolicy {
338 #[allow(dead_code)]
339 inner: Box<dyn RoutingPolicyTrait>,
340}
341
342impl RoutingPolicy {
343 pub fn latency() -> Self {
344 Self {
345 inner: Box::new(LatencyPolicy::default()),
346 }
347 }
348
349 pub fn locality() -> Self {
350 Self {
351 inner: Box::new(LocalityPolicy::default()),
352 }
353 }
354
355 pub fn privacy() -> Self {
356 Self {
357 inner: Box::new(PrivacyPolicy::default()),
358 }
359 }
360
361 pub fn cost() -> Self {
362 Self {
363 inner: Box::new(CostPolicy::default()),
364 }
365 }
366
367 pub fn health() -> Self {
368 Self {
369 inner: Box::new(HealthPolicy::default()),
370 }
371 }
372
373 pub fn enterprise() -> Self {
374 Self {
375 inner: Box::new(CompositePolicy::enterprise_default()),
376 }
377 }
378}
379
380#[cfg(test)]
385#[path = "policy_tests.rs"]
386mod tests;