From 8a2925f794466a5d4487a1438b1b6c700b88a1d6 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Fri, 24 Jul 2026 21:12:53 -0500 Subject: [PATCH] fix(cli): store custom endpoint API key in .env instead of config.yaml hermes model's custom-endpoint flow is the other write path that produced a plaintext key, on both the model block and the custom_providers entry. Route it through the same .env indirection as the Desktop panel, and swap an existing entry's inline key for the reference when the URL is re-saved. Co-authored-by: liuhao1024 --- hermes_cli/main.py | 14 ++++++++++++-- hermes_cli/model_setup_flows.py | 30 +++++++++++++++++++++++++----- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 32292368fe05..1e2aa7b921b0 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -3975,13 +3975,17 @@ def _custom_provider_base_url_config_value(provider_info, resolved_base_url=""): def _save_custom_provider( - base_url, api_key="", model="", context_length=None, name=None, api_mode=None + base_url, api_key="", model="", context_length=None, name=None, api_mode=None, + key_env="" ): """Save a custom endpoint to custom_providers in config.yaml. Deduplicates by base_url — if the URL already exists, updates the model name, context_length, and api_mode but doesn't add a duplicate entry. Uses *name* when provided, otherwise auto-generates from the URL. + + When *key_env* is set the caller has already written the key to ``.env``, + so the entry references it instead of inlining the secret (#69449). """ from hermes_cli.config import load_config, save_config @@ -4013,6 +4017,10 @@ def _save_custom_provider( elif "api_mode" in entry: entry.pop("api_mode", None) changed = True + if key_env and (entry.get("key_env") != key_env or entry.get("api_key")): + entry["key_env"] = key_env + entry.pop("api_key", None) + changed = True if changed: cfg["custom_providers"] = providers save_config(cfg) @@ -4023,7 +4031,9 @@ def _save_custom_provider( name = _auto_provider_name(base_url) entry = {"name": name, "base_url": base_url} - if api_key: + if key_env: + entry["key_env"] = key_env + elif api_key: entry["api_key"] = api_key if model: entry["model"] = model diff --git a/hermes_cli/model_setup_flows.py b/hermes_cli/model_setup_flows.py index 394360c16073..90f628578a78 100644 --- a/hermes_cli/model_setup_flows.py +++ b/hermes_cli/model_setup_flows.py @@ -23,6 +23,7 @@ from __future__ import annotations import argparse import os import subprocess +import urllib.parse from hermes_cli.config import clear_model_endpoint_credentials @@ -833,7 +834,13 @@ def _model_flow_custom(config): """ from hermes_cli.main import _auto_provider_name, _prompt_custom_api_mode_selection, _save_custom_provider from hermes_cli.auth import _save_model_choice, deactivate_provider - from hermes_cli.config import get_env_value, load_config, save_config + from hermes_cli.config import ( + custom_endpoint_key_env, + get_env_value, + load_config, + save_config, + save_env_value, + ) from hermes_cli.secret_prompt import masked_secret_prompt current_url = get_env_value("OPENAI_BASE_URL") or "" @@ -988,6 +995,18 @@ def _model_flow_custom(config): print(f"Invalid context length: {context_length_str} — will auto-detect.") context_length = None + # The key goes to .env and config.yaml only references it (#69449). Keyed + # on host:port so two servers on one machine keep separate credentials. + custom_key_env = "" + if effective_key: + _parsed = urllib.parse.urlparse(effective_url) + _identity = _parsed.hostname or "" + if _parsed.port: + _identity = f"{_identity}_{_parsed.port}" + custom_key_env = custom_endpoint_key_env(_identity) + save_env_value(custom_key_env, effective_key) + print(f" API key saved to .env as {custom_key_env}") + if model_name: _save_model_choice(model_name) @@ -999,8 +1018,8 @@ def _model_flow_custom(config): cfg["model"] = model model["provider"] = "custom" model["base_url"] = effective_url - if effective_key: - model["api_key"] = effective_key + if custom_key_env: + model["api_key"] = f"${{{custom_key_env}}}" if api_mode: model["api_mode"] = api_mode else: @@ -1025,8 +1044,8 @@ def _model_flow_custom(config): _caller_model = {"default": _caller_model} if _caller_model else {} _caller_model["provider"] = "custom" _caller_model["base_url"] = effective_url - if effective_key: - _caller_model["api_key"] = effective_key + if custom_key_env: + _caller_model["api_key"] = f"${{{custom_key_env}}}" if api_mode: _caller_model["api_mode"] = api_mode else: @@ -1042,6 +1061,7 @@ def _model_flow_custom(config): context_length=context_length, name=display_name, api_mode=api_mode, + key_env=custom_key_env, ) _prune_replaced_custom_model_config_credentials( effective_url,