Merge pull request #38465 from kshitijk4poor/portal-quick-setup-model

feat(cli): make `hermes portal` run the full quick-setup Nous flow (model picker)
This commit is contained in:
kshitij
2026-06-03 14:09:47 -07:00
committed by GitHub
8 changed files with 126 additions and 103 deletions

View File

@ -12681,9 +12681,9 @@ def main():
setup_parser.add_argument(
"--portal",
action="store_true",
help="One-shot Nous Portal setup: log in via OAuth, set Nous as the "
"inference provider, and opt into the Tool Gateway. Skips the "
"rest of the wizard.",
help="One-shot Nous Portal setup: log in via OAuth, pick a Nous "
"model, set Nous as the inference provider, and opt into the Tool "
"Gateway. Skips the rest of the wizard.",
)
setup_parser.set_defaults(func=cmd_setup)

View File

@ -1,10 +1,11 @@
"""``hermes portal`` — the human-readable entry point for Nous Portal.
Running ``hermes portal`` with no subcommand performs the one-shot Portal
onboarding: OAuth login, switch the inference provider to 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 setup --portal``.
onboarding: OAuth login, pick a Nous model, switch the inference provider to
Nous, and offer to enable the Tool Gateway. It is the friendly alias for
``hermes auth add nous --type oauth`` (which still works), is identical to
``hermes setup --portal``, and runs the same Nous flow as the first-time quick
setup.
Subcommands:
(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:
"""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
oauth`. It reuses the exact wiring behind `hermes setup --portal` so the
two commands stay in lockstep: device-code login, switch the inference
provider to Nous, then offer the Tool Gateway opt-in.
oauth`. It reuses the exact wiring behind `hermes setup --portal` (which in
turn runs the same Nous flow as the first-time quick setup), so the
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
@ -210,12 +212,13 @@ def add_parser(subparsers) -> None:
"""Register `hermes portal` on the given argparse subparsers object."""
portal_parser = subparsers.add_parser(
"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=(
"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 "
"--type oauth`, identical to `hermes setup --portal`). Subcommands: "
"login (default), info, open, tools."
"and set it up — pick a model, set Nous as your provider, and offer "
"the Tool Gateway (the human-readable alias for `hermes auth add "
"nous --type oauth`, identical to `hermes setup --portal`). "
"Subcommands: login (default), info, open, tools."
),
)
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:
"""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
besides what the underlying OAuth + Tool Gateway prompts already need.
Designed to be shareable as a single command (``hermes setup --portal``)
that gets a brand-new user from zero to a fully working Hermes session
with web/image/tts/browser tools all routed via their Portal sub.
Wired into ``hermes setup --portal`` and ``hermes portal``. This is the
Nous-Portal slice of the first-time quick setup, collapsed into a single
shareable command so a brand-new user goes from zero to a fully working
Hermes session — model selected, provider set, and web/image/tts/browser
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.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
from hermes_cli.config import load_config
print()
print(
@ -2758,73 +2761,42 @@ def _run_portal_one_shot(config: dict) -> None:
print_info(" Sign up: https://portal.nousresearch.com/manage-subscription")
print()
# Skip OAuth if already logged in (don't re-prompt every time the user
# runs `hermes setup --portal` after a successful first run).
already_logged_in = False
# _model_flow_nous handles BOTH the logged-out path (device-code OAuth,
# which selects a model internally) and the already-logged-in path (curated
# 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:
already_logged_in = bool((get_nous_auth_status() or {}).get("logged_in"))
except Exception:
already_logged_in = False
from hermes_cli.main import _model_flow_nous
if already_logged_in:
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):
pass
_model_flow_nous(config)
except (KeyboardInterrupt, EOFError, SystemExit):
# _login_nous raises SystemExit(130)/(1) on cancel/failure; the
# logged-out path inside _model_flow_nous catches it, but the
# expired-session re-login path only catches Exception, so a
# SystemExit there would otherwise escape and kill the whole CLI.
# Treat all of these as a graceful cancel/abort for the portal flow.
print()
print_info(" Setup cancelled.")
print_info(" You can retry later with `hermes portal`.")
return
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_success("Portal setup complete.")

View File

@ -109,3 +109,49 @@ def test_parser_registers_subcommands():
for sub in ("login", "info", "status", "open", "tools"):
ns = parser.parse_args(["portal", 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)."
)
@pytest.mark.parametrize("exc", [KeyboardInterrupt, EOFError, SystemExit])
def test_one_shot_swallows_cancel_and_systemexit(monkeypatch, exc):
"""A cancel/abort from the delegated Nous flow must NOT escape and kill the
CLI. `_login_nous` raises SystemExit(130)/(1) on cancel/failure, and the
expired-session re-login path inside `_model_flow_nous` only catches
Exception — so SystemExit could otherwise propagate out. The portal handler
must treat KeyboardInterrupt/EOFError/SystemExit as a graceful cancel.
"""
import hermes_cli.setup as setup_mod
def boom(config):
raise exc
monkeypatch.setattr("hermes_cli.main._model_flow_nous", boom)
monkeypatch.setattr("hermes_cli.config.load_config", lambda: {})
# Must return normally (None), not propagate the exception.
assert setup_mod._run_portal_one_shot({}) is None

View File

@ -95,7 +95,7 @@ You should see Hermes call `web_search` (Firecrawl-backed, through the gateway)
## 5. Pick the model you actually want
The default after `hermes setup --portal` is a sensible general-purpose model, but the whole point of the subscription is access to the full catalog. Switch with `/model` mid-session:
`hermes setup --portal` lets you pick a model during setup, but the whole point of the subscription is access to the full catalog — switch any time with `/model` mid-session:
```bash
/model anthropic/claude-sonnet-4.6 # best general-purpose agentic

View File

@ -14,7 +14,7 @@ If you only have time to set up one thing, set up this. The fastest path:
hermes setup --portal
```
That single command runs the Portal OAuth, sets Nous as your inference provider in `config.yaml`, and turns on the Tool Gateway. You're ready to `hermes chat` immediately after.
That single command runs the Portal OAuth, lets you pick a Nous model, sets Nous as your inference provider in `config.yaml`, and turns on the Tool Gateway. You're ready to `hermes chat` immediately after.
Don't have a subscription yet? [portal.nousresearch.com/manage-subscription](https://portal.nousresearch.com/manage-subscription) — sign up, then come back and run the command above.
@ -99,9 +99,10 @@ This runs the full setup in one shot:
1. Opens your browser to portal.nousresearch.com for OAuth login
2. Stores the refresh token at `~/.hermes/auth.json`
3. Sets Nous as your inference provider in `~/.hermes/config.yaml`
4. Turns on the Tool Gateway (web, image, TTS, browser routing)
5. Returns you to your terminal ready to `hermes chat`
3. Lets you pick a Nous model from the curated list (or skip to keep your current one)
4. Sets Nous as your inference provider in `~/.hermes/config.yaml` (when you pick a model)
5. Turns on the Tool Gateway (web, image, TTS, browser routing)
6. Returns you to your terminal ready to `hermes chat`
If you don't have a subscription yet, sign up at [portal.nousresearch.com/manage-subscription](https://portal.nousresearch.com/manage-subscription) first.
@ -136,7 +137,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` (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:

View File

@ -95,7 +95,7 @@ Hey, search the web for "Hermes Agent release notes" and summarize the top 3 hit
## 5. 选择你实际需要的模型
`hermes setup --portal` 后的默认模型是一个合理的通用模型,但订阅的意义在于可以访问完整的模型目录在会话中使用 `/model` 切换:
`hermes setup --portal` 会在设置过程中让你选择模型,但订阅的意义在于可以访问完整的模型目录——随时可在会话中使用 `/model` 切换:
```bash
/model anthropic/claude-sonnet-4.6 # 最佳通用 agentic 模型

View File

@ -14,7 +14,7 @@ description: "一个订阅300+ 前沿模型Tool Gateway以及 Nous Chat
hermes setup --portal
```
这条命令会完成 Portal OAuth 认证,在 `config.yaml` 中将 Nous 设为推理提供商,并开启 Tool Gateway。完成后即可立即运行 `hermes chat`
这条命令会完成 Portal OAuth 认证,让你选择一个 Nous 模型,`config.yaml` 中将 Nous 设为推理提供商,并开启 Tool Gateway。完成后即可立即运行 `hermes chat`
还没有订阅?前往 [portal.nousresearch.com/manage-subscription](https://portal.nousresearch.com/manage-subscription) 注册,然后回来运行上面的命令。
@ -95,9 +95,10 @@ hermes setup --portal
1. 打开浏览器跳转至 portal.nousresearch.com 进行 OAuth 登录
2. 将 refresh token 存储至 `~/.hermes/auth.json`
3. `~/.hermes/config.yaml` 中将 Nous 设为推理提供商
4. 开启 Tool Gateway网页、图像、TTS、浏览器路由
5. 返回终端,即可运行 `hermes chat`
3. 让你从精选列表中选择一个 Nous 模型(也可跳过以保留当前模型)
4. `~/.hermes/config.yaml` 中将 Nous 设为推理提供商(当你选择模型时
5. 开启 Tool Gateway网页、图像、TTS、浏览器路由
6. 返回终端,即可运行 `hermes chat`
如果还没有订阅,请先在 [portal.nousresearch.com/manage-subscription](https://portal.nousresearch.com/manage-subscription) 注册。
@ -132,7 +133,7 @@ hermes portal tools # 详细的 Tool Gateway 目录及每个工具的路由
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` 给出高层概览: