1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
//! Getting Started — 3 backends in under 60 lines.
//!
//! No CUSUM thresholds. No KL divergence. No Pareto frontiers.
//! Just: create a Router → pick an arm → make a call → score it → observe.
//!
//! This covers the 80% case: a small number of arms (here 3), deterministic
//! routing that prefers the best arm, delayed quality labeling (you score the
//! response AFTER the call), and automatic explore-first for new arms.
//!
//! Run with:
//! cargo run --example getting_started
use muxer::{Outcome, Router, RouterConfig};
fn main() {
// -----------------------------------------------------------------
// 1. Create a Router for 3 backends.
// -----------------------------------------------------------------
let arms = vec![
"gpt-4o".to_string(),
"claude-sonnet".to_string(),
"gemini-pro".to_string(),
];
// RouterConfig::default() gives you:
// - 100-outcome sliding window per arm
// - explore-first (each arm tried before exploitation starts)
// - deterministic UCB selection (same config → same picks)
//
// For throughput-aware window sizing:
// let cap = muxer::suggested_window_cap(500, 0.14); // ~500 calls/day, weekly change
// RouterConfig::default().window_cap(cap)
let mut router = Router::new(arms, RouterConfig::default()).unwrap();
// Simulated quality profiles per arm.
let quality_for = |arm: &str| -> (f64, bool) {
match arm {
"gpt-4o" => (0.92, false), // high quality
"claude-sonnet" => (0.78, false), // moderate quality (below threshold)
"gemini-pro" => (0.55, true), // frequent junk
_ => (1.0, false),
}
};
// -----------------------------------------------------------------
// 2. Routing loop.
// -----------------------------------------------------------------
println!("=== First 6 rounds (explore-first) ===");
for round in 0..30_u64 {
// Pick 1 arm. Use k=2 or k=3 for faster initial coverage of many arms.
let decision = router.select(1, round);
let arm = decision.primary().unwrap().to_string();
// Step 1: push the outcome immediately with junk=false (you don't
// know quality yet — the call hasn't been scored).
router.observe(&arm, Outcome::success(5, 200));
// Step 2 (delayed): score the response after downstream processing.
let (quality, is_junk) = quality_for(&arm);
router.set_last_junk_level(&arm, is_junk, false);
router.set_last_quality_score(&arm, quality);
if round < 6 {
println!(" round {round:2}: chose {:15} quality={quality:.2} junk={is_junk} prechosen={:?}",
arm, decision.prechosen);
}
}
// -----------------------------------------------------------------
// 3. Inspect results.
// -----------------------------------------------------------------
println!("\n=== After 30 rounds ===");
for arm in router.arms() {
let s = router.summary(arm);
let q = s
.mean_quality_score
.map(|v| format!("{v:.2}"))
.unwrap_or("—".into());
println!(
" {:15} calls={:2} ok={:.2} junk={:.2} quality={}",
arm,
s.calls,
s.ok_rate(),
s.junk_rate(),
q
);
}
// The Router selects the best arm given current stats.
let best = router.select(1, 99);
println!(
"\nBest arm now: {:?} (mode: {:?})",
best.primary().unwrap(),
router.mode()
);
// Note: gpt-4o and claude-sonnet both have ok_rate=1.0, junk=0 — without
// quality_weight they are indistinguishable and the router tie-breaks
// alphabetically. The quality_score field preserves the gradient.
// -----------------------------------------------------------------
// 4. Enable quality_weight to break the tie using the gradient signal.
// -----------------------------------------------------------------
use muxer::{select_mab, MabConfig};
use std::collections::BTreeMap;
let cfg = MabConfig {
exploration_c: 0.0,
..MabConfig::default()
}
.with_quality_weight(1.0);
let summaries: BTreeMap<String, _> = router
.arms()
.iter()
.map(|a| (a.clone(), router.summary(a)))
.collect();
let sel = select_mab(router.arms(), &summaries, cfg);
println!("\nWith quality_weight=1.0: best arm is {:?}", sel.chosen);
// → "gpt-4o" wins because mean_quality_score=0.92 > claude-sonnet's 0.78.
// -----------------------------------------------------------------
// 5. Where to go next.
// -----------------------------------------------------------------
// - Add monitoring + triage: RouterConfig::default().with_monitoring(400, 80).with_triage()
// - Use select(k=3) for faster initial coverage of 20+ arms.
// - Full production pattern: cargo run --example router_production --features stochastic
// - Theoretical background: examples/EXPERIMENTS.md
}