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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

## [Windows] Unreleased

- 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
9 changes: 9 additions & 0 deletions apps/desktop-tauri/src-tauri/src/commands/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@ mod openai_usage_tests;
pub(crate) mod pace;
mod quota_block;
mod status;
mod throughput;
#[cfg(test)]
mod throughput_tests;
pub(crate) use openai_usage::OpenAiApiUsageSnapshot;
pub use quota_block::MonthlyLimitBlockSnapshot;
pub(crate) use status::{compact_tray_status_label, friendly_provider_error};
pub(crate) use throughput::ProviderThroughputSnapshot;

use super::*;
use codexbar::core::BlockedWindows;
Expand Down Expand Up @@ -381,6 +385,9 @@ pub struct ProviderUsageSnapshot {
/// Per-day OpenAI Admin API history for the daily usage chart.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub open_ai_api_usage: Option<OpenAiApiUsageSnapshot>,
/// Last-hour Tokens/min grid (OpenAI Admin API 1-minute buckets).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub throughput: Option<ProviderThroughputSnapshot>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub session_equivalent_forecast: Option<SessionEquivalentForecastSnapshot>,
/// Recorded remaining-quota burndown for the selected window
Expand Down Expand Up @@ -713,6 +720,7 @@ impl ProviderUsageSnapshot {
fetch_duration_ms: None,
wayfinder_usage: result.wayfinder_usage.clone(),
open_ai_api_usage: result.open_ai_api_usage.as_ref().map(Into::into),
throughput: result.throughput.as_ref().map(Into::into),
session_equivalent_forecast,
quota_burndown,
}
Expand Down Expand Up @@ -773,6 +781,7 @@ impl ProviderUsageSnapshot {
fetch_duration_ms: None,
wayfinder_usage: None,
open_ai_api_usage: None,
throughput: None,
session_equivalent_forecast: None,
quota_burndown: None,
}
Expand Down
91 changes: 91 additions & 0 deletions apps/desktop-tauri/src-tauri/src/commands/bridge/throughput.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
//! Bridge DTOs for the last-hour Tokens/min grid (`throughput`, upstream 0.75.0
//! `ProviderThroughputSnapshot`, #4495).
//!
//! Mirrors `ProviderThroughputSnapshot` in `src/types/bridge.ts`. Times are epoch seconds.
//! Minutes after `frontier` are `isPending` and never count toward peaks or utilization.

use codexbar::core::{ProviderThroughput, ThroughputMinute, ThroughputModel};
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProviderThroughputSnapshot {
#[serde(default)]
pub minutes: Vec<ThroughputMinuteSnapshot>,
#[serde(default)]
pub frontier: Option<i64>,
#[serde(default)]
pub latest_tokens_per_minute: Option<u64>,
pub peak_tokens_per_minute: u64,
#[serde(default)]
pub peak_at: Option<i64>,
pub five_minute_average_tokens_per_minute: f64,
#[serde(default)]
pub models: Vec<ThroughputModelSnapshot>,
pub window_minutes: u32,
pub fetched_at: i64,
#[serde(default)]
pub project_id: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ThroughputMinuteSnapshot {
pub start: i64,
pub total_tokens: u64,
pub requests: u64,
pub is_pending: bool,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ThroughputModelSnapshot {
pub name: String,
pub latest_tpm: u64,
pub peak_tpm: u64,
pub total_tokens: u64,
pub peak_limit_counted_tokens: u64,
#[serde(default)]
pub limit_counted_tokens_by_minute: Vec<u64>,
}

impl From<&ProviderThroughput> for ProviderThroughputSnapshot {
fn from(throughput: &ProviderThroughput) -> Self {
Self {
minutes: throughput.minutes.iter().map(Into::into).collect(),
frontier: throughput.frontier,
latest_tokens_per_minute: throughput.latest_tokens_per_minute,
peak_tokens_per_minute: throughput.peak_tokens_per_minute,
peak_at: throughput.peak_at,
five_minute_average_tokens_per_minute: throughput.five_minute_average_tokens_per_minute,
models: throughput.models.iter().map(Into::into).collect(),
window_minutes: throughput.window_minutes,
fetched_at: throughput.fetched_at,
project_id: throughput.project_id.clone(),
}
}
}

impl From<&ThroughputMinute> for ThroughputMinuteSnapshot {
fn from(minute: &ThroughputMinute) -> Self {
Self {
start: minute.start,
total_tokens: minute.total_tokens,
requests: minute.requests,
is_pending: minute.is_pending,
}
}
}

impl From<&ThroughputModel> for ThroughputModelSnapshot {
fn from(model: &ThroughputModel) -> Self {
Self {
name: model.name.clone(),
latest_tpm: model.latest_tpm,
peak_tpm: model.peak_tpm,
total_tokens: model.total_tokens,
peak_limit_counted_tokens: model.peak_limit_counted_tokens,
limit_counted_tokens_by_minute: model.limit_counted_tokens_by_minute.clone(),
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
use crate::commands::ProviderUsageSnapshot;
use codexbar::core::{
ProviderFetchResult, ProviderId, ProviderThroughput, RateWindow, ThroughputMinute,
ThroughputModel, UsageSnapshot, instantiate_provider,
};

fn throughput() -> ProviderThroughput {
ProviderThroughput {
minutes: vec![
ThroughputMinute {
start: 1_700_179_020,
total_tokens: 120,
requests: 3,
is_pending: false,
},
ThroughputMinute {
start: 1_700_179_080,
total_tokens: 40,
requests: 1,
is_pending: true,
},
],
frontier: Some(1_700_179_020),
latest_tokens_per_minute: Some(120),
peak_tokens_per_minute: 120,
peak_at: Some(1_700_179_020),
five_minute_average_tokens_per_minute: 24.0,
models: vec![ThroughputModel {
name: "gpt-5".to_string(),
latest_tpm: 120,
peak_tpm: 120,
total_tokens: 160,
peak_limit_counted_tokens: 100,
limit_counted_tokens_by_minute: vec![100, 40],
}],
window_minutes: 60,
fetched_at: 1_700_179_105,
project_id: None,
}
}

fn snapshot(throughput: Option<ProviderThroughput>) -> ProviderUsageSnapshot {
let metadata = instantiate_provider(ProviderId::OpenAIApi)
.metadata()
.clone();
let mut result =
ProviderFetchResult::new(UsageSnapshot::new(RateWindow::new(0.0)), "admin-api");
result.throughput = throughput;
ProviderUsageSnapshot::from_fetch_result(ProviderId::OpenAIApi, &metadata, &result, None, None)
}

#[test]
fn fetch_result_throughput_reaches_the_bridge_as_camel_case_epoch_seconds() {
let json = serde_json::to_value(snapshot(Some(throughput()))).unwrap();
assert_eq!(
json["throughput"],
serde_json::json!({
"minutes": [
{"start": 1_700_179_020, "totalTokens": 120, "requests": 3, "isPending": false},
{"start": 1_700_179_080, "totalTokens": 40, "requests": 1, "isPending": true},
],
"frontier": 1_700_179_020,
"latestTokensPerMinute": 120,
"peakTokensPerMinute": 120,
"peakAt": 1_700_179_020,
"fiveMinuteAverageTokensPerMinute": 24.0,
"models": [{
"name": "gpt-5",
"latestTpm": 120,
"peakTpm": 120,
"totalTokens": 160,
"peakLimitCountedTokens": 100,
"limitCountedTokensByMinute": [100, 40],
}],
"windowMinutes": 60,
"fetchedAt": 1_700_179_105,
"projectId": null,
})
);
}

#[test]
fn missing_throughput_is_omitted_and_round_trips() {
let json = serde_json::to_value(snapshot(None)).unwrap();
assert!(json.get("throughput").is_none());

let original = snapshot(Some(throughput()));
let decoded: ProviderUsageSnapshot =
serde_json::from_value(serde_json::to_value(&original).unwrap()).unwrap();
assert_eq!(decoded.throughput, original.throughput);

let legacy: ProviderUsageSnapshot = serde_json::from_value(json).unwrap();
assert!(legacy.throughput.is_none());
}
2 changes: 2 additions & 0 deletions apps/desktop-tauri/src-tauri/src/commands/providers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,7 @@ pub(crate) fn build_fetch_context(
&& token_account_kind == Some(codexbar::core::TokenAccountKind::ApiKey)),
browser_cookie_import,
optional_details_enabled: settings.optional_details_enabled(id),
live_usage_enabled: settings.live_usage_enabled(id),
..FetchContext::default()
}
}
Expand Down Expand Up @@ -1843,6 +1844,7 @@ mod reset_backfill_tests {
fetch_duration_ms: None,
wayfinder_usage: None,
open_ai_api_usage: None,
throughput: None,
session_equivalent_forecast: None,
quota_burndown: None,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ mod tests {
wayfinder_usage: None,
quota_burndown: None,
open_ai_api_usage: None,
throughput: None,
session_equivalent_forecast: None,
}
}
Expand Down
16 changes: 16 additions & 0 deletions apps/desktop-tauri/src-tauri/src/commands/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1389,6 +1389,7 @@ fn usage_item_descriptors_keep_raw_ids_and_redact_titles() {
cost: None,
wayfinder_usage: None,
open_ai_api_usage: None,
throughput: None,
inventory: Vec::new(),
reset_credits: None,
display_details: Vec::new(),
Expand Down Expand Up @@ -1447,6 +1448,7 @@ fn usage_item_descriptors_include_detail_sections() {
pace_authoritative: true,
account_identity: None,
open_ai_api_usage: None,
throughput: None,
last_good_owner: None,
};
let snapshot =
Expand Down Expand Up @@ -1503,6 +1505,7 @@ fn usage_item_descriptors_skip_card_hidden_windows() {
pace_authoritative: true,
account_identity: None,
open_ai_api_usage: None,
throughput: None,
last_good_owner: None,
};
let mut snapshot =
Expand Down Expand Up @@ -1564,6 +1567,7 @@ fn claude_snapshot_with_sections(source_label: &str) -> ProviderUsageSnapshot {
pace_authoritative: true,
account_identity: None,
last_good_owner: None,
throughput: None,
};
let mut snapshot = ProviderUsageSnapshot::from_fetch_result(
ProviderId::Claude,
Expand Down Expand Up @@ -1879,6 +1883,7 @@ fn provider_cache_upsert_replaces_existing_provider() {
cost: None,
wayfinder_usage: None,
open_ai_api_usage: None,
throughput: None,
inventory: Vec::new(),
reset_credits: None,
display_details: Vec::new(),
Expand Down Expand Up @@ -1910,6 +1915,7 @@ fn provider_cache_prunes_disabled_providers() {
cost: None,
wayfinder_usage: None,
open_ai_api_usage: None,
throughput: None,
inventory: Vec::new(),
reset_credits: None,
display_details: Vec::new(),
Expand Down Expand Up @@ -1954,6 +1960,7 @@ fn claude_transient_auth_failure_preserves_first_last_good_snapshot() {
cost: None,
wayfinder_usage: None,
open_ai_api_usage: None,
throughput: None,
inventory: Vec::new(),
reset_credits: None,
display_details: Vec::new(),
Expand Down Expand Up @@ -1999,6 +2006,7 @@ fn codex_transient_transport_failure_helper_uses_typed_policy() {
cost: None,
wayfinder_usage: None,
open_ai_api_usage: None,
throughput: None,
inventory: Vec::new(),
reset_credits: None,
display_details: Vec::new(),
Expand Down Expand Up @@ -2038,6 +2046,7 @@ fn claude_repeated_auth_failure_surfaces_error() {
cost: None,
wayfinder_usage: None,
open_ai_api_usage: None,
throughput: None,
inventory: Vec::new(),
reset_credits: None,
display_details: Vec::new(),
Expand Down Expand Up @@ -2089,6 +2098,7 @@ fn claude_cloudflare_challenge_retains_prior_usage_while_surfaceing_guidance() {
cost: None,
wayfinder_usage: None,
open_ai_api_usage: None,
throughput: None,
inventory: Vec::new(),
reset_credits: None,
display_details: Vec::new(),
Expand Down Expand Up @@ -2151,6 +2161,7 @@ fn claude_cloudflare_challenge_keeps_prior_usage_when_guidance_surfaces() {
cost: None,
wayfinder_usage: None,
open_ai_api_usage: None,
throughput: None,
inventory: Vec::new(),
reset_credits: None,
display_details: Vec::new(),
Expand Down Expand Up @@ -2210,6 +2221,7 @@ fn claude_cli_parse_failure_keeps_last_good_every_time() {
cost: None,
wayfinder_usage: None,
open_ai_api_usage: None,
throughput: None,
inventory: Vec::new(),
reset_credits: None,
display_details: Vec::new(),
Expand Down Expand Up @@ -2265,6 +2277,7 @@ fn claude_hard_credentials_missing_does_not_preserve_stale() {
cost: None,
wayfinder_usage: None,
open_ai_api_usage: None,
throughput: None,
inventory: Vec::new(),
reset_credits: None,
display_details: Vec::new(),
Expand Down Expand Up @@ -2480,6 +2493,7 @@ fn japanese_provider_snapshot_localizes_weekly_label() {
cost: None,
wayfinder_usage: None,
open_ai_api_usage: None,
throughput: None,
inventory: Vec::new(),
reset_credits: None,
display_details: Vec::new(),
Expand Down Expand Up @@ -2522,6 +2536,7 @@ fn japanese_provider_snapshot_localizes_pace_reserve_description() {
cost: None,
wayfinder_usage: None,
open_ai_api_usage: None,
throughput: None,
inventory: Vec::new(),
reset_credits: None,
display_details: Vec::new(),
Expand Down Expand Up @@ -2962,6 +2977,7 @@ fn limit_reset_snapshot() -> ProviderUsageSnapshot {
cost: None,
wayfinder_usage: None,
open_ai_api_usage: None,
throughput: None,
inventory: Vec::new(),
reset_credits: None,
display_details: Vec::new(),
Expand Down
Loading