Skip to content

Commit c00c488

Browse files
committed
feat: add Nous Portal (Hermes Nous) provider
- Provider.NOUS in provider_types.py - NousSettings in config.py (api_key, base_url, default_model, default_headers) - NousLLM extending OpenAICompatibleLLM - Base URL defaults to https://inference.nousresearch.com/v1 - Default model: hermes-3-405b - _nous_portal_tags: product=fast-agent + client=fast-agent-client-v{__version__} - extra_body['tags'] injected on every request - NousLLM registered in ModelFactory._load_provider_class - ModelDatabase: H3_405B, H3_70B, H2_405B, H2_70B, NOUS_EPISTEM + MODELS dict Hermes parity: mirrors plugins/model-providers/nous/__init__.py but uses native fast-agent config schema (no plugin loader required).
1 parent de7be6b commit c00c488

6 files changed

Lines changed: 195 additions & 0 deletions

File tree

src/fast_agent/config.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1059,6 +1059,29 @@ class DeepSeekSettings(BaseModel):
10591059
model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
10601060

10611061

1062+
class NousSettings(BaseModel):
1063+
"""Settings for using Nous Research Portal provider in the fast-agent application."""
1064+
1065+
api_key: str | None = Field(default=None, description="Nous API key")
1066+
base_url: str | None = Field(
1067+
default="https://inference.nousresearch.com/v1",
1068+
description="Nous Portal API endpoint (default: https://inference.nousresearch.com/v1)",
1069+
)
1070+
default_model: str | None = Field(
1071+
default=None,
1072+
description="Default Nous model when Nous provider is selected without an explicit model",
1073+
)
1074+
default_headers: dict[str, str] | None = Field(
1075+
default=None,
1076+
description="Custom headers for all Nous API requests",
1077+
)
1078+
# Hermes passthrough: product=hermes-agent tag injected automatically in fast-agent
1079+
# as product=fast-agent; this field is not exposed in config schema — it is derived
1080+
# from the NousPortalAttributionPolicy in fast-agent 0.8+.
1081+
1082+
model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
1083+
1084+
10621085
class GoogleSettings(BaseModel):
10631086
"""Settings for using Google models in the fast-agent application."""
10641087

@@ -1557,6 +1580,9 @@ class Settings(BaseSettings):
15571580
deepseek: DeepSeekSettings | None = None
15581581
"""Settings for using DeepSeek models in the fast-agent application"""
15591582

1583+
nous: NousSettings | None = None
1584+
"""Settings for using Nous Research Portal in the fast-agent application"""
1585+
15601586
google: GoogleSettings | None = None
15611587
"""Settings for using DeepSeek models in the fast-agent application"""
15621588

src/fast_agent/llm/model_database.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -548,6 +548,46 @@ class ModelDatabase:
548548
default_provider=Provider.ANTHROPIC,
549549
)
550550

551+
# ---------------------------------------------------------------------------
552+
# Nous Research Portal models
553+
# Injected via extra_body["tags"] for portal product attribution.
554+
# ---------------------------------------------------------------------------
555+
H3_405B = ModelParameters(
556+
context_window=128000,
557+
max_output_tokens=8192,
558+
tokenizes=TEXT_ONLY,
559+
json_mode="object",
560+
default_provider=Provider.NOUS,
561+
)
562+
H3_70B = ModelParameters(
563+
context_window=128000,
564+
max_output_tokens=8192,
565+
tokenizes=TEXT_ONLY,
566+
json_mode="object",
567+
default_provider=Provider.NOUS,
568+
)
569+
H2_405B = ModelParameters(
570+
context_window=128000,
571+
max_output_tokens=8192,
572+
tokenizes=TEXT_ONLY,
573+
json_mode="object",
574+
default_provider=Provider.NOUS,
575+
)
576+
H2_70B = ModelParameters(
577+
context_window=128000,
578+
max_output_tokens=8192,
579+
tokenizes=TEXT_ONLY,
580+
json_mode="object",
581+
default_provider=Provider.NOUS,
582+
)
583+
NOUS_EPISTEM = ModelParameters(
584+
context_window=128000,
585+
max_output_tokens=8192,
586+
tokenizes=TEXT_ONLY,
587+
json_mode="object",
588+
default_provider=Provider.NOUS,
589+
)
590+
551591
DEEPSEEK_CHAT_STANDARD = ModelParameters(
552592
context_window=65536,
553593
max_output_tokens=8192,
@@ -925,6 +965,12 @@ class ModelDatabase:
925965
"claude-haiku-4-5": _with_fast(ANTHROPIC_SONNET_4_VERSIONED),
926966
# DeepSeek Models
927967
"deepseek-chat": _with_fast(DEEPSEEK_CHAT_STANDARD),
968+
# Nous Research Portal — Hermes brain, Nous gateway tagging
969+
"hermes-3-405b": H3_405B,
970+
"hermes-3-70b": H3_70B,
971+
"hermes-2-405b": H2_405B,
972+
"hermes-2-70b": H2_70B,
973+
"epistem-7b-hermes-2-beta": NOUS_EPISTEM,
928974
# Google Gemini Models (vanilla aliases and versioned)
929975
"gemini-2.0-flash": _with_fast(GEMINI_2_FLASH),
930976
"gemini-2.5-pro": GEMINI_STANDARD,

src/fast_agent/llm/model_factory.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -980,6 +980,10 @@ def _load_provider_class(cls, provider: Provider) -> type:
980980
from fast_agent.llm.provider.openai.llm_groq import GroqLLM
981981

982982
return GroqLLM
983+
if provider == Provider.NOUS:
984+
from fast_agent.llm.provider.nous.llm_nous import NousLLM
985+
986+
return NousLLM
983987
if provider == Provider.RESPONSES:
984988
from fast_agent.llm.provider.openai.responses import ResponsesLLM
985989

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""Nous Portal provider for fast-agent."""
2+
3+
from .llm_nous import NousLLM # noqa: F401
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
"""Nous Research (Nous Portal) provider for fast-agent.
2+
3+
Mirrors the Hermes Nous Portal provider in /hermes-agent/plugins/model-providers/nous/.
4+
Key differences from raw OpenAI:
5+
- Nous Portal product-attribution tags injected into ``extra_body`` on every call
6+
- Base URL defaults to ``https://inference.nousresearch.com/v1``
7+
- Auth via NOUS_API_KEY env var or ``nous.api_key`` in fast-agent config
8+
9+
Usage::
10+
11+
fast-agent --model nous.hermes-3-405b
12+
# or rely on default_model from config:
13+
# default_model: nous.hermes-3-405b
14+
"""
15+
16+
from typing import Any
17+
18+
from openai.types.chat import ChatCompletionMessageParam
19+
20+
from fast_agent.llm.provider.openai.llm_openai_compatible import OpenAICompatibleLLM
21+
from fast_agent.llm.provider_types import Provider
22+
from fast_agent.types import RequestParams
23+
24+
# ---------------------------------------------------------------------------
25+
# Constants
26+
# ---------------------------------------------------------------------------
27+
28+
NOUS_BASE_URL = "https://inference.nousresearch.com/v1"
29+
NOUS_DEFAULT_MODEL = "hermes-3-405b"
30+
31+
32+
# ---------------------------------------------------------------------------
33+
# NousLLM
34+
# ---------------------------------------------------------------------------
35+
36+
37+
class NousLLM(OpenAICompatibleLLM):
38+
"""Nous Portal provider — OpenAI-compatible calls with Portal attribution tags.
39+
40+
The base ``OpenAICompatibleLLM`` handles the standard OpenAI protocol.
41+
Nous adds two things:
42+
1. ``extra_body["tags"]`` — product=fast-agent + client version tags for portal attribution
43+
2. Nous-specific base URL (``https://inference.nousresearch.com/v1``)
44+
"""
45+
46+
def __init__(self, **kwargs) -> None:
47+
kwargs.pop("provider", None)
48+
super().__init__(provider=Provider.NOUS, **kwargs)
49+
50+
# ------------------------------------------------------------------
51+
# Base URL
52+
# ------------------------------------------------------------------
53+
54+
def _provider_base_url(self) -> str | None:
55+
"""Return Nous Portal base URL, honouring an explicit config override."""
56+
if self.context.config and self.context.config.nous:
57+
override = self.context.config.nous.base_url
58+
if override:
59+
return override
60+
return NOUS_BASE_URL
61+
62+
# ------------------------------------------------------------------
63+
# Default model (when no model is specified)
64+
# ------------------------------------------------------------------
65+
66+
def _initialize_default_params(self, kwargs: dict) -> RequestParams:
67+
"""Initialize Nous default parameters when no model is explicitly set."""
68+
return self._initialize_default_params_with_model_fallback(
69+
kwargs, NOUS_DEFAULT_MODEL
70+
)
71+
72+
# ------------------------------------------------------------------
73+
# Portal attribution tags injected on every request
74+
# ------------------------------------------------------------------
75+
76+
@staticmethod
77+
def _nous_portal_tags() -> list[str]:
78+
"""Return fast-agent product-attribution tags for Nous Portal requests.
79+
80+
Shape: ``["product=fast-agent", "client=fast-agent-client-v{__version__}"]``.
81+
82+
The version tag allows Nous to bucketed usage by agent toolkit release.
83+
"""
84+
try:
85+
from fast_agent import __version__ as _ver
86+
87+
client_tag = f"client=fast-agent-client-v{_ver}"
88+
except Exception:
89+
client_tag = "client=fast-agent-client-vunknown"
90+
return ["product=fast-agent", client_tag]
91+
92+
def _prepare_api_request(
93+
self,
94+
messages: list[ChatCompletionMessageParam],
95+
tools: list[Any] | None,
96+
request_params: RequestParams,
97+
) -> dict[str, Any]:
98+
"""Build keyword arguments for the OpenAI-compatible chat completion.
99+
100+
Calls ``super()._prepare_api_request()`` for the standard path, then
101+
adds ``tags`` to ``extra_body`` for Nous Portal product attribution.
102+
"""
103+
arguments: dict[str, Any] = super()._prepare_api_request(
104+
messages, tools, request_params
105+
)
106+
107+
# Nous Portal attribution tags — idempotent merge
108+
tags = self._nous_portal_tags()
109+
existing = arguments.get("extra_body")
110+
if isinstance(existing, dict):
111+
existing.setdefault("tags", []).extend(tags)
112+
else:
113+
arguments["extra_body"] = {"tags": tags}
114+
115+
return arguments

src/fast_agent/llm/provider_types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ def config_name(self) -> str:
3737
XAI = ("xai", "xAI") # For xAI Grok models via the Responses API
3838
BEDROCK = ("bedrock", "Bedrock")
3939
GROQ = ("groq", "Groq")
40+
NOUS = ("nous", "Nous Portal") # Nous Research Portal
4041
CODEX_RESPONSES = ("codexresponses", "Codex Responses")
4142
RESPONSES = ("responses", "Responses")
4243
OPENRESPONSES = ("openresponses", "OpenResponses")

0 commit comments

Comments
 (0)