From 4b648b3413d5c30d9508077cec58f1d9c5414f35 Mon Sep 17 00:00:00 2001 From: Ziheng Zhang <142805986+MuXinCG2004@users.noreply.github.com> Date: Tue, 17 Mar 2026 01:40:03 +0800 Subject: [PATCH] chore: add models (#30) Co-authored-by: Xi Zhang <106144707+X-iZhang@users.noreply.github.com> --- .env.example | 2 + EvoScientist/config/onboard.py | 80 +++++++++++++++++++++++++++++++++ EvoScientist/config/settings.py | 8 ++++ EvoScientist/llm/models.py | 17 ++++++- tests/test_llm.py | 30 +++++++++++++ 5 files changed, 135 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index e30a861..2f42db7 100644 --- a/.env.example +++ b/.env.example @@ -10,6 +10,8 @@ NVIDIA_API_KEY= # build.nvidia.com SILICONFLOW_API_KEY= # siliconflow.cn OPENROUTER_API_KEY= # openrouter.ai ZHIPU_API_KEY= # open.bigmodel.cn +VOLCENGINE_API_KEY= # volcengine.com (火山引擎) +DASHSCOPE_API_KEY= # dashscope.aliyuncs.com (阿里云) CUSTOM_OPENAI_API_KEY= # Third-party OpenAI-compatible endpoint CUSTOM_OPENAI_BASE_URL= # OpenAI-compatible base URL (optional) CUSTOM_ANTHROPIC_API_KEY= # Third-party Anthropic-compatible endpoint diff --git a/EvoScientist/config/onboard.py b/EvoScientist/config/onboard.py index bbf3c3a..cbe9e05 100644 --- a/EvoScientist/config/onboard.py +++ b/EvoScientist/config/onboard.py @@ -365,6 +365,66 @@ def validate_zhipu_key(api_key: str) -> tuple[bool, str]: return False, f"Error: {e}" +def validate_volcengine_key(api_key: str) -> tuple[bool, str]: + """Validate a Volcengine API key by making a test request. + + Returns: + Tuple of (is_valid, message). + """ + if not api_key: + return True, "Skipped (no key provided)" + + try: + import openai + + client = openai.OpenAI( + api_key=api_key, + base_url="https://ark.cn-beijing.volces.com/api/v3", + ) + client.models.list() + return True, "Valid" + except Exception as e: + error_str = str(e).lower() + if ( + "401" in error_str + or "unauthorized" in error_str + or "invalid" in error_str + or "authentication" in error_str + ): + return False, "Invalid API key" + return False, f"Error: {e}" + + +def validate_dashscope_key(api_key: str) -> tuple[bool, str]: + """Validate a DashScope API key by making a test request. + + Returns: + Tuple of (is_valid, message). + """ + if not api_key: + return True, "Skipped (no key provided)" + + try: + import openai + + client = openai.OpenAI( + api_key=api_key, + base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", + ) + client.models.list() + return True, "Valid" + except Exception as e: + error_str = str(e).lower() + if ( + "401" in error_str + or "unauthorized" in error_str + or "invalid" in error_str + or "authentication" 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. @@ -531,6 +591,14 @@ def _step_provider(config: EvoScientistConfig) -> str: title="ZhipuAI CodePlan (智谱代码计划 — GLM models for coding)", value="zhipu-code", ), + Choice( + title="Volcengine (火山引擎 — Doubao models)", + value="volcengine", + ), + Choice( + title="DashScope (阿里云 — Qwen models)", + value="dashscope", + ), Choice(title="Ollama (local models)", value="ollama"), Choice( title="OpenAI-compatible (third-party OpenAI endpoint)", @@ -599,6 +667,16 @@ def _provider_key_info(config: EvoScientistConfig, provider: str): config.zhipu_api_key or os.environ.get("ZHIPU_API_KEY", ""), validate_zhipu_key, ), + "volcengine": ( + "Volcengine", + config.volcengine_api_key or os.environ.get("VOLCENGINE_API_KEY", ""), + validate_volcengine_key, + ), + "dashscope": ( + "DashScope", + config.dashscope_api_key or os.environ.get("DASHSCOPE_API_KEY", ""), + validate_dashscope_key, + ), "custom-openai": ( "OpenAI-compatible", config.custom_openai_api_key or os.environ.get("CUSTOM_OPENAI_API_KEY", ""), @@ -2286,6 +2364,8 @@ def run_onboard(skip_validation: bool = False) -> bool: "openrouter": "openrouter_api_key", "zhipu": "zhipu_api_key", "zhipu-code": "zhipu_api_key", + "volcengine": "volcengine_api_key", + "dashscope": "dashscope_api_key", "custom-openai": "custom_openai_api_key", "custom-anthropic": "custom_anthropic_api_key", } diff --git a/EvoScientist/config/settings.py b/EvoScientist/config/settings.py index 25d970b..1be32ee 100644 --- a/EvoScientist/config/settings.py +++ b/EvoScientist/config/settings.py @@ -69,6 +69,8 @@ class EvoScientistConfig: siliconflow_api_key: str = "" openrouter_api_key: str = "" zhipu_api_key: str = "" + volcengine_api_key: str = "" + dashscope_api_key: str = "" custom_openai_api_key: str = "" custom_openai_base_url: str = "" custom_anthropic_api_key: str = "" @@ -343,6 +345,8 @@ _ENV_MAPPINGS = { "siliconflow_api_key": "SILICONFLOW_API_KEY", "openrouter_api_key": "OPENROUTER_API_KEY", "zhipu_api_key": "ZHIPU_API_KEY", + "volcengine_api_key": "VOLCENGINE_API_KEY", + "dashscope_api_key": "DASHSCOPE_API_KEY", "custom_openai_api_key": "CUSTOM_OPENAI_API_KEY", "custom_openai_base_url": "CUSTOM_OPENAI_BASE_URL", "custom_anthropic_api_key": "CUSTOM_ANTHROPIC_API_KEY", @@ -422,6 +426,10 @@ def apply_config_to_env(config: EvoScientistConfig) -> None: os.environ["OPENROUTER_API_KEY"] = config.openrouter_api_key if config.zhipu_api_key and not os.environ.get("ZHIPU_API_KEY"): os.environ["ZHIPU_API_KEY"] = config.zhipu_api_key + if config.volcengine_api_key and not os.environ.get("VOLCENGINE_API_KEY"): + os.environ["VOLCENGINE_API_KEY"] = config.volcengine_api_key + if config.dashscope_api_key and not os.environ.get("DASHSCOPE_API_KEY"): + os.environ["DASHSCOPE_API_KEY"] = config.dashscope_api_key if config.custom_openai_api_key and not os.environ.get("CUSTOM_OPENAI_API_KEY"): os.environ["CUSTOM_OPENAI_API_KEY"] = config.custom_openai_api_key if config.custom_openai_base_url and not os.environ.get("CUSTOM_OPENAI_BASE_URL"): diff --git a/EvoScientist/llm/models.py b/EvoScientist/llm/models.py index f9ec4e6..879ca40 100644 --- a/EvoScientist/llm/models.py +++ b/EvoScientist/llm/models.py @@ -2,8 +2,8 @@ This module provides a unified interface for creating chat model instances with support for multiple providers (Anthropic, OpenAI, Google GenAI, NVIDIA, -SiliconFlow, OpenRouter, ZhipuAI, Ollama, and custom OpenAI-compatible endpoints) and -convenient short names for common models. +SiliconFlow, OpenRouter, ZhipuAI, Volcengine, DashScope, Ollama, and custom +OpenAI-compatible endpoints) and convenient short names for common models. """ from __future__ import annotations @@ -67,6 +67,8 @@ _SILICONFLOW_BASE_URL = "https://api.siliconflow.cn/v1" _OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1" _ZHIPU_BASE_URL = "https://open.bigmodel.cn/api/paas/v4" _ZHIPU_CODE_BASE_URL = "https://open.bigmodel.cn/api/coding/paas/v4" +_VOLCENGINE_BASE_URL = "https://ark.cn-beijing.volces.com/api/v3" +_DASHSCOPE_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1" # Third-party providers routed through the OpenAI provider with a custom base_url. # Maps provider name → (base_url or None, env var for API key). @@ -75,6 +77,8 @@ _THIRD_PARTY_PROVIDERS: dict[str, tuple[str | None, str]] = { "openrouter": (_OPENROUTER_BASE_URL, "OPENROUTER_API_KEY"), "zhipu": (_ZHIPU_BASE_URL, "ZHIPU_API_KEY"), "zhipu-code": (_ZHIPU_CODE_BASE_URL, "ZHIPU_API_KEY"), + "volcengine": (_VOLCENGINE_BASE_URL, "VOLCENGINE_API_KEY"), + "dashscope": (_DASHSCOPE_BASE_URL, "DASHSCOPE_API_KEY"), "custom-openai": ( None, "CUSTOM_OPENAI_API_KEY", @@ -154,6 +158,15 @@ _MODEL_ENTRIES: list[tuple[str, str, str]] = [ ("glm-5", "glm-5", "zhipu"), ("glm-5-turbo", "glm-5-turbo", "zhipu"), ("glm-4.7", "glm-4.7", "zhipu"), + # Volcengine (火山引擎 — Doubao models) + ("doubao-seed-1.6", "doubao-seed-1.6", "volcengine"), + ("doubao-1.5-pro", "doubao-1.5-pro-256k", "volcengine"), + ("doubao-1.5-thinking-pro", "doubao-1.5-thinking-pro", "volcengine"), + # DashScope (阿里云 — Qwen models) + ("qwen3-coder", "qwen3-coder-plus", "dashscope"), + ("qwen3-235b", "qwen3-235b-a22b", "dashscope"), + ("qwen-max", "qwen-max", "dashscope"), + ("qwq-plus", "qwq-plus", "dashscope"), ] # Public dict for simple lookups (last entry wins for duplicate names). diff --git a/tests/test_llm.py b/tests/test_llm.py index 8370b97..702b6b4 100644 --- a/tests/test_llm.py +++ b/tests/test_llm.py @@ -34,6 +34,8 @@ class TestModelsRegistry: assert "openrouter" in providers assert "zhipu" in providers assert "zhipu-code" in providers + assert "volcengine" in providers + assert "dashscope" in providers def test_entries_are_valid_tuples(self): """Test that _MODEL_ENTRIES contains valid (name, model_id, provider) tuples.""" @@ -46,6 +48,8 @@ class TestModelsRegistry: "openrouter", "zhipu", "zhipu-code", + "volcengine", + "dashscope", "custom-openai", "custom-anthropic", } @@ -445,6 +449,32 @@ class TestThirdPartyRouting: call_kwargs = mock_init.call_args[1] assert "reasoning" not in call_kwargs + @patch("EvoScientist.llm.models.init_chat_model") + def test_volcengine_routes_through_openai(self, mock_init, monkeypatch): + """Volcengine provider should route through OpenAI with correct base_url.""" + mock_init.return_value = "mock_model" + monkeypatch.setenv("VOLCENGINE_API_KEY", "ve-key-123") + + get_chat_model("doubao-seed-1.6", provider="volcengine") + + call_kwargs = mock_init.call_args[1] + assert call_kwargs["model_provider"] == "openai" + assert call_kwargs["base_url"] == "https://ark.cn-beijing.volces.com/api/v3" + assert call_kwargs["api_key"] == "ve-key-123" + + @patch("EvoScientist.llm.models.init_chat_model") + def test_dashscope_routes_through_openai(self, mock_init, monkeypatch): + """DashScope provider should route through OpenAI with correct base_url.""" + mock_init.return_value = "mock_model" + monkeypatch.setenv("DASHSCOPE_API_KEY", "ds-key-456") + + get_chat_model("qwen-max", provider="dashscope") + + call_kwargs = mock_init.call_args[1] + assert call_kwargs["model_provider"] == "openai" + assert call_kwargs["base_url"] == "https://dashscope.aliyuncs.com/compatible-mode/v1" + assert call_kwargs["api_key"] == "ds-key-456" + # ============================================================================= # Test _apply_auto_config