feat: add support for SiliconFlow API key and update model providers
This commit is contained in:
@@ -63,6 +63,7 @@ class EvoScientistConfig:
|
||||
openai_api_key: str = ""
|
||||
nvidia_api_key: str = ""
|
||||
google_api_key: str = ""
|
||||
siliconflow_api_key: str = ""
|
||||
tavily_api_key: str = ""
|
||||
|
||||
# LLM Settings
|
||||
@@ -213,6 +214,7 @@ _ENV_MAPPINGS = {
|
||||
"openai_api_key": "OPENAI_API_KEY",
|
||||
"nvidia_api_key": "NVIDIA_API_KEY",
|
||||
"google_api_key": "GOOGLE_API_KEY",
|
||||
"siliconflow_api_key": "SILICONFLOW_API_KEY",
|
||||
"tavily_api_key": "TAVILY_API_KEY",
|
||||
"default_mode": "EVOSCIENTIST_DEFAULT_MODE",
|
||||
"default_workdir": "EVOSCIENTIST_WORKSPACE_DIR",
|
||||
@@ -281,5 +283,7 @@ def apply_config_to_env(config: EvoScientistConfig) -> None:
|
||||
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.siliconflow_api_key and not os.environ.get("SILICONFLOW_API_KEY"):
|
||||
os.environ["SILICONFLOW_API_KEY"] = config.siliconflow_api_key
|
||||
if config.tavily_api_key and not os.environ.get("TAVILY_API_KEY"):
|
||||
os.environ["TAVILY_API_KEY"] = config.tavily_api_key
|
||||
|
||||
@@ -7,10 +7,13 @@ short names for common models.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
from langchain.chat_models import init_chat_model
|
||||
|
||||
_SILICONFLOW_BASE_URL = "https://api.siliconflow.cn/v1"
|
||||
|
||||
# Model registry: short_name -> (model_id, provider)
|
||||
MODELS: dict[str, tuple[str, str]] = {
|
||||
# Anthropic (ordered by capability)
|
||||
@@ -20,11 +23,11 @@ MODELS: dict[str, tuple[str, str]] = {
|
||||
"claude-haiku-4-5": ("claude-haiku-4-5-20251001", "anthropic"),
|
||||
# OpenAI
|
||||
"gpt-5.2-codex": ("gpt-5.2-codex", "openai"),
|
||||
"gpt-5.2": ("gpt-5.2", "openai"),
|
||||
"gpt-5.1": ("gpt-5.1", "openai"),
|
||||
"gpt-5": ("gpt-5", "openai"),
|
||||
"gpt-5-mini": ("gpt-5-mini", "openai"),
|
||||
"gpt-5-nano": ("gpt-5-nano", "openai"),
|
||||
"gpt-5.2": ("gpt-5.2-2025-12-11", "openai"),
|
||||
"gpt-5.1": ("gpt-5.1-2025-11-13", "openai"),
|
||||
"gpt-5": ("gpt-5-2025-08-07", "openai"),
|
||||
"gpt-5-mini": ("gpt-5-mini-2025-08-07", "openai"),
|
||||
"gpt-5-nano": ("gpt-5-nano-2025-08-07", "openai"),
|
||||
# Google GenAI
|
||||
"gemini-3-pro": ("gemini-3-pro-preview", "google-genai"),
|
||||
"gemini-3-flash": ("gemini-3-flash-preview", "google-genai"),
|
||||
@@ -33,8 +36,15 @@ MODELS: dict[str, tuple[str, str]] = {
|
||||
"gemini-2.5-pro": ("gemini-2.5-pro", "google-genai"),
|
||||
# NVIDIA
|
||||
"glm4.7": ("z-ai/glm4.7", "nvidia"),
|
||||
"deepseek-v3.2": ("deepseek-ai/deepseek-v3.2", "nvidia"),
|
||||
"deepseek-v3.1": ("deepseek-ai/deepseek-v3.1-terminus", "nvidia"),
|
||||
"kimi-k2.5": ("moonshotai/kimi-k2.5", "nvidia"),
|
||||
"kimi-k2-thinking": ("moonshotai/kimi-k2-thinking", "nvidia"),
|
||||
"minimax-m2.1": ("minimaxai/minimax-m2.1", "nvidia"),
|
||||
"step-3.5-flash": ("stepfun-ai/step-3.5-flash", "nvidia"),
|
||||
"nemotron-nano": ("nvidia/nemotron-3-nano-30b-a3b", "nvidia"),
|
||||
# SiliconFlow (OpenAI-compatible API)
|
||||
"glm-5": ("Pro/zai-org/GLM-5", "siliconflow"),
|
||||
}
|
||||
|
||||
DEFAULT_MODEL = "claude-sonnet-4-5"
|
||||
@@ -85,12 +95,24 @@ def get_chat_model(
|
||||
else:
|
||||
provider = "anthropic" # Default fallback
|
||||
|
||||
# SiliconFlow → route through OpenAI provider with base_url
|
||||
_is_siliconflow = provider == "siliconflow"
|
||||
if _is_siliconflow:
|
||||
kwargs["base_url"] = _SILICONFLOW_BASE_URL
|
||||
api_key = os.environ.get("SILICONFLOW_API_KEY", "")
|
||||
if api_key:
|
||||
kwargs["api_key"] = api_key
|
||||
provider = "openai"
|
||||
|
||||
# Auto-enable thinking for Anthropic models
|
||||
if provider == "anthropic" and "thinking" not in kwargs:
|
||||
kwargs["thinking"] = {"type": "enabled", "budget_tokens": 2000}
|
||||
if model_id.startswith("claude-opus-4-6"):
|
||||
kwargs["thinking"] = {"type": "adaptive"}
|
||||
else:
|
||||
kwargs["thinking"] = {"type": "enabled", "budget_tokens": 2000}
|
||||
|
||||
# Auto-enable reasoning for OpenAI models
|
||||
if provider == "openai" and "reasoning" not in kwargs:
|
||||
# Auto-enable reasoning for OpenAI models (not for SiliconFlow)
|
||||
if provider == "openai" and not _is_siliconflow and "reasoning" not in kwargs:
|
||||
kwargs["reasoning"] = {"effort": "medium", "summary": "auto"}
|
||||
|
||||
# Auto-enable thinking visibility for Google GenAI models
|
||||
|
||||
+32
-5
@@ -240,6 +240,27 @@ def validate_google_key(api_key: str) -> tuple[bool, str]:
|
||||
return False, f"Error: {e}"
|
||||
|
||||
|
||||
def validate_siliconflow_key(api_key: str) -> tuple[bool, str]:
|
||||
"""Validate a SiliconFlow 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://api.siliconflow.cn/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.
|
||||
|
||||
@@ -322,11 +343,12 @@ def _step_provider(config: EvoScientistConfig) -> str:
|
||||
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"),
|
||||
Choice(title="NVIDIA (DeepSeek, Kimi, GLM, MiniMax, Step, etc.)", value="nvidia"),
|
||||
Choice(title="SiliconFlow (GLM, DeepSeek, Qwen, etc.)", value="siliconflow"),
|
||||
]
|
||||
|
||||
# Set default based on current config
|
||||
default = config.provider if config.provider in ["anthropic", "openai", "google-genai", "nvidia"] else "anthropic"
|
||||
default = config.provider if config.provider in ["anthropic", "openai", "google-genai", "nvidia", "siliconflow"] else "anthropic"
|
||||
|
||||
provider = questionary.select(
|
||||
"Select your LLM provider:",
|
||||
@@ -346,9 +368,10 @@ def _step_provider(config: EvoScientistConfig) -> str:
|
||||
def _provider_key_info(config: EvoScientistConfig, provider: str):
|
||||
"""Return (display_name, current_value, validate_fn) for a provider."""
|
||||
mapping = {
|
||||
"anthropic": ("Anthropic", config.anthropic_api_key or os.environ.get("ANTHROPIC_API_KEY", ""), validate_anthropic_key),
|
||||
"nvidia": ("NVIDIA", config.nvidia_api_key or os.environ.get("NVIDIA_API_KEY", ""), validate_nvidia_key),
|
||||
"google-genai": ("Google", config.google_api_key or os.environ.get("GOOGLE_API_KEY", ""), validate_google_key),
|
||||
"anthropic": ("Anthropic", config.anthropic_api_key or os.environ.get("ANTHROPIC_API_KEY", ""), validate_anthropic_key),
|
||||
"nvidia": ("NVIDIA", config.nvidia_api_key or os.environ.get("NVIDIA_API_KEY", ""), validate_nvidia_key),
|
||||
"google-genai": ("Google", config.google_api_key or os.environ.get("GOOGLE_API_KEY", ""), validate_google_key),
|
||||
"siliconflow": ("SiliconFlow", config.siliconflow_api_key or os.environ.get("SILICONFLOW_API_KEY", ""), validate_siliconflow_key),
|
||||
}
|
||||
return mapping.get(provider, ("OpenAI", config.openai_api_key or os.environ.get("OPENAI_API_KEY", ""), validate_openai_key))
|
||||
|
||||
@@ -1337,6 +1360,8 @@ def run_onboard(skip_validation: bool = False) -> bool:
|
||||
config.nvidia_api_key = new_key
|
||||
elif provider == "google-genai":
|
||||
config.google_api_key = new_key
|
||||
elif provider == "siliconflow":
|
||||
config.siliconflow_api_key = new_key
|
||||
else:
|
||||
config.openai_api_key = new_key
|
||||
else:
|
||||
@@ -1346,6 +1371,8 @@ def run_onboard(skip_validation: bool = False) -> bool:
|
||||
current = config.nvidia_api_key
|
||||
elif provider == "google-genai":
|
||||
current = config.google_api_key
|
||||
elif provider == "siliconflow":
|
||||
current = config.siliconflow_api_key
|
||||
else:
|
||||
current = config.openai_api_key
|
||||
if not current:
|
||||
|
||||
+3
-3
@@ -37,7 +37,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", "google-genai", "nvidia"), f"Unknown provider for '{name}': {provider}"
|
||||
assert provider in ("anthropic", "openai", "google-genai", "nvidia", "siliconflow"), f"Unknown provider for '{name}': {provider}"
|
||||
|
||||
def test_anthropic_models_have_anthropic_provider(self):
|
||||
"""Test that claude models use anthropic provider."""
|
||||
@@ -116,7 +116,7 @@ class TestGetModelInfo:
|
||||
def test_returns_correct_info(self):
|
||||
"""Test that get_model_info returns correct info."""
|
||||
model_id, provider = get_model_info("gpt-5-nano")
|
||||
assert model_id == "gpt-5-nano"
|
||||
assert model_id == "gpt-5-nano-2025-08-07"
|
||||
assert provider == "openai"
|
||||
|
||||
|
||||
@@ -159,7 +159,7 @@ class TestGetChatModel:
|
||||
get_chat_model("gpt-5-mini")
|
||||
|
||||
call_kwargs = mock_init.call_args[1]
|
||||
assert call_kwargs["model"] == "gpt-5-mini"
|
||||
assert call_kwargs["model"] == "gpt-5-mini-2025-08-07"
|
||||
assert call_kwargs["model_provider"] == "openai"
|
||||
|
||||
@mock.patch("EvoScientist.llm.models.init_chat_model")
|
||||
|
||||
Reference in New Issue
Block a user