Merge pull request #3 from EvoScientist/feat/google-genai

Add Google GenAI (Gemini) Support
This commit is contained in:
Xi Zhang
2026-02-05 11:08:39 +00:00
committed by GitHub
5 changed files with 68 additions and 3 deletions
+7 -1
View File
@@ -46,8 +46,10 @@ class EvoScientistConfig:
Attributes:
anthropic_api_key: Anthropic API key for Claude models.
openai_api_key: OpenAI API key for GPT models.
nvidia_api_key: NVIDIA API key for NVIDIA models.
google_api_key: Google API key for Gemini models.
tavily_api_key: Tavily API key for web search.
provider: Default LLM provider ('anthropic' or 'openai').
provider: Default LLM provider ('anthropic', 'openai', 'google-genai', or 'nvidia').
model: Default model name (short name or full ID).
default_mode: Default workspace mode ('daemon' or 'run').
default_workdir: Default workspace directory (empty = use ./workspace).
@@ -60,6 +62,7 @@ class EvoScientistConfig:
anthropic_api_key: str = ""
openai_api_key: str = ""
nvidia_api_key: str = ""
google_api_key: str = ""
tavily_api_key: str = ""
# LLM Settings
@@ -204,6 +207,7 @@ _ENV_MAPPINGS = {
"anthropic_api_key": "ANTHROPIC_API_KEY",
"openai_api_key": "OPENAI_API_KEY",
"nvidia_api_key": "NVIDIA_API_KEY",
"google_api_key": "GOOGLE_API_KEY",
"tavily_api_key": "TAVILY_API_KEY",
"default_mode": "EVOSCIENTIST_DEFAULT_MODE",
"default_workdir": "EVOSCIENTIST_WORKSPACE_DIR",
@@ -270,5 +274,7 @@ def apply_config_to_env(config: EvoScientistConfig) -> None:
os.environ["OPENAI_API_KEY"] = config.openai_api_key
if config.nvidia_api_key and not os.environ.get("NVIDIA_API_KEY"):
os.environ["NVIDIA_API_KEY"] = config.nvidia_api_key
if config.google_api_key and not os.environ.get("GOOGLE_API_KEY"):
os.environ["GOOGLE_API_KEY"] = config.google_api_key
if config.tavily_api_key and not os.environ.get("TAVILY_API_KEY"):
os.environ["TAVILY_API_KEY"] = config.tavily_api_key
+8
View File
@@ -23,6 +23,12 @@ MODELS: dict[str, tuple[str, str]] = {
"gpt-4o-mini": ("gpt-4o-mini", "openai"),
"o1": ("o1", "openai"),
"o1-mini": ("o1-mini", "openai"),
# Google GenAI
"gemini-3-pro": ("gemini-3-pro-preview", "google-genai"),
"gemini-3-flash": ("gemini-3-flash-preview", "google-genai"),
"gemini-2.5-flash": ("gemini-2.5-flash", "google-genai"),
"gemini-2.5-flash-lite": ("gemini-2.5-flash-lite", "google-genai"),
"gemini-2.5-pro": ("gemini-2.5-pro", "google-genai"),
# NVIDIA
"glm4.7": ("z-ai/glm4.7", "nvidia"),
"deepseek-v3.1": ("deepseek-ai/deepseek-v3.1-terminus", "nvidia"),
@@ -70,6 +76,8 @@ def get_chat_model(
provider = "anthropic"
elif model_id.startswith(("gpt-", "o1", "davinci", "text-")):
provider = "openai"
elif model_id.startswith("gemini"):
provider = "google-genai"
elif "/" in model_id:
provider = "nvidia"
else:
+35 -1
View File
@@ -172,6 +172,31 @@ def validate_nvidia_key(api_key: str) -> tuple[bool, str]:
return False, f"Error: {e}"
def validate_google_key(api_key: str) -> tuple[bool, str]:
"""Validate a Google GenAI API key by making a test request.
Args:
api_key: The API key to validate.
Returns:
Tuple of (is_valid, message).
"""
if not api_key:
return True, "Skipped (no key provided)"
try:
from google import genai
client = genai.Client(api_key=api_key)
# Make a minimal request to validate the key
list(client.models.list(config={"page_size": 1}))
return True, "Valid"
except Exception as e:
error_str = str(e).lower()
if "401" in error_str or "403" in error_str or "unauthorized" in error_str or "invalid" in error_str or "api key" in error_str:
return False, "Invalid API key"
return False, f"Error: {e}"
def validate_tavily_key(api_key: str) -> tuple[bool, str]:
"""Validate a Tavily API key by making a test request.
@@ -253,11 +278,12 @@ def _step_provider(config: EvoScientistConfig) -> str:
choices = [
Choice(title="Anthropic (Claude models)", value="anthropic"),
Choice(title="OpenAI (GPT models)", value="openai"),
Choice(title="Google GenAI (Gemini models)", value="google-genai"),
Choice(title="NVIDIA (GLM, MiniMax, Kimi, etc.)", value="nvidia"),
]
# Set default based on current config
default = config.provider if config.provider in ["anthropic", "openai", "nvidia"] else "anthropic"
default = config.provider if config.provider in ["anthropic", "openai", "google-genai", "nvidia"] else "anthropic"
provider = questionary.select(
"Select your LLM provider:",
@@ -296,6 +322,10 @@ def _step_provider_api_key(
key_name = "NVIDIA"
current = config.nvidia_api_key or os.environ.get("NVIDIA_API_KEY", "")
validate_fn = validate_nvidia_key
elif provider == "google-genai":
key_name = "Google"
current = config.google_api_key or os.environ.get("GOOGLE_API_KEY", "")
validate_fn = validate_google_key
else:
key_name = "OpenAI"
current = config.openai_api_key or os.environ.get("OPENAI_API_KEY", "")
@@ -658,6 +688,8 @@ def run_onboard(skip_validation: bool = False) -> bool:
config.anthropic_api_key = new_key
elif provider == "nvidia":
config.nvidia_api_key = new_key
elif provider == "google-genai":
config.google_api_key = new_key
else:
config.openai_api_key = new_key
else:
@@ -665,6 +697,8 @@ def run_onboard(skip_validation: bool = False) -> bool:
current = config.anthropic_api_key
elif provider == "nvidia":
current = config.nvidia_api_key
elif provider == "google-genai":
current = config.google_api_key
else:
current = config.openai_api_key
if not current:
+1
View File
@@ -21,6 +21,7 @@ dependencies = [
"langchain-anthropic>=1.3",
"langchain-openai>=0.3",
"langchain-nvidia-ai-endpoints>=0.3",
"langchain-google-genai>=4.2",
"tavily-python>=0.7",
"pyyaml>=6.0",
"rich>=14.0",
+17 -1
View File
@@ -43,7 +43,7 @@ class TestModelsRegistry:
model_id, provider = value
assert isinstance(model_id, str), f"model_id for '{name}' is not a string"
assert isinstance(provider, str), f"provider for '{name}' is not a string"
assert provider in ("anthropic", "openai", "nvidia"), f"Unknown provider for '{name}': {provider}"
assert provider in ("anthropic", "openai", "google-genai", "nvidia"), f"Unknown provider for '{name}': {provider}"
def test_anthropic_models_have_anthropic_provider(self):
"""Test that claude models use anthropic provider."""
@@ -57,6 +57,12 @@ class TestModelsRegistry:
if name.startswith(("gpt", "o1")):
assert provider == "openai", f"OpenAI model '{name}' doesn't use openai provider"
def test_google_models_have_google_provider(self):
"""Test that gemini models use google-genai provider."""
for name, (model_id, provider) in MODELS.items():
if name.startswith("gemini"):
assert provider == "google-genai", f"Google model '{name}' doesn't use google-genai provider"
# =============================================================================
# Test DEFAULT_MODEL
@@ -215,6 +221,16 @@ class TestGetChatModel:
call_kwargs = mock_init.call_args[1]
assert call_kwargs["model_provider"] == "openai"
@mock.patch("EvoScientist.llm.models.init_chat_model")
def test_infers_google_from_gemini_prefix(self, mock_init):
"""Test that google-genai is inferred from gemini prefix."""
mock_init.return_value = "mock_model"
get_chat_model("gemini-2.0-flash")
call_kwargs = mock_init.call_args[1]
assert call_kwargs["model_provider"] == "google-genai"
@mock.patch("EvoScientist.llm.models.init_chat_model")
def test_defaults_to_anthropic_for_unknown(self, mock_init):
"""Test that anthropic is default for unknown model prefixes."""