feat(cli): make hermes portal run the full quick-setup Nous flow (model picker)

`hermes portal` / `hermes setup --portal` previously logged in and set
provider=nous but left the model UNSELECTED (blank -> runtime default) and
never showed a picker — unlike the first-time quick setup, which runs the
model picker.

Route `_run_portal_one_shot` through `_model_flow_nous` — the exact same
routine quick setup (`_run_first_time_quick_setup`) and `hermes model` -> Nous
use. It handles both the logged-out path (device-code OAuth, which picks a
model internally) and the logged-in path (curated Nous model picker), then
offers the Tool Gateway opt-in and sets provider=nous. Net effect: `hermes
portal` now offers a model picker every time and is a true single-command
collapse of quick setup's Nous step.

Removes the hand-rolled auth_add_command + manual provider write + separate
Tool Gateway prompt (now a single source of truth). Re-syncs the in-memory
config from disk afterward so a caller's later save_config can't clobber the
model/provider written by the login flow.

Docs (CLI help, portal_cli docstrings, nous-portal EN + zh-Hans) updated to
mention model selection. New regression test asserts `_run_portal_one_shot`
delegates to `_model_flow_nous`.

Verified live: `hermes portal` now shows the 27-model curated picker, 'Skip
(keep current)' preserves prior provider/model.
This commit is contained in:
kshitijk4poor 2026-06-04 02:20:31 +05:30
parent d4787d3e2e
commit cd188b814e
5 changed files with 85 additions and 89 deletions

View File

@ -1,10 +1,11 @@
"""``hermes portal`` — the human-readable entry point for Nous Portal. """``hermes portal`` — the human-readable entry point for Nous Portal.
Running ``hermes portal`` with no subcommand performs the one-shot Portal Running ``hermes portal`` with no subcommand performs the one-shot Portal
onboarding: OAuth login, switch the inference provider to Nous, and offer to onboarding: OAuth login, pick a Nous model, switch the inference provider to
enable the Tool Gateway. It is the friendly alias for Nous, and offer to enable the Tool Gateway. It is the friendly alias for
``hermes auth add nous --type oauth`` (which still works) and is identical to ``hermes auth add nous --type oauth`` (which still works), is identical to
``hermes setup --portal``. ``hermes setup --portal``, and runs the same Nous flow as the first-time quick
setup.
Subcommands: Subcommands:
(none) Log in to Nous Portal + set it up (one-shot onboarding). (none) Log in to Nous Portal + set it up (one-shot onboarding).
@ -167,12 +168,13 @@ def _cmd_tools(args) -> int:
def _cmd_login(args) -> int: def _cmd_login(args) -> int:
"""Run the one-shot Nous Portal onboarding (OAuth + provider + Tool Gateway). """Run the one-shot Nous Portal onboarding (login + model + provider + tools).
This is the human-readable front door for `hermes auth add nous --type This is the human-readable front door for `hermes auth add nous --type
oauth`. It reuses the exact wiring behind `hermes setup --portal` so the oauth`. It reuses the exact wiring behind `hermes setup --portal` (which in
two commands stay in lockstep: device-code login, switch the inference turn runs the same Nous flow as the first-time quick setup), so the
provider to Nous, then offer the Tool Gateway opt-in. commands stay in lockstep: device-code login, pick a Nous model, switch the
inference provider to Nous, then offer the Tool Gateway opt-in.
""" """
from hermes_cli.setup import _run_portal_one_shot from hermes_cli.setup import _run_portal_one_shot
@ -210,12 +212,13 @@ def add_parser(subparsers) -> None:
"""Register `hermes portal` on the given argparse subparsers object.""" """Register `hermes portal` on the given argparse subparsers object."""
portal_parser = subparsers.add_parser( portal_parser = subparsers.add_parser(
"portal", "portal",
help="Set up Nous Portal (OAuth login + Tool Gateway); see also `portal info`", help="Set up Nous Portal (login, model pick, Tool Gateway); see also `portal info`",
description=( description=(
"Run `hermes portal` with no subcommand to log in to Nous Portal " "Run `hermes portal` with no subcommand to log in to Nous Portal "
"and set it up (the human-readable alias for `hermes auth add nous " "and set it up — pick a model, set Nous as your provider, and offer "
"--type oauth`, identical to `hermes setup --portal`). Subcommands: " "the Tool Gateway (the human-readable alias for `hermes auth add "
"login (default), info, open, tools." "nous --type oauth`, identical to `hermes setup --portal`). "
"Subcommands: login (default), info, open, tools."
), ),
) )
portal_sub = portal_parser.add_subparsers(dest="portal_command") portal_sub = portal_parser.add_subparsers(dest="portal_command")

View File

@ -2721,20 +2721,23 @@ SETUP_SECTIONS = [
def _run_portal_one_shot(config: dict) -> None: def _run_portal_one_shot(config: dict) -> None:
"""One-shot Nous Portal setup — OAuth + provider switch + Tool Gateway. """One-shot Nous Portal setup — OAuth + model pick + provider + Tool Gateway.
Wired into ``hermes setup --portal``. Does NOT prompt for anything Wired into ``hermes setup --portal`` and ``hermes portal``. This is the
besides what the underlying OAuth + Tool Gateway prompts already need. Nous-Portal slice of the first-time quick setup, collapsed into a single
Designed to be shareable as a single command (``hermes setup --portal``) shareable command so a brand-new user goes from zero to a fully working
that gets a brand-new user from zero to a fully working Hermes session Hermes session model selected, provider set, and web/image/tts/browser
with web/image/tts/browser tools all routed via their Portal sub. tools routed via their Portal sub without being told to run
``hermes setup`` and hunt for the quick-setup option.
The login + model selection + provider switch + Tool Gateway opt-in are all
delegated to ``_model_flow_nous`` the exact same flow quick setup uses
(``_run_first_time_quick_setup``) and the same one ``hermes model`` runs
when you pick Nous. Routing through it (instead of hand-rolling the auth +
provider write here) means ``hermes portal`` always offers a model picker,
and there is a single source of truth for the Nous onboarding steps.
""" """
from types import SimpleNamespace from hermes_cli.config import load_config
from hermes_cli.auth_commands import auth_add_command
from hermes_cli.config import save_config
from hermes_cli.auth import get_nous_auth_status
from hermes_cli.nous_subscription import prompt_enable_tool_gateway
print() print()
print( print(
@ -2758,73 +2761,37 @@ def _run_portal_one_shot(config: dict) -> None:
print_info(" Sign up: https://portal.nousresearch.com/manage-subscription") print_info(" Sign up: https://portal.nousresearch.com/manage-subscription")
print() print()
# Skip OAuth if already logged in (don't re-prompt every time the user # _model_flow_nous handles BOTH the logged-out path (device-code OAuth,
# runs `hermes setup --portal` after a successful first run). # which selects a model internally) and the already-logged-in path (curated
already_logged_in = False # Nous model picker), then offers the Tool Gateway opt-in and sets
# provider=nous via the login/model save. This is the same routine quick
# setup calls, so `hermes portal` == quick setup's Nous step.
try: try:
already_logged_in = bool((get_nous_auth_status() or {}).get("logged_in")) from hermes_cli.main import _model_flow_nous
except Exception:
already_logged_in = False
if already_logged_in: _model_flow_nous(config)
print_success(" Already logged into Nous Portal.")
else:
# Hand off to the shared auth wiring so the device-code flow is
# identical to `hermes auth add nous --type oauth`. SimpleNamespace
# mirrors the argparse Namespace contract that auth_add_command expects.
ns = SimpleNamespace(
provider="nous",
auth_type="oauth",
label=None,
api_key=None,
portal_url=None,
inference_url=None,
client_id=None,
scope=None,
no_browser=False,
timeout=None,
insecure=False,
ca_bundle=None,
)
try:
auth_add_command(ns)
except SystemExit as e:
print()
print_error(f" Nous Portal login failed (exit {e.code}).")
print_info(" You can retry later with `hermes portal`.")
return
except (KeyboardInterrupt, EOFError):
print()
print_info(" Setup cancelled.")
return
except Exception as exc:
print()
print_error(f" Nous Portal login failed: {exc}")
print_info(" You can retry later with `hermes portal`.")
return
# Set provider → nous so the model picker, status surfaces, and
# managed-tool gating all light up. Leave model.model empty so the
# runtime picks Nous's default model; the user can change it later
# with `hermes model`.
model_cfg = config.get("model")
if not isinstance(model_cfg, dict):
model_cfg = {}
config["model"] = model_cfg
model_cfg["provider"] = "nous"
save_config(config)
print()
print_success(" Nous set as your inference provider.")
# Offer the Tool Gateway opt-in (single Y/n) — same flow that fires
# from `hermes model` after picking Nous.
print()
try:
prompt_enable_tool_gateway(config)
except (KeyboardInterrupt, EOFError): except (KeyboardInterrupt, EOFError):
pass print()
print_info(" Setup cancelled.")
print_info(" You can retry later with `hermes portal`.")
return
except Exception as exc: except Exception as exc:
print_warning(f" Tool Gateway prompt skipped: {exc}") logger.debug("_model_flow_nous error during `hermes portal`: %s", exc)
print()
print_error(f" Nous Portal setup encountered an error: {exc}")
print_info(" You can retry later with `hermes portal`.")
return
# Re-sync the in-memory config from disk — _model_flow_nous (and the
# underlying login/model save) write via their own load/save cycle, so any
# later save_config(config) by a caller must not clobber those values.
try:
_refreshed = load_config()
if isinstance(_refreshed, dict):
config.clear()
config.update(_refreshed)
except Exception:
pass
print() print()
print_success("Portal setup complete.") print_success("Portal setup complete.")

View File

@ -109,3 +109,29 @@ def test_parser_registers_subcommands():
for sub in ("login", "info", "status", "open", "tools"): for sub in ("login", "info", "status", "open", "tools"):
ns = parser.parse_args(["portal", sub]) ns = parser.parse_args(["portal", sub])
assert ns.portal_command == sub assert ns.portal_command == sub
def test_one_shot_delegates_to_model_flow_nous(monkeypatch):
"""`hermes portal` must run the quick-setup Nous flow (login + MODEL PICK +
provider + Tool Gateway), i.e. delegate to `_model_flow_nous` not the
lighter auth-only path that skipped model selection.
"""
import hermes_cli.setup as setup_mod
calls = {"model_flow": 0}
def fake_model_flow(config):
calls["model_flow"] += 1
# _model_flow_nous lives in hermes_cli.main and is imported lazily inside
# _run_portal_one_shot, so patch it at the source module.
monkeypatch.setattr("hermes_cli.main._model_flow_nous", fake_model_flow)
# Keep the disk re-sync a no-op so the test never touches real config.
monkeypatch.setattr("hermes_cli.config.load_config", lambda: {})
setup_mod._run_portal_one_shot({})
assert calls["model_flow"] == 1, (
"`hermes portal` must route through _model_flow_nous so the model "
"picker runs every time (matching quick setup)."
)

View File

@ -136,7 +136,7 @@ hermes portal tools # detailed Tool Gateway catalog with per-tool routing
hermes portal open # open the subscription management page in your browser hermes portal open # open the subscription management page in your browser
``` ```
`hermes portal` (with no subcommand) is the human-readable alias for `hermes auth add nous --type oauth` — it logs you in, sets Nous as your inference provider, and offers the Tool Gateway opt-in (identical to `hermes setup --portal`). `hermes portal` (with no subcommand) is the human-readable alias for `hermes auth add nous --type oauth` — it logs you in, lets you pick a Nous model, sets Nous as your inference provider, and offers the Tool Gateway opt-in (identical to `hermes setup --portal`, and the same Nous flow as the first-time quick setup).
`hermes portal info` gives you the high-level overview: `hermes portal info` gives you the high-level overview:

View File

@ -132,7 +132,7 @@ hermes portal tools # 详细的 Tool Gateway 目录及每个工具的路由
hermes portal open # 在浏览器中打开订阅管理页面 hermes portal open # 在浏览器中打开订阅管理页面
``` ```
`hermes portal`(不带子命令)是 `hermes auth add nous --type oauth` 的易记别名——它会登录、把 Nous 设为推理服务商,并提供 Tool Gateway 启用选项(与 `hermes setup --portal` 等价)。 `hermes portal`(不带子命令)是 `hermes auth add nous --type oauth` 的易记别名——它会登录、让你选择 Nous 模型、把 Nous 设为推理服务商,并提供 Tool Gateway 启用选项(与 `hermes setup --portal` 等价,与首次快速设置走的是同一套 Nous 流程)。
`hermes portal info` 给出高层概览: `hermes portal info` 给出高层概览: