mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-27 11:22:03 +00:00
feat(desktop): route Nous subscribers onto the Tool Gateway from the GUI
When the GUI sets the main provider to Nous via POST /api/model/set, call the same apply_nous_managed_defaults the CLI uses after model selection, so GUI/onboarding users land on the Nous Tool Gateway the same way CLI users do — no separate prompt, no duplicated logic. Purely additive: apply_nous_managed_defaults skips any tool where the user has a direct key (FIRECRAWL_API_KEY, FAL_KEY, etc.) or explicit config, so it never overwrites a user's own setup. Only unconfigured tools get routed. - web_server.py: in set_model_assignment (scope=main, provider=nous), resolve enabled toolsets and apply managed defaults; guarded so a Portal hiccup never blocks saving the model. Returns routed tools as gateway_tools. - onboarding.ts: surface a 'Tool Gateway enabled' toast listing routed tools. - types/hermes.ts: add gateway_tools to ModelAssignmentResponse. - tests: cover nous-applies, non-nous-skips, and failure-doesnt-block-save.
This commit is contained in:
parent
967bc910b4
commit
c3a21c5d49
4 changed files with 138 additions and 2 deletions
|
|
@ -134,6 +134,36 @@ function notifyReady(provider: string) {
|
|||
notify({ kind: 'success', title: 'Hermes is ready', message: `${provider} connected.` })
|
||||
}
|
||||
|
||||
// Human-friendly labels for tools auto-routed through the Nous Tool Gateway,
|
||||
// mirroring hermes_cli/nous_subscription._GATEWAY_TOOL_LABELS so the GUI and
|
||||
// CLI describe the same thing.
|
||||
const GATEWAY_TOOL_LABELS: Record<string, string> = {
|
||||
browser: 'browser automation',
|
||||
image_gen: 'image generation',
|
||||
tts: 'text-to-speech',
|
||||
video_gen: 'video generation',
|
||||
web: 'web search & extract'
|
||||
}
|
||||
|
||||
// When switching to Nous auto-routes unconfigured tools through the Tool
|
||||
// Gateway, tell the user which ones — same information the CLI prints. Silent
|
||||
// when nothing changed (subscriber already configured, has own keys, etc.).
|
||||
function notifyGatewayTools(tools: string[] | undefined) {
|
||||
if (!tools || tools.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const labels = tools.map(t => GATEWAY_TOOL_LABELS[t] ?? t)
|
||||
const list = labels.length === 1 ? labels[0] : `${labels.slice(0, -1).join(', ')} and ${labels[labels.length - 1]}`
|
||||
|
||||
notify({
|
||||
durationMs: 8000,
|
||||
kind: 'info',
|
||||
message: `${list} now run through your Nous subscription — no separate API keys needed.`,
|
||||
title: 'Tool Gateway enabled'
|
||||
})
|
||||
}
|
||||
|
||||
// After credentials are persisted, ask the backend which provider+models
|
||||
// are now authenticated. Pick the first curated model for the matching
|
||||
// provider as a sensible default, persist it via /api/model/set, and
|
||||
|
|
@ -221,11 +251,12 @@ async function completeWithModelConfirm(
|
|||
// (3) If they bail out (e.g., refresh the page), they still end up
|
||||
// with a working config, not an empty-model fallback.
|
||||
try {
|
||||
await setModelAssignment({
|
||||
const res = await setModelAssignment({
|
||||
scope: 'main',
|
||||
provider: defaults.providerSlug,
|
||||
model: defaults.defaultModel
|
||||
})
|
||||
notifyGatewayTools(res.gateway_tools)
|
||||
} catch {
|
||||
// Persistence failed — still show the confirm card so the user can
|
||||
// pick something explicitly. The backend will pick its own default
|
||||
|
|
|
|||
|
|
@ -513,6 +513,10 @@ export interface ModelAssignmentRequest {
|
|||
}
|
||||
|
||||
export interface ModelAssignmentResponse {
|
||||
/** Toolset keys auto-routed through the Nous Tool Gateway as a result of
|
||||
* switching the main provider to Nous. Empty unless provider === 'nous'
|
||||
* and the user is a paid subscriber with unconfigured tools. */
|
||||
gateway_tools?: string[]
|
||||
model?: string
|
||||
ok: boolean
|
||||
provider?: string
|
||||
|
|
|
|||
|
|
@ -1396,8 +1396,44 @@ async def set_model_assignment(body: ModelAssignment):
|
|||
if "context_length" in model_cfg:
|
||||
model_cfg.pop("context_length", None)
|
||||
cfg["model"] = model_cfg
|
||||
|
||||
# When switching the main provider to Nous, mirror the CLI's
|
||||
# post-model-selection behaviour (hermes_cli/main.py
|
||||
# prompt_enable_tool_gateway / tools_config apply_nous_managed_defaults):
|
||||
# auto-route any *unconfigured* tools through the Nous Tool Gateway.
|
||||
# This is purely additive — apply_nous_managed_defaults skips every
|
||||
# tool where the user already has a direct key (FIRECRAWL_API_KEY,
|
||||
# FAL_KEY, etc.) or an explicit backend/provider in config, so it
|
||||
# never overwrites a user's own setup. GUI users thus land on the
|
||||
# gateway the same way CLI users do, without a separate prompt.
|
||||
gateway_tools: list[str] = []
|
||||
if provider.strip().lower() == "nous":
|
||||
try:
|
||||
from hermes_cli.nous_subscription import apply_nous_managed_defaults
|
||||
from hermes_cli.tools_config import _get_platform_tools
|
||||
|
||||
enabled = _get_platform_tools(
|
||||
cfg, "cli", include_default_mcp_servers=False
|
||||
)
|
||||
changed = apply_nous_managed_defaults(
|
||||
cfg,
|
||||
enabled_toolsets=enabled,
|
||||
force_fresh=True,
|
||||
)
|
||||
gateway_tools = sorted(changed)
|
||||
except Exception:
|
||||
# Portal lookup hiccups / non-subscriber / non-nous gating
|
||||
# must never block saving the model assignment.
|
||||
_log.debug("apply_nous_managed_defaults skipped", exc_info=True)
|
||||
|
||||
save_config(cfg)
|
||||
return {"ok": True, "scope": "main", "provider": provider, "model": model}
|
||||
return {
|
||||
"ok": True,
|
||||
"scope": "main",
|
||||
"provider": provider,
|
||||
"model": model,
|
||||
"gateway_tools": gateway_tools,
|
||||
}
|
||||
|
||||
# scope == "auxiliary"
|
||||
aux = cfg.get("auxiliary")
|
||||
|
|
|
|||
|
|
@ -605,6 +605,71 @@ class TestWebServerEndpoints:
|
|||
if resp.status_code == 200:
|
||||
assert "FastAPI" not in resp.text # Should not serve the actual source
|
||||
|
||||
def test_set_model_main_nous_applies_gateway_defaults(self, monkeypatch):
|
||||
"""Switching the main provider to Nous calls apply_nous_managed_defaults
|
||||
(mirroring the CLI's post-model-selection Tool Gateway routing) and
|
||||
surfaces the routed tools in the response."""
|
||||
import hermes_cli.nous_subscription as ns
|
||||
|
||||
called = {}
|
||||
|
||||
def fake_apply(config, *, enabled_toolsets=None, force_fresh=False):
|
||||
called["enabled"] = set(enabled_toolsets or ())
|
||||
called["force_fresh"] = force_fresh
|
||||
# Simulate routing the unconfigured web tool through the gateway.
|
||||
web = config.setdefault("web", {})
|
||||
web["backend"] = "firecrawl"
|
||||
return {"web"}
|
||||
|
||||
monkeypatch.setattr(ns, "apply_nous_managed_defaults", fake_apply)
|
||||
|
||||
resp = self.client.post(
|
||||
"/api/model/set",
|
||||
json={"scope": "main", "provider": "nous", "model": "hermes-4"},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["ok"] is True
|
||||
assert data["provider"] == "nous"
|
||||
assert data["gateway_tools"] == ["web"]
|
||||
assert called["force_fresh"] is True
|
||||
|
||||
def test_set_model_main_non_nous_skips_gateway_defaults(self, monkeypatch):
|
||||
"""Non-Nous providers must NOT trigger Tool Gateway auto-routing."""
|
||||
import hermes_cli.nous_subscription as ns
|
||||
|
||||
def boom(*args, **kwargs): # pragma: no cover - must not be called
|
||||
raise AssertionError("apply_nous_managed_defaults called for non-nous provider")
|
||||
|
||||
monkeypatch.setattr(ns, "apply_nous_managed_defaults", boom)
|
||||
|
||||
resp = self.client.post(
|
||||
"/api/model/set",
|
||||
json={"scope": "main", "provider": "openrouter", "model": "anthropic/claude-opus-4.8"},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["ok"] is True
|
||||
assert data.get("gateway_tools", []) == []
|
||||
|
||||
def test_set_model_main_gateway_failure_does_not_block_save(self, monkeypatch):
|
||||
"""A Portal/gateway hiccup must never prevent saving the model."""
|
||||
import hermes_cli.nous_subscription as ns
|
||||
|
||||
def boom(*args, **kwargs):
|
||||
raise RuntimeError("portal unreachable")
|
||||
|
||||
monkeypatch.setattr(ns, "apply_nous_managed_defaults", boom)
|
||||
|
||||
resp = self.client.post(
|
||||
"/api/model/set",
|
||||
json={"scope": "main", "provider": "nous", "model": "hermes-4"},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["ok"] is True
|
||||
assert data.get("gateway_tools", []) == []
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _build_schema_from_config tests
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue