From ddd37cf4d3dd449b00dd4da5c6800b2ae5e58699 Mon Sep 17 00:00:00 2001 From: teyrebaz33 Date: Thu, 12 Mar 2026 03:07:12 +0300 Subject: [PATCH] feat: add Google Gemini and Hugging Face as inference providers Closes #982 Add two new providers to PROVIDER_REGISTRY and setup wizard: Google Gemini: - Base URL: generativelanguage.googleapis.com/v1beta/openai (OpenAI-compatible) - Env vars: GEMINI_API_KEY, GOOGLE_API_KEY (fallback) - Free tier available at aistudio.google.com Hugging Face Inference API: - Base URL: api-inference.huggingface.co/v1 (OpenAI-compatible) - Env vars: HUGGINGFACE_API_KEY, HF_TOKEN (fallback) - Free tier available for many models Both follow the existing api_key auth pattern (Z.AI, Kimi, MiniMax). OAuth support is out of scope for this PR. Tests: 5 new tests in TestGeminiHuggingFaceProviders, 34 passed total. --- hermes_cli/auth.py | 16 +++++++++ hermes_cli/setup.py | 66 +++++++++++++++++++++++++++++++++- tests/hermes_cli/test_setup.py | 41 +++++++++++++++++++++ 3 files changed, 122 insertions(+), 1 deletion(-) diff --git a/hermes_cli/auth.py b/hermes_cli/auth.py index c90f77922..dcd5d3b1f 100644 --- a/hermes_cli/auth.py +++ b/hermes_cli/auth.py @@ -148,6 +148,22 @@ class ProviderConfig: api_key_env_vars=("MINIMAX_CN_API_KEY",), base_url_env_var="MINIMAX_CN_BASE_URL", ), + "gemini": ProviderConfig( + id="gemini", + name="Google Gemini", + auth_type="api_key", + inference_base_url="https://generativelanguage.googleapis.com/v1beta/openai", + api_key_env_vars=("GEMINI_API_KEY", "GOOGLE_API_KEY"), + base_url_env_var="GEMINI_BASE_URL", + ), + "huggingface": ProviderConfig( + id="huggingface", + name="Hugging Face Inference API", + auth_type="api_key", + inference_base_url="https://api-inference.huggingface.co/v1", + api_key_env_vars=("HUGGINGFACE_API_KEY", "HF_TOKEN"), + base_url_env_var="HUGGINGFACE_BASE_URL", + ), } diff --git a/hermes_cli/setup.py b/hermes_cli/setup.py index f533a9384..f9908bb45 100644 --- a/hermes_cli/setup.py +++ b/hermes_cli/setup.py @@ -627,6 +627,8 @@ def setup_model_provider(config: dict): "Kimi / Moonshot (Kimi coding models)", "MiniMax (global endpoint)", "MiniMax China (mainland China endpoint)", + "Google Gemini (free tier available)", + "Hugging Face Inference API (free tier available)", ] if keep_label: provider_choices.append(keep_label) @@ -1041,7 +1043,67 @@ def setup_model_provider(config: dict): _update_config_for_provider("minimax-cn", pconfig.inference_base_url) _set_model_provider(config, "minimax-cn", pconfig.inference_base_url) - # else: provider_idx == 9 (Keep current) — only shown when a provider already exists + elif provider_idx == 9: # Google Gemini + selected_provider = "gemini" + print() + print_header("Google Gemini API Key") + pconfig = PROVIDER_REGISTRY["gemini"] + print_info(f"Provider: {pconfig.name}") + print_info("Get your free API key at: https://aistudio.google.com/apikey") + print_info("Free tier available — no credit card required.") + print() + existing_key = get_env_value("GEMINI_API_KEY") or get_env_value("GOOGLE_API_KEY") + if existing_key: + print_info(f"Current: {existing_key[:8]}... (configured)") + if prompt_yes_no("Update API key?", False): + new_key = prompt(" Gemini API key", password=True) + if new_key: + save_env_value("GEMINI_API_KEY", new_key) + print_success("Gemini API key updated") + else: + api_key = prompt(" Gemini API key", password=True) + if api_key: + save_env_value("GEMINI_API_KEY", api_key) + print_success("Gemini API key saved") + else: + print_warning("Skipped - agent won't work without an API key") + if existing_custom: + save_env_value("OPENAI_BASE_URL", "") + save_env_value("OPENAI_API_KEY", "") + _update_config_for_provider("gemini", pconfig.inference_base_url) + _set_model_provider(config, "gemini", pconfig.inference_base_url) + + elif provider_idx == 10: # Hugging Face + selected_provider = "huggingface" + print() + print_header("Hugging Face Inference API Key") + pconfig = PROVIDER_REGISTRY["huggingface"] + print_info(f"Provider: {pconfig.name}") + print_info("Get your API token at: https://huggingface.co/settings/tokens") + print_info("Free tier available for many models.") + print() + existing_key = get_env_value("HUGGINGFACE_API_KEY") or get_env_value("HF_TOKEN") + if existing_key: + print_info(f"Current: {existing_key[:8]}... (configured)") + if prompt_yes_no("Update API key?", False): + new_key = prompt(" Hugging Face API token", password=True) + if new_key: + save_env_value("HUGGINGFACE_API_KEY", new_key) + print_success("Hugging Face API token updated") + else: + api_key = prompt(" Hugging Face API token", password=True) + if api_key: + save_env_value("HUGGINGFACE_API_KEY", api_key) + print_success("Hugging Face API token saved") + else: + print_warning("Skipped - agent won't work without an API token") + if existing_custom: + save_env_value("OPENAI_BASE_URL", "") + save_env_value("OPENAI_API_KEY", "") + _update_config_for_provider("huggingface", pconfig.inference_base_url) + _set_model_provider(config, "huggingface", pconfig.inference_base_url) + + # else: provider_idx == 11 (Keep current) — only shown when a provider already exists # ── OpenRouter API Key for tools (if not already set) ── # Tools (vision, web, MoA) use OpenRouter independently of the main provider. @@ -1055,6 +1117,8 @@ def setup_model_provider(config: dict): "kimi-coding", "minimax", "minimax-cn", + "gemini", + "huggingface", ) and not get_env_value("OPENROUTER_API_KEY"): print() print_header("OpenRouter API Key (for tools)") diff --git a/tests/hermes_cli/test_setup.py b/tests/hermes_cli/test_setup.py index 3c3de3208..901135f47 100644 --- a/tests/hermes_cli/test_setup.py +++ b/tests/hermes_cli/test_setup.py @@ -128,3 +128,44 @@ def test_custom_setup_clears_active_oauth_provider(tmp_path, monkeypatch): assert reloaded["model"]["provider"] == "custom" assert reloaded["model"]["base_url"] == "https://custom.example/v1" assert reloaded["model"]["default"] == "custom/model" + + +class TestGeminiHuggingFaceProviders: + """Tests for Gemini and HuggingFace provider registry entries.""" + + def test_gemini_in_provider_registry(self): + from hermes_cli.auth import PROVIDER_REGISTRY + assert "gemini" in PROVIDER_REGISTRY + p = PROVIDER_REGISTRY["gemini"] + assert p.auth_type == "api_key" + assert "googleapis.com" in p.inference_base_url + assert "GEMINI_API_KEY" in p.api_key_env_vars + + def test_huggingface_in_provider_registry(self): + from hermes_cli.auth import PROVIDER_REGISTRY + assert "huggingface" in PROVIDER_REGISTRY + p = PROVIDER_REGISTRY["huggingface"] + assert p.auth_type == "api_key" + assert "huggingface.co" in p.inference_base_url + assert "HUGGINGFACE_API_KEY" in p.api_key_env_vars + assert "HF_TOKEN" in p.api_key_env_vars + + def test_gemini_in_provider_choices(self): + """Gemini appears in the setup wizard provider list.""" + import hermes_cli.setup as setup_mod + import inspect + src = inspect.getsource(setup_mod.setup_model_provider) + assert "Google Gemini" in src + + def test_huggingface_in_provider_choices(self): + """HuggingFace appears in the setup wizard provider list.""" + import hermes_cli.setup as setup_mod + import inspect + src = inspect.getsource(setup_mod.setup_model_provider) + assert "Hugging Face" in src + + def test_gemini_google_api_key_fallback(self): + """GOOGLE_API_KEY is accepted as fallback for Gemini.""" + from hermes_cli.auth import PROVIDER_REGISTRY + p = PROVIDER_REGISTRY["gemini"] + assert "GOOGLE_API_KEY" in p.api_key_env_vars