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
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
@@ -1,5 +1,9 @@
mod openai_usage;
#[cfg(test)]
mod openai_usage_tests;
pub(crate) mod pace;
mod status;
pub(crate) use openai_usage::OpenAiApiUsageSnapshot;
pub(crate) use status::{compact_tray_status_label, friendly_provider_error};

use super::*;
Expand Down Expand Up @@ -267,6 +271,9 @@ pub struct ProviderUsageSnapshot {
pub fetch_duration_ms: Option<u128>,
#[serde(default)]
pub wayfinder_usage: Option<codexbar::core::WayfinderUsageSnapshot>,
/// 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>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub session_equivalent_forecast: Option<SessionEquivalentForecastSnapshot>,
}
Expand Down Expand Up @@ -492,6 +499,7 @@ impl ProviderUsageSnapshot {
tray_status_label: None,
fetch_duration_ms: None,
wayfinder_usage: result.wayfinder_usage.clone(),
open_ai_api_usage: result.open_ai_api_usage.as_ref().map(Into::into),
session_equivalent_forecast,
}
}
Expand Down Expand Up @@ -542,6 +550,7 @@ impl ProviderUsageSnapshot {
tray_status_label: None,
fetch_duration_ms: None,
wayfinder_usage: None,
open_ai_api_usage: None,
session_equivalent_forecast: None,
}
}
Expand Down
103 changes: 103 additions & 0 deletions apps/desktop-tauri/src-tauri/src/commands/bridge/openai_usage.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
//! Bridge DTOs for the OpenAI Admin API per-day usage history (`openAiApiUsage`).
//!
//! Mirrors `OpenAiApiUsageSnapshot` in `src/types/bridge.ts`. Times are epoch seconds and
//! counts are non-negative integers, as in upstream 0.66.0's card payload.

use codexbar::core::{
OpenAiApiDailyUsage, OpenAiApiLineItemCost, OpenAiApiModelUsage, OpenAiApiUsageHistory,
};
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OpenAiApiUsageSnapshot {
pub history_days: u32,
#[serde(default)]
pub project_id: Option<String>,
#[serde(default)]
pub daily: Vec<OpenAiApiDailyUsageSnapshot>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OpenAiApiDailyUsageSnapshot {
pub start_time: i64,
pub end_time: i64,
pub cost_usd: f64,
pub requests: u64,
pub input_tokens: u64,
pub cached_input_tokens: u64,
pub output_tokens: u64,
pub total_tokens: u64,
#[serde(default)]
pub line_items: Vec<OpenAiApiLineItemSnapshot>,
#[serde(default)]
pub models: Vec<OpenAiApiModelUsageSnapshot>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OpenAiApiLineItemSnapshot {
pub name: String,
pub cost_usd: f64,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OpenAiApiModelUsageSnapshot {
pub name: String,
pub requests: u64,
pub input_tokens: u64,
pub cached_input_tokens: u64,
pub output_tokens: u64,
pub total_tokens: u64,
}

impl From<&OpenAiApiUsageHistory> for OpenAiApiUsageSnapshot {
fn from(history: &OpenAiApiUsageHistory) -> Self {
Self {
history_days: history.history_days,
project_id: history.project_id.clone(),
daily: history.daily.iter().map(Into::into).collect(),
}
}
}

impl From<&OpenAiApiDailyUsage> for OpenAiApiDailyUsageSnapshot {
fn from(day: &OpenAiApiDailyUsage) -> Self {
Self {
start_time: day.start_time,
end_time: day.end_time,
cost_usd: day.cost_usd,
requests: day.requests,
input_tokens: day.input_tokens,
cached_input_tokens: day.cached_input_tokens,
output_tokens: day.output_tokens,
total_tokens: day.total_tokens,
line_items: day.line_items.iter().map(Into::into).collect(),
models: day.models.iter().map(Into::into).collect(),
}
}
}

impl From<&OpenAiApiLineItemCost> for OpenAiApiLineItemSnapshot {
fn from(item: &OpenAiApiLineItemCost) -> Self {
Self {
name: item.name.clone(),
cost_usd: item.cost_usd,
}
}
}

impl From<&OpenAiApiModelUsage> for OpenAiApiModelUsageSnapshot {
fn from(model: &OpenAiApiModelUsage) -> Self {
Self {
name: model.name.clone(),
requests: model.requests,
input_tokens: model.input_tokens,
cached_input_tokens: model.cached_input_tokens,
output_tokens: model.output_tokens,
total_tokens: model.total_tokens,
}
}
}
108 changes: 108 additions & 0 deletions apps/desktop-tauri/src-tauri/src/commands/bridge/openai_usage_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
use super::openai_usage::OpenAiApiUsageSnapshot;
use crate::commands::ProviderUsageSnapshot;
use codexbar::core::{
OpenAiApiDailyUsage, OpenAiApiLineItemCost, OpenAiApiModelUsage, OpenAiApiUsageHistory,
ProviderFetchResult, ProviderId, RateWindow, UsageSnapshot, instantiate_provider,
};

fn history() -> OpenAiApiUsageHistory {
OpenAiApiUsageHistory {
history_days: 30,
project_id: Some("proj_abc".to_string()),
daily: vec![OpenAiApiDailyUsage {
start_time: 1_700_000_000,
end_time: 1_700_086_400,
cost_usd: 14.75,
requests: 10,
input_tokens: 1300,
cached_input_tokens: 250,
output_tokens: 700,
total_tokens: 2000,
line_items: vec![OpenAiApiLineItemCost {
name: "Text tokens".to_string(),
cost_usd: 12.5,
}],
models: vec![OpenAiApiModelUsage {
name: "gpt-5.2".to_string(),
requests: 7,
input_tokens: 1000,
cached_input_tokens: 250,
output_tokens: 500,
total_tokens: 1500,
}],
}],
}
}

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

#[test]
fn fetch_result_history_reaches_the_bridge_as_camel_case_epoch_seconds() {
let json = serde_json::to_value(snapshot(Some(history()))).unwrap();
assert_eq!(
json["openAiApiUsage"],
serde_json::json!({
"historyDays": 30,
"projectId": "proj_abc",
"daily": [{
"startTime": 1_700_000_000,
"endTime": 1_700_086_400,
"costUsd": 14.75,
"requests": 10,
"inputTokens": 1300,
"cachedInputTokens": 250,
"outputTokens": 700,
"totalTokens": 2000,
"lineItems": [{"name": "Text tokens", "costUsd": 12.5}],
"models": [{
"name": "gpt-5.2",
"requests": 7,
"inputTokens": 1000,
"cachedInputTokens": 250,
"outputTokens": 500,
"totalTokens": 1500,
}],
}],
})
);
}

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

#[test]
fn history_round_trips_through_the_bridge_payload() {
let original = snapshot(Some(history()));
let decoded: ProviderUsageSnapshot =
serde_json::from_value(serde_json::to_value(&original).unwrap()).unwrap();
assert_eq!(decoded.open_ai_api_usage, original.open_ai_api_usage);
assert_eq!(
decoded.open_ai_api_usage.unwrap().project_id.as_deref(),
Some("proj_abc")
);
}

#[test]
fn an_empty_history_serializes_a_null_project_and_no_days() {
let empty = OpenAiApiUsageHistory {
history_days: 30,
project_id: None,
daily: Vec::new(),
};
let json = serde_json::to_value(OpenAiApiUsageSnapshot::from(&empty)).unwrap();
assert_eq!(
json,
serde_json::json!({"historyDays": 30, "projectId": null, "daily": []})
);
}
1 change: 1 addition & 0 deletions apps/desktop-tauri/src-tauri/src/commands/providers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1351,6 +1351,7 @@ mod reset_backfill_tests {
tray_status_label: None,
fetch_duration_ms: None,
wayfinder_usage: None,
open_ai_api_usage: None,
session_equivalent_forecast: None,
}
}
Expand Down
12 changes: 12 additions & 0 deletions apps/desktop-tauri/src-tauri/src/commands/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -942,6 +942,7 @@ fn usage_item_descriptors_keep_raw_ids_and_redact_titles() {
usage: codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(10.0)),
cost: None,
wayfinder_usage: None,
open_ai_api_usage: None,
inventory: Vec::new(),
display_details: Vec::new(),
source_label: "OAuth".to_string(),
Expand Down Expand Up @@ -1166,6 +1167,7 @@ fn provider_cache_upsert_replaces_existing_provider() {
usage: codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(10.0)),
cost: None,
wayfinder_usage: None,
open_ai_api_usage: None,
inventory: Vec::new(),
display_details: Vec::new(),
source_label: "CLI".to_string(),
Expand Down Expand Up @@ -1194,6 +1196,7 @@ fn provider_cache_prunes_disabled_providers() {
usage: codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(10.0)),
cost: None,
wayfinder_usage: None,
open_ai_api_usage: None,
inventory: Vec::new(),
display_details: Vec::new(),
source_label: "CLI".to_string(),
Expand Down Expand Up @@ -1230,6 +1233,7 @@ fn claude_transient_auth_failure_preserves_first_last_good_snapshot() {
usage: codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(42.0)),
cost: None,
wayfinder_usage: None,
open_ai_api_usage: None,
inventory: Vec::new(),
display_details: Vec::new(),
source_label: "OAuth".to_string(),
Expand Down Expand Up @@ -1267,6 +1271,7 @@ fn codex_transient_transport_failure_helper_uses_typed_policy() {
usage: codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(42.0)),
cost: None,
wayfinder_usage: None,
open_ai_api_usage: None,
inventory: Vec::new(),
display_details: Vec::new(),
source_label: "OAuth".to_string(),
Expand Down Expand Up @@ -1303,6 +1308,7 @@ fn claude_repeated_auth_failure_surfaces_error() {
usage: codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(42.0)),
cost: None,
wayfinder_usage: None,
open_ai_api_usage: None,
inventory: Vec::new(),
display_details: Vec::new(),
source_label: "OAuth".to_string(),
Expand Down Expand Up @@ -1346,6 +1352,7 @@ fn claude_cloudflare_challenge_retains_prior_usage_while_surfaceing_guidance() {
usage: codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(42.0)),
cost: None,
wayfinder_usage: None,
open_ai_api_usage: None,
inventory: Vec::new(),
display_details: Vec::new(),
source_label: "OAuth".to_string(),
Expand Down Expand Up @@ -1400,6 +1407,7 @@ fn claude_cloudflare_challenge_keeps_prior_usage_when_guidance_surfaces() {
usage: codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(42.0)),
cost: None,
wayfinder_usage: None,
open_ai_api_usage: None,
inventory: Vec::new(),
display_details: Vec::new(),
source_label: "Web".to_string(),
Expand Down Expand Up @@ -1451,6 +1459,7 @@ fn claude_cli_parse_failure_keeps_last_good_every_time() {
usage: codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(17.0)),
cost: None,
wayfinder_usage: None,
open_ai_api_usage: None,
inventory: Vec::new(),
display_details: Vec::new(),
source_label: "CLI".to_string(),
Expand Down Expand Up @@ -1498,6 +1507,7 @@ fn claude_hard_credentials_missing_does_not_preserve_stale() {
usage: codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(17.0)),
cost: None,
wayfinder_usage: None,
open_ai_api_usage: None,
inventory: Vec::new(),
display_details: Vec::new(),
source_label: "OAuth".to_string(),
Expand Down Expand Up @@ -1702,6 +1712,7 @@ fn japanese_provider_snapshot_localizes_weekly_label() {
usage,
cost: None,
wayfinder_usage: None,
open_ai_api_usage: None,
inventory: Vec::new(),
display_details: Vec::new(),
source_label: "OAuth".to_string(),
Expand Down Expand Up @@ -1736,6 +1747,7 @@ fn japanese_provider_snapshot_localizes_pace_reserve_description() {
usage,
cost: None,
wayfinder_usage: None,
open_ai_api_usage: None,
inventory: Vec::new(),
display_details: Vec::new(),
source_label: "OAuth".to_string(),
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop-tauri/src-tauri/src/powertoys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ mod tests {
tray_status_label: None,
fetch_duration_ms: None,
wayfinder_usage: None,
open_ai_api_usage: None,
session_equivalent_forecast: None,
});
let value = serde_json::to_value(snapshot).unwrap();
Expand Down Expand Up @@ -261,6 +262,7 @@ mod tests {
tray_status_label: None,
fetch_duration_ms: None,
wayfinder_usage: None,
open_ai_api_usage: None,
session_equivalent_forecast: None,
});
let value = serde_json::to_value(snapshot).unwrap();
Expand Down
1 change: 1 addition & 0 deletions apps/desktop-tauri/src-tauri/src/tray_bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1098,6 +1098,7 @@ mod tests {
tray_status_label: None,
fetch_duration_ms: None,
wayfinder_usage: None,
open_ai_api_usage: None,
session_equivalent_forecast: None,
}
}
Expand Down
1 change: 1 addition & 0 deletions apps/desktop-tauri/src-tauri/src/usage_metric.rs
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,7 @@ mod tests {
tray_status_label: None,
fetch_duration_ms: None,
wayfinder_usage: None,
open_ai_api_usage: None,
session_equivalent_forecast: None,
}
}
Expand Down
Loading