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}