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 <sunsky.lau@gmail.com>
This commit is contained in:
Brooklyn Nicholson 2026-07-24 21:12:53 -05:00
parent bd2dcfe9ca
commit 8a2925f794
2 changed files with 37 additions and 7 deletions

View file

@ -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

View file

@ -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,