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
This commit is contained in:
Yijia-Xiao
2026-10-03 21:27:43 +00:00
parent 9df071b25b
commit b838e257ee
4 changed files with 34 additions and 5 deletions
+1
View File
@@ -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=
+1 -1
View File
@@ -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
+21
View File
@@ -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(),
+11 -4
View File
@@ -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.