feat(desktop): mirror hermes model free/paid curation in GUI onboarding

GUI onboarding picked models[0] from /api/model/options, which ignores the
Nous free/paid tier — a free user could land on a paid default (e.g.
anthropic/claude-opus-4). Now the recommended default mirrors what `hermes
model` does.

- web_server.py: new GET /api/model/recommended-default?provider=<slug>. For
  Nous it runs the same curation as the CLI (get_curated_nous_model_ids +
  pricing + check_nous_free_tier + union_with_portal_{free,paid}_recommendations
  + partition_nous_models_by_tier) so free users get a free model and paid users
  get the curated default. Other providers fall back to the first curated model.
  Never 500s — returns empty model on error so onboarding degrades gracefully.
- hermes.ts: getRecommendedDefaultModel client + RecommendedDefaultModel type.
- onboarding.ts: fetchProviderDefaultModel prefers the recommended endpoint,
  falls back to models[0] when unavailable.
- tests: free-tier picks free model, paid-tier picks curated default, failure
  returns empty without 500.
This commit is contained in:
emozilla
2026-05-31 05:48:04 -04:00
parent c3a21c5d49
commit 8a9b4bb2c2
4 changed files with 171 additions and 1 deletions
+62
View File
@@ -670,6 +670,68 @@ class TestWebServerEndpoints:
assert data["ok"] is True
assert data.get("gateway_tools", []) == []
def test_recommended_default_nous_honors_free_tier(self, monkeypatch):
"""For a free-tier Nous user, the recommended default must be a free
model (mirroring `hermes model`), not the first curated paid entry."""
import hermes_cli.models as models_mod
monkeypatch.setattr(models_mod, "get_curated_nous_model_ids", lambda: ["paid/expensive", "free/cheap"])
monkeypatch.setattr(
models_mod, "get_pricing_for_provider",
lambda provider: {"paid/expensive": {"input": "1"}, "free/cheap": {"input": "0"}},
)
monkeypatch.setattr(models_mod, "check_nous_free_tier", lambda *, force_fresh=False: True)
monkeypatch.setattr(
models_mod, "union_with_portal_free_recommendations",
lambda ids, pricing, url: (ids, pricing),
)
# Free partition keeps only the free model selectable.
monkeypatch.setattr(
models_mod, "partition_nous_models_by_tier",
lambda ids, pricing, free_tier: (["free/cheap"], ["paid/expensive"]),
)
resp = self.client.get("/api/model/recommended-default?provider=nous")
assert resp.status_code == 200
data = resp.json()
assert data["provider"] == "nous"
assert data["model"] == "free/cheap"
assert data["free_tier"] is True
def test_recommended_default_nous_paid_uses_curated_default(self, monkeypatch):
"""A paid Nous user gets the first curated/paid-augmented model."""
import hermes_cli.models as models_mod
monkeypatch.setattr(models_mod, "get_curated_nous_model_ids", lambda: ["top/model", "other/model"])
monkeypatch.setattr(models_mod, "get_pricing_for_provider", lambda provider: {})
monkeypatch.setattr(models_mod, "check_nous_free_tier", lambda *, force_fresh=False: False)
monkeypatch.setattr(
models_mod, "union_with_portal_paid_recommendations",
lambda ids, pricing, url: (ids, pricing),
)
resp = self.client.get("/api/model/recommended-default?provider=nous")
assert resp.status_code == 200
data = resp.json()
assert data["provider"] == "nous"
assert data["model"] == "top/model"
assert data["free_tier"] is False
def test_recommended_default_handles_failure_gracefully(self, monkeypatch):
"""Endpoint never 500s — returns empty model on internal error."""
import hermes_cli.models as models_mod
def boom():
raise RuntimeError("portal down")
monkeypatch.setattr(models_mod, "get_curated_nous_model_ids", boom)
resp = self.client.get("/api/model/recommended-default?provider=nous")
assert resp.status_code == 200
data = resp.json()
assert data["model"] == ""
assert data["free_tier"] is None
# ---------------------------------------------------------------------------
# _build_schema_from_config tests