sentinel_proxy/proxy/
model_routing_metrics.rs

1//! Model-based routing metrics for observability.
2//!
3//! Provides Prometheus metrics for:
4//! - Model routing decisions by route, model, and upstream
5//! - Default upstream fallbacks when no pattern matches
6//! - Pattern match counts for tuning
7
8use anyhow::{Context, Result};
9use once_cell::sync::OnceCell;
10use prometheus::{register_int_counter_vec, IntCounterVec};
11use std::sync::Arc;
12
13/// Global model routing metrics instance.
14static MODEL_ROUTING_METRICS: OnceCell<Arc<ModelRoutingMetrics>> = OnceCell::new();
15
16/// Get or initialize the global model routing metrics.
17pub fn get_model_routing_metrics() -> Option<Arc<ModelRoutingMetrics>> {
18    MODEL_ROUTING_METRICS.get().cloned()
19}
20
21/// Initialize the global model routing metrics.
22/// Returns Ok if already initialized or initialization succeeds.
23pub fn init_model_routing_metrics() -> Result<Arc<ModelRoutingMetrics>> {
24    if let Some(metrics) = MODEL_ROUTING_METRICS.get() {
25        return Ok(metrics.clone());
26    }
27
28    let metrics = Arc::new(ModelRoutingMetrics::new()?);
29    let _ = MODEL_ROUTING_METRICS.set(metrics.clone());
30    Ok(metrics)
31}
32
33/// Model routing metrics collector.
34///
35/// Tracks model-based routing decisions for observability and capacity planning.
36pub struct ModelRoutingMetrics {
37    /// Total model routing decisions
38    /// Labels: route, model, upstream
39    model_routed: IntCounterVec,
40
41    /// Requests using default upstream (no pattern matched)
42    /// Labels: route
43    default_upstream_used: IntCounterVec,
44
45    /// Requests with no model header detected
46    /// Labels: route
47    no_model_header: IntCounterVec,
48
49    /// Provider override applied
50    /// Labels: route, upstream, provider
51    provider_override: IntCounterVec,
52}
53
54impl ModelRoutingMetrics {
55    /// Create new model routing metrics and register with Prometheus.
56    pub fn new() -> Result<Self> {
57        let model_routed = register_int_counter_vec!(
58            "sentinel_model_routing_total",
59            "Total number of requests routed based on model name",
60            &["route", "model", "upstream"]
61        )
62        .context("Failed to register model_routing metric")?;
63
64        let default_upstream_used = register_int_counter_vec!(
65            "sentinel_model_routing_default_total",
66            "Requests falling back to default upstream (no pattern matched)",
67            &["route"]
68        )
69        .context("Failed to register model_routing_default metric")?;
70
71        let no_model_header = register_int_counter_vec!(
72            "sentinel_model_routing_no_header_total",
73            "Requests with no model header detected",
74            &["route"]
75        )
76        .context("Failed to register model_routing_no_header metric")?;
77
78        let provider_override = register_int_counter_vec!(
79            "sentinel_model_routing_provider_override_total",
80            "Requests where provider was overridden by model routing",
81            &["route", "upstream", "provider"]
82        )
83        .context("Failed to register model_routing_provider_override metric")?;
84
85        Ok(Self {
86            model_routed,
87            default_upstream_used,
88            no_model_header,
89            provider_override,
90        })
91    }
92
93    /// Record a model routing decision.
94    ///
95    /// Called when a request is routed to a specific upstream based on model name.
96    pub fn record_model_routed(&self, route: &str, model: &str, upstream: &str) {
97        self.model_routed
98            .with_label_values(&[route, model, upstream])
99            .inc();
100    }
101
102    /// Record use of default upstream.
103    ///
104    /// Called when no model pattern matched and the default upstream is used.
105    pub fn record_default_upstream(&self, route: &str) {
106        self.default_upstream_used
107            .with_label_values(&[route])
108            .inc();
109    }
110
111    /// Record request with no model header.
112    ///
113    /// Called when model routing is configured but no model header was found.
114    pub fn record_no_model_header(&self, route: &str) {
115        self.no_model_header.with_label_values(&[route]).inc();
116    }
117
118    /// Record provider override.
119    ///
120    /// Called when model routing overrides the inference provider.
121    pub fn record_provider_override(&self, route: &str, upstream: &str, provider: &str) {
122        self.provider_override
123            .with_label_values(&[route, upstream, provider])
124            .inc();
125    }
126}