diff --git a/hermes_cli/runtime_provider.py b/hermes_cli/runtime_provider.py index ae3948da5b..ba41e5b747 100644 --- a/hermes_cli/runtime_provider.py +++ b/hermes_cli/runtime_provider.py @@ -171,6 +171,12 @@ def _resolve_openrouter_runtime( model_cfg = _get_model_config() cfg_base_url = model_cfg.get("base_url") if isinstance(model_cfg.get("base_url"), str) else "" cfg_provider = model_cfg.get("provider") if isinstance(model_cfg.get("provider"), str) else "" + cfg_api_key = "" + for k in ("api_key", "api"): + v = model_cfg.get(k) + if isinstance(v, str) and v.strip(): + cfg_api_key = v.strip() + break requested_norm = (requested_provider or "").strip().lower() cfg_provider = cfg_provider.strip().lower() @@ -178,26 +184,24 @@ def _resolve_openrouter_runtime( env_openrouter_base_url = os.getenv("OPENROUTER_BASE_URL", "").strip() use_config_base_url = False - if cfg_base_url.strip() and not explicit_base_url and not env_openai_base_url: + if cfg_base_url.strip() and not explicit_base_url: if requested_norm == "auto": - if not cfg_provider or cfg_provider == "auto": - use_config_base_url = True - elif requested_norm == "custom": - # Persisted custom endpoints store their base URL in config.yaml. - # If OPENAI_BASE_URL is not currently set in the environment, keep - # honoring that saved endpoint instead of falling back to OpenRouter. - if cfg_provider == "custom": + if (not cfg_provider or cfg_provider == "auto") and not env_openai_base_url: use_config_base_url = True + elif requested_norm == "custom" and cfg_provider == "custom": + # provider: custom — use base_url from config (Fixes #1760). + use_config_base_url = True # When the user explicitly requested the openrouter provider, skip # OPENAI_BASE_URL — it typically points to a custom / non-OpenRouter # endpoint and would prevent switching back to OpenRouter (#874). skip_openai_base = requested_norm == "openrouter" + # For custom, prefer config base_url over env so config.yaml is honored (#1760). base_url = ( (explicit_base_url or "").strip() - or ("" if skip_openai_base else env_openai_base_url) or (cfg_base_url.strip() if use_config_base_url else "") + or ("" if skip_openai_base else env_openai_base_url) or env_openrouter_base_url or OPENROUTER_BASE_URL ).rstrip("/") @@ -216,8 +220,10 @@ def _resolve_openrouter_runtime( or "" ) else: + # Custom endpoint: use api_key from config when using config base_url (#1760). api_key = ( explicit_api_key + or (cfg_api_key if use_config_base_url else "") or os.getenv("OPENAI_API_KEY") or os.getenv("OPENROUTER_API_KEY") or "" diff --git a/tests/test_runtime_provider_resolution.py b/tests/test_runtime_provider_resolution.py index 690c577697..bea73715ba 100644 --- a/tests/test_runtime_provider_resolution.py +++ b/tests/test_runtime_provider_resolution.py @@ -177,6 +177,50 @@ def test_custom_endpoint_uses_saved_config_base_url_when_env_missing(monkeypatch assert resolved["api_key"] == "local-key" +def test_custom_endpoint_uses_config_api_key_over_env(monkeypatch): + """provider: custom with base_url and api_key in config uses them (#1760).""" + monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "openrouter") + monkeypatch.setattr( + rp, + "_get_model_config", + lambda: { + "provider": "custom", + "base_url": "https://my-api.example.com/v1", + "api_key": "config-api-key", + }, + ) + monkeypatch.setenv("OPENAI_BASE_URL", "https://other.example.com/v1") + monkeypatch.setenv("OPENAI_API_KEY", "env-key") + monkeypatch.delenv("OPENROUTER_BASE_URL", raising=False) + + resolved = rp.resolve_runtime_provider(requested="custom") + + assert resolved["base_url"] == "https://my-api.example.com/v1" + assert resolved["api_key"] == "config-api-key" + + +def test_custom_endpoint_uses_config_api_field_when_no_api_key(monkeypatch): + """provider: custom with 'api' in config uses it as api_key (#1760).""" + monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "openrouter") + monkeypatch.setattr( + rp, + "_get_model_config", + lambda: { + "provider": "custom", + "base_url": "https://custom.example.com/v1", + "api": "config-api-field", + }, + ) + monkeypatch.delenv("OPENAI_BASE_URL", raising=False) + monkeypatch.delenv("OPENAI_API_KEY", raising=False) + monkeypatch.delenv("OPENROUTER_API_KEY", raising=False) + + resolved = rp.resolve_runtime_provider(requested="custom") + + assert resolved["base_url"] == "https://custom.example.com/v1" + assert resolved["api_key"] == "config-api-field" + + def test_custom_endpoint_auto_provider_prefers_openai_key(monkeypatch): """Auto provider with non-OpenRouter base_url should prefer OPENAI_API_KEY.