lean_ctx/core/context_kernel/
etpao_live.rs1use std::collections::HashMap;
4
5use serde::{Deserialize, Serialize};
6
7use super::coverage_class::CoverageClass;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct RequestMetrics {
12 pub input_tokens: usize,
14 pub output_tokens: usize,
16 pub reasoning_tokens: usize,
18 pub schema_tokens: usize,
20 pub cache_write_tokens: usize,
22 pub retry_count: usize,
24 pub client_id: String,
26 pub coverage_class: CoverageClass,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct OutcomeMetrics {
33 pub accepted: bool,
35 pub quality_score: f64,
37 pub first_pass: bool,
39 pub client_id: String,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct EtpaoSummary {
46 pub etpao: f64,
48 pub total_tokens: usize,
50 pub accepted_outcomes: usize,
52 pub first_pass_rate: f64,
54 pub retry_tax_pct: f64,
56 pub by_coverage_class: HashMap<String, f64>,
58}
59
60#[derive(Debug, Clone, Default)]
62pub struct EtpaoLive {
63 requests: Vec<RequestMetrics>,
64 outcomes: Vec<OutcomeMetrics>,
65}
66
67impl EtpaoLive {
68 pub fn new() -> Self {
70 Self::default()
71 }
72
73 pub fn record_request(&mut self, req: RequestMetrics) {
75 self.requests.push(req);
76 }
77
78 pub fn record_outcome(&mut self, outcome: OutcomeMetrics) {
80 self.outcomes.push(outcome);
81 }
82
83 pub fn current_etpao(&self) -> f64 {
85 let tokens = self
86 .requests
87 .iter()
88 .map(total_request_tokens)
89 .sum::<usize>();
90 tokens as f64 / self.accepted_outcomes().max(1) as f64
91 }
92
93 pub fn etpao_for_client(&self, client_id: &str) -> Option<f64> {
95 let mut requests = self
96 .requests
97 .iter()
98 .filter(|req| req.client_id == client_id);
99 let first = requests.next()?;
100 let tokens = requests.fold(total_request_tokens(first), |total, req| {
101 total.saturating_add(total_request_tokens(req))
102 });
103 let accepted = self
104 .outcomes
105 .iter()
106 .filter(|outcome| outcome.client_id == client_id && outcome.accepted)
107 .count();
108 Some(tokens as f64 / accepted.max(1) as f64)
109 }
110
111 pub fn retry_tax(&self) -> f64 {
113 let total = self
114 .requests
115 .iter()
116 .map(total_request_tokens)
117 .sum::<usize>();
118 if total == 0 {
119 return 0.0;
120 }
121 let retried = self
122 .requests
123 .iter()
124 .filter(|req| req.retry_count > 0)
125 .map(total_request_tokens)
126 .sum::<usize>();
127 retried as f64 / total as f64
128 }
129
130 pub fn summary(&self) -> EtpaoSummary {
132 let total_tokens = self.requests.iter().map(total_request_tokens).sum();
133 let accepted_outcomes = self.accepted_outcomes();
134 let first_pass_rate = if self.outcomes.is_empty() {
135 0.0
136 } else {
137 self.outcomes
138 .iter()
139 .filter(|outcome| outcome.first_pass)
140 .count() as f64
141 / self.outcomes.len() as f64
142 };
143 EtpaoSummary {
144 etpao: self.current_etpao(),
145 total_tokens,
146 accepted_outcomes,
147 first_pass_rate,
148 retry_tax_pct: self.retry_tax() * 100.0,
149 by_coverage_class: self.etpao_by_coverage_class(),
150 }
151 }
152
153 pub fn request_count(&self) -> usize {
155 self.requests.len()
156 }
157
158 pub fn outcome_count(&self) -> usize {
160 self.outcomes.len()
161 }
162
163 fn accepted_outcomes(&self) -> usize {
164 self.outcomes
165 .iter()
166 .filter(|outcome| outcome.accepted)
167 .count()
168 }
169
170 fn etpao_by_coverage_class(&self) -> HashMap<String, f64> {
171 let mut totals = HashMap::<CoverageClass, usize>::new();
172 for request in &self.requests {
173 let total = totals.entry(request.coverage_class).or_default();
174 *total = total.saturating_add(total_request_tokens(request));
175 }
176 totals
177 .into_iter()
178 .map(|(class, tokens)| {
179 let accepted = self
180 .outcomes
181 .iter()
182 .filter(|outcome| {
183 outcome.accepted
184 && self.requests.iter().any(|request| {
185 request.coverage_class == class
186 && request.client_id == outcome.client_id
187 })
188 })
189 .count();
190 (format!("{class:?}"), tokens as f64 / accepted.max(1) as f64)
191 })
192 .collect()
193 }
194}
195
196pub fn total_request_tokens(req: &RequestMetrics) -> usize {
198 req.input_tokens
199 .saturating_add(req.output_tokens)
200 .saturating_add(req.reasoning_tokens)
201 .saturating_add(req.schema_tokens)
202 .saturating_add(req.cache_write_tokens)
203}
204
205#[cfg(test)]
206#[rustfmt::skip]
207mod tests {
208 use super::{EtpaoLive, OutcomeMetrics, RequestMetrics};
209 use crate::core::context_kernel::coverage_class::CoverageClass;
210
211 fn request(client: &str, tokens: usize, class: CoverageClass) -> RequestMetrics {
212 RequestMetrics { input_tokens: tokens, output_tokens: 0, reasoning_tokens: 0,
213 schema_tokens: 0, cache_write_tokens: 0, retry_count: 0,
214 client_id: client.to_owned(), coverage_class: class }
215 }
216
217 fn outcome(client: &str, accepted: bool, first_pass: bool) -> OutcomeMetrics {
218 OutcomeMetrics { accepted, quality_score: 1.0, first_pass,
219 client_id: client.to_owned() }
220 }
221
222 #[test]
223 fn empty_etpao_is_zero() { assert_eq!(EtpaoLive::new().current_etpao(), 0.0); }
224
225 #[test]
226 fn single_request_single_outcome() { let mut live = EtpaoLive::new(); live.record_request(request("a", 1_000, CoverageClass::FullInline)); live.record_outcome(outcome("a", true, true)); assert_eq!(live.current_etpao(), 1_000.0); }
227
228 #[test]
229 fn retry_tax_computed() { let mut live = EtpaoLive::new(); for client in ["a", "b", "c"] { live.record_request(request(client, 100, CoverageClass::ContextControlled)); } live.requests[1].retry_count = 1; assert!((live.retry_tax() - 1.0 / 3.0).abs() < f64::EPSILON); }
230
231 #[test]
232 fn etpao_for_client_filters() { let mut live = EtpaoLive::new(); live.record_request(request("a", 100, CoverageClass::FullInline)); live.record_request(request("b", 250, CoverageClass::FullInline)); live.record_outcome(outcome("a", true, true)); live.record_outcome(outcome("b", true, true)); assert_eq!(live.etpao_for_client("a"), Some(100.0)); assert_eq!(live.etpao_for_client("b"), Some(250.0)); assert_eq!(live.etpao_for_client("missing"), None); }
233
234 #[test]
235 fn summary_has_all_fields() { let mut live = EtpaoLive::new(); live.record_request(request("a", 100, CoverageClass::FullInline)); live.record_outcome(outcome("a", true, true)); let summary = live.summary(); assert_eq!((summary.etpao, summary.total_tokens), (100.0, 100)); assert_eq!((summary.accepted_outcomes, summary.first_pass_rate), (1, 1.0)); assert_eq!(summary.retry_tax_pct, 0.0); assert_eq!((live.request_count(), live.outcome_count()), (1, 1)); }
236
237 #[test]
238 fn by_coverage_class_separation() { let mut live = EtpaoLive::new(); live.record_request(request("inline", 100, CoverageClass::FullInline)); live.record_request(request("controlled", 300, CoverageClass::ContextControlled)); live.record_outcome(outcome("inline", true, true)); live.record_outcome(outcome("controlled", true, true)); let by_class = live.summary().by_coverage_class; assert_eq!(by_class.get("FullInline"), Some(&100.0)); assert_eq!(by_class.get("ContextControlled"), Some(&300.0)); }
239
240 #[test]
241 fn first_pass_rate_correct() { let mut live = EtpaoLive::new(); for index in 0..10 { live.record_outcome(outcome("a", true, index < 7)); } assert_eq!(live.summary().first_pass_rate, 0.7); }
242}