diff --git a/agent/image_routing.py b/agent/image_routing.py index 5eb962bfdf..099c6d9014 100644 --- a/agent/image_routing.py +++ b/agent/image_routing.py @@ -99,14 +99,13 @@ def _clean_str(raw: Any) -> str: return str(raw or "").strip() -def _runtime_main(key: str) -> str: - """Stripped context-local main-runtime value, or "" when unavailable.""" +def _runtime_main(key: str) -> Any: + """Context-local credential source or stripped runtime text; "" when unavailable.""" try: from agent.auxiliary_client import _runtime_main_value - from agent.command_token_source import materialize_probe_api_key value = _runtime_main_value(key) - return materialize_probe_api_key(value) if key == "api_key" else _clean_str(value) + return value if key == "api_key" else _clean_str(value) except Exception: return "" @@ -178,6 +177,12 @@ def _resolve_inference_value( → ``custom_providers[].``, ```` covering the provider and ``model.provider`` in both bare and ``custom:``-prefixed forms.""" runtime = _runtime_main(key) + # A declared source owns authentication even when its mint fails. + if key == "api_key": + from agent.command_token_source import materialize_probe_api_key + if callable(runtime): + return materialize_probe_api_key(runtime) + runtime = materialize_probe_api_key(runtime) if runtime and runtime_ok(runtime): return runtime if not isinstance(cfg, dict): diff --git a/hermes_cli/models_local.py b/hermes_cli/models_local.py index 3944e85f20..16e7457cef 100644 --- a/hermes_cli/models_local.py +++ b/hermes_cli/models_local.py @@ -158,8 +158,9 @@ def _get_ollama_native_headers(base_url: Optional[str], *, api_key: Optional[str if not configured_matches and not explicit_key: return {} headers = _get_ollama_request_headers() if configured_matches else {} - if explicit_key: + if explicit_key or callable(api_key): _drop_authorization(headers) + if explicit_key: headers["Authorization"] = f"Bearer {explicit_key}" return headers diff --git a/tests/agent/test_capability_probe_credentials.py b/tests/agent/test_capability_probe_credentials.py index cac7ad56e7..ef27e6988c 100644 --- a/tests/agent/test_capability_probe_credentials.py +++ b/tests/agent/test_capability_probe_credentials.py @@ -30,9 +30,31 @@ def test_capability_paths_share_concrete_bearer(credential, expected): auxiliary_client.clear_runtime_main() -def test_failed_callable_never_becomes_a_bearer(): +def test_failed_callable_never_becomes_a_bearer(monkeypatch): def failed(): raise RuntimeError("secret-bearing command failure") for value in (failed, lambda: object(), object(), None): assert model_metadata._auth_headers(value) == {} assert "Authorization" not in models_local._lmstudio_request_headers(value) + + from hermes_cli import models + url = "http://localhost:11434/v1" + configured = {"base_url": url, "api_key": "provider-fallback", "extra_headers": { + "aUtHoRiZaTiOn": "Bearer configured-fallback", "X-Probe-Fixture": "preserved", + }} + monkeypatch.setattr(models, "_get_provider_config_dict", lambda _: configured) + for value in (failed, lambda: object(), lambda: ""): + auxiliary_client.set_runtime_main("custom", "fixture", api_key=value) + try: + for cfg in ({"model": {"api_key": "model-fallback"}}, + {"providers": {"custom": {"api_key": "provider-fallback"}}}): + assert image_routing._resolve_inference_api_key(cfg, "custom") == "" + assert models_local._get_ollama_native_headers(url, api_key=value) == { + "X-Probe-Fixture": "preserved", + } + assert auxiliary_client._runtime_main_value("api_key") is value + finally: + auxiliary_client.clear_runtime_main() + # An absent explicit credential still permits configured authentication. + assert models_local._get_ollama_native_headers(url)["aUtHoRiZaTiOn"] == "Bearer configured-fallback" + assert image_routing._resolve_inference_api_key({"model": {"api_key": "model-fallback"}}, "custom") == "model-fallback" diff --git a/website/docs/integrations/providers.md b/website/docs/integrations/providers.md index 1007319612..dfeeda226f 100644 --- a/website/docs/integrations/providers.md +++ b/website/docs/integrations/providers.md @@ -1327,7 +1327,10 @@ Vision, thinking, and native local-model capability probes materialize the same callable credential used by chat before building authentication headers. They reuse the command token cache without replacing the chat client's callable. If a command cannot mint a string token, these best-effort probes send no bearer -rather than an object representation; chat retains its normal error handling. +rather than an object representation or a lower-priority configured credential. +Native local-model probes remove inherited Authorization on a failed explicit +callable while retaining unrelated configured headers. Chat retains its normal +error handling. Enterprise gateways often issue short-lived bearer tokens (SSO/OIDC brokers, cloud IAM, internal auth proxies) rather than static API keys, so a token copied into `.env` goes stale mid-session and requests start returning 401. `key_cmd` names a command that *prints* a token; Hermes runs it and caches the result until shortly before expiry, so long sessions keep working with no restart: