Skip to main content

zeph_llm/router/
aware.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Sealed extension trait for router-specific quality-signal methods.
5//!
6//! [`RouterAware`] exposes the subset of [`crate::router::RouterProvider`] methods that only
7//! make sense when the underlying provider is a multi-provider router:
8//!
9//! - [`RouterAware::set_memory_confidence`] — MAR (Memory-Augmented Routing) signal
10//! - [`RouterAware::record_quality_outcome`] — RAPS (Reputation-Aware Provider Selection) signal
11//!
12//! The trait is **sealed** via the private `Sealed` supertrait. External crates cannot
13//! implement it, which prevents accidental silent no-ops on non-router providers.
14//!
15//! # Import discipline
16//!
17//! Call sites that need quality signals must import this trait explicitly:
18//!
19//! ```rust,no_run
20//! use zeph_llm::router::RouterAware;
21//! use zeph_llm::any::AnyProvider;
22//!
23//! fn report_quality(provider: &AnyProvider, name: &str, success: bool) {
24//!     provider.record_quality_outcome(name, success);
25//! }
26//! ```
27//!
28//! When the provider is not a [`crate::router::RouterProvider`], both methods are no-ops that
29//! emit a `tracing::trace!` event so the drop is observable in trace JSON.
30
31use crate::any::AnyProvider;
32use crate::provider::LlmProvider;
33use crate::router::RouterProvider;
34
35mod sealed {
36    pub trait Sealed {}
37}
38
39/// Extension trait for router-specific quality-signal methods.
40///
41/// Implemented only on [`crate::router::RouterProvider`] and [`AnyProvider`]. The trait is sealed to
42/// prevent external implementations — quality signals are only meaningful for multi-provider
43/// routers, and the `AnyProvider` impl provides the correct no-op + trace for non-router
44/// variants.
45///
46/// See the [module documentation](self) for usage.
47pub trait RouterAware: sealed::Sealed {
48    /// Set the MAR (Memory-Augmented Routing) confidence signal for the current turn.
49    ///
50    /// Must be called before `chat` / `chat_stream` to influence bandit provider selection.
51    /// Pass `None` to disable MAR for this turn.
52    ///
53    /// No-op (with a `tracing::trace!` event) when the underlying provider is not a router.
54    fn set_memory_confidence(&self, confidence: Option<f32>);
55
56    /// Record a semantic quality outcome for the last active sub-provider (RAPS).
57    ///
58    /// Call only for semantic failures (invalid tool arguments, parse errors).
59    /// Do NOT call for network errors, rate limits, or transient I/O failures.
60    ///
61    /// No-op (with a `tracing::trace!` event) when the underlying provider is not a router
62    /// or when reputation scoring is not enabled.
63    fn record_quality_outcome(&self, provider_name: &str, success: bool);
64}
65
66impl sealed::Sealed for RouterProvider {}
67
68impl RouterAware for RouterProvider {
69    fn set_memory_confidence(&self, confidence: Option<f32>) {
70        RouterProvider::set_memory_confidence(self, confidence);
71    }
72
73    fn record_quality_outcome(&self, provider_name: &str, success: bool) {
74        RouterProvider::record_quality_outcome(self, provider_name, success);
75    }
76}
77
78impl sealed::Sealed for AnyProvider {}
79
80impl RouterAware for AnyProvider {
81    fn set_memory_confidence(&self, confidence: Option<f32>) {
82        if let AnyProvider::Router(r) = self {
83            r.set_memory_confidence(confidence);
84        } else {
85            tracing::trace!(
86                provider_variant = self.name(),
87                confidence = ?confidence,
88                "set_memory_confidence: no-op (non-router provider; MAR signal requires RouterProvider)"
89            );
90        }
91    }
92
93    fn record_quality_outcome(&self, provider_name: &str, success: bool) {
94        if let AnyProvider::Router(p) = self {
95            p.record_quality_outcome(provider_name, success);
96        } else {
97            tracing::trace!(
98                provider_name,
99                success,
100                provider_variant = self.name(),
101                "record_quality_outcome: no-op (non-router provider; quality signals require RouterProvider)"
102            );
103        }
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110    use crate::claude::ClaudeProvider;
111    use crate::mock::MockProvider;
112    use crate::ollama::OllamaProvider;
113
114    #[test]
115    fn router_aware_noop_on_ollama_set_memory_confidence() {
116        let provider = AnyProvider::Ollama(OllamaProvider::new(
117            "http://localhost:11434",
118            "test".into(),
119            "embed".into(),
120        ));
121        // Must not panic; emits a tracing::trace! but is otherwise a no-op.
122        provider.set_memory_confidence(Some(0.9));
123    }
124
125    #[test]
126    fn router_aware_noop_on_claude_record_quality_outcome() {
127        let provider = AnyProvider::Claude(ClaudeProvider::new("key".into(), "model".into(), 1024));
128        // Must not panic; emits a tracing::trace! but is otherwise a no-op.
129        provider.record_quality_outcome("claude", true);
130    }
131
132    #[test]
133    fn router_aware_noop_on_mock_set_memory_confidence() {
134        let provider = AnyProvider::Mock(MockProvider::with_responses(vec!["ok".into()]));
135        provider.set_memory_confidence(None);
136    }
137
138    #[test]
139    fn router_aware_noop_on_mock_record_quality_outcome() {
140        let provider = AnyProvider::Mock(MockProvider::with_responses(vec!["ok".into()]));
141        provider.record_quality_outcome("mock", false);
142    }
143}