1use serde::{Deserialize, Serialize};
4
5use super::{baseline::BaselineMeasurement, recommendation::TreatmentMeasurement};
6
7pub const SAME_HOST_MODEL_SEMANTICS: &str = "same-host-model/v2";
9pub const LEGACY_SEMANTICS: &str = "legacy-reference-model";
13
14fn legacy_semantics() -> String {
15 LEGACY_SEMANTICS.into()
16}
17
18#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
19pub struct ShadowReport {
20 #[serde(default = "legacy_semantics")]
21 pub semantics: String,
22 pub report_id: String,
23 pub timestamp: String,
24 pub tasks_count: u32,
25 pub tasks_accepted: u32,
26 pub baseline: AggregateMetrics,
27 pub treatment: AggregateMetrics,
28 pub delta: DeltaMetrics,
30 pub savings: SavingsMetrics,
31 pub recommendations: Vec<Recommendation>,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
35pub struct AggregateMetrics {
36 pub total_cost_micros: u64,
37 pub total_tokens: u64,
38 pub avg_cpao_micros: Option<u64>,
39 pub avg_latency_ms: u64,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
43pub struct DeltaMetrics {
44 pub cost_micros: i64,
45 pub tokens: i64,
46 pub cpao_micros: Option<i64>,
47}
48
49#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
50pub struct SavingsMetrics {
51 pub absolute_micros: u64,
52 pub relative_percent: f64,
54 pub tokens_saved: u64,
55 pub quality_maintained: bool,
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59pub struct Recommendation {
60 pub category: String,
61 pub description: String,
62 pub estimated_savings_micros: u64,
63}
64
65pub fn compare(
66 baselines: &[BaselineMeasurement],
67 treatments: &[TreatmentMeasurement],
68) -> ShadowReport {
69 let baseline = aggregate(baselines.iter().map(Metric::baseline));
70 let treatment = aggregate(treatments.iter().map(Metric::treatment));
71 let cost_delta = signed_delta(treatment.total_cost_micros, baseline.total_cost_micros);
72 let token_delta = signed_delta(treatment.total_tokens, baseline.total_tokens);
73 let baseline_accepted = baselines
74 .iter()
75 .filter(|item| item.outcome_accepted)
76 .count();
77 let treatment_accepted = treatments
78 .iter()
79 .filter(|item| item.outcome_accepted)
80 .count();
81 let quality_maintained = acceptance_rate(treatment_accepted, treatments.len())
82 >= acceptance_rate(baseline_accepted, baselines.len());
83 let absolute_micros = baseline
84 .total_cost_micros
85 .saturating_sub(treatment.total_cost_micros);
86 let tokens_saved = baseline.total_tokens.saturating_sub(treatment.total_tokens);
87 let savings = SavingsMetrics {
88 absolute_micros,
89 relative_percent: percentage_savings(
90 baseline.total_cost_micros,
91 treatment.total_cost_micros,
92 ),
93 tokens_saved,
94 quality_maintained,
95 };
96 let delta = DeltaMetrics {
97 cost_micros: cost_delta,
98 tokens: token_delta,
99 cpao_micros: match (treatment.avg_cpao_micros, baseline.avg_cpao_micros) {
100 (Some(treatment), Some(baseline)) => Some(signed_delta(treatment, baseline)),
101 _ => None,
102 },
103 };
104 let recommendations = recommendations(baselines, treatments, &savings);
105
106 ShadowReport {
107 semantics: SAME_HOST_MODEL_SEMANTICS.into(),
108 report_id: format!(
109 "shadow-{}-{}-{}",
110 baseline.total_cost_micros, treatment.total_cost_micros, baseline.total_tokens
111 ),
112 timestamp: "deterministic".into(),
113 tasks_count: u32::try_from(baselines.len()).unwrap_or(u32::MAX),
114 tasks_accepted: u32::try_from(treatment_accepted).unwrap_or(u32::MAX),
115 baseline,
116 treatment,
117 delta,
118 savings,
119 recommendations,
120 }
121}
122
123pub const BASELINE_NOTE: &str = "Note: the baseline is simulated (uncompressed token counts, same outcome signals), not a second measured run. Cost and token deltas are estimates; the acceptance line is not a measured quality comparison.";
126
127impl ShadowReport {
128 pub fn is_legacy(&self) -> bool {
130 self.semantics != SAME_HOST_MODEL_SEMANTICS
131 }
132}
133
134pub const LEGACY_NOTE: &str = "Legacy record: written before reports compared each task with its own host model. Its baseline may be priced with a different reference model, so its cost difference is not a context saving and its recommendations are not current guidance. Kept for history only.";
135
136pub fn format_report(report: &ShadowReport) -> String {
137 if report.is_legacy() {
138 return format!(
141 "# Shadow Comparison Report (legacy record)\n\n{LEGACY_NOTE}\n\nRecorded tasks: {}. Tokens: baseline {}, treatment {}.\n\n{BASELINE_NOTE}\n\n## Recommendations\n- Withheld: recommendations of legacy records are not current guidance.",
142 report.tasks_count, report.baseline.total_tokens, report.treatment.total_tokens,
143 );
144 }
145 let recommendations = if report.recommendations.is_empty() {
146 "- No recommendation: treatment did not reduce cost without lowering outcome acceptance."
147 .into()
148 } else {
149 report
150 .recommendations
151 .iter()
152 .map(|item| {
153 format!(
154 "- {}: {} (estimated savings: {} micros)",
155 item.category, item.description, item.estimated_savings_micros
156 )
157 })
158 .collect::<Vec<_>>()
159 .join("\n")
160 };
161
162 format!(
163 "# Shadow Comparison Report\n\n| Metric | Baseline | Treatment | Delta |\n|---|---:|---:|---:|\n| Cost (micros) | {} | {} | {} |\n| Tokens | {} | {} | {} |\n| Avg CPAO (micros) | {} | {} | {} |\n| Avg latency (ms) | {} | {} | {} |\n\nCost savings: {} micros ({:.2}%). Token savings: {}. Outcome acceptance not below baseline: {}.\n\n{BASELINE_NOTE}\n\n## Recommendations\n{}",
164 report.baseline.total_cost_micros,
165 report.treatment.total_cost_micros,
166 format_delta(report.delta.cost_micros, "saved", "more"),
167 report.baseline.total_tokens,
168 report.treatment.total_tokens,
169 format_delta(report.delta.tokens, "saved", "more"),
170 display_cpao(report.baseline.avg_cpao_micros),
171 display_cpao(report.treatment.avg_cpao_micros),
172 display_cpao_delta(report.delta.cpao_micros),
173 report.baseline.avg_latency_ms,
174 report.treatment.avg_latency_ms,
175 format_delta(
176 signed_delta(
177 report.treatment.avg_latency_ms,
178 report.baseline.avg_latency_ms
179 ),
180 "faster",
181 "slower",
182 ),
183 report.savings.absolute_micros,
184 report.savings.relative_percent,
185 report.savings.tokens_saved,
186 if report.savings.quality_maintained {
187 "yes"
188 } else {
189 "no"
190 },
191 recommendations,
192 )
193}
194
195fn aggregate(items: impl Iterator<Item = Metric>) -> AggregateMetrics {
196 let values: Vec<_> = items.collect();
197 let accepted: Vec<_> = values.iter().filter(|item| item.accepted).collect();
198 AggregateMetrics {
199 total_cost_micros: values.iter().map(|item| item.cost).sum(),
200 total_tokens: values.iter().map(|item| item.tokens).sum(),
201 avg_cpao_micros: (!accepted.is_empty())
202 .then(|| accepted.iter().map(|item| item.cost).sum::<u64>() / accepted.len() as u64),
203 avg_latency_ms: if values.is_empty() {
204 0
205 } else {
206 values.iter().map(|item| item.duration).sum::<u64>() / values.len() as u64
207 },
208 }
209}
210
211fn recommendations(
215 baselines: &[BaselineMeasurement],
216 treatments: &[TreatmentMeasurement],
217 savings: &SavingsMetrics,
218) -> Vec<Recommendation> {
219 let same_models = baselines.len() == treatments.len()
220 && baselines
221 .iter()
222 .zip(treatments)
223 .all(|(baseline, treatment)| baseline.model == treatment.model);
224 if !same_models
225 || savings.absolute_micros == 0
226 || savings.tokens_saved == 0
227 || !savings.quality_maintained
228 {
229 return Vec::new();
230 }
231 vec![Recommendation {
232 category: "Context compression".into(),
233 description: format!(
234 "Keep context compression: it removed an estimated {} tokens against the simulated baseline.",
235 savings.tokens_saved
236 ),
237 estimated_savings_micros: savings.absolute_micros,
238 }]
239}
240
241#[derive(Clone, Copy)]
242struct Metric {
243 tokens: u64,
244 cost: u64,
245 duration: u64,
246 accepted: bool,
247}
248
249impl Metric {
250 fn baseline(item: &BaselineMeasurement) -> Self {
251 Self::new(
252 item.input_tokens,
253 item.output_tokens,
254 item.total_cost_micros,
255 item.duration_ms,
256 item.outcome_accepted,
257 )
258 }
259
260 fn treatment(item: &TreatmentMeasurement) -> Self {
261 Self::new(
262 item.input_tokens,
263 item.output_tokens,
264 item.total_cost_micros,
265 item.duration_ms,
266 item.outcome_accepted,
267 )
268 }
269
270 fn new(input: u64, output: u64, cost: u64, duration: u64, accepted: bool) -> Self {
271 Self {
272 tokens: input.saturating_add(output),
273 cost,
274 duration,
275 accepted,
276 }
277 }
278}
279
280fn acceptance_rate(accepted: usize, total: usize) -> f64 {
281 if total == 0 {
282 1.0
283 } else {
284 accepted as f64 / total as f64
285 }
286}
287
288fn percentage_savings(baseline: u64, treatment: u64) -> f64 {
289 if baseline == 0 {
290 0.0
291 } else {
292 (baseline as f64 - treatment as f64) * 100.0 / baseline as f64
293 }
294}
295
296fn signed_delta(treatment: u64, baseline: u64) -> i64 {
297 let difference = i128::from(treatment) - i128::from(baseline);
298 i64::try_from(difference).unwrap_or_else(|_| {
299 if difference.is_negative() {
300 i64::MIN
301 } else {
302 i64::MAX
303 }
304 })
305}
306
307fn format_delta(delta: i64, lower_label: &str, higher_label: &str) -> String {
308 match delta.cmp(&0) {
309 std::cmp::Ordering::Less => format!("{} {lower_label}", delta.unsigned_abs()),
310 std::cmp::Ordering::Greater => format!("{} {higher_label}", delta.unsigned_abs()),
311 std::cmp::Ordering::Equal => "unchanged".into(),
312 }
313}
314
315fn display_cpao(value: Option<u64>) -> String {
316 value.map_or_else(|| "n/a".into(), |item| item.to_string())
317}
318
319fn display_cpao_delta(value: Option<i64>) -> String {
320 value.map_or_else(
321 || "n/a".into(),
322 |delta| format_delta(delta, "lower", "higher"),
323 )
324}