diff --git a/hermes_cli/doctor.py b/hermes_cli/doctor.py index 4138aeaa2..191535432 100644 --- a/hermes_cli/doctor.py +++ b/hermes_cli/doctor.py @@ -277,6 +277,79 @@ def run_doctor(args): config_path = HERMES_HOME / 'config.yaml' if config_path.exists(): check_ok(f"{_DHH}/config.yaml exists") + + # Validate model.provider and model.default values + try: + import yaml as _yaml + cfg = _yaml.safe_load(config_path.read_text(encoding="utf-8")) or {} + model_section = cfg.get("model") or {} + provider_raw = (model_section.get("provider") or "").strip() + provider = provider_raw.lower() + default_model = (model_section.get("default") or model_section.get("model") or "").strip() + + known_providers: set = set() + try: + from hermes_cli.auth import PROVIDER_REGISTRY + known_providers = set(PROVIDER_REGISTRY.keys()) | {"openrouter", "custom", "auto"} + except Exception: + pass + try: + from hermes_cli.auth import resolve_provider as _resolve_provider + except Exception: + _resolve_provider = None + + canonical_provider = provider + if provider and _resolve_provider is not None and provider != "auto": + try: + canonical_provider = _resolve_provider(provider) + except Exception: + canonical_provider = None + + if provider and provider != "auto": + if canonical_provider is None or (known_providers and canonical_provider not in known_providers): + known_list = ", ".join(sorted(known_providers)) if known_providers else "(unavailable)" + check_fail( + f"model.provider '{provider_raw}' is not a recognised provider", + f"(known: {known_list})", + ) + issues.append( + f"model.provider '{provider_raw}' is unknown. " + f"Valid providers: {known_list}. " + f"Fix: run 'hermes config set model.provider '" + ) + + # Warn if model is set to a provider-prefixed name on a provider that doesn't use them + if default_model and "/" in default_model and canonical_provider and canonical_provider not in ("openrouter", "custom", "auto", "ai-gateway", "kilocode", "opencode-zen", "huggingface", "nous"): + check_warn( + f"model.default '{default_model}' uses a vendor/model slug but provider is '{provider_raw}'", + "(vendor-prefixed slugs belong to aggregators like openrouter)", + ) + issues.append( + f"model.default '{default_model}' is vendor-prefixed but model.provider is '{provider_raw}'. " + "Either set model.provider to 'openrouter', or drop the vendor prefix." + ) + + # Check credentials for the configured provider + if canonical_provider and canonical_provider not in ("auto", "custom"): + try: + from hermes_cli.auth import get_auth_status + status = get_auth_status(canonical_provider) or {} + configured = bool(status.get("configured") or status.get("logged_in") or status.get("api_key")) + if not configured: + check_fail( + f"model.provider '{canonical_provider}' is set but not authenticated", + "(check ~/.hermes/.env or run 'hermes setup')", + ) + issues.append( + f"No credentials found for provider '{canonical_provider}'. " + f"Run 'hermes setup' or set the provider's API key in {_DHH}/.env, " + f"or switch providers with 'hermes config set model.provider '" + ) + except Exception: + pass + + except Exception as e: + check_warn("Could not validate model/provider config", f"({e})") else: fallback_config = PROJECT_ROOT / 'cli-config.yaml' if fallback_config.exists(): @@ -778,6 +851,16 @@ def run_doctor(args): elif response.status_code == 401: print(f"\r {color('✗', Colors.RED)} OpenRouter API {color('(invalid API key)', Colors.DIM)} ") issues.append("Check OPENROUTER_API_KEY in .env") + elif response.status_code == 402: + print(f"\r {color('✗', Colors.RED)} OpenRouter API {color('(out of credits — payment required)', Colors.DIM)}") + issues.append( + "OpenRouter account has insufficient credits. " + "Fix: run 'hermes config set model.provider ' to switch providers, " + "or fund your OpenRouter account at https://openrouter.ai/settings/credits" + ) + elif response.status_code == 429: + print(f"\r {color('✗', Colors.RED)} OpenRouter API {color('(rate limited)', Colors.DIM)} ") + issues.append("OpenRouter rate limit hit — consider switching to a different provider or waiting") else: print(f"\r {color('✗', Colors.RED)} OpenRouter API {color(f'(HTTP {response.status_code})', Colors.DIM)} ") except Exception as e: