diff --git a/hermes_cli/model_normalize.py b/hermes_cli/model_normalize.py index 31bd5ff3c5..21d10c5e6f 100644 --- a/hermes_cli/model_normalize.py +++ b/hermes_cli/model_normalize.py @@ -104,6 +104,7 @@ _MATCHING_PREFIX_STRIP_PROVIDERS: frozenset[str] = frozenset({ "xiaomi", "arcee", "ollama-cloud", + "nebius-token-factory", "custom", "gemini", "xai", diff --git a/hermes_cli/models.py b/hermes_cli/models.py index b545ffea65..f838df6247 100644 --- a/hermes_cli/models.py +++ b/hermes_cli/models.py @@ -1423,6 +1423,11 @@ _PROVIDER_ALIASES = { "actual-computer": "actual", "actualcomputer": "actual", "aci": "actual", + "nebius": "nebius-token-factory", + "nebius-tokenfactory": "nebius-token-factory", + "nebius-tf": "nebius-token-factory", + "token-factory": "nebius-token-factory", + "tokenfactory": "nebius-token-factory", "minimax-china": "minimax-cn", "minimax_cn": "minimax-cn", "minimax-portal": "minimax-oauth", diff --git a/hermes_cli/providers.py b/hermes_cli/providers.py index 2a02909ff6..caf6947e43 100644 --- a/hermes_cli/providers.py +++ b/hermes_cli/providers.py @@ -224,6 +224,12 @@ HERMES_OVERLAYS: Dict[str, HermesOverlay] = { base_url_override="https://api.upstage.ai/v1", base_url_env_var="UPSTAGE_BASE_URL", ), + "nebius-token-factory": HermesOverlay( + transport="openai_chat", + extra_env_vars=("NEBIUS_API_KEY", "NEBIUS_TOKEN_FACTORY_API_KEY"), + base_url_override="https://api.tokenfactory.nebius.com/v1", + base_url_env_var="NEBIUS_BASE_URL", + ), "ollama-cloud": HermesOverlay( transport="openai_chat", base_url_override="https://ollama.com/v1", @@ -404,6 +410,12 @@ ALIASES: Dict[str, str] = { "actual-computer": "actual", "actualcomputer": "actual", "aci": "actual", + # Nebius Token Factory + "nebius": "nebius-token-factory", + "nebius-tokenfactory": "nebius-token-factory", + "nebius-tf": "nebius-token-factory", + "token-factory": "nebius-token-factory", + "tokenfactory": "nebius-token-factory", # Local server aliases → virtual "local" concept (resolved via user config) "lmstudio": "lmstudio", @@ -432,6 +444,7 @@ _LABEL_OVERRIDES: Dict[str, str] = { "upstage": "Upstage Solar", "actual": "Actual Computer", "tencent-tokenhub": "Tencent TokenHub", + "nebius-token-factory": "Nebius Token Factory", "lmstudio": "LM Studio", "local": "Local endpoint", "bedrock": "AWS Bedrock", diff --git a/plugins/model-providers/nebius-token-factory/__init__.py b/plugins/model-providers/nebius-token-factory/__init__.py new file mode 100644 index 0000000000..19567c55f8 --- /dev/null +++ b/plugins/model-providers/nebius-token-factory/__init__.py @@ -0,0 +1,104 @@ +"""Nebius Token Factory provider profile.""" + +from __future__ import annotations + +from typing import Any + +from providers import register_provider +from providers.base import ProviderProfile + + +def _flat_model_name(model: str | None) -> str: + """Return a lowercase model id, tolerating vendor-prefixed IDs.""" + return (model or "").strip().rsplit("/", 1)[-1].lower() + + +def _model_supports_reasoning_effort(model: str | None) -> bool: + """Conservative allowlist for Nebius models that expose reasoning effort.""" + model_name = _flat_model_name(model) + if not model_name: + return False + return any( + marker in model_name + for marker in ( + "deepseek-r1", + "deepseek-v4", + "deepseek-reasoner", + "gpt-oss", + "glm-5", + "kimi-k2", + "minimax-m2", + "qwen3", + ) + ) + + +class NebiusTokenFactoryProfile(ProviderProfile): + """Nebius Token Factory - top-level reasoning_effort.""" + + def build_api_kwargs_extras( + self, + *, + reasoning_config: dict | None = None, + model: str | None = None, + supports_reasoning: bool = False, + **context: Any, + ) -> tuple[dict[str, Any], dict[str, Any]]: + if not supports_reasoning and not _model_supports_reasoning_effort(model): + return {}, {} + + if isinstance(reasoning_config, dict): + enabled = reasoning_config.get("enabled", True) + raw_effort = reasoning_config.get("effort", "medium") + else: + enabled = True + raw_effort = "medium" + + effort = str(raw_effort or "medium").strip().lower() + if enabled is False or effort in {"none", "off", "disabled"}: + return {}, {} + if effort in {"xhigh", "max"}: + effort = "high" + elif effort == "minimal": + effort = "low" + elif effort not in {"low", "medium", "high"}: + effort = "medium" + + return {}, {"reasoning_effort": effort} + + +nebius_token_factory = NebiusTokenFactoryProfile( + name="nebius-token-factory", + aliases=( + "nebius", + "nebius-tokenfactory", + "nebius-tf", + "token-factory", + "tokenfactory", + ), + display_name="Nebius Token Factory", + description="Nebius Token Factory — OpenAI-compatible inference", + signup_url="https://tokenfactory.nebius.com/", + env_vars=( + "NEBIUS_API_KEY", + "NEBIUS_TOKEN_FACTORY_API_KEY", + "NEBIUS_BASE_URL", + ), + base_url="https://api.tokenfactory.nebius.com/v1", + models_url="https://api.tokenfactory.nebius.com/v1/models", + auth_type="api_key", + default_aux_model="nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B", + fallback_models=( + "Qwen/Qwen3.5-397B-A17B-fast", + "deepseek-ai/DeepSeek-V4-Pro", + "zai-org/GLM-5.1", + "moonshotai/Kimi-K2.5-fast", + "MiniMaxAI/MiniMax-M2.5-fast", + "deepseek-ai/DeepSeek-V3.2-fast", + "NousResearch/Hermes-4-70B", + "openai/gpt-oss-120b-fast", + "meta-llama/Llama-3.3-70B-Instruct", + ), +) + +register_provider(nebius_token_factory) diff --git a/plugins/model-providers/nebius-token-factory/plugin.yaml b/plugins/model-providers/nebius-token-factory/plugin.yaml new file mode 100644 index 0000000000..b76acc5d55 --- /dev/null +++ b/plugins/model-providers/nebius-token-factory/plugin.yaml @@ -0,0 +1,5 @@ +name: nebius-token-factory-provider +kind: model-provider +version: 1.0.0 +description: Nebius Token Factory OpenAI-compatible inference +author: Nous Research diff --git a/tests/hermes_cli/test_nebius_token_factory_provider.py b/tests/hermes_cli/test_nebius_token_factory_provider.py new file mode 100644 index 0000000000..99983cfaeb --- /dev/null +++ b/tests/hermes_cli/test_nebius_token_factory_provider.py @@ -0,0 +1,230 @@ +"""Focused tests for Nebius Token Factory provider wiring.""" + +from __future__ import annotations + +from hermes_cli.auth import ( + PROVIDER_REGISTRY, + resolve_api_key_provider_credentials, + resolve_provider, +) +from hermes_cli.model_normalize import normalize_model_for_provider +from hermes_cli.models import ( + CANONICAL_PROVIDERS, + _PROVIDER_ALIASES, + _PROVIDER_LABELS, + normalize_provider, + provider_model_ids, +) + + +def test_nebius_provider_profile_loads(): + from providers import get_provider_profile + + profile = get_provider_profile("nebius-token-factory") + assert profile is not None + assert profile.name == "nebius-token-factory" + assert profile.display_name == "Nebius Token Factory" + assert profile.base_url == "https://api.tokenfactory.nebius.com/v1" + assert profile.models_url == "https://api.tokenfactory.nebius.com/v1/models" + assert profile.env_vars[:2] == ( + "NEBIUS_API_KEY", + "NEBIUS_TOKEN_FACTORY_API_KEY", + ) + assert profile.default_aux_model == "nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B" + assert "Qwen/Qwen3.5-397B-A17B-fast" in profile.fallback_models + + +def test_nebius_aliases_resolve(monkeypatch): + monkeypatch.setenv("NEBIUS_API_KEY", "nebius-test-key") + + for alias in ( + "nebius", + "nebius-tokenfactory", + "nebius-tf", + "token-factory", + "tokenfactory", + ): + assert resolve_provider(alias) == "nebius-token-factory" + assert normalize_provider(alias) == "nebius-token-factory" + assert _PROVIDER_ALIASES[alias] == "nebius-token-factory" + + +def test_nebius_provider_registry_and_credentials(monkeypatch): + monkeypatch.setenv("NEBIUS_API_KEY", "nebius-secret") + monkeypatch.setenv("NEBIUS_BASE_URL", "https://custom.nebius.example/v1") + + pconfig = PROVIDER_REGISTRY["nebius-token-factory"] + assert pconfig.id == "nebius-token-factory" + assert pconfig.name == "Nebius Token Factory" + assert pconfig.auth_type == "api_key" + assert pconfig.inference_base_url == "https://api.tokenfactory.nebius.com/v1" + assert pconfig.api_key_env_vars == ( + "NEBIUS_API_KEY", + "NEBIUS_TOKEN_FACTORY_API_KEY", + ) + assert pconfig.base_url_env_var == "NEBIUS_BASE_URL" + + creds = resolve_api_key_provider_credentials("nebius-token-factory") + assert creds["provider"] == "nebius-token-factory" + assert creds["api_key"] == "nebius-secret" + assert creds["base_url"] == "https://custom.nebius.example/v1" + + +def test_nebius_canonical_provider_and_label(): + slugs = [p.slug for p in CANONICAL_PROVIDERS] + assert "nebius-token-factory" in slugs + assert _PROVIDER_LABELS["nebius-token-factory"] == "Nebius Token Factory" + + +def test_nebius_provider_module_overlay(): + from hermes_cli.providers import ( + HERMES_OVERLAYS, + determine_api_mode, + get_label, + get_provider, + normalize_provider as normalize_provider_in_providers, + ) + + overlay = HERMES_OVERLAYS["nebius-token-factory"] + assert overlay.transport == "openai_chat" + assert overlay.base_url_override == "https://api.tokenfactory.nebius.com/v1" + assert overlay.base_url_env_var == "NEBIUS_BASE_URL" + + provider = get_provider("nebius") + assert provider is not None + assert provider.id == "nebius-token-factory" + assert provider.api_key_env_vars == ( + "NEBIUS_API_KEY", + "NEBIUS_TOKEN_FACTORY_API_KEY", + ) + assert provider.base_url == "https://api.tokenfactory.nebius.com/v1" + assert normalize_provider_in_providers("token-factory") == "nebius-token-factory" + assert get_label("nebius-token-factory") == "Nebius Token Factory" + assert determine_api_mode( + "nebius-token-factory", + "https://api.tokenfactory.nebius.com/v1", + ) == "chat_completions" + + +def test_nebius_model_catalog_prefers_live_profile_fetch(monkeypatch): + from providers import get_provider_profile + + profile = get_provider_profile("nebius-token-factory") + assert profile is not None + monkeypatch.setattr( + "hermes_cli.auth.resolve_api_key_provider_credentials", + lambda provider_id: { + "provider": provider_id, + "api_key": "nebius-live-key", + "base_url": "https://api.tokenfactory.nebius.com/v1", + "source": "NEBIUS_API_KEY", + }, + ) + monkeypatch.setattr( + profile, + "fetch_models", + lambda *, api_key=None, timeout=8.0: [ + "deepseek-ai/DeepSeek-V4-Pro", + "NousResearch/Hermes-4-70B", + ], + ) + + assert provider_model_ids("nebius-token-factory") == [ + "deepseek-ai/DeepSeek-V4-Pro", + "NousResearch/Hermes-4-70B", + ] + + +def test_nebius_model_catalog_falls_back_to_profile_models(monkeypatch): + from providers import get_provider_profile + + profile = get_provider_profile("nebius-token-factory") + assert profile is not None + monkeypatch.setattr( + "hermes_cli.auth.resolve_api_key_provider_credentials", + lambda provider_id: { + "provider": provider_id, + "api_key": "nebius-live-key", + "base_url": "https://api.tokenfactory.nebius.com/v1", + "source": "NEBIUS_API_KEY", + }, + ) + monkeypatch.setattr(profile, "fetch_models", lambda *, api_key=None, timeout=8.0: None) + + assert provider_model_ids("nebius") == list(profile.fallback_models) + + +def test_nebius_reasoning_models_emit_top_level_reasoning_effort(): + from providers import get_provider_profile + + profile = get_provider_profile("nebius-token-factory") + assert profile is not None + + extra_body, top_level = profile.build_api_kwargs_extras( + reasoning_config={"enabled": True, "effort": "xhigh"}, + model="openai/gpt-oss-120b-fast", + ) + assert extra_body == {} + assert top_level == {"reasoning_effort": "high"} + + +def test_nebius_reasoning_defaults_to_medium_for_known_reasoning_model(): + from providers import get_provider_profile + + profile = get_provider_profile("nebius-token-factory") + assert profile is not None + + extra_body, top_level = profile.build_api_kwargs_extras( + reasoning_config=None, + model="deepseek-ai/DeepSeek-V4-Pro", + ) + assert extra_body == {} + assert top_level == {"reasoning_effort": "medium"} + + +def test_nebius_reasoning_skips_disabled_and_non_reasoning_models(): + from providers import get_provider_profile + + profile = get_provider_profile("nebius-token-factory") + assert profile is not None + + assert profile.build_api_kwargs_extras( + reasoning_config={"enabled": False, "effort": "high"}, + model="deepseek-ai/DeepSeek-V4-Pro", + ) == ({}, {}) + assert profile.build_api_kwargs_extras( + reasoning_config={"enabled": True, "effort": "high"}, + model="meta-llama/Llama-3.3-70B-Instruct", + ) == ({}, {}) + + +def test_nebius_transport_emits_top_level_reasoning_effort(): + from agent.transports.chat_completions import ChatCompletionsTransport + from providers import get_provider_profile + + profile = get_provider_profile("nebius-token-factory") + assert profile is not None + + kwargs = ChatCompletionsTransport().build_kwargs( + model="deepseek-ai/DeepSeek-V4-Pro", + messages=[{"role": "user", "content": "ping"}], + tools=None, + provider_profile=profile, + reasoning_config={"enabled": True, "effort": "low"}, + base_url="https://api.tokenfactory.nebius.com/v1", + provider_name="nebius-token-factory", + ) + assert kwargs["reasoning_effort"] == "low" + assert "extra_body" not in kwargs + + +def test_nebius_model_normalization_strips_canonical_and_alias_prefixes(): + model = "Qwen/Qwen3.5-397B-A17B-fast" + assert normalize_model_for_provider( + f"nebius-token-factory/{model}", "nebius-token-factory" + ) == model + assert normalize_model_for_provider(f"nebius/{model}", "nebius-token-factory") == model + assert normalize_model_for_provider(f"nebius/{model}", "nebius") == model + assert normalize_model_for_provider( + "openai/gpt-oss-120b-fast", "nebius-token-factory" + ) == "openai/gpt-oss-120b-fast" diff --git a/tests/providers/test_plugin_discovery.py b/tests/providers/test_plugin_discovery.py index 2c828631bd..b725764512 100644 --- a/tests/providers/test_plugin_discovery.py +++ b/tests/providers/test_plugin_discovery.py @@ -70,6 +70,7 @@ def test_all_profiles_register(): for required in ( "openrouter", "anthropic", "custom", "bedrock", "openai-codex", "minimax-oauth", "gmi", "xiaomi", "alibaba-coding-plan", "fireworks", + "nebius-token-factory", ): assert required in names, f"Missing profile: {required}"