Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions src/fast_agent/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -1059,6 +1059,29 @@ class DeepSeekSettings(BaseModel):
model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)


class NousSettings(BaseModel):
"""Settings for using Nous Research Portal provider in the fast-agent application."""

api_key: str | None = Field(default=None, description="Nous API key")
base_url: str | None = Field(
default="https://inference.nousresearch.com/v1",
description="Nous Portal API endpoint (default: https://inference.nousresearch.com/v1)",
)
default_model: str | None = Field(
default=None,
description="Default Nous model when Nous provider is selected without an explicit model",
)
default_headers: dict[str, str] | None = Field(
default=None,
description="Custom headers for all Nous API requests",
)
# Hermes passthrough: product=hermes-agent tag injected automatically in fast-agent
# as product=fast-agent; this field is not exposed in config schema — it is derived
# from the NousPortalAttributionPolicy in fast-agent 0.8+.

model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)


class GoogleSettings(BaseModel):
"""Settings for using Google models in the fast-agent application."""

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

nous: NousSettings | None = None
"""Settings for using Nous Research Portal in the fast-agent application"""

google: GoogleSettings | None = None
"""Settings for using DeepSeek models in the fast-agent application"""

Expand Down
55 changes: 55 additions & 0 deletions src/fast_agent/llm/model_database.py
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,53 @@ class ModelDatabase:
default_provider=Provider.ANTHROPIC,
)

# ---------------------------------------------------------------------------
# Nous Research Portal models
# Injected via extra_body["tags"] for portal product attribution.
# ---------------------------------------------------------------------------
H3_405B = ModelParameters(
context_window=128000,
max_output_tokens=8192,
tokenizes=TEXT_ONLY,
json_mode="object",
default_provider=Provider.NOUS,
)
H3_70B = ModelParameters(
context_window=128000,
max_output_tokens=8192,
tokenizes=TEXT_ONLY,
json_mode="object",
default_provider=Provider.NOUS,
)
H2_405B = ModelParameters(
context_window=128000,
max_output_tokens=8192,
tokenizes=TEXT_ONLY,
json_mode="object",
default_provider=Provider.NOUS,
)
H2_70B = ModelParameters(
context_window=128000,
max_output_tokens=8192,
tokenizes=TEXT_ONLY,
json_mode="object",
default_provider=Provider.NOUS,
)
NOUS_EPISTEM = ModelParameters(
context_window=128000,
max_output_tokens=8192,
tokenizes=TEXT_ONLY,
json_mode="object",
default_provider=Provider.NOUS,
)
STEPFUN_STEP_35_FLASH = ModelParameters(
context_window=128000,
max_output_tokens=8192,
tokenizes=TEXT_ONLY,
json_mode="object",
default_provider=Provider.NOUS,
)

DEEPSEEK_CHAT_STANDARD = ModelParameters(
context_window=65536,
max_output_tokens=8192,
Expand Down Expand Up @@ -925,6 +972,14 @@ class ModelDatabase:
"claude-haiku-4-5": _with_fast(ANTHROPIC_SONNET_4_VERSIONED),
# DeepSeek Models
"deepseek-chat": _with_fast(DEEPSEEK_CHAT_STANDARD),
# Nous Research Portal — Hermes brain, Nous gateway tagging
"hermes-3-405b": H3_405B,
"hermes-3-70b": H3_70B,
"hermes-2-405b": H2_405B,
"hermes-2-70b": H2_70B,
"epistem-7b-hermes-2-beta": NOUS_EPISTEM,
# StepFun via Nous Portal
"stepfun/step-3.5-flash": STEPFUN_STEP_35_FLASH,
# Google Gemini Models (vanilla aliases and versioned)
"gemini-2.0-flash": _with_fast(GEMINI_2_FLASH),
"gemini-2.5-pro": GEMINI_STANDARD,
Expand Down
4 changes: 4 additions & 0 deletions src/fast_agent/llm/model_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -980,6 +980,10 @@ def _load_provider_class(cls, provider: Provider) -> type:
from fast_agent.llm.provider.openai.llm_groq import GroqLLM

return GroqLLM
if provider == Provider.NOUS:
from fast_agent.llm.provider.nous.llm_nous import NousLLM

return NousLLM
if provider == Provider.RESPONSES:
from fast_agent.llm.provider.openai.responses import ResponsesLLM

Expand Down
110 changes: 110 additions & 0 deletions src/fast_agent/llm/model_selection.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,116 @@ class ModelSelectionCatalog:
model="playback",
),
),
Provider.NOUS: (
# Curated models from inference-api.nousresearch.com
# (matches Hermes Agent's curated list — 24 models)
# Aliases use n- prefix to avoid conflict with native provider presets
CatalogModelEntry(
alias="n-opus47",
model="nous.anthropic/claude-opus-4.7",
),
CatalogModelEntry(
alias="n-opus46",
model="nous.anthropic/claude-opus-4.6",
),
CatalogModelEntry(
alias="n-sonnet46",
model="nous.anthropic/claude-sonnet-4.6",
),
CatalogModelEntry(
alias="n-kimi26",
model="nous.moonshotai/kimi-k2.6",
),
CatalogModelEntry(
alias="n-qwen36p",
model="nous.qwen/qwen3.6-plus",
),
CatalogModelEntry(
alias="n-haiku45",
model="nous.anthropic/claude-haiku-4.5",
fast=True,
),
CatalogModelEntry(
alias="n-gpt55",
model="nous.openai/gpt-5.5",
),
CatalogModelEntry(
alias="n-gpt55pro",
model="nous.openai/gpt-5.5-pro",
),
CatalogModelEntry(
alias="n-gpt54mini",
model="nous.openai/gpt-5.4-mini",
fast=True,
),
CatalogModelEntry(
alias="n-gpt54nano",
model="nous.openai/gpt-5.4-nano",
fast=True,
),
CatalogModelEntry(
alias="n-gpt53cx",
model="nous.openai/gpt-5.3-codex",
),
CatalogModelEntry(
alias="n-mimo25p",
model="nous.xiaomi/mimo-v2.5-pro",
),
CatalogModelEntry(
alias="n-hy3",
model="nous.tencent/hy3-preview",
fast=True,
),
CatalogModelEntry(
alias="n-gem3pro",
model="nous.google/gemini-3-pro-preview",
),
CatalogModelEntry(
alias="n-gem3fl",
model="nous.google/gemini-3-flash-preview",
fast=True,
),
CatalogModelEntry(
alias="n-gem31p",
model="nous.google/gemini-3.1-pro-preview",
),
CatalogModelEntry(
alias="n-gem31fl",
model="nous.google/gemini-3.1-flash-lite-preview",
fast=True,
),
CatalogModelEntry(
alias="n-qwen36b",
model="nous.qwen/qwen3.6-35b-a3b",
fast=True,
),
CatalogModelEntry(
alias="step35",
model="nous.stepfun/step-3.5-flash",
fast=True,
),
CatalogModelEntry(
alias="n-mm27",
model="nous.minimax/minimax-m2.7",
),
CatalogModelEntry(
alias="n-glm51",
model="nous.z-ai/glm-5.1",
),
CatalogModelEntry(
alias="n-grok43",
model="nous.x-ai/grok-4.3",
),
CatalogModelEntry(
alias="n-nemo3",
model="nous.nvidia/nemotron-3-super-120b-a12b",
fast=True,
),
CatalogModelEntry(
alias="n-dsv4p",
model="nous.deepseek/deepseek-v4-pro",
),
),
}

@staticmethod
Expand Down
3 changes: 3 additions & 0 deletions src/fast_agent/llm/provider/nous/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""Nous Portal provider for fast-agent."""

from .llm_nous import NousLLM # noqa: F401
115 changes: 115 additions & 0 deletions src/fast_agent/llm/provider/nous/llm_nous.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
"""Nous Research (Nous Portal) provider for fast-agent.

Mirrors the Hermes Nous Portal provider in /hermes-agent/plugins/model-providers/nous/.
Key differences from raw OpenAI:
- Nous Portal product-attribution tags injected into ``extra_body`` on every call
- Base URL defaults to ``https://inference-api.nousresearch.com/v1``
- Auth via NOUS_API_KEY env var or ``nous.api_key`` in fast-agent config

Usage::

fast-agent --model nous.hermes-3-405b
# or rely on default_model from config:
# default_model: nous.hermes-3-405b
"""

from typing import Any

from openai.types.chat import ChatCompletionMessageParam

from fast_agent.llm.provider.openai.llm_openai_compatible import OpenAICompatibleLLM
from fast_agent.llm.provider_types import Provider
from fast_agent.types import RequestParams

# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

NOUS_BASE_URL = "https://inference-api.nousresearch.com/v1"
NOUS_DEFAULT_MODEL = "hermes-3-405b"


# ---------------------------------------------------------------------------
# NousLLM
# ---------------------------------------------------------------------------


class NousLLM(OpenAICompatibleLLM):
"""Nous Portal provider — OpenAI-compatible calls with Portal attribution tags.

The base ``OpenAICompatibleLLM`` handles the standard OpenAI protocol.
Nous adds two things:
1. ``extra_body["tags"]`` — product=fast-agent + client version tags for portal attribution
2. Nous-specific base URL (``https://inference.nousresearch.com/v1``)
"""

def __init__(self, **kwargs) -> None:
kwargs.pop("provider", None)
super().__init__(provider=Provider.NOUS, **kwargs)

# ------------------------------------------------------------------
# Base URL
# ------------------------------------------------------------------

def _provider_base_url(self) -> str | None:
"""Return Nous Portal base URL, honouring an explicit config override."""
if self.context.config and self.context.config.nous:
override = self.context.config.nous.base_url
if override:
return override
return NOUS_BASE_URL

# ------------------------------------------------------------------
# Default model (when no model is specified)
# ------------------------------------------------------------------

def _initialize_default_params(self, kwargs: dict) -> RequestParams:
"""Initialize Nous default parameters when no model is explicitly set."""
return self._initialize_default_params_with_model_fallback(
kwargs, NOUS_DEFAULT_MODEL
)

# ------------------------------------------------------------------
# Portal attribution tags injected on every request
# ------------------------------------------------------------------

@staticmethod
def _nous_portal_tags() -> list[str]:
"""Return fast-agent product-attribution tags for Nous Portal requests.

Shape: ``["product=fast-agent", "client=fast-agent-client-v{__version__}"]``.

The version tag allows Nous to bucketed usage by agent toolkit release.
"""
try:
from fast_agent import __version__ as _ver

client_tag = f"client=fast-agent-client-v{_ver}"
except Exception:
client_tag = "client=fast-agent-client-vunknown"
return ["product=fast-agent", client_tag]

def _prepare_api_request(
self,
messages: list[ChatCompletionMessageParam],
tools: list[Any] | None,
request_params: RequestParams,
) -> dict[str, Any]:
"""Build keyword arguments for the OpenAI-compatible chat completion.

Calls ``super()._prepare_api_request()`` for the standard path, then
adds ``tags`` to ``extra_body`` for Nous Portal product attribution.
"""
arguments: dict[str, Any] = super()._prepare_api_request(
messages, tools, request_params
)

# Nous Portal attribution tags — idempotent merge
tags = self._nous_portal_tags()
existing = arguments.get("extra_body")
if isinstance(existing, dict):
existing.setdefault("tags", []).extend(tags)
else:
arguments["extra_body"] = {"tags": tags}

return arguments
1 change: 1 addition & 0 deletions src/fast_agent/llm/provider_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ def config_name(self) -> str:
XAI = ("xai", "xAI") # For xAI Grok models via the Responses API
BEDROCK = ("bedrock", "Bedrock")
GROQ = ("groq", "Groq")
NOUS = ("nous", "Nous Portal") # Nous Research Portal
CODEX_RESPONSES = ("codexresponses", "Codex Responses")
RESPONSES = ("responses", "Responses")
OPENRESPONSES = ("openresponses", "OpenResponses")
1 change: 1 addition & 0 deletions src/fast_agent/ui/model_picker_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
Provider.BEDROCK,
Provider.DEEPSEEK,
Provider.ALIYUN,
Provider.NOUS,
Provider.OPENROUTER,
Provider.FAST_AGENT,
)
Expand Down
Loading