From b838e257ee8aa2895678e448fd44df5020bcc0bb Mon Sep 17 00:00:00 2001 From: Yijia-Xiao Date: Thu, 1 Oct 2026 01:51:02 +0000 Subject: [PATCH] fix(sentiment): send Jev requests to the endpoint TYPESAFE_BASE_URL names (#1416) - post screening reads TypeSafe's own TYPESAFE_BASE_URL and appends /v1/systemone, as the TypeSafe SDKs do, so Jev can be reached through OpenRouter; the key still turns screening on --- .env.example | 1 + README.md | 2 +- tests/test_post_screen.py | 21 +++++++++++++++++++++ tradingagents/agents/post_screen.py | 15 +++++++++++---- 4 files changed, 34 insertions(+), 5 deletions(-) diff --git a/.env.example b/.env.example index cea96cb0f..556bca125 100644 --- a/.env.example +++ b/.env.example @@ -25,6 +25,7 @@ NVIDIA_API_KEY= # TypeSafe Jev: screens StockTwits and Reddit posts for the Sentiment Analyst. Optional. #TYPESAFE_API_KEY= #TYPESAFE_DEFAULT_MODEL=jev-latest +#TYPESAFE_BASE_URL=https://openrouter.ai/api # Jev through OpenRouter, with an OpenRouter key above # Custom OpenAI-compatible endpoint (vLLM, LM Studio, llama.cpp). Local servers need no key. #OPENAI_COMPATIBLE_API_KEY= diff --git a/README.md b/README.md index 23ffca984..13e69d606 100644 --- a/README.md +++ b/README.md @@ -188,7 +188,7 @@ For local models, configure Ollama with `llm_provider: "ollama"`. The default en For any other OpenAI-compatible server (vLLM, LM Studio, llama.cpp, or a custom relay), use `llm_provider: "openai_compatible"` and set the endpoint via `backend_url` (or `TRADINGAGENTS_LLM_BACKEND_URL`), e.g. `http://localhost:8000/v1` for vLLM or `http://localhost:1234/v1` for LM Studio. The model is whatever your server serves. No key is needed for local servers; set `OPENAI_COMPATIBLE_API_KEY` when the endpoint requires one. -With `TYPESAFE_API_KEY` set, the Sentiment Analyst screens StockTwits and Reddit posts with TypeSafe's Jev before reading them. Posts that are not about the company are dropped, and each source opens with a count of the remaining posts by stance: bullish, bearish, neutral, or unclear. Without the key, posts pass through unscreened. `jev-latest` moves with new releases; set `TYPESAFE_DEFAULT_MODEL` to a versioned ID such as `jev-1.13.0` to hold it fixed across runs. +With `TYPESAFE_API_KEY` set, the Sentiment Analyst screens StockTwits and Reddit posts with TypeSafe's Jev before reading them. Posts that are not about the company are dropped, and each source opens with a count of the remaining posts by stance: bullish, bearish, neutral, or unclear. Without the key, posts pass through unscreened. `jev-latest` moves with new releases; set `TYPESAFE_DEFAULT_MODEL` to a versioned ID such as `jev-1.13.0` to hold it fixed across runs. To reach Jev through OpenRouter, put an OpenRouter key in `TYPESAFE_API_KEY` and set `TYPESAFE_BASE_URL=https://openrouter.ai/api`. Alternatively, copy `.env.example` to `.env` and fill in your keys: ```bash diff --git a/tests/test_post_screen.py b/tests/test_post_screen.py index 11c11fe42..59f48abca 100644 --- a/tests/test_post_screen.py +++ b/tests/test_post_screen.py @@ -45,6 +45,7 @@ def post(monkeypatch): monkeypatch.setattr(typesafe.time, "sleep", calls.sleeps.append) monkeypatch.setenv("TYPESAFE_API_KEY", "ts-test") monkeypatch.delenv("TYPESAFE_DEFAULT_MODEL", raising=False) + monkeypatch.delenv("TYPESAFE_BASE_URL", raising=False) return calls @@ -76,6 +77,26 @@ def test_the_model_follows_the_sdk_environment(post, monkeypatch): assert post[0][1]["json"]["model"] == "jev-1.13.0" +@pytest.mark.unit +@pytest.mark.parametrize("base", ["https://openrouter.ai/api", "https://openrouter.ai/api/"]) +def test_the_endpoint_follows_the_sdk_environment(post, monkeypatch, base): + """TypeSafe's SDKs read TYPESAFE_BASE_URL and append /v1/systemone; OpenRouter + serves Jev at https://openrouter.ai/api with an OpenRouter key (#1416).""" + monkeypatch.setenv("TYPESAFE_BASE_URL", base) + post.queue.append(_ok()) + + typesafe.system_one("s", QUESTIONS) + + assert post[0][0] == "https://openrouter.ai/api/v1/systemone" + + +@pytest.mark.unit +def test_a_base_url_without_a_key_does_not_turn_screening_on(monkeypatch): + monkeypatch.delenv("TYPESAFE_API_KEY", raising=False) + monkeypatch.setenv("TYPESAFE_BASE_URL", "https://openrouter.ai/api") + assert typesafe.jev_screen("AAPL") is None + + @pytest.mark.unit @pytest.mark.parametrize("transient", [ _Response(429), _Response(529), requests.ConnectionError(), requests.Timeout(), diff --git a/tradingagents/agents/post_screen.py b/tradingagents/agents/post_screen.py index 271c0009a..1d3d3a4b2 100644 --- a/tradingagents/agents/post_screen.py +++ b/tradingagents/agents/post_screen.py @@ -6,8 +6,9 @@ lean on the instrument's stock. Code turns the answers into what the Sentiment Analyst reads: posts that are clearly about something else are dropped, and a stance count over the rest heads the source's block. -Configured by TypeSafe's own SDK variables, ``TYPESAFE_API_KEY`` and -``TYPESAFE_DEFAULT_MODEL``. Without a key nothing here runs; if any request fails, the source's posts are kept unscreened and the +Configured by TypeSafe's own SDK variables, ``TYPESAFE_API_KEY``, +``TYPESAFE_DEFAULT_MODEL`` and ``TYPESAFE_BASE_URL`` (OpenRouter serves Jev at +``https://openrouter.ai/api``). Without a key nothing here runs; if any request fails, the source's posts are kept unscreened and the block says screening was unavailable. """ @@ -23,7 +24,7 @@ from tradingagents.agents.context import resolve_instrument_identity logger = logging.getLogger(__name__) -_URL = "https://api.typesafe.ai/v1/systemone" +_DEFAULT_BASE_URL = "https://api.typesafe.ai" _DEFAULT_MODEL = "jev-latest" _RETRY_STATUSES = (429, 529) # rate limited, overloaded: back off and retry _TRANSIENT = (requests.ConnectionError, requests.Timeout, requests.exceptions.ChunkedEncodingError) @@ -83,7 +84,7 @@ def system_one(state, questions: dict) -> dict[str, dict]: time.sleep(retry_after if retry_after is not None else backoff * random.uniform(0.8, 1.2)) backoff *= 2 try: - response = requests.post(_URL, json=body, headers=headers, timeout=_TIMEOUT) + response = requests.post(_url(), json=body, headers=headers, timeout=_TIMEOUT) except requests.RequestException as exc: failure, retry_after = type(exc).__name__, None if isinstance(exc, _TRANSIENT): @@ -123,6 +124,12 @@ def _stance(answer: dict) -> str: return choice +def _url() -> str: + """System One's endpoint: the base TypeSafe's SDKs read, plus /v1/systemone.""" + base = os.environ.get("TYPESAFE_BASE_URL") or _DEFAULT_BASE_URL + return base.rstrip("/") + "/v1/systemone" + + def jev_screen(ticker: str): """A post screen for the social fetchers, or None without a TypeSafe key.