hermes-agent/tests/hermes_cli/test_gemini_provider.py
alt-glitch a1e667b9f2 fix(restructure): fix test regressions from import rewrite
Fix variable name breakage (run_agent, hermes_constants, etc.) where
import rewriter changed 'import X' to 'import hermes_agent.Y' but
test code still referenced 'X' as a variable name.

Fix package-vs-module confusion (cli.auth, cli.models, cli.ui) where
single files became directories.

Fix hardcoded file paths in tests pointing to old locations.
Fix tool registry to discover tools in subpackage directories.
Fix stale import in hermes_agent/tools/__init__.py.

Part of #14182, #14183
2026-04-23 12:05:10 +05:30

356 lines
15 KiB
Python

"""Tests for Google AI Studio (Gemini) provider integration."""
import os
import pytest
from unittest.mock import patch, MagicMock
from hermes_agent.cli.auth.auth import PROVIDER_REGISTRY, resolve_provider, resolve_api_key_provider_credentials
from hermes_agent.cli.models.models import _PROVIDER_MODELS, _PROVIDER_LABELS, _PROVIDER_ALIASES, normalize_provider
from hermes_agent.cli.models.normalize import normalize_model_for_provider, detect_vendor
from hermes_agent.providers.metadata import get_model_context_length
from hermes_agent.providers.metadata_dev import PROVIDER_TO_MODELS_DEV, list_agentic_models, _NOISE_PATTERNS
# ── Provider Registry ──
class TestGeminiProviderRegistry:
def test_gemini_in_registry(self):
assert "gemini" in PROVIDER_REGISTRY
def test_gemini_config(self):
pconfig = PROVIDER_REGISTRY["gemini"]
assert pconfig.id == "gemini"
assert pconfig.name == "Google AI Studio"
assert pconfig.auth_type == "api_key"
assert pconfig.inference_base_url == "https://generativelanguage.googleapis.com/v1beta"
def test_gemini_env_vars(self):
pconfig = PROVIDER_REGISTRY["gemini"]
assert pconfig.api_key_env_vars == ("GOOGLE_API_KEY", "GEMINI_API_KEY")
assert pconfig.base_url_env_var == "GEMINI_BASE_URL"
def test_gemini_base_url(self):
assert "generativelanguage.googleapis.com" in PROVIDER_REGISTRY["gemini"].inference_base_url
# ── Provider Aliases ──
PROVIDER_ENV_VARS = (
"OPENROUTER_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY",
"GOOGLE_API_KEY", "GEMINI_API_KEY", "GEMINI_BASE_URL",
"GLM_API_KEY", "ZAI_API_KEY", "KIMI_API_KEY",
"MINIMAX_API_KEY", "DEEPSEEK_API_KEY",
)
@pytest.fixture(autouse=True)
def _clean_provider_env(monkeypatch):
for var in PROVIDER_ENV_VARS:
monkeypatch.delenv(var, raising=False)
class TestGeminiAliases:
def test_explicit_gemini(self):
assert resolve_provider("gemini") == "gemini"
def test_alias_google(self):
assert resolve_provider("google") == "gemini"
def test_alias_google_gemini(self):
assert resolve_provider("google-gemini") == "gemini"
def test_alias_google_ai_studio(self):
assert resolve_provider("google-ai-studio") == "gemini"
def test_models_py_aliases(self):
assert _PROVIDER_ALIASES.get("google") == "gemini"
assert _PROVIDER_ALIASES.get("google-gemini") == "gemini"
assert _PROVIDER_ALIASES.get("google-ai-studio") == "gemini"
def test_normalize_provider(self):
assert normalize_provider("google") == "gemini"
assert normalize_provider("gemini") == "gemini"
assert normalize_provider("google-ai-studio") == "gemini"
# ── Auto-detection ──
class TestGeminiAutoDetection:
def test_auto_detects_google_api_key(self, monkeypatch):
monkeypatch.setenv("GOOGLE_API_KEY", "test-google-key")
assert resolve_provider("auto") == "gemini"
def test_auto_detects_gemini_api_key(self, monkeypatch):
monkeypatch.setenv("GEMINI_API_KEY", "test-gemini-key")
assert resolve_provider("auto") == "gemini"
def test_google_api_key_priority_over_gemini(self, monkeypatch):
monkeypatch.setenv("GOOGLE_API_KEY", "primary-key")
monkeypatch.setenv("GEMINI_API_KEY", "alias-key")
creds = resolve_api_key_provider_credentials("gemini")
assert creds["api_key"] == "primary-key"
assert creds["source"] == "GOOGLE_API_KEY"
# ── Credential Resolution ──
class TestGeminiCredentials:
def test_resolve_with_google_api_key(self, monkeypatch):
monkeypatch.setenv("GOOGLE_API_KEY", "google-secret")
creds = resolve_api_key_provider_credentials("gemini")
assert creds["provider"] == "gemini"
assert creds["api_key"] == "google-secret"
assert creds["base_url"] == "https://generativelanguage.googleapis.com/v1beta"
def test_resolve_with_gemini_api_key(self, monkeypatch):
monkeypatch.setenv("GEMINI_API_KEY", "gemini-secret")
creds = resolve_api_key_provider_credentials("gemini")
assert creds["api_key"] == "gemini-secret"
def test_resolve_with_custom_base_url(self, monkeypatch):
monkeypatch.setenv("GOOGLE_API_KEY", "key")
monkeypatch.setenv("GEMINI_BASE_URL", "https://custom.endpoint/v1")
creds = resolve_api_key_provider_credentials("gemini")
assert creds["base_url"] == "https://custom.endpoint/v1"
def test_runtime_gemini(self, monkeypatch):
monkeypatch.setenv("GOOGLE_API_KEY", "google-key")
from hermes_agent.cli.runtime_provider import resolve_runtime_provider
result = resolve_runtime_provider(requested="gemini")
assert result["provider"] == "gemini"
assert result["api_mode"] == "chat_completions"
assert result["api_key"] == "google-key"
assert result["base_url"] == "https://generativelanguage.googleapis.com/v1beta"
# ── Model Catalog ──
class TestGeminiModelCatalog:
def test_provider_entry_exists(self):
"""Gemini provider has a model catalog entry. Specific model names
are data that changes with Google releases and don't belong in tests.
"""
assert "gemini" in _PROVIDER_MODELS
assert len(_PROVIDER_MODELS["gemini"]) >= 1
def test_provider_label(self):
assert "gemini" in _PROVIDER_LABELS
assert _PROVIDER_LABELS["gemini"] == "Google AI Studio"
# ── Model Normalization ──
class TestGeminiModelNormalization:
def test_passthrough_bare_name(self):
assert normalize_model_for_provider("gemini-2.5-flash", "gemini") == "gemini-2.5-flash"
def test_strip_vendor_prefix(self):
assert normalize_model_for_provider("google/gemini-2.5-flash", "gemini") == "google/gemini-2.5-flash"
def test_gemma_vendor_detection(self):
assert detect_vendor("gemma-4-31b-it") == "google"
def test_gemini_vendor_detection(self):
assert detect_vendor("gemini-2.5-flash") == "google"
def test_aggregator_prepends_vendor(self):
result = normalize_model_for_provider("gemini-2.5-flash", "openrouter")
assert result == "google/gemini-2.5-flash"
def test_gemma_aggregator_prepends_vendor(self):
result = normalize_model_for_provider("gemma-4-31b-it", "openrouter")
assert result == "google/gemma-4-31b-it"
# ── Context Length ──
class TestGeminiContextLength:
def test_gemma_4_31b_context(self):
# Mock external API lookups to test against hardcoded defaults
# (models.dev and OpenRouter may return different values like 262144).
with patch("hermes_agent.providers.metadata_dev.lookup_models_dev_context", return_value=None), \
patch("hermes_agent.providers.metadata.fetch_model_metadata", return_value={}):
ctx = get_model_context_length("gemma-4-31b-it", provider="gemini")
assert ctx == 256000
def test_gemini_3_context(self):
ctx = get_model_context_length("gemini-3.1-pro-preview", provider="gemini")
assert ctx == 1048576
# ── Agent Init (no SyntaxError) ──
class TestGeminiAgentInit:
def test_agent_imports_without_error(self):
"""Verify run_agent.py has no SyntaxError (the critical bug)."""
import importlib
from hermes_agent.agent import loop as run_agent
importlib.reload(run_agent)
def test_gemini_agent_uses_chat_completions(self, monkeypatch):
"""Gemini still reports chat_completions even though the transport is native."""
monkeypatch.setenv("GOOGLE_API_KEY", "test-key")
with patch("hermes_agent.providers.gemini_adapter.GeminiNativeClient") as mock_client:
mock_client.return_value = MagicMock()
from hermes_agent.agent.loop import AIAgent
agent = AIAgent(
model="gemini-2.5-flash",
provider="gemini",
api_key="test-key",
base_url="https://generativelanguage.googleapis.com/v1beta",
)
assert agent.api_mode == "chat_completions"
assert agent.provider == "gemini"
def test_gemini_agent_uses_native_client(self, monkeypatch):
monkeypatch.setenv("GOOGLE_API_KEY", "AIzaSy_REAL_KEY")
with patch("hermes_agent.providers.gemini_adapter.GeminiNativeClient") as mock_client, \
patch("hermes_agent.agent.loop.OpenAI") as mock_openai, \
patch("hermes_agent.agent.loop.ContextCompressor") as mock_compressor:
mock_client.return_value = MagicMock()
mock_compressor.return_value = MagicMock(context_length=1048576, threshold_tokens=524288)
from hermes_agent.agent.loop import AIAgent
AIAgent(
model="gemini-2.5-flash",
provider="gemini",
api_key="AIzaSy_REAL_KEY",
base_url="https://generativelanguage.googleapis.com/v1beta",
)
assert mock_client.called
mock_openai.assert_not_called()
def test_gemini_custom_base_url_keeps_openai_client(self, monkeypatch):
monkeypatch.setenv("GOOGLE_API_KEY", "AIzaSy_REAL_KEY")
with patch("hermes_agent.providers.gemini_adapter.GeminiNativeClient") as mock_client, \
patch("hermes_agent.agent.loop.OpenAI") as mock_openai, \
patch("hermes_agent.agent.loop.ContextCompressor") as mock_compressor:
mock_openai.return_value = MagicMock()
mock_compressor.return_value = MagicMock(context_length=128000, threshold_tokens=64000)
from hermes_agent.agent.loop import AIAgent
AIAgent(
model="gemini-2.5-flash",
provider="gemini",
api_key="AIzaSy_REAL_KEY",
base_url="https://proxy.example.com/v1",
)
mock_openai.assert_called_once()
def test_gemini_openai_compat_base_url_keeps_openai_client(self, monkeypatch):
monkeypatch.setenv("GOOGLE_API_KEY", "AIzaSy_REAL_KEY")
with patch("hermes_agent.providers.gemini_adapter.GeminiNativeClient") as mock_client, \
patch("hermes_agent.agent.loop.OpenAI") as mock_openai, \
patch("hermes_agent.agent.loop.ContextCompressor") as mock_compressor:
mock_openai.return_value = MagicMock()
mock_compressor.return_value = MagicMock(context_length=1048576, threshold_tokens=524288)
from hermes_agent.agent.loop import AIAgent
AIAgent(
model="gemini-2.5-flash",
provider="gemini",
api_key="AIzaSy_REAL_KEY",
base_url="https://generativelanguage.googleapis.com/v1beta/openai",
)
mock_openai.assert_called_once()
def test_gemini_resolve_provider_client_uses_native_client(self, monkeypatch):
"""resolve_provider_client('gemini') should build GeminiNativeClient."""
monkeypatch.setenv("GEMINI_API_KEY", "AIzaSy_TEST_KEY")
with patch("hermes_agent.providers.gemini_adapter.GeminiNativeClient") as mock_client, \
patch("hermes_agent.providers.auxiliary.OpenAI") as mock_openai:
mock_client.return_value = MagicMock()
from hermes_agent.providers.auxiliary import resolve_provider_client
resolve_provider_client("gemini")
assert mock_client.called
mock_openai.assert_not_called()
def test_gemini_resolve_provider_client_keeps_openai_for_non_native_base_url(self, monkeypatch):
monkeypatch.setenv("GOOGLE_API_KEY", "AIzaSy_TEST_KEY")
monkeypatch.setenv("GEMINI_BASE_URL", "https://proxy.example.com/v1")
with patch("hermes_agent.providers.gemini_adapter.GeminiNativeClient") as mock_client, \
patch("hermes_agent.providers.auxiliary.OpenAI") as mock_openai:
mock_openai.return_value = MagicMock()
from hermes_agent.providers.auxiliary import resolve_provider_client
resolve_provider_client("gemini")
mock_openai.assert_called_once()
# ── models.dev Integration ──
class TestGeminiModelsDev:
def test_gemini_mapped_to_google(self):
assert PROVIDER_TO_MODELS_DEV.get("gemini") == "google"
def test_noise_filter_excludes_tts(self):
assert _NOISE_PATTERNS.search("gemini-2.5-pro-preview-tts")
def test_noise_filter_excludes_dated_preview(self):
assert _NOISE_PATTERNS.search("gemini-2.5-flash-preview-04-17")
def test_noise_filter_excludes_embedding(self):
assert _NOISE_PATTERNS.search("gemini-embedding-001")
def test_noise_filter_excludes_live(self):
assert _NOISE_PATTERNS.search("gemini-live-2.5-flash")
def test_noise_filter_excludes_image(self):
assert _NOISE_PATTERNS.search("gemini-2.5-flash-image")
def test_noise_filter_excludes_customtools(self):
assert _NOISE_PATTERNS.search("gemini-3.1-pro-preview-customtools")
def test_noise_filter_passes_stable(self):
assert not _NOISE_PATTERNS.search("gemini-2.5-flash")
def test_noise_filter_passes_preview(self):
# Non-dated preview (e.g. gemini-3-flash-preview) should pass
assert not _NOISE_PATTERNS.search("gemini-3-flash-preview")
def test_noise_filter_passes_gemma(self):
assert not _NOISE_PATTERNS.search("gemma-4-31b-it")
def test_list_agentic_models_with_mock_data(self):
"""list_agentic_models filters correctly from mock models.dev data."""
mock_data = {
"google": {
"models": {
"gemini-3-flash-preview": {"tool_call": True},
"gemini-2.5-pro": {"tool_call": True},
"gemini-embedding-001": {"tool_call": False},
"gemini-2.5-flash-preview-tts": {"tool_call": False},
"gemini-live-2.5-flash": {"tool_call": True},
"gemini-2.5-flash-preview-04-17": {"tool_call": True},
"gemma-4-31b-it": {"tool_call": True},
}
}
}
with patch("hermes_agent.providers.metadata_dev.fetch_models_dev", return_value=mock_data):
result = list_agentic_models("gemini")
assert "gemini-3-flash-preview" in result
assert "gemini-2.5-pro" in result
assert "gemma-4-31b-it" not in result
# Filtered out:
assert "gemini-embedding-001" not in result # no tool_call
assert "gemini-2.5-flash-preview-tts" not in result # no tool_call
assert "gemini-live-2.5-flash" not in result # noise: live-
assert "gemini-2.5-flash-preview-04-17" not in result # noise: dated preview
def test_list_provider_models_hides_low_tpm_google_gemmas(self):
mock_data = {
"google": {
"models": {
"gemini-2.5-pro": {},
"gemma-4-31b-it": {},
"gemma-3-27b-it": {},
"gemini-1.5-pro": {},
"gemini-2.0-flash": {},
}
}
}
with patch("hermes_agent.providers.metadata_dev.fetch_models_dev", return_value=mock_data):
from hermes_agent.providers.metadata_dev import list_provider_models
result = list_provider_models("gemini")
assert "gemini-2.5-pro" in result
assert "gemma-4-31b-it" not in result
assert "gemma-3-27b-it" not in result
assert "gemini-1.5-pro" not in result
assert "gemini-2.0-flash" not in result