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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 7 additions & 2 deletions apps/desktop-tauri/src/components/providers/providerIcons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import crossmodel from "./icons/ProviderIcon-crossmodel.svg?raw";
import cursor from "./icons/ProviderIcon-cursor.svg?raw";
import deepgram from "./icons/ProviderIcon-deepgram.svg?raw";
import deepinfra from "./icons/ProviderIcon-deepinfra.svg?raw";
import fireworks from "./icons/ProviderIcon-fireworks.svg?raw";
import aiand from "./icons/ProviderIcon-aiand.svg?raw";
import clinepass from "./icons/ProviderIcon-clinepass.svg?raw";
import longcat from "./icons/ProviderIcon-longcat.svg?raw";
Expand Down Expand Up @@ -99,6 +100,7 @@ const RAW: Record<string, string> = {
cursor: tint(cursor),
deepgram: tint(deepgram),
deepinfra: tint(deepinfra),
fireworks: tint(fireworks),
aiand: tint(aiand),
clinepass: tint(clinepass),
longcat: tint(longcat),
Expand Down Expand Up @@ -158,6 +160,7 @@ export const PROVIDER_ICON_REGISTRY: Record<string, ProviderIcon> = {
cursor: { id: "cursor", brandColor: "#00bfa5", fallbackLetter: "▸", svgPath: RAW.cursor },
deepgram: { id: "deepgram", brandColor: "#13ef93", fallbackLetter: "D", svgPath: RAW.deepgram },
deepinfra: { id: "deepinfra", brandColor: "#2a3275", fallbackLetter: "D", svgPath: RAW.deepinfra },
fireworks: { id: "fireworks", brandColor: "#f25b1c", fallbackLetter: "F", svgPath: RAW.fireworks },
aiand: { id: "aiand", brandColor: "#e25c2b", fallbackLetter: "&", svgPath: RAW.aiand },
clinepass: { id: "clinepass", brandColor: "#61a3fa", fallbackLetter: "C", svgPath: RAW.clinepass },
longcat: { id: "longcat", brandColor: "#ffd100", fallbackLetter: "L", svgPath: RAW.longcat },
Expand Down Expand Up @@ -246,8 +249,10 @@ const ALIASES: Record<string, string> = {
"deep seek": "deepseek",
"deep-seek": "deepseek",
"deep infra": "deepinfra",
"deep-infra": "deepinfra",
di: "deepinfra",
"deep-infra": "deepinfra",
di: "deepinfra",
"fireworks-ai": "fireworks",
fw: "fireworks",
"ai&": "aiand",
"ai-and": "aiand",
"ai and": "aiand",
Expand Down
16 changes: 11 additions & 5 deletions apps/desktop-tauri/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -4267,14 +4267,20 @@ html:has(.menu-surface--tray) {
.menu-metric__reset {
font-size: 11px;
color: var(--text-secondary);
white-space: nowrap;
/* Long provider descriptions (credits remaining, etc.) used to overflow
the tray card because nowrap had no max-width/ellipsis. */
/* Upstream 0.49.0 #2742 (refs #2182): long metric reset and pace details
wrap to a second line instead of truncating, so non-English locales keep
the full reset information. #2846: the percent value keeps its own line
slot (row only stacks when both cannot fit) and the two-line clamp keeps
cached card heights bounded. */
min-width: 0;
max-width: 62%;
overflow: hidden;
text-overflow: ellipsis;
text-align: right;
white-space: normal;
overflow-wrap: break-word;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}

.menu-metric__exhausted {
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop-tauri/src/surfaces/TrayPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const HAS_DASHBOARD = new Set([
"mimo", "minimax", "mistral", "nanogpt", "notion", "ollama", "openaiapi",
"opencode", "opencodego", "openrouter", "perplexity", "qoder", "codebuddy", "sakana", "stepfun",
"t3chat", "venice", "vertexai", "warp", "windsurf",
"xai", "zai",
"xai", "zai", "fireworks",
]);
/** Provider IDs that have a status page URL in the backend */
const HAS_STATUS_PAGE = new Set([
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ const WORKSPACE_EXTRA_IDS: Record<string, true> = {
zed: true,
sub2api: true,
xai: true,
fireworks: true,
};

function extraConfig(providerId: string, t: Props["t"]) {
Expand Down Expand Up @@ -168,6 +169,13 @@ function extraConfig(providerId: string, t: Props["t"]) {
placeholder: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
help: "Required. Shown in the xAI Console URL and team settings. Or set XAI_TEAM_ID. Pair with a Management API key (not an inference key).",
};
case "fireworks":
return {
title: "Fireworks account",
label: "Account slug",
placeholder: "your-account-slug",
help: "From app.fireworks.ai/accounts/<slug>. Or set FIREWORKS_ACCOUNT_SLUG. Pair with a Fireworks API key to read 30-day rated billing spend.",
};
default:
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ function providerSourceHintShort(
case "groq":
case "llmproxy":
case "xai":
case "fireworks":
return t("ProviderSourceApiShort");
case "kiro":
return t("ProviderSourceKiroEnvShort");
Expand Down
1 change: 1 addition & 0 deletions apps/desktop-tauri/src/test/providerCatalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export const TEST_PROVIDER_CATALOG: Array<[string, string]> = [
["codebuff", "Codebuff"],
["deepseek", "DeepSeek"],
["deepinfra", "DeepInfra"],
["fireworks", "Fireworks"],
["aiand", "ai&"],
["zenmux", "ZenMux"],
["clinepass", "ClinePass"],
Expand Down
11 changes: 10 additions & 1 deletion rust/src/core/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ pub enum ProviderId {
QwenCloud,
Notion,
Xai,
Fireworks,
}

impl ProviderId {
Expand Down Expand Up @@ -155,6 +156,7 @@ impl ProviderId {
ProviderId::QwenCloud,
ProviderId::Notion,
ProviderId::Xai,
ProviderId::Fireworks,
]
}

Expand Down Expand Up @@ -196,6 +198,7 @@ impl ProviderId {
ProviderId::Codebuff => "codebuff",
ProviderId::DeepSeek => "deepseek",
ProviderId::DeepInfra => "deepinfra",
ProviderId::Fireworks => "fireworks",
ProviderId::AiAnd => "aiand",
ProviderId::Windsurf => "windsurf",
ProviderId::Manus => "manus",
Expand Down Expand Up @@ -272,6 +275,7 @@ impl ProviderId {
ProviderId::Codebuff => "Codebuff",
ProviderId::DeepSeek => "DeepSeek",
ProviderId::DeepInfra => "DeepInfra",
ProviderId::Fireworks => "Fireworks",
ProviderId::AiAnd => "ai&",
ProviderId::Windsurf => "Windsurf",
ProviderId::Manus => "Manus",
Expand Down Expand Up @@ -361,6 +365,7 @@ impl ProviderId {
ProviderId::Codebuff => None,
ProviderId::DeepSeek => None,
ProviderId::DeepInfra => None,
ProviderId::Fireworks => None,
ProviderId::AiAnd => None,
ProviderId::Windsurf => None,
ProviderId::Doubao => None,
Expand Down Expand Up @@ -433,6 +438,7 @@ impl ProviderId {
"codebuff" | "manicode" => Some(ProviderId::Codebuff),
"deepseek" | "deep-seek" | "ds" => Some(ProviderId::DeepSeek),
"deepinfra" | "deep-infra" | "di" => Some(ProviderId::DeepInfra),
"fireworks" | "fireworks-ai" | "fw" => Some(ProviderId::Fireworks),
"aiand" | "ai&" | "ai-and" | "ai and" => Some(ProviderId::AiAnd),
"windsurf" | "codeium" => Some(ProviderId::Windsurf),
"manus" => Some(ProviderId::Manus),
Expand Down Expand Up @@ -681,6 +687,8 @@ pub fn cli_name_map() -> HashMap<&'static str, ProviderId> {
map.insert("ds", ProviderId::DeepSeek);
map.insert("deep-infra", ProviderId::DeepInfra);
map.insert("di", ProviderId::DeepInfra);
map.insert("fireworks-ai", ProviderId::Fireworks);
map.insert("fw", ProviderId::Fireworks);
map.insert("ai&", ProviderId::AiAnd);
map.insert("ai-and", ProviderId::AiAnd);
map.insert("codeium", ProviderId::Windsurf);
Expand Down Expand Up @@ -740,9 +748,10 @@ mod tests {
#[test]
fn test_provider_id_all() {
let all = ProviderId::all();
assert_eq!(all.len(), 69);
assert_eq!(all.len(), 70);
assert!(all.contains(&ProviderId::Claude));
assert!(all.contains(&ProviderId::Codex));
assert!(all.contains(&ProviderId::Fireworks));
assert!(all.contains(&ProviderId::Kimi));
assert!(all.contains(&ProviderId::KimiK2));
assert!(all.contains(&ProviderId::Amp));
Expand Down
7 changes: 4 additions & 3 deletions rust/src/core/provider_factory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ use crate::providers::{
ClaudeProvider, ClinePassProvider, CodeBuddyProvider, CodebuffProvider, CodexProvider,
CommandCodeProvider, CopilotProvider, CrofProvider, CrossModelProvider, CursorProvider,
DeepInfraProvider, DeepSeekProvider, DeepgramProvider, DevinProvider, DoubaoProvider,
ElevenLabsProvider, FactoryProvider, GeminiProvider, GrokProvider, GroqProvider,
InfiniProvider, JetBrainsProvider, KiloProvider, KimiK2Provider, KimiProvider, KiroProvider,
LLMProxyProvider, LiteLLMProvider, LongCatProvider, ManusProvider, MiMoProvider,
ElevenLabsProvider, FactoryProvider, FireworksProvider, GeminiProvider, GrokProvider,
GroqProvider, InfiniProvider, JetBrainsProvider, KiloProvider, KimiK2Provider, KimiProvider,
KiroProvider, LLMProxyProvider, LiteLLMProvider, LongCatProvider, ManusProvider, MiMoProvider,
MiniMaxProvider, MistralProvider, NanoGPTProvider, NeuralwattProvider, NotionProvider,
OllamaProvider, OpenAIApiProvider, OpenCodeGoProvider, OpenCodeProvider, OpenRouterProvider,
PerplexityProvider, PoeProvider, QoderProvider, QwenCloudProvider, SakanaProvider,
Expand Down Expand Up @@ -98,6 +98,7 @@ pub fn instantiate(id: ProviderId) -> Box<dyn Provider> {
ProviderId::QwenCloud => Box::new(QwenCloudProvider::new()),
ProviderId::Notion => Box::new(NotionProvider::new()),
ProviderId::Xai => Box::new(XaiProvider::new()),
ProviderId::Fireworks => Box::new(FireworksProvider::new()),
}
}

Expand Down
3 changes: 2 additions & 1 deletion rust/src/core/token_accounts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,8 @@ impl TokenAccountSupport {
| ProviderId::CrossModel
| ProviderId::LongCat
| ProviderId::Wayfinder
| ProviderId::QwenCloud => None,
| ProviderId::QwenCloud
| ProviderId::Fireworks => None,
}
}

Expand Down
105 changes: 96 additions & 9 deletions rust/src/providers/amp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -313,16 +313,20 @@ pub fn parse_amp_free_percent_remaining(text: &str) -> Option<f64> {
None
}

/// Parse Amp subscription display text (Megawatt dual other/orb windows).
/// Parse Amp subscription display text (Megawatt/Gigawatt dual other/orb windows).
///
/// Matches:
/// `Subscription Megawatt: 42% other usage and 88% orb usage remaining - resets upon renewal in 12 days`
/// `Subscription Gigawatt: 10% other usage and 95% orb usage remaining - resets upon renewal in 2 months`
///
/// Upstream 0.49.6 #2601: monthly (Gigawatt) renewals advance by calendar
/// month, not 30-day buckets.
pub fn parse_amp_subscription_usage(
text: &str,
now: chrono::DateTime<chrono::Utc>,
) -> Option<AmpSubscriptionUsage> {
let re = regex_lite::Regex::new(
r"(?im)^\s*Subscription\s+(.+?):\s*([0-9][0-9,]*(?:\.[0-9]+)?)\s*%\s+other\s+usage\s+and\s+([0-9][0-9,]*(?:\.[0-9]+)?)\s*%\s+orb\s+usage\s+remaining\s*-\s*resets\s+upon\s+renewal\s+in\s+([0-9][0-9,]*)\s+days?(?:\s+-\s+https?://\S+)?\s*$",
r"(?im)^\s*Subscription\s+(.+?):\s*([0-9][0-9,]*(?:\.[0-9]+)?)\s*%\s+other\s+usage\s+and\s+([0-9][0-9,]*(?:\.[0-9]+)?)\s*%\s+orb\s+usage\s+remaining\s*-\s*resets\s+upon\s+renewal\s+in\s+([0-9][0-9,]*)\s+(days?|months?)(?:\s+-\s+https?://\S+)?\s*$",
)
.ok()?;

Expand All @@ -336,26 +340,46 @@ pub fn parse_amp_subscription_usage(
}
let other_remaining = parse_amp_number(caps.get(2)?.as_str())?;
let orb_remaining = parse_amp_number(caps.get(3)?.as_str())?;
let renewal_days: i64 = caps.get(4)?.as_str().replace(',', "").parse().ok()?;
if renewal_days < 0 {
let renewal_value: i64 = caps.get(4)?.as_str().replace(',', "").parse().ok()?;
if renewal_value < 0 {
continue;
}
let reset_description = if renewal_days == 1 {
"renews in 1 day".to_string()
let unit = caps.get(5)?.as_str().to_ascii_lowercase();
let resets_at = if unit.starts_with("month") {
add_calendar_months(now, renewal_value)?
} else {
now + chrono::Duration::days(renewal_value)
};
let singular_unit = if unit.starts_with("month") {
"month"
} else {
format!("renews in {renewal_days} days")
"day"
};
let reset_description = if renewal_value == 1 {
format!("renews in 1 {singular_unit}")
} else {
format!("renews in {renewal_value} {singular_unit}s")
};
return Some(AmpSubscriptionUsage {
plan: plan.to_string(),
other_used_percent: 100.0 - other_remaining.clamp(0.0, 100.0),
orb_used_percent: 100.0 - orb_remaining.clamp(0.0, 100.0),
resets_at: now + chrono::Duration::days(renewal_days),
resets_at,
reset_description,
});
}
None
}

/// Add whole calendar months via chrono's calendar arithmetic, mirroring
/// upstream `Calendar.date(byAdding: .month:)` for monthly renewals.
fn add_calendar_months(
now: chrono::DateTime<chrono::Utc>,
months: i64,
) -> Option<chrono::DateTime<chrono::Utc>> {
now.checked_add_months(chrono::Months::new(u32::try_from(months).ok()?))
}

/// Build a [`UsageSnapshot`] from Amp Free / subscription display text.
///
/// Subscription (Megawatt) wins for primary/secondary windows when present:
Expand Down Expand Up @@ -392,15 +416,38 @@ pub fn usage_snapshot_from_amp_display_text(
}

let free_used = parse_amp_free_percent_remaining(text)?;
// Upstream 0.49.6 #2601: the Amp Free daily tier resets at 8:00 PM
// America/New_York, not local midnight.
let primary = RateWindow::with_details(
free_used,
Some(24 * 60),
None,
next_free_tier_reset(now),
Some("resets daily".to_string()),
);
Some(UsageSnapshot::new(primary).with_login_method("Amp Free"))
}

/// Next 8:00 PM America/New_York boundary strictly after `now`.
fn next_free_tier_reset(
now: chrono::DateTime<chrono::Utc>,
) -> Option<chrono::DateTime<chrono::Utc>> {
use chrono::{Datelike, TimeZone};
let tz = chrono_tz::America::New_York;
let local_now = now.with_timezone(&tz);
let today = local_now.date_naive();
let today_reset = tz
.with_ymd_and_hms(today.year(), today.month(), today.day(), 20, 0, 0)
.single()?
.with_timezone(&chrono::Utc);
if today_reset > now {
return Some(today_reset);
}
let tomorrow = today + chrono::Duration::days(1);
tz.with_ymd_and_hms(tomorrow.year(), tomorrow.month(), tomorrow.day(), 20, 0, 0)
.single()
.map(|dt| dt.with_timezone(&chrono::Utc))
}

fn parse_amp_number(raw: &str) -> Option<f64> {
let value: f64 = raw.replace(',', "").parse().ok()?;
value.is_finite().then_some(value)
Expand Down Expand Up @@ -488,6 +535,46 @@ Subscription Megawatt: 42% other usage and 88% orb usage remaining - resets upon
assert!((sub.orb_used_percent - 0.0).abs() < f64::EPSILON);
}

#[test]
fn gigawatt_monthly_renewal_advances_calendar_months() {
// Upstream 0.49.6 #2601: monthly renewals (Gigawatt) use calendar
// months, not 30-day buckets.
let now = Utc.with_ymd_and_hms(2026, 8, 17, 12, 0, 0).unwrap();
let text = "Subscription Gigawatt: 10% other usage and 95% orb usage remaining - resets upon renewal in 2 months";
let sub = parse_amp_subscription_usage(text, now).expect("subscription");
assert_eq!(sub.plan, "Gigawatt");
assert_eq!(sub.reset_description, "renews in 2 months");
assert_eq!(
sub.resets_at,
Utc.with_ymd_and_hms(2026, 10, 17, 12, 0, 0).unwrap()
);
}

#[test]
fn free_tier_resets_at_8pm_new_york() {
// Upstream 0.49.6 #2601: Amp Free resets at 8:00 PM America/New_York.
// 2026-08-17 18:00 UTC = 14:00 EDT → same-day 20:00 EDT = 00:00 UTC Aug 18.
let now = Utc.with_ymd_and_hms(2026, 8, 17, 18, 0, 0).unwrap();
let snapshot =
usage_snapshot_from_amp_display_text("Amp Free: 72% remaining (resets daily)", now)
.expect("snapshot");
assert_eq!(
snapshot.primary.resets_at,
Some(Utc.with_ymd_and_hms(2026, 8, 18, 0, 0, 0).unwrap())
);

// 2026-08-18 00:30 UTC = 20:30 EDT Aug 17 (after the boundary) → the
// next reset is Aug 18 20:00 EDT = Aug 19 00:00 UTC.
let later = Utc.with_ymd_and_hms(2026, 8, 18, 0, 30, 0).unwrap();
let snapshot =
usage_snapshot_from_amp_display_text("Amp Free: 72% remaining (resets daily)", later)
.expect("snapshot");
assert_eq!(
snapshot.primary.resets_at,
Some(Utc.with_ymd_and_hms(2026, 8, 19, 0, 0, 0).unwrap())
);
}

#[test]
fn free_path_still_builds_snapshot() {
let now = Utc.with_ymd_and_hms(2026, 7, 1, 0, 0, 0).unwrap();
Expand Down
Loading
Loading