Merge pull request #3 from EvoScientist/feat/google-genai
Add Google GenAI (Gemini) Support
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user