lean_ctx/proxy/
break_even.rs1use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, Ordering};
2
3pub struct BreakEvenCalculator {
5 proxy_tokens_saved: AtomicI64,
6 schema_overhead_per_turn: u64,
7 turn_count: AtomicU64,
8 has_used_ctx_tools: AtomicBool,
9}
10
11#[derive(Debug, Clone)]
13pub struct BreakEvenSummary {
14 pub proxy_savings: i64,
15 pub estimated_mcp_overhead: i64,
16 pub net: i64,
17 pub mcp_recommended: bool,
18 pub reason: &'static str,
19}
20
21impl BreakEvenCalculator {
22 pub fn new(schema_overhead_per_turn: u64) -> Self {
25 Self {
26 proxy_tokens_saved: AtomicI64::new(0),
27 schema_overhead_per_turn,
28 turn_count: AtomicU64::new(0),
29 has_used_ctx_tools: AtomicBool::new(false),
30 }
31 }
32
33 pub fn record_proxy_savings(&self, tokens: i64) {
35 self.proxy_tokens_saved.fetch_add(tokens, Ordering::Relaxed);
36 }
37
38 pub fn record_turn(&self) {
40 self.turn_count.fetch_add(1, Ordering::Relaxed);
41 }
42
43 pub fn mark_ctx_tool_used(&self) {
46 self.has_used_ctx_tools.store(true, Ordering::Relaxed);
47 }
48
49 pub fn should_enable_mcp(&self) -> bool {
51 if self.has_used_ctx_tools.load(Ordering::Relaxed) {
52 return true;
53 }
54
55 let turns = self.turn_count.load(Ordering::Relaxed);
56 if turns < 2 {
57 return false;
58 }
59
60 let savings = self.proxy_tokens_saved.load(Ordering::Relaxed);
61 let overhead = self.schema_overhead_per_turn as i64 * turns as i64;
62 savings > overhead
63 }
64
65 pub fn summary(&self) -> BreakEvenSummary {
67 let turns = self.turn_count.load(Ordering::Relaxed);
68 let savings = self.proxy_tokens_saved.load(Ordering::Relaxed);
69 let overhead = self.schema_overhead_per_turn as i64 * turns as i64;
70 let net = savings - overhead;
71 let mcp_recommended = self.should_enable_mcp();
72
73 let reason = if self.has_used_ctx_tools.load(Ordering::Relaxed) {
74 "ctx_tools used — MCP locked on"
75 } else if turns < 2 {
76 "too few turns — proxy-only"
77 } else if mcp_recommended {
78 "savings exceed schema overhead"
79 } else {
80 "schema overhead exceeds savings"
81 };
82
83 BreakEvenSummary {
84 proxy_savings: savings,
85 estimated_mcp_overhead: overhead,
86 net,
87 mcp_recommended,
88 reason,
89 }
90 }
91
92 pub fn turn_count(&self) -> u64 {
94 self.turn_count.load(Ordering::Relaxed)
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::BreakEvenCalculator;
101
102 #[test]
103 fn new_calculator_starts_proxy_only() {
104 let calc = BreakEvenCalculator::new(3500);
105 assert!(!calc.should_enable_mcp());
106 assert_eq!(calc.turn_count(), 0);
107 }
108
109 #[test]
110 fn first_two_turns_always_proxy_only() {
111 let calc = BreakEvenCalculator::new(1500);
112 calc.record_proxy_savings(50000);
113 calc.record_turn();
114 assert!(!calc.should_enable_mcp());
115 }
116
117 #[test]
118 fn high_savings_enables_mcp() {
119 let calc = BreakEvenCalculator::new(1500);
120 for _ in 0..5 {
121 calc.record_proxy_savings(5000);
122 calc.record_turn();
123 }
124 assert!(calc.should_enable_mcp());
125 }
126
127 #[test]
128 fn low_savings_stays_proxy_only() {
129 let calc = BreakEvenCalculator::new(3500);
130 for _ in 0..3 {
131 calc.record_proxy_savings(500);
132 calc.record_turn();
133 }
134 assert!(!calc.should_enable_mcp());
135 }
136
137 #[test]
138 fn ctx_tool_usage_locks_mcp_on() {
139 let calc = BreakEvenCalculator::new(3500);
140 calc.mark_ctx_tool_used();
141 assert!(calc.should_enable_mcp());
142 assert!(calc.should_enable_mcp());
143 }
144
145 #[test]
146 fn summary_reflects_state() {
147 let calc = BreakEvenCalculator::new(2000);
148 calc.record_turn();
149 calc.record_turn();
150 calc.record_proxy_savings(3000);
151 let summary = calc.summary();
152 assert_eq!(summary.proxy_savings, 3000);
153 assert_eq!(summary.estimated_mcp_overhead, 4000);
154 assert_eq!(summary.net, -1000);
155 assert!(!summary.mcp_recommended);
156 }
157
158 #[test]
159 fn summary_reason_for_ctx_tools() {
160 let calc = BreakEvenCalculator::new(2000);
161 calc.mark_ctx_tool_used();
162 let summary = calc.summary();
163 assert_eq!(summary.reason, "ctx_tools used — MCP locked on");
164 assert!(summary.mcp_recommended);
165 }
166}