diff --git a/src-tauri/src/hermes_config.rs b/src-tauri/src/hermes_config.rs index a851167d5..9565c1988 100644 --- a/src-tauri/src/hermes_config.rs +++ b/src-tauri/src/hermes_config.rs @@ -635,6 +635,23 @@ pub fn get_providers() -> Result, App Ok(map) } +/// Reject writes that would target a dict-only overlay entry. +/// +/// `verb` is inlined into the user-facing error so both "edit" and "remove" +/// callers can share one implementation. +fn ensure_provider_writable( + config: &serde_yaml::Value, + name: &str, + verb: &str, +) -> Result<(), AppError> { + if is_dict_only_provider(config, name) { + return Err(AppError::Config(format!( + "Provider '{name}' is managed by Hermes' 'providers:' dict — {verb} via Hermes Web UI" + ))); + } + Ok(()) +} + /// True when `name` appears in `providers:` dict but not in `custom_providers:` /// list — i.e. it is a read-only overlay CC Switch must not touch. fn is_dict_only_provider(config: &serde_yaml::Value, name: &str) -> bool { @@ -691,11 +708,7 @@ pub fn set_provider( let _guard = hermes_write_lock().lock()?; let config = read_hermes_config()?; - if is_dict_only_provider(&config, name) { - return Err(AppError::Config(format!( - "Provider '{name}' is managed by Hermes' 'providers:' dict — edit via Hermes Web UI" - ))); - } + ensure_provider_writable(&config, name, "edit")?; let mut providers: Vec = config .get("custom_providers") .and_then(|v| v.as_sequence()) @@ -768,11 +781,7 @@ pub fn remove_provider(name: &str) -> Result { let _guard = hermes_write_lock().lock()?; let config = read_hermes_config()?; - if is_dict_only_provider(&config, name) { - return Err(AppError::Config(format!( - "Provider '{name}' is managed by Hermes' 'providers:' dict — remove via Hermes Web UI" - ))); - } + ensure_provider_writable(&config, name, "remove")?; let mut providers: Vec = config .get("custom_providers") diff --git a/src/components/providers/ProviderCard.tsx b/src/components/providers/ProviderCard.tsx index a54d3da15..049672715 100644 --- a/src/components/providers/ProviderCard.tsx +++ b/src/components/providers/ProviderCard.tsx @@ -15,6 +15,7 @@ import SubscriptionQuotaFooter from "@/components/SubscriptionQuotaFooter"; import CopilotQuotaFooter from "@/components/CopilotQuotaFooter"; import CodexOauthQuotaFooter from "@/components/CodexOauthQuotaFooter"; import { PROVIDER_TYPES } from "@/config/constants"; +import { isHermesReadOnlyProvider } from "@/config/hermesProviderPresets"; import { ProviderHealthBadge } from "@/components/providers/ProviderHealthBadge"; import { FailoverPriorityBadge } from "@/components/providers/FailoverPriorityBadge"; import { extractCodexBaseUrl } from "@/utils/providerConfigUtils"; @@ -183,9 +184,7 @@ export function ProviderCard({ // Hermes v12+ overlay entries live under the `providers:` dict and are // read-only here — writes have to go through Hermes Web UI. const isHermesReadOnly = - appId === "hermes" && - (provider.settingsConfig as Record)?._cc_source === - "providers_dict"; + appId === "hermes" && isHermesReadOnlyProvider(provider.settingsConfig); const isCodexOauth = provider.meta?.providerType === PROVIDER_TYPES.CODEX_OAUTH; diff --git a/src/config/hermesProviderPresets.ts b/src/config/hermesProviderPresets.ts index 59d048d9d..bcc41e523 100644 --- a/src/config/hermesProviderPresets.ts +++ b/src/config/hermesProviderPresets.ts @@ -5,6 +5,29 @@ import type { ProviderCategory } from "../types"; import type { PresetTheme, TemplateValueConfig } from "./claudeProviderPresets"; +/** + * Marker field and source values that `hermes_config.rs::get_providers` + * injects onto each settings payload. Kept in sync with the Rust constants + * `PROVIDER_SOURCE_FIELD` / `PROVIDER_SOURCE_CUSTOM_LIST` / `PROVIDER_SOURCE_DICT`. + */ +export const HERMES_PROVIDER_SOURCE_FIELD = "_cc_source"; +export const HERMES_PROVIDER_SOURCE_CUSTOM_LIST = "custom_providers"; +export const HERMES_PROVIDER_SOURCE_DICT = "providers_dict"; + +/** + * True when the provider was sourced from Hermes' v12+ `providers:` dict — + * CC Switch renders those read-only and routes edits to Hermes Web UI. + */ +export function isHermesReadOnlyProvider(settingsConfig: unknown): boolean { + if (!settingsConfig || typeof settingsConfig !== "object") { + return false; + } + const marker = (settingsConfig as Record)[ + HERMES_PROVIDER_SOURCE_FIELD + ]; + return marker === HERMES_PROVIDER_SOURCE_DICT; +} + /** * A model entry under a Hermes custom_provider. *