diff --git a/apps/desktop-tauri/src/components/providers/icons/ProviderIcon-clinepass.svg b/apps/desktop-tauri/src/components/providers/icons/ProviderIcon-clinepass.svg
new file mode 100644
index 0000000000..1ce7fe20be
--- /dev/null
+++ b/apps/desktop-tauri/src/components/providers/icons/ProviderIcon-clinepass.svg
@@ -0,0 +1,4 @@
+
diff --git a/apps/desktop-tauri/src/components/providers/icons/ProviderIcon-longcat.svg b/apps/desktop-tauri/src/components/providers/icons/ProviderIcon-longcat.svg
new file mode 100644
index 0000000000..dd1201c95e
--- /dev/null
+++ b/apps/desktop-tauri/src/components/providers/icons/ProviderIcon-longcat.svg
@@ -0,0 +1,4 @@
+
diff --git a/apps/desktop-tauri/src/components/providers/icons/ProviderIcon-neuralwatt.svg b/apps/desktop-tauri/src/components/providers/icons/ProviderIcon-neuralwatt.svg
new file mode 100644
index 0000000000..cf43777aca
--- /dev/null
+++ b/apps/desktop-tauri/src/components/providers/icons/ProviderIcon-neuralwatt.svg
@@ -0,0 +1,3 @@
+
diff --git a/apps/desktop-tauri/src/components/providers/icons/ProviderIcon-zenmux.svg b/apps/desktop-tauri/src/components/providers/icons/ProviderIcon-zenmux.svg
new file mode 100644
index 0000000000..3dc2a97c65
--- /dev/null
+++ b/apps/desktop-tauri/src/components/providers/icons/ProviderIcon-zenmux.svg
@@ -0,0 +1,3 @@
+
diff --git a/apps/desktop-tauri/src/components/providers/providerIcons.ts b/apps/desktop-tauri/src/components/providers/providerIcons.ts
index 4252a1d68e..9c97d56bf0 100644
--- a/apps/desktop-tauri/src/components/providers/providerIcons.ts
+++ b/apps/desktop-tauri/src/components/providers/providerIcons.ts
@@ -18,6 +18,10 @@ 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 clinepass from "./icons/ProviderIcon-clinepass.svg?raw";
+import longcat from "./icons/ProviderIcon-longcat.svg?raw";
+import neuralwatt from "./icons/ProviderIcon-neuralwatt.svg?raw";
+import zenmux from "./icons/ProviderIcon-zenmux.svg?raw";
import deepseek from "./icons/ProviderIcon-deepseek.svg?raw";
import doubao from "./icons/ProviderIcon-doubao.svg?raw";
import elevenlabs from "./icons/ProviderIcon-elevenlabs.svg?raw";
@@ -91,6 +95,10 @@ const RAW: Record = {
cursor: tint(cursor),
deepgram: tint(deepgram),
deepinfra: tint(deepinfra),
+ clinepass: tint(clinepass),
+ longcat: tint(longcat),
+ neuralwatt: tint(neuralwatt),
+ zenmux: tint(zenmux),
deepseek: tint(deepseek),
doubao: tint(doubao),
elevenlabs: tint(elevenlabs),
@@ -142,6 +150,10 @@ export const PROVIDER_ICON_REGISTRY: Record = {
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 },
+ clinepass: { id: "clinepass", brandColor: "#61a3fa", fallbackLetter: "C", svgPath: RAW.clinepass },
+ longcat: { id: "longcat", brandColor: "#ffd100", fallbackLetter: "L", svgPath: RAW.longcat },
+ neuralwatt: { id: "neuralwatt", brandColor: "#38d98c", fallbackLetter: "N", svgPath: RAW.neuralwatt },
+ zenmux: { id: "zenmux", brandColor: "#6c5ce7", fallbackLetter: "Z", svgPath: RAW.zenmux },
deepseek: { id: "deepseek", brandColor: "#527df0", fallbackLetter: "D", svgPath: RAW.deepseek },
elevenlabs: { id: "elevenlabs", brandColor: "#111827", fallbackLetter: "E", svgPath: RAW.elevenlabs },
factory: { id: "factory", brandColor: "#ff6b35", fallbackLetter: "◎", svgPath: RAW.factory },
@@ -217,6 +229,12 @@ const ALIASES: Record = {
"deep infra": "deepinfra",
"deep-infra": "deepinfra",
di: "deepinfra",
+ "zen-mux": "zenmux",
+ "cline-pass": "clinepass",
+ "long-cat": "longcat",
+ lc: "longcat",
+ "neural-watt": "neuralwatt",
+ nw: "neuralwatt",
codeium: "windsurf",
"xiaomi mimo": "mimo",
xiaomimimo: "mimo",
diff --git a/apps/desktop-tauri/src/surfaces/TrayPanel.tsx b/apps/desktop-tauri/src/surfaces/TrayPanel.tsx
index 0a0e614524..3b2a4b20bf 100644
--- a/apps/desktop-tauri/src/surfaces/TrayPanel.tsx
+++ b/apps/desktop-tauri/src/surfaces/TrayPanel.tsx
@@ -38,7 +38,7 @@ import AgentSessions from "../components/AgentSessions";
const HAS_DASHBOARD = new Set([
"abacus", "alibaba", "alibabatokenplan", "amp", "augment",
"azureopenai", "bedrock", "claude", "codex", "codebuff",
- "commandcode", "copilot", "crof", "crossmodel", "cursor", "deepgram", "deepinfra", "deepseek",
+ "commandcode", "copilot", "crof", "crossmodel", "cursor", "deepgram", "deepinfra", "deepseek", "zenmux", "clinepass", "longcat", "neuralwatt",
"doubao", "elevenlabs", "factory", "gemini", "grok", "groq",
"infini", "jetbrains", "kilo", "kimi", "kimik2", "kiro", "manus",
"mimo", "minimax", "mistral", "nanogpt", "ollama", "openaiapi",
@@ -49,7 +49,7 @@ const HAS_DASHBOARD = new Set([
/** Provider IDs that have a status page URL in the backend */
const HAS_STATUS_PAGE = new Set([
"alibabatokenplan", "amp", "augment", "azureopenai", "bedrock",
- "claude", "codex", "copilot", "deepgram", "deepinfra", "deepseek", "elevenlabs",
+ "claude", "codex", "copilot", "deepgram", "deepinfra", "deepseek", "zenmux", "clinepass", "longcat", "neuralwatt", "elevenlabs",
"gemini", "grok", "groq", "kiro", "mistral", "openaiapi",
"openrouter", "vertexai", "windsurf",
]);
diff --git a/apps/desktop-tauri/src/surfaces/settings/tabs/ProvidersTab.tsx b/apps/desktop-tauri/src/surfaces/settings/tabs/ProvidersTab.tsx
index cb4f05c312..fb030f12ec 100644
--- a/apps/desktop-tauri/src/surfaces/settings/tabs/ProvidersTab.tsx
+++ b/apps/desktop-tauri/src/surfaces/settings/tabs/ProvidersTab.tsx
@@ -230,6 +230,9 @@ function providerSourceHintShort(
case "nanogpt":
case "warp":
case "deepinfra":
+ case "zenmux":
+ case "clinepass":
+ case "neuralwatt":
case "doubao":
case "crof":
case "stepfun":
diff --git a/apps/desktop-tauri/src/test/providerCatalog.ts b/apps/desktop-tauri/src/test/providerCatalog.ts
index 972b0e4ba3..d158e3417c 100644
--- a/apps/desktop-tauri/src/test/providerCatalog.ts
+++ b/apps/desktop-tauri/src/test/providerCatalog.ts
@@ -34,6 +34,10 @@ export const TEST_PROVIDER_CATALOG: Array<[string, string]> = [
["codebuff", "Codebuff"],
["deepseek", "DeepSeek"],
["deepinfra", "DeepInfra"],
+ ["zenmux", "ZenMux"],
+ ["clinepass", "ClinePass"],
+ ["longcat", "LongCat"],
+ ["neuralwatt", "Neuralwatt"],
["windsurf", "Windsurf"],
["manus", "Manus"],
["mimo", "Xiaomi MiMo"],
diff --git a/apps/desktop-tauri/src/types/bridge.ts b/apps/desktop-tauri/src/types/bridge.ts
index 13c3799f34..4454995fd0 100644
--- a/apps/desktop-tauri/src/types/bridge.ts
+++ b/apps/desktop-tauri/src/types/bridge.ts
@@ -90,6 +90,10 @@ export type ProofProviderId =
| "codebuff"
| "deepseek"
| "deepinfra"
+ | "zenmux"
+ | "clinepass"
+ | "longcat"
+ | "neuralwatt"
| "windsurf"
| "manus"
| "mimo"
diff --git a/rust/src/core/provider.rs b/rust/src/core/provider.rs
index 18ad2cc9ad..19c283c200 100755
--- a/rust/src/core/provider.rs
+++ b/rust/src/core/provider.rs
@@ -70,6 +70,10 @@ pub enum ProviderId {
Sakana,
Sub2Api,
Wayfinder,
+ ZenMux,
+ ClinePass,
+ LongCat,
+ Neuralwatt,
}
impl ProviderId {
@@ -135,6 +139,10 @@ impl ProviderId {
ProviderId::Sakana,
ProviderId::Sub2Api,
ProviderId::Wayfinder,
+ ProviderId::ZenMux,
+ ProviderId::ClinePass,
+ ProviderId::LongCat,
+ ProviderId::Neuralwatt,
]
}
@@ -200,6 +208,10 @@ impl ProviderId {
ProviderId::Sakana => "sakana",
ProviderId::Sub2Api => "sub2api",
ProviderId::Wayfinder => "wayfinder",
+ ProviderId::ZenMux => "zenmux",
+ ProviderId::ClinePass => "clinepass",
+ ProviderId::LongCat => "longcat",
+ ProviderId::Neuralwatt => "neuralwatt",
}
}
@@ -265,6 +277,10 @@ impl ProviderId {
ProviderId::Sakana => "Sakana AI",
ProviderId::Sub2Api => "sub2api",
ProviderId::Wayfinder => "Wayfinder",
+ ProviderId::ZenMux => "ZenMux",
+ ProviderId::ClinePass => "ClinePass",
+ ProviderId::LongCat => "LongCat",
+ ProviderId::Neuralwatt => "Neuralwatt",
}
}
@@ -302,6 +318,7 @@ impl ProviderId {
ProviderId::Grok => Some("grok.com"),
ProviderId::Qoder => Some("qoder.com"),
ProviderId::Sakana => Some("console.sakana.ai"),
+ ProviderId::LongCat => Some("longcat.chat"),
// Token-based providers (don't use cookies)
ProviderId::Copilot => None,
ProviderId::Zai => None,
@@ -335,6 +352,9 @@ impl ProviderId {
ProviderId::CrossModel => None,
ProviderId::Sub2Api => None,
ProviderId::Wayfinder => None,
+ ProviderId::ZenMux => None,
+ ProviderId::ClinePass => None,
+ ProviderId::Neuralwatt => None,
}
}
@@ -407,6 +427,10 @@ impl ProviderId {
"sakana" | "sakana-ai" | "sakana ai" => Some(ProviderId::Sakana),
"sub2api" | "sub-2-api" | "sub 2 api" => Some(ProviderId::Sub2Api),
"wayfinder" => Some(ProviderId::Wayfinder),
+ "zenmux" | "zen-mux" => Some(ProviderId::ZenMux),
+ "clinepass" | "cline-pass" | "cline" => Some(ProviderId::ClinePass),
+ "longcat" | "long-cat" | "lc" => Some(ProviderId::LongCat),
+ "neuralwatt" | "neural-watt" | "nw" | "neural" => Some(ProviderId::Neuralwatt),
_ => None,
}
}
@@ -682,6 +706,10 @@ mod tests {
assert!(all.contains(&ProviderId::Sakana));
assert!(all.contains(&ProviderId::Sub2Api));
assert!(all.contains(&ProviderId::Wayfinder));
+ assert!(all.contains(&ProviderId::ZenMux));
+ assert!(all.contains(&ProviderId::ClinePass));
+ assert!(all.contains(&ProviderId::LongCat));
+ assert!(all.contains(&ProviderId::Neuralwatt));
}
#[test]
diff --git a/rust/src/core/provider_factory.rs b/rust/src/core/provider_factory.rs
index 84514ea40c..a5ff4c2094 100644
--- a/rust/src/core/provider_factory.rs
+++ b/rust/src/core/provider_factory.rs
@@ -8,16 +8,17 @@
use super::{Provider, ProviderId};
use crate::providers::{
AbacusProvider, AlibabaProvider, AlibabaTokenPlanProvider, AmpProvider, AntigravityProvider,
- AugmentProvider, AzureOpenAIProvider, BedrockProvider, ChutesProvider, ClaudeProvider,
- 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, ManusProvider, MiMoProvider, MiniMaxProvider,
- MistralProvider, NanoGPTProvider, OllamaProvider, OpenAIApiProvider, OpenCodeGoProvider,
- OpenCodeProvider, OpenRouterProvider, PerplexityProvider, PoeProvider, QoderProvider,
- SakanaProvider, StepFunProvider, Sub2ApiProvider, T3ChatProvider, VeniceProvider,
- VertexAIProvider, WarpProvider, WayfinderProvider, WindsurfProvider, ZaiProvider, ZedProvider,
+ AugmentProvider, AzureOpenAIProvider, BedrockProvider, ChutesProvider, ClinePassProvider,
+ ClaudeProvider, 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, MiniMaxProvider, MistralProvider, NanoGPTProvider,
+ NeuralwattProvider, OllamaProvider, OpenAIApiProvider, OpenCodeGoProvider, OpenCodeProvider,
+ OpenRouterProvider, PerplexityProvider, PoeProvider, QoderProvider, SakanaProvider,
+ StepFunProvider, Sub2ApiProvider, T3ChatProvider, VeniceProvider, VertexAIProvider,
+ WarpProvider, WayfinderProvider, WindsurfProvider, ZaiProvider, ZedProvider, ZenMuxProvider,
};
/// Instantiate the concrete [`Provider`] implementation for a given [`ProviderId`].
@@ -85,6 +86,10 @@ pub fn instantiate(id: ProviderId) -> Box {
ProviderId::Sakana => Box::new(SakanaProvider::new()),
ProviderId::Sub2Api => Box::new(Sub2ApiProvider::new()),
ProviderId::Wayfinder => Box::new(WayfinderProvider::new()),
+ ProviderId::ZenMux => Box::new(ZenMuxProvider::new()),
+ ProviderId::ClinePass => Box::new(ClinePassProvider::new()),
+ ProviderId::LongCat => Box::new(LongCatProvider::new()),
+ ProviderId::Neuralwatt => Box::new(NeuralwattProvider::new()),
}
}
diff --git a/rust/src/core/token_accounts.rs b/rust/src/core/token_accounts.rs
index 3b53b8b46e..56384039a5 100755
--- a/rust/src/core/token_accounts.rs
+++ b/rust/src/core/token_accounts.rs
@@ -209,6 +209,36 @@ impl TokenAccountSupport {
requires_manual_cookie_source: false,
cookie_name: None,
}),
+ ProviderId::ZenMux => Some(TokenAccountSupport {
+ title: "API keys",
+ subtitle: "Store multiple ZenMux Management API keys.",
+ placeholder: "Management API key",
+ injection: TokenInjection::Environment {
+ key: "ZENMUX_MANAGEMENT_API_KEY".to_string(),
+ },
+ requires_manual_cookie_source: false,
+ cookie_name: None,
+ }),
+ ProviderId::ClinePass => Some(TokenAccountSupport {
+ title: "API keys",
+ subtitle: "Store multiple ClinePass API keys.",
+ placeholder: "API key",
+ injection: TokenInjection::Environment {
+ key: "CLINEPASS_API_KEY".to_string(),
+ },
+ requires_manual_cookie_source: false,
+ cookie_name: None,
+ }),
+ ProviderId::Neuralwatt => Some(TokenAccountSupport {
+ title: "API keys",
+ subtitle: "Store multiple Neuralwatt API keys.",
+ placeholder: "API key",
+ injection: TokenInjection::Environment {
+ key: "NEURALWATT_API_KEY".to_string(),
+ },
+ requires_manual_cookie_source: false,
+ cookie_name: None,
+ }),
// Upstream 0.45 #2271: labeled OpenRouter API keys via token accounts.
ProviderId::OpenRouter => Some(TokenAccountSupport {
title: "API keys",
@@ -267,6 +297,7 @@ impl TokenAccountSupport {
| ProviderId::Devin
| ProviderId::Zed
| ProviderId::CrossModel
+ | ProviderId::LongCat
| ProviderId::Wayfinder => None,
}
}
diff --git a/rust/src/providers/clinepass/mod.rs b/rust/src/providers/clinepass/mod.rs
new file mode 100644
index 0000000000..c22f0f2e7d
--- /dev/null
+++ b/rust/src/providers/clinepass/mod.rs
@@ -0,0 +1,216 @@
+//! ClinePass API-key usage provider (upstream 0.44 #2219).
+//!
+//! `GET https://api.cline.bot/api/v1/users/me/plan/usage-limits`
+
+use async_trait::async_trait;
+use chrono::{DateTime, Utc};
+use reqwest::Client;
+use serde::Deserialize;
+
+use crate::core::{
+ FetchContext, Provider, ProviderError, ProviderFetchResult, ProviderId, ProviderMetadata,
+ RateWindow, SourceMode, UsageSnapshot,
+};
+
+const USAGE_URL: &str = "https://api.cline.bot/api/v1/users/me/plan/usage-limits";
+const CREDENTIAL_TARGET: &str = "codexbar-clinepass";
+const ENV_KEYS: &[&str] = &["CLINEPASS_API_KEY", "CLINE_API_KEY"];
+
+#[derive(Debug, Deserialize)]
+struct LimitsResponse {
+ success: bool,
+ data: LimitsData,
+}
+
+#[derive(Debug, Deserialize)]
+struct LimitsData {
+ limits: Vec,
+}
+
+#[derive(Debug, Deserialize)]
+struct LimitEntry {
+ #[serde(rename = "type")]
+ limit_type: String,
+ #[serde(rename = "percentUsed")]
+ percent_used: f64,
+ #[serde(rename = "resetsAt")]
+ resets_at: Option,
+}
+
+pub struct ClinePassProvider {
+ metadata: ProviderMetadata,
+ client: Client,
+}
+
+impl ClinePassProvider {
+ pub fn new() -> Self {
+ Self {
+ metadata: ProviderMetadata {
+ id: ProviderId::ClinePass,
+ display_name: "ClinePass",
+ session_label: "5-hour",
+ weekly_label: "Weekly",
+ supports_opus: true,
+ supports_credits: false,
+ default_enabled: false,
+ is_primary: false,
+ dashboard_url: Some(
+ "https://app.cline.bot/dashboard/subscription?personal=true",
+ ),
+ status_page_url: None,
+ },
+ client: crate::core::credentialed_http_client_builder()
+ .timeout(std::time::Duration::from_secs(15))
+ .build()
+ .unwrap_or_else(|_| Client::new()),
+ }
+ }
+}
+
+impl Default for ClinePassProvider {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+#[async_trait]
+impl Provider for ClinePassProvider {
+ fn id(&self) -> ProviderId {
+ ProviderId::ClinePass
+ }
+
+ fn metadata(&self) -> &ProviderMetadata {
+ &self.metadata
+ }
+
+ async fn fetch_usage(&self, ctx: &FetchContext) -> Result {
+ match ctx.source_mode {
+ SourceMode::Auto | SourceMode::OAuth => {
+ let key = crate::providers::resolve_api_key(
+ ctx.api_key.as_deref(),
+ CREDENTIAL_TARGET,
+ ENV_KEYS,
+ )?;
+ let resp = self
+ .client
+ .get(USAGE_URL)
+ .bearer_auth(key)
+ .header("Accept", "application/json")
+ .send()
+ .await?;
+ let status = resp.status();
+ if status == reqwest::StatusCode::UNAUTHORIZED
+ || status == reqwest::StatusCode::FORBIDDEN
+ {
+ return Err(ProviderError::AuthRequired);
+ }
+ if !status.is_success() {
+ return Err(ProviderError::Other(format!(
+ "ClinePass API error: HTTP {status}"
+ )));
+ }
+ let body: LimitsResponse = resp.json().await.map_err(|e| {
+ ProviderError::Parse(format!("Failed to parse ClinePass usage: {e}"))
+ })?;
+ let snap = snapshot_from_limits(&body)?;
+ Ok(ProviderFetchResult::new(snap, "api"))
+ }
+ SourceMode::Web | SourceMode::Cli => {
+ Err(ProviderError::UnsupportedSource(ctx.source_mode))
+ }
+ }
+ }
+
+ fn available_sources(&self) -> Vec {
+ vec![SourceMode::Auto, SourceMode::OAuth]
+ }
+}
+
+fn parse_iso(raw: Option<&str>) -> Option> {
+ let raw = raw?.trim();
+ if raw.is_empty() {
+ return None;
+ }
+ DateTime::parse_from_rfc3339(raw)
+ .ok()
+ .map(|d| d.with_timezone(&Utc))
+}
+
+fn window_for(entry: &LimitEntry) -> Option<(RateWindow, &'static str)> {
+ let minutes = match entry.limit_type.as_str() {
+ "five_hour" => Some(5 * 60),
+ "weekly" => Some(7 * 24 * 60),
+ "monthly" => Some(30 * 24 * 60),
+ _ => None,
+ }?;
+ let mut w = RateWindow::new(entry.percent_used.clamp(0.0, 100.0));
+ w.window_minutes = Some(minutes);
+ w.resets_at = parse_iso(entry.resets_at.as_deref());
+ let slot = match entry.limit_type.as_str() {
+ "five_hour" => "primary",
+ "weekly" => "secondary",
+ "monthly" => "tertiary",
+ _ => return None,
+ };
+ Some((w, slot))
+}
+
+fn snapshot_from_limits(body: &LimitsResponse) -> Result {
+ if !body.success {
+ return Err(ProviderError::Parse(
+ "ClinePass response success was false".into(),
+ ));
+ }
+ let mut primary = None;
+ let mut secondary = None;
+ let mut tertiary = None;
+ for limit in &body.data.limits {
+ if let Some((w, slot)) = window_for(limit) {
+ match slot {
+ "primary" => primary = Some(w),
+ "secondary" => secondary = Some(w),
+ "tertiary" => tertiary = Some(w),
+ _ => {}
+ }
+ }
+ }
+ let primary = primary.ok_or_else(|| {
+ ProviderError::Parse("ClinePass response missing five_hour window".into())
+ })?;
+ let mut snap = UsageSnapshot::new(primary).with_login_method("API key");
+ if let Some(s) = secondary {
+ snap = snap.with_secondary(s);
+ }
+ if let Some(t) = tertiary {
+ snap = snap.with_tertiary(t);
+ }
+ Ok(snap)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn ignores_unknown_limit_types() {
+ let body: LimitsResponse = serde_json::from_str(
+ r#"{
+ "success": true,
+ "data": {
+ "limits": [
+ { "type": "five_hour", "percentUsed": 12.5, "resetsAt": "2026-07-16T15:00:00Z" },
+ { "type": "experimental_pool", "percentUsed": 77, "resetsAt": "2026-07-16T15:00:00Z" },
+ { "type": "weekly", "percentUsed": 25, "resetsAt": "2026-07-20T00:00:00Z" },
+ { "type": "monthly", "percentUsed": 40, "resetsAt": null }
+ ]
+ }
+ }"#,
+ )
+ .unwrap();
+ let snap = snapshot_from_limits(&body).unwrap();
+ assert!((snap.primary.used_percent - 12.5).abs() < 0.01);
+ assert_eq!(snap.primary.window_minutes, Some(300));
+ assert!((snap.secondary.as_ref().unwrap().used_percent - 25.0).abs() < 0.01);
+ assert!((snap.tertiary.as_ref().unwrap().used_percent - 40.0).abs() < 0.01);
+ }
+}
diff --git a/rust/src/providers/longcat/mod.rs b/rust/src/providers/longcat/mod.rs
new file mode 100644
index 0000000000..5e0cc05462
--- /dev/null
+++ b/rust/src/providers/longcat/mod.rs
@@ -0,0 +1,301 @@
+//! LongCat cookie-based quota + fuel-pack provider (upstream 0.44 #1697).
+//!
+//! Default-disabled. Auth via manual cookie or browser import for longcat.chat.
+
+use async_trait::async_trait;
+use chrono::{DateTime, Utc};
+use reqwest::Client;
+use serde_json::Value;
+
+use crate::core::{
+ FetchContext, Provider, ProviderError, ProviderFetchResult, ProviderId, ProviderMetadata,
+ RateWindow, SourceMode, UsageSnapshot,
+};
+
+const HOST: &str = "https://longcat.chat";
+const USER_CURRENT: &str = "/api/v1/user-current";
+const TOKEN_USAGE: &str = "/api/lc-platform/v1/tokenUsage";
+const PENDING_FUEL: &str = "/api/lc-platform/v1/pending-fuel-packages";
+
+pub struct LongCatProvider {
+ metadata: ProviderMetadata,
+ client: Client,
+}
+
+impl LongCatProvider {
+ pub fn new() -> Self {
+ Self {
+ metadata: ProviderMetadata {
+ id: ProviderId::LongCat,
+ display_name: "LongCat",
+ session_label: "Quota",
+ weekly_label: "Fuel Pack",
+ supports_opus: false,
+ supports_credits: false,
+ default_enabled: false,
+ is_primary: false,
+ dashboard_url: Some("https://longcat.chat/platform/"),
+ status_page_url: None,
+ },
+ // Isolated cookie-free client — auth is only the explicit Cookie header.
+ client: crate::core::credentialed_http_client_builder()
+ .cookie_store(false)
+ .timeout(std::time::Duration::from_secs(20))
+ .build()
+ .unwrap_or_else(|_| Client::new()),
+ }
+ }
+
+ async fn get_json(&self, path: &str, cookie: &str) -> Result {
+ let url = format!("{HOST}{path}");
+ let resp = self
+ .client
+ .get(&url)
+ .header("Cookie", cookie)
+ .header("Accept", "application/json")
+ .send()
+ .await?;
+ let status = resp.status();
+ if status == reqwest::StatusCode::UNAUTHORIZED || status == reqwest::StatusCode::FORBIDDEN {
+ return Err(ProviderError::AuthRequired);
+ }
+ if !status.is_success() {
+ return Err(ProviderError::Other(format!(
+ "LongCat API {path} returned HTTP {status}"
+ )));
+ }
+ resp.json()
+ .await
+ .map_err(|e| ProviderError::Parse(format!("Failed to parse LongCat {path}: {e}")))
+ }
+}
+
+impl Default for LongCatProvider {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+#[async_trait]
+impl Provider for LongCatProvider {
+ fn id(&self) -> ProviderId {
+ ProviderId::LongCat
+ }
+
+ fn metadata(&self) -> &ProviderMetadata {
+ &self.metadata
+ }
+
+ async fn fetch_usage(&self, ctx: &FetchContext) -> Result {
+ match ctx.source_mode {
+ SourceMode::Auto | SourceMode::Web => {
+ let cookie = match ctx.manual_cookie_header.as_deref() {
+ Some(c) => normalize_cookie_header(c).ok_or(ProviderError::NoCookies)?,
+ None => crate::providers::browser_cookie_header(&["longcat.chat"])?,
+ };
+ let account = self.get_json(USER_CURRENT, &cookie).await?;
+ // Meituan-style envelope may return HTTP 200 with business 401.
+ if let Some(code) = envelope_code(&account) {
+ if code == 401 || code == 403 {
+ return Err(ProviderError::AuthRequired);
+ }
+ }
+ let usage_raw = self.get_json(TOKEN_USAGE, &cookie).await?;
+ let fuel = match self.get_json(PENDING_FUEL, &cookie).await {
+ Ok(v) => Some(v),
+ Err(ProviderError::AuthRequired) => return Err(ProviderError::AuthRequired),
+ Err(_) => None,
+ };
+ let snap = build_snapshot(&account, &usage_raw, fuel.as_ref())?;
+ Ok(ProviderFetchResult::new(snap, "web"))
+ }
+ SourceMode::Cli | SourceMode::OAuth => {
+ Err(ProviderError::UnsupportedSource(ctx.source_mode))
+ }
+ }
+ }
+
+ fn available_sources(&self) -> Vec {
+ vec![SourceMode::Auto, SourceMode::Web]
+ }
+}
+
+fn normalize_cookie_header(raw: &str) -> Option {
+ let mut header = raw.trim().to_string();
+ if header
+ .get(.."cookie:".len())
+ .is_some_and(|p| p.eq_ignore_ascii_case("cookie:"))
+ {
+ header = header["cookie:".len()..].trim().to_string();
+ }
+ (!header.is_empty()).then_some(header)
+}
+
+fn envelope_code(value: &Value) -> Option {
+ value
+ .get("code")
+ .and_then(|c| c.as_i64())
+ .or_else(|| value.get("status").and_then(|c| c.as_i64()))
+}
+
+fn envelope_data(value: &Value) -> &Value {
+ value.get("data").unwrap_or(value)
+}
+
+fn json_f64(value: &Value, key: &str) -> Option {
+ let v = value.get(key)?;
+ v.as_f64()
+ .or_else(|| v.as_i64().map(|i| i as f64))
+ .or_else(|| v.as_str()?.parse().ok())
+}
+
+fn json_str(value: &Value, key: &str) -> Option {
+ value
+ .get(key)
+ .and_then(|v| v.as_str())
+ .map(|s| s.trim().to_string())
+ .filter(|s| !s.is_empty())
+}
+
+fn build_snapshot(
+ account: &Value,
+ usage_raw: &Value,
+ fuel_raw: Option<&Value>,
+) -> Result {
+ let account_data = envelope_data(account);
+ let usage_outer = envelope_data(usage_raw);
+ let usage = usage_outer
+ .get("usage")
+ .filter(|u| u.is_object())
+ .unwrap_or(usage_outer);
+
+ let total = json_f64(usage, "totalToken").ok_or_else(|| {
+ ProviderError::Parse("tokenUsage data was missing totalToken".into())
+ })?;
+ let remaining = json_f64(usage, "availableToken");
+ let used = remaining.map(|r| (total - r).max(0.0)).unwrap_or(0.0);
+
+ let primary = if total > 0.0 {
+ let mut w = RateWindow::new(((used / total) * 100.0).clamp(0.0, 100.0));
+ w.reset_description = Some(format!("{}/{}", used as i64, total as i64));
+ w
+ } else {
+ RateWindow::informational("No token quota")
+ };
+
+ let account_name = json_str(account_data, "name")
+ .or_else(|| json_str(account_data, "nickname"))
+ .or_else(|| json_str(account_data, "userName"));
+
+ let mut snap = UsageSnapshot::new(primary);
+ if let Some(name) = account_name {
+ snap.account_organization = Some(name);
+ }
+
+ if let Some(fuel_raw) = fuel_raw {
+ let fuel_data = envelope_data(fuel_raw);
+ if let Some((total_fuel, remaining_fuel, expiry)) = parse_fuel(fuel_data) {
+ if total_fuel > 0.0 {
+ let used_fuel = (total_fuel - remaining_fuel).max(0.0);
+ let mut secondary =
+ RateWindow::new(((used_fuel / total_fuel) * 100.0).clamp(0.0, 100.0));
+ secondary.resets_at = expiry;
+ secondary.reset_description = Some(format!(
+ "Fuel pack: {}/{}",
+ remaining_fuel as i64, total_fuel as i64
+ ));
+ snap = snap.with_secondary(secondary);
+ }
+ }
+ }
+
+ Ok(snap)
+}
+
+fn parse_fuel(fuel: &Value) -> Option<(f64, f64, Option>)> {
+ // Accept either { packages: [...] } or a bare array.
+ let packages = fuel
+ .get("packages")
+ .and_then(|p| p.as_array())
+ .or_else(|| fuel.as_array())?;
+
+ let mut total = 0.0;
+ let mut remaining = 0.0;
+ let mut saw_remaining = false;
+ let mut nearest: Option> = None;
+
+ for pkg in packages {
+ if let Some(t) = json_f64(pkg, "totalToken")
+ .or_else(|| json_f64(pkg, "total"))
+ .or_else(|| json_f64(pkg, "amount"))
+ {
+ total += t.max(0.0);
+ }
+ if let Some(r) = json_f64(pkg, "availableToken")
+ .or_else(|| json_f64(pkg, "remainingToken"))
+ .or_else(|| json_f64(pkg, "remaining"))
+ {
+ remaining += r.max(0.0);
+ saw_remaining = true;
+ }
+ if let Some(raw) = json_str(pkg, "expireTime")
+ .or_else(|| json_str(pkg, "expireAt"))
+ .or_else(|| json_str(pkg, "expiresAt"))
+ {
+ if let Ok(dt) = DateTime::parse_from_rfc3339(&raw) {
+ let utc = dt.with_timezone(&Utc);
+ nearest = Some(match nearest {
+ Some(n) if n < utc => n,
+ _ => utc,
+ });
+ }
+ }
+ }
+
+ if total <= 0.0 && !saw_remaining {
+ return None;
+ }
+ if total <= 0.0 {
+ total = remaining;
+ }
+ let remaining = if saw_remaining { remaining } else { total };
+ Some((total, remaining, nearest))
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use serde_json::json;
+
+ #[test]
+ fn builds_quota_and_fuel() {
+ let account = json!({ "code": 0, "data": { "name": "cat" } });
+ let usage = json!({
+ "code": 0,
+ "data": {
+ "usage": { "totalToken": 1000, "availableToken": 250 }
+ }
+ });
+ let fuel = json!({
+ "code": 0,
+ "data": {
+ "packages": [
+ { "totalToken": 200, "availableToken": 50, "expireTime": "2026-08-01T00:00:00Z" }
+ ]
+ }
+ });
+ let snap = build_snapshot(&account, &usage, Some(&fuel)).unwrap();
+ assert!((snap.primary.used_percent - 75.0).abs() < 0.01);
+ assert_eq!(snap.account_organization.as_deref(), Some("cat"));
+ let fuel_w = snap.secondary.unwrap();
+ assert!((fuel_w.used_percent - 75.0).abs() < 0.01);
+ }
+
+ #[test]
+ fn normalizes_cookie() {
+ assert_eq!(
+ normalize_cookie_header("Cookie: a=1; b=2").as_deref(),
+ Some("a=1; b=2")
+ );
+ }
+}
diff --git a/rust/src/providers/mod.rs b/rust/src/providers/mod.rs
index c626404be8..4a349f776c 100755
--- a/rust/src/providers/mod.rs
+++ b/rust/src/providers/mod.rs
@@ -11,6 +11,7 @@ pub mod augment;
pub mod azureopenai;
pub mod bedrock;
pub mod chutes;
+pub mod clinepass;
pub mod claude;
pub mod codebuff;
pub mod codex;
@@ -37,11 +38,13 @@ pub mod kimik2;
pub mod kiro;
pub mod litellm;
pub mod llmproxy;
+pub mod longcat;
pub mod manus;
pub mod mimo;
pub mod minimax;
pub mod mistral;
pub mod nanogpt;
+pub mod neuralwatt;
pub mod ollama;
pub mod openai;
pub mod openaiapi;
@@ -62,6 +65,7 @@ pub mod wayfinder;
pub mod windsurf;
pub mod zai;
pub mod zed;
+pub mod zenmux;
// Re-export provider implementations
pub use abacus::AbacusProvider;
@@ -73,6 +77,7 @@ pub use augment::AugmentProvider;
pub use azureopenai::AzureOpenAIProvider;
pub use bedrock::BedrockProvider;
pub use chutes::ChutesProvider;
+pub use clinepass::ClinePassProvider;
pub use claude::ClaudeProvider;
pub use codebuff::CodebuffProvider;
pub use codex::CodexProvider;
@@ -98,12 +103,14 @@ pub use kimi::KimiProvider;
pub use kimik2::KimiK2Provider;
pub use kiro::KiroProvider;
pub use litellm::LiteLLMProvider;
+pub use longcat::LongCatProvider;
pub use llmproxy::LLMProxyProvider;
pub use manus::ManusProvider;
pub use mimo::MiMoProvider;
pub use minimax::{MiniMaxProvider, MiniMaxRegion};
pub use mistral::MistralProvider;
pub use nanogpt::NanoGPTProvider;
+pub use neuralwatt::NeuralwattProvider;
pub use ollama::OllamaProvider;
pub use openaiapi::OpenAIApiProvider;
pub use opencode::OpenCodeProvider;
@@ -123,6 +130,7 @@ pub use wayfinder::WayfinderProvider;
pub use windsurf::WindsurfProvider;
pub use zai::ZaiProvider;
pub use zed::ZedProvider;
+pub use zenmux::ZenMuxProvider;
pub(crate) fn browser_cookie_header(
domains: &[&str],
diff --git a/rust/src/providers/neuralwatt/mod.rs b/rust/src/providers/neuralwatt/mod.rs
new file mode 100644
index 0000000000..e4324593c5
--- /dev/null
+++ b/rust/src/providers/neuralwatt/mod.rs
@@ -0,0 +1,363 @@
+//! Neuralwatt API-key usage provider (upstream 0.44 #2220).
+//!
+//! `GET https://api.neuralwatt.com/v1/quota` — subscription kWh + prepaid credits.
+
+use async_trait::async_trait;
+use chrono::{DateTime, Utc};
+use reqwest::Client;
+use serde::Deserialize;
+
+use crate::core::{
+ CostSnapshot, FetchContext, NamedRateWindow, Provider, ProviderError, ProviderFetchResult,
+ ProviderId, ProviderMetadata, RateWindow, SourceMode, UsageSnapshot,
+};
+
+const DEFAULT_API_BASE: &str = "https://api.neuralwatt.com";
+const CREDENTIAL_TARGET: &str = "codexbar-neuralwatt";
+const ENV_KEYS: &[&str] = &["NEURALWATT_API_KEY"];
+
+#[derive(Debug, Deserialize, Default)]
+struct QuotaResponse {
+ balance: Option,
+ usage: Option,
+ subscription: Option,
+ key: Option,
+}
+
+#[derive(Debug, Deserialize, Default, Clone)]
+struct Balance {
+ credits_remaining_usd: Option,
+ total_credits_usd: Option,
+ credits_used_usd: Option,
+ accounting_method: Option,
+}
+
+#[derive(Debug, Deserialize, Default)]
+struct Usage {
+ current_month: Option,
+}
+
+#[derive(Debug, Deserialize, Default)]
+struct UsagePeriod {
+ cost_usd: Option,
+ energy_kwh: Option,
+}
+
+#[derive(Debug, Deserialize, Default, Clone)]
+struct Subscription {
+ plan: Option,
+ status: Option,
+ current_period_start: Option,
+ current_period_end: Option,
+ kwh_included: Option,
+ kwh_used: Option,
+ kwh_remaining: Option,
+ #[allow(dead_code)]
+ in_overage: Option,
+}
+
+#[derive(Debug, Deserialize, Default)]
+struct KeyInfo {
+ allowance: Option,
+}
+
+#[derive(Debug, Deserialize, Default, Clone)]
+struct KeyAllowance {
+ limit_usd: Option,
+ period: Option,
+ spent_usd: Option,
+ remaining_usd: Option,
+ blocked: Option,
+}
+
+pub struct NeuralwattProvider {
+ metadata: ProviderMetadata,
+ client: Client,
+}
+
+impl NeuralwattProvider {
+ pub fn new() -> Self {
+ Self {
+ metadata: ProviderMetadata {
+ id: ProviderId::Neuralwatt,
+ display_name: "Neuralwatt",
+ session_label: "Subscription",
+ weekly_label: "Key allowance",
+ supports_opus: false,
+ supports_credits: false,
+ default_enabled: false,
+ is_primary: false,
+ dashboard_url: Some("https://portal.neuralwatt.com/dashboard"),
+ status_page_url: None,
+ },
+ client: crate::core::credentialed_http_client_builder()
+ .timeout(std::time::Duration::from_secs(15))
+ .build()
+ .unwrap_or_else(|_| Client::new()),
+ }
+ }
+
+ fn quota_url() -> String {
+ let base = std::env::var("NEURALWATT_API_URL")
+ .ok()
+ .map(|s| s.trim().trim_end_matches('/').to_string())
+ .filter(|s| !s.is_empty())
+ .unwrap_or_else(|| DEFAULT_API_BASE.to_string());
+ if base.ends_with("/v1") {
+ format!("{base}/quota")
+ } else {
+ format!("{base}/v1/quota")
+ }
+ }
+}
+
+impl Default for NeuralwattProvider {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+#[async_trait]
+impl Provider for NeuralwattProvider {
+ fn id(&self) -> ProviderId {
+ ProviderId::Neuralwatt
+ }
+
+ fn metadata(&self) -> &ProviderMetadata {
+ &self.metadata
+ }
+
+ async fn fetch_usage(&self, ctx: &FetchContext) -> Result {
+ match ctx.source_mode {
+ SourceMode::Auto | SourceMode::OAuth => {
+ let key = crate::providers::resolve_api_key(
+ ctx.api_key.as_deref(),
+ CREDENTIAL_TARGET,
+ ENV_KEYS,
+ )?;
+ let resp = self
+ .client
+ .get(Self::quota_url())
+ .bearer_auth(key)
+ .header("Accept", "application/json")
+ .send()
+ .await?;
+ let status = resp.status();
+ if status == reqwest::StatusCode::UNAUTHORIZED
+ || status == reqwest::StatusCode::FORBIDDEN
+ {
+ return Err(ProviderError::AuthRequired);
+ }
+ if !status.is_success() {
+ return Err(ProviderError::Other(format!(
+ "Neuralwatt API error: HTTP {status}"
+ )));
+ }
+ let body: QuotaResponse = resp.json().await.map_err(|e| {
+ ProviderError::Parse(format!("Failed to parse Neuralwatt quota: {e}"))
+ })?;
+ let (snap, cost) = snapshot_from_quota(&body)?;
+ let mut result = ProviderFetchResult::new(snap, "api");
+ if let Some(cost) = cost {
+ result = result.with_cost(cost);
+ }
+ Ok(result)
+ }
+ SourceMode::Web | SourceMode::Cli => {
+ Err(ProviderError::UnsupportedSource(ctx.source_mode))
+ }
+ }
+ }
+
+ fn available_sources(&self) -> Vec {
+ vec![SourceMode::Auto, SourceMode::OAuth]
+ }
+}
+
+fn valid_nn(v: Option) -> Option {
+ v.filter(|x| x.is_finite() && *x >= 0.0)
+}
+
+fn valid_pos(v: Option) -> Option {
+ v.filter(|x| x.is_finite() && *x > 0.0)
+}
+
+fn parse_iso(raw: Option<&str>) -> Option> {
+ let raw = raw?.trim();
+ if raw.is_empty() {
+ return None;
+ }
+ DateTime::parse_from_rfc3339(raw)
+ .ok()
+ .map(|d| d.with_timezone(&Utc))
+}
+
+fn format_kwh(value: f64) -> String {
+ if (value - value.round()).abs() < f64::EPSILON {
+ format!("{:.0}", value)
+ } else {
+ format!("{:.2}", value)
+ }
+}
+
+fn subscription_window(sub: &Subscription) -> Option {
+ let total = valid_pos(sub.kwh_included).or_else(|| {
+ let used = valid_nn(sub.kwh_used)?;
+ let remaining = valid_nn(sub.kwh_remaining)?;
+ let t = used + remaining;
+ (t > 0.0).then_some(t)
+ })?;
+ let used = valid_nn(sub.kwh_used).or_else(|| {
+ let remaining = valid_nn(sub.kwh_remaining)?;
+ Some((total - remaining).max(0.0))
+ })?;
+ let mut w = RateWindow::new(((used / total) * 100.0).clamp(0.0, 100.0));
+ if let (Some(start), Some(end)) = (
+ parse_iso(sub.current_period_start.as_deref()),
+ parse_iso(sub.current_period_end.as_deref()),
+ ) {
+ if end > start {
+ let mins = ((end - start).num_minutes()).max(1) as u32;
+ w.window_minutes = Some(mins);
+ }
+ w.resets_at = Some(end);
+ }
+ w.reset_description = Some(format!(
+ "{} / {} kWh",
+ format_kwh(used),
+ format_kwh(total)
+ ));
+ Some(w)
+}
+
+fn prepaid_remaining(bal: &Balance) -> Option {
+ if let Some(r) = valid_nn(bal.credits_remaining_usd) {
+ return Some(r);
+ }
+ let total = valid_pos(bal.total_credits_usd)?;
+ let used = valid_nn(bal.credits_used_usd)?;
+ Some((total - used).max(0.0))
+}
+
+fn snapshot_from_quota(
+ body: &QuotaResponse,
+) -> Result<(UsageSnapshot, Option), ProviderError> {
+ let sub_window = body
+ .subscription
+ .as_ref()
+ .and_then(subscription_window)
+ .unwrap_or_else(|| RateWindow::informational("No active subscription kWh"));
+
+ let mut snap = UsageSnapshot::new(sub_window);
+ if let Some(sub) = &body.subscription {
+ if let Some(plan) = sub
+ .plan
+ .as_deref()
+ .map(str::trim)
+ .filter(|s| !s.is_empty())
+ {
+ let label = plan.replace('_', " ");
+ snap = snap.with_login_method(format!(
+ "{} plan",
+ label
+ .split_whitespace()
+ .map(|w| {
+ let mut c = w.chars();
+ match c.next() {
+ None => String::new(),
+ Some(f) => f.to_uppercase().collect::() + c.as_str(),
+ }
+ })
+ .collect::>()
+ .join(" ")
+ ));
+ } else if let Some(method) = body
+ .balance
+ .as_ref()
+ .and_then(|b| b.accounting_method.clone())
+ .filter(|s| !s.is_empty())
+ {
+ snap = snap.with_login_method(method);
+ }
+ let _status = sub.status.as_ref();
+ }
+
+ if let Some(allowance) = body.key.as_ref().and_then(|k| k.allowance.clone()) {
+ let percent = if allowance.blocked == Some(true) {
+ Some(100.0)
+ } else if let (Some(spent), Some(limit)) =
+ (valid_nn(allowance.spent_usd), valid_pos(allowance.limit_usd))
+ {
+ Some(((spent / limit) * 100.0).clamp(0.0, 100.0))
+ } else {
+ None
+ };
+ if let Some(percent) = percent {
+ let period = allowance
+ .period
+ .as_deref()
+ .unwrap_or("allowance")
+ .to_string();
+ let title = format!(
+ "Key {}",
+ period
+ .chars()
+ .next()
+ .map(|c| c.to_uppercase().collect::() + &period[c.len_utf8()..])
+ .unwrap_or_else(|| period)
+ );
+ snap.extra_rate_windows
+ .push(NamedRateWindow::new("key-allowance", title, RateWindow::new(percent)));
+ let _remaining = allowance.remaining_usd;
+ }
+ }
+
+ let cost = body
+ .balance
+ .as_ref()
+ .and_then(prepaid_remaining)
+ .map(|remaining| CostSnapshot::new(remaining, "USD", "Neuralwatt prepaid balance"));
+
+ let _month = body.usage.as_ref().and_then(|u| u.current_month.as_ref());
+ Ok((snap, cost))
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn subscription_kwh_and_prepaid() {
+ let body: QuotaResponse = serde_json::from_str(
+ r#"{
+ "balance": {
+ "credits_remaining_usd": 8.5,
+ "total_credits_usd": 20.0,
+ "credits_used_usd": 11.5
+ },
+ "subscription": {
+ "plan": "starter",
+ "kwh_included": 100.0,
+ "kwh_used": 40.0,
+ "kwh_remaining": 60.0,
+ "current_period_end": "2026-08-01T00:00:00Z"
+ },
+ "key": {
+ "allowance": {
+ "limit_usd": 50.0,
+ "period": "month",
+ "spent_usd": 10.0,
+ "blocked": false
+ }
+ }
+ }"#,
+ )
+ .unwrap();
+ let (snap, cost) = snapshot_from_quota(&body).unwrap();
+ assert!((snap.primary.used_percent - 40.0).abs() < 0.01);
+ assert!(snap.login_method.as_deref().unwrap().contains("Starter"));
+ assert_eq!(snap.extra_rate_windows.len(), 1);
+ assert!((snap.extra_rate_windows[0].window.used_percent - 20.0).abs() < 0.01);
+ assert!((cost.unwrap().used - 8.5).abs() < 0.001);
+ }
+}
diff --git a/rust/src/providers/zenmux/mod.rs b/rust/src/providers/zenmux/mod.rs
new file mode 100644
index 0000000000..4d739570dd
--- /dev/null
+++ b/rust/src/providers/zenmux/mod.rs
@@ -0,0 +1,298 @@
+//! ZenMux Management API usage provider (upstream 0.44).
+//!
+//! - `GET https://zenmux.ai/api/v1/management/subscription/detail`
+//! - Optional PAYG: `GET .../payg/balance`
+
+use async_trait::async_trait;
+use chrono::{DateTime, Utc};
+use reqwest::Client;
+use serde::Deserialize;
+
+use crate::core::{
+ CostSnapshot, FetchContext, Provider, ProviderError, ProviderFetchResult, ProviderId,
+ ProviderMetadata, RateWindow, SourceMode, UsageSnapshot,
+};
+
+const MANAGEMENT_BASE: &str = "https://zenmux.ai/api/v1/management";
+const CREDENTIAL_TARGET: &str = "codexbar-zenmux";
+const ENV_KEYS: &[&str] = &["ZENMUX_MANAGEMENT_API_KEY", "ZENMUX_API_KEY"];
+
+#[derive(Debug, Deserialize)]
+struct SubscriptionEnvelope {
+ success: bool,
+ data: SubscriptionData,
+}
+
+#[derive(Debug, Deserialize)]
+struct SubscriptionData {
+ plan: PlanInfo,
+ #[serde(default)]
+ account_status: String,
+ quota_5_hour: QuotaInfo,
+ quota_7_day: QuotaInfo,
+}
+
+#[derive(Debug, Deserialize)]
+struct PlanInfo {
+ #[serde(default)]
+ tier: String,
+ expires_at: Option,
+}
+
+#[derive(Debug, Deserialize)]
+struct QuotaInfo {
+ usage_percentage: f64,
+ resets_at: Option,
+ max_flows: f64,
+ used_flows: f64,
+ #[allow(dead_code)]
+ remaining_flows: f64,
+}
+
+#[derive(Debug, Deserialize)]
+struct BalanceEnvelope {
+ success: bool,
+ data: BalanceData,
+}
+
+#[derive(Debug, Deserialize)]
+struct BalanceData {
+ currency: String,
+ total_credits: f64,
+}
+
+pub struct ZenMuxProvider {
+ metadata: ProviderMetadata,
+ client: Client,
+}
+
+impl ZenMuxProvider {
+ pub fn new() -> Self {
+ Self {
+ metadata: ProviderMetadata {
+ id: ProviderId::ZenMux,
+ display_name: "ZenMux",
+ session_label: "5-hour quota",
+ weekly_label: "Weekly quota",
+ supports_opus: false,
+ supports_credits: false,
+ default_enabled: false,
+ is_primary: false,
+ dashboard_url: Some("https://zenmux.ai/platform/management"),
+ status_page_url: None,
+ },
+ client: crate::core::credentialed_http_client_builder()
+ .timeout(std::time::Duration::from_secs(15))
+ .build()
+ .unwrap_or_else(|_| Client::new()),
+ }
+ }
+
+ fn resolve_key(api_key: Option<&str>) -> Result {
+ crate::providers::resolve_api_key(api_key, CREDENTIAL_TARGET, ENV_KEYS)
+ }
+
+ async fn fetch_json(&self, path: &str, key: &str) -> Result {
+ let url = format!("{MANAGEMENT_BASE}/{path}");
+ let resp = self
+ .client
+ .get(&url)
+ .bearer_auth(key)
+ .header("Accept", "application/json")
+ .send()
+ .await?;
+ let status = resp.status();
+ if status == reqwest::StatusCode::UNAUTHORIZED || status == reqwest::StatusCode::FORBIDDEN {
+ return Err(ProviderError::AuthRequired);
+ }
+ if !status.is_success() {
+ return Err(ProviderError::Other(format!(
+ "ZenMux Management API returned HTTP {status}"
+ )));
+ }
+ resp.json()
+ .await
+ .map_err(|e| ProviderError::Parse(format!("Failed to parse ZenMux response: {e}")))
+ }
+}
+
+impl Default for ZenMuxProvider {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+#[async_trait]
+impl Provider for ZenMuxProvider {
+ fn id(&self) -> ProviderId {
+ ProviderId::ZenMux
+ }
+
+ fn metadata(&self) -> &ProviderMetadata {
+ &self.metadata
+ }
+
+ async fn fetch_usage(&self, ctx: &FetchContext) -> Result {
+ match ctx.source_mode {
+ SourceMode::Auto | SourceMode::OAuth => {
+ let key = Self::resolve_key(ctx.api_key.as_deref())?;
+ let sub_val = self.fetch_json("subscription/detail", &key).await?;
+ let snapshot = snapshot_from_subscription(&sub_val)?;
+
+ let mut result = ProviderFetchResult::new(snapshot, "api");
+ // PAYG balance is optional; failures do not fail the whole fetch.
+ if let Ok(bal_val) = self.fetch_json("payg/balance", &key).await
+ && let Ok(cost) = payg_cost_from_balance(&bal_val)
+ {
+ result = result.with_cost(cost);
+ }
+ Ok(result)
+ }
+ SourceMode::Web | SourceMode::Cli => {
+ Err(ProviderError::UnsupportedSource(ctx.source_mode))
+ }
+ }
+ }
+
+ fn available_sources(&self) -> Vec {
+ vec![SourceMode::Auto, SourceMode::OAuth]
+ }
+}
+
+fn parse_iso(raw: Option<&str>) -> Option> {
+ let raw = raw?.trim();
+ if raw.is_empty() {
+ return None;
+ }
+ DateTime::parse_from_rfc3339(raw)
+ .ok()
+ .map(|d| d.with_timezone(&Utc))
+}
+
+fn quota_window(q: &QuotaInfo, minutes: u32) -> RateWindow {
+ let used = (q.usage_percentage * 100.0).clamp(0.0, 100.0);
+ let mut w = RateWindow::new(used);
+ w.window_minutes = Some(minutes);
+ w.resets_at = parse_iso(q.resets_at.as_deref());
+ w.reset_description = Some(format!(
+ "{} / {} flows",
+ format_amount(q.used_flows),
+ format_amount(q.max_flows)
+ ));
+ w
+}
+
+fn format_amount(value: f64) -> String {
+ if (value - value.round()).abs() < f64::EPSILON {
+ format!("{:.0}", value)
+ } else {
+ format!("{:.2}", value)
+ }
+}
+
+fn snapshot_from_subscription(value: &serde_json::Value) -> Result {
+ let env: SubscriptionEnvelope = serde_json::from_value(value.clone())
+ .map_err(|e| ProviderError::Parse(format!("Failed to parse ZenMux subscription: {e}")))?;
+ if !env.success {
+ return Err(ProviderError::Parse(
+ "ZenMux subscription response reported failure".into(),
+ ));
+ }
+ let plan = env.data.plan.tier.trim();
+ let status = env.data.account_status.trim();
+ let login = if status.eq_ignore_ascii_case("healthy") || status.is_empty() {
+ if plan.is_empty() {
+ None
+ } else {
+ Some(format!("{} plan", capitalize(plan)))
+ }
+ } else if plan.is_empty() {
+ Some(capitalize(status))
+ } else {
+ Some(format!("{} plan · {}", capitalize(plan), capitalize(status)))
+ };
+
+ let mut snap = UsageSnapshot::new(quota_window(&env.data.quota_5_hour, 5 * 60))
+ .with_secondary(quota_window(&env.data.quota_7_day, 7 * 24 * 60));
+ if let Some(login) = login {
+ snap = snap.with_login_method(login);
+ }
+ let _expires = parse_iso(env.data.plan.expires_at.as_deref());
+ Ok(snap)
+}
+
+fn payg_cost_from_balance(value: &serde_json::Value) -> Result {
+ let env: BalanceEnvelope = serde_json::from_value(value.clone())
+ .map_err(|e| ProviderError::Parse(format!("Failed to parse ZenMux balance: {e}")))?;
+ if !env.success {
+ return Err(ProviderError::Parse(
+ "ZenMux balance response reported failure".into(),
+ ));
+ }
+ if !env.data.currency.trim().eq_ignore_ascii_case("usd") {
+ return Err(ProviderError::Parse(
+ "ZenMux balance currency is not USD".into(),
+ ));
+ }
+ Ok(CostSnapshot::new(
+ env.data.total_credits,
+ "USD",
+ "ZenMux PAYG balance",
+ ))
+}
+
+fn capitalize(s: &str) -> String {
+ let mut c = s.chars();
+ match c.next() {
+ None => String::new(),
+ Some(f) => f.to_uppercase().collect::() + c.as_str(),
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use serde_json::json;
+
+ #[test]
+ fn parses_subscription_windows() {
+ let value = json!({
+ "success": true,
+ "data": {
+ "plan": { "tier": "pro", "expires_at": "2026-08-01T00:00:00Z" },
+ "account_status": "healthy",
+ "quota_5_hour": {
+ "usage_percentage": 0.25,
+ "resets_at": "2026-07-21T10:00:00Z",
+ "max_flows": 100,
+ "used_flows": 25,
+ "remaining_flows": 75
+ },
+ "quota_7_day": {
+ "usage_percentage": 0.1,
+ "resets_at": "2026-07-28T00:00:00Z",
+ "max_flows": 1000,
+ "used_flows": 100,
+ "remaining_flows": 900
+ }
+ }
+ });
+ let snap = snapshot_from_subscription(&value).unwrap();
+ assert!((snap.primary.used_percent - 25.0).abs() < 0.01);
+ assert_eq!(snap.primary.window_minutes, Some(300));
+ let weekly = snap.secondary.unwrap();
+ assert!((weekly.used_percent - 10.0).abs() < 0.01);
+ assert_eq!(snap.login_method.as_deref(), Some("Pro plan"));
+ }
+
+ #[test]
+ fn parses_payg_balance() {
+ let value = json!({
+ "success": true,
+ "data": { "currency": "USD", "total_credits": 12.5 }
+ });
+ let cost = payg_cost_from_balance(&value).unwrap();
+ assert!((cost.used - 12.5).abs() < 0.001);
+ assert_eq!(cost.currency_code, "USD");
+ }
+}
diff --git a/rust/src/settings/api_keys.rs b/rust/src/settings/api_keys.rs
index 61c2e42719..d36d92e8be 100644
--- a/rust/src/settings/api_keys.rs
+++ b/rust/src/settings/api_keys.rs
@@ -310,6 +310,37 @@ pub fn get_api_key_providers() -> Vec {
config_file_path: None,
dashboard_url: Some("https://deepinfra.com/dash"),
},
+ ProviderConfigInfo {
+ id: ProviderId::ZenMux,
+ name: "ZenMux",
+ requires_api_key: true,
+ api_key_env_var: Some("ZENMUX_MANAGEMENT_API_KEY"),
+ api_key_help: Some(
+ "Use a ZenMux Management API key (not an inference key). Also accepts ZENMUX_API_KEY.",
+ ),
+ config_file_path: None,
+ dashboard_url: Some("https://zenmux.ai/platform/management"),
+ },
+ ProviderConfigInfo {
+ id: ProviderId::ClinePass,
+ name: "ClinePass",
+ requires_api_key: true,
+ api_key_env_var: Some("CLINEPASS_API_KEY"),
+ api_key_help: Some(
+ "Get your API key from Cline / ClinePass. Also accepts CLINE_API_KEY.",
+ ),
+ config_file_path: None,
+ dashboard_url: Some("https://app.cline.bot/dashboard/subscription?personal=true"),
+ },
+ ProviderConfigInfo {
+ id: ProviderId::Neuralwatt,
+ name: "Neuralwatt",
+ requires_api_key: true,
+ api_key_env_var: Some("NEURALWATT_API_KEY"),
+ api_key_help: Some("Get your API key from portal.neuralwatt.com."),
+ config_file_path: None,
+ dashboard_url: Some("https://portal.neuralwatt.com/dashboard"),
+ },
ProviderConfigInfo {
id: ProviderId::Doubao,
name: "Doubao / Volcengine Ark",