Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
191 changes: 170 additions & 21 deletions apps/desktop-tauri/src-tauri/src/commands/chart.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ use codexbar::core::OpenAIDashboardCacheStore;
use codexbar::cost_scanner::{CostScanner, CostSummary, get_daily_cost_history};
use codexbar::locale::{self, LocaleKey};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::sync::{
Arc, Mutex, OnceLock,
atomic::{AtomicBool, Ordering},
};
use std::time::{Duration, Instant};
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};

const LOCAL_USAGE_TTL: Duration = Duration::from_secs(30);

Expand Down Expand Up @@ -53,6 +53,7 @@ pub struct ProviderLocalUsageSummary {
pub latest_tokens: Option<u64>,
pub top_model: Option<String>,
pub estimate_note: String,
pub token_cost_updated_at_ms: i64,
}

/// Full chart data bundle for one provider.
Expand Down Expand Up @@ -87,10 +88,12 @@ pub async fn get_provider_chart_data(
pub async fn get_provider_local_usage_summary(
provider_id: String,
) -> Option<ProviderLocalUsageSummary> {
let failure_provider_id = provider_id.clone();
tauri::async_runtime::spawn_blocking(move || load_provider_local_usage_summary(&provider_id))
.await
.unwrap_or_else(|err| {
tracing::warn!("Provider local usage worker failed: {}", err);
record_local_usage_fetch_failure(&failure_provider_id, CostFetchFailure::Failed);
None
})
}
Expand Down Expand Up @@ -165,29 +168,47 @@ fn load_local_usage_summary(
provider_id: &str,
cancel: Option<&AtomicBool>,
) -> Option<ProviderLocalUsageSummary> {
let thirty_day = scan_local_cost(provider_id, 30, cancel)?;
load_local_usage_summary_with_unknown_models(provider_id, cancel).0
}

fn load_local_usage_summary_with_unknown_models(
provider_id: &str,
cancel: Option<&AtomicBool>,
) -> (Option<ProviderLocalUsageSummary>, HashSet<String>) {
let Some(thirty_day) = scan_local_cost(provider_id, 30, cancel) else {
return (None, HashSet::new());
};
if cancel.is_some_and(|flag| flag.load(Ordering::Relaxed)) {
return None;
return (None, HashSet::new());
}
let today = scan_local_cost(provider_id, 1, cancel).unwrap_or_default();
let unknown_models = thirty_day
.unknown_models
.union(&today.unknown_models)
.cloned()
.collect();

let thirty_day_tokens = total_tokens(&thirty_day);
let latest_tokens = total_tokens(&today);
let has_usage =
thirty_day.sessions_count > 0 || thirty_day.total_cost_usd > 0.0 || thirty_day_tokens > 0;
if !has_usage {
return None;
return (None, unknown_models);
}

let lang = locale::current_language();
Some(ProviderLocalUsageSummary {
today_cost: non_zero_f64(today.total_cost_usd),
thirty_day_cost: non_zero_f64(thirty_day.total_cost_usd),
thirty_day_tokens: non_zero_u64(thirty_day_tokens),
latest_tokens: non_zero_u64(latest_tokens),
top_model: top_model(&thirty_day),
estimate_note: localized_estimate_note(provider_id, lang),
})
(
Some(ProviderLocalUsageSummary {
today_cost: non_zero_f64(today.total_cost_usd),
thirty_day_cost: non_zero_f64(thirty_day.total_cost_usd),
thirty_day_tokens: non_zero_u64(thirty_day_tokens),
latest_tokens: non_zero_u64(latest_tokens),
top_model: top_model(&thirty_day),
estimate_note: localized_estimate_note(provider_id, lang),
token_cost_updated_at_ms: current_unix_ms(),
}),
unknown_models,
)
}

pub(crate) fn load_provider_local_usage_summary(
Expand Down Expand Up @@ -228,15 +249,47 @@ pub(crate) async fn refresh_provider_local_usage_cache(provider_ids: Vec<String>
return;
}

if let Err(err) = tauri::async_runtime::spawn_blocking(move || {
for provider_id in provider_ids {
let summary = load_local_usage_summary(&provider_id, None);
store_local_usage_summary(&provider_id, summary);
}
let failure_provider_ids = provider_ids.clone();
let scans = match tauri::async_runtime::spawn_blocking(move || {
provider_ids
.into_iter()
.map(|provider_id| {
let (summary, unknown_models) =
load_local_usage_summary_with_unknown_models(&provider_id, None);
(provider_id, summary, unknown_models)
})
.collect::<Vec<_>>()
})
.await
{
tracing::warn!("Provider local usage refresh worker failed: {err}");
Ok(scans) => scans,
Err(err) => {
tracing::warn!("Provider local usage refresh worker failed: {err}");
for provider_id in failure_provider_ids {
record_local_usage_fetch_failure(&provider_id, CostFetchFailure::Failed);
}
return;
}
};

for (provider_id, mut summary, unknown_models) in scans {
let pricing_provider = match provider_id.as_str() {
"codex" => Some("openai"),
"claude" => Some("anthropic"),
_ => None,
};
if let Some(pricing_provider) = pricing_provider
&& codexbar::core::refresh_unknown_models_if_needed(pricing_provider, &unknown_models)
.await
{
let rescan_provider = provider_id.clone();
summary = tauri::async_runtime::spawn_blocking(move || {
load_local_usage_summary(&rescan_provider, None)
})
.await
.unwrap_or(summary);
}
store_local_usage_summary(&provider_id, summary);
}
}

Expand All @@ -255,7 +308,7 @@ fn load_local_usage_summary_cached(
let cache = local_usage_cache();
if let Ok(guard) = cache.lock()
&& let Some(entry) = guard.get(provider_id)
&& entry.loaded_at.elapsed() <= LOCAL_USAGE_TTL
&& token_cost_cache_is_fresh(Some(entry.loaded_at), Instant::now(), LOCAL_USAGE_TTL)
{
return entry.summary.clone();
}
Expand Down Expand Up @@ -285,6 +338,52 @@ fn store_local_usage_summary(provider_id: &str, summary: Option<ProviderLocalUsa
}
}

fn record_local_usage_fetch_failure(provider_id: &str, failure: CostFetchFailure) {
let loaded_at = if cost_fetch_failure_allows_early_retry(failure) {
Instant::now() - LOCAL_USAGE_TTL - Duration::from_secs(1)
} else {
Instant::now()
};
if let Ok(mut guard) = local_usage_cache().lock() {
guard.insert(
provider_id.to_string(),
CachedLocalUsage {
loaded_at,
summary: None,
},
);
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)]
pub(crate) enum CostFetchFailure {
Failed,
TimedOut,
}

pub(crate) fn token_cost_cache_is_fresh(
loaded_at: Option<Instant>,
now: Instant,
ttl: Duration,
) -> bool {
loaded_at
.and_then(|loaded| now.checked_duration_since(loaded))
.map(|age| age <= ttl)
.unwrap_or(false)
}

pub(crate) fn cost_fetch_failure_allows_early_retry(failure: CostFetchFailure) -> bool {
!matches!(failure, CostFetchFailure::TimedOut)
}

fn current_unix_ms() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_millis().min(i64::MAX as u128) as i64)
.unwrap_or(0)
}

fn localized_estimate_note(provider_id: &str, lang: codexbar::settings::Language) -> String {
match provider_id {
"claude" => locale::get_text(lang, LocaleKey::PanelEstimatedFromLocalLogsClaude),
Expand Down Expand Up @@ -392,8 +491,58 @@ fn load_openai_dashboard_chart_data(

#[cfg(test)]
mod tests {
use super::localized_estimate_note;
use super::{
CostFetchFailure, ProviderLocalUsageSummary, cost_fetch_failure_allows_early_retry,
localized_estimate_note, token_cost_cache_is_fresh,
};
use crate::commands::is_provider_cache_fresh;
use codexbar::settings::Language;
use std::time::{Duration, Instant};

#[test]
fn token_cost_age_does_not_use_provider_quota_age() {
let now = Instant::now();
let token_loaded = now - Duration::from_secs(31);
let provider_updated = now;
assert!(!token_cost_cache_is_fresh(
Some(token_loaded),
now,
Duration::from_secs(30)
));
assert!(is_provider_cache_fresh(
Some(provider_updated),
Duration::from_secs(30)
));
}

#[test]
fn fast_cost_failures_allow_the_next_pass_to_retry() {
assert!(cost_fetch_failure_allows_early_retry(
CostFetchFailure::Failed
));
assert!(!cost_fetch_failure_allows_early_retry(
CostFetchFailure::TimedOut
));
}

#[test]
fn local_usage_summary_serializes_token_cost_timestamp() {
let summary = ProviderLocalUsageSummary {
today_cost: Some(1.0),
thirty_day_cost: Some(2.0),
thirty_day_tokens: Some(300),
latest_tokens: Some(40),
top_model: Some("gpt-5".to_string()),
estimate_note: "estimated".to_string(),
token_cost_updated_at_ms: 1234,
};

let json = serde_json::to_value(summary).expect("serialize summary");
assert_eq!(
json.get("tokenCostUpdatedAtMs").and_then(|v| v.as_i64()),
Some(1234)
);
}

#[test]
fn japanese_estimate_note_is_localized() {
Expand Down
1 change: 1 addition & 0 deletions apps/desktop-tauri/src-tauri/src/powertoys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ mod tests {
latest_tokens: Some(1_200),
top_model: Some("gpt-5".to_string()),
estimate_note: "cached".to_string(),
token_cost_updated_at_ms: 1234,
}),
);

Expand Down
1 change: 1 addition & 0 deletions apps/desktop-tauri/src/components/MenuCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ describe("MenuCard", () => {
latestTokens: null,
topModel: "glim-4.6",
estimateNote: "Estimated from local logs",
tokenCostUpdatedAtMs: 1234,
},
});
eventMocks.listen.mockResolvedValue(() => {});
Expand Down
1 change: 1 addition & 0 deletions apps/desktop-tauri/src/floatbar/FloatBar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ describe("FloatBar", () => {
latestTokens: 200,
topModel: "gpt-5",
estimateNote: "Estimated from local logs",
tokenCostUpdatedAtMs: 1234,
});

renderFloatBar(bootstrap());
Expand Down
1 change: 1 addition & 0 deletions apps/desktop-tauri/src/surfaces/TrayPanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ describe("TrayPanel provider grid", () => {
latestTokens: 1200,
topModel: "gpt-5.5",
estimateNote: "Estimated from local logs",
tokenCostUpdatedAtMs: 1234,
},
});

Expand Down
1 change: 1 addition & 0 deletions apps/desktop-tauri/src/types/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,7 @@ export interface ProviderLocalUsageSummary {
latestTokens: number | null;
topModel: string | null;
estimateNote: string;
tokenCostUpdatedAtMs: number;
}

export interface ProviderChartData {
Expand Down
Loading
Loading