diff --git a/.env.example b/.env.example index 924146613c457..8baacaaa7cdce 100644 --- a/.env.example +++ b/.env.example @@ -70,6 +70,15 @@ # ARCEEAI_API_KEY= # ARCEE_BASE_URL= # Override default base URL +# ============================================================================= +# LLM PROVIDER (Featherless) +# ============================================================================= +# Featherless serves thousands of open-source models (Hugging Face repo IDs) +# via an OpenAI-compatible API — e.g. zai-org/GLM-5.2, meta-llama/Llama-3.1-8B-Instruct +# Get a Featherless key at: https://featherless.ai/ +# FEATHERLESS_API_KEY= +# FEATHERLESS_BASE_URL= # Override default base URL + # ============================================================================= # LLM PROVIDER (MiniMax) # ============================================================================= diff --git a/agent/model_metadata.py b/agent/model_metadata.py index 4493eae5f1f82..ba2fb650ac477 100644 --- a/agent/model_metadata.py +++ b/agent/model_metadata.py @@ -53,6 +53,7 @@ def _resolve_requests_verify() -> bool | str: "xiaomi", "arcee", "gmi", + "featherless", "tencent-tokenhub", "custom", "local", # Common aliases @@ -65,6 +66,7 @@ def _resolve_requests_verify() -> bool | str: "tencent", "tokenhub", "tencent-cloud", "tencentmaas", "arcee-ai", "arceeai", "gmi-cloud", "gmicloud", + "featherless-ai", "featherlessai", "xai", "x-ai", "x.ai", "grok", "nvidia", "nim", "nvidia-nim", "nemotron", "qwen-portal", "novita-ai", "novitaai", @@ -420,6 +422,7 @@ def _is_custom_endpoint(base_url: str) -> bool: "api.stepfun.ai": "stepfun", "api.stepfun.com": "stepfun", "api.arcee.ai": "arcee", + "api.featherless.ai": "featherless", "api.minimax": "minimax", "dashscope.aliyuncs.com": "alibaba", "dashscope-intl.aliyuncs.com": "alibaba", @@ -1635,7 +1638,7 @@ def get_model_context_length( cache fallback with suffix/version normalisation. Only portal-derived values are persisted to disk. c. Codex OAuth /models probe - d. GMI /models endpoint + d. GMI / Featherless /models endpoint (provider-served context_length) e. Ollama native /api/show probe (any base_url, provider-agnostic) f. models.dev registry lookup (with :cloud/-cloud suffix fallback) 6. OpenRouter live API metadata (Kimi-family 32k guard) @@ -1882,6 +1885,15 @@ def get_model_context_length( ctx = _resolve_endpoint_context_length(model, base_url, api_key=api_key) if ctx is not None: return ctx + if effective_provider == "featherless" and base_url: + # Featherless serves each model at a provider-specific context_length + # (e.g. zai-org/GLM-5.2 at 256K, not the model's native 1M; an 8B Llama + # at 32K) exposed via /v1/models. Prefer that authoritative endpoint + # value over models.dev and the hardcoded native-context fallback so + # token budgeting matches what the endpoint will actually accept. + ctx = _resolve_endpoint_context_length(model, base_url, api_key=api_key) + if ctx is not None: + return ctx # 5e. Ollama native /api/show probe — runs for ANY provider with a # base_url, not just ollama-cloud. Ollama-compatible servers expose # this endpoint regardless of hostname (local Ollama, Ollama Cloud, diff --git a/apps/desktop/src/app/settings/constants.ts b/apps/desktop/src/app/settings/constants.ts index 1cf7cf3ce165d..26acf28ed0e74 100644 --- a/apps/desktop/src/app/settings/constants.ts +++ b/apps/desktop/src/app/settings/constants.ts @@ -191,6 +191,13 @@ export const PROVIDER_GROUPS: ProviderPrefix[] = [ priority: 20 }, { prefix: 'ARCEE_', name: 'Arcee AI', priority: 20 }, + { + prefix: 'FEATHERLESS_', + name: 'Featherless', + description: 'Thousands of open-source models, serverless', + docsUrl: 'https://featherless.ai/', + priority: 20 + }, { prefix: 'GMI_', name: 'GMI Cloud', diff --git a/cli-config.yaml.example b/cli-config.yaml.example index 8d3525019c869..d5c35fa93f141 100644 --- a/cli-config.yaml.example +++ b/cli-config.yaml.example @@ -27,6 +27,7 @@ model: # "nvidia" - NVIDIA NIM / build.nvidia.com (requires: NVIDIA_API_KEY) # "xiaomi" - Xiaomi MiMo (requires: XIAOMI_API_KEY) # "arcee" - Arcee AI Trinity models (requires: ARCEEAI_API_KEY) + # "featherless" - Featherless open-source models (requires: FEATHERLESS_API_KEY) # "ollama-cloud" - Ollama Cloud (requires: OLLAMA_API_KEY — https://ollama.com/settings) # "kilocode" - KiloCode gateway (requires: KILOCODE_API_KEY) # "azure-foundry" - Microsoft Foundry / Azure OpenAI (API key or Entra ID) diff --git a/hermes_cli/auth.py b/hermes_cli/auth.py index 61c2bbed7865b..5bd2171e244d7 100644 --- a/hermes_cli/auth.py +++ b/hermes_cli/auth.py @@ -284,6 +284,14 @@ class ProviderConfig: api_key_env_vars=("ARCEEAI_API_KEY",), base_url_env_var="ARCEE_BASE_URL", ), + "featherless": ProviderConfig( + id="featherless", + name="Featherless", + auth_type="api_key", + inference_base_url="https://api.featherless.ai/v1", + api_key_env_vars=("FEATHERLESS_API_KEY",), + base_url_env_var="FEATHERLESS_BASE_URL", + ), "gmi": ProviderConfig( id="gmi", name="GMI Cloud", @@ -1518,6 +1526,7 @@ def resolve_provider( "kimi-cn": "kimi-coding-cn", "moonshot-cn": "kimi-coding-cn", "step": "stepfun", "stepfun-coding-plan": "stepfun", "arcee-ai": "arcee", "arceeai": "arcee", + "featherless-ai": "featherless", "featherlessai": "featherless", "gmi-cloud": "gmi", "gmicloud": "gmi", "minimax-china": "minimax-cn", "minimax_cn": "minimax-cn", "minimax-portal": "minimax-oauth", "minimax-global": "minimax-oauth", "minimax_oauth": "minimax-oauth", diff --git a/hermes_cli/config.py b/hermes_cli/config.py index 356839f9903d3..9b29e37160ab0 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -2784,6 +2784,22 @@ def _ensure_hermes_home_managed(home: Path): "category": "provider", "advanced": True, }, + "FEATHERLESS_API_KEY": { + "description": "Featherless API key", + "prompt": "Featherless API key", + "url": "https://featherless.ai/", + "password": True, + "category": "provider", + "advanced": True, + }, + "FEATHERLESS_BASE_URL": { + "description": "Featherless base URL override", + "prompt": "Featherless base URL (leave empty for default)", + "url": None, + "password": False, + "category": "provider", + "advanced": True, + }, "GMI_API_KEY": { "description": "GMI Cloud API key", "prompt": "GMI Cloud API key", diff --git a/hermes_cli/doctor.py b/hermes_cli/doctor.py index 127adefb39c4e..b983cfe6d887e 100644 --- a/hermes_cli/doctor.py +++ b/hermes_cli/doctor.py @@ -41,6 +41,7 @@ "KIMI_API_KEY", "KIMI_CN_API_KEY", "GMI_API_KEY", + "FEATHERLESS_API_KEY", "MINIMAX_API_KEY", "MINIMAX_CN_API_KEY", "KILOCODE_API_KEY", @@ -383,6 +384,7 @@ def _build_apikey_providers_list() -> list: ("StepFun Step Plan", ("STEPFUN_API_KEY",), "https://api.stepfun.ai/step_plan/v1/models", "STEPFUN_BASE_URL", True), ("Kimi / Moonshot (China)", ("KIMI_CN_API_KEY",), "https://api.moonshot.cn/v1/models", None, True), ("Arcee AI", ("ARCEEAI_API_KEY",), "https://api.arcee.ai/api/v1/models", "ARCEE_BASE_URL", True), + ("Featherless", ("FEATHERLESS_API_KEY",), "https://api.featherless.ai/v1/models", "FEATHERLESS_BASE_URL", True), ("GMI Cloud", ("GMI_API_KEY",), "https://api.gmi-serving.com/v1/models", "GMI_BASE_URL", True), ("DeepSeek", ("DEEPSEEK_API_KEY",), "https://api.deepseek.com/v1/models", "DEEPSEEK_BASE_URL", True), ("Hugging Face", ("HF_TOKEN",), "https://router.huggingface.co/v1/models", "HF_BASE_URL", True), @@ -404,7 +406,7 @@ def _build_apikey_providers_list() -> list: _name_to_canonical = { "Z.AI / GLM": "zai", "Kimi / Moonshot": "kimi-coding", "StepFun Step Plan": "stepfun", "Kimi / Moonshot (China)": "kimi-coding-cn", - "Arcee AI": "arcee", "GMI Cloud": "gmi", "DeepSeek": "deepseek", + "Arcee AI": "arcee", "Featherless": "featherless", "GMI Cloud": "gmi", "DeepSeek": "deepseek", "Hugging Face": "huggingface", "NVIDIA NIM": "nvidia", "Alibaba/DashScope": "alibaba", "MiniMax": "minimax", "MiniMax (China)": "minimax-cn", diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 0394ef90a2e9e..57177181090c7 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -3040,6 +3040,7 @@ def _active_custom_key_from_base_url() -> str: "huggingface", "xiaomi", "arcee", + "featherless", "gmi", "nvidia", "ollama-cloud", diff --git a/hermes_cli/models.py b/hermes_cli/models.py index f84ac69564e53..e65b32e5424ec 100644 --- a/hermes_cli/models.py +++ b/hermes_cli/models.py @@ -381,6 +381,14 @@ def _xai_curated_models() -> list[str]: "trinity-large-preview", "trinity-mini", ], + "featherless": [ + "zai-org/GLM-5.2", + "zai-org/GLM-4.7", + "moonshotai/Kimi-K2-Thinking", + "deepseek-ai/DeepSeek-V3.2", + "Qwen/Qwen3.5-397B-A17B", + "meta-llama/Llama-3.1-8B-Instruct", + ], "gmi": [ "zai-org/GLM-5.1-FP8", "deepseek-ai/DeepSeek-V3.2", @@ -1040,6 +1048,7 @@ class ProviderEntry(NamedTuple): ProviderEntry("minimax-cn", "MiniMax (China)", "MiniMax China (Domestic direct API)"), ProviderEntry("ollama-cloud", "Ollama Cloud", "Ollama Cloud (Cloud-hosted open models, ollama.com)"), ProviderEntry("arcee", "Arcee AI", "Arcee AI (Trinity models, direct API)"), + ProviderEntry("featherless", "Featherless", "Featherless (Open-source models via HF repo IDs, direct API)"), ProviderEntry("gmi", "GMI Cloud", "GMI Cloud (Multi-model direct API)"), ProviderEntry("kilocode", "Kilo Code", "Kilo Code (Kilo Gateway API)"), ProviderEntry("opencode-zen", "OpenCode Zen", "OpenCode Zen (Curated models, pay-as-you-go)"), @@ -1198,6 +1207,8 @@ def group_providers(slugs): "stepfun-coding-plan": "stepfun", "arcee-ai": "arcee", "arceeai": "arcee", + "featherless-ai": "featherless", + "featherlessai": "featherless", "gmi-cloud": "gmi", "gmicloud": "gmi", "minimax-china": "minimax-cn", diff --git a/hermes_cli/providers.py b/hermes_cli/providers.py index efc3a8576ed14..1636b89d07896 100644 --- a/hermes_cli/providers.py +++ b/hermes_cli/providers.py @@ -190,6 +190,11 @@ class HermesOverlay: base_url_override="https://api.arcee.ai/api/v1", base_url_env_var="ARCEE_BASE_URL", ), + "featherless": HermesOverlay( + transport="openai_chat", + base_url_override="https://api.featherless.ai/v1", + base_url_env_var="FEATHERLESS_BASE_URL", + ), "gmi": HermesOverlay( transport="openai_chat", extra_env_vars=("GMI_API_KEY",), @@ -344,6 +349,10 @@ class ProviderDef: "arcee-ai": "arcee", "arceeai": "arcee", + # featherless + "featherless-ai": "featherless", + "featherlessai": "featherless", + # gmi "gmi-cloud": "gmi", "gmicloud": "gmi", diff --git a/hermes_cli/setup.py b/hermes_cli/setup.py index b809af6ecf792..7cd066ffd9cc8 100644 --- a/hermes_cli/setup.py +++ b/hermes_cli/setup.py @@ -98,6 +98,7 @@ def _supports_same_provider_pool_setup(provider: str) -> bool: "kimi-coding-cn": ["kimi-k2.6", "kimi-k2.5", "kimi-k2-thinking", "kimi-k2-turbo-preview"], "stepfun": ["step-3.5-flash", "step-3.5-flash-2603"], "arcee": ["trinity-large-thinking", "trinity-large-preview", "trinity-mini"], + "featherless": ["zai-org/GLM-5.2", "zai-org/GLM-4.7", "moonshotai/Kimi-K2-Thinking", "deepseek-ai/DeepSeek-V3.2", "Qwen/Qwen3.5-397B-A17B", "meta-llama/Llama-3.1-8B-Instruct"], "minimax": ["MiniMax-M2.7", "MiniMax-M2.5", "MiniMax-M2.1", "MiniMax-M2"], "minimax-cn": ["MiniMax-M2.7", "MiniMax-M2.5", "MiniMax-M2.1", "MiniMax-M2"], "kilocode": ["anthropic/claude-opus-4.6", "anthropic/claude-sonnet-4.6", "openai/gpt-5.4", "google/gemini-3-pro-preview", "google/gemini-3-flash-preview"], diff --git a/plugins/model-providers/featherless/__init__.py b/plugins/model-providers/featherless/__init__.py new file mode 100644 index 0000000000000..d108843637a4e --- /dev/null +++ b/plugins/model-providers/featherless/__init__.py @@ -0,0 +1,13 @@ +"""Featherless provider profile.""" + +from providers import register_provider +from providers.base import ProviderProfile + +featherless = ProviderProfile( + name="featherless", + aliases=("featherless-ai", "featherlessai"), + env_vars=("FEATHERLESS_API_KEY",), + base_url="https://api.featherless.ai/v1", +) + +register_provider(featherless) diff --git a/plugins/model-providers/featherless/plugin.yaml b/plugins/model-providers/featherless/plugin.yaml new file mode 100644 index 0000000000000..b72fd75460894 --- /dev/null +++ b/plugins/model-providers/featherless/plugin.yaml @@ -0,0 +1,5 @@ +name: featherless-provider +kind: model-provider +version: 1.0.0 +description: Featherless +author: Nous Research diff --git a/tests/hermes_cli/test_featherless_provider.py b/tests/hermes_cli/test_featherless_provider.py new file mode 100644 index 0000000000000..5a49d431697e2 --- /dev/null +++ b/tests/hermes_cli/test_featherless_provider.py @@ -0,0 +1,259 @@ +"""Tests for Featherless provider support — OpenAI-compatible direct API. + +Featherless (https://featherless.ai/) serves thousands of open-source models +addressed by their Hugging Face repo IDs (e.g. ``zai-org/GLM-5.2``) through an +OpenAI-compatible endpoint, so it is wired like the other HF-style API-key +providers (GMI) and uses a main-model-first auxiliary design (like Arcee). +""" + +import types + +import pytest + +from hermes_cli.auth import ( + PROVIDER_REGISTRY, + resolve_provider, + get_api_key_provider_status, + resolve_api_key_provider_credentials, +) + + +_OTHER_PROVIDER_KEYS = ( + "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "DEEPSEEK_API_KEY", + "GOOGLE_API_KEY", "GEMINI_API_KEY", "DASHSCOPE_API_KEY", + "XAI_API_KEY", "KIMI_API_KEY", "KIMI_CN_API_KEY", + "MINIMAX_API_KEY", "MINIMAX_CN_API_KEY", + "KILOCODE_API_KEY", "HF_TOKEN", "GLM_API_KEY", "ZAI_API_KEY", + "XIAOMI_API_KEY", "TOKENHUB_API_KEY", "ARCEEAI_API_KEY", "GMI_API_KEY", + "COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN", +) + + +# ============================================================================= +# Provider Registry +# ============================================================================= + + +class TestFeatherlessProviderRegistry: + def test_registered(self): + assert "featherless" in PROVIDER_REGISTRY + + def test_name(self): + assert PROVIDER_REGISTRY["featherless"].name == "Featherless" + + def test_auth_type(self): + assert PROVIDER_REGISTRY["featherless"].auth_type == "api_key" + + def test_inference_base_url(self): + assert PROVIDER_REGISTRY["featherless"].inference_base_url == "https://api.featherless.ai/v1" + + def test_api_key_env_vars(self): + assert PROVIDER_REGISTRY["featherless"].api_key_env_vars == ("FEATHERLESS_API_KEY",) + + def test_base_url_env_var(self): + assert PROVIDER_REGISTRY["featherless"].base_url_env_var == "FEATHERLESS_BASE_URL" + + +# ============================================================================= +# Aliases +# ============================================================================= + + +class TestFeatherlessAliases: + @pytest.mark.parametrize("alias", ["featherless", "featherless-ai", "featherlessai"]) + def test_alias_resolves(self, alias, monkeypatch): + for key in _OTHER_PROVIDER_KEYS + ("OPENROUTER_API_KEY",): + monkeypatch.delenv(key, raising=False) + monkeypatch.setenv("FEATHERLESS_API_KEY", "fl-test-12345") + assert resolve_provider(alias) == "featherless" + + def test_normalize_provider_models_py(self): + from hermes_cli.models import normalize_provider + assert normalize_provider("featherless-ai") == "featherless" + assert normalize_provider("featherlessai") == "featherless" + + def test_normalize_provider_providers_py(self): + from hermes_cli.providers import normalize_provider + assert normalize_provider("featherless-ai") == "featherless" + assert normalize_provider("featherlessai") == "featherless" + + +# ============================================================================= +# Credentials +# ============================================================================= + + +class TestFeatherlessCredentials: + def test_status_configured(self, monkeypatch): + monkeypatch.setenv("FEATHERLESS_API_KEY", "fl-test") + status = get_api_key_provider_status("featherless") + assert status["configured"] + + def test_status_not_configured(self, monkeypatch): + monkeypatch.delenv("FEATHERLESS_API_KEY", raising=False) + status = get_api_key_provider_status("featherless") + assert not status["configured"] + + def test_openrouter_key_does_not_make_featherless_configured(self, monkeypatch): + """OpenRouter users should NOT see featherless as configured.""" + monkeypatch.delenv("FEATHERLESS_API_KEY", raising=False) + monkeypatch.setenv("OPENROUTER_API_KEY", "sk-or-test") + status = get_api_key_provider_status("featherless") + assert not status["configured"] + + def test_resolve_credentials(self, monkeypatch): + monkeypatch.setenv("FEATHERLESS_API_KEY", "fl-direct-key") + monkeypatch.delenv("FEATHERLESS_BASE_URL", raising=False) + creds = resolve_api_key_provider_credentials("featherless") + assert creds["api_key"] == "fl-direct-key" + assert creds["base_url"] == "https://api.featherless.ai/v1" + + def test_custom_base_url_override(self, monkeypatch): + monkeypatch.setenv("FEATHERLESS_API_KEY", "fl-x") + monkeypatch.setenv("FEATHERLESS_BASE_URL", "https://custom.featherless.example/v1") + creds = resolve_api_key_provider_credentials("featherless") + assert creds["base_url"] == "https://custom.featherless.example/v1" + + +# ============================================================================= +# Config registry (OPTIONAL_ENV_VARS) +# ============================================================================= + + +class TestFeatherlessConfigRegistry: + def test_optional_env_vars_include_featherless(self): + from hermes_cli.config import OPTIONAL_ENV_VARS + + assert "FEATHERLESS_API_KEY" in OPTIONAL_ENV_VARS + assert OPTIONAL_ENV_VARS["FEATHERLESS_API_KEY"]["category"] == "provider" + assert OPTIONAL_ENV_VARS["FEATHERLESS_API_KEY"]["password"] is True + assert OPTIONAL_ENV_VARS["FEATHERLESS_API_KEY"]["url"] == "https://featherless.ai/" + + assert "FEATHERLESS_BASE_URL" in OPTIONAL_ENV_VARS + assert OPTIONAL_ENV_VARS["FEATHERLESS_BASE_URL"]["category"] == "provider" + assert OPTIONAL_ENV_VARS["FEATHERLESS_BASE_URL"]["password"] is False + + +# ============================================================================= +# Model catalog +# ============================================================================= + + +class TestFeatherlessModelCatalog: + def test_static_model_list(self): + """Featherless has a static _PROVIDER_MODELS catalog entry. Specific + model names track upstream releases and don't belong in tests. + """ + from hermes_cli.models import _PROVIDER_MODELS + assert "featherless" in _PROVIDER_MODELS + assert len(_PROVIDER_MODELS["featherless"]) >= 1 + + def test_default_model_is_glm(self): + """The first catalog entry is the default offered on selection.""" + from hermes_cli.models import _PROVIDER_MODELS + assert _PROVIDER_MODELS["featherless"][0] == "zai-org/GLM-5.2" + + def test_canonical_provider_entry(self): + from hermes_cli.models import CANONICAL_PROVIDERS + slugs = [p.slug for p in CANONICAL_PROVIDERS] + assert "featherless" in slugs + + +# ============================================================================= +# URL mapping + provider prefixes +# ============================================================================= + + +class TestFeatherlessURLMapping: + def test_url_to_provider(self): + from agent.model_metadata import _URL_TO_PROVIDER + assert _URL_TO_PROVIDER.get("api.featherless.ai") == "featherless" + + def test_provider_prefixes(self): + from agent.model_metadata import _PROVIDER_PREFIXES + assert "featherless" in _PROVIDER_PREFIXES + assert "featherless-ai" in _PROVIDER_PREFIXES + assert "featherlessai" in _PROVIDER_PREFIXES + + def test_trajectory_compressor_detects_featherless(self): + import trajectory_compressor as tc + comp = tc.TrajectoryCompressor.__new__(tc.TrajectoryCompressor) + comp.config = types.SimpleNamespace(base_url="https://api.featherless.ai/v1") + assert comp._detect_provider() == "featherless" + + +# ============================================================================= +# providers.py overlay + label +# ============================================================================= + + +class TestFeatherlessProvidersModule: + def test_overlay_exists(self): + from hermes_cli.providers import HERMES_OVERLAYS + assert "featherless" in HERMES_OVERLAYS + overlay = HERMES_OVERLAYS["featherless"] + assert overlay.transport == "openai_chat" + assert overlay.base_url_env_var == "FEATHERLESS_BASE_URL" + assert not overlay.is_aggregator + + def test_label(self): + from hermes_cli.models import _PROVIDER_LABELS + assert _PROVIDER_LABELS["featherless"] == "Featherless" + + +# ============================================================================= +# Doctor +# ============================================================================= + + +class TestFeatherlessDoctor: + def test_provider_env_hints_include_featherless(self): + from hermes_cli.doctor import _PROVIDER_ENV_HINTS + assert "FEATHERLESS_API_KEY" in _PROVIDER_ENV_HINTS + + +# ============================================================================= +# Auxiliary client — main-model-first design +# ============================================================================= + + +class TestFeatherlessAuxiliary: + def test_main_model_first_design(self): + """Featherless uses main-model-first — no entry in _API_KEY_PROVIDER_AUX_MODELS.""" + from agent.auxiliary_client import _API_KEY_PROVIDER_AUX_MODELS + assert "featherless" not in _API_KEY_PROVIDER_AUX_MODELS + + +# ============================================================================= +# Context length — provider-served value (from /v1/models) wins over native +# ============================================================================= + + +class TestFeatherlessContextLength: + def test_endpoint_served_context_wins_over_native(self): + """Featherless serves models at a capped context (e.g. zai-org/GLM-5.2 at + 256K, not its native 1M). The featherless branch in get_model_context_length + must prefer the /v1/models context_length over the hardcoded native fallback. + """ + from unittest.mock import patch + from agent.model_metadata import get_model_context_length + + with patch( + "agent.model_metadata.get_cached_context_length", return_value=None, + ), patch( + "agent.model_metadata.fetch_endpoint_model_metadata", + return_value={"zai-org/GLM-5.2": {"context_length": 262144}}, + ), patch( + "agent.models_dev.lookup_models_dev_context", return_value=None, + ), patch( + "agent.model_metadata.fetch_model_metadata", return_value={}, + ): + result = get_model_context_length( + "zai-org/GLM-5.2", + base_url="https://api.featherless.ai/v1", + api_key="fl-test-key", + provider="featherless", + ) + + # 262144 (served) — NOT the native 1,048,576 hardcoded for glm-5.2. + assert result == 262144 diff --git a/trajectory_compressor.py b/trajectory_compressor.py index 9dc3826a854d0..66627e5e150ec 100644 --- a/trajectory_compressor.py +++ b/trajectory_compressor.py @@ -454,6 +454,8 @@ def _detect_provider(self) -> str: return "kimi-coding" if base_url_host_matches(url, "arcee.ai"): return "arcee" + if base_url_host_matches(url, "featherless.ai"): + return "featherless" if base_url_host_matches(url, "minimaxi.com"): return "minimax-cn" if base_url_host_matches(url, "minimax.io"): diff --git a/website/docs/developer-guide/provider-runtime.md b/website/docs/developer-guide/provider-runtime.md index b412ff479a3af..81467cb874283 100644 --- a/website/docs/developer-guide/provider-runtime.md +++ b/website/docs/developer-guide/provider-runtime.md @@ -61,6 +61,7 @@ Current provider families include (see `plugins/model-providers/` for the comple - NVIDIA NIM - xAI (Grok) - Arcee +- Featherless - GMI Cloud - StepFun - Qwen OAuth diff --git a/website/docs/getting-started/quickstart.md b/website/docs/getting-started/quickstart.md index 630df6e2938ce..a45311abdb965 100644 --- a/website/docs/getting-started/quickstart.md +++ b/website/docs/getting-started/quickstart.md @@ -107,6 +107,7 @@ Good defaults: | **Kimi / Moonshot** | Moonshot-hosted coding and chat models | Set `KIMI_API_KEY` (or the Kimi-Coding-specific `KIMI_CODING_API_KEY`) | | **Kimi / Moonshot China** | China-region Moonshot endpoint | Set `KIMI_CN_API_KEY` | | **Arcee AI** | Trinity models | Set `ARCEEAI_API_KEY` | +| **Featherless** | Thousands of open-source models (HF repo IDs), e.g. `zai-org/GLM-5.2` | Set `FEATHERLESS_API_KEY` | | **GMI Cloud** | Multi-model direct API | Set `GMI_API_KEY` | | **MiniMax (OAuth)** | MiniMax frontier model via browser OAuth — no API key needed (model name in `hermes_cli/models.py` may change between releases) | `hermes model` → MiniMax (OAuth) | | **MiniMax** | International MiniMax endpoint | Set `MINIMAX_API_KEY` | diff --git a/website/docs/integrations/providers.md b/website/docs/integrations/providers.md index 6ab24d0a4216b..2edb180cb41e8 100644 --- a/website/docs/integrations/providers.md +++ b/website/docs/integrations/providers.md @@ -25,6 +25,7 @@ You need at least one way to connect to an LLM. Use `hermes model` to switch pro | **Kimi / Moonshot** | `KIMI_API_KEY` in `~/.hermes/.env` (provider: `kimi-coding`) | | **Kimi / Moonshot (China)** | `KIMI_CN_API_KEY` in `~/.hermes/.env` (provider: `kimi-coding-cn`; aliases: `kimi-cn`, `moonshot-cn`) | | **Arcee AI** | `ARCEEAI_API_KEY` in `~/.hermes/.env` (provider: `arcee`; aliases: `arcee-ai`, `arceeai`) | +| **Featherless** | `FEATHERLESS_API_KEY` in `~/.hermes/.env` (provider: `featherless`; aliases: `featherless-ai`, `featherlessai`) | | **GMI Cloud** | `GMI_API_KEY` in `~/.hermes/.env` (provider: `gmi`; aliases: `gmi-cloud`, `gmicloud`) | | **MiniMax** | `MINIMAX_API_KEY` in `~/.hermes/.env` (provider: `minimax`) | | **MiniMax China** | `MINIMAX_CN_API_KEY` in `~/.hermes/.env` (provider: `minimax-cn`) | @@ -254,6 +255,10 @@ hermes chat --provider tencent-tokenhub --model hy3-preview hermes chat --provider arcee --model trinity-large-thinking # Requires: ARCEEAI_API_KEY in ~/.hermes/.env +# Featherless (open-source models via Hugging Face repo IDs) +hermes chat --provider featherless --model zai-org/GLM-5.2 +# Requires: FEATHERLESS_API_KEY in ~/.hermes/.env + # GMI Cloud # Use the exact model ID returned by GMI's /v1/models endpoint. hermes chat --provider gmi --model zai-org/GLM-5.1-FP8 @@ -1522,7 +1527,7 @@ fallback_model: When activated, the fallback swaps the model and provider mid-session without losing your conversation. The chain is tried entry-by-entry; activation is one-shot per session. -Supported providers: `openrouter`, `nous`, `novita`, `openai-codex`, `copilot`, `copilot-acp`, `anthropic`, `gemini`, `google-gemini-cli`, `qwen-oauth`, `huggingface`, `zai`, `kimi-coding`, `kimi-coding-cn`, `minimax`, `minimax-cn`, `minimax-oauth`, `deepseek`, `nvidia`, `xai`, `xai-oauth`, `ollama-cloud`, `bedrock`, `azure-foundry`, `opencode-zen`, `opencode-go`, `kilocode`, `xiaomi`, `arcee`, `gmi`, `stepfun`, `lmstudio`, `alibaba`, `alibaba-coding-plan`, `tencent-tokenhub`, `custom`. +Supported providers: `openrouter`, `nous`, `novita`, `openai-codex`, `copilot`, `copilot-acp`, `anthropic`, `gemini`, `google-gemini-cli`, `qwen-oauth`, `huggingface`, `zai`, `kimi-coding`, `kimi-coding-cn`, `minimax`, `minimax-cn`, `minimax-oauth`, `deepseek`, `nvidia`, `xai`, `xai-oauth`, `ollama-cloud`, `bedrock`, `azure-foundry`, `opencode-zen`, `opencode-go`, `kilocode`, `xiaomi`, `arcee`, `featherless`, `gmi`, `stepfun`, `lmstudio`, `alibaba`, `alibaba-coding-plan`, `tencent-tokenhub`, `custom`. :::tip Fallback is configured exclusively through `config.yaml` — or interactively via `hermes fallback`. For full details on when it triggers, how the chain advances, and how it interacts with auxiliary tasks and delegation, see [Fallback Providers](/user-guide/features/fallback-providers). diff --git a/website/docs/reference/environment-variables.md b/website/docs/reference/environment-variables.md index 9e8220dd03781..a801c2f57b5b0 100644 --- a/website/docs/reference/environment-variables.md +++ b/website/docs/reference/environment-variables.md @@ -40,6 +40,8 @@ All variables go in `~/.hermes/.env`. You can also set them with `hermes config | `KIMI_CN_API_KEY` | Kimi / Moonshot China API key ([moonshot.cn](https://platform.moonshot.cn)) | | `ARCEEAI_API_KEY` | Arcee AI API key ([chat.arcee.ai](https://chat.arcee.ai/)) | | `ARCEE_BASE_URL` | Override Arcee base URL (default: `https://api.arcee.ai/api/v1`) | +| `FEATHERLESS_API_KEY` | Featherless API key ([featherless.ai](https://featherless.ai/)) | +| `FEATHERLESS_BASE_URL` | Override Featherless base URL (default: `https://api.featherless.ai/v1`) | | `GMI_API_KEY` | GMI Cloud API key ([gmicloud.ai](https://www.gmicloud.ai/)) | | `GMI_BASE_URL` | Override GMI Cloud base URL (default: `https://api.gmi-serving.com/v1`) | | `MINIMAX_API_KEY` | MiniMax API key — global endpoint ([minimax.io](https://www.minimax.io)). **Not used by `minimax-oauth`** (OAuth path uses browser login instead). | diff --git a/website/docs/user-guide/configuration.md b/website/docs/user-guide/configuration.md index 307ec5a2e454e..48845683958f0 100644 --- a/website/docs/user-guide/configuration.md +++ b/website/docs/user-guide/configuration.md @@ -946,7 +946,7 @@ Every model slot in Hermes — auxiliary tasks, compression, fallback — uses t When `base_url` is set, Hermes ignores the provider and calls that endpoint directly (using `api_key` or `OPENAI_API_KEY` for auth). When only `provider` is set, Hermes uses that provider's built-in auth and base URL. -Available providers for auxiliary tasks: `auto`, `main`, plus any provider in the [provider registry](/reference/environment-variables) — `openrouter`, `nous`, `openai-codex`, `copilot`, `copilot-acp`, `anthropic`, `gemini`, `google-gemini-cli`, `qwen-oauth`, `zai`, `kimi-coding`, `kimi-coding-cn`, `minimax`, `minimax-cn`, `minimax-oauth`, `deepseek`, `nvidia`, `xai`, `xai-oauth`, `ollama-cloud`, `alibaba`, `bedrock`, `huggingface`, `arcee`, `xiaomi`, `kilocode`, `opencode-zen`, `opencode-go`, `azure-foundry` — or any named custom provider from your `custom_providers` list (e.g. `provider: "beans"`). +Available providers for auxiliary tasks: `auto`, `main`, plus any provider in the [provider registry](/reference/environment-variables) — `openrouter`, `nous`, `openai-codex`, `copilot`, `copilot-acp`, `anthropic`, `gemini`, `google-gemini-cli`, `qwen-oauth`, `zai`, `kimi-coding`, `kimi-coding-cn`, `minimax`, `minimax-cn`, `minimax-oauth`, `deepseek`, `nvidia`, `xai`, `xai-oauth`, `ollama-cloud`, `alibaba`, `bedrock`, `huggingface`, `arcee`, `featherless`, `xiaomi`, `kilocode`, `opencode-zen`, `opencode-go`, `azure-foundry` — or any named custom provider from your `custom_providers` list (e.g. `provider: "beans"`). :::tip MiniMax OAuth `minimax-oauth` logs in via browser OAuth (no API key needed). Run `hermes model` and select **MiniMax (OAuth)** to authenticate. Auxiliary tasks use `MiniMax-M2.7-highspeed` automatically. See the [MiniMax OAuth guide](../guides/minimax-oauth.md). diff --git a/website/docs/user-guide/features/fallback-providers.md b/website/docs/user-guide/features/fallback-providers.md index dbe431fc1ea43..6d84006238235 100644 --- a/website/docs/user-guide/features/fallback-providers.md +++ b/website/docs/user-guide/features/fallback-providers.md @@ -74,6 +74,7 @@ Each entry requires both `provider` and `model`. Entries missing either field ar | Kilo Code | `kilocode` | `KILOCODE_API_KEY` | | Xiaomi MiMo | `xiaomi` | `XIAOMI_API_KEY` | | Arcee AI | `arcee` | `ARCEEAI_API_KEY` | +| Featherless | `featherless` | `FEATHERLESS_API_KEY` | | GMI Cloud | `gmi` | `GMI_API_KEY` | | Alibaba / DashScope | `alibaba` | `DASHSCOPE_API_KEY` | | Alibaba Coding Plan | `alibaba-coding-plan` | `ALIBABA_CODING_PLAN_API_KEY` (falls back to `DASHSCOPE_API_KEY`) |