diff --git a/EvoScientist/config/settings.py b/EvoScientist/config/settings.py index d204968..01c5b3e 100644 --- a/EvoScientist/config/settings.py +++ b/EvoScientist/config/settings.py @@ -60,6 +60,7 @@ class EvoScientistConfig: # API Keys anthropic_api_key: str = "" + anthropic_base_url: str = "" openai_api_key: str = "" nvidia_api_key: str = "" google_api_key: str = "" @@ -329,6 +330,7 @@ def list_config() -> dict[str, Any]: # Environment variable mappings _ENV_MAPPINGS = { "anthropic_api_key": "ANTHROPIC_API_KEY", + "anthropic_base_url": "ANTHROPIC_BASE_URL", "openai_api_key": "OPENAI_API_KEY", "nvidia_api_key": "NVIDIA_API_KEY", "google_api_key": "GOOGLE_API_KEY", @@ -398,6 +400,8 @@ def apply_config_to_env(config: EvoScientistConfig) -> None: """ if config.anthropic_api_key and not os.environ.get("ANTHROPIC_API_KEY"): os.environ["ANTHROPIC_API_KEY"] = config.anthropic_api_key + if config.anthropic_base_url and not os.environ.get("ANTHROPIC_BASE_URL"): + os.environ["ANTHROPIC_BASE_URL"] = config.anthropic_base_url if config.openai_api_key and not os.environ.get("OPENAI_API_KEY"): os.environ["OPENAI_API_KEY"] = config.openai_api_key if config.nvidia_api_key and not os.environ.get("NVIDIA_API_KEY"): diff --git a/EvoScientist/llm/models.py b/EvoScientist/llm/models.py index 56ab2fc..3a03a85 100644 --- a/EvoScientist/llm/models.py +++ b/EvoScientist/llm/models.py @@ -196,9 +196,18 @@ def get_chat_model( else: provider = "anthropic" # Default fallback - # Third-party providers → route through OpenAI provider with base_url + # Anthropic base_url override (e.g. ccproxy at localhost:8000/api/v1) _is_third_party = provider in _THIRD_PARTY_PROVIDERS - if provider in _THIRD_PARTY_PROVIDERS: + if provider == "anthropic": + base_url = os.environ.get("ANTHROPIC_BASE_URL", "") + if base_url: + kwargs["base_url"] = base_url + api_key = os.environ.get("ANTHROPIC_API_KEY", "") + if api_key: + kwargs["api_key"] = api_key + + # Third-party providers → route through OpenAI provider with base_url + elif provider in _THIRD_PARTY_PROVIDERS: base_url_default, api_key_env = _THIRD_PARTY_PROVIDERS[provider] if provider == "custom": base_url = os.environ.get("CUSTOM_BASE_URL", "") diff --git a/tests/test_llm.py b/tests/test_llm.py index f4800b0..fe5ec87 100644 --- a/tests/test_llm.py +++ b/tests/test_llm.py @@ -392,6 +392,35 @@ class TestThirdPartyRouting: assert call_kwargs["base_url"] == "https://my-llm.example.com/v1" assert call_kwargs["api_key"] == "custom-key-789" + @patch("EvoScientist.llm.models.init_chat_model") + def test_anthropic_base_url_override(self, mock_init, monkeypatch): + """Anthropic provider should support base_url override (e.g. ccproxy).""" + mock_init.return_value = "mock_model" + monkeypatch.setenv("ANTHROPIC_BASE_URL", "http://localhost:8000/api/v1") + monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-dummy") + + get_chat_model("claude-sonnet-4-6", provider="anthropic") + + call_kwargs = mock_init.call_args[1] + assert call_kwargs["model_provider"] == "anthropic" + assert call_kwargs["base_url"] == "http://localhost:8000/api/v1" + assert call_kwargs["api_key"] == "sk-dummy" + # Should still get anthropic auto-config (thinking) + assert "thinking" in call_kwargs + + @patch("EvoScientist.llm.models.init_chat_model") + def test_anthropic_no_base_url_when_unset(self, mock_init, monkeypatch): + """Anthropic provider should not set base_url when env var is empty.""" + mock_init.return_value = "mock_model" + monkeypatch.delenv("ANTHROPIC_BASE_URL", raising=False) + monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-real") + + get_chat_model("claude-sonnet-4-6", provider="anthropic") + + call_kwargs = mock_init.call_args[1] + assert call_kwargs["model_provider"] == "anthropic" + assert "base_url" not in call_kwargs + @patch("EvoScientist.llm.models.init_chat_model") def test_third_party_no_reasoning(self, mock_init, monkeypatch): """Third-party providers routed through OpenAI should NOT get auto-reasoning."""