fix(cli): route keyless first run into provider onboarding instead of a broken chat
A completely unconfigured install previously booted into a working-looking chat (banner showed model 'unknown'), accepted a message, spun ~30s, then failed with 'Set OPENROUTER_API_KEY' — a provider the user never chose — and never offered setup. - HermesCLI.run() now probes provider readiness at startup (TTY only) and offers the shared provider picker (hermes model flow, which fronts Quick Setup / Nous Portal OAuth) when nothing is configured. Decline is respected; picker state re-syncs into the live CLI so the next turn works without a restart. - New silent probe _runtime_credentials_ready(): no printing, no state mutation; handles keyless local endpoints and callable bearer providers. - The empty-api-key error is provider-aware: names the actual resolved provider and points at 'hermes model' / 'hermes setup' instead of hardcoding OPENROUTER_API_KEY. - Banner: unconfigured installs render 'no model configured — run /model' in red instead of the silent 'unknown' model slug. Consumer-onboarding audit finding #2 (sev 5), Aug 2026.
This commit is contained in:
@@ -111,8 +111,13 @@ class CLIAgentSetupMixin:
|
||||
base_url, _source,
|
||||
)
|
||||
else:
|
||||
print("\n⚠️ Provider resolver returned an empty API key. "
|
||||
"Set OPENROUTER_API_KEY or run: hermes setup")
|
||||
_prov = (resolved_provider or self.requested_provider or "").strip()
|
||||
if _prov and _prov != "auto":
|
||||
print(f"\n⚠️ No API key found for provider '{_prov}'.")
|
||||
else:
|
||||
print("\n⚠️ No inference provider is configured.")
|
||||
print(" Run 'hermes model' to choose a provider, or "
|
||||
"'hermes setup' for first-time setup.")
|
||||
return False
|
||||
if not isinstance(base_url, str) or not base_url:
|
||||
print("\n⚠️ Provider resolver returned an empty base URL. "
|
||||
@@ -179,6 +184,104 @@ class CLIAgentSetupMixin:
|
||||
|
||||
return True
|
||||
|
||||
def _runtime_credentials_ready(self) -> bool:
|
||||
"""Silently probe whether any inference provider can be resolved.
|
||||
|
||||
Unlike ``_ensure_runtime_credentials`` this never prints and never
|
||||
mutates CLI state — it exists so the interactive first-run path can
|
||||
detect a completely unconfigured install *before* the user types a
|
||||
message into a chat that cannot work (#62935-adjacent UX class:
|
||||
keyless first run must route into onboarding, not a broken chat).
|
||||
"""
|
||||
from hermes_cli.runtime_provider import resolve_runtime_provider
|
||||
|
||||
try:
|
||||
runtime = resolve_runtime_provider(
|
||||
requested=self.requested_provider,
|
||||
explicit_api_key=self._explicit_api_key,
|
||||
explicit_base_url=self._explicit_base_url,
|
||||
)
|
||||
except Exception:
|
||||
return False
|
||||
if not isinstance(runtime, dict):
|
||||
return False
|
||||
api_key = runtime.get("api_key")
|
||||
base_url = runtime.get("base_url")
|
||||
if callable(api_key) and not isinstance(api_key, str):
|
||||
return bool(base_url)
|
||||
if isinstance(api_key, str) and api_key:
|
||||
return bool(base_url)
|
||||
# Keyless custom/local endpoints (ollama, llama.cpp, vLLM…) are fine.
|
||||
return bool(
|
||||
isinstance(base_url, str)
|
||||
and base_url
|
||||
and "openrouter.ai" not in base_url
|
||||
)
|
||||
|
||||
def _offer_first_run_setup(self) -> bool:
|
||||
"""Offer the provider picker when no provider is configured at all.
|
||||
|
||||
Called from the interactive startup path when
|
||||
``_runtime_credentials_ready()`` is False and stdin is a TTY. Runs the
|
||||
exact same flow as ``hermes model`` (which fronts Quick Setup / Nous
|
||||
Portal OAuth as the first, recommended option) so there is a single
|
||||
source of truth for provider onboarding. Returns True when a provider
|
||||
was configured.
|
||||
"""
|
||||
from cli import _cprint, logger
|
||||
|
||||
_cprint("")
|
||||
_cprint("⚕ No inference provider is configured yet — let's fix that.")
|
||||
_cprint(" You'll pick a provider (Nous Portal OAuth is the fastest; "
|
||||
"no API key needed) and a model.")
|
||||
try:
|
||||
answer = input(" Set up a provider now? [Y/n]: ").strip().lower()
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
print()
|
||||
answer = "n"
|
||||
if answer in {"n", "no"}:
|
||||
_cprint(" Skipped. Run 'hermes model' or 'hermes setup' any time.")
|
||||
return False
|
||||
|
||||
try:
|
||||
from hermes_cli.main import select_provider_and_model
|
||||
select_provider_and_model()
|
||||
except (KeyboardInterrupt, EOFError, SystemExit):
|
||||
print()
|
||||
_cprint(" Setup cancelled. Run 'hermes model' any time.")
|
||||
return False
|
||||
except Exception as exc:
|
||||
logger.debug("first-run provider setup failed: %s", exc)
|
||||
_cprint(f" ⚠️ Provider setup failed: {exc}")
|
||||
_cprint(" Run 'hermes model' to try again.")
|
||||
return False
|
||||
|
||||
# Re-sync CLI state from what the picker persisted so the very next
|
||||
# turn uses the new provider without a restart.
|
||||
try:
|
||||
from hermes_cli.config import load_config
|
||||
_model_cfg = (load_config().get("model") or {})
|
||||
if isinstance(_model_cfg, dict):
|
||||
_new_provider = (_model_cfg.get("provider") or "").strip()
|
||||
if _new_provider:
|
||||
self.requested_provider = _new_provider
|
||||
_new_model = (
|
||||
_model_cfg.get("default") or _model_cfg.get("model") or ""
|
||||
).strip()
|
||||
if _new_model:
|
||||
self.model = _new_model
|
||||
except Exception as exc:
|
||||
logger.debug("first-run config re-sync failed: %s", exc)
|
||||
# Force credential re-resolution + agent rebuild on next use.
|
||||
self.agent = None
|
||||
self._active_agent_route_signature = None
|
||||
|
||||
if self._runtime_credentials_ready():
|
||||
_cprint(" ✓ Provider configured — you're ready to chat.")
|
||||
return True
|
||||
_cprint(" Provider setup didn't complete. Run 'hermes model' to retry.")
|
||||
return False
|
||||
|
||||
def _resolve_turn_agent_config(self, user_message: str) -> dict:
|
||||
"""Build the effective model/runtime config for a single user turn.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user