From b36c19a22ace681da4f82ead83881a371592a089 Mon Sep 17 00:00:00 2001 From: Xi Zhang <106144707+X-iZhang@users.noreply.github.com> Date: Mon, 7 Sep 2026 17:22:18 +0800 Subject: [PATCH] fix(llm): honor openrouter_app_title only alongside a custom referer (#453) OpenRouter keys app pages by HTTP-Referer; X-Title only renames that page. A custom openrouter_app_title on the default referer therefore renamed the shared EvoScientist app page for everyone. Force the default title whenever the resolved referer is the default, silently, so usage keeps being attributed to EvoScientist; a private fork still overrides both together. --- EvoScientist/config/settings.py | 4 +++- EvoScientist/llm/models.py | 5 +++++ tests/test_llm.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) 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