Skip to content
Closed
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
6 changes: 5 additions & 1 deletion gateway/slash_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
from gateway.config import HomeChannel, Platform, PlatformConfig
from gateway.platforms.base import EphemeralReply, MessageEvent, MessageType
from gateway.session import SessionSource, build_session_key
from hermes_cli.config import cfg_get
from hermes_cli.config import cfg_get, clear_model_endpoint_credentials
from utils import (
atomic_json_write,
atomic_yaml_write,
Expand Down Expand Up @@ -1239,6 +1239,8 @@ async def _on_model_selected(
_persist_model_cfg["provider"] = result.target_provider
if result.base_url:
_persist_model_cfg["base_url"] = result.base_url
if str(result.target_provider or "").strip().lower() != "custom":
clear_model_endpoint_credentials(_persist_model_cfg)
from hermes_cli.config import save_config
save_config(_persist_cfg)
except Exception as e:
Expand Down Expand Up @@ -1429,6 +1431,8 @@ async def _finish_switch() -> str:
model_cfg["provider"] = result.target_provider
if result.base_url:
model_cfg["base_url"] = result.base_url
if str(result.target_provider or "").strip().lower() != "custom":
clear_model_endpoint_credentials(model_cfg)
from hermes_cli.config import save_config
save_config(cfg)
except Exception as e:
Expand Down
16 changes: 6 additions & 10 deletions hermes_cli/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -6386,16 +6386,12 @@ def _update_config_for_provider(
# Clear stale base_url to prevent contamination when switching providers
model_cfg.pop("base_url", None)

# Clear stale api_key/api_mode left over from a previous custom provider.
# When the user switches from e.g. a MiniMax custom endpoint
# (api_mode=anthropic_messages, api_key=mxp-...) to a built-in provider
# (e.g. OpenRouter), the stale api_key/api_mode would override the new
# provider's credentials and transport choice. Built-in providers that
# need a specific api_mode (copilot, xai) set it at request-resolution
# time via `_copilot_runtime_api_mode` / `_detect_api_mode_for_url`, so
# removing the persisted value here is safe.
model_cfg.pop("api_key", None)
model_cfg.pop("api_mode", None)
# Clear stale endpoint credentials left over from a previous custom provider.
# Built-in providers resolve credentials from env/auth state, not inline
# model.api_key.
from hermes_cli.config import clear_model_endpoint_credentials

clear_model_endpoint_credentials(model_cfg)

# When switching to a non-OpenRouter provider, ensure model.default is
# valid for the new provider. An OpenRouter-formatted name like
Expand Down
24 changes: 24 additions & 0 deletions hermes_cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3915,6 +3915,30 @@ def _set_nested(config, dotted_key: str, value):
current[last] = value


def clear_model_endpoint_credentials(
model_cfg: Dict[str, Any],
*,
clear_api_key: bool = True,
clear_api_mode: bool = True,
) -> Dict[str, Any]:
"""Remove stale inline endpoint credentials from a model config.

``model.api_key`` is valid only for explicit custom endpoint assignments.
Built-in providers resolve credentials from env vars, auth.json, or the
credential pool. When switching away from a custom endpoint, leaving these
fields behind keeps secrets in config.yaml and can contaminate later custom
resolution paths.
"""
if not isinstance(model_cfg, dict):
return model_cfg
if clear_api_key:
model_cfg.pop("api_key", None)
model_cfg.pop("api", None)
if clear_api_mode:
model_cfg.pop("api_mode", None)
return model_cfg


def get_missing_config_fields() -> List[Dict[str, Any]]:
"""
Check which config fields are missing or outdated (recursive).
Expand Down
16 changes: 16 additions & 0 deletions hermes_cli/model_setup_flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
import os
import subprocess

from hermes_cli.config import clear_model_endpoint_credentials


def _prompt_auth_credentials_choice(title: str) -> str:
"""Prompt for reuse / reauthenticate / cancel with the standard radio UI.
Expand Down Expand Up @@ -123,6 +125,7 @@ def _model_flow_openrouter(config, current_model=""):
model["provider"] = "openrouter"
model["base_url"] = OPENROUTER_BASE_URL
model["api_mode"] = "chat_completions"
clear_model_endpoint_credentials(model, clear_api_mode=False)
save_config(cfg)
deactivate_provider()
print(f"Default model set to: {selected} (via OpenRouter)")
Expand Down Expand Up @@ -325,6 +328,9 @@ def _model_flow_nous(config, current_model="", args=None):
# Reactivate Nous as the provider and update config
inference_url = creds.get("base_url", "")
_update_config_for_provider("nous", inference_url)
# Reload after the auth helper writes provider state. The incoming
# config object may still contain stale custom-provider fields.
config = load_config()
current_model_cfg = config.get("model")
if isinstance(current_model_cfg, dict):
model_cfg = dict(current_model_cfg)
Expand All @@ -338,6 +344,7 @@ def _model_flow_nous(config, current_model="", args=None):
model_cfg["base_url"] = inference_url.rstrip("/")
else:
model_cfg.pop("base_url", None)
clear_model_endpoint_credentials(model_cfg)
config["model"] = model_cfg
# Clear any custom endpoint that might conflict
if get_env_value("OPENAI_BASE_URL"):
Expand Down Expand Up @@ -1246,6 +1253,7 @@ def _model_flow_azure_foundry(config, current_model=""):
model["api_mode"] = api_mode
model["default"] = effective_model
model["auth_mode"] = auth_mode_label
clear_model_endpoint_credentials(model, clear_api_mode=False)
if use_entra:
# Persist only the non-default Entra scope so config.yaml stays tidy.
# Azure identity selection stays in standard AZURE_* env vars.
Expand Down Expand Up @@ -1667,6 +1675,7 @@ def _model_flow_copilot(config, current_model=""):
catalog=catalog,
api_key=api_key,
)
clear_model_endpoint_credentials(model, clear_api_mode=False)
if selected_effort is not None:
_set_reasoning_effort(cfg, selected_effort)
save_config(cfg)
Expand Down Expand Up @@ -1792,6 +1801,7 @@ def _model_flow_copilot_acp(config, current_model=""):
model["provider"] = provider_id
model["base_url"] = effective_base
model["api_mode"] = "chat_completions"
clear_model_endpoint_credentials(model, clear_api_mode=False)
save_config(cfg)
deactivate_provider()

Expand Down Expand Up @@ -1881,6 +1891,7 @@ def _model_flow_kimi(config, current_model=""):
model["provider"] = provider_id
model["base_url"] = effective_base
model.pop("api_mode", None) # let runtime auto-detect from URL
clear_model_endpoint_credentials(model, clear_api_mode=False)
save_config(cfg)
deactivate_provider()

Expand Down Expand Up @@ -1994,6 +2005,7 @@ def _model_flow_stepfun(config, current_model=""):
model["provider"] = provider_id
model["base_url"] = effective_base
model.pop("api_mode", None)
clear_model_endpoint_credentials(model, clear_api_mode=False)
save_config(cfg)
deactivate_provider()

Expand Down Expand Up @@ -2077,6 +2089,7 @@ def _model_flow_bedrock_api_key(config, region, current_model=""):
model["provider"] = "custom"
model["base_url"] = mantle_base_url
model.pop("api_mode", None) # chat_completions is the default
clear_model_endpoint_credentials(model, clear_api_mode=False)

# Also save region in bedrock config for reference
bedrock_cfg = cfg.get("bedrock", {})
Expand Down Expand Up @@ -2270,6 +2283,7 @@ def _sort_key(m):
model["provider"] = "bedrock"
model["base_url"] = f"https://bedrock-runtime.{region}.amazonaws.com"
model.pop("api_mode", None) # bedrock_converse is auto-detected
clear_model_endpoint_credentials(model, clear_api_mode=False)

bedrock_cfg = cfg.get("bedrock", {})
if not isinstance(bedrock_cfg, dict):
Expand Down Expand Up @@ -2563,6 +2577,7 @@ def _model_flow_api_key_provider(config, provider_id, current_model=""):
cfg["model"] = model
model["provider"] = provider_id
model["base_url"] = effective_base
clear_model_endpoint_credentials(model, clear_api_mode=False)
if provider_id in {"opencode-zen", "opencode-go"}:
model["api_mode"] = opencode_model_api_mode(provider_id, selected)
else:
Expand Down Expand Up @@ -2717,6 +2732,7 @@ def _model_flow_anthropic(config, current_model=""):
cfg["model"] = model
model["provider"] = "anthropic"
model.pop("base_url", None)
clear_model_endpoint_credentials(model)
save_config(cfg)
deactivate_provider()

Expand Down
13 changes: 12 additions & 1 deletion hermes_cli/web_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
cfg_get,
DEFAULT_CONFIG,
OPTIONAL_ENV_VARS,
clear_model_endpoint_credentials,
get_config_path,
get_env_path,
get_hermes_home,
Expand Down Expand Up @@ -901,8 +902,11 @@ def _apply_main_model_assignment(
# same-provider re-pick so re-selecting a model doesn't wipe the key.
if api_key.strip():
model_cfg["api_key"] = api_key.strip()
model_cfg.pop("api", None)
elif model_cfg.get("api_key") and new_provider != prev_provider:
model_cfg["api_key"] = ""
clear_model_endpoint_credentials(model_cfg, clear_api_mode=False)
if new_provider != prev_provider:
clear_model_endpoint_credentials(model_cfg, clear_api_key=False)
model_cfg.pop("context_length", None)
return model_cfg

Expand Down Expand Up @@ -3871,6 +3875,8 @@ def _apply_model_assignment_sync(
slot_cfg = {}
slot_cfg["provider"] = "auto"
slot_cfg["model"] = ""
slot_cfg.pop("base_url", None)
clear_model_endpoint_credentials(slot_cfg)
aux[slot] = slot_cfg
cfg["auxiliary"] = aux
save_config(cfg)
Expand All @@ -3886,8 +3892,13 @@ def _apply_model_assignment_sync(
slot_cfg = aux.get(slot)
if not isinstance(slot_cfg, dict):
slot_cfg = {}
prev_provider = str(slot_cfg.get("provider") or "").strip().lower()
new_provider = provider.strip().lower()
slot_cfg["provider"] = provider
slot_cfg["model"] = model
if new_provider != prev_provider and new_provider != "custom":
slot_cfg.pop("base_url", None)
clear_model_endpoint_credentials(slot_cfg)
aux[slot] = slot_cfg

cfg["auxiliary"] = aux
Expand Down
Loading
Loading