Skip to content
This repository was archived by the owner on Jul 30, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 7 additions & 44 deletions crates/tokscale-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1079,53 +1079,16 @@ fn emit_cursor_setup_warnings(warnings: &[String]) {
}

fn warp_setup_warnings_for_report(
home_dir: &Option<String>,
_home_dir: &Option<String>,
clients: &Option<Vec<String>>,
) -> Vec<String> {
if !client_filter_explicitly_requests_warp(clients) {
return Vec::new();
}

let (home_path, home_override) = match home_dir {
Some(home) => (PathBuf::from(home), true),
None => match dirs::home_dir() {
Some(home) => (home, false),
None => {
return vec![
"Warp usage requires Tokscale's Warp aggregate cache, but the home directory could not be resolved. Tokscale does not parse local Warp transcripts.".to_string(),
];
}
},
};
let has_cache = if home_override {
warp::has_usage_cache_in_home(&home_path)
} else {
warp::load_usage_cache().is_some()
};
if has_cache {
return Vec::new();
}

let cache_glob = if home_override {
home_path
.join(".config/tokscale/warp-cache/usage*.json")
.to_string_lossy()
.to_string()
} else {
"~/.config/tokscale/warp-cache/usage*.json".to_string()
};
let action = if home_override {
"run `tokscale warp sync` for the default profile or populate that cache before running a report with --home"
} else if warp::has_credentials() {
"run `tokscale warp sync`"
} else {
"run `tokscale warp login` and `tokscale warp sync`"
};

vec![format!(
"Warp usage requires Tokscale's aggregate API cache at `{}`; {}. Tokscale does not parse local Warp/Oz session transcripts and does not infer tokens from request counts.",
cache_glob, action
)]
vec![
"Warp aggregate request/spend data is not included in local reports because it has no token buckets. Tokscale does not parse local Warp/Oz session transcripts; add Warp again only when a token-level source is available.".to_string(),
]
}

fn setup_warnings_for_report(
Expand Down Expand Up @@ -6574,16 +6537,16 @@ mod tests {
}

#[test]
fn warp_setup_warning_explains_missing_aggregate_cache() {
fn warp_setup_warning_explains_aggregate_cache_is_not_reported() {
let temp = tempfile::TempDir::new().unwrap();
let warnings = warp_setup_warnings_for_report(
&Some(temp.path().to_string_lossy().to_string()),
&Some(vec!["warp".to_string()]),
);

assert_eq!(warnings.len(), 1);
assert!(warnings[0].contains("tokscale warp"));
assert!(warnings[0].contains("does not infer tokens from request counts"));
assert!(warnings[0].contains("not included in local reports"));
assert!(warnings[0].contains("no token buckets"));
}

#[test]
Expand Down
6 changes: 0 additions & 6 deletions crates/tokscale-cli/src/warp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,12 +132,6 @@ pub(crate) fn load_usage_cache() -> Option<WarpUsageCache> {
serde_json::from_str(&content).ok()
}

pub fn has_usage_cache_in_home(home_dir: &Path) -> bool {
home_dir
.join(".config/tokscale/warp-cache/usage.json")
.exists()
}

pub fn run_warp_login(token: Option<String>, cookie: bool) -> Result<()> {
println!("\n {}\n", "Warp/Oz - Login".cyan());
let auth_value = match token {
Expand Down
20 changes: 4 additions & 16 deletions crates/tokscale-cli/tests/cli_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1792,10 +1792,7 @@ fn test_models_json_offline_without_pricing_cache_still_succeeds() {
assert_eq!(json["totalMessages"].as_i64().unwrap(), 3);
assert_eq!(json["entries"].as_array().unwrap().len(), 2);
let total_cost = json["totalCost"].as_f64().unwrap();
assert!(
(total_cost - 0.10).abs() < 1e-9,
"unexpected totalCost without pricing: {total_cost}"
);
assert_eq!(total_cost, 0.0);
}

#[test]
Expand All @@ -1817,10 +1814,7 @@ fn test_monthly_json_offline_without_pricing_cache_still_succeeds() {
assert_eq!(entries[0]["month"].as_str().unwrap(), "2024-06");
assert_eq!(entries[1]["month"].as_str().unwrap(), "2025-01");
let total_cost = json["totalCost"].as_f64().unwrap();
assert!(
(total_cost - 0.10).abs() < 1e-9,
"unexpected totalCost without pricing: {total_cost}"
);
assert_eq!(total_cost, 0.0);
}

#[test]
Expand All @@ -1841,10 +1835,7 @@ fn test_graph_offline_without_pricing_cache_still_succeeds() {
assert_eq!(json["summary"]["activeDays"].as_i64().unwrap(), 2);
assert_eq!(json["contributions"].as_array().unwrap().len(), 2);
let total_cost = json["summary"]["totalCost"].as_f64().unwrap();
assert!(
(total_cost - 0.10).abs() < 1e-9,
"unexpected totalCost without pricing: {total_cost}"
);
assert_eq!(total_cost, 0.0);
}

#[test]
Expand Down Expand Up @@ -1894,10 +1885,7 @@ fn test_hourly_json_offline_without_pricing_cache_still_succeeds() {
1000
);
let total_cost = json["totalCost"].as_f64().unwrap();
assert!(
(total_cost - 0.10).abs() < 1e-9,
"unexpected totalCost without pricing: {total_cost}"
);
assert_eq!(total_cost, 0.0);
}

#[test]
Expand Down
2 changes: 1 addition & 1 deletion crates/tokscale-core/src/adapters/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,6 @@ fn apply_pricing_to_messages(
) {
for message in messages {
message.refresh_derived_fields();
crate::apply_pricing_if_available(message, pricing);
crate::apply_token_pricing(message, pricing);
}
}
2 changes: 1 addition & 1 deletion crates/tokscale-core/src/adapters/codex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ fn apply_pricing_to_messages(
) {
for message in messages {
message.refresh_derived_fields();
crate::apply_pricing_if_available(message, pricing);
crate::apply_token_pricing(message, pricing);
}
}

Expand Down
3 changes: 1 addition & 2 deletions crates/tokscale-core/src/adapters/crush.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ use std::path::{Path, PathBuf};
use rayon::prelude::*;
use serde::Deserialize;

use crate::adapters::file::PricingPolicy;
use crate::adapters::{
AdapterScanContext, FoldContext, LocalSourceAdapter, MessageSink, ParseContext, ParsedUnit,
SourceUnit, SourceUnitMeta, UnitMessageSource,
Expand Down Expand Up @@ -52,7 +51,7 @@ impl LocalSourceAdapter for CrushAdapter {
};
for message in &mut messages {
message.set_workspace(workspace_key.clone(), workspace_label.clone());
PricingPolicy::ApplyAlways.apply(message, ctx.pricing);
crate::apply_token_pricing(message, ctx.pricing);
}
ParsedUnit {
unit,
Expand Down
40 changes: 3 additions & 37 deletions crates/tokscale-core/src/adapters/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,46 +21,14 @@ pub(crate) struct CachedFileAdapter {
parse: fn(&Path) -> Vec<UnifiedMessage>,
}

#[derive(Clone, Copy)]
pub(crate) enum PricingPolicy {
ApplyAlways,
ApplyIfCostNonPositive,
Never,
}

impl PricingPolicy {
pub(crate) fn apply(
self,
message: &mut UnifiedMessage,
pricing: Option<&crate::pricing::PricingService>,
) {
match self {
PricingPolicy::ApplyAlways => crate::apply_pricing_if_available(message, pricing),
PricingPolicy::ApplyIfCostNonPositive if message.cost <= 0.0 => {
crate::apply_pricing_if_available(message, pricing);
}
PricingPolicy::ApplyIfCostNonPositive | PricingPolicy::Never => {}
}
}
}

pub(crate) struct NonCachedFileAdapter {
client: ClientId,
parse: fn(&Path) -> Vec<UnifiedMessage>,
pricing_policy: PricingPolicy,
}

impl NonCachedFileAdapter {
pub(crate) const fn new(
client: ClientId,
parse: fn(&Path) -> Vec<UnifiedMessage>,
pricing_policy: PricingPolicy,
) -> Self {
Self {
client,
parse,
pricing_policy,
}
pub(crate) const fn new(client: ClientId, parse: fn(&Path) -> Vec<UnifiedMessage>) -> Self {
Self { client, parse }
}
}

Expand All @@ -75,13 +43,12 @@ impl LocalSourceAdapter for NonCachedFileAdapter {

fn parse(&self, units: Vec<SourceUnit>, ctx: &ParseContext<'_>) -> Vec<ParsedUnit> {
let parse = self.parse;
let pricing_policy = self.pricing_policy;
units
.into_par_iter()
.map(|unit| {
let mut messages = parse(&unit.path);
for message in &mut messages {
pricing_policy.apply(message, ctx.pricing);
crate::apply_token_pricing(message, ctx.pricing);
}
ParsedUnit {
unit,
Expand Down Expand Up @@ -267,7 +234,6 @@ pub(crate) static COMMANDCODE_ADAPTER: CachedFileAdapter = CachedFileAdapter::ne
pub(crate) static ANTIGRAVITY_ADAPTER: NonCachedFileAdapter = NonCachedFileAdapter::new(
ClientId::Antigravity,
sessions::antigravity::parse_antigravity_file,
PricingPolicy::ApplyAlways,
);

#[cfg(test)]
Expand Down
9 changes: 4 additions & 5 deletions crates/tokscale-core/src/adapters/gjc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ use std::path::PathBuf;
use rayon::prelude::*;

use crate::adapters::discover as adapter_discover;
use crate::adapters::file::PricingPolicy;
use crate::adapters::{
AdapterScanContext, FingerprintPolicy, FoldContext, LocalSourceAdapter, MessageSink,
ParseContext, ParsedUnit, SourceUnit, UnitMessageSource,
Expand Down Expand Up @@ -39,7 +38,7 @@ impl LocalSourceAdapter for GjcAdapter {
.map(|unit| {
let mut messages = sessions::gjc::parse_gjc_file(&unit.path);
for message in &mut messages {
PricingPolicy::ApplyIfCostNonPositive.apply(message, ctx.pricing);
crate::apply_token_pricing(message, ctx.pricing);
}
ParsedUnit {
unit,
Expand Down Expand Up @@ -132,7 +131,7 @@ mod tests {
}

#[test]
fn gjc_adapter_preserves_embedded_cost_and_prices_missing_cost() {
fn gjc_adapter_ignores_embedded_cost_and_applies_token_pricing() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("project/session.jsonl");
write_file(
Expand Down Expand Up @@ -170,7 +169,7 @@ mod tests {
.iter()
.find(|message| message.dedup_key == Some(sessions::dedup_hash_str("gjc_ses:missing")))
.unwrap();
assert_eq!(embedded.cost, 1.25);
assert!(missing.cost > 0.0);
assert_eq!(embedded.cost, 0.02);
assert_eq!(missing.cost, 0.02);
}
}
3 changes: 1 addition & 2 deletions crates/tokscale-core/src/adapters/goose.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ use std::path::PathBuf;
use rayon::prelude::*;

use crate::adapters::discover as adapter_discover;
use crate::adapters::file::PricingPolicy;
use crate::adapters::{
AdapterScanContext, FoldContext, LocalSourceAdapter, MessageSink, ParseContext, ParsedUnit,
SourceUnit, UnitMessageSource,
Expand Down Expand Up @@ -32,7 +31,7 @@ impl LocalSourceAdapter for GooseAdapter {
.map(|unit| {
let mut messages = sessions::goose::parse_goose_sqlite(&unit.path);
for message in &mut messages {
PricingPolicy::ApplyAlways.apply(message, ctx.pricing);
crate::apply_token_pricing(message, ctx.pricing);
}
ParsedUnit {
unit,
Expand Down
3 changes: 1 addition & 2 deletions crates/tokscale-core/src/adapters/hermes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ use std::collections::HashSet;
use rayon::prelude::*;

use crate::adapters::discover as adapter_discover;
use crate::adapters::file::PricingPolicy;
use crate::adapters::{
AdapterScanContext, FingerprintPolicy, FoldContext, LocalSourceAdapter, MessageSink,
ParseContext, ParsedUnit, SourceUnit, UnitMessageSource,
Expand Down Expand Up @@ -48,7 +47,7 @@ impl LocalSourceAdapter for HermesAdapter {
.map(|unit| {
let mut messages = sessions::hermes::parse_hermes_sqlite(&unit.path);
for message in &mut messages {
PricingPolicy::ApplyIfCostNonPositive.apply(message, ctx.pricing);
crate::apply_token_pricing(message, ctx.pricing);
}
ParsedUnit {
unit,
Expand Down
3 changes: 1 addition & 2 deletions crates/tokscale-core/src/adapters/kilo.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
use rayon::prelude::*;

use crate::adapters::discover as adapter_discover;
use crate::adapters::file::PricingPolicy;
use crate::adapters::{
AdapterScanContext, FoldContext, LocalSourceAdapter, MessageSink, ParseContext, ParsedUnit,
SourceUnit, UnitMessageSource,
Expand Down Expand Up @@ -39,7 +38,7 @@ impl LocalSourceAdapter for KiloAdapter {
.map(|unit| {
let mut messages = sessions::kilo::parse_kilo_sqlite(&unit.path);
for message in &mut messages {
PricingPolicy::ApplyAlways.apply(message, ctx.pricing);
crate::apply_token_pricing(message, ctx.pricing);
}
ParsedUnit {
unit,
Expand Down
3 changes: 1 addition & 2 deletions crates/tokscale-core/src/adapters/kiro.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ use rayon::prelude::*;

use crate::adapters::cache as adapter_cache;
use crate::adapters::discover as adapter_discover;
use crate::adapters::file::PricingPolicy;
use crate::adapters::{
AdapterScanContext, FingerprintPolicy, FoldContext, LocalSourceAdapter, MessageSink,
ParseContext, ParsedUnit, SourceUnit, SourceUnitMeta, UnitMessageSource,
Expand Down Expand Up @@ -64,7 +63,7 @@ impl LocalSourceAdapter for KiroAdapter {
SourceUnitMeta::KiroSqlite => {
let mut messages = sessions::kiro::parse_kiro_sqlite(&unit.path);
for message in &mut messages {
PricingPolicy::ApplyAlways.apply(message, ctx.pricing);
crate::apply_token_pricing(message, ctx.pricing);
}
ParsedUnit {
unit,
Expand Down
7 changes: 3 additions & 4 deletions crates/tokscale-core/src/adapters/trae.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
use rayon::prelude::*;

use crate::adapters::discover as adapter_discover;
use crate::adapters::file::PricingPolicy;
use crate::adapters::{
AdapterScanContext, FingerprintPolicy, FoldContext, LocalSourceAdapter, MessageSink,
ParseContext, ParsedUnit, SourceUnit, UnitMessageSource,
Expand Down Expand Up @@ -30,7 +29,7 @@ impl LocalSourceAdapter for TraeAdapter {
.map(|unit| {
let mut messages = sessions::trae::parse_trae_file("trae", &unit.path);
for message in &mut messages {
PricingPolicy::Never.apply(message, ctx.pricing);
crate::apply_token_pricing(message, ctx.pricing);
}
ParsedUnit {
unit,
Expand Down Expand Up @@ -86,7 +85,7 @@ mod tests {
}

#[test]
fn trae_adapter_dedupes_latest_session_and_never_reprices() {
fn trae_adapter_dedupes_latest_session_and_applies_token_pricing() {
let dir = tempfile::TempDir::new().unwrap();
let older = dir.path().join("older.json");
let newer = dir.path().join("newer.json");
Expand Down Expand Up @@ -123,6 +122,6 @@ mod tests {

assert_eq!(sink.len(), 1);
assert_eq!(sink[0].timestamp, 1_776_000_001_000);
assert_eq!(sink[0].cost, 0.2);
assert_eq!(sink[0].cost, 110.0);
}
}
Loading
Loading