diff --git a/apps/desktop-tauri/src/components/providers/icons/ProviderIcon-crof.svg b/apps/desktop-tauri/src/components/providers/icons/ProviderIcon-crof.svg
deleted file mode 100644
index fdde018b8f..0000000000
--- a/apps/desktop-tauri/src/components/providers/icons/ProviderIcon-crof.svg
+++ /dev/null
@@ -1,3 +0,0 @@
-
diff --git a/apps/desktop-tauri/src/components/providers/providerIcons.test.ts b/apps/desktop-tauri/src/components/providers/providerIcons.test.ts
index 27c677024b..a204e1155a 100644
--- a/apps/desktop-tauri/src/components/providers/providerIcons.test.ts
+++ b/apps/desktop-tauri/src/components/providers/providerIcons.test.ts
@@ -8,4 +8,8 @@ describe("provider icon registry", () => {
expect(PROVIDER_ICON_REGISTRY[id], id).toBeDefined();
}
});
+
+ it("does not expose the retired Crof provider", () => {
+ expect(PROVIDER_ICON_REGISTRY).not.toHaveProperty("crof");
+ });
});
diff --git a/apps/desktop-tauri/src/components/providers/providerIcons.ts b/apps/desktop-tauri/src/components/providers/providerIcons.ts
index ccfa304078..540527b7d5 100644
--- a/apps/desktop-tauri/src/components/providers/providerIcons.ts
+++ b/apps/desktop-tauri/src/components/providers/providerIcons.ts
@@ -14,7 +14,6 @@ import coderabbit from "./icons/ProviderIcon-coderabbit.svg?raw";
import codex from "./icons/ProviderIcon-codex.svg?raw";
import commandcode from "./icons/ProviderIcon-commandcode.svg?raw";
import copilot from "./icons/ProviderIcon-copilot.svg?raw";
-import crof from "./icons/ProviderIcon-crof.svg?raw";
import crossmodel from "./icons/ProviderIcon-crossmodel.svg?raw";
import cursor from "./icons/ProviderIcon-cursor.svg?raw";
import deepgram from "./icons/ProviderIcon-deepgram.svg?raw";
@@ -102,7 +101,6 @@ const RAW: Record = {
codex: tint(codex),
commandcode: tint(commandcode),
copilot: tint(copilot),
- crof: tint(crof),
crossmodel: tint(crossmodel),
cursor: tint(cursor),
deepgram: tint(deepgram),
@@ -218,7 +216,6 @@ export const PROVIDER_ICON_REGISTRY: Record = {
mimo: { id: "mimo", brandColor: "#ff6900", fallbackLetter: "M", svgPath: RAW.mimo },
doubao: { id: "doubao", brandColor: "#2563eb", fallbackLetter: "D", svgPath: RAW.doubao },
commandcode: { id: "commandcode", brandColor: "#44ff00", fallbackLetter: "C", svgPath: RAW.commandcode },
- crof: { id: "crof", brandColor: "#7c3aed", fallbackLetter: "C", svgPath: RAW.crof },
crossmodel: { id: "crossmodel", brandColor: "#c084fc", fallbackLetter: "X", svgPath: RAW.crossmodel },
qoder: { id: "qoder", brandColor: "#2563eb", fallbackLetter: "Q", svgPath: RAW.qoder },
replicate: { id: "replicate", brandColor: "#000000", fallbackLetter: "R", svgPath: RAW.replicate },
diff --git a/apps/desktop-tauri/src/surfaces/TrayPanel.tsx b/apps/desktop-tauri/src/surfaces/TrayPanel.tsx
index ccc2e8fe2f..09ead3c1d3 100644
--- a/apps/desktop-tauri/src/surfaces/TrayPanel.tsx
+++ b/apps/desktop-tauri/src/surfaces/TrayPanel.tsx
@@ -30,7 +30,7 @@ import {
const HAS_DASHBOARD = new Set([
"abacus", "alibaba", "alibabatokenplan", "amp", "augment",
"azureopenai", "bedrock", "claude", "codex", "codebuff",
- "aiand", "commandcode", "copilot", "crof", "crossmodel", "cursor", "deepgram", "deepinfra", "deepseek", "zenmux", "clinepass", "longcat", "neuralwatt", "zoommate",
+ "aiand", "commandcode", "copilot", "crossmodel", "cursor", "deepgram", "deepinfra", "deepseek", "zenmux", "clinepass", "longcat", "neuralwatt", "zoommate",
"doubao", "elevenlabs", "factory", "gemini", "grok", "groq",
"infini", "jetbrains", "kilo", "kimi", "kimik2", "kiro", "manus", "replicate",
"mimo", "minimax", "mistral", "nanogpt", "notion", "ollama", "openaiapi",
diff --git a/apps/desktop-tauri/src/surfaces/settings/tabs/ProvidersTab.tsx b/apps/desktop-tauri/src/surfaces/settings/tabs/ProvidersTab.tsx
index 48c200779c..ccf5295187 100644
--- a/apps/desktop-tauri/src/surfaces/settings/tabs/ProvidersTab.tsx
+++ b/apps/desktop-tauri/src/surfaces/settings/tabs/ProvidersTab.tsx
@@ -242,7 +242,6 @@ function providerSourceHintShort(
case "clinepass":
case "neuralwatt":
case "doubao":
- case "crof":
case "stepfun":
case "venice":
case "openaiapi":
diff --git a/apps/desktop-tauri/src/test/providerCatalog.ts b/apps/desktop-tauri/src/test/providerCatalog.ts
index c8ab2daa5d..dd27d5a469 100644
--- a/apps/desktop-tauri/src/test/providerCatalog.ts
+++ b/apps/desktop-tauri/src/test/providerCatalog.ts
@@ -48,7 +48,6 @@ export const TEST_PROVIDER_CATALOG: Array<[string, string]> = [
["mimo", "Xiaomi MiMo"],
["doubao", "Doubao"],
["commandcode", "Command Code"],
- ["crof", "Crof"],
["stepfun", "StepFun"],
["venice", "Venice"],
["openaiapi", "OpenAI API"],
diff --git a/rust/assets/icons/ProviderIcon-crof.svg b/rust/assets/icons/ProviderIcon-crof.svg
deleted file mode 100644
index fdde018b8f..0000000000
--- a/rust/assets/icons/ProviderIcon-crof.svg
+++ /dev/null
@@ -1,3 +0,0 @@
-
diff --git a/rust/src/cli/serve/dashboard/icons.rs b/rust/src/cli/serve/dashboard/icons.rs
index 6ac230cc63..cfdd22c6c6 100644
--- a/rust/src/cli/serve/dashboard/icons.rs
+++ b/rust/src/cli/serve/dashboard/icons.rs
@@ -121,10 +121,6 @@ static ICONS: &[(&str, &[u8])] = &[
"ProviderIcon-copilot",
include_bytes!("icons/ProviderIcon-copilot.svg"),
),
- (
- "ProviderIcon-crof",
- include_bytes!("icons/ProviderIcon-crof.svg"),
- ),
(
"ProviderIcon-cursor",
include_bytes!("icons/ProviderIcon-cursor.svg"),
diff --git a/rust/src/cli/serve/dashboard/icons/ProviderIcon-crof.svg b/rust/src/cli/serve/dashboard/icons/ProviderIcon-crof.svg
deleted file mode 100644
index fdde018b8f..0000000000
--- a/rust/src/cli/serve/dashboard/icons/ProviderIcon-crof.svg
+++ /dev/null
@@ -1,3 +0,0 @@
-
diff --git a/rust/src/core/provider.rs b/rust/src/core/provider.rs
index d6a38df551..ee639a7ea3 100755
--- a/rust/src/core/provider.rs
+++ b/rust/src/core/provider.rs
@@ -55,7 +55,6 @@ pub enum ProviderId {
MiMo,
Doubao,
CommandCode,
- Crof,
StepFun,
Venice,
OpenAIApi,
@@ -139,7 +138,6 @@ impl ProviderId {
ProviderId::MiMo,
ProviderId::Doubao,
ProviderId::CommandCode,
- ProviderId::Crof,
ProviderId::StepFun,
ProviderId::Venice,
ProviderId::OpenAIApi,
@@ -226,7 +224,6 @@ impl ProviderId {
ProviderId::MiMo => "mimo",
ProviderId::Doubao => "doubao",
ProviderId::CommandCode => "commandcode",
- ProviderId::Crof => "crof",
ProviderId::StepFun => "stepfun",
ProviderId::Venice => "venice",
ProviderId::OpenAIApi => "openaiapi",
@@ -310,7 +307,6 @@ impl ProviderId {
ProviderId::MiMo => "Xiaomi MiMo",
ProviderId::Doubao => "Doubao",
ProviderId::CommandCode => "Command Code",
- ProviderId::Crof => "Crof",
ProviderId::StepFun => "StepFun",
ProviderId::Venice => "Venice",
ProviderId::OpenAIApi => "OpenAI API",
@@ -406,7 +402,6 @@ impl ProviderId {
ProviderId::AiAnd => None,
ProviderId::Windsurf => None,
ProviderId::Doubao => None,
- ProviderId::Crof => None,
ProviderId::StepFun => None,
ProviderId::OpenAIApi => None,
ProviderId::ElevenLabs => None,
@@ -490,7 +485,6 @@ impl ProviderId {
}
"doubao" | "ark" | "volcengine" => Some(ProviderId::Doubao),
"commandcode" | "command-code" | "command code" => Some(ProviderId::CommandCode),
- "crof" => Some(ProviderId::Crof),
"stepfun" | "step-fun" | "step fun" => Some(ProviderId::StepFun),
"venice" => Some(ProviderId::Venice),
"openaiapi" | "openai-api" | "openai api" | "openai-balance" => {
@@ -1048,7 +1042,6 @@ pub fn brand_color(id: ProviderId) -> &'static str {
ProviderId::MiMo => "#FF6900",
ProviderId::Doubao => "#2563EB",
ProviderId::CommandCode => "#44FF00",
- ProviderId::Crof => "#7C3AED",
ProviderId::StepFun => "#999999",
ProviderId::Venice => "#111827",
ProviderId::OpenAIApi => "#10A37F",
@@ -1096,7 +1089,7 @@ mod tests {
#[test]
fn test_provider_id_all() {
let all = ProviderId::all();
- assert_eq!(all.len(), 77);
+ assert_eq!(all.len(), 76);
assert!(all.contains(&ProviderId::Claude));
assert!(all.contains(&ProviderId::Codex));
assert!(all.contains(&ProviderId::Pi));
@@ -1121,7 +1114,6 @@ mod tests {
assert!(all.contains(&ProviderId::MiMo));
assert!(all.contains(&ProviderId::Doubao));
assert!(all.contains(&ProviderId::CommandCode));
- assert!(all.contains(&ProviderId::Crof));
assert!(all.contains(&ProviderId::StepFun));
assert!(all.contains(&ProviderId::Venice));
assert!(all.contains(&ProviderId::OpenAIApi));
@@ -1231,6 +1223,7 @@ mod tests {
Some(ProviderId::Antigravity)
);
assert_eq!(ProviderId::from_cli_name("zed"), Some(ProviderId::Zed));
+ assert_eq!(ProviderId::from_cli_name("crof"), None);
assert_eq!(ProviderId::from_cli_name("unknown"), None);
assert_eq!(
ProviderId::from_cli_name("code-rabbit"),
diff --git a/rust/src/core/provider_factory.rs b/rust/src/core/provider_factory.rs
index aff4b6f31d..04b139d707 100644
--- a/rust/src/core/provider_factory.rs
+++ b/rust/src/core/provider_factory.rs
@@ -10,14 +10,14 @@ use crate::providers::{
AbacusProvider, AiAndProvider, AlibabaProvider, AlibabaTokenPlanProvider, AmpProvider,
AntigravityProvider, AugmentProvider, AzureOpenAIProvider, BedrockProvider, ChutesProvider,
ClaudeProvider, ClinePassProvider, CodeBuddyProvider, CodeRabbitProvider, CodebuffProvider,
- CodexProvider, CommandCodeProvider, CopilotProvider, CrofProvider, CrossModelProvider,
- CursorProvider, DeepInfraProvider, DeepSeekProvider, DeepgramProvider, DevinProvider,
- DoubaoProvider, ElevenLabsProvider, FactoryProvider, FireworksProvider, GeminiProvider,
- GrokProvider, GroqProvider, HuggingFaceProvider, InfiniProvider, JetBrainsProvider,
- KiloProvider, KimiK2Provider, KimiProvider, KiroProvider, LLMProxyProvider, LiteLLMProvider,
- LongCatProvider, ManusProvider, MetaProvider, MiMoProvider, MiniMaxProvider, MistralProvider,
- MuseProvider, NanoGPTProvider, NeuralwattProvider, NotionProvider, NousProvider,
- OllamaProvider, OpenAIApiProvider, OpenCodeGoProvider, OpenCodeProvider, OpenRouterProvider,
+ CodexProvider, CommandCodeProvider, CopilotProvider, CrossModelProvider, CursorProvider,
+ DeepInfraProvider, DeepSeekProvider, DeepgramProvider, DevinProvider, DoubaoProvider,
+ ElevenLabsProvider, FactoryProvider, FireworksProvider, GeminiProvider, GrokProvider,
+ GroqProvider, HuggingFaceProvider, InfiniProvider, JetBrainsProvider, KiloProvider,
+ KimiK2Provider, KimiProvider, KiroProvider, LLMProxyProvider, LiteLLMProvider, LongCatProvider,
+ ManusProvider, MetaProvider, MiMoProvider, MiniMaxProvider, MistralProvider, MuseProvider,
+ NanoGPTProvider, NeuralwattProvider, NotionProvider, NousProvider, OllamaProvider,
+ OpenAIApiProvider, OpenCodeGoProvider, OpenCodeProvider, OpenRouterProvider,
PerplexityProvider, PiProvider, PoeProvider, QoderProvider, QwenCloudProvider,
ReplicateProvider, SakanaProvider, StepFunProvider, Sub2ApiProvider, T3ChatProvider,
VeniceProvider, VertexAIProvider, WarpProvider, WayfinderProvider, WindsurfProvider,
@@ -73,7 +73,6 @@ pub fn instantiate(id: ProviderId) -> Box {
ProviderId::MiMo => Box::new(MiMoProvider::new()),
ProviderId::Doubao => Box::new(DoubaoProvider::new()),
ProviderId::CommandCode => Box::new(CommandCodeProvider::new()),
- ProviderId::Crof => Box::new(CrofProvider::new()),
ProviderId::StepFun => Box::new(StepFunProvider::new()),
ProviderId::Venice => Box::new(VeniceProvider::new()),
ProviderId::OpenAIApi => Box::new(OpenAIApiProvider::new()),
diff --git a/rust/src/core/token_accounts.rs b/rust/src/core/token_accounts.rs
index a8f3cc71ae..8780e978c5 100755
--- a/rust/src/core/token_accounts.rs
+++ b/rust/src/core/token_accounts.rs
@@ -354,7 +354,6 @@ impl TokenAccountSupport {
| ProviderId::DeepSeek
| ProviderId::Windsurf
| ProviderId::Doubao
- | ProviderId::Crof
| ProviderId::StepFun
| ProviderId::Venice
| ProviderId::OpenAIApi
diff --git a/rust/src/providers/crof/mod.rs b/rust/src/providers/crof/mod.rs
deleted file mode 100644
index 4abef6b4ca..0000000000
--- a/rust/src/providers/crof/mod.rs
+++ /dev/null
@@ -1,249 +0,0 @@
-//! Crof provider implementation.
-//!
-//! Fetches API key based credit/request quota data from Crof.
-
-use async_trait::async_trait;
-use reqwest::Client;
-use serde::Deserialize;
-
-use crate::core::{
- FetchContext, Provider, ProviderError, ProviderFetchResult, ProviderId, ProviderMetadata,
- RateWindow, SourceMode, UsageSnapshot,
-};
-
-const CROF_USAGE_URL: &str = "https://crof.ai/usage_api/";
-const CROF_CREDENTIAL_TARGET: &str = "codexbar-crof";
-const BROWSER_USER_AGENT: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36";
-
-#[derive(Debug, Deserialize)]
-struct CrofUsageResponse {
- credits: f64,
- #[serde(default, rename = "requests_plan")]
- requests_plan: Option,
- #[serde(default, rename = "usable_requests")]
- usable_requests: Option,
-}
-
-pub struct CrofProvider {
- metadata: ProviderMetadata,
- client: Client,
-}
-
-impl CrofProvider {
- pub fn new() -> Self {
- Self {
- metadata: ProviderMetadata {
- id: ProviderId::Crof,
- display_name: "Crof",
- session_label: "Balance",
- weekly_label: "Requests",
- supports_opus: false,
- supports_credits: true,
- default_enabled: false,
- is_primary: false,
- dashboard_url: Some("https://crof.ai"),
- status_page_url: None,
- tertiary_label_key: None,
- },
- client: crate::core::credentialed_http_client_builder()
- .timeout(std::time::Duration::from_secs(15))
- .build()
- .unwrap_or_else(|_| Client::new()),
- }
- }
-
- fn api_key(api_key: Option<&str>) -> Result {
- super_key(
- api_key,
- CROF_CREDENTIAL_TARGET,
- &["CROF_API_KEY", "CROFAI_API_KEY"],
- )
- }
-
- async fn fetch_api(&self, api_key: &str) -> Result {
- let response = self
- .client
- .get(CROF_USAGE_URL)
- .bearer_auth(api_key)
- .header("Accept", "application/json")
- .header("User-Agent", BROWSER_USER_AGENT)
- .send()
- .await?;
-
- let status = response.status();
- let body = response.text().await.unwrap_or_default();
- if status == reqwest::StatusCode::UNAUTHORIZED {
- return Err(ProviderError::AuthRequired);
- }
- if status == reqwest::StatusCode::FORBIDDEN {
- if body.contains("cloudflare") || body.contains("Error 1010") {
- return Err(ProviderError::Other(
- "Crof usage API blocked by Cloudflare (1010). Retry from the desktop app."
- .into(),
- ));
- }
- return Err(ProviderError::AuthRequired);
- }
- if !status.is_success() {
- return Err(ProviderError::Other(format!(
- "Crof API returned status {status}"
- )));
- }
-
- let usage: CrofUsageResponse = serde_json::from_str(&body)
- .map_err(|e| ProviderError::Parse(format!("Failed to parse Crof usage: {e}")))?;
- Ok(snapshot_from_usage(&usage))
- }
-}
-
-fn snapshot_from_usage(usage: &CrofUsageResponse) -> UsageSnapshot {
- let credits = usage.credits.max(0.0);
- let display = if credits <= 0.0 {
- "$0.00".to_string()
- } else if credits >= 0.01 {
- format!("${:.2}", (credits * 100.0).floor() / 100.0)
- } else {
- format!("${credits:.4}")
- };
- let mut primary = RateWindow::new(if credits > 0.0 { 0.0 } else { 100.0 });
- primary.reset_description = Some(display.clone());
-
- let mut snapshot = UsageSnapshot::new(primary).with_login_method(format!("{display} balance"));
-
- if let (Some(plan), Some(usable)) = (usage.requests_plan, usage.usable_requests) {
- let remaining = usable.max(0.0).min(plan.max(0.0));
- let remaining_percent = if plan > 0.0 {
- ((remaining / plan) * 100.0).clamp(0.0, 100.0)
- } else {
- 0.0
- };
- let mut requests = RateWindow::new(100.0 - remaining_percent);
- requests.reset_description = Some(format!("{remaining:.0} requests left"));
- snapshot = snapshot.with_secondary(requests);
- }
-
- snapshot
-}
-
-impl Default for CrofProvider {
- fn default() -> Self {
- Self::new()
- }
-}
-
-#[async_trait]
-impl Provider for CrofProvider {
- fn id(&self) -> ProviderId {
- ProviderId::Crof
- }
-
- fn metadata(&self) -> &ProviderMetadata {
- &self.metadata
- }
-
- async fn fetch_usage(&self, ctx: &FetchContext) -> Result {
- match ctx.source_mode {
- SourceMode::Auto | SourceMode::OAuth => {
- let api_key = Self::api_key(ctx.api_key.as_deref())?;
- Ok(ProviderFetchResult::new(
- self.fetch_api(&api_key).await?,
- "api",
- ))
- }
- SourceMode::Web | SourceMode::Cli => {
- Err(ProviderError::UnsupportedSource(ctx.source_mode))
- }
- }
- }
-
- fn available_sources(&self) -> Vec {
- vec![SourceMode::Auto, SourceMode::OAuth]
- }
-}
-
-fn super_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::*;
-
- #[test]
- fn crof_snapshot_formats_request_and_credit_windows() {
- let snapshot = snapshot_from_usage(&CrofUsageResponse {
- credits: 12.5,
- requests_plan: Some(100.0),
- usable_requests: Some(25.0),
- });
- assert_eq!(snapshot.primary.used_percent, 0.0);
- assert_eq!(
- snapshot.primary.reset_description.as_deref(),
- Some("$12.50")
- );
- assert_eq!(snapshot.secondary.unwrap().used_percent, 75.0);
- }
-
- #[test]
- fn crof_payg_balance_only_does_not_require_request_quota() {
- let snapshot = snapshot_from_usage(&CrofUsageResponse {
- credits: 3.019,
- requests_plan: None,
- usable_requests: None,
- });
- assert_eq!(snapshot.primary.used_percent, 0.0);
- assert_eq!(snapshot.primary.reset_description.as_deref(), Some("$3.01"));
- assert!(snapshot.secondary.is_none());
- assert_eq!(snapshot.login_method.as_deref(), Some("$3.01 balance"));
- }
-
- #[test]
- fn crof_sub_cent_balance_is_not_exhausted() {
- let snapshot = snapshot_from_usage(&CrofUsageResponse {
- credits: 0.0073,
- requests_plan: None,
- usable_requests: None,
- });
- assert_eq!(snapshot.primary.used_percent, 0.0);
- assert_eq!(
- snapshot.primary.reset_description.as_deref(),
- Some("$0.0073")
- );
- }
-
- #[test]
- fn crof_zero_balance_is_exhausted() {
- let snapshot = snapshot_from_usage(&CrofUsageResponse {
- credits: 0.0,
- requests_plan: None,
- usable_requests: None,
- });
- assert_eq!(snapshot.primary.used_percent, 100.0);
- assert_eq!(snapshot.primary.reset_description.as_deref(), Some("$0.00"));
- }
-}
diff --git a/rust/src/providers/mod.rs b/rust/src/providers/mod.rs
index ad697ad321..6bd6432888 100755
--- a/rust/src/providers/mod.rs
+++ b/rust/src/providers/mod.rs
@@ -24,7 +24,6 @@ pub mod coderabbit;
pub mod codex;
pub mod commandcode;
pub mod copilot;
-pub mod crof;
pub mod crossmodel;
pub mod cursor;
pub mod deepgram;
@@ -104,7 +103,6 @@ pub use coderabbit::CodeRabbitProvider;
pub use codex::CodexProvider;
pub use commandcode::CommandCodeProvider;
pub use copilot::CopilotProvider;
-pub use crof::CrofProvider;
pub use crossmodel::CrossModelProvider;
pub use cursor::CursorProvider;
pub use deepgram::DeepgramProvider;
diff --git a/rust/src/settings/api_keys.rs b/rust/src/settings/api_keys.rs
index 3a7b166386..515a20ad09 100644
--- a/rust/src/settings/api_keys.rs
+++ b/rust/src/settings/api_keys.rs
@@ -428,15 +428,6 @@ pub fn get_api_key_providers() -> Vec {
config_file_path: None,
dashboard_url: Some("https://console.volcengine.com/ark/region:ark+cn-beijing/usage"),
},
- ProviderConfigInfo {
- id: ProviderId::Crof,
- name: "Crof",
- requires_api_key: true,
- api_key_env_var: Some("CROF_API_KEY"),
- api_key_help: Some("Get your API key from Crof."),
- config_file_path: None,
- dashboard_url: Some("https://crof.ai"),
- },
ProviderConfigInfo {
id: ProviderId::StepFun,
name: "StepFun",
diff --git a/rust/src/settings/raw.rs b/rust/src/settings/raw.rs
index 850c90e960..abe68d2cfe 100644
--- a/rust/src/settings/raw.rs
+++ b/rust/src/settings/raw.rs
@@ -1,4 +1,86 @@
use super::*;
+use serde::Deserializer;
+use serde::de::{IgnoredAny, MapAccess, Visitor};
+use std::fmt;
+
+fn canonical_provider_id(raw: &str) -> Option {
+ ProviderId::from_cli_name(raw).map(|provider| provider.cli_name().to_string())
+}
+
+fn canonicalize_provider_id_list(ids: impl IntoIterator- ) -> Vec {
+ let mut seen = HashSet::new();
+ ids.into_iter()
+ .filter_map(|raw| canonical_provider_id(&raw))
+ .filter(|canonical| seen.insert(canonical.clone()))
+ .collect()
+}
+
+fn canonicalize_provider_metrics(
+ metrics: HashMap,
+) -> HashMap {
+ let mut entries = metrics
+ .into_iter()
+ .filter_map(|(raw, preference)| {
+ let canonical = canonical_provider_id(&raw)?;
+ let canonical_spelling = raw.eq_ignore_ascii_case(&canonical);
+ Some((canonical, canonical_spelling, raw, preference))
+ })
+ .collect::>();
+
+ // HashMap iteration order is unstable. Sort before resolving aliases so a
+ // canonical spelling always wins and alias-only collisions are repeatable.
+ entries.sort_by(|left, right| {
+ left.0
+ .cmp(&right.0)
+ .then_with(|| left.1.cmp(&right.1))
+ .then_with(|| {
+ left.2
+ .to_ascii_lowercase()
+ .cmp(&right.2.to_ascii_lowercase())
+ })
+ .then_with(|| left.2.cmp(&right.2))
+ });
+
+ let mut canonical = HashMap::with_capacity(entries.len());
+ for (provider_id, _, _, preference) in entries {
+ canonical.insert(provider_id, preference);
+ }
+ canonical
+}
+
+fn deserialize_provider_configs<'de, D>(
+ deserializer: D,
+) -> Result, D::Error>
+where
+ D: Deserializer<'de>,
+{
+ struct ProviderConfigsVisitor;
+
+ impl<'de> Visitor<'de> for ProviderConfigsVisitor {
+ type Value = HashMap;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str("a map of provider IDs to provider settings")
+ }
+
+ fn visit_map(self, mut map: M) -> Result
+ where
+ M: MapAccess<'de>,
+ {
+ let mut configs = HashMap::with_capacity(map.size_hint().unwrap_or(0));
+ while let Some(key) = map.next_key::()? {
+ if let Some(provider_id) = ProviderId::from_cli_name(&key) {
+ configs.insert(provider_id, map.next_value()?);
+ } else {
+ map.next_value::()?;
+ }
+ }
+ Ok(configs)
+ }
+ }
+
+ deserializer.deserialize_map(ProviderConfigsVisitor)
+}
/// Raw on-disk shape of [`Settings`] used purely for deserialization.
///
@@ -52,6 +134,7 @@ pub(super) struct RawSettings {
show_all_token_accounts_in_menu: bool,
// ── New unified per-provider map ─────────────────────────────────
+ #[serde(default, deserialize_with = "deserialize_provider_configs")]
provider_configs: HashMap,
// ── Legacy flat per-provider fields (migrated on load) ───────────
@@ -515,7 +598,11 @@ impl From for Settings {
};
Settings {
- enabled_providers: raw.enabled_providers,
+ enabled_providers: raw
+ .enabled_providers
+ .into_iter()
+ .filter_map(|provider_id| canonical_provider_id(&provider_id))
+ .collect(),
refresh_interval_secs: raw.refresh_interval_secs,
adaptive_refresh: raw.adaptive_refresh,
refresh_all_providers_on_menu_open: raw.refresh_all_providers_on_menu_open,
@@ -549,7 +636,7 @@ impl From for Settings {
disable_keychain_access: raw.disable_keychain_access,
hide_personal_info: raw.hide_personal_info,
update_channel: raw.update_channel,
- provider_metrics: raw.provider_metrics,
+ provider_metrics: canonicalize_provider_metrics(raw.provider_metrics),
provider_order: if raw.provider_order.is_empty() {
Vec::new()
} else {
@@ -578,7 +665,7 @@ impl From for Settings {
float_bar_orientation: normalize_float_bar_orientation(&raw.float_bar_orientation),
float_bar_style: normalize_float_bar_style(&raw.float_bar_style),
float_bar_click_through: raw.float_bar_click_through,
- float_bar_provider_ids: raw.float_bar_provider_ids,
+ float_bar_provider_ids: canonicalize_provider_id_list(raw.float_bar_provider_ids),
float_bar_dark_text: raw.float_bar_dark_text,
float_bar_show_reset_inline: raw.float_bar_show_reset_inline,
float_bar_show_cost: raw.float_bar_show_cost,
diff --git a/rust/src/settings/tests.rs b/rust/src/settings/tests.rs
index a6db4aa05b..d214d6b008 100644
--- a/rust/src/settings/tests.rs
+++ b/rust/src/settings/tests.rs
@@ -1045,6 +1045,78 @@ fn test_new_format_provider_configs_only() {
assert_eq!(settings.api_region(ProviderId::Zai), "global");
}
+#[test]
+fn retired_provider_config_is_ignored_until_explicit_save() {
+ let original = r#"{
+ "enabled_providers": ["codex", "crof"],
+ "refresh_interval_secs": 300,
+ "provider_metrics": { "codex": "weekly", "crof": "session" },
+ "float_bar_provider_ids": ["codex", "crof"],
+ "provider_configs": {
+ "crof": { "api_token": "retired-fixture-key" },
+ "codex": { "cookie_source": "manual", "openai_web_extras": false },
+ "alibaba": { "api_region": "cn", "manual_cookie_header": "ali=PLACEHOLDER" }
+ }
+ }"#;
+ let original_bytes = original.as_bytes().to_vec();
+
+ let settings: Settings =
+ serde_json::from_str(original).expect("load settings with retired key");
+
+ assert_eq!(original.as_bytes(), original_bytes);
+ assert_eq!(settings.cookie_source(ProviderId::Codex), "manual");
+ assert!(!settings.openai_web_extras(ProviderId::Codex));
+ assert_eq!(
+ settings.enabled_providers,
+ HashSet::from(["codex".to_string()])
+ );
+ assert_eq!(settings.provider_metrics.len(), 1);
+ assert_eq!(settings.float_bar_provider_ids, ["codex"]);
+ assert_eq!(settings.api_region(ProviderId::Alibaba), "cn");
+ assert_eq!(
+ settings.manual_cookie_header(ProviderId::Alibaba),
+ "ali=PLACEHOLDER"
+ );
+
+ let saved = serde_json::to_string(&settings).expect("serialize sanitized settings");
+ let saved_value: serde_json::Value = serde_json::from_str(&saved).unwrap();
+ let saved_configs = saved_value["provider_configs"].as_object().unwrap();
+ assert!(!saved_configs.contains_key("crof"));
+ assert!(saved_configs.contains_key("codex"));
+ assert!(saved_configs.contains_key("alibaba"));
+ assert!(
+ !saved.contains("\"crof\""),
+ "saved settings retained Crof: {saved}"
+ );
+}
+
+#[test]
+fn provider_aliases_are_canonicalized_at_the_load_boundary() {
+ let settings: Settings = serde_json::from_str(
+ r#"{
+ "enabled_providers": ["openai", "ClAuDe", "not-a-provider"],
+ "provider_metrics": {
+ "openai": "weekly",
+ "CoDeX": "session",
+ "not-a-provider": "weekly"
+ },
+ "float_bar_provider_ids": ["OPENAI", "codex", "ClAuDe", "unknown"]
+ }"#,
+ )
+ .expect("load settings containing provider aliases");
+
+ assert_eq!(
+ settings.enabled_providers,
+ HashSet::from(["claude".to_string(), "codex".to_string()])
+ );
+ assert_eq!(
+ settings.provider_metrics.get("codex"),
+ Some(&MetricPreference::Session)
+ );
+ assert_eq!(settings.provider_metrics.len(), 1);
+ assert_eq!(settings.float_bar_provider_ids, ["codex", "claude"]);
+}
+
/// Default `Settings` should serialize WITHOUT a `provider_configs`
/// field (empty map skipped).
#[test]