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
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@
- **Claude**: in Auto mode, a Claude CLI probe timeout now falls back to the retained CLI usage from the last 15 minutes, as other CLI failures already did, instead of failing the refresh outright. Without a retained result the timeout is reported as before.
- Settings providers: show the FLORA Workspace ID and Fireworks account slug fields, which were defined but never displayed, so they no longer need the `FLORA_WORKSPACE_ID` / `FIREWORKS_ACCOUNT_SLUG` environment variables.
- Localization: align the Raycast auto-import and cookie-disabled strings in ru-RU, uk-UA, tr-TR and zh-CN with each locale's existing cookie and installed-browser terminology.
- OpenAI API: each Admin refresh also reads the last hour of 1-minute usage from the Admin Usage API (input plus output tokens, cached input included, Batch API traffic kept out of limit headroom) and carries a Tokens/min grid with a reporting frontier, so buckets that arrive late show as pending instead of zero. The backend can also load 15-minute, 1-hour or 6-hour windows, read per-project rate-limit caps, and measure headroom against manual per-model organization limits stored in settings. The per-refresh request can be turned off, and a failure never affects the daily usage. The menu bar token comes in a later release (upstream #4495).
- OpenAI API: each Admin refresh also reads the last hour of 1-minute usage from the Admin Usage API (input plus output tokens, cached input included, Batch API traffic kept out of limit headroom) and carries a Tokens/min grid with a reporting frontier, so buckets that arrive late show as pending instead of zero. The backend can also load 15-minute, 1-hour or 6-hour windows, read per-project rate-limit caps, and measure headroom against manual per-model organization limits stored in settings. The per-refresh request can be turned off, and a failure never affects the daily usage (upstream #4495).
- OpenAI API: the card shows the last-hour Tokens/min grid (latest, peak, 5-minute average, reporting frontier, and pending minutes drawn as outlines), and Settings > Providers > OpenAI adds a Live Usage section with a refresh switch, a per-model organization limits editor, a 15-minute / 1-hour / 6-hour window with per-model rows, and the organization caps list, which stays hidden when a project id is saved (upstream #4495).
- OpenAI API: add **Tokens/min** to the Settings > Providers > OpenAI API menu and floating bar metric picker. With it selected, the tray tooltip and tray status row show the latest reported minute, for example `OpenAI API 49M/min`, or "Tokens/min unavailable" when Live usage has not reported a complete minute. The tray meter and floating bar keep the Automatic usage lane (upstream #4495).
- Tray: add a **Progress bars stacked** menu bar style that draws the session and weekly meters as two equal-height rows on the single tray icon, with the same windows and pace tint as **Meter bars**; older settings files keep their style (upstream #4436).
- Settings providers: show only enabled providers from the sidebar options menu, search by CLI alias (such as `openai` for Codex), restore the default provider order, and undo a drag reorder or a restore with Ctrl+Z or the options menu. Hidden providers keep their slots when the order changes (upstream #4440).
- Add **Color by provider** to Settings > Menu Bar (off by default): tints the tray usage bars and percent text with the shown provider's brand or accent color, keeps pace colors and the error colors unchanged, and falls back to the usual colors when a tint would be hard to see on the tray background (upstream #4321).
- OpenAI API: each Admin refresh also reads the last hour of 1-minute usage from the Admin Usage API (input plus output tokens, cached input included, Batch API traffic kept out of limit headroom) and carries a Tokens/min grid with a reporting frontier, so buckets that arrive late show as pending instead of zero. The backend can also load 15-minute, 1-hour or 6-hour windows, read per-project rate-limit caps, and measure headroom against manual per-model organization limits stored in settings. The per-refresh request can be turned off, and a failure never affects the daily usage. The chart, menu bar token and Settings controls come in a later release (upstream #4495).
- **Claude**: show the plan renewal or expiry date from claude.ai billing under the quota rows when your cookie session supplies it, and make saved limit-reset credits, Extra usage (including a prepaid balance), and plan dates independently selectable under Visible usage items. Existing settings keep all three visible; Admin API spend is unaffected (upstream #4324, #4484).
- Devin: use Devin's official transparent hexagon mark for the provider icon in Settings and the CLI dashboard, instead of the settings-gear badge and the letter fallback (upstream #4458).
- Kimi (China): use the current Kimi Desktop session from its Local Storage (`%APPDATA%\kimi-desktop`) for web usage and monthly membership enrichment, and check that session again before each request, so a signed-out, replaced, or expired Desktop token is never sent or recovered from an older record (upstream #4416, #3536).
Expand Down
13 changes: 13 additions & 0 deletions apps/desktop-tauri/scripts/proof-scenarios.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,19 @@
{ "expectCall": { "cmd": "codex_account_switch", "args": { "id": "proof-codex-personal" } } }
]
},
{
"name": "settings-openaiapi-tokens-per-minute-metric",
"query": "window=settings&tab=providers:openaiapi&theme=dark",
"size": [880, 620],
"steps": [
{ "expectText": ["OpenAI API", "Menu and floating bar metric", "Tokens/min"] },
{ "expectField": { "label": "Menu and floating bar metric", "value": "automatic" } },
{ "select": { "label": "Menu and floating bar metric", "value": "tokensPerMinute" } },
{ "expectCall": { "cmd": "update_settings", "args": { "patch": { "providerMetrics": { "openaiapi": "tokensPerMinute" } } } } },
{ "expectField": { "label": "Menu and floating bar metric", "value": "tokensPerMinute" } },
{ "screenshot": "settings-openaiapi-tokens-per-minute-metric" }
]
},
{
"name": "settings-menu-work-days",
"query": "window=settings&tab=menu&theme=dark",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,7 @@ fn metric_preference_label(pref: MetricPreference) -> &'static str {
MetricPreference::ExtraUsage => "extraUsage",
MetricPreference::MonthlyPlan => "monthlyPlan",
MetricPreference::Average => "average",
MetricPreference::TokensPerMinute => "tokensPerMinute",
}
}

Expand All @@ -470,6 +471,7 @@ pub(in crate::commands) fn parse_metric_preference(s: &str) -> Option<MetricPref
"extraUsage" | "extrausage" => Some(MetricPreference::ExtraUsage),
"monthlyPlan" | "monthlyplan" => Some(MetricPreference::MonthlyPlan),
"average" => Some(MetricPreference::Average),
"tokensPerMinute" | "tokensperminute" => Some(MetricPreference::TokensPerMinute),
_ => None,
}
}
4 changes: 4 additions & 0 deletions apps/desktop-tauri/src-tauri/src/commands/provider_detail.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ pub struct ProviderDetail {
/// allowance (upstream 0.70.0 #4072). Offers the Monthly Plan metric in
/// the settings metric picker.
pub monthly_plan_window_id: Option<&'static str>,
/// Offers the Tokens/min metric in the settings metric picker (upstream
/// 0.75.0 #4495).
pub tokens_per_minute_metric: bool,
/// Metric picker label for the primary lane when it is not a session
/// window (upstream `menuBarLayoutPrimaryLabel`).
pub primary_metric_label: Option<&'static str>,
Expand Down Expand Up @@ -143,6 +146,7 @@ pub(crate) fn build_provider_detail(
tertiary: None,
tertiary_label_key: metadata.tertiary_label_key,
monthly_plan_window_id: provider.monthly_plan_window_id(),
tokens_per_minute_metric: provider.reports_tokens_per_minute(),
primary_metric_label: provider.menu_bar_primary_label(),
extra_rate_windows: Vec::new(),
usage_items: Vec::new(),
Expand Down
76 changes: 70 additions & 6 deletions apps/desktop-tauri/src-tauri/src/tray_bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -413,14 +413,20 @@ pub fn update_tray_icon_and_tooltip(
let icon = Image::new_owned(rgba, w, h);
let _ = tray.set_icon(Some(icon));

let tooltip = build_tooltip(snapshots, settings.ui_language, settings.hide_personal_info);
let tooltip = build_tooltip(
snapshots,
&settings,
settings.ui_language,
settings.hide_personal_info,
);
let tooltip = crate::status_poller::with_incident_lines(app, &settings, tooltip);
let _ = tray.set_tooltip(Some(tooltip));
}

/// Build a compact multi-line tooltip string from provider snapshots.
fn build_tooltip(
snapshots: &[crate::commands::ProviderUsageSnapshot],
settings: &Settings,
lang: codexbar::settings::Language,
hide_personal_info: bool,
) -> String {
Expand All @@ -443,7 +449,10 @@ fn build_tooltip(
let short = truncate_tooltip_text(&err, 36);
format!("{}: {} ({})", s.display_name, error_label, short)
} else {
let label = crate::commands::compact_tray_status_label(headline_window(s), lang);
let label = crate::tray_presentation::tokens_per_minute_status(s, settings, lang)
.unwrap_or_else(|| {
crate::commands::compact_tray_status_label(headline_window(s), lang)
});
format!("{}: {}", s.display_name, truncate_tooltip_text(&label, 42))
};
lines.push(status);
Expand Down Expand Up @@ -643,6 +652,7 @@ mod tests {

let tooltip = build_tooltip(
&[claude, codex],
&Settings::default(),
codexbar::settings::Language::English,
false,
);
Expand All @@ -653,6 +663,37 @@ mod tests {
);
}

#[test]
fn tooltip_shows_tokens_per_minute_when_selected() {
let mut settings = Settings::default();
settings.set_provider_metric(
codexbar::core::ProviderId::OpenAIApi,
codexbar::settings::MetricPreference::TokensPerMinute,
);
let mut openai = fake_snapshot("openaiapi", "OpenAI API", 40.0);
openai.throughput = Some(crate::commands::ProviderThroughputSnapshot {
minutes: Vec::new(),
frontier: Some(1_700_000_000),
latest_tokens_per_minute: Some(1_500_000),
peak_tokens_per_minute: 1_500_000,
peak_at: None,
five_minute_average_tokens_per_minute: 0.0,
models: Vec::new(),
window_minutes: 60,
fetched_at: 1_700_000_060,
project_id: None,
});

let tooltip = build_tooltip(
&[openai, fake_snapshot("codex", "Codex", 8.0)],
&settings,
codexbar::settings::Language::English,
false,
);

assert_eq!(tooltip, "CodexBar\nOpenAI API: 1.5M/min\nCodex: 8%");
}

#[test]
fn tooltip_skips_informational_codex_primary() {
// Weekly-only Codex plan: the 5h lane is an informational placeholder,
Expand All @@ -663,7 +704,12 @@ mod tests {
codex.primary.reset_description = Some("No active 5h session".to_string());
codex.secondary.as_mut().unwrap().reset_description = Some("3d 17h".to_string());

let tooltip = build_tooltip(&[codex], codexbar::settings::Language::English, false);
let tooltip = build_tooltip(
&[codex],
&Settings::default(),
codexbar::settings::Language::English,
false,
);

assert_eq!(tooltip, "CodexBar\nCodex: 16% • Resets in 3d 17h");
}
Expand All @@ -674,7 +720,12 @@ mod tests {
claude.primary.reset_description =
Some("resets in Jun 10 at 3:00PM with extra noisy suffix".to_string());

let tooltip = build_tooltip(&[claude], codexbar::settings::Language::English, false);
let tooltip = build_tooltip(
&[claude],
&Settings::default(),
codexbar::settings::Language::English,
false,
);

let line = tooltip.lines().nth(1).expect("provider tooltip line");
assert!(line.starts_with("Claude: 13% • Resets in Jun 10 at 3:00PM"));
Expand All @@ -689,12 +740,18 @@ mod tests {

let shown = build_tooltip(
&[codex.clone()],
&Settings::default(),
codexbar::settings::Language::English,
false,
);
assert!(shown.contains("proof@example.com"), "{shown}");

let hidden = build_tooltip(&[codex], codexbar::settings::Language::English, true);
let hidden = build_tooltip(
&[codex],
&Settings::default(),
codexbar::settings::Language::English,
true,
);
assert!(hidden.contains("session expired"), "{hidden}");
assert!(!hidden.contains('@'), "{hidden}");
assert!(!hidden.contains("example.com"), "{hidden}");
Expand All @@ -705,7 +762,12 @@ mod tests {
let mut claude = fake_snapshot("claude", "Claude", 13.0);
claude.error = Some("network timeout".to_string());

let tooltip = build_tooltip(&[claude], codexbar::settings::Language::Japanese, false);
let tooltip = build_tooltip(
&[claude],
&Settings::default(),
codexbar::settings::Language::Japanese,
false,
);

assert!(tooltip.contains("エラー"), "{tooltip}");
assert!(!tooltip.contains(": error ("), "{tooltip}");
Expand All @@ -719,11 +781,13 @@ mod tests {

let english_tooltip = build_tooltip(
&[claude.clone()],
&Settings::default(),
codexbar::settings::Language::English,
false,
);
let japanese_tooltip = build_tooltip(
&[claude.clone()],
&Settings::default(),
codexbar::settings::Language::Japanese,
false,
);
Expand Down
35 changes: 34 additions & 1 deletion apps/desktop-tauri/src-tauri/src/tray_presentation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -455,13 +455,46 @@ fn provider_status_label(
);
}

let label = crate::commands::compact_tray_status_label(headline_window(snapshot), language);
let label = tokens_per_minute_status(snapshot, settings, language).unwrap_or_else(|| {
crate::commands::compact_tray_status_label(headline_window(snapshot), language)
});
(
snapshot.provider_id.clone(),
format!("{} {}", snapshot.display_name, label),
)
}

/// Tray status text for the Tokens/min metric: the latest reported minute
/// ("12K/min"), or an explicit unavailable label when no minute has been
/// reported (Live usage off, no Admin key, or nothing complete yet). `None`
/// when the provider's metric is not Tokens/min or the provider cannot report
/// a rate, so a saved value never leaks onto another provider's row.
pub(crate) fn tokens_per_minute_status(
snapshot: &ProviderUsageSnapshot,
settings: &Settings,
language: Language,
) -> Option<String> {
let provider = ProviderId::from_cli_name(&snapshot.provider_id)?;
if !provider.reports_tokens_per_minute()
|| settings.get_provider_metric(provider) != MetricPreference::TokensPerMinute
{
return None;
}
Some(
match snapshot
.throughput
.as_ref()
.and_then(|throughput| throughput.latest_tokens_per_minute)
{
Some(tokens) => codexbar::core::tokens_per_minute_label(tokens),
None => codexbar::locale::get_text(
language,
codexbar::locale::LocaleKey::TrayTokensPerMinuteUnavailable,
),
},
)
}

/// Window that headline tray surfaces should label for a provider.
pub(crate) fn headline_window(snapshot: &ProviderUsageSnapshot) -> &RateWindowSnapshot {
if snapshot.provider_id == "codex" {
Expand Down
111 changes: 111 additions & 0 deletions apps/desktop-tauri/src-tauri/src/tray_presentation_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1272,3 +1272,114 @@ fn pick_tray_provider_none_when_empty() {
assert!(pick_tray_provider(&refs, true).is_none());
assert!(pick_tray_provider(&refs, false).is_none());
}

fn openai_api_throughput(latest: Option<u64>) -> crate::commands::ProviderThroughputSnapshot {
crate::commands::ProviderThroughputSnapshot {
minutes: Vec::new(),
frontier: latest.map(|_| 1_700_000_000),
latest_tokens_per_minute: latest,
peak_tokens_per_minute: latest.unwrap_or(0),
peak_at: None,
five_minute_average_tokens_per_minute: 0.0,
models: Vec::new(),
window_minutes: 60,
fetched_at: 1_700_000_060,
project_id: None,
}
}

#[test]
fn tokens_per_minute_metric_labels_status_with_the_latest_rate() {
let mut settings = Settings {
tray_icon_mode: TrayIconMode::PerProvider,
show_as_used: true,
..Settings::default()
};
settings.set_provider_metric(ProviderId::OpenAIApi, MetricPreference::TokensPerMinute);
let mut openai = fake_snapshot("openaiapi", "OpenAI API", 40.0);
openai.throughput = Some(openai_api_throughput(Some(49_000_000)));
let snapshots = vec![openai.clone(), fake_snapshot("codex", "Codex", 30.0)];

let plan = TrayPresentationPlan::resolve(&settings, &snapshots, None);
let labels = plan.status_labels(Language::English);

assert!(
labels.contains(&("openaiapi".to_string(), "OpenAI API 49M/min".to_string())),
"{labels:?}"
);
// Other providers keep their usage label; the meter keeps a usage lane.
assert!(
labels.contains(&("codex".to_string(), "Codex 30%".to_string())),
"{labels:?}"
);

openai.throughput = Some(openai_api_throughput(Some(0)));
assert_eq!(
tokens_per_minute_status(&openai, &settings, Language::English).as_deref(),
Some("0/min")
);
}

#[test]
fn tokens_per_minute_metric_without_reported_minutes_is_unavailable() {
let mut settings = Settings::default();
settings.set_provider_metric(ProviderId::OpenAIApi, MetricPreference::TokensPerMinute);
let mut openai = fake_snapshot("openaiapi", "OpenAI API", 40.0);

assert_eq!(
tokens_per_minute_status(&openai, &settings, Language::English).as_deref(),
Some("Tokens/min unavailable")
);
openai.throughput = Some(openai_api_throughput(None));
assert_eq!(
tokens_per_minute_status(&openai, &settings, Language::English).as_deref(),
Some("Tokens/min unavailable")
);

// Any other metric leaves the usage label alone.
let automatic = Settings::default();
openai.throughput = Some(openai_api_throughput(Some(1_500)));
assert_eq!(
tokens_per_minute_status(&openai, &automatic, Language::English),
None
);
}

#[test]
fn tokens_per_minute_metric_on_a_provider_without_a_rate_falls_back() {
// A saved Tokens/min value must not leak onto a provider that cannot
// report a rate: the row keeps the Automatic usage label instead.
let mut settings = Settings::default();
settings.set_provider_metric(ProviderId::Codex, MetricPreference::TokensPerMinute);
let mut codex = fake_snapshot("codex", "Codex", 30.0);
codex.throughput = Some(openai_api_throughput(Some(12_000)));
assert_eq!(
tokens_per_minute_status(&codex, &settings, Language::English),
None
);
}

#[test]
fn tokens_per_minute_metric_keeps_the_automatic_icon_lane() {
let mut settings = Settings {
tray_icon_mode: TrayIconMode::Single,
show_as_used: true,
..Settings::default()
};
let mut openai = fake_snapshot("openaiapi", "OpenAI API", 40.0);
openai.throughput = Some(openai_api_throughput(Some(12_000)));
let snapshots = vec![openai];
let automatic_icon = TrayPresentationPlan::resolve(&settings, &snapshots, None).icon;

settings.set_provider_metric(ProviderId::OpenAIApi, MetricPreference::TokensPerMinute);
let plan = TrayPresentationPlan::resolve(&settings, &snapshots, None);

assert_eq!(plan.icon, automatic_icon);
assert_eq!(
plan.status_labels(Language::English),
vec![(
"status_summary".to_string(),
"OpenAI API 12K/min".to_string()
)]
);
}
Loading