1use crate::outline::estimate_tokens;
4use serde::{Deserialize, Serialize};
5use std::{borrow::Cow, collections::BTreeMap};
6use thiserror::Error;
7
8pub const TOKEN_ESTIMATE_KIND: &str = "heuristic";
10pub const TOKEN_ESTIMATOR: &str = "chars_or_bytes_div_ceil_4";
12pub const TOKEN_ESTIMATE_SCOPE: &str = "workflow_payload_estimate_not_model_billing_tokens";
14pub const TOKEN_PROVIDER_HEURISTIC: &str = "heuristic";
16pub const TOKEN_MODEL_UNKNOWN: &str = "unknown";
18pub const TOKENIZER_BACKEND_HEURISTIC: &str = "chars_div_4";
20pub const TOKEN_ACCURACY_HEURISTIC: &str = "heuristic_estimate";
22pub const TOKEN_BUCKET_FULL_FILE_COMPRESSION: &str = "full_file_compression";
24pub const TOKEN_BUCKET_NAVIGATION_AVOIDANCE: &str = "navigation_avoidance";
26pub const TOKEN_BASELINE_FULL_FILE: &str = "full_file";
28pub const TOKEN_BASELINE_SELECTED_CANDIDATES: &str = "selected_candidates";
30pub const TOKEN_BASELINE_DIRECTORY_WALK: &str = "directory_walk";
32pub const TOKEN_AVERAGE_DIRECTORY_WALK_PERCENT: usize = 50;
34pub const TOKEN_AVERAGE_POLICY_EVIDENCE: &str =
36 "fixed_policy_estimate_not_benchmark_or_provider_measurement";
37pub const TOKEN_AVERAGE_POLICY_OVERFLOW_EVIDENCE: &str =
39 "fixed_policy_estimate_unclassified_overflow_uses_maximum";
40pub const TOKEN_CONFIDENCE_OBSERVED: &str = "observed";
42pub const TOKEN_CONFIDENCE_INFERRED: &str = "inferred";
44pub const TOKEN_CONFIDENCE_POLICY_ESTIMATE: &str = "policy_estimate";
46pub const TOKEN_TRACE_HEURISTIC: &str = "heuristic=ceil(chars_or_bytes/4)";
48pub const TOKEN_ACCOUNTING_OBSERVED_DELTA: &str = "observed_delta";
50pub const TOKEN_ACCOUNTING_MODELED_AVOIDANCE: &str = "modeled_avoidance";
52pub const TOKEN_ESTIMATE_METHOD_HEURISTIC: &str = "heuristic_chars_or_bytes_div_ceil_4";
54pub const TOKEN_DEDUPE_SCOPE_EVENT: &str = "event";
56pub const TOKEN_DEDUPE_SCOPE_SESSION: &str = "session";
58pub const READ_AVOIDANCE_CONFIDENCE_OBSERVED: &str = "observed";
60pub const READ_AVOIDANCE_CONFIDENCE_MODELED: &str = "modeled";
62pub const READ_AVOIDANCE_CONFIDENCE_NOT_RECORDED: &str = "not_recorded";
64pub const READ_AVOIDANCE_SCOPE: &str =
66 "summary_search_slice_calls_that_likely_replaced_whole_file_reads";
67pub const TOKEN_COMMAND_SUMMARY: &str = "summary";
69pub const TOKEN_COMMAND_OUTLINE: &str = "outline";
71pub const TOKEN_COMMAND_SLICE: &str = "slice";
73pub const TOKEN_COMMAND_SYMBOL_SLICE: &str = "symbol-slice";
75pub const TOKEN_COMMAND_SEARCH: &str = "search";
77pub const TOKEN_COMMAND_MCP_FILE_SUMMARY: &str = "mcp.atlas_file_summary";
79pub const TOKEN_COMMAND_MCP_OUTLINE: &str = "mcp.atlas_outline";
81pub const TOKEN_COMMAND_MCP_SLICE: &str = "mcp.atlas_slice";
83pub const TOKEN_COMMAND_MCP_SEARCH: &str = "mcp.atlas_search";
85
86#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
88pub enum TelemetryContractError {
89 #[error("the zero usage instance identifier is reserved")]
91 ZeroUsageInstanceId,
92}
93
94#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
96pub struct UsageInstanceId([u8; 16]);
97
98impl UsageInstanceId {
99 pub fn from_bytes(bytes: [u8; 16]) -> Result<Self, TelemetryContractError> {
105 if bytes == [0; 16] {
106 return Err(TelemetryContractError::ZeroUsageInstanceId);
107 }
108 Ok(Self(bytes))
109 }
110
111 #[must_use]
113 pub const fn as_bytes(self) -> [u8; 16] {
114 self.0
115 }
116}
117
118#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
120#[serde(rename_all = "snake_case")]
121pub enum UsageInstanceOwner {
122 CliInvocation,
124 McpProcess,
126 LibraryHandle,
128 MigratedLegacy,
130}
131
132impl UsageInstanceOwner {
133 #[must_use]
135 pub const fn as_str(self) -> &'static str {
136 match self {
137 Self::CliInvocation => "cli_invocation",
138 Self::McpProcess => "mcp_process",
139 Self::LibraryHandle => "library_handle",
140 Self::MigratedLegacy => "migrated_legacy",
141 }
142 }
143
144 #[must_use]
146 pub fn parse(value: &str) -> Option<Self> {
147 match value {
148 "cli_invocation" => Some(Self::CliInvocation),
149 "mcp_process" => Some(Self::McpProcess),
150 "library_handle" => Some(Self::LibraryHandle),
151 "migrated_legacy" => Some(Self::MigratedLegacy),
152 _ => None,
153 }
154 }
155}
156
157#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
159#[serde(rename_all = "snake_case")]
160pub enum UsageDetailAvailability {
161 Retained,
163 Partial,
165 Expired,
167 #[default]
169 Unavailable,
170}
171
172impl UsageDetailAvailability {
173 #[must_use]
175 pub const fn as_str(self) -> &'static str {
176 match self {
177 Self::Retained => "retained",
178 Self::Partial => "partial",
179 Self::Expired => "expired",
180 Self::Unavailable => "unavailable",
181 }
182 }
183
184 #[must_use]
186 pub fn parse(value: &str) -> Option<Self> {
187 match value {
188 "retained" => Some(Self::Retained),
189 "partial" => Some(Self::Partial),
190 "expired" => Some(Self::Expired),
191 "unavailable" => Some(Self::Unavailable),
192 _ => None,
193 }
194 }
195}
196
197#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
199pub struct TokenAccountingTotals {
200 pub measured_tokens_saved: i128,
202 pub gross_modeled_tokens_avoided: i128,
204 pub deduped_modeled_tokens_avoided: i128,
206 pub average_modeled_tokens_avoided: i128,
208 pub repeated_baselines_deduped: u128,
210 pub observed_file_read_replacements: u128,
212 pub modeled_file_reads_avoided: u128,
214}
215
216#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
218pub struct UsageEvent {
219 pub session_id: String,
221 pub command: String,
223 pub path: Option<String>,
225 pub query: Option<String>,
227 pub estimated_tokens_without_projectatlas: Option<usize>,
229 pub estimated_tokens_with_projectatlas: Option<usize>,
231 pub estimated_tokens_saved: Option<isize>,
233 #[serde(default = "default_token_savings_bucket")]
235 pub token_savings_bucket: String,
236 #[serde(default = "default_token_provider")]
238 pub provider: String,
239 #[serde(default = "default_token_model")]
241 pub model: String,
242 #[serde(default = "default_tokenizer_backend")]
244 pub tokenizer_backend: String,
245 #[serde(default = "default_token_accuracy")]
247 pub accuracy: String,
248 #[serde(default = "default_token_baseline_kind")]
250 pub baseline_kind: String,
251 #[serde(default = "default_token_confidence")]
253 pub confidence: String,
254 #[serde(default = "default_token_trace")]
256 pub calculation_trace: String,
257 #[serde(default = "default_accounting_layer")]
259 pub accounting_layer: String,
260 #[serde(default = "default_estimate_method")]
262 pub estimate_method: String,
263 #[serde(default = "default_denominator_kind")]
265 pub denominator_kind: String,
266 #[serde(default)]
268 pub baseline_identity: String,
269 #[serde(default)]
271 pub baseline_fingerprint: String,
272 #[serde(default = "default_dedupe_scope")]
274 pub dedupe_scope: String,
275}
276
277#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
279pub struct TokenBucketOverview {
280 pub token_savings_bucket: String,
282 pub provider: String,
284 pub model: String,
286 pub tokenizer_backend: String,
288 pub accuracy: String,
290 pub baseline_kind: String,
292 pub confidence: String,
294 pub calls: usize,
296 pub estimated_without_projectatlas: usize,
298 pub estimated_with_projectatlas: usize,
300 pub estimated_saved: isize,
302 pub savings_rate: Option<f64>,
304 pub accounting_layer: String,
306 pub estimate_method: String,
308 pub denominator_kind: String,
310 pub dedupe_scope: String,
312}
313
314#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
316pub struct TokenCalibrationOverview {
317 pub tokenizer: String,
319 pub provider: String,
321 pub model: String,
323 pub tokenizer_backend: String,
325 pub accuracy: String,
327 pub files: usize,
329 pub bytes: usize,
331 pub heuristic_tokens: usize,
333 pub calibrated_tokens: usize,
335 pub heuristic_to_calibrated_ratio: Option<f64>,
337}
338
339#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
341#[serde(rename_all = "snake_case")]
342pub enum AgentEfficiencyEvidenceState {
343 #[default]
345 Unavailable,
346 Failed,
348 Incompatible,
350 Partial,
352 Compatible,
354}
355
356impl AgentEfficiencyEvidenceState {
357 #[must_use]
359 pub const fn as_str(self) -> &'static str {
360 match self {
361 Self::Unavailable => "unavailable",
362 Self::Failed => "failed",
363 Self::Incompatible => "incompatible",
364 Self::Partial => "partial",
365 Self::Compatible => "compatible",
366 }
367 }
368}
369
370#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
372#[serde(rename_all = "snake_case")]
373pub enum AgentEfficiencyBaseline {
374 FrozenProjectAtlasV0326,
376 PlainCodex,
378}
379
380#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
382pub struct AgentEfficiencyArtifactIdentity {
383 pub schema_version: u32,
385 pub artifact_digest_kind: String,
387 pub artifact_digest: String,
389 pub candidate_version: String,
391 pub candidate_runtime_sha256: String,
393 #[serde(default)]
395 pub candidate_source_head: String,
396 #[serde(default)]
398 pub candidate_functional_head: String,
399 #[serde(default)]
401 pub candidate_checklist_head: String,
402 pub frozen_version: String,
404 pub frozen_runtime_sha256: String,
406}
407
408#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
410#[serde(rename_all = "snake_case")]
411pub enum AgentEfficiencyMetricKind {
412 TotalToolCalls,
414 ProjectAtlasCalls,
416 ProductiveFolders,
418 ProductiveFiles,
420 ProductiveRelations,
422 WrongFolders,
424 WrongFiles,
426 WrongRelations,
428 BroadReads,
430 FullReads,
432 Backtracks,
434 GrossNavigationBytes,
436 NetNavigationBytes,
438 GrossNavigationTokens,
440 NetNavigationTokens,
442 SetupWallSeconds,
444 RuntimeWallSeconds,
446 PersistentBytes,
448}
449
450#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
452pub struct AgentEfficiencyMetricComparison {
453 pub metric: AgentEfficiencyMetricKind,
455 pub candidate_median: f64,
457 pub baseline_median: f64,
459 pub candidate_maximum: f64,
461 pub baseline_maximum: f64,
463 pub median_percent_saving: Option<f64>,
465}
466
467#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
469pub struct AgentEfficiencyBreakEven {
470 pub workload: String,
472 pub wall_time_tasks: Option<u64>,
474}
475
476#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
478#[serde(rename_all = "snake_case")]
479pub enum AgentEfficiencyProviderMetricKind {
480 InputTokens,
482 CachedInputTokens,
484 CacheWriteInputTokens,
486 OutputTokens,
488 ReasoningOutputTokens,
490}
491
492#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
494pub struct AgentEfficiencyProviderMetric {
495 pub metric: AgentEfficiencyProviderMetricKind,
497 pub candidate_median: f64,
499 pub baseline_median: f64,
501 pub candidate_maximum: f64,
503 pub baseline_maximum: f64,
505 pub causal_attribution: bool,
507}
508
509#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
511pub struct AgentEfficiencyBaselineRow {
512 pub baseline: AgentEfficiencyBaseline,
514 pub state: AgentEfficiencyEvidenceState,
516 pub matched_trials: usize,
518 pub candidate_failed_trials: usize,
520 pub baseline_failed_trials: usize,
522 pub unmatched_trials: usize,
524 pub metrics: Vec<AgentEfficiencyMetricComparison>,
526 pub break_even: Vec<AgentEfficiencyBreakEven>,
528 pub provider_usage_descriptive_only: Vec<AgentEfficiencyProviderMetric>,
530}
531
532#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
534#[serde(rename_all = "snake_case")]
535pub enum AgentEfficiencyCapability {
536 Discovery,
538 SummaryAndSlice,
540 Search,
542 SymbolsAndRelations,
544 Other,
546}
547
548#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
550pub struct AgentEfficiencyCapabilityContribution {
551 pub capability: AgentEfficiencyCapability,
553 pub calls: usize,
555 pub emitted_bytes: u64,
557}
558
559#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
561pub struct AgentEfficiencyComparison {
562 pub state: AgentEfficiencyEvidenceState,
564 pub reason: Option<String>,
566 pub artifact: Option<AgentEfficiencyArtifactIdentity>,
568 pub baselines: Vec<AgentEfficiencyBaselineRow>,
570 pub capabilities: Vec<AgentEfficiencyCapabilityContribution>,
572 pub provider_counters_descriptive_only: bool,
574}
575
576impl Default for AgentEfficiencyComparison {
577 fn default() -> Self {
578 Self {
579 state: AgentEfficiencyEvidenceState::Unavailable,
580 reason: Some("benchmark artifact not supplied".to_string()),
581 artifact: None,
582 baselines: Vec::new(),
583 capabilities: Vec::new(),
584 provider_counters_descriptive_only: true,
585 }
586 }
587}
588
589#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
591pub struct TokenAveragePolicy {
592 pub directory_walk_baseline_percent: usize,
594 pub atlas_payload_percent: usize,
596 pub evidence: String,
598}
599
600impl Default for TokenAveragePolicy {
601 fn default() -> Self {
602 Self {
603 directory_walk_baseline_percent: TOKEN_AVERAGE_DIRECTORY_WALK_PERCENT,
604 atlas_payload_percent: 100,
605 evidence: TOKEN_AVERAGE_POLICY_EVIDENCE.to_string(),
606 }
607 }
608}
609
610#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
612pub struct TokenOverview {
613 pub estimate_kind: String,
615 pub estimator: String,
617 pub estimate_scope: String,
619 pub calls: usize,
621 pub estimated_without_projectatlas: usize,
623 pub estimated_with_projectatlas: usize,
625 pub estimated_saved: isize,
627 pub savings_rate: Option<f64>,
629 pub buckets: Vec<TokenBucketOverview>,
631 pub measured_tokens_saved: isize,
633 pub gross_modeled_tokens_avoided: isize,
635 pub deduped_modeled_tokens_avoided: isize,
637 #[serde(default)]
639 pub average_modeled_tokens_avoided: isize,
640 #[serde(default)]
642 pub average_tokens_avoided: isize,
643 #[serde(default)]
645 pub maximum_tokens_avoided: isize,
646 #[serde(default)]
648 pub average_policy: TokenAveragePolicy,
649 pub tokens_avoided: isize,
651 pub legacy_gross_estimated_saved: isize,
653 pub repeated_baselines_deduped: usize,
655 #[serde(default)]
657 pub observed_file_read_replacements: usize,
658 #[serde(default)]
660 pub modeled_file_reads_avoided: usize,
661 #[serde(default)]
663 pub likely_file_reads_avoided: usize,
664 #[serde(default = "default_read_avoidance_scope")]
666 pub read_avoidance_scope: String,
667 #[serde(default = "default_read_avoidance_confidence")]
669 pub read_avoidance_confidence: String,
670 pub calibration: Option<TokenCalibrationOverview>,
672 #[serde(default)]
674 pub detail_availability: UsageDetailAvailability,
675 #[serde(default)]
677 pub agent_efficiency: AgentEfficiencyComparison,
678}
679
680#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
682#[serde(rename_all = "lowercase")]
683pub enum TokenTrendWindow {
684 Day,
686 Week,
688 Month,
690 Year,
692}
693
694impl TokenTrendWindow {
695 #[must_use]
697 pub fn parse(value: &str) -> Option<Self> {
698 match value {
699 "day" => Some(Self::Day),
700 "week" => Some(Self::Week),
701 "month" => Some(Self::Month),
702 "year" => Some(Self::Year),
703 _ => None,
704 }
705 }
706
707 #[must_use]
709 pub const fn as_str(self) -> &'static str {
710 match self {
711 Self::Day => "day",
712 Self::Week => "week",
713 Self::Month => "month",
714 Self::Year => "year",
715 }
716 }
717}
718
719impl std::fmt::Display for TokenTrendWindow {
720 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
721 formatter.write_str(self.as_str())
722 }
723}
724
725#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
727pub struct TokenTrendPeriod {
728 pub period: String,
730 pub calls: usize,
732 pub estimated_without_projectatlas: usize,
734 pub estimated_with_projectatlas: usize,
736 pub estimated_saved: isize,
738 pub savings_rate: Option<f64>,
740 pub buckets: Vec<TokenBucketOverview>,
742}
743
744#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
746pub struct TokenTrendReport {
747 pub estimate_kind: String,
749 pub estimator: String,
751 pub estimate_scope: String,
753 pub session: Option<String>,
755 pub window: TokenTrendWindow,
757 pub periods: Vec<TokenTrendPeriod>,
759 #[serde(default)]
761 pub detail_availability: UsageDetailAvailability,
762}
763
764impl UsageEvent {
765 #[must_use]
767 pub fn is_observed(&self) -> bool {
768 is_observed_event(self)
769 }
770
771 #[must_use]
773 pub fn is_modeled(&self) -> bool {
774 is_modeled_event(self)
775 }
776
777 #[must_use]
779 pub fn is_observed_file_read_replacement(&self, baseline_tokens: usize) -> bool {
780 is_observed_read_replacement_event(self, baseline_tokens)
781 }
782
783 #[must_use]
785 pub fn is_modeled_file_read_avoidance(&self, baseline_tokens: usize) -> bool {
786 is_modeled_read_avoidance_event(self, baseline_tokens)
787 }
788
789 #[must_use]
791 pub fn report_accounting_layer(&self) -> &str {
792 if self.is_observed() {
793 TOKEN_ACCOUNTING_OBSERVED_DELTA
794 } else {
795 &self.accounting_layer
796 }
797 }
798
799 #[must_use]
801 pub fn report_denominator_kind(&self) -> &str {
802 if self.is_observed() {
803 TOKEN_BASELINE_FULL_FILE
804 } else {
805 &self.denominator_kind
806 }
807 }
808
809 #[must_use]
811 pub fn report_dedupe_scope(&self) -> &str {
812 if self.is_observed() {
813 TOKEN_DEDUPE_SCOPE_EVENT
814 } else {
815 &self.dedupe_scope
816 }
817 }
818
819 #[must_use]
821 pub fn effective_baseline_identity(&self) -> Cow<'_, str> {
822 if self.baseline_identity.is_empty() {
823 Cow::Owned(default_baseline_identity(
824 &self.command,
825 self.path.as_deref(),
826 self.query.as_deref(),
827 &self.baseline_kind,
828 ))
829 } else {
830 Cow::Borrowed(&self.baseline_identity)
831 }
832 }
833
834 #[must_use]
836 pub fn effective_baseline_fingerprint(&self) -> Cow<'_, str> {
837 if self.baseline_fingerprint.is_empty() {
838 self.effective_baseline_identity()
839 } else {
840 Cow::Borrowed(&self.baseline_fingerprint)
841 }
842 }
843
844 #[must_use]
846 pub fn modeled_baseline_key(&self) -> [u8; 32] {
847 let identity = self.effective_baseline_identity();
848 let fingerprint = if self.baseline_fingerprint.is_empty() {
849 identity.as_ref()
850 } else {
851 self.baseline_fingerprint.as_str()
852 };
853 let mut hasher = blake3::Hasher::new();
854 for value in [
855 identity.as_ref(),
856 fingerprint,
857 self.denominator_kind.as_str(),
858 ] {
859 let bytes = value.as_bytes();
860 hasher.update(&(bytes.len() as u64).to_le_bytes());
861 hasher.update(bytes);
862 }
863 *hasher.finalize().as_bytes()
864 }
865}
866
867impl TokenOverview {
868 #[must_use]
870 pub fn from_events(events: &[UsageEvent]) -> Self {
871 let mut totals = BTreeMap::<TokenBucketKey, (u128, u128, u128)>::new();
872 for event in events {
873 let (Some(event_without), Some(event_with)) = (
874 event.estimated_tokens_without_projectatlas,
875 event.estimated_tokens_with_projectatlas,
876 ) else {
877 continue;
878 };
879 let entry = totals.entry(TokenBucketKey::from(event)).or_default();
880 entry.0 = entry.0.saturating_add(1);
881 entry.1 = entry.1.saturating_add(event_without as u128);
882 entry.2 = entry.2.saturating_add(event_with as u128);
883 }
884 let buckets = totals
885 .into_iter()
886 .map(|(key, (calls, without, with))| key.into_overview(calls, without, with))
887 .collect();
888 let mut overview = Self::from_buckets(buckets);
889 overview.apply_accounting_from_events(events);
890 overview
891 }
892
893 #[must_use]
895 pub fn from_estimated_totals(calls: u128, without: u128, with: u128) -> Self {
896 Self::from_buckets(vec![TokenBucketOverview::from_totals(
897 default_token_savings_bucket(),
898 default_token_provider(),
899 default_token_model(),
900 default_tokenizer_backend(),
901 default_token_accuracy(),
902 default_token_baseline_kind(),
903 default_token_confidence(),
904 default_accounting_layer(),
905 default_estimate_method(),
906 default_denominator_kind(),
907 default_dedupe_scope(),
908 calls,
909 without,
910 with,
911 )])
912 }
913
914 #[must_use]
916 pub fn from_buckets(buckets: Vec<TokenBucketOverview>) -> Self {
917 let calls = buckets.iter().fold(0u128, |acc, bucket| {
918 acc.saturating_add(bucket.calls as u128)
919 });
920 let without = buckets.iter().fold(0u128, |acc, bucket| {
921 acc.saturating_add(bucket.estimated_without_projectatlas as u128)
922 });
923 let with = buckets.iter().fold(0u128, |acc, bucket| {
924 acc.saturating_add(bucket.estimated_with_projectatlas as u128)
925 });
926 let saved = aggregate_token_delta(without, with);
927 let savings_rate = if without == 0 {
928 None
929 } else {
930 Some((without as f64 - with as f64) / without as f64)
931 };
932 let measured_tokens_saved_wide = measured_tokens_saved_from_buckets(&buckets);
933 let gross_modeled_tokens_avoided_wide = modeled_tokens_saved_from_buckets(&buckets);
934 let average_modeled_tokens_avoided_wide =
935 average_modeled_tokens_saved_from_buckets(&buckets);
936 let measured_tokens_saved = saturating_i128_to_isize(measured_tokens_saved_wide);
937 let gross_modeled_tokens_avoided =
938 saturating_i128_to_isize(gross_modeled_tokens_avoided_wide);
939 let average_modeled_tokens_avoided =
940 saturating_i128_to_isize(average_modeled_tokens_avoided_wide);
941 let average_tokens_avoided = saturating_i128_to_isize(
942 measured_tokens_saved_wide.saturating_add(average_modeled_tokens_avoided_wide),
943 );
944 let maximum_tokens_avoided = saturating_i128_to_isize(
945 measured_tokens_saved_wide.saturating_add(gross_modeled_tokens_avoided_wide),
946 );
947 Self {
948 estimate_kind: TOKEN_ESTIMATE_KIND.to_string(),
949 estimator: TOKEN_ESTIMATOR.to_string(),
950 estimate_scope: TOKEN_ESTIMATE_SCOPE.to_string(),
951 calls: saturating_u128_to_usize(calls),
952 estimated_without_projectatlas: saturating_u128_to_usize(without),
953 estimated_with_projectatlas: saturating_u128_to_usize(with),
954 estimated_saved: saved,
955 savings_rate,
956 measured_tokens_saved,
957 gross_modeled_tokens_avoided,
958 deduped_modeled_tokens_avoided: gross_modeled_tokens_avoided,
959 average_modeled_tokens_avoided,
960 average_tokens_avoided,
961 maximum_tokens_avoided,
962 average_policy: TokenAveragePolicy::default(),
963 tokens_avoided: average_tokens_avoided,
964 legacy_gross_estimated_saved: saved,
965 repeated_baselines_deduped: 0,
966 observed_file_read_replacements: 0,
967 modeled_file_reads_avoided: 0,
968 likely_file_reads_avoided: 0,
969 read_avoidance_scope: READ_AVOIDANCE_SCOPE.to_string(),
970 read_avoidance_confidence: READ_AVOIDANCE_CONFIDENCE_NOT_RECORDED.to_string(),
971 calibration: None,
972 detail_availability: UsageDetailAvailability::Retained,
973 agent_efficiency: AgentEfficiencyComparison::default(),
974 buckets,
975 }
976 }
977
978 pub fn set_calibration(&mut self, calibration: TokenCalibrationOverview) {
980 self.calibration = Some(calibration);
981 }
982
983 pub fn set_agent_efficiency(&mut self, comparison: AgentEfficiencyComparison) {
985 self.agent_efficiency = comparison;
986 }
987
988 pub fn apply_accounting_totals(&mut self, totals: TokenAccountingTotals) {
990 self.measured_tokens_saved = saturating_i128_to_isize(totals.measured_tokens_saved);
991 self.gross_modeled_tokens_avoided =
992 saturating_i128_to_isize(totals.gross_modeled_tokens_avoided);
993 self.deduped_modeled_tokens_avoided =
994 saturating_i128_to_isize(totals.deduped_modeled_tokens_avoided);
995 self.average_modeled_tokens_avoided =
996 saturating_i128_to_isize(totals.average_modeled_tokens_avoided);
997 self.average_tokens_avoided = saturating_i128_to_isize(
998 totals
999 .measured_tokens_saved
1000 .saturating_add(totals.average_modeled_tokens_avoided),
1001 );
1002 self.maximum_tokens_avoided = saturating_i128_to_isize(
1003 totals
1004 .measured_tokens_saved
1005 .saturating_add(totals.deduped_modeled_tokens_avoided),
1006 );
1007 self.tokens_avoided = self.average_tokens_avoided;
1008 self.repeated_baselines_deduped =
1009 saturating_u128_to_usize(totals.repeated_baselines_deduped);
1010 self.observed_file_read_replacements =
1011 saturating_u128_to_usize(totals.observed_file_read_replacements);
1012 self.modeled_file_reads_avoided =
1013 saturating_u128_to_usize(totals.modeled_file_reads_avoided);
1014 self.likely_file_reads_avoided = self
1015 .observed_file_read_replacements
1016 .saturating_add(self.modeled_file_reads_avoided);
1017 self.read_avoidance_confidence = read_avoidance_confidence_for(
1018 self.observed_file_read_replacements,
1019 self.modeled_file_reads_avoided,
1020 )
1021 .to_string();
1022 }
1023
1024 pub const fn set_detail_availability(&mut self, availability: UsageDetailAvailability) {
1026 self.detail_availability = availability;
1027 }
1028
1029 pub fn apply_accounting_from_events(&mut self, events: &[UsageEvent]) {
1031 let summary = TokenAccountingSummary::from_events(events);
1032 self.measured_tokens_saved = summary.measured_tokens_saved;
1033 self.gross_modeled_tokens_avoided = summary.gross_modeled_tokens_avoided;
1034 self.deduped_modeled_tokens_avoided = summary.deduped_modeled_tokens_avoided;
1035 self.average_modeled_tokens_avoided = summary.average_modeled_tokens_avoided;
1036 self.average_tokens_avoided = summary.average_tokens_avoided;
1037 self.maximum_tokens_avoided = summary.maximum_tokens_avoided;
1038 self.tokens_avoided = summary.average_tokens_avoided;
1039 self.repeated_baselines_deduped = summary.repeated_baselines_deduped;
1040 self.observed_file_read_replacements = summary.observed_file_read_replacements;
1041 self.modeled_file_reads_avoided = summary.modeled_file_reads_avoided;
1042 self.likely_file_reads_avoided = summary.likely_file_reads_avoided;
1043 self.read_avoidance_confidence = read_avoidance_confidence_for(
1044 self.observed_file_read_replacements,
1045 self.modeled_file_reads_avoided,
1046 )
1047 .to_string();
1048 }
1049}
1050
1051impl TokenBucketOverview {
1052 #[must_use]
1054 #[allow(clippy::too_many_arguments)]
1055 pub fn from_totals(
1056 token_savings_bucket: String,
1057 provider: String,
1058 model: String,
1059 tokenizer_backend: String,
1060 accuracy: String,
1061 baseline_kind: String,
1062 confidence: String,
1063 accounting_layer: String,
1064 estimate_method: String,
1065 denominator_kind: String,
1066 dedupe_scope: String,
1067 calls: u128,
1068 without: u128,
1069 with: u128,
1070 ) -> Self {
1071 let estimated_saved = aggregate_token_delta(without, with);
1072 let savings_rate = if without == 0 {
1073 None
1074 } else {
1075 Some((without as f64 - with as f64) / without as f64)
1076 };
1077 Self {
1078 token_savings_bucket,
1079 provider,
1080 model,
1081 tokenizer_backend,
1082 accuracy,
1083 baseline_kind,
1084 confidence,
1085 calls: saturating_u128_to_usize(calls),
1086 estimated_without_projectatlas: saturating_u128_to_usize(without),
1087 estimated_with_projectatlas: saturating_u128_to_usize(with),
1088 estimated_saved,
1089 savings_rate,
1090 accounting_layer,
1091 estimate_method,
1092 denominator_kind,
1093 dedupe_scope,
1094 }
1095 }
1096}
1097
1098impl TokenTrendPeriod {
1099 #[must_use]
1101 pub fn from_totals(period: String, calls: u128, without: u128, with: u128) -> Self {
1102 let bucket = TokenBucketOverview::from_totals(
1103 default_token_savings_bucket(),
1104 default_token_provider(),
1105 default_token_model(),
1106 default_tokenizer_backend(),
1107 default_token_accuracy(),
1108 default_token_baseline_kind(),
1109 default_token_confidence(),
1110 default_accounting_layer(),
1111 default_estimate_method(),
1112 default_denominator_kind(),
1113 default_dedupe_scope(),
1114 calls,
1115 without,
1116 with,
1117 );
1118 Self::from_buckets(period, vec![bucket])
1119 }
1120
1121 #[must_use]
1123 pub fn from_buckets(period: String, buckets: Vec<TokenBucketOverview>) -> Self {
1124 let calls = buckets.iter().fold(0u128, |acc, bucket| {
1125 acc.saturating_add(bucket.calls as u128)
1126 });
1127 let without = buckets.iter().fold(0u128, |acc, bucket| {
1128 acc.saturating_add(bucket.estimated_without_projectatlas as u128)
1129 });
1130 let with = buckets.iter().fold(0u128, |acc, bucket| {
1131 acc.saturating_add(bucket.estimated_with_projectatlas as u128)
1132 });
1133 let saved = aggregate_token_delta(without, with);
1134 let savings_rate = if without == 0 {
1135 None
1136 } else {
1137 Some((without as f64 - with as f64) / without as f64)
1138 };
1139 Self {
1140 period,
1141 calls: saturating_u128_to_usize(calls),
1142 estimated_without_projectatlas: saturating_u128_to_usize(without),
1143 estimated_with_projectatlas: saturating_u128_to_usize(with),
1144 estimated_saved: saved,
1145 savings_rate,
1146 buckets,
1147 }
1148 }
1149}
1150
1151impl TokenTrendReport {
1152 #[must_use]
1154 pub fn new(
1155 session: Option<String>,
1156 window: TokenTrendWindow,
1157 periods: Vec<TokenTrendPeriod>,
1158 ) -> Self {
1159 Self {
1160 estimate_kind: TOKEN_ESTIMATE_KIND.to_string(),
1161 estimator: TOKEN_ESTIMATOR.to_string(),
1162 estimate_scope: TOKEN_ESTIMATE_SCOPE.to_string(),
1163 session,
1164 window,
1165 periods,
1166 detail_availability: UsageDetailAvailability::Retained,
1167 }
1168 }
1169
1170 pub const fn set_detail_availability(&mut self, availability: UsageDetailAvailability) {
1172 self.detail_availability = availability;
1173 }
1174}
1175
1176#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
1178struct TokenBucketKey {
1179 token_savings_bucket: String,
1181 provider: String,
1183 model: String,
1185 tokenizer_backend: String,
1187 accuracy: String,
1189 baseline_kind: String,
1191 confidence: String,
1193 accounting_layer: String,
1195 estimate_method: String,
1197 denominator_kind: String,
1199 dedupe_scope: String,
1201}
1202
1203#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
1205struct ModeledBaselineKey {
1206 session_id: String,
1208 baseline_identity: String,
1210 baseline_fingerprint: String,
1212 denominator_kind: String,
1214}
1215
1216#[derive(Default)]
1218struct ModeledBaselineTotals {
1219 calls: usize,
1221 baseline_without_projectatlas: usize,
1223 emitted_with_projectatlas: u128,
1225}
1226
1227#[derive(Default)]
1229struct TokenAccountingSummary {
1230 measured_tokens_saved: isize,
1232 gross_modeled_tokens_avoided: isize,
1234 deduped_modeled_tokens_avoided: isize,
1236 average_modeled_tokens_avoided: isize,
1238 average_tokens_avoided: isize,
1240 maximum_tokens_avoided: isize,
1242 repeated_baselines_deduped: usize,
1244 observed_file_read_replacements: usize,
1246 modeled_file_reads_avoided: usize,
1248 likely_file_reads_avoided: usize,
1250}
1251
1252impl TokenAccountingSummary {
1253 fn from_events(events: &[UsageEvent]) -> Self {
1255 let mut measured_tokens_saved = 0i128;
1256 let mut gross_modeled_tokens_avoided = 0i128;
1257 let mut event_scoped_modeled_tokens_avoided = 0i128;
1258 let mut average_non_directory_tokens_avoided = 0i128;
1259 let mut average_directory_without = 0u128;
1260 let mut average_directory_with = 0u128;
1261 let mut observed_file_read_replacements = 0usize;
1262 let mut modeled_file_reads_avoided = 0usize;
1263 let mut modeled_baselines = BTreeMap::<ModeledBaselineKey, ModeledBaselineTotals>::new();
1264
1265 for event in events {
1266 let (Some(without), Some(with)) = (
1267 event.estimated_tokens_without_projectatlas,
1268 event.estimated_tokens_with_projectatlas,
1269 ) else {
1270 continue;
1271 };
1272 let delta = aggregate_token_delta_wide(without as u128, with as u128);
1273 if is_observed_event(event) {
1274 measured_tokens_saved = measured_tokens_saved.saturating_add(delta);
1275 if is_observed_read_replacement_event(event, without) {
1276 observed_file_read_replacements =
1277 observed_file_read_replacements.saturating_add(1);
1278 }
1279 continue;
1280 }
1281 if !is_modeled_event(event) {
1282 continue;
1283 }
1284 if is_modeled_read_avoidance_event(event, without) {
1285 modeled_file_reads_avoided = modeled_file_reads_avoided.saturating_add(1);
1286 }
1287 gross_modeled_tokens_avoided = gross_modeled_tokens_avoided.saturating_add(delta);
1288 if event.dedupe_scope == TOKEN_DEDUPE_SCOPE_EVENT {
1289 event_scoped_modeled_tokens_avoided =
1290 event_scoped_modeled_tokens_avoided.saturating_add(delta);
1291 if event.denominator_kind == TOKEN_BASELINE_DIRECTORY_WALK {
1292 average_directory_without =
1293 average_directory_without.saturating_add(without as u128);
1294 average_directory_with = average_directory_with.saturating_add(with as u128);
1295 } else {
1296 average_non_directory_tokens_avoided =
1297 average_non_directory_tokens_avoided.saturating_add(delta);
1298 }
1299 continue;
1300 }
1301 let entry = modeled_baselines
1302 .entry(ModeledBaselineKey::from_event(event))
1303 .or_default();
1304 entry.calls = entry.calls.saturating_add(1);
1305 entry.baseline_without_projectatlas = entry.baseline_without_projectatlas.max(without);
1306 entry.emitted_with_projectatlas =
1307 entry.emitted_with_projectatlas.saturating_add(with as u128);
1308 }
1309
1310 let mut deduped_modeled_tokens_avoided = event_scoped_modeled_tokens_avoided;
1311 let mut repeated_baselines_deduped = 0usize;
1312 for (key, totals) in &modeled_baselines {
1313 if totals.calls > 1 {
1314 repeated_baselines_deduped =
1315 repeated_baselines_deduped.saturating_add(totals.calls.saturating_sub(1));
1316 }
1317 let delta = aggregate_token_delta_wide(
1318 totals.baseline_without_projectatlas as u128,
1319 totals.emitted_with_projectatlas,
1320 );
1321 deduped_modeled_tokens_avoided = deduped_modeled_tokens_avoided.saturating_add(delta);
1322 if key.denominator_kind == TOKEN_BASELINE_DIRECTORY_WALK {
1323 average_directory_without = average_directory_without
1324 .saturating_add(totals.baseline_without_projectatlas as u128);
1325 average_directory_with =
1326 average_directory_with.saturating_add(totals.emitted_with_projectatlas);
1327 } else {
1328 average_non_directory_tokens_avoided =
1329 average_non_directory_tokens_avoided.saturating_add(delta);
1330 }
1331 }
1332 let average_directory_tokens_avoided = aggregate_token_delta_wide(
1333 average_modeled_baseline_tokens(
1334 TOKEN_BASELINE_DIRECTORY_WALK,
1335 average_directory_without,
1336 ),
1337 average_directory_with,
1338 );
1339 let average_modeled_tokens_avoided =
1340 average_non_directory_tokens_avoided.saturating_add(average_directory_tokens_avoided);
1341 let average_tokens_avoided =
1342 measured_tokens_saved.saturating_add(average_modeled_tokens_avoided);
1343 let maximum_tokens_avoided =
1344 measured_tokens_saved.saturating_add(deduped_modeled_tokens_avoided);
1345 let likely_file_reads_avoided =
1346 observed_file_read_replacements.saturating_add(modeled_file_reads_avoided);
1347 Self {
1348 measured_tokens_saved: saturating_i128_to_isize(measured_tokens_saved),
1349 gross_modeled_tokens_avoided: saturating_i128_to_isize(gross_modeled_tokens_avoided),
1350 deduped_modeled_tokens_avoided: saturating_i128_to_isize(
1351 deduped_modeled_tokens_avoided,
1352 ),
1353 average_modeled_tokens_avoided: saturating_i128_to_isize(
1354 average_modeled_tokens_avoided,
1355 ),
1356 average_tokens_avoided: saturating_i128_to_isize(average_tokens_avoided),
1357 maximum_tokens_avoided: saturating_i128_to_isize(maximum_tokens_avoided),
1358 repeated_baselines_deduped,
1359 observed_file_read_replacements,
1360 modeled_file_reads_avoided,
1361 likely_file_reads_avoided,
1362 }
1363 }
1364}
1365
1366impl ModeledBaselineKey {
1367 fn from_event(event: &UsageEvent) -> Self {
1369 let identity = if event.baseline_identity.is_empty() {
1370 default_baseline_identity(
1371 &event.command,
1372 event.path.as_deref(),
1373 event.query.as_deref(),
1374 &event.baseline_kind,
1375 )
1376 } else {
1377 event.baseline_identity.clone()
1378 };
1379 let fingerprint = if event.baseline_fingerprint.is_empty() {
1380 identity.clone()
1381 } else {
1382 event.baseline_fingerprint.clone()
1383 };
1384 Self {
1385 session_id: event.session_id.clone(),
1386 baseline_identity: identity,
1387 baseline_fingerprint: fingerprint,
1388 denominator_kind: event.denominator_kind.clone(),
1389 }
1390 }
1391}
1392
1393impl TokenBucketKey {
1394 fn from(event: &UsageEvent) -> Self {
1396 let observed = is_observed_event(event);
1397 Self {
1398 token_savings_bucket: event.token_savings_bucket.clone(),
1399 provider: event.provider.clone(),
1400 model: event.model.clone(),
1401 tokenizer_backend: event.tokenizer_backend.clone(),
1402 accuracy: event.accuracy.clone(),
1403 baseline_kind: event.baseline_kind.clone(),
1404 confidence: event.confidence.clone(),
1405 accounting_layer: if observed {
1406 TOKEN_ACCOUNTING_OBSERVED_DELTA.to_string()
1407 } else {
1408 event.accounting_layer.clone()
1409 },
1410 estimate_method: event.estimate_method.clone(),
1411 denominator_kind: if observed {
1412 TOKEN_BASELINE_FULL_FILE.to_string()
1413 } else {
1414 event.denominator_kind.clone()
1415 },
1416 dedupe_scope: if observed {
1417 TOKEN_DEDUPE_SCOPE_EVENT.to_string()
1418 } else {
1419 event.dedupe_scope.clone()
1420 },
1421 }
1422 }
1423
1424 fn into_overview(self, calls: u128, without: u128, with: u128) -> TokenBucketOverview {
1426 TokenBucketOverview::from_totals(
1427 self.token_savings_bucket,
1428 self.provider,
1429 self.model,
1430 self.tokenizer_backend,
1431 self.accuracy,
1432 self.baseline_kind,
1433 self.confidence,
1434 self.accounting_layer,
1435 self.estimate_method,
1436 self.denominator_kind,
1437 self.dedupe_scope,
1438 calls,
1439 without,
1440 with,
1441 )
1442 }
1443}
1444
1445#[must_use]
1447pub fn usage_from_text(
1448 session_id: &str,
1449 command: &str,
1450 path: Option<String>,
1451 query: Option<String>,
1452 baseline_text: &str,
1453 projectatlas_text: &str,
1454) -> UsageEvent {
1455 let without = estimate_tokens(baseline_text);
1456 let with = estimate_tokens(projectatlas_text);
1457 usage_from_estimates_with_accounting(
1458 session_id,
1459 command,
1460 path,
1461 query,
1462 without,
1463 with,
1464 TOKEN_BUCKET_FULL_FILE_COMPRESSION,
1465 TOKEN_BASELINE_FULL_FILE,
1466 TOKEN_CONFIDENCE_OBSERVED,
1467 TOKEN_ACCOUNTING_OBSERVED_DELTA,
1468 TOKEN_BASELINE_FULL_FILE,
1469 TOKEN_DEDUPE_SCOPE_EVENT,
1470 )
1471}
1472
1473#[must_use]
1475pub fn usage_from_estimates(
1476 session_id: &str,
1477 command: &str,
1478 path: Option<String>,
1479 query: Option<String>,
1480 estimated_without_projectatlas: usize,
1481 estimated_with_projectatlas: usize,
1482) -> UsageEvent {
1483 usage_from_estimates_with_accounting(
1484 session_id,
1485 command,
1486 path,
1487 query,
1488 estimated_without_projectatlas,
1489 estimated_with_projectatlas,
1490 TOKEN_BUCKET_NAVIGATION_AVOIDANCE,
1491 TOKEN_BASELINE_SELECTED_CANDIDATES,
1492 TOKEN_CONFIDENCE_INFERRED,
1493 TOKEN_ACCOUNTING_MODELED_AVOIDANCE,
1494 TOKEN_BASELINE_SELECTED_CANDIDATES,
1495 TOKEN_DEDUPE_SCOPE_SESSION,
1496 )
1497}
1498
1499#[must_use]
1501#[allow(clippy::too_many_arguments)]
1502pub fn usage_from_estimates_with_context(
1503 session_id: &str,
1504 command: &str,
1505 path: Option<String>,
1506 query: Option<String>,
1507 estimated_without_projectatlas: usize,
1508 estimated_with_projectatlas: usize,
1509 token_savings_bucket: &str,
1510 baseline_kind: &str,
1511 confidence: &str,
1512) -> UsageEvent {
1513 usage_from_estimates_with_accounting(
1514 session_id,
1515 command,
1516 path,
1517 query,
1518 estimated_without_projectatlas,
1519 estimated_with_projectatlas,
1520 token_savings_bucket,
1521 baseline_kind,
1522 confidence,
1523 if token_savings_bucket == TOKEN_BUCKET_FULL_FILE_COMPRESSION {
1524 TOKEN_ACCOUNTING_OBSERVED_DELTA
1525 } else {
1526 TOKEN_ACCOUNTING_MODELED_AVOIDANCE
1527 },
1528 baseline_kind,
1529 if token_savings_bucket == TOKEN_BUCKET_FULL_FILE_COMPRESSION {
1530 TOKEN_DEDUPE_SCOPE_EVENT
1531 } else {
1532 TOKEN_DEDUPE_SCOPE_SESSION
1533 },
1534 )
1535}
1536
1537#[must_use]
1539#[allow(clippy::too_many_arguments)]
1540pub fn usage_from_estimates_with_accounting(
1541 session_id: &str,
1542 command: &str,
1543 path: Option<String>,
1544 query: Option<String>,
1545 estimated_without_projectatlas: usize,
1546 estimated_with_projectatlas: usize,
1547 token_savings_bucket: &str,
1548 baseline_kind: &str,
1549 confidence: &str,
1550 accounting_layer: &str,
1551 denominator_kind: &str,
1552 dedupe_scope: &str,
1553) -> UsageEvent {
1554 let baseline_identity =
1555 default_baseline_identity(command, path.as_deref(), query.as_deref(), baseline_kind);
1556 let baseline_fingerprint = baseline_identity.clone();
1557 UsageEvent {
1558 session_id: session_id.to_string(),
1559 command: command.to_string(),
1560 path,
1561 query,
1562 estimated_tokens_without_projectatlas: Some(estimated_without_projectatlas),
1563 estimated_tokens_with_projectatlas: Some(estimated_with_projectatlas),
1564 estimated_tokens_saved: Some(token_delta(
1565 estimated_without_projectatlas,
1566 estimated_with_projectatlas,
1567 )),
1568 token_savings_bucket: token_savings_bucket.to_string(),
1569 provider: default_token_provider(),
1570 model: default_token_model(),
1571 tokenizer_backend: default_tokenizer_backend(),
1572 accuracy: default_token_accuracy(),
1573 baseline_kind: baseline_kind.to_string(),
1574 confidence: confidence.to_string(),
1575 calculation_trace: default_token_trace(),
1576 accounting_layer: accounting_layer.to_string(),
1577 estimate_method: default_estimate_method(),
1578 denominator_kind: denominator_kind.to_string(),
1579 baseline_identity,
1580 baseline_fingerprint,
1581 dedupe_scope: dedupe_scope.to_string(),
1582 }
1583}
1584
1585#[must_use]
1587pub fn default_token_savings_bucket() -> String {
1588 TOKEN_BUCKET_NAVIGATION_AVOIDANCE.to_string()
1589}
1590
1591#[must_use]
1593pub fn default_token_provider() -> String {
1594 TOKEN_PROVIDER_HEURISTIC.to_string()
1595}
1596
1597#[must_use]
1599pub fn default_token_model() -> String {
1600 TOKEN_MODEL_UNKNOWN.to_string()
1601}
1602
1603#[must_use]
1605pub fn default_tokenizer_backend() -> String {
1606 TOKENIZER_BACKEND_HEURISTIC.to_string()
1607}
1608
1609#[must_use]
1611pub fn default_token_accuracy() -> String {
1612 TOKEN_ACCURACY_HEURISTIC.to_string()
1613}
1614
1615#[must_use]
1617pub fn default_token_baseline_kind() -> String {
1618 TOKEN_BASELINE_SELECTED_CANDIDATES.to_string()
1619}
1620
1621#[must_use]
1623pub fn default_token_confidence() -> String {
1624 TOKEN_CONFIDENCE_INFERRED.to_string()
1625}
1626
1627#[must_use]
1629pub fn default_token_trace() -> String {
1630 TOKEN_TRACE_HEURISTIC.to_string()
1631}
1632
1633#[must_use]
1635pub fn default_accounting_layer() -> String {
1636 TOKEN_ACCOUNTING_MODELED_AVOIDANCE.to_string()
1637}
1638
1639#[must_use]
1641pub fn default_estimate_method() -> String {
1642 TOKEN_ESTIMATE_METHOD_HEURISTIC.to_string()
1643}
1644
1645#[must_use]
1647pub fn default_denominator_kind() -> String {
1648 TOKEN_BASELINE_SELECTED_CANDIDATES.to_string()
1649}
1650
1651#[must_use]
1653pub fn default_dedupe_scope() -> String {
1654 TOKEN_DEDUPE_SCOPE_SESSION.to_string()
1655}
1656
1657#[must_use]
1659pub fn default_read_avoidance_scope() -> String {
1660 READ_AVOIDANCE_SCOPE.to_string()
1661}
1662
1663#[must_use]
1665pub fn default_read_avoidance_confidence() -> String {
1666 READ_AVOIDANCE_CONFIDENCE_NOT_RECORDED.to_string()
1667}
1668
1669#[must_use]
1671pub fn default_baseline_identity(
1672 command: &str,
1673 path: Option<&str>,
1674 query: Option<&str>,
1675 baseline_kind: &str,
1676) -> String {
1677 format!(
1678 "{baseline_kind}:command={command}:path={path}:query={query}",
1679 path = path.unwrap_or("*"),
1680 query = query.unwrap_or("*")
1681 )
1682}
1683
1684fn token_delta(without: usize, with: usize) -> isize {
1686 let without = isize::try_from(without).unwrap_or(isize::MAX);
1687 let with = isize::try_from(with).unwrap_or(isize::MAX);
1688 without.saturating_sub(with)
1689}
1690
1691fn aggregate_token_delta(without: u128, with: u128) -> isize {
1693 saturating_i128_to_isize(aggregate_token_delta_wide(without, with))
1694}
1695
1696fn aggregate_token_delta_wide(without: u128, with: u128) -> i128 {
1698 if without >= with {
1699 let delta = without - with;
1700 if delta > i128::MAX as u128 {
1701 i128::MAX
1702 } else {
1703 delta as i128
1704 }
1705 } else {
1706 let delta = with - without;
1707 if delta > i128::MAX as u128 {
1708 i128::MIN
1709 } else {
1710 -(delta as i128)
1711 }
1712 }
1713}
1714
1715#[must_use]
1717pub fn average_modeled_baseline_tokens(denominator_kind: &str, without: u128) -> u128 {
1718 if denominator_kind == TOKEN_BASELINE_DIRECTORY_WALK {
1719 without / 2
1720 } else {
1721 without
1722 }
1723}
1724
1725fn saturating_u128_to_usize(value: u128) -> usize {
1727 if value > usize::MAX as u128 {
1728 usize::MAX
1729 } else {
1730 value as usize
1731 }
1732}
1733
1734fn saturating_i128_to_isize(value: i128) -> isize {
1736 if value > isize::MAX as i128 {
1737 isize::MAX
1738 } else if value < isize::MIN as i128 {
1739 isize::MIN
1740 } else {
1741 value as isize
1742 }
1743}
1744
1745fn measured_tokens_saved_from_buckets(buckets: &[TokenBucketOverview]) -> i128 {
1747 buckets
1748 .iter()
1749 .filter(|bucket| is_observed_bucket(bucket))
1750 .fold(0i128, |acc, bucket| {
1751 acc.saturating_add(bucket.estimated_saved as i128)
1752 })
1753}
1754
1755fn modeled_tokens_saved_from_buckets(buckets: &[TokenBucketOverview]) -> i128 {
1757 buckets
1758 .iter()
1759 .filter(|bucket| is_modeled_bucket(bucket))
1760 .fold(0i128, |acc, bucket| {
1761 acc.saturating_add(bucket.estimated_saved as i128)
1762 })
1763}
1764
1765fn average_modeled_tokens_saved_from_buckets(buckets: &[TokenBucketOverview]) -> i128 {
1767 let mut non_directory_tokens_avoided = 0i128;
1768 let mut directory_without = 0u128;
1769 let mut directory_with = 0u128;
1770 for bucket in buckets.iter().filter(|bucket| is_modeled_bucket(bucket)) {
1771 if bucket.denominator_kind == TOKEN_BASELINE_DIRECTORY_WALK {
1772 directory_without =
1773 directory_without.saturating_add(bucket.estimated_without_projectatlas as u128);
1774 directory_with =
1775 directory_with.saturating_add(bucket.estimated_with_projectatlas as u128);
1776 } else {
1777 non_directory_tokens_avoided =
1778 non_directory_tokens_avoided.saturating_add(bucket.estimated_saved as i128);
1779 }
1780 }
1781 let directory_tokens_avoided = aggregate_token_delta_wide(
1782 average_modeled_baseline_tokens(TOKEN_BASELINE_DIRECTORY_WALK, directory_without),
1783 directory_with,
1784 );
1785 non_directory_tokens_avoided.saturating_add(directory_tokens_avoided)
1786}
1787
1788fn is_observed_event(event: &UsageEvent) -> bool {
1790 event.accounting_layer == TOKEN_ACCOUNTING_OBSERVED_DELTA
1791 || event.token_savings_bucket == TOKEN_BUCKET_FULL_FILE_COMPRESSION
1792 || event.confidence == TOKEN_CONFIDENCE_OBSERVED
1793}
1794
1795fn is_modeled_event(event: &UsageEvent) -> bool {
1797 event.accounting_layer == TOKEN_ACCOUNTING_MODELED_AVOIDANCE || !is_observed_event(event)
1798}
1799
1800fn is_observed_read_replacement_event(event: &UsageEvent, baseline_tokens: usize) -> bool {
1802 baseline_tokens > 0
1803 && matches!(
1804 event.command.as_str(),
1805 TOKEN_COMMAND_SUMMARY
1806 | TOKEN_COMMAND_OUTLINE
1807 | TOKEN_COMMAND_SLICE
1808 | TOKEN_COMMAND_SYMBOL_SLICE
1809 | TOKEN_COMMAND_MCP_FILE_SUMMARY
1810 | TOKEN_COMMAND_MCP_OUTLINE
1811 | TOKEN_COMMAND_MCP_SLICE
1812 )
1813}
1814
1815fn is_modeled_read_avoidance_event(event: &UsageEvent, baseline_tokens: usize) -> bool {
1817 baseline_tokens > 0
1818 && matches!(
1819 event.command.as_str(),
1820 TOKEN_COMMAND_SEARCH | TOKEN_COMMAND_MCP_SEARCH
1821 )
1822 && event.denominator_kind == TOKEN_BASELINE_SELECTED_CANDIDATES
1823}
1824
1825fn read_avoidance_confidence_for(
1827 observed_file_read_replacements: usize,
1828 modeled_file_reads_avoided: usize,
1829) -> &'static str {
1830 if observed_file_read_replacements == 0 && modeled_file_reads_avoided == 0 {
1831 READ_AVOIDANCE_CONFIDENCE_NOT_RECORDED
1832 } else if modeled_file_reads_avoided == 0 {
1833 READ_AVOIDANCE_CONFIDENCE_OBSERVED
1834 } else {
1835 READ_AVOIDANCE_CONFIDENCE_MODELED
1836 }
1837}
1838
1839fn is_observed_bucket(bucket: &TokenBucketOverview) -> bool {
1841 bucket.accounting_layer == TOKEN_ACCOUNTING_OBSERVED_DELTA
1842 || bucket.token_savings_bucket == TOKEN_BUCKET_FULL_FILE_COMPRESSION
1843 || bucket.confidence == TOKEN_CONFIDENCE_OBSERVED
1844}
1845
1846fn is_modeled_bucket(bucket: &TokenBucketOverview) -> bool {
1848 bucket.accounting_layer == TOKEN_ACCOUNTING_MODELED_AVOIDANCE || !is_observed_bucket(bucket)
1849}
1850
1851#[cfg(test)]
1852mod tests {
1853 use super::{
1854 AgentEfficiencyEvidenceState, READ_AVOIDANCE_CONFIDENCE_MODELED,
1855 READ_AVOIDANCE_CONFIDENCE_NOT_RECORDED, READ_AVOIDANCE_CONFIDENCE_OBSERVED,
1856 READ_AVOIDANCE_SCOPE, TOKEN_AVERAGE_DIRECTORY_WALK_PERCENT, TOKEN_BASELINE_DIRECTORY_WALK,
1857 TOKEN_BUCKET_FULL_FILE_COMPRESSION, TOKEN_BUCKET_NAVIGATION_AVOIDANCE,
1858 TOKEN_DEDUPE_SCOPE_EVENT, TOKEN_ESTIMATE_KIND, TOKEN_ESTIMATE_SCOPE, TOKEN_ESTIMATOR,
1859 TelemetryContractError, TokenAccountingTotals, TokenOverview, TokenTrendReport,
1860 TokenTrendWindow, UsageDetailAvailability, UsageInstanceId, UsageInstanceOwner,
1861 usage_from_estimates, usage_from_text,
1862 };
1863 use std::io;
1864
1865 fn require_eq<T: std::fmt::Debug + PartialEq>(
1866 actual: &T,
1867 expected: &T,
1868 label: &str,
1869 ) -> Result<(), Box<dyn std::error::Error>> {
1870 if actual == expected {
1871 Ok(())
1872 } else {
1873 Err(io::Error::other(format!(
1874 "{label} mismatch: expected {expected:?}, got {actual:?}"
1875 ))
1876 .into())
1877 }
1878 }
1879
1880 #[test]
1881 fn usage_instance_ids_validate_and_round_trip() {
1882 let bytes = [7; 16];
1883 let identity = UsageInstanceId::from_bytes(bytes);
1884 assert_eq!(identity.map(UsageInstanceId::as_bytes), Ok(bytes));
1885 assert_eq!(
1886 UsageInstanceId::from_bytes([0; 16]),
1887 Err(TelemetryContractError::ZeroUsageInstanceId)
1888 );
1889 }
1890
1891 #[test]
1892 fn usage_states_parse_and_missing_report_state_fails_honest()
1893 -> Result<(), Box<dyn std::error::Error>> {
1894 for (value, expected) in [
1895 ("cli_invocation", UsageInstanceOwner::CliInvocation),
1896 ("mcp_process", UsageInstanceOwner::McpProcess),
1897 ("library_handle", UsageInstanceOwner::LibraryHandle),
1898 ("migrated_legacy", UsageInstanceOwner::MigratedLegacy),
1899 ] {
1900 require_eq(
1901 &UsageInstanceOwner::parse(value),
1902 &Some(expected),
1903 "usage instance owner parse",
1904 )?;
1905 require_eq(&expected.as_str(), &value, "usage instance owner encoding")?;
1906 }
1907 require_eq(
1908 &UsageInstanceOwner::parse("unknown"),
1909 &None,
1910 "unknown usage instance owner",
1911 )?;
1912
1913 for (value, expected) in [
1914 ("retained", UsageDetailAvailability::Retained),
1915 ("partial", UsageDetailAvailability::Partial),
1916 ("expired", UsageDetailAvailability::Expired),
1917 ("unavailable", UsageDetailAvailability::Unavailable),
1918 ] {
1919 require_eq(
1920 &UsageDetailAvailability::parse(value),
1921 &Some(expected),
1922 "detail availability parse",
1923 )?;
1924 require_eq(&expected.as_str(), &value, "detail availability encoding")?;
1925 }
1926 require_eq(
1927 &UsageDetailAvailability::parse("unknown"),
1928 &None,
1929 "unknown detail availability",
1930 )?;
1931 require_eq(
1932 &UsageDetailAvailability::default(),
1933 &UsageDetailAvailability::Unavailable,
1934 "default detail availability",
1935 )?;
1936
1937 let overview = TokenOverview::from_events(&[]);
1938 require_eq(
1939 &overview.detail_availability,
1940 &UsageDetailAvailability::Retained,
1941 "new overview detail availability",
1942 )?;
1943 let mut overview_value = serde_json::to_value(overview)?;
1944 let overview_object = overview_value
1945 .as_object_mut()
1946 .ok_or_else(|| io::Error::other("serialized token overview was not an object"))?;
1947 overview_object.remove("detail_availability");
1948 overview_object.remove("agent_efficiency");
1949 overview_object.remove("average_modeled_tokens_avoided");
1950 overview_object.remove("average_tokens_avoided");
1951 overview_object.remove("maximum_tokens_avoided");
1952 overview_object.remove("average_policy");
1953 let decoded_overview: TokenOverview = serde_json::from_value(overview_value)?;
1954 require_eq(
1955 &decoded_overview.detail_availability,
1956 &UsageDetailAvailability::Unavailable,
1957 "missing overview detail availability",
1958 )?;
1959 require_eq(
1960 &decoded_overview.agent_efficiency.state,
1961 &AgentEfficiencyEvidenceState::Unavailable,
1962 "missing agent-efficiency evidence state",
1963 )?;
1964 require_eq(
1965 &decoded_overview.agent_efficiency.baselines,
1966 &Vec::new(),
1967 "missing agent-efficiency baseline rows",
1968 )?;
1969 require_eq(
1970 &decoded_overview.average_tokens_avoided,
1971 &0,
1972 "missing average tokens avoided",
1973 )?;
1974 require_eq(
1975 &decoded_overview.maximum_tokens_avoided,
1976 &0,
1977 "missing maximum tokens avoided",
1978 )?;
1979 require_eq(
1980 &decoded_overview
1981 .average_policy
1982 .directory_walk_baseline_percent,
1983 &TOKEN_AVERAGE_DIRECTORY_WALK_PERCENT,
1984 "missing average policy",
1985 )?;
1986 require_eq(
1987 &AgentEfficiencyEvidenceState::Partial.as_str(),
1988 &"partial",
1989 "agent-efficiency evidence encoding",
1990 )?;
1991
1992 let trends = TokenTrendReport::new(None, TokenTrendWindow::Day, Vec::new());
1993 require_eq(
1994 &trends.detail_availability,
1995 &UsageDetailAvailability::Retained,
1996 "new trend detail availability",
1997 )?;
1998 let mut trends_value = serde_json::to_value(trends)?;
1999 let trends_object = trends_value
2000 .as_object_mut()
2001 .ok_or_else(|| io::Error::other("serialized token trends were not an object"))?;
2002 trends_object.remove("detail_availability");
2003 let decoded_trends: TokenTrendReport = serde_json::from_value(trends_value)?;
2004 require_eq(
2005 &decoded_trends.detail_availability,
2006 &UsageDetailAvailability::Unavailable,
2007 "missing trend detail availability",
2008 )?;
2009 Ok(())
2010 }
2011
2012 #[test]
2013 fn modeled_baseline_keys_preserve_legacy_fallback_and_component_boundaries() {
2014 let event = usage_from_estimates(
2015 "session",
2016 "search",
2017 Some("src/lib.rs".to_string()),
2018 Some("needle".to_string()),
2019 100,
2020 20,
2021 );
2022 let expected_key = event.modeled_baseline_key();
2023 assert_eq!(
2024 event.effective_baseline_identity().as_ref(),
2025 event.baseline_identity
2026 );
2027 assert_eq!(
2028 event.effective_baseline_fingerprint().as_ref(),
2029 event.baseline_fingerprint
2030 );
2031
2032 let mut legacy = event.clone();
2033 legacy.baseline_identity.clear();
2034 legacy.baseline_fingerprint.clear();
2035 assert_eq!(legacy.modeled_baseline_key(), expected_key);
2036
2037 let mut changed_fingerprint = event.clone();
2038 changed_fingerprint
2039 .baseline_fingerprint
2040 .push_str("-changed");
2041 assert_ne!(changed_fingerprint.modeled_baseline_key(), expected_key);
2042
2043 let mut left = event.clone();
2044 left.baseline_identity = "ab".to_string();
2045 left.baseline_fingerprint = "c".to_string();
2046 let mut right = event;
2047 right.baseline_identity = "a".to_string();
2048 right.baseline_fingerprint = "bc".to_string();
2049 assert_ne!(left.modeled_baseline_key(), right.modeled_baseline_key());
2050 }
2051
2052 #[test]
2053 fn wide_accounting_totals_narrow_only_at_the_report_boundary() {
2054 let mut overview = TokenOverview::from_events(&[]);
2055 overview.apply_accounting_totals(TokenAccountingTotals {
2056 measured_tokens_saved: 7,
2057 gross_modeled_tokens_avoided: 100,
2058 deduped_modeled_tokens_avoided: 30,
2059 average_modeled_tokens_avoided: 10,
2060 repeated_baselines_deduped: 2,
2061 observed_file_read_replacements: 1,
2062 modeled_file_reads_avoided: 3,
2063 });
2064 assert_eq!(overview.measured_tokens_saved, 7);
2065 assert_eq!(overview.gross_modeled_tokens_avoided, 100);
2066 assert_eq!(overview.deduped_modeled_tokens_avoided, 30);
2067 assert_eq!(overview.average_modeled_tokens_avoided, 10);
2068 assert_eq!(overview.average_tokens_avoided, 17);
2069 assert_eq!(overview.maximum_tokens_avoided, 37);
2070 assert_eq!(overview.tokens_avoided, overview.average_tokens_avoided);
2071 assert_eq!(overview.repeated_baselines_deduped, 2);
2072 assert_eq!(overview.observed_file_read_replacements, 1);
2073 assert_eq!(overview.modeled_file_reads_avoided, 3);
2074 assert_eq!(overview.likely_file_reads_avoided, 4);
2075 assert_eq!(
2076 overview.read_avoidance_confidence,
2077 READ_AVOIDANCE_CONFIDENCE_MODELED
2078 );
2079
2080 overview.apply_accounting_totals(TokenAccountingTotals {
2081 measured_tokens_saved: i128::MAX,
2082 gross_modeled_tokens_avoided: i128::MIN,
2083 deduped_modeled_tokens_avoided: i128::MAX,
2084 average_modeled_tokens_avoided: i128::MAX,
2085 repeated_baselines_deduped: u128::MAX,
2086 observed_file_read_replacements: u128::MAX,
2087 modeled_file_reads_avoided: u128::MAX,
2088 });
2089 assert_eq!(overview.measured_tokens_saved, isize::MAX);
2090 assert_eq!(overview.gross_modeled_tokens_avoided, isize::MIN);
2091 assert_eq!(overview.deduped_modeled_tokens_avoided, isize::MAX);
2092 assert_eq!(overview.average_modeled_tokens_avoided, isize::MAX);
2093 assert_eq!(overview.average_tokens_avoided, isize::MAX);
2094 assert_eq!(overview.maximum_tokens_avoided, isize::MAX);
2095 assert_eq!(overview.tokens_avoided, isize::MAX);
2096 assert_eq!(overview.repeated_baselines_deduped, usize::MAX);
2097 assert_eq!(overview.observed_file_read_replacements, usize::MAX);
2098 assert_eq!(overview.modeled_file_reads_avoided, usize::MAX);
2099 assert_eq!(overview.likely_file_reads_avoided, usize::MAX);
2100 }
2101
2102 #[test]
2103 fn usage_from_text_tracks_positive_and_negative_savings() {
2104 let positive = usage_from_text("s", "outline", None, None, "abcdefghijkl", "abcd");
2105 assert_eq!(positive.estimated_tokens_without_projectatlas, Some(3));
2106 assert_eq!(positive.estimated_tokens_with_projectatlas, Some(1));
2107 assert_eq!(positive.estimated_tokens_saved, Some(2));
2108
2109 let negative = usage_from_estimates("s", "overview", None, None, 1, 4);
2110 assert_eq!(negative.estimated_tokens_saved, Some(-3));
2111 }
2112
2113 #[test]
2114 fn huge_estimates_use_saturating_signed_delta() {
2115 let event = usage_from_estimates("s", "large-repo", None, None, usize::MAX, 0);
2116 assert_eq!(event.estimated_tokens_saved, Some(isize::MAX));
2117 }
2118
2119 #[test]
2120 fn overview_recomputes_saved_from_aggregate_without_and_with() {
2121 let mut first = usage_from_estimates("s", "a", None, None, 20, 50);
2122 first.estimated_tokens_saved = Some(999);
2123 let mut second = usage_from_estimates("s", "b", None, None, 0, 10);
2124 second.estimated_tokens_saved = Some(999);
2125 let overview = TokenOverview::from_events(&[first, second]);
2126
2127 assert_eq!(overview.estimate_kind, TOKEN_ESTIMATE_KIND);
2128 assert_eq!(overview.estimator, TOKEN_ESTIMATOR);
2129 assert_eq!(overview.estimate_scope, TOKEN_ESTIMATE_SCOPE);
2130 assert_eq!(overview.calls, 2);
2131 assert_eq!(overview.estimated_without_projectatlas, 20);
2132 assert_eq!(overview.estimated_with_projectatlas, 60);
2133 assert_eq!(overview.estimated_saved, -40);
2134 assert_eq!(overview.savings_rate, Some(-2.0));
2135 }
2136
2137 #[test]
2138 fn overview_keeps_source_compression_and_navigation_buckets_separate() {
2139 let overview = TokenOverview::from_events(&[
2140 usage_from_text("s", "summary", None, None, "abcdefghijkl", "abcd"),
2141 usage_from_estimates("s", "search", None, None, 100, 20),
2142 ]);
2143
2144 assert_eq!(overview.calls, 2);
2145 assert_eq!(overview.buckets.len(), 2);
2146 assert_eq!(
2147 overview.buckets[0].token_savings_bucket,
2148 TOKEN_BUCKET_FULL_FILE_COMPRESSION
2149 );
2150 assert_eq!(
2151 overview.buckets[1].token_savings_bucket,
2152 TOKEN_BUCKET_NAVIGATION_AVOIDANCE
2153 );
2154 assert_eq!(overview.observed_file_read_replacements, 1);
2155 assert_eq!(overview.modeled_file_reads_avoided, 1);
2156 assert_eq!(overview.likely_file_reads_avoided, 2);
2157 assert_eq!(
2158 overview.read_avoidance_confidence,
2159 READ_AVOIDANCE_CONFIDENCE_MODELED
2160 );
2161 assert_eq!(overview.read_avoidance_scope, READ_AVOIDANCE_SCOPE);
2162 }
2163
2164 #[test]
2165 fn observed_only_overview_reports_observed_read_avoidance_confidence() {
2166 let overview = TokenOverview::from_events(&[usage_from_text(
2167 "s",
2168 "summary",
2169 None,
2170 None,
2171 "abcdefghijkl",
2172 "abcd",
2173 )]);
2174
2175 assert_eq!(overview.observed_file_read_replacements, 1);
2176 assert_eq!(overview.modeled_file_reads_avoided, 0);
2177 assert_eq!(overview.likely_file_reads_avoided, 1);
2178 assert_eq!(
2179 overview.read_avoidance_confidence,
2180 READ_AVOIDANCE_CONFIDENCE_OBSERVED
2181 );
2182 }
2183
2184 #[test]
2185 fn bucket_overview_does_not_infer_read_avoidance_without_raw_events() {
2186 let event_overview = TokenOverview::from_events(&[
2187 usage_from_text("s", "summary", None, None, "abcdefghijkl", "abcd"),
2188 usage_from_estimates("s", "search", None, None, 100, 20),
2189 ]);
2190 let bucket_overview = TokenOverview::from_buckets(event_overview.buckets);
2191
2192 assert_eq!(bucket_overview.observed_file_read_replacements, 0);
2193 assert_eq!(bucket_overview.modeled_file_reads_avoided, 0);
2194 assert_eq!(bucket_overview.likely_file_reads_avoided, 0);
2195 assert_eq!(
2196 bucket_overview.read_avoidance_confidence,
2197 READ_AVOIDANCE_CONFIDENCE_NOT_RECORDED
2198 );
2199 }
2200
2201 #[test]
2202 fn non_file_read_navigation_events_do_not_increment_read_avoidance() {
2203 let overview = TokenOverview::from_events(&[
2204 usage_from_estimates("s", "overview", None, None, 100, 20),
2205 usage_from_estimates("s", "folders", None, None, 100, 20),
2206 usage_from_estimates("s", "files", None, None, 100, 20),
2207 usage_from_estimates("s", "mcp.atlas_health", None, None, 100, 20),
2208 usage_from_estimates("s", "mcp.atlas_purpose_queue", None, None, 100, 20),
2209 ]);
2210
2211 assert_eq!(overview.modeled_file_reads_avoided, 0);
2212 assert_eq!(overview.likely_file_reads_avoided, 0);
2213 }
2214
2215 #[test]
2216 fn zero_baseline_events_do_not_increment_read_avoidance() {
2217 let overview = TokenOverview::from_events(&[
2218 usage_from_estimates("s", "search", None, None, 0, 20),
2219 usage_from_text("s", "summary", None, None, "", "summary"),
2220 ]);
2221
2222 assert_eq!(overview.observed_file_read_replacements, 0);
2223 assert_eq!(overview.modeled_file_reads_avoided, 0);
2224 assert_eq!(overview.likely_file_reads_avoided, 0);
2225 }
2226
2227 #[test]
2228 fn average_policy_halves_only_deduped_directory_walk_baselines() {
2229 let mut first_folder =
2230 usage_from_estimates("s", "folders", Some("src".to_string()), None, 101, 20);
2231 first_folder.denominator_kind = TOKEN_BASELINE_DIRECTORY_WALK.to_string();
2232 first_folder.baseline_identity = "directory:src".to_string();
2233 first_folder.baseline_fingerprint = "directory:src@1".to_string();
2234 let mut second_folder = first_folder.clone();
2235 second_folder.estimated_tokens_with_projectatlas = Some(10);
2236 second_folder.estimated_tokens_saved = Some(91);
2237
2238 let overview = TokenOverview::from_events(&[
2239 usage_from_text(
2240 "s",
2241 "summary",
2242 Some("src/lib.rs".to_string()),
2243 None,
2244 "abcdabcd",
2245 "ab",
2246 ),
2247 first_folder,
2248 second_folder,
2249 usage_from_estimates("s", "search", None, Some("token".to_string()), 80, 20),
2250 ]);
2251
2252 assert_eq!(overview.measured_tokens_saved, 1);
2253 assert_eq!(overview.gross_modeled_tokens_avoided, 232);
2254 assert_eq!(overview.deduped_modeled_tokens_avoided, 131);
2255 assert_eq!(overview.average_modeled_tokens_avoided, 80);
2256 assert_eq!(overview.average_tokens_avoided, 81);
2257 assert_eq!(overview.maximum_tokens_avoided, 132);
2258 assert_eq!(overview.tokens_avoided, overview.average_tokens_avoided);
2259 assert_eq!(overview.repeated_baselines_deduped, 1);
2260 }
2261
2262 #[test]
2263 fn average_directory_walk_policy_preserves_signed_payload_cost() {
2264 let mut event = usage_from_estimates("s", "folders", Some("src".to_string()), None, 5, 4);
2265 event.denominator_kind = TOKEN_BASELINE_DIRECTORY_WALK.to_string();
2266 let overview = TokenOverview::from_events(&[event]);
2267
2268 assert_eq!(overview.average_modeled_tokens_avoided, -2);
2269 assert_eq!(overview.average_tokens_avoided, -2);
2270 assert_eq!(overview.maximum_tokens_avoided, 1);
2271 assert_eq!(overview.tokens_avoided, -2);
2272 }
2273
2274 #[test]
2275 fn raw_accounting_narrows_once_after_wide_signed_aggregation() {
2276 let bound = isize::MAX as usize;
2277 let mut events = vec![
2278 usage_from_estimates("s", "search", None, Some("a".to_string()), bound, 0),
2279 usage_from_estimates("s", "search", None, Some("b".to_string()), bound, 0),
2280 usage_from_estimates("s", "search", None, Some("c".to_string()), 0, bound),
2281 ];
2282 for event in &mut events {
2283 event.dedupe_scope = TOKEN_DEDUPE_SCOPE_EVENT.to_string();
2284 }
2285
2286 let overview = TokenOverview::from_events(&events);
2287
2288 assert_eq!(overview.gross_modeled_tokens_avoided, isize::MAX);
2289 assert_eq!(overview.deduped_modeled_tokens_avoided, isize::MAX);
2290 assert_eq!(overview.average_modeled_tokens_avoided, isize::MAX);
2291 assert_eq!(overview.average_tokens_avoided, isize::MAX);
2292 assert_eq!(overview.maximum_tokens_avoided, isize::MAX);
2293 }
2294
2295 #[test]
2296 fn bucket_accounting_narrows_once_after_wide_signed_aggregation() {
2297 let modeled_bucket = |saved| {
2298 let mut bucket = TokenOverview::from_estimated_totals(1, 1, 1)
2299 .buckets
2300 .remove(0);
2301 bucket.estimated_saved = saved;
2302 bucket
2303 };
2304 let expected = isize::MAX - 1;
2305 for order in [
2306 [isize::MAX, isize::MAX, isize::MIN],
2307 [isize::MAX, isize::MIN, isize::MAX],
2308 ] {
2309 let overview = TokenOverview::from_buckets(
2310 order.into_iter().map(&modeled_bucket).collect::<Vec<_>>(),
2311 );
2312 assert_eq!(overview.gross_modeled_tokens_avoided, expected);
2313 assert_eq!(overview.average_modeled_tokens_avoided, expected);
2314 assert_eq!(overview.average_tokens_avoided, expected);
2315 assert_eq!(overview.maximum_tokens_avoided, expected);
2316 }
2317 }
2318
2319 #[test]
2320 fn overview_dedupes_repeated_modeled_baselines_without_hiding_measured_savings() {
2321 let overview = TokenOverview::from_events(&[
2322 usage_from_text(
2323 "s",
2324 "summary",
2325 Some("src/lib.rs".to_string()),
2326 None,
2327 "abcdabcd",
2328 "ab",
2329 ),
2330 usage_from_estimates("s", "search", None, Some("token".to_string()), 400, 40),
2331 usage_from_estimates("s", "search", None, Some("token".to_string()), 400, 30),
2332 usage_from_estimates("s", "search", None, Some("token".to_string()), 400, 20),
2333 ]);
2334
2335 assert_eq!(overview.estimated_saved, 1111);
2336 assert_eq!(overview.legacy_gross_estimated_saved, 1111);
2337 assert_eq!(overview.measured_tokens_saved, 1);
2338 assert_eq!(overview.gross_modeled_tokens_avoided, 1110);
2339 assert_eq!(overview.deduped_modeled_tokens_avoided, 310);
2340 assert_eq!(overview.tokens_avoided, 311);
2341 assert_eq!(overview.repeated_baselines_deduped, 2);
2342 assert_eq!(overview.observed_file_read_replacements, 1);
2343 assert_eq!(overview.modeled_file_reads_avoided, 3);
2344 assert_eq!(overview.likely_file_reads_avoided, 4);
2345 }
2346}