diff --git a/EvoScientist/config/settings.py b/EvoScientist/config/settings.py index bc8d9b5..979a8f6 100644 --- a/EvoScientist/config/settings.py +++ b/EvoScientist/config/settings.py @@ -334,7 +334,9 @@ class EvoScientistConfig: openrouter_anthropic_prompt_cache: bool = True # OpenRouter app attribution (issue #339). Sent only for the openrouter # provider; identifies EvoScientist in OpenRouter's app rankings/analytics. - # Override (e.g. a private fork) via these fields or their env vars. + # Override (e.g. a private fork) via these fields or their env vars. A custom + # title only takes effect together with a custom referer: OpenRouter keys app + # pages by referer, so a lone title would rename the shared EvoScientist page. # Defaults live in the module constants above (also imported by llm/models.py). openrouter_http_referer: str = OPENROUTER_DEFAULT_HTTP_REFERER openrouter_app_title: str = OPENROUTER_DEFAULT_APP_TITLE diff --git a/EvoScientist/llm/models.py b/EvoScientist/llm/models.py index 89c3758..df13d14 100644 --- a/EvoScientist/llm/models.py +++ b/EvoScientist/llm/models.py @@ -501,6 +501,11 @@ def get_chat_model( os.environ.get("EVOSCIENTIST_OPENROUTER_APP_TITLE", "").strip() or OPENROUTER_DEFAULT_APP_TITLE, ) + # OpenRouter keys app pages by HTTP-Referer and X-Title only renames that + # page, so a custom title on the default referer would rename the shared + # EvoScientist page for everyone. Honor it only with a custom referer. + if kwargs["app_url"] == OPENROUTER_DEFAULT_HTTP_REFERER: + kwargs["app_title"] = OPENROUTER_DEFAULT_APP_TITLE # app_categories must be a list[str] (langchain-openrouter joins it into # the X-OpenRouter-Categories header); split the comma-separated config # value and drop blanks so a stray comma/space can't emit an empty one. diff --git a/tests/test_llm.py b/tests/test_llm.py index 78298a3..d74a5a1 100644 --- a/tests/test_llm.py +++ b/tests/test_llm.py @@ -1,5 +1,6 @@ """Tests for EvoScientist LLM module.""" +import warnings from unittest.mock import patch import pytest @@ -864,6 +865,34 @@ class TestThirdPartyRouting: # An explicit list is preserved verbatim, not re-split. assert call_kwargs["app_categories"] == ["only-this"] + @pytest.mark.parametrize("source", ["env", "kwarg"]) + @patch("EvoScientist.llm.models.init_chat_model") + def test_openrouter_title_override_without_referer_falls_back_silently( + self, mock_init, monkeypatch, source + ): + """OpenRouter keys app pages by HTTP-Referer, so a custom title on the + default referer would rename the shared EvoScientist page. It is + replaced by the default title, without any user-facing warning, + whether the title came from the env (config) or an explicit kwarg.""" + mock_init.return_value = "mock_model" + monkeypatch.setenv("OPENROUTER_API_KEY", "or-key") + for _env in self._APP_ATTR_ENV: + monkeypatch.delenv(_env, raising=False) + extra = {} + if source == "env": + monkeypatch.setenv("EVOSCIENTIST_OPENROUTER_APP_TITLE", "Acme") + else: + extra["app_title"] = "Acme" + + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + get_chat_model("x-ai/grok-4.3", provider="openrouter", **extra) + + assert not [w for w in caught if "openrouter" in str(w.message).lower()] + call_kwargs = mock_init.call_args[1] + assert call_kwargs["app_url"] == "https://github.com/EvoScientist/EvoScientist" + assert call_kwargs["app_title"] == "EvoScientist" + @patch("EvoScientist.llm.models.init_chat_model") def test_non_openrouter_providers_get_no_app_attribution( self, mock_init, monkeypatch