vct_core/usage/pipeline.rs
1//! The one-shot `usage` scan policy, shared by every non-interactive frontend.
2//!
3//! Fetching pricing before the scan (so per-request context-tier classification
4//! has its thresholds), degrading to base-rate classification when the fetch
5//! fails, and running the scan on the caller's pool is policy the CLI used to
6//! inline. Keeping it here lets a non-CLI backend (e.g. a future GUI) run the
7//! exact same pipeline instead of re-deriving it.
8
9use crate::config::ProvidersConfig;
10use crate::models::TimeRange;
11use crate::pricing::{ModelPricingMap, fetch_model_pricing};
12use crate::usage::{
13 UsageCollection, UsageScanOptions, aggregate_usage_from_home_with_diagnostics_opts,
14};
15use anyhow::Result;
16use std::collections::HashMap;
17use std::sync::Arc;
18
19/// A completed usage scan together with the pricing map it was classified with.
20pub struct PricedUsageScan {
21 /// Collected usage plus scan diagnostics.
22 pub collection: UsageCollection,
23 /// The pricing map used for tier classification; empty when the fetch failed.
24 pub pricing: ModelPricingMap,
25 /// The pricing-fetch error when it degraded to an empty map (costs
26 /// unavailable), so the caller can surface the concrete cause; `None` on
27 /// success.
28 pub pricing_error: Option<String>,
29}
30
31/// Fetches pricing, derives the context-tier thresholds, and scans usage.
32///
33/// A failed pricing fetch is logged and downgraded to an empty map (the scan
34/// still runs, classifying every request at the base rate) rather than aborting;
35/// the returned [`PricedUsageScan::pricing_error`] carries the concrete cause so
36/// the caller can surface it however it wants. The scan runs on `pool` so it
37/// never touches Rayon's global pool.
38///
39/// # Errors
40///
41/// Propagates only a hard scan failure (an all-failed collection); pricing
42/// failures degrade instead of erroring.
43pub fn scan_usage_priced(
44 time_range: TimeRange,
45 providers: ProvidersConfig,
46 pool: &rayon::ThreadPool,
47) -> Result<PricedUsageScan> {
48 let (pricing, pricing_error) = match fetch_model_pricing() {
49 Ok(map) => (map, None),
50 Err(e) => {
51 log::warn!("failed to fetch pricing data: {e}; costs unavailable");
52 (ModelPricingMap::new(HashMap::new()), Some(e.to_string()))
53 }
54 };
55 let options = UsageScanOptions {
56 tiers: Some(Arc::new(pricing.tier_thresholds())),
57 };
58 let collection = pool.install(|| {
59 aggregate_usage_from_home_with_diagnostics_opts(time_range, providers, &options)
60 })?;
61 Ok(PricedUsageScan {
62 collection,
63 pricing,
64 pricing_error,
65 })
66}