Skip to content
Open
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
36 changes: 36 additions & 0 deletions rust/src/cli/usage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,15 @@ fn append_cost_line(lines: &mut Vec<String>, cost: Option<&CostSnapshot>) {
return;
};

// Provider-supplied OpenRouter Activity is a completed reporting window,
// rather than the ordinary current-cost meter. Keep its source period and
// known zero visible in text output without adding a second generic cost
// line. The daily points remain available in the JSON cost payload.
if cost.limit.is_none() && cost.period == "Last 30 days (UTC)" {
lines.push(format!(" {}: {}", cost.period, cost.format_used()));
return;
}

if let Some(limit) = cost.format_limit() {
lines.push(format!(
" Cost: {} / {} ({})",
Expand Down Expand Up @@ -972,4 +981,31 @@ mod tests {
assert!(output.contains("Plan: Gemini Code Assist in Google One AI Pro"));
assert!(!output.contains("Google One Ai Pro"));
}

#[test]
fn openrouter_history_preserves_period_and_known_zero_in_text() {
let result = fetch_result(UsageSnapshot::new(RateWindow::new(0.0)))
.with_cost(CostSnapshot::new(0.0, "USD", "Last 30 days (UTC)"));

let output = render_text_with_status(ProviderId::OpenRouter, &result, None, false);

assert!(output.contains("Last 30 days (UTC): $0.00"));
assert!(!output.contains("Cost: $0.00"));

let json = render_json_result(ProviderId::OpenRouter, result, None);
assert!(json.get("usage").is_some());
assert!(json.get("cost").is_some());
assert!(json.get("history").is_none());
}

#[test]
fn ordinary_costs_keep_the_existing_cost_line() {
let result = fetch_result(UsageSnapshot::new(RateWindow::new(0.0)))
.with_cost(CostSnapshot::new(2.5, "EUR", "This month (API key)"));

let output = render_text_with_status(ProviderId::OpenRouter, &result, None, false);

assert!(output.contains("Cost: €2.50 (This month (API key))"));
assert!(!output.contains("Last 30 days"));
}
}
58 changes: 51 additions & 7 deletions rust/src/providers/openrouter/activity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use serde_json::Value;
use crate::core::{CostDailyPoint, CostSnapshot, ProviderError};

const MAX_ACTIVITY_ROWS: usize = 20_000;
const MAX_SAFE_INTEGER: u64 = 9_007_199_254_740_991;

pub(super) fn parse_activity_cost(
payloads: &[Value],
Expand Down Expand Up @@ -57,7 +58,12 @@ pub(super) fn parse_activity_cost(
"OpenRouter activity.data[{index}].date must be a real calendar date"
))
})?;
if parsed_day > latest_completed || parsed_day < cutoff {
if parsed_day > latest_completed {
return Err(ProviderError::Parse(format!(
"OpenRouter activity.data[{index}].date must be a completed UTC day"
)));
}
if parsed_day < cutoff {
continue;
}
let model = object
Expand All @@ -78,9 +84,12 @@ pub(super) fn parse_activity_cost(
Some(Value::Null) | None => 0,
value => nonnegative_integer(value, index, "reasoning_tokens")?,
};
if reasoning > completion {
if prompt
.checked_add(completion)
.is_none_or(|total| total > MAX_SAFE_INTEGER)
{
return Err(ProviderError::Parse(format!(
"OpenRouter activity.data[{index}].reasoning_tokens exceeds completion_tokens"
"OpenRouter activity.data[{index}] token total overflowed"
)));
}
let requests = nonnegative_integer(object.get("requests"), index, "requests")?;
Expand Down Expand Up @@ -122,6 +131,11 @@ pub(super) fn parse_activity_cost(
continue;
}
seen.insert(identity, signature);
if seen.len() > 10_000 {
return Err(ProviderError::Parse(
"OpenRouter activity.data exceeds 10000 distinct rows".into(),
));
}
total += cost;
*daily.entry(day.to_string()).or_default() += cost;
}
Expand Down Expand Up @@ -178,11 +192,17 @@ fn nonnegative_integer(
"OpenRouter activity.data[{index}].{field} is missing"
))
})?;
value.as_u64().ok_or_else(|| {
let value = value.as_u64().ok_or_else(|| {
ProviderError::Parse(format!(
"OpenRouter activity.data[{index}].{field} must be a nonnegative integer"
))
})
})?;
if value > MAX_SAFE_INTEGER {
return Err(ProviderError::Parse(format!(
"OpenRouter activity.data[{index}].{field} must be a nonnegative safe integer"
)));
}
Ok(value)
}

fn nonnegative_number(
Expand Down Expand Up @@ -225,6 +245,19 @@ mod tests {
assert_eq!(cost.period, "Last 30 days (UTC)");
}

#[test]
fn preserves_reasoning_tokens_when_they_exceed_completion_tokens() {
let payload = serde_json::json!({"data":[
{"date":"2026-08-21","model":"reasoning-model","prompt_tokens":10,
"completion_tokens":2,"reasoning_tokens":8,"requests":1,"usage":1.0}
]});

let cost = parse_activity_cost(&[payload], now()).unwrap();

assert_eq!(cost.used, 1.0);
assert_eq!(cost.daily.len(), 1);
}

#[test]
fn rejects_conflicting_duplicate_activity_rows() {
let a = serde_json::json!({"data":[
Expand All @@ -240,8 +273,7 @@ mod tests {
fn filters_rows_outside_exact_30_day_window() {
let payload = serde_json::json!({"data":[
{"date":"2026-07-22","model":"old","prompt_tokens":10,"completion_tokens":5,"requests":1,"usage":99.0},
{"date":"2026-07-23","model":"in","prompt_tokens":10,"completion_tokens":5,"requests":1,"usage":1.0},
{"date":"2026-08-22","model":"today","prompt_tokens":10,"completion_tokens":5,"requests":1,"usage":99.0}
{"date":"2026-07-23","model":"in","prompt_tokens":10,"completion_tokens":5,"requests":1,"usage":1.0}
]});
let cost = parse_activity_cost(&[payload], now()).unwrap();
assert_eq!(cost.used, 1.0);
Expand All @@ -268,4 +300,16 @@ mod tests {
assert!(parse_activity_cost(&[payload], now()).is_err());
}
}

#[test]
fn rejects_activity_rows_from_an_incomplete_utc_day() {
let payload = serde_json::json!({"data":[
{"date":"2026-08-22","model":"today","prompt_tokens":10,
"completion_tokens":5,"requests":1,"usage":1.0}
]});

let error = parse_activity_cost(&[payload], now()).unwrap_err();

assert!(error.to_string().contains("completed UTC day"));
}
}
Loading