Skip to main content

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}