Skip to content
Draft
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
43 changes: 37 additions & 6 deletions apps/desktop-tauri/src-tauri/src/commands/usage_spend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ pub struct UsageSpendRow {
pub thirty_day_estimate: Option<codexbar::spend_contract::LocalCostEstimate>,
pub seven_day_tokens: Option<u64>,
pub thirty_day_tokens: Option<u64>,
/// The token figure is a floor from an incomplete scan ("at least N").
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub seven_day_tokens_lower_bound: bool,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub thirty_day_tokens_lower_bound: bool,
pub currency: String,
pub source: String,
/// Included in the shared Overview spend denominator.
Expand Down Expand Up @@ -501,6 +506,7 @@ fn build_usage_spend_summary(
.unwrap_or_else(|| provider_id.clone());

let mut local_cost_estimates = None;
let mut token_lower_bounds = (false, false);
let spend = match provider_id.as_str() {
"codex" => SpendValues {
seven_day: codex_7_contract.known_cost_usd,
Expand Down Expand Up @@ -591,10 +597,17 @@ fn build_usage_spend_summary(
spend
}
"antigravity" => {
let seven = codexbar::providers::antigravity::local_sessions::summarize(7);
let thirty = codexbar::providers::antigravity::local_sessions::summarize(30);
use codexbar::providers::antigravity::local_sessions;
let seven = local_sessions::summarize(7);
let thirty = local_sessions::summarize(30);
// Upstream 0.64: the app refreshes unknown-model pricing in the
// background; a later read (provider refresh or Refresh) reprices.
if let Some(refresh) = local_sessions::background_pricing_refresh(&thirty) {
tauri::async_runtime::spawn(refresh);
}
let spend =
antigravity_spend_values(cached_spend(cached_snapshot), &seven, &thirty);
token_lower_bounds = (seven.lower_bound, thirty.lower_bound);
local_cost_estimates = Some((seven.cost_estimate, thirty.cost_estimate));
spend
}
Expand Down Expand Up @@ -629,6 +642,8 @@ fn build_usage_spend_summary(
thirty_day_estimate,
seven_day_tokens: spend.seven_day_tokens,
thirty_day_tokens: spend.thirty_day_tokens,
seven_day_tokens_lower_bound: token_lower_bounds.0,
thirty_day_tokens_lower_bound: token_lower_bounds.1,
currency,
source: spend.source,
included_in_overview: include_in_shared_overview(
Expand Down Expand Up @@ -714,10 +729,9 @@ fn antigravity_spend_values(

spend.seven_day = seven.total_usd();
spend.thirty_day = thirty.total_usd();
spend.seven_day_tokens =
(seven.coverage == LocalHistoryCoverage::Complete).then_some(seven.total_tokens);
spend.thirty_day_tokens =
(thirty.coverage == LocalHistoryCoverage::Complete).then_some(thirty.total_tokens);
// Exact for a complete scan, a floor for a lower bound, unknown otherwise.
spend.seven_day_tokens = seven.published_tokens();
spend.thirty_day_tokens = thirty.published_tokens();
if spend.thirty_day.is_some() {
spend.source = "local Antigravity history · API list-price estimate".to_string();
} else if thirty.cost_estimate.known_subtotal_usd.is_some() {
Expand Down Expand Up @@ -822,7 +836,9 @@ mod cache_key_tests {
unpriced,
..Default::default()
},
..Default::default()
},
..Default::default()
}
}

Expand Down Expand Up @@ -898,6 +914,21 @@ mod cache_key_tests {
assert!(spend.source.contains("known API list-price subtotal"));
}

#[test]
fn antigravity_lower_bound_history_publishes_floors_not_exact_totals() {
use codexbar::spend_contract::LocalHistoryCoverage;

let mut seven = local_history(100, LocalHistoryCoverage::Partial, Some(1.25), 0);
seven.lower_bound = true;
let withheld = local_history(0, LocalHistoryCoverage::Partial, None, 0);
let spend = antigravity_spend_values(cached_spend(None), &seven, &withheld);

assert_eq!(spend.seven_day, None);
assert_eq!(spend.seven_day_tokens, Some(100));
assert_eq!(spend.thirty_day, None);
assert_eq!(spend.thirty_day_tokens, None);
}

#[test]
fn antigravity_complete_empty_history_is_a_known_zero() {
use codexbar::spend_contract::LocalHistoryCoverage;
Expand Down
13 changes: 13 additions & 0 deletions apps/desktop-tauri/src/lib/usageSpendSharing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,19 @@ describe("usage spend sharing", () => {
expect(formatSpendMetric(null, 1_500, "USD", "tokens", 0.0125)).toMatch(/^≥.* known/);
});

it("marks a lower-bound token count with a floor sign and leaves exact counts bare", () => {
const floor = formatSpendMetric(null, 1_500, "USD", "tokens", 0.0125, undefined, true);
expect(floor).toContain(`≥${(1_500).toLocaleString()} tokens`);
expect(floor).toMatch(/^≥.* known/);
const exact = formatSpendMetric(null, 1_500, "USD", "tokens", null, undefined, false);
expect(exact).toBe(`${(1_500).toLocaleString()} tokens`);
});

it("keeps the localized subtotal label next to a lower-bound token count", () => {
const metric = formatSpendMetric(null, 500, "USD", "Token", 6, "{} (teilweise)", true);
expect(metric).toBe(`${formatUsd(6, "USD")} (teilweise) · ≥500 Token`);
});

it("uses the caller's localized subtotal label", () => {
const metric = formatSpendMetric(null, 500, "USD", "Token", 6, "{} (teilweise)");
expect(metric).toBe(`${formatUsd(6, "USD")} (teilweise) · 500 Token`);
Expand Down
8 changes: 7 additions & 1 deletion apps/desktop-tauri/src/lib/usageSpendSharing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ export function formatUsd(value: number | null | undefined, currency: string): s
/**
* Canonical "cost · tokens" cell for spend tables and share renders.
* `knownSubtotalTemplate` is the localized subtotal label; `{}` is the amount.
* `tokensAreLowerBound` marks a token floor from an incomplete scan ("≥N").
*/
export function formatSpendMetric(
cost: number | null | undefined,
Expand All @@ -135,6 +136,7 @@ export function formatSpendMetric(
tokenLabel: string,
knownSubtotal?: number | null,
knownSubtotalTemplate = "≥{} known",
tokensAreLowerBound = false,
): string {
const parts: string[] = [];
if (cost != null && Number.isFinite(cost)) {
Expand All @@ -143,7 +145,7 @@ export function formatSpendMetric(
parts.push(knownSubtotalTemplate.replace("{}", formatUsd(knownSubtotal, currency)));
}
if (tokens != null && Number.isFinite(tokens)) {
parts.push(`${Math.max(0, tokens).toLocaleString()} ${tokenLabel}`);
parts.push(`${tokensAreLowerBound ? "≥" : ""}${Math.max(0, tokens).toLocaleString()} ${tokenLabel}`);
}
return parts.length > 0 ? parts.join(" · ") : "—";
}
Expand Down Expand Up @@ -217,13 +219,17 @@ export function renderUsageSpendSharePng(summary: UsageSpendSummary, title: stri
row.currency,
"tokens",
row.sevenDayEstimate?.knownSubtotalUsd,
undefined,
row.sevenDayTokensLowerBound,
),
formatSpendMetric(
row.thirtyDay,
row.thirtyDayTokens,
row.currency,
"tokens",
row.thirtyDayEstimate?.knownSubtotalUsd,
undefined,
row.thirtyDayTokensLowerBound,
),
row.currency || "USD",
row.source,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ export default function UsageSpendTab(_props: TabProps) {
t("UsageSpendTokens"),
row.sevenDayEstimate?.knownSubtotalUsd,
t("UsageSpendKnownSubtotal"),
row.sevenDayTokensLowerBound,
)}
</td>
<td>
Expand All @@ -252,6 +253,7 @@ export default function UsageSpendTab(_props: TabProps) {
t("UsageSpendTokens"),
row.thirtyDayEstimate?.knownSubtotalUsd,
t("UsageSpendKnownSubtotal"),
row.thirtyDayTokensLowerBound,
)}
</td>
<td>{row.currency || "USD"}</td>
Expand Down
3 changes: 3 additions & 0 deletions apps/desktop-tauri/src/types/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,9 @@ export interface UsageSpendRow {
thirtyDayEstimate?: LocalCostEstimate;
sevenDayTokens?: number | null;
thirtyDayTokens?: number | null;
/** The token figure is a floor from an incomplete scan ("at least N"). */
sevenDayTokensLowerBound?: boolean;
thirtyDayTokensLowerBound?: boolean;
currency: string;
source: string;
includedInOverview: boolean;
Expand Down
4 changes: 3 additions & 1 deletion docs/CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ codexbar cost -p codex --remote user@mac-host
codexbar cost -p codex --format json --summary-only --provider-native-only --days 30
```

Claude/Codex costs come from local session logs. Antigravity exposes local **token history only** through `cost`; dollar cost remains unknown rather than becoming a false `$0`. Other providers may differ; do not assume upstream Cursor dashboard cost behavior unless implemented in this tree.
Claude/Codex costs come from local session logs. Antigravity reads supported local token history; known models receive API list-price estimates from the bundled price table or the models.dev pricing catalog, and unknown models stay unpriced rather than becoming a false `$0`. These estimates are not Antigravity charges or credit deductions. Other providers may differ; do not assume upstream Cursor dashboard cost behavior unless implemented in this tree.

Antigravity history that stopped short is never shown as exact. A scan whose decoded rows are trustworthy reports `tokensAreLowerBound` / `costIsLowerBound` (text: "at least N"); a scan cut off by a hard limit, or one whose sources contradict each other, is withheld and publishes no total. A later partial read does not replace an earlier complete read of the same window and roots within one process. Reading the history never waits on the network. When a recorded model has no known public price, the desktop Usage & Spend view and `serve /cost` start one bounded models.dev pricing refresh in the background and a later read picks up the new prices; `codexbar cost --provider antigravity --refresh` waits for that refresh and rescans, while a plain `codexbar cost` starts no download. The request carries no account identity or usage data, and a failure leaves the affected models unpriced. Empty or absent history never starts a pricing download.

`--remote` adds one separate native Codex report fetched through non-interactive SSH; overlapping local and remote histories are never combined. `--summary-only` emits the versioned, path-free JSON contract used by the remote comparison and accepts only `--provider codex --format json`. Both modes reject session grouping and other provider selections.

Expand Down
33 changes: 29 additions & 4 deletions rust/src/cli/cost.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ pub struct CostArgs {
/// Emit the versioned native Codex summary contract as JSON.
#[arg(long = "summary-only")]
pub summary_only: bool,

/// Antigravity only: when a recorded model has no known public price, wait
/// for one bounded models.dev pricing refresh and rescan. Without it the
/// CLI starts no pricing download.
#[arg(long)]
pub refresh: bool,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
Expand Down Expand Up @@ -169,9 +175,13 @@ pub async fn run(args: CostArgs) -> anyhow::Result<()> {
display_name: provider.display_name().to_string(),
summary: CostSummary::default(),
supported: true,
token_history: Some(crate::providers::antigravity::local_sessions::summarize(
args.days,
)),
token_history: Some(
crate::providers::antigravity::local_sessions::summarize_with_pricing_refresh(
args.days,
args.refresh,
)
.await,
),
});
}
ProviderId::Muse => {
Expand Down Expand Up @@ -375,6 +385,13 @@ fn print_text_output(results: &[CostResult], use_color: bool, days: u32, group_b
fn print_local_token_history(history: &crate::spend_contract::LocalTokenHistorySummary, days: u32) {
use crate::spend_contract::LocalHistoryCoverage;
match history.coverage {
LocalHistoryCoverage::Partial if history.lower_bound => {
println!(
" Tokens: at least {} (lower bound; scan stopped short)",
format_number(history.total_tokens)
);
println!(" Sessions: at least {}", history.session_count);
}
LocalHistoryCoverage::Complete if history.total_tokens == 0 => {
println!(" No token usage in the last {days} days (scan complete)");
}
Expand All @@ -395,7 +412,7 @@ fn print_local_token_history(history: &crate::spend_contract::LocalTokenHistoryS
history.cost_estimate.coverage.unpriced
);
} else {
println!(" Known API list-price subtotal: ${cost:.2} (history incomplete)");
println!(" Known API list-price subtotal: at least ${cost:.2} (history incomplete)");
}
} else {
println!(" Local token history; dollar costs unavailable");
Expand Down Expand Up @@ -639,6 +656,7 @@ mod tests {
session_count: 2,
coverage: LocalHistoryCoverage::Complete,
cost_estimate: Default::default(),
..Default::default()
},
30,
);
Expand All @@ -654,6 +672,7 @@ mod tests {
session_count: 1,
coverage: LocalHistoryCoverage::Partial,
cost_estimate: Default::default(),
..Default::default()
},
30,
);
Expand All @@ -677,7 +696,9 @@ mod tests {
estimated: 1,
..Default::default()
},
..Default::default()
},
..Default::default()
},
30,
);
Expand All @@ -704,7 +725,9 @@ mod tests {
estimated: 1,
..Default::default()
},
..Default::default()
},
..Default::default()
},
30,
);
Expand Down Expand Up @@ -754,7 +777,9 @@ mod tests {
unpriced: 1,
..Default::default()
},
..Default::default()
},
..Default::default()
},
30,
);
Expand Down
10 changes: 9 additions & 1 deletion rust/src/cli/serve/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,13 @@ pub async fn cost_response(provider: Option<&str>) -> String {
let mut results = Vec::new();
for provider_id in selection.as_list() {
if provider_id == ProviderId::Antigravity {
let history = crate::providers::antigravity::local_sessions::summarize(30);
use crate::providers::antigravity::local_sessions;
let history = local_sessions::summarize(30);
// Upstream 0.64 `serve` refreshes unknown-model pricing in the
// background; a later `/cost` read picks up the new prices.
if let Some(refresh) = local_sessions::background_pricing_refresh(&history) {
tokio::spawn(refresh);
}
results.push(crate::spend_contract::local_token_history_json(
"antigravity",
&history,
Expand Down Expand Up @@ -164,6 +170,7 @@ mod tests {
session_count: 1,
coverage: LocalHistoryCoverage::Complete,
cost_estimate: Default::default(),
..Default::default()
},
30,
);
Expand All @@ -178,6 +185,7 @@ mod tests {
session_count: 1,
coverage: LocalHistoryCoverage::Partial,
cost_estimate: Default::default(),
..Default::default()
},
30,
);
Expand Down
2 changes: 1 addition & 1 deletion rust/src/core/claude_routed_pricing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ pub fn models_dev_target(model: &str, normalized: String) -> Option<(&'static st
Some((provider, normalized))
}

fn models_dev_targets(model: &str, normalized: String) -> Vec<(&'static str, String)> {
pub(crate) fn models_dev_targets(model: &str, normalized: String) -> Vec<(&'static str, String)> {
let Some(primary) = models_dev_target(model, normalized) else {
return Vec::new();
};
Expand Down
7 changes: 7 additions & 0 deletions rust/src/core/cost_pricing/claude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,13 @@ impl CostUsagePricing {
claude_routed_pricing::models_dev_target(model, Self::normalize_claude_model(model))
}

/// models.dev `(provider, model)` entries the routed Claude resolver tries
/// for `model`, in order. A pricing refresh for unpriced history targets
/// exactly the entries a rescan prices from.
pub(crate) fn claude_models_dev_pricing_targets(model: &str) -> Vec<(&'static str, String)> {
claude_routed_pricing::models_dev_targets(model, Self::normalize_claude_model(model))
}

/// Calculate cost for Claude usage in USD
pub fn claude_cost_usd(
model: &str,
Expand Down
Loading