From 4ceb88f03c76177e56a9459997999877a364d231 Mon Sep 17 00:00:00 2001 From: NessZerra <90105158+Finesssee@users.noreply.github.com> Date: Sat, 19 Sep 2026 21:34:59 +0700 Subject: [PATCH 1/9] Add transient provider inventory plumbing --- .../src-tauri/src/commands/bridge.rs | 24 +++++ .../src-tauri/src/commands/provider_detail.rs | 3 + .../src-tauri/src/commands/providers.rs | 1 + .../src-tauri/src/commands/tests.rs | 44 +++++++- apps/desktop-tauri/src-tauri/src/powertoys.rs | 2 + .../src-tauri/src/tray_bridge.rs | 1 + .../src-tauri/src/usage_metric.rs | 1 + .../src/components/MenuCardDetails.tsx | 51 ++++++++- .../providers/sections/UsageSection.test.tsx | 23 ++++ .../providers/sections/UsageSection.tsx | 33 +++++- apps/desktop-tauri/src/types/bridge.ts | 11 ++ rust/src/cli/usage.rs | 100 +++++++++++++++++- rust/src/core/usage_snapshot.rs | 50 +++++++++ 13 files changed, 338 insertions(+), 6 deletions(-) diff --git a/apps/desktop-tauri/src-tauri/src/commands/bridge.rs b/apps/desktop-tauri/src-tauri/src/commands/bridge.rs index 8cd5e91924..1a86eab7b4 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/bridge.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/bridge.rs @@ -178,6 +178,17 @@ pub struct SubscriptionMetadataSnapshot { pub renews_at: Option, } +/// Display-only provider inventory. Redemption identifiers never cross the +/// bridge. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ProviderInventoryItemSnapshot { + pub id: String, + pub title: String, + pub available_count: u32, + pub next_expires_at: Option, +} + /// A frontend-friendly snapshot of one provider's usage data. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -200,6 +211,8 @@ pub struct ProviderUsageSnapshot { pub tertiary: Option, #[serde(default)] pub extra_rate_windows: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub inventory: Vec, #[serde(default)] pub cost: Option, #[serde(default)] @@ -384,6 +397,16 @@ impl ProviderUsageSnapshot { window: RateWindowSnapshot::from_rate_window(&extra.window), }) .collect(), + inventory: result + .inventory + .iter() + .map(|item| ProviderInventoryItemSnapshot { + id: item.id.clone(), + title: item.title.clone(), + available_count: item.available_count, + next_expires_at: item.next_expires_at.map(|date| date.to_rfc3339()), + }) + .collect(), cost: result.cost.as_ref().map(|c| CostSnapshotBridge { used: c.used, limit: c.limit, @@ -461,6 +484,7 @@ impl ProviderUsageSnapshot { tertiary: None, tertiary_label: None, extra_rate_windows: Vec::new(), + inventory: Vec::new(), cost: None, plan_name: None, account_email: None, diff --git a/apps/desktop-tauri/src-tauri/src/commands/provider_detail.rs b/apps/desktop-tauri/src-tauri/src/commands/provider_detail.rs index 7690a5b2aa..18b1def39b 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/provider_detail.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/provider_detail.rs @@ -26,6 +26,7 @@ pub struct ProviderDetail { pub model_specific: Option, pub tertiary: Option, pub extra_rate_windows: Vec, + pub inventory: Vec, // Cost / pace. pub cost: Option, @@ -91,6 +92,7 @@ pub(crate) fn build_provider_detail(provider_id: &str) -> Result::from_timestamp(1_900_000_000, 0).unwrap(); + let result = ProviderFetchResult::new( + codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(12.0)), + "web", + ) + .with_inventory_item(ProviderInventoryItem { + id: "reset-credits".to_string(), + title: "Limit Reset Credits".to_string(), + available_count: 2, + next_expires_at: Some(expiry), + }); + let metadata = instantiate_provider(ProviderId::Grok).metadata().clone(); + let snapshot = + ProviderUsageSnapshot::from_fetch_result(ProviderId::Grok, &metadata, &result, None); + + assert_eq!(snapshot.inventory.len(), 1); + assert_eq!(snapshot.inventory[0].available_count, 2); + assert_eq!( + snapshot.inventory[0].next_expires_at.as_deref(), + Some("2030-03-17T17:46:40+00:00") + ); + let serialized = serde_json::to_string(&snapshot).unwrap(); + assert!(serialized.contains("reset-credits")); + assert!(!serialized.contains("coupon-token-secret")); +} + #[test] fn provider_cache_is_fresh_inside_stale_window() { assert!(super::is_provider_cache_fresh( @@ -1057,6 +1085,7 @@ fn provider_cache_upsert_replaces_existing_provider() { usage: codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(10.0)), cost: None, wayfinder_usage: None, + inventory: Vec::new(), source_label: "CLI".to_string(), has_successful_claude_cli_quota: false, pace_authoritative: true, @@ -1083,6 +1112,7 @@ fn provider_cache_prunes_disabled_providers() { usage: codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(10.0)), cost: None, wayfinder_usage: None, + inventory: Vec::new(), source_label: "CLI".to_string(), has_successful_claude_cli_quota: false, pace_authoritative: true, @@ -1116,6 +1146,7 @@ fn hiding_codex_spark_rows_preserves_other_extra_usage() { usage: codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(10.0)), cost: None, wayfinder_usage: None, + inventory: Vec::new(), source_label: "CLI".to_string(), has_successful_claude_cli_quota: false, pace_authoritative: true, @@ -1149,6 +1180,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, + inventory: Vec::new(), source_label: "OAuth".to_string(), has_successful_claude_cli_quota: false, pace_authoritative: true, @@ -1184,6 +1216,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, + inventory: Vec::new(), source_label: "OAuth".to_string(), has_successful_claude_cli_quota: false, pace_authoritative: true, @@ -1218,6 +1251,7 @@ fn claude_repeated_auth_failure_surfaces_error() { usage: codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(42.0)), cost: None, wayfinder_usage: None, + inventory: Vec::new(), source_label: "OAuth".to_string(), has_successful_claude_cli_quota: false, pace_authoritative: true, @@ -1259,6 +1293,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, + inventory: Vec::new(), source_label: "OAuth".to_string(), has_successful_claude_cli_quota: false, pace_authoritative: true, @@ -1311,6 +1346,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, + inventory: Vec::new(), source_label: "Web".to_string(), has_successful_claude_cli_quota: false, pace_authoritative: true, @@ -1360,6 +1396,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, + inventory: Vec::new(), source_label: "CLI".to_string(), has_successful_claude_cli_quota: true, pace_authoritative: true, @@ -1405,6 +1442,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, + inventory: Vec::new(), source_label: "OAuth".to_string(), has_successful_claude_cli_quota: false, pace_authoritative: true, @@ -1575,6 +1613,7 @@ fn japanese_provider_snapshot_localizes_weekly_label() { usage, cost: None, wayfinder_usage: None, + inventory: Vec::new(), source_label: "OAuth".to_string(), has_successful_claude_cli_quota: false, pace_authoritative: true, @@ -1607,6 +1646,7 @@ fn japanese_provider_snapshot_localizes_pace_reserve_description() { usage, cost: None, wayfinder_usage: None, + inventory: Vec::new(), source_label: "OAuth".to_string(), has_successful_claude_cli_quota: false, pace_authoritative: true, diff --git a/apps/desktop-tauri/src-tauri/src/powertoys.rs b/apps/desktop-tauri/src-tauri/src/powertoys.rs index 4f5ce88465..f38f080161 100644 --- a/apps/desktop-tauri/src-tauri/src/powertoys.rs +++ b/apps/desktop-tauri/src-tauri/src/powertoys.rs @@ -195,6 +195,7 @@ mod tests { tertiary: None, tertiary_label: None, extra_rate_windows: Vec::new(), + inventory: Vec::new(), cost: None, plan_name: Some("Team".to_string()), account_email: Some("dev@example.com".to_string()), @@ -243,6 +244,7 @@ mod tests { tertiary: None, tertiary_label: None, extra_rate_windows: Vec::new(), + inventory: Vec::new(), cost: None, plan_name: None, account_email: None, diff --git a/apps/desktop-tauri/src-tauri/src/tray_bridge.rs b/apps/desktop-tauri/src-tauri/src/tray_bridge.rs index 00ba7aff36..0ec2e19b85 100644 --- a/apps/desktop-tauri/src-tauri/src/tray_bridge.rs +++ b/apps/desktop-tauri/src-tauri/src/tray_bridge.rs @@ -1080,6 +1080,7 @@ mod tests { }), tertiary_label: None, extra_rate_windows: Vec::new(), + inventory: Vec::new(), cost: cost.map(|(used, limit)| crate::commands::CostSnapshotBridge { used, limit: Some(limit), diff --git a/apps/desktop-tauri/src-tauri/src/usage_metric.rs b/apps/desktop-tauri/src-tauri/src/usage_metric.rs index 73d642534c..697b94d7a2 100644 --- a/apps/desktop-tauri/src-tauri/src/usage_metric.rs +++ b/apps/desktop-tauri/src-tauri/src/usage_metric.rs @@ -296,6 +296,7 @@ mod tests { tertiary: None, tertiary_label: None, extra_rate_windows: Vec::new(), + inventory: Vec::new(), cost: None, plan_name: None, account_email: None, diff --git a/apps/desktop-tauri/src/components/MenuCardDetails.tsx b/apps/desktop-tauri/src/components/MenuCardDetails.tsx index 4d0b47d24d..8225a10817 100644 --- a/apps/desktop-tauri/src/components/MenuCardDetails.tsx +++ b/apps/desktop-tauri/src/components/MenuCardDetails.tsx @@ -3,6 +3,7 @@ import type { CostSummaryDisplayStyle, DailyCostPoint, PaceSnapshot, + ProviderInventoryItem, ProviderChartData, ProviderLocalUsageSummary, ProviderUsageSnapshot, @@ -413,6 +414,7 @@ function MetricRow({ export interface MenuCardPresence { hasMetrics: boolean; + hasInventory: boolean; hasCost: boolean; hasPace: boolean; hasCharts: boolean; @@ -456,6 +458,7 @@ export function describeCard( const localUsage = provider.error ? null : chartData?.localUsage ?? null; const wayfinderUsage = isWayfinder ? provider.wayfinderUsage : null; const hasMetrics = visibleMetrics.length > 0; + const hasInventory = !provider.error && (provider.inventory?.length ?? 0) > 0; const hasCost = !!provider.cost && (costSummaryDisplayStyle !== "hidden" || provider.cost.alwaysVisible === true); @@ -465,9 +468,16 @@ export function describeCard( !!provider.pace; const hasDetails = !provider.error && - (hasMetrics || hasCost || hasPace || hasCharts || !!localUsage || !!wayfinderUsage); + (hasMetrics || + hasInventory || + hasCost || + hasPace || + hasCharts || + !!localUsage || + !!wayfinderUsage); return { hasMetrics, + hasInventory, hasCost, hasPace, hasCharts, @@ -505,6 +515,7 @@ export default function MenuCardDetails({ const { hasMetrics, + hasInventory, hasCost, hasPace, hasCharts, @@ -540,6 +551,18 @@ export default function MenuCardDetails({ )} + {!provider.error && hasInventory && ( +
+ {provider.inventory?.map((item) => ( + + ))} +
+ )} + {wayfinderUsage && } {hasMetrics && hasCost &&
} @@ -712,3 +735,29 @@ export default function MenuCardDetails({
); } + +function InventoryItemRow({ + item, + resetTimeRelative, +}: { + item: ProviderInventoryItem; + resetTimeRelative: boolean; +}) { + const formattedExpiry = useFormattedResetTime( + item.nextExpiresAt, + null, + resetTimeRelative, + "expires", + ); + + return ( +
+ {item.title}: {item.availableCount} available + {formattedExpiry && ( + + {formattedExpiry} + + )} +
+ ); +} diff --git a/apps/desktop-tauri/src/surfaces/settings/providers/sections/UsageSection.test.tsx b/apps/desktop-tauri/src/surfaces/settings/providers/sections/UsageSection.test.tsx index 1a4807c882..c32df0cf15 100644 --- a/apps/desktop-tauri/src/surfaces/settings/providers/sections/UsageSection.test.tsx +++ b/apps/desktop-tauri/src/surfaces/settings/providers/sections/UsageSection.test.tsx @@ -102,4 +102,27 @@ describe("UsageSection", () => { expect(label.parentElement).toHaveTextContent("No active 5h session"); expect(label.parentElement?.querySelector(".provider-usage-bar__track")).toBeNull(); }); + + it("renders discrete inventory without turning it into a quota bar", async () => { + const detail = provider(); + detail.session = null; + detail.extraRateWindows = []; + detail.inventory = [ + { + id: "reset-credits", + title: "Limit Reset Credits", + availableCount: 2, + nextExpiresAt: "2099-01-01T00:00:00Z", + }, + ]; + + const { container } = render( + + key} /> + , + ); + + expect(await screen.findByText(/Limit Reset Credits: 2 available/)).toBeInTheDocument(); + expect(container.querySelector(".provider-usage-bar__track")).toBeNull(); + }); }); diff --git a/apps/desktop-tauri/src/surfaces/settings/providers/sections/UsageSection.tsx b/apps/desktop-tauri/src/surfaces/settings/providers/sections/UsageSection.tsx index a51c294ed2..64a3fa7b6a 100644 --- a/apps/desktop-tauri/src/surfaces/settings/providers/sections/UsageSection.tsx +++ b/apps/desktop-tauri/src/surfaces/settings/providers/sections/UsageSection.tsx @@ -1,4 +1,5 @@ import type { + ProviderInventoryItem, ProviderDetail, RateWindowSnapshot, } from "../../../../types/bridge"; @@ -60,7 +61,8 @@ export function UsageSection({ provider, resetTimeRelative, t }: Props) { }); } - if (bars.length === 0) { + const inventory = provider.inventory ?? []; + if (bars.length === 0 && inventory.length === 0) { return null; } @@ -76,10 +78,39 @@ export function UsageSection({ provider, resetTimeRelative, t }: Props) { t={t} /> ))} + {inventory.map((item) => ( + + ))} ); } +function InventoryRow({ + item, + resetTimeRelative, +}: { + item: ProviderInventoryItem; + resetTimeRelative: boolean; +}) { + const formattedExpiry = useFormattedResetTime( + item.nextExpiresAt, + null, + resetTimeRelative, + "expires", + ); + + return ( +
+ {item.title}: {item.availableCount} available + {formattedExpiry && {formattedExpiry}} +
+ ); +} + function UsageBar({ label, rate, diff --git a/apps/desktop-tauri/src/types/bridge.ts b/apps/desktop-tauri/src/types/bridge.ts index 865625efeb..963779ae2f 100644 --- a/apps/desktop-tauri/src/types/bridge.ts +++ b/apps/desktop-tauri/src/types/bridge.ts @@ -592,6 +592,13 @@ export interface SubscriptionMetadataSnapshot { renewsAt: string | null; } +export interface ProviderInventoryItem { + id: string; + title: string; + availableCount: number; + nextExpiresAt: string | null; +} + /** Backend-classified provider availability state (camelCase serde on the bridge). */ export type ProviderStateKind = | "ready" @@ -618,6 +625,8 @@ export interface ProviderUsageSnapshot { title: string; window: RateWindowSnapshot; }>; + /** Display-only discrete provider inventory; never used as quota math. */ + inventory?: ProviderInventoryItem[]; cost: CostSnapshotBridge | null; planName: string | null; accountEmail: string | null; @@ -892,6 +901,8 @@ export interface ProviderDetail { title: string; window: RateWindowSnapshot; }>; + /** Display-only discrete provider inventory; never used as quota math. */ + inventory?: ProviderInventoryItem[]; cost: CostSnapshotBridge | null; pace: PaceSnapshot | null; diff --git a/rust/src/cli/usage.rs b/rust/src/cli/usage.rs index e0c19618d9..25b988e5c0 100755 --- a/rust/src/cli/usage.rs +++ b/rust/src/cli/usage.rs @@ -1,11 +1,13 @@ //! Usage command implementation +use chrono::{DateTime, Utc}; use clap::Args; use serde::Serialize; use crate::core::{ - CostSnapshot, FetchContext, ProviderFetchResult, ProviderId, RateWindow, SourceMode, - TokenAccountStore, TokenAccountSupport, UsagePace, UsageSnapshot, instantiate_provider, + CostSnapshot, FetchContext, ProviderFetchResult, ProviderId, ProviderInventoryItem, RateWindow, + SourceMode, TokenAccountStore, TokenAccountSupport, UsagePace, UsageSnapshot, + instantiate_provider, }; use crate::settings::ApiKeys; use crate::status::{ProviderStatus as StatusInfo, StatusLevel, fetch_provider_status}; @@ -396,6 +398,23 @@ fn render_json_result( }); } + if !result.inventory.is_empty() { + json_result["inventory"] = serde_json::Value::Array( + result + .inventory + .iter() + .map(|item| { + serde_json::json!({ + "id": &item.id, + "title": &item.title, + "availableCount": item.available_count, + "nextExpiresAt": item.next_expires_at.map(|date| date.to_rfc3339()), + }) + }) + .collect(), + ); + } + if let Some(s) = status { json_result["status"] = serde_json::json!({ "level": format!("{:?}", s.level).to_lowercase(), @@ -460,6 +479,7 @@ pub fn render_text_with_status( append_status_line(&mut lines, status); append_account_lines(&mut lines, &result.usage); append_usage_window_lines(&mut lines, &result.usage, &metadata, use_color); + append_inventory_lines(&mut lines, &result.inventory); append_cost_line(&mut lines, result.cost.as_ref()); lines.join("\n") @@ -579,6 +599,38 @@ fn append_usage_window_lines( } } +fn append_inventory_lines(lines: &mut Vec, inventory: &[ProviderInventoryItem]) { + if inventory.is_empty() { + return; + } + let now = Utc::now(); + for item in inventory { + lines.push(format!( + " {}: {} available", + item.title, item.available_count + )); + if let Some(expires_at) = item.next_expires_at { + lines.push(format!( + " Next expires in {}", + format_inventory_countdown(expires_at, now) + )); + } + } +} + +fn format_inventory_countdown(expires_at: DateTime, now: DateTime) -> String { + let seconds = expires_at.signed_duration_since(now).num_seconds(); + if seconds <= 0 { + return "now".to_string(); + } + let minutes = (seconds + 59) / 60; + if minutes >= 24 * 60 { + format!("{}d {}h", minutes / (24 * 60), (minutes / 60) % 24) + } else { + format!("{}h {}m", minutes / 60, minutes % 60) + } +} + fn append_window_line(lines: &mut Vec, label: &str, window: &RateWindow, use_color: bool) { if window.is_informational { let description = window.reset_description.as_deref().unwrap_or("unavailable"); @@ -935,6 +987,50 @@ mod tests { ); } + #[test] + fn inventory_is_rendered_in_full_text_but_not_brief_text() { + let result = fetch_result(UsageSnapshot::new(RateWindow::new(10.0))).with_inventory_item( + ProviderInventoryItem { + id: "reset-credits".to_string(), + title: "Limit Reset Credits".to_string(), + available_count: 2, + next_expires_at: Some(Utc::now() + chrono::Duration::hours(3)), + }, + ); + + let full = render_text_with_status(ProviderId::Grok, &result, None, false); + let brief = render_brief_text(ProviderId::Grok, &result); + + assert!(full.contains("Limit Reset Credits: 2 available")); + assert!(full.contains("Next expires in")); + assert!(!brief.contains("Limit Reset Credits")); + } + + #[test] + fn json_inventory_is_additive_and_contains_no_redemption_token() { + let result = fetch_result(UsageSnapshot::new(RateWindow::new(10.0))).with_inventory_item( + ProviderInventoryItem { + id: "reset-credits".to_string(), + title: "Limit Reset Credits".to_string(), + available_count: 1, + next_expires_at: None, + }, + ); + + let json = render_json_result(ProviderId::Grok, result, None); + assert_eq!(json["inventory"][0]["availableCount"], 1); + assert!( + serde_json::to_string(&json) + .unwrap() + .contains("reset-credits") + ); + assert!( + !serde_json::to_string(&json) + .unwrap() + .contains("coupon-token-secret") + ); + } + #[test] fn secondary_label_override_is_shared_by_full_and_brief_renderers() { let result = fetch_result( diff --git a/rust/src/core/usage_snapshot.rs b/rust/src/core/usage_snapshot.rs index 3cfcd2ccf2..b0a1440814 100755 --- a/rust/src/core/usage_snapshot.rs +++ b/rust/src/core/usage_snapshot.rs @@ -85,6 +85,21 @@ pub struct NamedRateWindow { pub usage_known: bool, } +/// One display-only item of provider-issued discrete inventory. +/// +/// This is deliberately separate from [`RateWindow`]: inventory does not +/// represent a percentage quota and must not participate in quota arithmetic, +/// tray metric selection, pace, notifications, or auto-resume decisions. +/// Provider-specific redemption identifiers stay private to the provider +/// parser and never enter this type. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ProviderInventoryItem { + pub id: String, + pub title: String, + pub available_count: u32, + pub next_expires_at: Option>, +} + fn named_rate_window_usage_known_default() -> bool { true } @@ -585,6 +600,13 @@ pub struct ProviderFetchResult { #[serde(skip_serializing_if = "Option::is_none")] pub wayfinder_usage: Option, + /// Transient non-quota inventory for provider-specific display. + /// + /// The field is intentionally skipped by serde: it belongs to the current + /// fetch and must not change persisted `ProviderFetchResult` JSON. + #[serde(skip)] + pub inventory: Vec, + /// Label describing the data source (e.g., "oauth", "web", "cli") pub source_label: String, @@ -613,6 +635,7 @@ impl ProviderFetchResult { usage, cost: None, wayfinder_usage: None, + inventory: Vec::new(), source_label: source_label.into(), has_successful_claude_cli_quota: false, pace_authoritative: true, @@ -652,6 +675,12 @@ impl ProviderFetchResult { self.wayfinder_usage = Some(usage); self } + + /// Attach one display-only inventory item without exposing redemption IDs. + pub fn with_inventory_item(mut self, item: ProviderInventoryItem) -> Self { + self.inventory.push(item); + self + } } #[cfg(test)] @@ -669,6 +698,27 @@ mod tests { ); } + #[test] + fn fetch_result_inventory_is_transient_and_not_serialized() { + let usage = UsageSnapshot::new(RateWindow::new(25.0)); + let expiry = DateTime::::from_timestamp(1_900_000_000, 0).unwrap(); + let result = + ProviderFetchResult::new(usage, "api").with_inventory_item(ProviderInventoryItem { + id: "reset-credits".to_string(), + title: "Limit Reset Credits".to_string(), + available_count: 2, + next_expires_at: Some(expiry), + }); + + assert_eq!(result.inventory.len(), 1); + let encoded = serde_json::to_value(&result).unwrap(); + assert!(encoded.get("inventory").is_none()); + assert!(encoded.get("reset-credits").is_none()); + + let decoded: ProviderFetchResult = serde_json::from_value(encoded).unwrap(); + assert!(decoded.inventory.is_empty()); + } + #[test] fn cost_snapshot_ignores_non_finite_values() { let cost = CostSnapshot::new(f64::NAN, "USD", "Monthly").with_limit(f64::INFINITY); From 4468703a92a7166e84ad2a44290af2ffe3d0d01b Mon Sep 17 00:00:00 2001 From: NessZerra <90105158+Finesssee@users.noreply.github.com> Date: Sat, 19 Sep 2026 22:47:04 +0700 Subject: [PATCH 2/9] Add transient provider detail carrier --- .../src-tauri/src/commands/bridge.rs | 37 ++++ .../src-tauri/src/commands/provider_detail.rs | 3 + .../src-tauri/src/commands/providers.rs | 1 + .../src-tauri/src/commands/tests.rs | 29 ++- apps/desktop-tauri/src-tauri/src/powertoys.rs | 2 + .../src-tauri/src/tray_bridge.rs | 1 + .../src-tauri/src/usage_metric.rs | 1 + .../src/components/MenuCardDetails.tsx | 37 ++++ .../providers/sections/UsageSection.tsx | 28 ++- apps/desktop-tauri/src/types/bridge.ts | 18 ++ rust/src/cli/usage.rs | 62 ++++++ rust/src/core/usage_snapshot.rs | 179 ++++++++++++++++++ 12 files changed, 394 insertions(+), 4 deletions(-) diff --git a/apps/desktop-tauri/src-tauri/src/commands/bridge.rs b/apps/desktop-tauri/src-tauri/src/commands/bridge.rs index 1a86eab7b4..96f05a366a 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/bridge.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/bridge.rs @@ -189,6 +189,25 @@ pub struct ProviderInventoryItemSnapshot { pub next_expires_at: Option, } +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ProviderDisplayProgressSnapshot { + pub used: f64, + pub total: f64, +} + +/// Display-only provider detail row. It never participates in quota math or +/// core persistence and contains values validated by the provider carrier. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ProviderDisplayDetailSnapshot { + pub id: String, + pub title: String, + pub value: String, + pub secondary_value: Option, + pub progress: Option, +} + /// A frontend-friendly snapshot of one provider's usage data. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -213,6 +232,8 @@ pub struct ProviderUsageSnapshot { pub extra_rate_windows: Vec, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub inventory: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub display_details: Vec, #[serde(default)] pub cost: Option, #[serde(default)] @@ -407,6 +428,21 @@ impl ProviderUsageSnapshot { next_expires_at: item.next_expires_at.map(|date| date.to_rfc3339()), }) .collect(), + display_details: result + .display_details() + .map(|detail| ProviderDisplayDetailSnapshot { + id: detail.id().to_string(), + title: detail.title().to_string(), + value: detail.value().to_string(), + secondary_value: detail.secondary_value().map(ToOwned::to_owned), + progress: detail + .progress() + .map(|progress| ProviderDisplayProgressSnapshot { + used: progress.used(), + total: progress.total(), + }), + }) + .collect(), cost: result.cost.as_ref().map(|c| CostSnapshotBridge { used: c.used, limit: c.limit, @@ -485,6 +521,7 @@ impl ProviderUsageSnapshot { tertiary_label: None, extra_rate_windows: Vec::new(), inventory: Vec::new(), + display_details: Vec::new(), cost: None, plan_name: None, account_email: None, diff --git a/apps/desktop-tauri/src-tauri/src/commands/provider_detail.rs b/apps/desktop-tauri/src-tauri/src/commands/provider_detail.rs index 18b1def39b..df5dd7d9c6 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/provider_detail.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/provider_detail.rs @@ -27,6 +27,7 @@ pub struct ProviderDetail { pub tertiary: Option, pub extra_rate_windows: Vec, pub inventory: Vec, + pub display_details: Vec, // Cost / pace. pub cost: Option, @@ -93,6 +94,7 @@ pub(crate) fn build_provider_detail(provider_id: &str) -> Result 0; const hasInventory = !provider.error && (provider.inventory?.length ?? 0) > 0; + const hasDisplayDetails = !provider.error && (provider.displayDetails?.length ?? 0) > 0; const hasCost = !!provider.cost && (costSummaryDisplayStyle !== "hidden" || provider.cost.alwaysVisible === true); @@ -470,6 +473,7 @@ export function describeCard( !provider.error && (hasMetrics || hasInventory || + hasDisplayDetails || hasCost || hasPace || hasCharts || @@ -478,6 +482,7 @@ export function describeCard( return { hasMetrics, hasInventory, + hasDisplayDetails, hasCost, hasPace, hasCharts, @@ -516,6 +521,7 @@ export default function MenuCardDetails({ const { hasMetrics, hasInventory, + hasDisplayDetails, hasCost, hasPace, hasCharts, @@ -563,6 +569,14 @@ export default function MenuCardDetails({ )} + {!provider.error && hasDisplayDetails && ( +
+ {provider.displayDetails?.map((detail, index) => ( + + ))} +
+ )} + {wayfinderUsage && } {hasMetrics && hasCost &&
} @@ -761,3 +775,26 @@ function InventoryItemRow({
); } + +function DisplayDetailRow({ detail }: { detail: ProviderDisplayDetail }) { + const progress = detail.progress; + const progressPercent = progress && Number.isFinite(progress.used) && Number.isFinite(progress.total) && progress.total > 0 + ? Math.max(0, Math.min(100, (progress.used / progress.total) * 100)) + : null; + + return ( +
+
+ {detail.title}: {detail.value} + {detail.secondaryValue && ( + {detail.secondaryValue} + )} +
+ {progressPercent != null && ( +
+
+
+ )} +
+ ); +} diff --git a/apps/desktop-tauri/src/surfaces/settings/providers/sections/UsageSection.tsx b/apps/desktop-tauri/src/surfaces/settings/providers/sections/UsageSection.tsx index 64a3fa7b6a..84323eab68 100644 --- a/apps/desktop-tauri/src/surfaces/settings/providers/sections/UsageSection.tsx +++ b/apps/desktop-tauri/src/surfaces/settings/providers/sections/UsageSection.tsx @@ -1,4 +1,5 @@ import type { + ProviderDisplayDetail, ProviderInventoryItem, ProviderDetail, RateWindowSnapshot, @@ -62,7 +63,8 @@ export function UsageSection({ provider, resetTimeRelative, t }: Props) { } const inventory = provider.inventory ?? []; - if (bars.length === 0 && inventory.length === 0) { + const displayDetails = provider.displayDetails ?? []; + if (bars.length === 0 && inventory.length === 0 && displayDetails.length === 0) { return null; } @@ -85,6 +87,9 @@ export function UsageSection({ provider, resetTimeRelative, t }: Props) { resetTimeRelative={resetTimeRelative} /> ))} + {displayDetails.map((detail, index) => ( + + ))} ); } @@ -111,6 +116,27 @@ function InventoryRow({ ); } +function DisplayDetailRow({ detail }: { detail: ProviderDisplayDetail }) { + const progress = detail.progress; + const progressPercent = progress && Number.isFinite(progress.used) && Number.isFinite(progress.total) && progress.total > 0 + ? Math.max(0, Math.min(100, (progress.used / progress.total) * 100)) + : null; + + return ( +
+
+ {detail.title}: {detail.value} + {detail.secondaryValue && {detail.secondaryValue}} +
+ {progressPercent != null && ( +
+
+
+ )} +
+ ); +} + function UsageBar({ label, rate, diff --git a/apps/desktop-tauri/src/types/bridge.ts b/apps/desktop-tauri/src/types/bridge.ts index 963779ae2f..7fd4af94d1 100644 --- a/apps/desktop-tauri/src/types/bridge.ts +++ b/apps/desktop-tauri/src/types/bridge.ts @@ -599,6 +599,20 @@ export interface ProviderInventoryItem { nextExpiresAt: string | null; } +export interface ProviderDisplayProgress { + used: number; + total: number; +} + +/** Transient provider detail row; it is display-only and never quota math. */ +export interface ProviderDisplayDetail { + id: string; + title: string; + value: string; + secondaryValue: string | null; + progress: ProviderDisplayProgress | null; +} + /** Backend-classified provider availability state (camelCase serde on the bridge). */ export type ProviderStateKind = | "ready" @@ -627,6 +641,8 @@ export interface ProviderUsageSnapshot { }>; /** Display-only discrete provider inventory; never used as quota math. */ inventory?: ProviderInventoryItem[]; + /** Provider-specific display rows; never used as quota math or persistence. */ + displayDetails?: ProviderDisplayDetail[]; cost: CostSnapshotBridge | null; planName: string | null; accountEmail: string | null; @@ -903,6 +919,8 @@ export interface ProviderDetail { }>; /** Display-only discrete provider inventory; never used as quota math. */ inventory?: ProviderInventoryItem[]; + /** Provider-specific display rows; never used as quota math or persistence. */ + displayDetails?: ProviderDisplayDetail[]; cost: CostSnapshotBridge | null; pace: PaceSnapshot | null; diff --git a/rust/src/cli/usage.rs b/rust/src/cli/usage.rs index 25b988e5c0..9697b883c7 100755 --- a/rust/src/cli/usage.rs +++ b/rust/src/cli/usage.rs @@ -415,6 +415,28 @@ fn render_json_result( ); } + if result.display_details().next().is_some() { + json_result["details"] = serde_json::Value::Array( + result + .display_details() + .map(|detail| { + serde_json::json!({ + "id": detail.id(), + "title": detail.title(), + "value": detail.value(), + "secondaryValue": detail.secondary_value(), + "progress": detail.progress().map(|progress| { + serde_json::json!({ + "used": progress.used(), + "total": progress.total(), + }) + }), + }) + }) + .collect(), + ); + } + if let Some(s) = status { json_result["status"] = serde_json::json!({ "level": format!("{:?}", s.level).to_lowercase(), @@ -480,6 +502,7 @@ pub fn render_text_with_status( append_account_lines(&mut lines, &result.usage); append_usage_window_lines(&mut lines, &result.usage, &metadata, use_color); append_inventory_lines(&mut lines, &result.inventory); + append_display_detail_lines(&mut lines, result.display_details()); append_cost_line(&mut lines, result.cost.as_ref()); lines.join("\n") @@ -618,6 +641,29 @@ fn append_inventory_lines(lines: &mut Vec, inventory: &[ProviderInventor } } +fn append_display_detail_lines<'a>( + lines: &mut Vec, + details: impl IntoIterator, +) { + for detail in details { + let secondary = detail + .secondary_value() + .map(|value| format!(" ({value})")) + .unwrap_or_default(); + let progress = detail + .progress() + .map(|value| format!(" [{:.2}/{:.2}]", value.used(), value.total())) + .unwrap_or_default(); + lines.push(format!( + " {}: {}{}{}", + detail.title(), + detail.value(), + secondary, + progress + )); + } +} + fn format_inventory_countdown(expires_at: DateTime, now: DateTime) -> String { let seconds = expires_at.signed_duration_since(now).num_seconds(); if seconds <= 0 { @@ -1031,6 +1077,22 @@ mod tests { ); } + #[test] + fn display_details_are_rendered_in_full_text_and_json() { + let result = fetch_result(UsageSnapshot::new(RateWindow::new(10.0))).with_display_detail( + crate::core::ProviderDisplayDetail::new("credits", "Used this cycle", "12") + .with_secondary_value("Monthly refill: 100") + .with_progress(12.0, 100.0), + ); + + let full = render_text_with_status(ProviderId::Grok, &result, None, false); + let json = render_json_result(ProviderId::Grok, result, None); + + assert!(full.contains("Used this cycle: 12 (Monthly refill: 100) [12.00/100.00]")); + assert_eq!(json["details"][0]["title"], "Used this cycle"); + assert_eq!(json["details"][0]["progress"]["total"], 100.0); + } + #[test] fn secondary_label_override_is_shared_by_full_and_brief_renderers() { let result = fetch_result( diff --git a/rust/src/core/usage_snapshot.rs b/rust/src/core/usage_snapshot.rs index b0a1440814..86df6c0caa 100755 --- a/rust/src/core/usage_snapshot.rs +++ b/rust/src/core/usage_snapshot.rs @@ -100,6 +100,121 @@ pub struct ProviderInventoryItem { pub next_expires_at: Option>, } +/// One transient provider detail row for display surfaces. +/// +/// These rows are intentionally separate from quota windows and inventory: +/// providers may report credit balances, subscription metadata, or other +/// values that must be shown without becoming quota math or persisted core +/// fetch state. The builder validates compact, display-safe values before a +/// row enters a fetch result; the desktop bridge may then export those rows +/// as part of its current display snapshot. +#[derive(Debug, Clone, PartialEq)] +pub struct ProviderDisplayDetail { + id: String, + title: String, + value: String, + secondary_value: Option, + progress: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct ProviderDisplayProgress { + used: f64, + total: f64, +} + +impl ProviderDisplayDetail { + pub fn new(id: impl Into, title: impl Into, value: impl Into) -> Self { + Self { + id: id.into(), + title: title.into(), + value: value.into(), + secondary_value: None, + progress: None, + } + } + + pub fn with_secondary_value(mut self, value: impl Into) -> Self { + self.secondary_value = Some(value.into()); + self + } + + pub fn with_progress(mut self, used: f64, total: f64) -> Self { + if used.is_finite() && total.is_finite() && used >= 0.0 && total > 0.0 { + self.progress = Some(ProviderDisplayProgress { used, total }); + } + self + } + + pub fn id(&self) -> &str { + &self.id + } + + pub fn title(&self) -> &str { + &self.title + } + + pub fn value(&self) -> &str { + &self.value + } + + pub fn secondary_value(&self) -> Option<&str> { + self.secondary_value.as_deref() + } + + pub fn progress(&self) -> Option { + self.progress + } + + fn is_display_safe(&self) -> bool { + is_display_safe_text(&self.id, 64) + && is_display_safe_text(&self.title, 128) + && is_display_safe_text(&self.value, 512) + && self + .secondary_value + .as_deref() + .is_none_or(|value| is_display_safe_text(value, 512)) + && self.progress.is_none_or(|progress| { + progress.used.is_finite() + && progress.total.is_finite() + && progress.used >= 0.0 + && progress.total > 0.0 + }) + } +} + +impl ProviderDisplayProgress { + pub fn used(&self) -> f64 { + self.used + } + + pub fn total(&self) -> f64 { + self.total + } +} + +fn is_display_safe_text(value: &str, max_len: usize) -> bool { + if value.is_empty() || value.chars().count() > max_len || value.chars().any(char::is_control) { + return false; + } + + let lower = value.to_ascii_lowercase(); + [ + "authorization:", + "bearer ", + "cookie:", + "set-cookie:", + "access_token", + "api_key", + "api-key", + "client_secret", + "refresh_token", + "x-api-key", + ] + .iter() + .all(|marker| !lower.contains(marker)) +} + fn named_rate_window_usage_known_default() -> bool { true } @@ -607,6 +722,12 @@ pub struct ProviderFetchResult { #[serde(skip)] pub inventory: Vec, + /// Transient provider-specific detail rows for display only. They are not + /// serialized by the core result; use [`Self::display_details`] for an + /// explicit surface projection. + #[serde(skip)] + pub display_details: Vec, + /// Label describing the data source (e.g., "oauth", "web", "cli") pub source_label: String, @@ -636,6 +757,7 @@ impl ProviderFetchResult { cost: None, wayfinder_usage: None, inventory: Vec::new(), + display_details: Vec::new(), source_label: source_label.into(), has_successful_claude_cli_quota: false, pace_authoritative: true, @@ -681,6 +803,20 @@ impl ProviderFetchResult { self.inventory.push(item); self } + + /// Attach one transient provider-specific detail row without persisting it. + pub fn with_display_detail(mut self, detail: ProviderDisplayDetail) -> Self { + if detail.is_display_safe() && !self.display_details.iter().any(|row| row.id == detail.id) { + self.display_details.push(detail); + } + self + } + + pub fn display_details(&self) -> impl Iterator { + self.display_details + .iter() + .filter(|detail| detail.is_display_safe()) + } } #[cfg(test)] @@ -719,6 +855,49 @@ mod tests { assert!(decoded.inventory.is_empty()); } + #[test] + fn fetch_result_display_details_are_transient_and_validate_progress() { + let usage = UsageSnapshot::new(RateWindow::new(25.0)); + let result = ProviderFetchResult::new(usage, "web").with_display_detail( + ProviderDisplayDetail::new("credits", "Used this cycle", "12") + .with_secondary_value("Monthly refill: 100") + .with_progress(12.0, 100.0), + ); + + let details: Vec<_> = result.display_details().collect(); + assert_eq!(details.len(), 1); + assert!(details[0].progress().is_some()); + assert!( + ProviderDisplayDetail::new("invalid", "Invalid", "value") + .with_progress(f64::NAN, 1.0) + .progress + .is_none() + ); + let encoded = serde_json::to_value(&result).unwrap(); + assert!(encoded.get("display_details").is_none()); + } + + #[test] + fn display_details_reject_secret_markers_and_duplicate_ids() { + let usage = UsageSnapshot::new(RateWindow::new(25.0)); + let result = ProviderFetchResult::new(usage, "web") + .with_display_detail(ProviderDisplayDetail::new("credits", "Credits", "12")) + .with_display_detail(ProviderDisplayDetail::new( + "credits", + "Credits duplicate", + "13", + )) + .with_display_detail(ProviderDisplayDetail::new( + "secret", + "Authorization", + "Bearer hidden", + )); + + let details: Vec<_> = result.display_details().collect(); + assert_eq!(details.len(), 1); + assert_eq!(details[0].value(), "12"); + } + #[test] fn cost_snapshot_ignores_non_finite_values() { let cost = CostSnapshot::new(f64::NAN, "USD", "Monthly").with_limit(f64::INFINITY); From c617ec7a25243630c51fb027a30f2f417e7eaa9e Mon Sep 17 00:00:00 2001 From: NessZerra <90105158+Finesssee@users.noreply.github.com> Date: Sat, 19 Sep 2026 23:05:27 +0700 Subject: [PATCH 3/9] Port Venice web subscription credits --- .../src-tauri/src/commands/tests.rs | 26 ++ .../sections/UsageSourceSection.test.tsx | 5 + .../providers/sections/usageSourcePolicy.ts | 19 + rust/src/core/provider.rs | 3 +- rust/src/providers/venice/mod.rs | 386 +++++++++++++++++- 5 files changed, 432 insertions(+), 7 deletions(-) diff --git a/apps/desktop-tauri/src-tauri/src/commands/tests.rs b/apps/desktop-tauri/src-tauri/src/commands/tests.rs index a81fd4e41f..804e8c7fd7 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/tests.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/tests.rs @@ -1020,6 +1020,32 @@ fn provider_inventory_maps_to_the_bridge_without_token_ids() { assert!(!serialized.contains("coupon-token-secret")); } +#[test] +fn venice_display_details_map_to_the_bridge_without_identity() { + let metadata = instantiate_provider(ProviderId::Venice).metadata().clone(); + let result = ProviderFetchResult::new( + codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::informational( + "Venice web credits", + )), + "web", + ) + .with_display_detail( + ProviderDisplayDetail::new("used-this-cycle", "Used this cycle", "12") + .with_secondary_value("Monthly refill: 100") + .with_progress(12.0, 100.0), + ); + + let snapshot = + ProviderUsageSnapshot::from_fetch_result(ProviderId::Venice, &metadata, &result, None); + + assert!(snapshot.primary.is_informational); + assert_eq!(snapshot.source_label, "web"); + assert_eq!(snapshot.display_details.len(), 1); + assert_eq!(snapshot.display_details[0].id, "used-this-cycle"); + assert_eq!(snapshot.account_email, None); + assert_eq!(snapshot.account_organization, None); +} + #[test] fn provider_cache_is_fresh_inside_stale_window() { assert!(super::is_provider_cache_fresh( diff --git a/apps/desktop-tauri/src/surfaces/settings/providers/sections/UsageSourceSection.test.tsx b/apps/desktop-tauri/src/surfaces/settings/providers/sections/UsageSourceSection.test.tsx index f16b2de5d8..77087ce037 100644 --- a/apps/desktop-tauri/src/surfaces/settings/providers/sections/UsageSourceSection.test.tsx +++ b/apps/desktop-tauri/src/surfaces/settings/providers/sections/UsageSourceSection.test.tsx @@ -27,6 +27,11 @@ describe("usage source policy", () => { "auto", "cli", ]); + expect(usageSourcePolicy("venice")?.options.map((option) => option.value)).toEqual([ + "auto", + "oauth", + "web", + ]); expect(usageSourcePolicy("antigravity")?.options[0].description).toContain( "skips agy reports without account identity", ); diff --git a/apps/desktop-tauri/src/surfaces/settings/providers/sections/usageSourcePolicy.ts b/apps/desktop-tauri/src/surfaces/settings/providers/sections/usageSourcePolicy.ts index 90b88c4ff2..61a8f05c7e 100644 --- a/apps/desktop-tauri/src/surfaces/settings/providers/sections/usageSourcePolicy.ts +++ b/apps/desktop-tauri/src/surfaces/settings/providers/sections/usageSourcePolicy.ts @@ -40,6 +40,25 @@ const POLICIES: Readonly> = { }, ], }, + venice: { + options: [ + { + value: "auto", + label: "Auto", + description: "Uses the Venice API key or token account; browser sessions are used only when Web is selected.", + }, + { + value: "oauth", + label: "API", + description: "Uses the Venice API key or token account only.", + }, + { + value: "web", + label: "Browser session", + description: "Reads Venice subscription credits from the selected browser session or manual cookie header.", + }, + ], + }, }; export function usageSourcePolicy(providerId: string): UsageSourcePolicy | null { diff --git a/rust/src/core/provider.rs b/rust/src/core/provider.rs index 55ab70d391..efa87b0187 100755 --- a/rust/src/core/provider.rs +++ b/rust/src/core/provider.rs @@ -352,6 +352,7 @@ impl ProviderId { ProviderId::MiMo => Some("platform.xiaomimimo.com"), ProviderId::CommandCode => Some("commandcode.ai"), ProviderId::Grok => Some("grok.com"), + ProviderId::Venice => Some("venice.ai"), ProviderId::Qoder => Some("qoder.com"), ProviderId::CodeBuddy => Some("codebuddy.cn"), ProviderId::Sakana => Some("console.sakana.ai"), @@ -378,7 +379,6 @@ impl ProviderId { ProviderId::Doubao => None, ProviderId::Crof => None, ProviderId::StepFun => None, - ProviderId::Venice => None, ProviderId::OpenAIApi => None, ProviderId::ElevenLabs => None, ProviderId::Deepgram => None, @@ -1177,6 +1177,7 @@ mod tests { assert_eq!(ProviderId::Kiro.cookie_domain(), Some("kiro.dev")); assert_eq!(ProviderId::Kimi.cookie_domain(), Some("kimi.moonshot.cn")); assert_eq!(ProviderId::OpenCode.cookie_domain(), Some("opencode.ai")); + assert_eq!(ProviderId::Venice.cookie_domain(), Some("venice.ai")); // Token-based providers (no cookies) assert_eq!(ProviderId::Copilot.cookie_domain(), None); diff --git a/rust/src/providers/venice/mod.rs b/rust/src/providers/venice/mod.rs index 9ff20acf44..a3230aa545 100644 --- a/rust/src/providers/venice/mod.rs +++ b/rust/src/providers/venice/mod.rs @@ -3,16 +3,26 @@ //! Fetches API balance data from Venice's billing endpoint. use async_trait::async_trait; +use chrono::{DateTime, Utc}; use reqwest::Client; use serde::Deserialize; +use serde_json::Value; +use std::collections::BTreeMap; use crate::core::{ - FetchContext, Provider, ProviderError, ProviderFetchResult, ProviderId, ProviderMetadata, - RateWindow, SourceMode, UsageSnapshot, + FetchContext, Provider, ProviderDisplayDetail, ProviderError, ProviderFetchResult, ProviderId, + ProviderMetadata, RateWindow, SourceMode, UsageSnapshot, }; const VENICE_BALANCE_URL: &str = "https://api.venice.ai/api/v1/billing/balance"; +const VENICE_SESSION_URL: &str = "https://outerface.venice.ai/api/user/session"; const VENICE_CREDENTIAL_TARGET: &str = "codexbar-venice"; +const VENICE_SESSION_COOKIE: &str = "__venice-auth.session-token"; +const VENICE_COOKIE_DOMAINS: &[&str] = &["venice.ai", "outerface.venice.ai"]; +const VENICE_EXPIRATION_SKEW_SECS: i64 = 60; +const MAX_VENICE_COOKIE_HEADER_LEN: usize = 1_048_576; +const MAX_VENICE_COOKIE_VALUE_LEN: usize = 16_384; +const MAX_VENICE_COOKIE_CHUNKS: usize = 64; #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] @@ -29,6 +39,11 @@ struct VeniceBalances { usd: Option, } +#[derive(Debug, Deserialize)] +struct VeniceSessionResponse { + token: Option, +} + pub struct VeniceProvider { metadata: ProviderMetadata, client: Client, @@ -87,6 +102,55 @@ impl VeniceProvider { .map_err(|e| ProviderError::Parse(format!("Failed to parse Venice balance: {e}")))?; Ok(snapshot_from_balance(&balance)) } + + async fn fetch_web( + &self, + manual_cookie_header: Option<&str>, + ) -> Result { + let raw_cookie_header = match manual_cookie_header { + Some(header) => header.to_string(), + None => crate::providers::browser_cookie_header(VENICE_COOKIE_DOMAINS)?, + }; + let cookie_header = + session_cookie_header(&raw_cookie_header).ok_or(ProviderError::NoCookies)?; + + let response = self + .client + .get(VENICE_SESSION_URL) + .header("Cookie", cookie_header) + .header("Accept", "application/json") + .send() + .await?; + + if response.status() == reqwest::StatusCode::UNAUTHORIZED + || response.status() == reqwest::StatusCode::FORBIDDEN + { + return Err(ProviderError::AuthRequired); + } + if !response.status().is_success() { + return Err(ProviderError::Other(format!( + "Venice web session returned status {}", + response.status() + ))); + } + + let session: VeniceSessionResponse = response.json().await.map_err(|e| { + ProviderError::Parse(format!("Failed to parse Venice web session: {e}")) + })?; + let token = session + .token + .as_deref() + .filter(|token| !token.trim().is_empty()) + .ok_or(ProviderError::AuthRequired)?; + let claims = crate::codex_accounts::api::jwt_payload(token) + .ok_or_else(|| ProviderError::Parse("Venice session token is not a JWT".into()))?; + let (usage, details) = snapshot_from_web_claims(&claims, Utc::now())?; + let mut result = ProviderFetchResult::new(usage, "web").with_non_authoritative_pace(); + for detail in details { + result = result.with_display_detail(detail); + } + Ok(result) + } } fn snapshot_from_balance(balance: &VeniceBalanceResponse) -> UsageSnapshot { @@ -158,14 +222,224 @@ impl Provider for VeniceProvider { "api", )) } - SourceMode::Web | SourceMode::Cli => { - Err(ProviderError::UnsupportedSource(ctx.source_mode)) - } + SourceMode::Web => self.fetch_web(ctx.manual_cookie_header.as_deref()).await, + SourceMode::Cli => Err(ProviderError::UnsupportedSource(ctx.source_mode)), } } fn available_sources(&self) -> Vec { - vec![SourceMode::Auto, SourceMode::OAuth] + vec![SourceMode::Auto, SourceMode::OAuth, SourceMode::Web] + } + + fn supports_web(&self) -> bool { + true + } + + fn owns_browser_cookie_resolution(&self) -> bool { + true + } +} + +fn session_cookie_header(raw: &str) -> Option { + if raw.len() > MAX_VENICE_COOKIE_HEADER_LEN { + return None; + } + + let mut exact = None; + let mut chunks = BTreeMap::new(); + let chunk_prefix = format!("{VENICE_SESSION_COOKIE}."); + + for part in raw.split(';') { + let Some((raw_name, raw_value)) = part.split_once('=') else { + continue; + }; + let name = raw_name.trim(); + let value = raw_value.trim(); + if value.is_empty() + || value.len() > MAX_VENICE_COOKIE_VALUE_LEN + || value.chars().any(char::is_control) + { + continue; + } + if name == VENICE_SESSION_COOKIE { + if exact.is_some() { + return None; + } + exact = Some(value.to_string()); + continue; + } + let Some(index) = name + .strip_prefix(&chunk_prefix) + .and_then(|value| value.parse::().ok()) + else { + continue; + }; + if index >= MAX_VENICE_COOKIE_CHUNKS || chunks.contains_key(&index) { + return None; + } + chunks.insert(index, value.to_string()); + } + + if let Some(value) = exact { + return Some(format!("{VENICE_SESSION_COOKIE}={value}")); + } + if chunks.is_empty() || !chunks.keys().next().is_some_and(|index| *index == 0) { + return None; + } + + let mut values = Vec::with_capacity(chunks.len()); + for index in 0..chunks.len() { + values.push(chunks.get(&index)?.as_str()); + } + Some(format!("{VENICE_SESSION_COOKIE}={}", values.concat())) +} + +fn snapshot_from_web_claims( + claims: &serde_json::Map, + now: DateTime, +) -> Result<(UsageSnapshot, Vec), ProviderError> { + let expiration = finite_non_negative(claims.get("exp")) + .filter(|value| (1_000_000_000.0..=4_000_000_000.0).contains(value)) + .and_then(unix_seconds_to_datetime) + .ok_or_else(|| ProviderError::AuthRequired)?; + if expiration < now - chrono::Duration::seconds(VENICE_EXPIRATION_SKEW_SECS) { + return Err(ProviderError::AuthRequired); + } + + if claims + .get("userType") + .and_then(Value::as_str) + .is_some_and(is_anonymous_user_type) + { + return Err(ProviderError::AuthRequired); + } + + let usage = claims + .get("bundledCreditsUsage") + .and_then(Value::as_object) + .ok_or_else(|| ProviderError::Parse("Venice web session has no credits usage".into()))?; + let used_this_cycle = finite_non_negative(usage.get("usedThisCycle")) + .ok_or_else(|| ProviderError::Parse("Venice web session has invalid usage".into()))?; + let monthly_refill_credits = finite_non_negative(usage.get("monthlyRefillCredits")) + .filter(|value| *value > 0.0) + .ok_or_else(|| { + ProviderError::Parse("Venice web session has invalid refill credits".into()) + })?; + + let available_credits = finite_non_negative(usage.get("availableCredits")) + .or_else(|| finite_non_negative(claims.get("bundledCredits"))); + let venice_credits = finite_non_negative(claims.get("veniceCredits")); + let tier_cap = finite_non_negative(usage.get("tierCap")); + let next_refill_at = epoch_to_datetime(usage.get("nextRefillAt")); + + let mut details = Vec::new(); + if let Some(available) = available_credits { + details.push(ProviderDisplayDetail::new( + "subscription-credits", + "Subscription credits available", + format_credits(available), + )); + } + if let Some(total) = venice_credits { + details.push(ProviderDisplayDetail::new( + "total-credits", + "Total credits available", + format_credits(total), + )); + } + details.push( + ProviderDisplayDetail::new( + "used-this-cycle", + "Used this cycle", + format_credits(used_this_cycle), + ) + .with_secondary_value(format!( + "Monthly refill: {}", + format_credits(monthly_refill_credits) + )) + .with_progress(used_this_cycle, monthly_refill_credits), + ); + if let Some(cap) = tier_cap { + details.push(ProviderDisplayDetail::new( + "bank-cap", + "Bank cap", + format_credits(cap), + )); + } + if let Some(next_refill) = next_refill_at { + details.push(ProviderDisplayDetail::new( + "next-refill", + "Next refill", + next_refill.to_rfc3339(), + )); + } + if let Some(user_type) = claims.get("userType").and_then(Value::as_str) { + let user_type = user_type.trim(); + if !user_type.is_empty() { + details.push(ProviderDisplayDetail::new("plan", "Plan", user_type)); + } + } + + Ok(( + UsageSnapshot::new(RateWindow::informational("Venice web credits")), + details, + )) +} + +fn finite_non_negative(value: Option<&Value>) -> Option { + match value { + Some(Value::Number(number)) => number + .as_f64() + .filter(|value| value.is_finite() && *value >= 0.0), + Some(Value::String(value)) => value + .trim() + .parse::() + .ok() + .filter(|value| value.is_finite() && *value >= 0.0), + _ => None, + } +} + +fn epoch_to_datetime(value: Option<&Value>) -> Option> { + let value = finite_non_negative(value)?; + let seconds = if value > 4_000_000_000.0 { + value / 1000.0 + } else { + value + }; + if !(1_000_000_000.0..=4_000_000_000.0).contains(&seconds) { + return None; + } + unix_seconds_to_datetime(seconds) +} + +fn unix_seconds_to_datetime(seconds: f64) -> Option> { + if !(1_000_000_000.0..=4_000_000_000.0).contains(&seconds) { + return None; + } + // The range check above proves this conversion is within i64 bounds; the + // fractional part is intentionally discarded because JWT/epoch values are + // rendered at second precision. + #[allow( + clippy::cast_possible_truncation, + reason = "the preceding range check bounds this conversion to valid Unix seconds" + )] + let seconds = seconds as i64; + DateTime::::from_timestamp(seconds, 0) +} + +fn is_anonymous_user_type(value: &str) -> bool { + matches!( + value.to_ascii_lowercase().as_str(), + "anonymous" | "anon" | "guest" | "unauthenticated" | "logged_out" + ) +} + +fn format_credits(value: f64) -> String { + if (value - value.round()).abs() < 0.005 { + format!("{value:.0}") + } else { + format!("{value:.2}") } } @@ -202,6 +476,23 @@ fn resolve_api_key( mod tests { use super::*; + fn web_claims() -> serde_json::Map { + serde_json::from_value(serde_json::json!({ + "exp": 1_900_000_000, + "userType": "paid", + "bundledCredits": 80, + "veniceCredits": 120, + "bundledCreditsUsage": { + "usedThisCycle": 12, + "monthlyRefillCredits": 100, + "availableCredits": 88, + "tierCap": 200, + "nextRefillAt": 1_900_000_000_000i64 + } + })) + .unwrap() + } + #[test] fn venice_snapshot_uses_diem_allocation() { let snapshot = snapshot_from_balance(&VeniceBalanceResponse { @@ -215,4 +506,87 @@ mod tests { }); assert_eq!(snapshot.primary.used_percent, 75.0); } + + #[test] + fn session_cookie_prefers_exact_and_reassembles_contiguous_chunks() { + assert_eq!( + session_cookie_header( + "other=x; __venice-auth.session-token.0=ab; __venice-auth.session-token.1=cd" + ), + Some("__venice-auth.session-token=abcd".to_string()) + ); + assert_eq!( + session_cookie_header( + "__venice-auth.session-token.0=ab; __venice-auth.session-token.2=cd" + ), + None + ); + assert_eq!( + session_cookie_header( + "__venice-auth.session-token=exact; __venice-auth.session-token.0=chunk" + ), + Some("__venice-auth.session-token=exact".to_string()) + ); + assert_eq!( + session_cookie_header("__venice-auth.session-token.0=a\nsecret"), + None + ); + assert_eq!( + session_cookie_header( + "__venice-auth.session-token=one; __venice-auth.session-token=two" + ), + None + ); + assert_eq!( + session_cookie_header("__venice-auth.session-token.not-a-chunk=value"), + None + ); + let oversized = format!( + "__venice-auth.session-token={}", + "x".repeat(MAX_VENICE_COOKIE_VALUE_LEN + 1) + ); + assert_eq!(session_cookie_header(&oversized), None); + } + + #[test] + fn web_claims_produce_display_details_without_quota_math() { + let (snapshot, details) = snapshot_from_web_claims( + &web_claims(), + DateTime::::from_timestamp(1_800_000_000, 0).unwrap(), + ) + .unwrap(); + + assert!(snapshot.primary.is_informational); + assert_eq!(details.len(), 6); + assert_eq!(details[0].value(), "88"); + assert_eq!( + details[2].progress().map(|progress| progress.total()), + Some(100.0) + ); + } + + #[test] + fn web_claims_reject_expired_anonymous_and_missing_usage() { + let now = DateTime::::from_timestamp(1_900_000_000, 0).unwrap(); + let mut expired = web_claims(); + expired.insert("exp".into(), Value::from(1_800_000_000)); + assert!(matches!( + snapshot_from_web_claims(&expired, now), + Err(ProviderError::AuthRequired) + )); + + let mut anonymous = web_claims(); + anonymous.insert("userType".into(), Value::from("guest")); + assert!(matches!( + snapshot_from_web_claims(&anonymous, now), + Err(ProviderError::AuthRequired) + )); + + let mut missing = web_claims(); + missing.remove("bundledCreditsUsage"); + assert!(matches!( + snapshot_from_web_claims(&missing, now), + Err(ProviderError::Parse(_)) + )); + } } From 5526ad642f4b6c149bd14a69cd131cb1a48454eb Mon Sep 17 00:00:00 2001 From: NessZerra <90105158+Finesssee@users.noreply.github.com> Date: Mon, 21 Sep 2026 03:39:02 +0700 Subject: [PATCH 4/9] Fix duplicate inventory field and struct from merge --- .../src-tauri/src/commands/bridge.rs | 10 ---------- rust/src/cli/usage.rs | 1 - rust/src/core/usage_snapshot.rs | 15 --------------- 3 files changed, 26 deletions(-) diff --git a/apps/desktop-tauri/src-tauri/src/commands/bridge.rs b/apps/desktop-tauri/src-tauri/src/commands/bridge.rs index ae2aa0daa5..62d1976398 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/bridge.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/bridge.rs @@ -433,16 +433,6 @@ impl ProviderUsageSnapshot { next_expires_at: item.next_expires_at.map(|date| date.to_rfc3339()), }) .collect(), - inventory: result - .inventory - .iter() - .map(|item| ProviderInventoryItemSnapshot { - id: item.id.clone(), - title: item.title.clone(), - available_count: item.available_count, - next_expires_at: item.next_expires_at.map(|date| date.to_rfc3339()), - }) - .collect(), display_details: result .display_details() .map(|detail| ProviderDisplayDetailSnapshot { diff --git a/rust/src/cli/usage.rs b/rust/src/cli/usage.rs index c171ee7f45..b6e8136696 100755 --- a/rust/src/cli/usage.rs +++ b/rust/src/cli/usage.rs @@ -1,6 +1,5 @@ //! Usage command implementation -use chrono::{DateTime, Utc}; use clap::Args; use serde::Serialize; diff --git a/rust/src/core/usage_snapshot.rs b/rust/src/core/usage_snapshot.rs index 4e3ec9c023..5f6cf0f8b2 100755 --- a/rust/src/core/usage_snapshot.rs +++ b/rust/src/core/usage_snapshot.rs @@ -105,21 +105,6 @@ pub struct ProviderInventoryItem { pub next_expires_at: Option>, } -/// One display-only item of provider-issued discrete inventory. -/// -/// This is deliberately separate from [`RateWindow`]: inventory does not -/// represent a percentage quota and must not participate in quota arithmetic, -/// tray metric selection, pace, notifications, or auto-resume decisions. -/// Provider-specific redemption identifiers stay private to the provider -/// parser and never enter this type. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ProviderInventoryItem { - pub id: String, - pub title: String, - pub available_count: u32, - pub next_expires_at: Option>, -} - /// One transient provider detail row for display surfaces. /// /// These rows are intentionally separate from quota windows and inventory: From f29fd23c68b6bc1098fef4f703cbabf0796faa16 Mon Sep 17 00:00:00 2001 From: NessZerra <90105158+Finesssee@users.noreply.github.com> Date: Mon, 21 Sep 2026 04:28:32 +0700 Subject: [PATCH 5/9] Address thermo-nuclear review: fold epoch helpers, return result directly --- .../src/components/MenuCardDetails.tsx | 2 - .../providers/sections/UsageSection.tsx | 22 --- rust/src/providers/venice/mod.rs | 128 ++++++++---------- 3 files changed, 57 insertions(+), 95 deletions(-) diff --git a/apps/desktop-tauri/src/components/MenuCardDetails.tsx b/apps/desktop-tauri/src/components/MenuCardDetails.tsx index 2d088de501..6ad91f5040 100644 --- a/apps/desktop-tauri/src/components/MenuCardDetails.tsx +++ b/apps/desktop-tauri/src/components/MenuCardDetails.tsx @@ -590,8 +590,6 @@ export default function MenuCardDetails({ {wayfinderUsage && !compactOverview && } - {wayfinderUsage && !compactOverview && } - {!compactOverview && hasMetrics && hasCost &&
} {!compactOverview && hasCost && provider.cost && ( diff --git a/apps/desktop-tauri/src/surfaces/settings/providers/sections/UsageSection.tsx b/apps/desktop-tauri/src/surfaces/settings/providers/sections/UsageSection.tsx index 07e52415bd..a8cee0b7e1 100644 --- a/apps/desktop-tauri/src/surfaces/settings/providers/sections/UsageSection.tsx +++ b/apps/desktop-tauri/src/surfaces/settings/providers/sections/UsageSection.tsx @@ -95,28 +95,6 @@ export function UsageSection({ provider, resetTimeRelative, t }: Props) { ); } -function InventoryRow({ - item, - resetTimeRelative, -}: { - item: ProviderInventoryItem; - resetTimeRelative: boolean; -}) { - const formattedExpiry = useFormattedResetTime( - item.nextExpiresAt, - null, - resetTimeRelative, - "expires", - ); - - return ( -
- {item.title}: {item.availableCount} available - {formattedExpiry && {formattedExpiry}} -
- ); -} - function DisplayDetailRow({ detail }: { detail: ProviderDisplayDetail }) { const progress = detail.progress; const progressPercent = progress && Number.isFinite(progress.used) && Number.isFinite(progress.total) && progress.total > 0 diff --git a/rust/src/providers/venice/mod.rs b/rust/src/providers/venice/mod.rs index 2e276a7246..394991654f 100644 --- a/rust/src/providers/venice/mod.rs +++ b/rust/src/providers/venice/mod.rs @@ -41,7 +41,7 @@ struct VeniceBalances { #[derive(Debug, Deserialize)] struct VeniceSessionResponse { - token: Option, + token: String, } pub struct VeniceProvider { @@ -73,7 +73,7 @@ impl VeniceProvider { } fn api_key(api_key: Option<&str>) -> Result { - resolve_api_key(api_key, VENICE_CREDENTIAL_TARGET, &["VENICE_API_KEY"]) + crate::providers::resolve_api_key(api_key, VENICE_CREDENTIAL_TARGET, &["VENICE_API_KEY"]) } async fn fetch_api(&self, api_key: &str) -> Result { @@ -138,19 +138,13 @@ impl VeniceProvider { let session: VeniceSessionResponse = response.json().await.map_err(|e| { ProviderError::Parse(format!("Failed to parse Venice web session: {e}")) })?; - let token = session - .token - .as_deref() - .filter(|token| !token.trim().is_empty()) - .ok_or(ProviderError::AuthRequired)?; + if session.token.trim().is_empty() { + return Err(ProviderError::AuthRequired); + } + let token = session.token.as_str(); let claims = crate::codex_accounts::api::jwt_payload(token) .ok_or_else(|| ProviderError::Parse("Venice session token is not a JWT".into()))?; - let (usage, details) = snapshot_from_web_claims(&claims, Utc::now())?; - let mut result = ProviderFetchResult::new(usage, "web").with_non_authoritative_pace(); - for detail in details { - result = result.with_display_detail(detail); - } - Ok(result) + snapshot_from_web_claims(&claims, Utc::now()) } } @@ -284,25 +278,23 @@ fn session_cookie_header(raw: &str) -> Option { if let Some(value) = exact { return Some(format!("{VENICE_SESSION_COOKIE}={value}")); } - if chunks.is_empty() || !chunks.keys().next().is_some_and(|index| *index == 0) { + if chunks.is_empty() || chunks.keys().max() != Some(&(chunks.len() - 1)) { + // Chunked cookies are contiguous 0..len-1 by construction; a gap or a + // tail that starts above 0 means a partial or forged set, so the + // session token cannot be reassembled safely. return None; } - let mut values = Vec::with_capacity(chunks.len()); - for index in 0..chunks.len() { - values.push(chunks.get(&index)?.as_str()); - } + let values: Vec = chunks.into_values().collect(); Some(format!("{VENICE_SESSION_COOKIE}={}", values.concat())) } fn snapshot_from_web_claims( claims: &serde_json::Map, now: DateTime, -) -> Result<(UsageSnapshot, Vec), ProviderError> { - let expiration = finite_non_negative(claims.get("exp")) - .filter(|value| (1_000_000_000.0..=4_000_000_000.0).contains(value)) - .and_then(unix_seconds_to_datetime) - .ok_or_else(|| ProviderError::AuthRequired)?; +) -> Result { + let expiration = + epoch_value_to_datetime(claims.get("exp")).ok_or_else(|| ProviderError::AuthRequired)?; if expiration < now - chrono::Duration::seconds(VENICE_EXPIRATION_SKEW_SECS) { return Err(ProviderError::AuthRequired); } @@ -331,7 +323,7 @@ fn snapshot_from_web_claims( .or_else(|| finite_non_negative(claims.get("bundledCredits"))); let venice_credits = finite_non_negative(claims.get("veniceCredits")); let tier_cap = finite_non_negative(usage.get("tierCap")); - let next_refill_at = epoch_to_datetime(usage.get("nextRefillAt")); + let next_refill_at = epoch_value_to_datetime(usage.get("nextRefillAt")); let mut details = Vec::new(); if let Some(available) = available_credits { @@ -381,10 +373,15 @@ fn snapshot_from_web_claims( } } - Ok(( + let mut result = ProviderFetchResult::new( UsageSnapshot::new(RateWindow::informational("Venice web credits")), - details, - )) + "web", + ) + .with_non_authoritative_pace(); + for detail in details { + result = result.with_display_detail(detail); + } + Ok(result) } fn finite_non_negative(value: Option<&Value>) -> Option { @@ -401,20 +398,16 @@ fn finite_non_negative(value: Option<&Value>) -> Option { } } -fn epoch_to_datetime(value: Option<&Value>) -> Option> { +/// Convert one epoch-valued JSON field (seconds or milliseconds) into a UTC +/// timestamp. Accepts only values in the sane 2001-2096 second range, which +/// also bounds the i64 cast below. +fn epoch_value_to_datetime(value: Option<&Value>) -> Option> { let value = finite_non_negative(value)?; let seconds = if value > 4_000_000_000.0 { value / 1000.0 } else { value }; - if !(1_000_000_000.0..=4_000_000_000.0).contains(&seconds) { - return None; - } - unix_seconds_to_datetime(seconds) -} - -fn unix_seconds_to_datetime(seconds: f64) -> Option> { if !(1_000_000_000.0..=4_000_000_000.0).contains(&seconds) { return None; } @@ -429,11 +422,12 @@ fn unix_seconds_to_datetime(seconds: f64) -> Option> { DateTime::::from_timestamp(seconds, 0) } +/// Venice documents `userType: "anonymous"` for logged-out sessions; the +/// claim is compared case-insensitively because the API treats the enum as a +/// free-form string. Other spellings are not guessed here: an unknown value +/// is treated as an authenticated user type. fn is_anonymous_user_type(value: &str) -> bool { - matches!( - value.to_ascii_lowercase().as_str(), - "anonymous" | "anon" | "guest" | "unauthenticated" | "logged_out" - ) + value.eq_ignore_ascii_case("anonymous") } fn format_credits(value: f64) -> String { @@ -444,35 +438,6 @@ fn format_credits(value: f64) -> String { } } -fn resolve_api_key( - explicit: Option<&str>, - credential_target: &str, - env_names: &[&str], -) -> Result { - if let Some(key) = explicit - && !key.trim().is_empty() - { - return Ok(key.trim().to_string()); - } - if let Ok(entry) = keyring::Entry::new(credential_target, "api_key") - && let Ok(key) = entry.get_password() - && !key.trim().is_empty() - { - return Ok(key); - } - for env in env_names { - if let Ok(key) = std::env::var(env) - && !key.trim().is_empty() - { - return Ok(key); - } - } - Err(ProviderError::NotInstalled(format!( - "API key not found. Set {} in Preferences or environment.", - env_names.join(" / ") - ))) -} - #[cfg(test)] mod tests { use super::*; @@ -551,13 +516,14 @@ mod tests { #[test] fn web_claims_produce_display_details_without_quota_math() { - let (snapshot, details) = snapshot_from_web_claims( + let result = snapshot_from_web_claims( &web_claims(), DateTime::::from_timestamp(1_800_000_000, 0).unwrap(), ) .unwrap(); - assert!(snapshot.primary.is_informational); + assert!(result.usage.primary.is_informational); + let details: Vec<_> = result.display_details().collect(); assert_eq!(details.len(), 6); assert_eq!(details[0].value(), "88"); assert_eq!( @@ -566,6 +532,26 @@ mod tests { ); } + #[test] + fn epoch_value_accepts_seconds_milliseconds_and_rejects_outliers() { + let seconds = serde_json::json!(1_900_000_000u64); + let millis = serde_json::json!(1_900_000_000_000i64); + assert_eq!( + epoch_value_to_datetime(Some(&seconds)), + DateTime::::from_timestamp(1_900_000_000, 0) + ); + assert_eq!( + epoch_value_to_datetime(Some(&millis)), + DateTime::::from_timestamp(1_900_000_000, 0) + ); + assert_eq!(epoch_value_to_datetime(None), None); + assert_eq!(epoch_value_to_datetime(Some(&serde_json::json!(42))), None); + assert_eq!( + epoch_value_to_datetime(Some(&serde_json::json!("1900000000"))), + DateTime::::from_timestamp(1_900_000_000, 0) + ); + } + #[test] fn web_claims_reject_expired_anonymous_and_missing_usage() { let now = DateTime::::from_timestamp(1_900_000_000, 0).unwrap(); @@ -577,7 +563,7 @@ mod tests { )); let mut anonymous = web_claims(); - anonymous.insert("userType".into(), Value::from("guest")); + anonymous.insert("userType".into(), Value::from("anonymous")); assert!(matches!( snapshot_from_web_claims(&anonymous, now), Err(ProviderError::AuthRequired) From e822002b418d4e36658e1b4103848441a0cdf329 Mon Sep 17 00:00:00 2001 From: NessZerra <90105158+Finesssee@users.noreply.github.com> Date: Mon, 21 Sep 2026 07:38:58 +0700 Subject: [PATCH 6/9] Fix display details tests for main API --- rust/src/cli/usage_tests.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/rust/src/cli/usage_tests.rs b/rust/src/cli/usage_tests.rs index 0d20e67743..25428709c7 100644 --- a/rust/src/cli/usage_tests.rs +++ b/rust/src/cli/usage_tests.rs @@ -324,4 +324,3 @@ fn json_inventory_is_additive_and_contains_no_redemption_token() { .contains("coupon-token-secret") ); } - From 3cc24fa41e910af00acfa47ea9ccb5cb27346a51 Mon Sep 17 00:00:00 2001 From: NessZerra <90105158+Finesssee@users.noreply.github.com> Date: Mon, 21 Sep 2026 08:04:45 +0700 Subject: [PATCH 7/9] Trigger CI re-run From b48e029068fff5fcc0868f3be3739b1a89aec423 Mon Sep 17 00:00:00 2001 From: NessZerra <90105158+Finesssee@users.noreply.github.com> Date: Mon, 21 Sep 2026 13:29:36 +0700 Subject: [PATCH 8/9] Retry over cap test within budget --- rust/src/cli/serve/tests.rs | 42 ++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/rust/src/cli/serve/tests.rs b/rust/src/cli/serve/tests.rs index 2419b0cc1f..36d4146a44 100644 --- a/rust/src/cli/serve/tests.rs +++ b/rust/src/cli/serve/tests.rs @@ -474,24 +474,38 @@ async fn over_cap_connection_closes_immediately_without_response() { ); // Ending the tricklers releases their permits via EOF; a normal client - // must then be served (strict outer timeout). + // must then be served (strict outer timeout). Permit release races the + // server's graceful close-drain window, so a single fixed wait can see a + // connection reset; retry within a bounded budget instead. for task in &tricklers { task.abort(); } - tokio::time::sleep(Duration::from_millis(400)).await; - let mut good = TcpStream::connect(addr).await.unwrap(); - good.write_all(b"GET /health HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n") - .await - .unwrap(); - let mut response = Vec::new(); - tokio::time::timeout(Duration::from_secs(5), good.read_to_end(&mut response)) - .await - .expect("no connection slot freed after trickling clients ended") - .unwrap(); + let request = b"GET /health HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n"; + let mut served: Option = None; + let retry = tokio::time::Instant::now(); + while retry.elapsed() < Duration::from_secs(2) { + tokio::time::sleep(Duration::from_millis(100)).await; + let Ok(mut good) = TcpStream::connect(addr).await else { + continue; + }; + if good.write_all(request).await.is_err() { + continue; + } + let mut response = Vec::new(); + match tokio::time::timeout(Duration::from_secs(5), good.read_to_end(&mut response)).await { + // A reset mid-handshake is the drain race; retry. + Ok(Err(_)) | Err(_) => continue, + Ok(Ok(_)) => {} + } + if String::from_utf8_lossy(&response).starts_with("HTTP/1.1 200") { + served = Some(String::from_utf8_lossy(&response).into_owned()); + break; + } + } + let served = served.expect("no freed slot served a normal request within retry budget"); assert!( - String::from_utf8_lossy(&response).starts_with("HTTP/1.1 200"), - "freed slot must serve a normal request, got: {}", - String::from_utf8_lossy(&response) + served.starts_with("HTTP/1.1 200"), + "freed slot must serve a normal request, got: {served}" ); server_task.abort(); } From ee01c834b4b48cfc4457a0f3d88732f27100fa88 Mon Sep 17 00:00:00 2001 From: NessZerra <90105158+Finesssee@users.noreply.github.com> Date: Mon, 21 Sep 2026 14:34:04 +0700 Subject: [PATCH 9/9] Restore Venice usage source policy lost in merge --- .../providers/sections/usageSourcePolicy.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/apps/desktop-tauri/src/surfaces/settings/providers/sections/usageSourcePolicy.ts b/apps/desktop-tauri/src/surfaces/settings/providers/sections/usageSourcePolicy.ts index fb53b7d203..5673c0dbc2 100644 --- a/apps/desktop-tauri/src/surfaces/settings/providers/sections/usageSourcePolicy.ts +++ b/apps/desktop-tauri/src/surfaces/settings/providers/sections/usageSourcePolicy.ts @@ -46,6 +46,25 @@ const POLICIES: Readonly> = { { value: "oauth", label: "Muse Code login", description: "Uses the local Muse Code device-code login only." }, ], }, + venice: { + options: [ + { + value: "auto", + label: "Auto", + description: "Uses the Venice API key or token account; browser sessions are used only when Web is selected.", + }, + { + value: "oauth", + label: "API", + description: "Uses the Venice API key or token account only.", + }, + { + value: "web", + label: "Browser session", + description: "Reads Venice subscription credits from the selected browser session or manual cookie header.", + }, + ], + }, }; export function usageSourcePolicy(providerId: string): UsageSourcePolicy | null {