mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-11-06 01:17:15 +02:00
feat(llm): let each model tier run on its own provider (#1440)
- quick_think_provider / deep_think_provider and their backend URLs (with TRADINGAGENTS_* variables) choose a tier's provider; unset, a tier uses llm_provider and backend_url - a tier on another provider gets that provider's own settings and endpoint, and the run settings and report header name each tier's provider - the CLI checks each tier provider's model variable and key before the run
This commit is contained in:
@@ -42,6 +42,11 @@ NVIDIA_API_KEY=
|
||||
#TRADINGAGENTS_LLM_PROVIDER=openai
|
||||
#TRADINGAGENTS_DEEP_THINK_LLM=gpt-6-sol
|
||||
#TRADINGAGENTS_QUICK_THINK_LLM=gpt-6-luna
|
||||
# A tier on its own provider (set its model above too); its URL only for a local or relay endpoint.
|
||||
#TRADINGAGENTS_DEEP_THINK_PROVIDER=
|
||||
#TRADINGAGENTS_QUICK_THINK_PROVIDER=
|
||||
#TRADINGAGENTS_DEEP_THINK_BACKEND_URL=
|
||||
#TRADINGAGENTS_QUICK_THINK_BACKEND_URL=
|
||||
#TRADINGAGENTS_LLM_BACKEND_URL=
|
||||
#TRADINGAGENTS_OUTPUT_LANGUAGE=English
|
||||
#TRADINGAGENTS_MAX_DEBATE_ROUNDS=1
|
||||
|
||||
@@ -277,6 +277,15 @@ _, decision = ta.propagate("NVDA", "2026-09-01")
|
||||
print(decision)
|
||||
```
|
||||
|
||||
The quick model serves the analysts, researchers, debaters and trader; the deep model serves the research and portfolio managers. Each can run on its own provider, for example the managers on Claude while the rest run on OpenAI:
|
||||
|
||||
```python
|
||||
config["deep_think_provider"] = "anthropic"
|
||||
config["deep_think_llm"] = "claude-opus-5-5"
|
||||
```
|
||||
|
||||
A tier on its own provider uses that provider's key and default endpoint; set `quick_think_backend_url` or `deep_think_backend_url` for a local or relay endpoint. The `TRADINGAGENTS_DEEP_THINK_PROVIDER` and `TRADINGAGENTS_QUICK_THINK_PROVIDER` variables set them for the CLI, together with the tier's model variable.
|
||||
|
||||
See `tradingagents/default_config.py` for all configuration options.
|
||||
|
||||
### Fundamentals as filed
|
||||
|
||||
+25
-6
@@ -68,6 +68,29 @@ def unattended_gaps(flags) -> list[str]:
|
||||
return gaps
|
||||
|
||||
|
||||
def _check_tier_providers(main_provider: str) -> None:
|
||||
"""Check each provider the model tiers use (#1440) before the run.
|
||||
|
||||
A tier on another provider needs its model from its variable, since the
|
||||
model question offers the main provider's models. Each provider a tier uses
|
||||
has its key checked now, prompting for a missing one, rather than at its
|
||||
first call, which for the deep tier comes after everything else has been
|
||||
paid for; a provider no tier uses needs no key.
|
||||
"""
|
||||
used = []
|
||||
for tier in ("quick", "deep"):
|
||||
provider = (DEFAULT_CONFIG.get(f"{tier}_think_provider") or main_provider).lower()
|
||||
if provider != main_provider.lower():
|
||||
variable = f"TRADINGAGENTS_{tier.upper()}_THINK_LLM"
|
||||
if not os.environ.get(variable):
|
||||
console.print(f"[red]The {tier} tier runs on {provider}; set {variable} to one of its models.[/red]")
|
||||
raise typer.Exit(code=1)
|
||||
if provider not in used:
|
||||
used.append(provider)
|
||||
for provider in used:
|
||||
ensure_api_key(provider)
|
||||
|
||||
|
||||
def _from_flag(parse, value, *args):
|
||||
"""A flag's value through the same check its prompt applies; a bad one ends the run."""
|
||||
try:
|
||||
@@ -223,8 +246,6 @@ def _prompt_selections(prefs, flags):
|
||||
)
|
||||
console.print(f"[green]✓ LLM provider from environment:[/green] {selected_llm_provider}")
|
||||
console.print(f"[green]✓ Backend URL:[/green] {backend_url}")
|
||||
# Still confirm/persist the API key so the run doesn't fail later.
|
||||
ensure_api_key(selected_llm_provider)
|
||||
else:
|
||||
console.print(
|
||||
create_question_box(
|
||||
@@ -261,10 +282,8 @@ def _prompt_selections(prefs, flags):
|
||||
if selected_llm_provider == "ollama":
|
||||
confirm_ollama_endpoint(backend_url)
|
||||
|
||||
# Confirm the provider's API key is present; prompt the user to paste
|
||||
# one and persist it to .env if it's missing, so the analysis run
|
||||
# doesn't fail later at the first API call.
|
||||
ensure_api_key(selected_llm_provider)
|
||||
|
||||
_check_tier_providers(selected_llm_provider)
|
||||
|
||||
# Step 7: Thinking agents (skipped when either model is set via environment)
|
||||
if os.environ.get("TRADINGAGENTS_QUICK_THINK_LLM") or os.environ.get("TRADINGAGENTS_DEEP_THINK_LLM"):
|
||||
|
||||
@@ -185,3 +185,49 @@ def test_an_unknown_analyst_error_names_the_analysts_as_users_know_them():
|
||||
with pytest.raises(ValueError) as caught:
|
||||
prompts.parse_analysts("macro", AssetType.STOCK)
|
||||
assert "sentiment" in str(caught.value) and "social" not in str(caught.value)
|
||||
|
||||
|
||||
def _selections_with_env(monkeypatch, keys=None):
|
||||
for name, value in UNATTENDED_ENV.items():
|
||||
monkeypatch.setenv(name, value)
|
||||
monkeypatch.setattr(selections, "fetch_announcements", lambda: None)
|
||||
monkeypatch.setattr(selections, "display_announcements", lambda *a: None)
|
||||
checked = keys if keys is not None else []
|
||||
monkeypatch.setattr(selections, "ensure_api_key", lambda provider: checked.append(provider))
|
||||
return checked
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_a_tier_on_another_provider_needs_its_own_model(monkeypatch, capsys):
|
||||
"""Otherwise the CLI would take the main provider's model for it (#1440)."""
|
||||
_selections_with_env(monkeypatch)
|
||||
monkeypatch.setitem(selections.DEFAULT_CONFIG, "deep_think_provider", "anthropic")
|
||||
monkeypatch.delenv("TRADINGAGENTS_DEEP_THINK_LLM", raising=False)
|
||||
|
||||
with pytest.raises(typer.Exit):
|
||||
selections._prompt_selections({}, FLAGS)
|
||||
assert "TRADINGAGENTS_DEEP_THINK_LLM" in capsys.readouterr().out
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_each_tier_provider_s_key_is_checked_before_the_run(monkeypatch):
|
||||
checked = _selections_with_env(monkeypatch)
|
||||
monkeypatch.setitem(selections.DEFAULT_CONFIG, "deep_think_provider", "anthropic")
|
||||
monkeypatch.setenv("TRADINGAGENTS_DEEP_THINK_LLM", "claude-opus-5-5")
|
||||
|
||||
selections._prompt_selections({}, FLAGS)
|
||||
|
||||
assert checked == ["openai", "anthropic"]
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_only_the_providers_the_tiers_use_need_a_key(monkeypatch):
|
||||
"""Both tiers on a local Ollama: the main provider's key is not needed."""
|
||||
checked = _selections_with_env(monkeypatch)
|
||||
for tier in ("quick", "deep"):
|
||||
monkeypatch.setitem(selections.DEFAULT_CONFIG, f"{tier}_think_provider", "ollama")
|
||||
monkeypatch.setenv(f"TRADINGAGENTS_{tier.upper()}_THINK_LLM", "llama3.1")
|
||||
|
||||
selections._prompt_selections({}, FLAGS)
|
||||
|
||||
assert checked == ["ollama"]
|
||||
|
||||
@@ -124,7 +124,7 @@ def _graph(tmp_path, monkeypatch, model, debug=False, **config):
|
||||
cfg = copy.deepcopy(DEFAULT_CONFIG)
|
||||
cfg.update(results_dir=str(tmp_path / "results"), data_cache_dir=str(tmp_path / "cache"),
|
||||
memory_log_path=str(tmp_path / "log.md"), **config)
|
||||
monkeypatch.setattr(trading_graph, "create_llm_client", lambda **k: _Client(model))
|
||||
monkeypatch.setattr(trading_graph, "create_tier_client", lambda config, tier, **k: _Client(model))
|
||||
return trading_graph.TradingAgentsGraph(config=cfg, debug=debug)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
"""Each model tier can name its own provider (#1440).
|
||||
|
||||
The quick tier (analysts, researchers, debaters, trader) and the deep tier
|
||||
(research and portfolio managers) default to ``llm_provider``. A tier that names
|
||||
another provider gets that provider's own settings and endpoint, never the main
|
||||
provider's URL, and the run's settings and report say which provider each used.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from tradingagents import default_config
|
||||
from tradingagents.llm_clients import factory
|
||||
|
||||
|
||||
def _config(**overrides):
|
||||
config = {**default_config.DEFAULT_CONFIG, "llm_provider": "openai", "backend_url": "https://api.openai.com/v1",
|
||||
"quick_think_llm": "gpt-6-luna", "deep_think_llm": "gpt-6-sol",
|
||||
"openai_reasoning_effort": "low", "google_thinking_level": "high"}
|
||||
config.update(overrides)
|
||||
return config
|
||||
|
||||
|
||||
def _captured(monkeypatch):
|
||||
made = []
|
||||
monkeypatch.setattr(factory, "create_llm_client",
|
||||
lambda provider, model, base_url=None, **kw: made.append((provider, model, base_url, kw)))
|
||||
return made
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_a_tier_without_its_own_provider_uses_the_run_s(monkeypatch):
|
||||
made = _captured(monkeypatch)
|
||||
|
||||
factory.create_tier_client(_config(), "deep")
|
||||
|
||||
provider, model, base_url, kw = made[0]
|
||||
assert (provider, model, base_url) == ("openai", "gpt-6-sol", "https://api.openai.com/v1")
|
||||
assert kw["reasoning_effort"] == "low"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_a_tier_on_another_provider_gets_that_provider_s_settings_and_not_the_main_url(monkeypatch):
|
||||
made = _captured(monkeypatch)
|
||||
|
||||
factory.create_tier_client(_config(deep_think_provider="google", deep_think_llm="gemini-3.8-flash"), "deep")
|
||||
|
||||
provider, model, base_url, kw = made[0]
|
||||
assert (provider, model) == ("google", "gemini-3.8-flash")
|
||||
assert base_url is None # the OpenAI URL must not follow it
|
||||
assert kw["thinking_level"] == "high" and "reasoning_effort" not in kw
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_a_tier_url_is_used_for_that_tier_only(monkeypatch):
|
||||
made = _captured(monkeypatch)
|
||||
config = _config(quick_think_provider="openai_compatible", quick_think_backend_url="http://localhost:8000/v1",
|
||||
quick_think_llm="local-model")
|
||||
|
||||
factory.create_tier_client(config, "quick")
|
||||
factory.create_tier_client(config, "deep")
|
||||
|
||||
assert made[0][:3] == ("openai_compatible", "local-model", "http://localhost:8000/v1")
|
||||
assert made[1][:3] == ("openai", "gpt-6-sol", "https://api.openai.com/v1")
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_the_run_settings_and_report_name_each_tier_s_provider():
|
||||
from tradingagents.graph.trading_graph import TradingAgentsGraph
|
||||
from tradingagents.reporting import _header
|
||||
|
||||
graph = object.__new__(TradingAgentsGraph)
|
||||
graph.selected_analysts = ("market",)
|
||||
graph.config = _config(deep_think_provider="anthropic", deep_think_llm="claude-opus-5-5")
|
||||
|
||||
settings = graph.run_settings()
|
||||
assert settings["quick_think_provider"] == "openai" and settings["deep_think_provider"] == "anthropic"
|
||||
header = _header("NVDA", {}, settings)
|
||||
assert "deep anthropic claude-opus-5-5" in header and "quick openai gpt-6-luna" in header
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_the_tier_settings_are_read_from_the_environment(monkeypatch):
|
||||
monkeypatch.setenv("TRADINGAGENTS_DEEP_THINK_PROVIDER", "anthropic")
|
||||
monkeypatch.setenv("TRADINGAGENTS_QUICK_THINK_BACKEND_URL", "http://localhost:8000/v1")
|
||||
|
||||
config = default_config.build_default_config()
|
||||
|
||||
assert config["deep_think_provider"] == "anthropic"
|
||||
assert config["quick_think_backend_url"] == "http://localhost:8000/v1"
|
||||
assert config["quick_think_provider"] is None
|
||||
@@ -11,6 +11,10 @@ _ENV_OVERRIDES = {
|
||||
"TRADINGAGENTS_LLM_PROVIDER": "llm_provider",
|
||||
"TRADINGAGENTS_DEEP_THINK_LLM": "deep_think_llm",
|
||||
"TRADINGAGENTS_QUICK_THINK_LLM": "quick_think_llm",
|
||||
"TRADINGAGENTS_DEEP_THINK_PROVIDER": "deep_think_provider",
|
||||
"TRADINGAGENTS_QUICK_THINK_PROVIDER": "quick_think_provider",
|
||||
"TRADINGAGENTS_DEEP_THINK_BACKEND_URL": "deep_think_backend_url",
|
||||
"TRADINGAGENTS_QUICK_THINK_BACKEND_URL": "quick_think_backend_url",
|
||||
"TRADINGAGENTS_LLM_BACKEND_URL": "backend_url",
|
||||
"TRADINGAGENTS_OUTPUT_LANGUAGE": "output_language",
|
||||
"TRADINGAGENTS_MAX_DEBATE_ROUNDS": "max_debate_rounds",
|
||||
@@ -94,6 +98,13 @@ def build_default_config() -> dict:
|
||||
# provider-specific URL here would leak (e.g. OpenAI's /v1 was previously
|
||||
# being forwarded to Gemini, producing malformed request URLs).
|
||||
"backend_url": None,
|
||||
# A tier may name its own provider and endpoint (#1440): the quick tier serves
|
||||
# the analysts, researchers, debaters and trader, the deep tier the managers.
|
||||
# None means the tier uses llm_provider and backend_url.
|
||||
"quick_think_provider": None,
|
||||
"deep_think_provider": None,
|
||||
"quick_think_backend_url": None,
|
||||
"deep_think_backend_url": None,
|
||||
# Provider-specific thinking configuration
|
||||
"google_thinking_level": None, # "high", "minimal", etc.
|
||||
"openai_reasoning_effort": None, # "medium", "high", "low"
|
||||
|
||||
@@ -14,7 +14,7 @@ from tradingagents.dataflows.config import run_config, run_config_context, set_c
|
||||
from tradingagents.dataflows.date_window import get_current_date, is_historical
|
||||
from tradingagents.dataflows.symbols import safe_ticker_component
|
||||
from tradingagents.default_config import DEFAULT_CONFIG
|
||||
from tradingagents.llm_clients import build_llm_kwargs, create_llm_client
|
||||
from tradingagents.llm_clients import create_tier_client, tier_provider
|
||||
from tradingagents.memory import TradingMemoryLog, settlement
|
||||
from tradingagents.memory.reflection import Reflector
|
||||
from tradingagents.reporting import write_report_tree
|
||||
@@ -75,26 +75,9 @@ class TradingAgentsGraph:
|
||||
os.makedirs(self.config["data_cache_dir"], exist_ok=True)
|
||||
os.makedirs(self.config["results_dir"], exist_ok=True)
|
||||
|
||||
llm_kwargs = build_llm_kwargs(self.config)
|
||||
|
||||
if self.callbacks:
|
||||
llm_kwargs["callbacks"] = self.callbacks
|
||||
|
||||
deep_client = create_llm_client(
|
||||
provider=self.config["llm_provider"],
|
||||
model=self.config["deep_think_llm"],
|
||||
base_url=self.config.get("backend_url"),
|
||||
**llm_kwargs,
|
||||
)
|
||||
quick_client = create_llm_client(
|
||||
provider=self.config["llm_provider"],
|
||||
model=self.config["quick_think_llm"],
|
||||
base_url=self.config.get("backend_url"),
|
||||
**llm_kwargs,
|
||||
)
|
||||
|
||||
self.deep_thinking_llm = deep_client.get_llm()
|
||||
self.quick_thinking_llm = quick_client.get_llm()
|
||||
extra = {"callbacks": self.callbacks} if self.callbacks else {}
|
||||
self.deep_thinking_llm = create_tier_client(self.config, "deep", **extra).get_llm()
|
||||
self.quick_thinking_llm = create_tier_client(self.config, "quick", **extra).get_llm()
|
||||
|
||||
self.memory_log = TradingMemoryLog(self.config)
|
||||
|
||||
@@ -279,7 +262,9 @@ class TradingAgentsGraph:
|
||||
return {
|
||||
"version": tradingagents.__version__,
|
||||
"llm_provider": cfg.get("llm_provider"),
|
||||
"deep_think_provider": tier_provider(cfg, "deep") if cfg.get("llm_provider") else None,
|
||||
"deep_think_llm": cfg.get("deep_think_llm"),
|
||||
"quick_think_provider": tier_provider(cfg, "quick") if cfg.get("llm_provider") else None,
|
||||
"quick_think_llm": cfg.get("quick_think_llm"),
|
||||
"analysts": list(self.selected_analysts),
|
||||
"max_debate_rounds": cfg.get("max_debate_rounds"),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from .base_client import BaseLLMClient
|
||||
from .factory import build_llm_kwargs, create_llm_client
|
||||
from .factory import build_llm_kwargs, create_llm_client, create_tier_client, tier_provider
|
||||
|
||||
__all__ = ["BaseLLMClient", "build_llm_kwargs", "create_llm_client"]
|
||||
__all__ = ["BaseLLMClient", "build_llm_kwargs", "create_llm_client", "create_tier_client", "tier_provider"]
|
||||
|
||||
@@ -128,3 +128,25 @@ def build_llm_kwargs(config: dict) -> dict[str, Any]:
|
||||
kwargs[key] = _coerce_max_tokens(max_tokens)
|
||||
|
||||
return kwargs
|
||||
|
||||
|
||||
def tier_provider(config: dict, tier: str) -> str:
|
||||
"""The provider of the ``quick`` or ``deep`` model tier: its own, else ``llm_provider``."""
|
||||
return (config.get(f"{tier}_think_provider") or config["llm_provider"]).lower()
|
||||
|
||||
|
||||
def create_tier_client(config: dict, tier: str, **extra) -> BaseLLMClient:
|
||||
"""The client for the ``quick`` or ``deep`` model tier (#1440).
|
||||
|
||||
A tier on its own provider gets that provider's settings (thinking level,
|
||||
reasoning effort, the name of the output-token cap) and its own endpoint,
|
||||
else the provider's default: ``backend_url`` belongs to ``llm_provider``.
|
||||
"""
|
||||
provider = tier_provider(config, tier)
|
||||
if provider == config["llm_provider"].lower():
|
||||
base_url = config.get(f"{tier}_think_backend_url") or config.get("backend_url")
|
||||
else:
|
||||
base_url = config.get(f"{tier}_think_backend_url")
|
||||
kwargs = build_llm_kwargs({**config, "llm_provider": provider})
|
||||
return create_llm_client(provider=provider, model=config[f"{tier}_think_llm"], base_url=base_url,
|
||||
**kwargs, **extra)
|
||||
|
||||
@@ -18,8 +18,14 @@ def _header(ticker: str, final_state: dict, settings: dict | None) -> str:
|
||||
lines.append(f"- Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
if settings:
|
||||
s = settings.get
|
||||
lines.append(f"- TradingAgents {s('version', '?')}: {s('llm_provider', '?')}, "
|
||||
f"deep {s('deep_think_llm', '?')}, quick {s('quick_think_llm', '?')}")
|
||||
deep_provider = s("deep_think_provider") or s("llm_provider", "?")
|
||||
quick_provider = s("quick_think_provider") or s("llm_provider", "?")
|
||||
if deep_provider == quick_provider:
|
||||
models = f"{deep_provider}, deep {s('deep_think_llm', '?')}, quick {s('quick_think_llm', '?')}"
|
||||
else: # each tier on its own provider (#1440)
|
||||
models = (f"deep {deep_provider} {s('deep_think_llm', '?')}, "
|
||||
f"quick {quick_provider} {s('quick_think_llm', '?')}")
|
||||
lines.append(f"- TradingAgents {s('version', '?')}: {models}")
|
||||
lines.append(f"- Analysts: {', '.join(s('analysts') or [])}; "
|
||||
f"research debate rounds {s('max_debate_rounds', '?')}, "
|
||||
f"risk debate rounds {s('max_risk_discuss_rounds', '?')}")
|
||||
|
||||
Reference in New Issue
Block a user