diff --git a/src/fast_agent/config.py b/src/fast_agent/config.py index 2034c09ce..fc9853385 100644 --- a/src/fast_agent/config.py +++ b/src/fast_agent/config.py @@ -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.""" @@ -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""" diff --git a/src/fast_agent/llm/model_database.py b/src/fast_agent/llm/model_database.py index 7d7941e9b..ada7b4a28 100644 --- a/src/fast_agent/llm/model_database.py +++ b/src/fast_agent/llm/model_database.py @@ -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, @@ -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, diff --git a/src/fast_agent/llm/model_factory.py b/src/fast_agent/llm/model_factory.py index c9d2e49dd..f88e7972c 100644 --- a/src/fast_agent/llm/model_factory.py +++ b/src/fast_agent/llm/model_factory.py @@ -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 diff --git a/src/fast_agent/llm/model_selection.py b/src/fast_agent/llm/model_selection.py index df2ef8874..2038d8dca 100644 --- a/src/fast_agent/llm/model_selection.py +++ b/src/fast_agent/llm/model_selection.py @@ -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 diff --git a/src/fast_agent/llm/provider/nous/__init__.py b/src/fast_agent/llm/provider/nous/__init__.py new file mode 100644 index 000000000..9acff5e64 --- /dev/null +++ b/src/fast_agent/llm/provider/nous/__init__.py @@ -0,0 +1,3 @@ +"""Nous Portal provider for fast-agent.""" + +from .llm_nous import NousLLM # noqa: F401 diff --git a/src/fast_agent/llm/provider/nous/llm_nous.py b/src/fast_agent/llm/provider/nous/llm_nous.py new file mode 100644 index 000000000..0156d85a5 --- /dev/null +++ b/src/fast_agent/llm/provider/nous/llm_nous.py @@ -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 diff --git a/src/fast_agent/llm/provider_types.py b/src/fast_agent/llm/provider_types.py index 71ebfcb79..6f7224608 100644 --- a/src/fast_agent/llm/provider_types.py +++ b/src/fast_agent/llm/provider_types.py @@ -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") diff --git a/src/fast_agent/ui/model_picker_common.py b/src/fast_agent/ui/model_picker_common.py index edd3a589e..9cd0840cd 100644 --- a/src/fast_agent/ui/model_picker_common.py +++ b/src/fast_agent/ui/model_picker_common.py @@ -41,6 +41,7 @@ Provider.BEDROCK, Provider.DEEPSEEK, Provider.ALIYUN, + Provider.NOUS, Provider.OPENROUTER, Provider.FAST_AGENT, )