Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 74 additions & 10 deletions agent/auxiliary_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3560,6 +3560,78 @@ def _try_configured_fallback_for_unavailable_client(
)


def _explicit_provider_unavailable_message(provider: str) -> str:
"""Explain why an explicit auxiliary provider could not build a client."""
explicit = (provider or "").strip().lower()
auth_type = ""

try:
from hermes_cli.auth import PROVIDER_REGISTRY

pconfig = PROVIDER_REGISTRY.get(explicit)
if pconfig is not None:
auth_type = str(getattr(pconfig, "auth_type", "") or "").strip().lower()
except Exception:
pass

if not auth_type:
try:
from providers import get_provider_profile

profile = get_provider_profile(explicit)
if profile is not None:
auth_type = str(getattr(profile, "auth_type", "") or "").strip().lower()
except Exception:
pass

if auth_type == "vertex" or explicit in {"vertex", "google-vertex", "vertex-ai", "gcp-vertex"}:
return (
f"Provider '{explicit}' is set in config.yaml but provider credentials "
"could not be resolved. Vertex uses Google Cloud Application Default "
"Credentials (ADC), OAuth, or a service account rather than a static "
"API key; run `gcloud auth application-default login`, configure the "
"vertex section in config.yaml, ensure google-auth is installed, or "
"switch to a different provider with `hermes model`."
)

if auth_type in {"oauth_device_code", "oauth_external"}:
return (
f"Provider '{explicit}' is set in config.yaml but OAuth credentials "
"could not be resolved. Re-authorize the provider with `hermes auth` "
"or `hermes model`, ensure any provider dependencies are installed, "
"or switch to a different provider."
)

if auth_type == "aws_sdk":
return (
f"Provider '{explicit}' is set in config.yaml but AWS SDK credentials "
"could not be resolved. Configure AWS credentials/SSO/role access and "
"the provider region, install required dependencies, or switch to a "
"different provider."
)

if auth_type == "external_process":
return (
f"Provider '{explicit}' is set in config.yaml but its external-process "
"credentials could not be resolved. Ensure the provider CLI/helper is "
"installed and authenticated, or switch to a different provider."
)

if auth_type and auth_type != "api_key":
return (
f"Provider '{explicit}' is set in config.yaml but provider credentials "
f"for auth_type '{auth_type}' could not be resolved. Configure the "
"provider credentials/dependencies, or switch to a different provider."
)

key_env = f"{explicit.upper()}_API_KEY"
return (
f"Provider '{explicit}' is set in config.yaml but no API key "
f"was found. Set the {key_env} environment "
f"variable, or switch to a different provider with `hermes model`."
)


def _fallback_entry_api_key(entry: Dict[str, Any]) -> Optional[str]:
"""Resolve inline or env-backed API key from a fallback-chain entry."""
explicit = str(entry.get("api_key") or "").strip()
Expand Down Expand Up @@ -5946,11 +6018,7 @@ def call_llm(
client, final_model = fb_client, fb_model
resolved_provider = fb_label or resolved_provider
else:
raise RuntimeError(
f"Provider '{_explicit}' is set in config.yaml but no API key "
f"was found. Set the {_explicit.upper()}_API_KEY environment "
f"variable, or switch to a different provider with `hermes model`."
)
raise RuntimeError(_explicit_provider_unavailable_message(_explicit))
# For auto/custom with no credentials, try the full auto chain
# rather than hardcoding OpenRouter (which may be depleted).
# Pass model=None so each provider uses its own default —
Expand Down Expand Up @@ -6520,11 +6588,7 @@ async def async_call_llm(
)
resolved_provider = fb_label or resolved_provider
else:
raise RuntimeError(
f"Provider '{_explicit}' is set in config.yaml but no API key "
f"was found. Set the {_explicit.upper()}_API_KEY environment "
f"variable, or switch to a different provider with `hermes model`."
)
raise RuntimeError(_explicit_provider_unavailable_message(_explicit))
if client is None and not resolved_base_url:
logger.info("Auxiliary %s: provider %s unavailable, trying auto-detection chain",
task or "call", resolved_provider)
Expand Down
37 changes: 37 additions & 0 deletions tests/agent/test_auxiliary_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2047,6 +2047,43 @@ def test_401_auth_error_no_fallback_with_explicit_provider(self, monkeypatch):
mock_fb.assert_not_called()


class TestExplicitProviderUnavailableMessages:
"""Startup client-build failures should name the provider's credential type."""

def _call_with_unavailable_provider(self, provider):
with patch("agent.auxiliary_client._resolve_task_provider_model",
return_value=(provider, "test-model", None, None, None)), \
patch("agent.auxiliary_client._get_cached_client",
return_value=(None, None)), \
patch("agent.auxiliary_client._try_configured_fallback_for_unavailable_client",
return_value=(None, None, "")):
return call_llm(
task="compression",
messages=[{"role": "user", "content": "hello"}],
)

def test_vertex_unavailable_mentions_adc_oauth_not_fake_api_key(self):
with pytest.raises(RuntimeError) as excinfo:
self._call_with_unavailable_provider("vertex")

message = str(excinfo.value)
assert "VERTEX_API_KEY" not in message
assert "GEMINI_API_KEY" not in message
assert "API key was found" not in message
assert "provider credentials" in message
assert "Application Default Credentials" in message
assert "OAuth" in message
assert "google-auth" in message

def test_api_key_provider_unavailable_keeps_api_key_guidance(self):
with pytest.raises(RuntimeError) as excinfo:
self._call_with_unavailable_provider("zai")

message = str(excinfo.value)
assert "no API key was found" in message
assert "ZAI_API_KEY" in message


class TestAuxiliaryFallbackLayering:
"""Explicit-provider users get layered fallback: configured_chain → main agent → warn."""

Expand Down
Loading