fix: propagate key_env from custom_providers into ProviderDef

resolve_custom_provider() previously returned api_key_env_vars=()
for every custom provider entry, silently dropping the configured
key_env field. This caused 401 errors for any custom provider that
required an API key via environment variable (e.g. Xiaomi MiMo Token
Plan, self-hosted OpenAI-compatible servers).

The key_env field is already documented in _VALID_CUSTOM_PROVIDER_FIELDS
and normalized by normalize_custom_provider_entry(), so this was just
an oversight in the ProviderDef construction.

Also adds a regression test that verifies key_env is properly
propagated into the resolved ProviderDef.
This commit is contained in:
Telos 2026-04-22 21:25:23 -07:00 committed by Teknium
parent 9f97915163
commit fa11b11cf5
2 changed files with 60 additions and 5 deletions

View file

@ -639,7 +639,7 @@ def resolve_custom_provider(
# from a prior model-switch bug), fall back to the first custom
# provider entry so existing configs self-heal. (GH #17478)
bare_custom_fallback = requested == "custom"
first_valid = None
first_valid: Optional[Tuple[str, str, Tuple[str, ...]]] = None
for entry in custom_providers:
if not isinstance(entry, dict):
@ -655,9 +655,14 @@ def resolve_custom_provider(
if not display_name or not api_url:
continue
key_env = (entry.get("key_env") or "").strip()
env_vars: List[str] = []
if key_env:
env_vars.append(key_env)
# Stash the first valid entry for bare-"custom" fallback
if first_valid is None:
first_valid = (display_name, api_url)
first_valid = (display_name, api_url, tuple(env_vars))
slug = custom_provider_slug(display_name)
if requested not in {display_name.lower(), slug}:
@ -667,7 +672,7 @@ def resolve_custom_provider(
id=slug,
name=display_name,
transport="openai_chat",
api_key_env_vars=(),
api_key_env_vars=tuple(env_vars),
base_url=api_url,
is_aggregator=False,
auth_type="api_key",
@ -676,13 +681,13 @@ def resolve_custom_provider(
# Self-heal: bare "custom" matched nothing — return first valid entry
if bare_custom_fallback and first_valid:
dname, aurl = first_valid
dname, aurl, denv = first_valid
slug = custom_provider_slug(dname)
return ProviderDef(
id=slug,
name=dname,
transport="openai_chat",
api_key_env_vars=(),
api_key_env_vars=denv,
base_url=aurl,
is_aggregator=False,
auth_type="api_key",

View file

@ -337,6 +337,7 @@ def test_list_dedupes_dict_model_matching_singular_default(monkeypatch):
# ─────────────────────────────────────────────────────────────────────────────
# #9210: group custom_providers by (base_url, api_key) in /model picker
# ─────────────────────────────────────────────────────────────────────────────
@ -791,3 +792,52 @@ def test_custom_providers_discover_models_false_string_is_normalised(monkeypatch
assert gateway_prov is not None
assert calls == [], "string 'false' must disable live discovery"
assert gateway_prov["models"] == ["only-model"]
def test_resolve_custom_provider_passes_key_env():
"""resolve_custom_provider should propagate key_env into api_key_env_vars.
Regression: previously api_key_env_vars was always (), silently dropping
the configured env var and causing 401s on every request.
"""
from hermes_cli.providers import resolve_custom_provider
resolved = resolve_custom_provider(
"custom:token-plan",
custom_providers=[
{
"name": "token-plan",
"base_url": "https://token-plan-sgp.xiaomimimo.com/v1",
"key_env": "XIAOMI_MIMO_API_KEY",
"model": "mimo-v2-pro",
}
],
)
assert resolved is not None
assert resolved.api_key_env_vars == ("XIAOMI_MIMO_API_KEY",)
assert resolved.base_url == "https://token-plan-sgp.xiaomimimo.com/v1"
def test_resolve_custom_provider_bare_custom_self_heal_passes_key_env():
"""The bare-'custom' self-heal path must also propagate key_env.
A corrupt stored provider of the bare string 'custom' falls back to the
first valid entry; that fallback previously hardcoded api_key_env_vars=(),
dropping the env var just like the named-match path did.
"""
from hermes_cli.providers import resolve_custom_provider
resolved = resolve_custom_provider(
"custom",
custom_providers=[
{
"name": "token-plan",
"base_url": "https://token-plan-sgp.xiaomimimo.com/v1",
"key_env": "XIAOMI_MIMO_API_KEY",
}
],
)
assert resolved is not None
assert resolved.api_key_env_vars == ("XIAOMI_MIMO_API_KEY",)