From d3d1310986c1e3e1be614ab1875e3fa3e2bfcab8 Mon Sep 17 00:00:00 2001 From: Dinos Papakostas Date: Thu, 5 Feb 2026 18:49:43 +0800 Subject: [PATCH 1/3] feat(llm): support google genai models --- EvoScientist/config.py | 8 +++++++- EvoScientist/llm/models.py | 8 ++++++++ EvoScientist/onboard.py | 36 +++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/EvoScientist/config.py b/EvoScientist/config.py index 6b66559..2620043 100644 --- a/EvoScientist/config.py +++ b/EvoScientist/config.py @@ -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 diff --git a/EvoScientist/llm/models.py b/EvoScientist/llm/models.py index dda5e66..0c5a982 100644 --- a/EvoScientist/llm/models.py +++ b/EvoScientist/llm/models.py @@ -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: diff --git a/EvoScientist/onboard.py b/EvoScientist/onboard.py index 0b4729d..8c9a3e5 100644 --- a/EvoScientist/onboard.py +++ b/EvoScientist/onboard.py @@ -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: diff --git a/pyproject.toml b/pyproject.toml index 399899e..81c1f56 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,7 @@ dependencies = [ "langchain-anthropic>=1.3", "langchain-openai>=0.3", "langchain-nvidia-ai-endpoints>=0.3", + "langchain-google-genai>=2.0", "tavily-python>=0.7", "pyyaml>=6.0", "rich>=14.0", From 905dd5568a467aa8644d34de6dbccbd127e695ac Mon Sep 17 00:00:00 2001 From: Dinos Papakostas Date: Thu, 5 Feb 2026 18:52:22 +0800 Subject: [PATCH 2/3] chore(deps): bump langchain-google-genai to a more recent version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 81c1f56..25ace98 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ dependencies = [ "langchain-anthropic>=1.3", "langchain-openai>=0.3", "langchain-nvidia-ai-endpoints>=0.3", - "langchain-google-genai>=2.0", + "langchain-google-genai>=4.2", "tavily-python>=0.7", "pyyaml>=6.0", "rich>=14.0", From 10a63c785e7a79c37ac3fb570322662e6670731e Mon Sep 17 00:00:00 2001 From: Dinos Papakostas Date: Thu, 5 Feb 2026 18:57:56 +0800 Subject: [PATCH 3/3] test(llm): update tests --- tests/test_llm.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/tests/test_llm.py b/tests/test_llm.py index f6b512b..93cbfac 100644 --- a/tests/test_llm.py +++ b/tests/test_llm.py @@ -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."""