From 67ec2aa5183d33e7a499714919e0cc500557d626 Mon Sep 17 00:00:00 2001 From: RCD <90105158+Finesssee@users.noreply.github.com> Date: Tue, 29 Sep 2026 22:24:35 +0700 Subject: [PATCH 1/4] Port upstream 0.69.0: mark Kimi shorter windows blocked by an exhausted monthly limit --- .../src-tauri/src/auto_resume_tests.rs | 1 + .../src-tauri/src/commands/bridge.rs | 34 +++- .../src-tauri/src/commands/providers.rs | 1 + .../src-tauri/src/commands/tests.rs | 33 ++++ apps/desktop-tauri/src-tauri/src/powertoys.rs | 1 + .../src-tauri/src/tray_bridge.rs | 3 + .../src-tauri/src/tray_presentation_tests.rs | 4 + .../src-tauri/src/usage_metric.rs | 1 + .../src/components/MenuCard.test.tsx | 50 ++++++ .../src/components/MenuCardDetails.tsx | 31 +++- apps/desktop-tauri/src/i18n/keys.ts | 1 + apps/desktop-tauri/src/types/bridge.ts | 2 + rust/src/core/blocking_quota.rs | 161 ++++++++++++++++++ rust/src/core/mod.rs | 2 + rust/src/core/provider.rs | 9 + rust/src/locale.rs | 1 + rust/src/locale/en-US.ftl | 1 + rust/src/providers/kimi/mod.rs | 5 +- 18 files changed, 328 insertions(+), 13 deletions(-) create mode 100644 rust/src/core/blocking_quota.rs diff --git a/apps/desktop-tauri/src-tauri/src/auto_resume_tests.rs b/apps/desktop-tauri/src-tauri/src/auto_resume_tests.rs index 07b289498f..23498d1ee7 100644 --- a/apps/desktop-tauri/src-tauri/src/auto_resume_tests.rs +++ b/apps/desktop-tauri/src-tauri/src/auto_resume_tests.rs @@ -42,6 +42,7 @@ mod tests { reserve_description: None, reserve_will_last_to_reset: false, reserve_eta_seconds: None, + blocked_by_monthly_limit: false, } } diff --git a/apps/desktop-tauri/src-tauri/src/commands/bridge.rs b/apps/desktop-tauri/src-tauri/src/commands/bridge.rs index a69be60f1c..acff1f93d8 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/bridge.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/bridge.rs @@ -3,6 +3,7 @@ mod status; pub(crate) use status::{compact_tray_status_label, friendly_provider_error}; use super::*; +use codexbar::core::BlockedWindows; // ── Bridge snapshot types ──────────────────────────────────────────── @@ -31,6 +32,10 @@ pub struct RateWindowSnapshot { pub reserve_will_last_to_reset: bool, #[serde(default)] pub reserve_eta_seconds: Option, + /// A longer exhausted pool (Kimi's monthly membership) blocks this window. + /// Presentation-only: the raw percentages above stay the provider's data. + #[serde(default)] + pub blocked_by_monthly_limit: bool, } /// Serde default for [`RateWindowSnapshot::remaining_percent`] — the common @@ -53,9 +58,15 @@ impl RateWindowSnapshot { reserve_description: None, reserve_will_last_to_reset: false, reserve_eta_seconds: None, + blocked_by_monthly_limit: false, } } + fn with_blocked(mut self, blocked: bool) -> Self { + self.blocked_by_monthly_limit = blocked; + self + } + /// Enrich with raw reserve info derived from pace analysis. /// delta_percent = actual - expected; negative means ahead (in reserve). /// Only meaningful for longer windows (weekly); skip if reserve rounds to 0. @@ -355,10 +366,12 @@ impl ProviderUsageSnapshot { }); let secondary_pace = secondary_pace.flatten(); - let primary_snap = RateWindowSnapshot::from_rate_window(&usage.primary); + let blocked = BlockedWindows::evaluate(id, usage, chrono::Utc::now()); + let primary_snap = + RateWindowSnapshot::from_rate_window(&usage.primary).with_blocked(blocked.primary); let secondary_snap = usage.secondary.as_ref().map(|sw| { - let mut s = RateWindowSnapshot::from_rate_window(sw); + let mut s = RateWindowSnapshot::from_rate_window(sw).with_blocked(blocked.secondary); if let Some(ref p) = secondary_pace { s = s.with_pace_reserve(p); } @@ -394,14 +407,13 @@ impl ProviderUsageSnapshot { .clone() .unwrap_or_else(|| metadata.weekly_label.to_string()) }), - model_specific: usage - .model_specific - .as_ref() - .map(RateWindowSnapshot::from_rate_window), + model_specific: usage.model_specific.as_ref().map(|w| { + RateWindowSnapshot::from_rate_window(w).with_blocked(blocked.model_specific) + }), tertiary: usage .tertiary .as_ref() - .map(RateWindowSnapshot::from_rate_window), + .map(|w| RateWindowSnapshot::from_rate_window(w).with_blocked(blocked.tertiary)), // F5 (upstream 0.48.0): label the tertiary lane by its duration cadence // so surfaces (MenuCard, CLI, tray) can show "Monthly" instead of the // generic "DetailWindowTertiary" slot key. @@ -416,10 +428,12 @@ impl ProviderUsageSnapshot { extra_rate_windows: usage .extra_rate_windows .iter() - .map(|extra| NamedRateWindowSnapshot { + .zip(&blocked.extra) + .map(|(extra, &is_blocked)| NamedRateWindowSnapshot { id: extra.id.clone(), title: extra.title.clone(), - window: RateWindowSnapshot::from_rate_window(&extra.window), + window: RateWindowSnapshot::from_rate_window(&extra.window) + .with_blocked(is_blocked), fallback_lane: extra.fallback_lane, }) .collect(), @@ -518,6 +532,7 @@ impl ProviderUsageSnapshot { reserve_description: None, reserve_will_last_to_reset: false, reserve_eta_seconds: None, + blocked_by_monthly_limit: false, }, primary_label: Some(metadata.session_label.to_string()), secondary: None, @@ -1017,6 +1032,7 @@ mod tests { reserve_description: None, reserve_will_last_to_reset: false, reserve_eta_seconds: None, + blocked_by_monthly_limit: false, } } diff --git a/apps/desktop-tauri/src-tauri/src/commands/providers.rs b/apps/desktop-tauri/src-tauri/src/commands/providers.rs index 779f9c801c..07aef3da88 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/providers.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/providers.rs @@ -1341,6 +1341,7 @@ mod reset_backfill_tests { reserve_description: None, reserve_will_last_to_reset: false, reserve_eta_seconds: None, + blocked_by_monthly_limit: false, } } diff --git a/apps/desktop-tauri/src-tauri/src/commands/tests.rs b/apps/desktop-tauri/src-tauri/src/commands/tests.rs index 94ac348f11..f1d1511eaf 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/tests.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/tests.rs @@ -1138,6 +1138,39 @@ fn provider_inventory_maps_to_the_bridge_without_token_ids() { assert!(!serialized.contains("coupon-token-secret")); } +#[test] +fn exhausted_kimi_monthly_pool_marks_shorter_windows_without_changing_raw_usage() { + use codexbar::core::{RateWindow, UsageSnapshot}; + + let reset = chrono::Utc::now() + chrono::Duration::days(20); + let usage = UsageSnapshot::new(RateWindow::with_details(0.0, Some(10_080), None, None)) + .with_extra_rate_window( + "kimi-monthly", + "Total usage", + RateWindow::with_details(100.0, Some(43_200), Some(reset), None), + ); + let result = ProviderFetchResult::new(usage, "web"); + let metadata = instantiate_provider(ProviderId::Kimi).metadata().clone(); + let snapshot = + ProviderUsageSnapshot::from_fetch_result(ProviderId::Kimi, &metadata, &result, None); + + assert!(snapshot.primary.blocked_by_monthly_limit); + assert_eq!(snapshot.primary.used_percent, 0.0); + assert!( + !snapshot.extra_rate_windows[0] + .window + .blocked_by_monthly_limit + ); + let json = serde_json::to_value(&snapshot).unwrap(); + assert_eq!(json["primary"]["blockedByMonthlyLimit"], true); + + // Other providers never derive a blocker from a same-named window. + let metadata = instantiate_provider(ProviderId::Codex).metadata().clone(); + let snapshot = + ProviderUsageSnapshot::from_fetch_result(ProviderId::Codex, &metadata, &result, None); + assert!(!snapshot.primary.blocked_by_monthly_limit); +} + #[test] fn provider_cache_is_fresh_inside_stale_window() { assert!(super::is_provider_cache_fresh( diff --git a/apps/desktop-tauri/src-tauri/src/powertoys.rs b/apps/desktop-tauri/src-tauri/src/powertoys.rs index 99012ae7c3..2f4249944b 100644 --- a/apps/desktop-tauri/src-tauri/src/powertoys.rs +++ b/apps/desktop-tauri/src-tauri/src/powertoys.rs @@ -178,6 +178,7 @@ mod tests { reserve_percent: None, reserve_description: None, reserve_eta_seconds: None, + blocked_by_monthly_limit: false, reserve_will_last_to_reset: false, } } diff --git a/apps/desktop-tauri/src-tauri/src/tray_bridge.rs b/apps/desktop-tauri/src-tauri/src/tray_bridge.rs index b656562b17..1083ca2d79 100644 --- a/apps/desktop-tauri/src-tauri/src/tray_bridge.rs +++ b/apps/desktop-tauri/src-tauri/src/tray_bridge.rs @@ -824,6 +824,7 @@ mod tests { reserve_description: None, reserve_will_last_to_reset: false, reserve_eta_seconds: None, + blocked_by_monthly_limit: false, }, primary_label: None, secondary: secondary_percent.map(|pct| crate::commands::RateWindowSnapshot { @@ -838,6 +839,7 @@ mod tests { reserve_description: None, reserve_will_last_to_reset: false, reserve_eta_seconds: None, + blocked_by_monthly_limit: false, }), secondary_label: None, model_specific: None, @@ -853,6 +855,7 @@ mod tests { reserve_description: None, reserve_will_last_to_reset: false, reserve_eta_seconds: None, + blocked_by_monthly_limit: false, }), tertiary_label: None, extra_rate_windows: Vec::new(), diff --git a/apps/desktop-tauri/src-tauri/src/tray_presentation_tests.rs b/apps/desktop-tauri/src-tauri/src/tray_presentation_tests.rs index 3fce3f03d7..008b3af19d 100644 --- a/apps/desktop-tauri/src-tauri/src/tray_presentation_tests.rs +++ b/apps/desktop-tauri/src-tauri/src/tray_presentation_tests.rs @@ -25,6 +25,7 @@ fn fake_snapshot_with( reserve_description: None, reserve_will_last_to_reset: false, reserve_eta_seconds: None, + blocked_by_monthly_limit: false, }; ProviderUsageSnapshot { @@ -375,6 +376,7 @@ fn fake_extra_window(percent: f64) -> crate::commands::NamedRateWindowSnapshot { reserve_description: None, reserve_will_last_to_reset: false, reserve_eta_seconds: None, + blocked_by_monthly_limit: false, }, } } @@ -596,6 +598,7 @@ fn claude_automatic_prefers_weekly_when_model_exhausted() { reserve_description: None, reserve_will_last_to_reset: false, reserve_eta_seconds: None, + blocked_by_monthly_limit: false, }); let (primary, _) = selected_tray_percents(&snapshot, &settings); @@ -639,6 +642,7 @@ fn automatic_picks_highest_among_model_and_extra_windows() { reserve_description: None, reserve_will_last_to_reset: false, reserve_eta_seconds: None, + blocked_by_monthly_limit: false, }); snapshot.extra_rate_windows.push(fake_extra_window(90.0)); diff --git a/apps/desktop-tauri/src-tauri/src/usage_metric.rs b/apps/desktop-tauri/src-tauri/src/usage_metric.rs index f6e8f5f532..2695ede1f8 100644 --- a/apps/desktop-tauri/src-tauri/src/usage_metric.rs +++ b/apps/desktop-tauri/src-tauri/src/usage_metric.rs @@ -267,6 +267,7 @@ fn derived_window(used_percent: f64, resets_at: Option) -> RateWindowSna reserve_description: None, reserve_will_last_to_reset: false, reserve_eta_seconds: None, + blocked_by_monthly_limit: false, } } diff --git a/apps/desktop-tauri/src/components/MenuCard.test.tsx b/apps/desktop-tauri/src/components/MenuCard.test.tsx index c8ec795546..076d5f0423 100644 --- a/apps/desktop-tauri/src/components/MenuCard.test.tsx +++ b/apps/desktop-tauri/src/components/MenuCard.test.tsx @@ -125,6 +125,7 @@ describe("MenuCard", () => { PanelNow: "now", PanelOneHour: "1h", PanelFiveHours: "5h", + PanelBlockedByMonthlyLimit: "Blocked by monthly limit", PanelOnPaceBudget: "On-pace budget", PanelReserveSuffix: "in reserve", PanelThirtyDayCost: "30d cost", @@ -585,6 +586,55 @@ describe("MenuCard", () => { expect(container.querySelector(".menu-metric__forecast")).not.toBeInTheDocument(); }); + it("marks windows blocked by an exhausted monthly pool without touching the pool row", async () => { + const future = new Date(Date.now() + 3 * 60 * 60 * 1000).toISOString(); + const snapshot = provider(null); + snapshot.providerId = "kimi"; + snapshot.displayName = "Kimi"; + snapshot.primary = { + ...rateWindow(0, { + windowMinutes: 7 * 24 * 60, + resetsAt: future, + reservePercent: 30, + }), + blockedByMonthlyLimit: true, + }; + snapshot.secondary = { + ...rateWindow(0, { windowMinutes: 5 * 60, resetsAt: future }), + blockedByMonthlyLimit: true, + }; + snapshot.extraRateWindows = [ + { + id: "kimi-monthly", + title: "Total usage", + window: rateWindow(100, { + windowMinutes: 30 * 24 * 60, + exhausted: true, + resetsAt: new Date(Date.now() + 20 * 24 * 60 * 60 * 1000).toISOString(), + }), + }, + ]; + + const { container } = renderCard(snapshot, { showAsUsed: true }); + + expect(await screen.findAllByText("Blocked by monthly limit")).toHaveLength(2); + // Both blocked rows read fully used with no reset, pace, or reserve text. + expect(screen.getAllByText("100% used")).toHaveLength(3); + expect(container.querySelectorAll(".menu-metric__reset")).toHaveLength(1); + expect(screen.queryByText(/in reserve/)).not.toBeInTheDocument(); + expect(screen.queryByText("On-pace budget")).not.toBeInTheDocument(); + expect(container.querySelectorAll(".menu-metric__exhausted")).toHaveLength(3); + }); + + it("keeps windows that are not blocked untouched", async () => { + const snapshot = provider(null, 0, {}); + snapshot.providerId = "kimi"; + renderCard(snapshot, { showAsUsed: true }); + + expect(await screen.findByText("0% used")).toBeInTheDocument(); + expect(screen.queryByText("Blocked by monthly limit")).not.toBeInTheDocument(); + }); + it("renders local token and cost totals after chart data loads", async () => { const { container } = renderCard(provider(null)); diff --git a/apps/desktop-tauri/src/components/MenuCardDetails.tsx b/apps/desktop-tauri/src/components/MenuCardDetails.tsx index 31634bd910..5202044563 100644 --- a/apps/desktop-tauri/src/components/MenuCardDetails.tsx +++ b/apps/desktop-tauri/src/components/MenuCardDetails.tsx @@ -287,6 +287,26 @@ function getMetricPaceView(snap: RateWindowSnapshot): MetricPaceView { return { kind: "none" }; } +/** + * A window blocked by an exhausted longer pool reads fully used with no reset, + * pace, or reserve of its own; the pool's row owns the reset. Raw provider + * percentages stay untouched in the snapshot (upstream 0.69.0 #4091). + */ +function toBlockedSnapshot(snap: RateWindowSnapshot): RateWindowSnapshot { + return { + ...snap, + usedPercent: 100, + remainingPercent: 0, + isExhausted: true, + resetsAt: null, + resetDescription: null, + reservePercent: null, + reserveDescription: null, + reserveWillLastToReset: false, + reserveEtaSeconds: null, + }; +} + type MetricRowDisplay = { resetTimeRelative: boolean; showResetWhenExhausted?: boolean; @@ -304,7 +324,7 @@ type MetricRowDisplay = { */ function MetricRow({ title, - snap, + snap: rawSnap, exhaustedLabel, display, expanded, @@ -322,6 +342,8 @@ function MetricRow({ sessionEquivalentForecast?: SessionEquivalentForecastSnapshot | null; }) { const { t } = useLocale(); + const blocked = rawSnap.blockedByMonthlyLimit === true; + const snap = blocked ? toBlockedSnapshot(rawSnap) : rawSnap; const { resetTimeRelative, showResetWhenExhausted = false, @@ -353,7 +375,7 @@ function MetricRow({ resetText !== null; const paceView = showPace ? getMetricPaceView(snap) : { kind: "none" as const }; const reserveDescription = formatReserveDescription(snap, t); - const forecastText = formatSessionEquivalentEstimate(sessionEquivalentForecast); + const forecastText = blocked ? null : formatSessionEquivalentEstimate(sessionEquivalentForecast); return (
{title} @@ -380,7 +402,10 @@ function MetricRow({ {resetText} )}
- {!compactOverview && !isInformational && snap.isExhausted && ( + {blocked && ( +
{t("PanelBlockedByMonthlyLimit")}
+ )} + {!blocked && !compactOverview && !isInformational && snap.isExhausted && (
{exhaustedLabel}
)} {!compactOverview && !isInformational && paceView.kind === "budget" && ( diff --git a/apps/desktop-tauri/src/i18n/keys.ts b/apps/desktop-tauri/src/i18n/keys.ts index 1c07040562..10c823f5a6 100644 --- a/apps/desktop-tauri/src/i18n/keys.ts +++ b/apps/desktop-tauri/src/i18n/keys.ts @@ -662,6 +662,7 @@ export const ALL_LOCALE_KEYS = [ "PanelFiveHours", "PanelTodayBudget", "PanelReserveSuffix", + "PanelBlockedByMonthlyLimit", "PanelReserveLastsUntilReset", "PanelReserveRunsOutInDaysHours", "PanelReserveRunsOutInHours", diff --git a/apps/desktop-tauri/src/types/bridge.ts b/apps/desktop-tauri/src/types/bridge.ts index fd8dd55109..0adaa4e35c 100644 --- a/apps/desktop-tauri/src/types/bridge.ts +++ b/apps/desktop-tauri/src/types/bridge.ts @@ -567,6 +567,8 @@ export interface RateWindowSnapshot { reserveDescription: string | null; reserveWillLastToReset?: boolean; reserveEtaSeconds?: number | null; + /** A longer exhausted pool (Kimi monthly) blocks this window; raw percentages stay the provider data. */ + blockedByMonthlyLimit?: boolean; } export interface CostDailyPoint { diff --git a/rust/src/core/blocking_quota.rs b/rust/src/core/blocking_quota.rs new file mode 100644 index 0000000000..17853482ac --- /dev/null +++ b/rust/src/core/blocking_quota.rs @@ -0,0 +1,161 @@ +//! Longer-window quota that blocks shorter windows (upstream 0.69.0 #4091). +//! +//! When a provider's known longer pool (Kimi's monthly membership) is +//! exhausted, its shorter windows still report fresh capacity from their own +//! counters even though nothing can be used. Surfaces mark those rows as +//! blocked while the raw percentages stay untouched, so notifications, the tray +//! icon, and explicit menu-bar selections keep reading the provider's data. +//! Mirrors upstream `RateWindow.bindingQuotaProjection` plus +//! `MenuCardView.blockingQuotaMetrics`; the blocker's own reset owns the reset +//! text, so no projected reset is computed here. + +use chrono::{DateTime, Utc}; + +use super::{ProviderId, RateWindow, SESSION_WINDOW_MINUTES, UsageSnapshot}; + +/// Which windows of one snapshot a longer exhausted quota blocks. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct BlockedWindows { + pub primary: bool, + pub secondary: bool, + pub model_specific: bool, + pub tertiary: bool, + /// Parallel to [`UsageSnapshot::extra_rate_windows`]. + pub extra: Vec, +} + +impl BlockedWindows { + /// Evaluate `usage` against the provider's declared blocking quota. + pub fn evaluate(id: ProviderId, usage: &UsageSnapshot, now: DateTime) -> Self { + let mut blocked = Self { + extra: vec![false; usage.extra_rate_windows.len()], + ..Self::default() + }; + let Some(blocker_id) = id.blocking_quota_window_id() else { + return blocked; + }; + let Some(blocker) = usage + .extra_rate_windows + .iter() + .find(|extra| extra.id == blocker_id && extra.usage_known) + .map(|extra| &extra.window) + else { + return blocked; + }; + let blocks = |window: &RateWindow| is_blocked_by(window, blocker, now); + blocked.primary = blocks(&usage.primary); + blocked.secondary = usage.secondary.as_ref().is_some_and(blocks); + blocked.model_specific = usage.model_specific.as_ref().is_some_and(blocks); + blocked.tertiary = usage.tertiary.as_ref().is_some_and(blocks); + for (flag, extra) in blocked.extra.iter_mut().zip(&usage.extra_rate_windows) { + *flag = extra.usage_known && blocks(&extra.window); + } + blocked + } +} + +/// True when `blocker` is a longer, actively exhausted lane than `window`. +/// A window without duration metadata counts as the 5-hour session lane. +fn is_blocked_by(window: &RateWindow, blocker: &RateWindow, now: DateTime) -> bool { + if window.is_informational { + return false; + } + let window_minutes = window.window_minutes.unwrap_or(SESSION_WINDOW_MINUTES); + blocker + .window_minutes + .is_some_and(|minutes| minutes > window_minutes) + && blocker.remaining_percent() <= 0.0 + && blocker.resets_at.is_none_or(|reset| reset > now) +} + +#[cfg(test)] +mod tests { + use super::*; + use chrono::{Duration, TimeZone}; + + const MONTH: u32 = 30 * 24 * 60; + + fn now() -> DateTime { + Utc.timestamp_opt(1_788_000_000, 0).single().unwrap() + } + + fn window(used: f64, minutes: Option, reset: Option>) -> RateWindow { + RateWindow::with_details(used, minutes, reset, None) + } + + fn kimi_snapshot(monthly: RateWindow) -> UsageSnapshot { + let mut usage = UsageSnapshot::new(window(0.0, Some(10_080), None)) + .with_extra_rate_window("kimi-monthly", "Total usage", monthly) + .with_extra_rate_window( + "kimi-code-7d", + "Code 7-day", + window(25.0, Some(10_080), None), + ); + usage.secondary = Some(window(0.0, None, None)); + usage + } + + #[test] + fn exhausted_monthly_blocks_shorter_windows_but_not_itself() { + let usage = kimi_snapshot(window(100.0, Some(MONTH), Some(now() + Duration::days(30)))); + let blocked = BlockedWindows::evaluate(ProviderId::Kimi, &usage, now()); + assert!(blocked.primary && blocked.secondary); + assert_eq!(blocked.extra, vec![false, true]); + assert!(!blocked.tertiary && !blocked.model_specific); + // Evaluation never rewrites the raw percentages. + assert_eq!(usage.primary.used_percent, 0.0); + } + + #[test] + fn unknown_reset_still_blocks() { + let usage = kimi_snapshot(window(100.0, Some(MONTH), None)); + assert!(BlockedWindows::evaluate(ProviderId::Kimi, &usage, now()).primary); + } + + #[test] + fn available_expired_or_unknown_usage_never_blocks() { + let unblocked = BlockedWindows { + extra: vec![false, false], + ..Default::default() + }; + for monthly in [ + window(99.9, Some(MONTH), Some(now() + Duration::days(30))), + window(100.0, Some(MONTH), Some(now())), + window(100.0, Some(MONTH), Some(now() - Duration::days(1))), + ] { + let usage = kimi_snapshot(monthly); + assert_eq!( + BlockedWindows::evaluate(ProviderId::Kimi, &usage, now()), + unblocked + ); + } + + let mut usage = kimi_snapshot(window(100.0, Some(MONTH), None)); + usage.extra_rate_windows[0].usage_known = false; + assert_eq!( + BlockedWindows::evaluate(ProviderId::Kimi, &usage, now()), + unblocked + ); + } + + #[test] + fn missing_minutes_default_to_the_session_lane_and_informational_rows_are_skipped() { + let mut usage = kimi_snapshot(window(100.0, Some(MONTH), None)); + usage.secondary = Some(RateWindow::informational("No active session")); + let blocked = BlockedWindows::evaluate(ProviderId::Kimi, &usage, now()); + assert!(blocked.primary && !blocked.secondary); + + // A blocker no longer than the window (or without minutes) cannot block it. + let usage = kimi_snapshot(window(100.0, Some(10_080), None)); + assert!(!BlockedWindows::evaluate(ProviderId::Kimi, &usage, now()).primary); + let usage = kimi_snapshot(window(100.0, None, None)); + assert!(!BlockedWindows::evaluate(ProviderId::Kimi, &usage, now()).primary); + } + + #[test] + fn providers_without_a_declared_blocker_are_untouched() { + let usage = kimi_snapshot(window(100.0, Some(MONTH), None)); + let blocked = BlockedWindows::evaluate(ProviderId::Codex, &usage, now()); + assert!(!blocked.primary && blocked.extra.iter().all(|flag| !flag)); + } +} diff --git a/rust/src/core/mod.rs b/rust/src/core/mod.rs index 436c7d5e0a..a163963b56 100755 --- a/rust/src/core/mod.rs +++ b/rust/src/core/mod.rs @@ -2,6 +2,7 @@ mod adaptive_refresh; mod aws_signing; +mod blocking_quota; mod claude_routed_pricing; mod codex_routed_pricing; mod cost_cache_budget; @@ -32,6 +33,7 @@ mod widget_snapshot; pub use adaptive_refresh::*; pub use aws_signing::*; +pub use blocking_quota::*; pub use cost_cache_budget::*; pub use cost_pricing::*; pub use curl_capture::*; diff --git a/rust/src/core/provider.rs b/rust/src/core/provider.rs index 218901b770..94c4b920ad 100755 --- a/rust/src/core/provider.rs +++ b/rust/src/core/provider.rs @@ -442,6 +442,15 @@ impl ProviderId { } } + /// Id of the longer pool that blocks this provider's shorter windows once + /// exhausted (upstream 0.69.0 #4091). See [`super::BlockedWindows`]. + pub fn blocking_quota_window_id(&self) -> Option<&'static str> { + match self { + ProviderId::Kimi => Some(crate::providers::kimi::MONTHLY_WINDOW_ID), + _ => None, + } + } + /// Parse from CLI name string pub fn from_cli_name(name: &str) -> Option { match name.to_lowercase().as_str() { diff --git a/rust/src/locale.rs b/rust/src/locale.rs index 276a31160e..3bf5740b30 100644 --- a/rust/src/locale.rs +++ b/rust/src/locale.rs @@ -936,6 +936,7 @@ locale_keys! { PanelFiveHours, PanelTodayBudget, PanelReserveSuffix, + PanelBlockedByMonthlyLimit, PanelReserveLastsUntilReset, PanelReserveRunsOutInDaysHours, PanelReserveRunsOutInHours, diff --git a/rust/src/locale/en-US.ftl b/rust/src/locale/en-US.ftl index 53b4453f6d..3ee91ead0e 100644 --- a/rust/src/locale/en-US.ftl +++ b/rust/src/locale/en-US.ftl @@ -599,6 +599,7 @@ PanelOneHour = 1h PanelFiveHours = 5h PanelTodayBudget = today PanelReserveSuffix = in reserve +PanelBlockedByMonthlyLimit = Blocked by monthly limit PanelReserveLastsUntilReset = Lasts until reset PanelReserveRunsOutInDaysHours = Runs out in { "{}" }d { "{}" }h PanelReserveRunsOutInHours = Runs out in { "{}" }h diff --git a/rust/src/providers/kimi/mod.rs b/rust/src/providers/kimi/mod.rs index d222417246..028c832b8e 100755 --- a/rust/src/providers/kimi/mod.rs +++ b/rust/src/providers/kimi/mod.rs @@ -30,6 +30,9 @@ use crate::core::{ RateWindow, SourceMode, UsageSnapshot, }; +/// Extra-window id of the monthly membership pool (`Total usage`). +pub const MONTHLY_WINDOW_ID: &str = "kimi-monthly"; + const KIMI_WEB_USAGE_SERVICE: &str = "kimi.gateway.billing.v1.BillingService/GetUsages"; const KIMI_SUBSCRIPTION_STATS_SERVICE: &str = "kimi.gateway.membership.v2.MembershipService/GetSubscriptionStats"; @@ -462,7 +465,7 @@ fn apply_subscription_windows( { // Verified monthly sentinel (#2431 / #2566). usage = usage.with_extra_rate_window( - "kimi-monthly", + MONTHLY_WINDOW_ID, "Total usage", RateWindow::with_details( ratio * 100.0, From c2205638743f93470e9cfce35097425f053f49bd Mon Sep 17 00:00:00 2001 From: RCD <90105158+Finesssee@users.noreply.github.com> Date: Tue, 29 Sep 2026 22:34:38 +0700 Subject: [PATCH 2/4] Address thermo review --- .../src/components/MenuCardDetails.tsx | 13 +++++++------ rust/src/core/blocking_quota.rs | 7 +++++++ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/apps/desktop-tauri/src/components/MenuCardDetails.tsx b/apps/desktop-tauri/src/components/MenuCardDetails.tsx index 5202044563..7b93297b10 100644 --- a/apps/desktop-tauri/src/components/MenuCardDetails.tsx +++ b/apps/desktop-tauri/src/components/MenuCardDetails.tsx @@ -376,6 +376,12 @@ function MetricRow({ const paceView = showPace ? getMetricPaceView(snap) : { kind: "none" as const }; const reserveDescription = formatReserveDescription(snap, t); const forecastText = blocked ? null : formatSessionEquivalentEstimate(sessionEquivalentForecast); + // Blocked rows explain themselves everywhere; plain exhaustion only in the full card. + const statusLabel = blocked + ? t("PanelBlockedByMonthlyLimit") + : !compactOverview && !isInformational && snap.isExhausted + ? exhaustedLabel + : null; return (
{title} @@ -402,12 +408,7 @@ function MetricRow({ {resetText} )}
- {blocked && ( -
{t("PanelBlockedByMonthlyLimit")}
- )} - {!blocked && !compactOverview && !isInformational && snap.isExhausted && ( -
{exhaustedLabel}
- )} + {statusLabel &&
{statusLabel}
} {!compactOverview && !isInformational && paceView.kind === "budget" && (