refactor(llm_clients): build the client keyword arguments in llm_clients

- build_llm_kwargs(config) replaces the graph's _get_provider_kwargs, with the retry and token coercion beside it
This commit is contained in:
Yijia-Xiao
2026-09-24 05:00:36 +00:00
parent e7354d8af4
commit c9695673bf
6 changed files with 94 additions and 104 deletions
+2 -75
View File
@@ -16,7 +16,7 @@ from tradingagents.dataflows.symbols import safe_ticker_component
from tradingagents.dataflows.vendors.yahoo.market import get_closes
from tradingagents.decision_log import TradingMemoryLog
from tradingagents.default_config import DEFAULT_CONFIG
from tradingagents.llm_clients import create_llm_client
from tradingagents.llm_clients import build_llm_kwargs, create_llm_client
from tradingagents.reporting import write_report_tree
from .checkpointer import checkpoint_step, clear_checkpoint, get_checkpointer, thread_id
@@ -42,37 +42,6 @@ def _validate_trade_date(trade_date) -> str:
return value
def _coerce_max_retries(value):
"""Validate an ``llm_max_retries`` value to a non-negative int.
Accepts an int or a numeric string (env vars arrive as strings). Rejects
booleans and negatives loudly so a misconfiguration fails at startup rather
than silently disabling retries.
"""
if isinstance(value, bool):
raise ValueError(f"llm_max_retries must be an integer, not a boolean: {value!r}")
try:
n = int(value)
except (TypeError, ValueError) as exc:
raise ValueError(f"llm_max_retries must be an integer, got {value!r}") from exc
if n < 0:
raise ValueError(f"llm_max_retries must be >= 0, got {n}")
return n
def _coerce_max_tokens(value):
"""Validate a ``max_tokens`` value to a positive int (env vars are strings)."""
if isinstance(value, bool):
raise ValueError(f"max_tokens must be an integer, not a boolean: {value!r}")
try:
n = int(value)
except (TypeError, ValueError) as exc:
raise ValueError(f"max_tokens must be an integer, got {value!r}") from exc
if n <= 0:
raise ValueError(f"max_tokens must be > 0, got {n}")
return n
class TradingAgentsGraph:
"""Main class that orchestrates the trading agents framework."""
@@ -103,7 +72,7 @@ class TradingAgentsGraph:
os.makedirs(self.config["results_dir"], exist_ok=True)
# Initialize LLMs with provider-specific thinking configuration
llm_kwargs = self._get_provider_kwargs()
llm_kwargs = build_llm_kwargs(self.config)
# Add callbacks to kwargs if provided (passed to LLM constructor)
if self.callbacks:
@@ -152,48 +121,6 @@ class TradingAgentsGraph:
self._checkpointer_ctx = None
self._resuming = False
def _get_provider_kwargs(self) -> dict[str, Any]:
"""Get provider-specific kwargs for LLM client creation."""
kwargs = {}
provider = self.config.get("llm_provider", "").lower()
if provider == "google":
thinking_level = self.config.get("google_thinking_level")
if thinking_level:
kwargs["thinking_level"] = thinking_level
elif provider == "openai":
reasoning_effort = self.config.get("openai_reasoning_effort")
if reasoning_effort:
kwargs["reasoning_effort"] = reasoning_effort
elif provider == "anthropic":
effort = self.config.get("anthropic_effort")
if effort:
kwargs["effort"] = effort
# Sampling temperature is cross-provider: forward it whenever set.
# float() here so a value coming from a TRADINGAGENTS_TEMPERATURE env
# string ("0.2") works the same as a programmatic float.
temperature = self.config.get("temperature")
if temperature is not None and temperature != "":
kwargs["temperature"] = float(temperature)
# SDK retry budget is cross-provider. Forward it only when explicitly set
# so each provider keeps its own default (usually 2) otherwise (#1091).
max_retries = self.config.get("llm_max_retries")
if max_retries is not None and max_retries != "":
kwargs["max_retries"] = _coerce_max_retries(max_retries)
# Output-token cap is cross-provider, but Gemini names it
# ``max_output_tokens``; forward under the right key when set (#1204).
max_tokens = self.config.get("max_tokens")
if max_tokens is not None and max_tokens != "":
key = "max_output_tokens" if provider == "google" else "max_tokens"
kwargs[key] = _coerce_max_tokens(max_tokens)
return kwargs
def _resolve_benchmark(self, ticker: str) -> str:
"""Pick the benchmark ticker for alpha calculation against ``ticker``.
+2 -2
View File
@@ -1,4 +1,4 @@
from .base_client import BaseLLMClient
from .factory import create_llm_client
from .factory import build_llm_kwargs, create_llm_client
__all__ = ["BaseLLMClient", "create_llm_client"]
__all__ = ["BaseLLMClient", "build_llm_kwargs", "create_llm_client"]
+76
View File
@@ -1,4 +1,6 @@
from typing import Any
from .base_client import BaseLLMClient
@@ -52,3 +54,77 @@ def create_llm_client(
return OpenAIClient(model, base_url, provider=provider_lower, **kwargs)
raise ValueError(f"Unsupported LLM provider: {provider}")
def _coerce_max_retries(value):
"""Validate an ``llm_max_retries`` value to a non-negative int.
Accepts an int or a numeric string (env vars arrive as strings). Rejects
booleans and negatives loudly so a misconfiguration fails at startup rather
than silently disabling retries.
"""
if isinstance(value, bool):
raise ValueError(f"llm_max_retries must be an integer, not a boolean: {value!r}")
try:
n = int(value)
except (TypeError, ValueError) as exc:
raise ValueError(f"llm_max_retries must be an integer, got {value!r}") from exc
if n < 0:
raise ValueError(f"llm_max_retries must be >= 0, got {n}")
return n
def _coerce_max_tokens(value):
"""Validate a ``max_tokens`` value to a positive int (env vars are strings)."""
if isinstance(value, bool):
raise ValueError(f"max_tokens must be an integer, not a boolean: {value!r}")
try:
n = int(value)
except (TypeError, ValueError) as exc:
raise ValueError(f"max_tokens must be an integer, got {value!r}") from exc
if n <= 0:
raise ValueError(f"max_tokens must be > 0, got {n}")
return n
def build_llm_kwargs(config: dict) -> dict[str, Any]:
"""Keyword arguments for ``create_llm_client`` from a TradingAgents config."""
kwargs = {}
provider = config.get("llm_provider", "").lower()
if provider == "google":
thinking_level = config.get("google_thinking_level")
if thinking_level:
kwargs["thinking_level"] = thinking_level
elif provider == "openai":
reasoning_effort = config.get("openai_reasoning_effort")
if reasoning_effort:
kwargs["reasoning_effort"] = reasoning_effort
elif provider == "anthropic":
effort = config.get("anthropic_effort")
if effort:
kwargs["effort"] = effort
# Sampling temperature is cross-provider: forward it whenever set.
# float() here so a value coming from a TRADINGAGENTS_TEMPERATURE env
# string ("0.2") works the same as a programmatic float.
temperature = config.get("temperature")
if temperature is not None and temperature != "":
kwargs["temperature"] = float(temperature)
# SDK retry budget is cross-provider. Forward it only when explicitly set
# so each provider keeps its own default (usually 2) otherwise (#1091).
max_retries = config.get("llm_max_retries")
if max_retries is not None and max_retries != "":
kwargs["max_retries"] = _coerce_max_retries(max_retries)
# Output-token cap is cross-provider, but Gemini names it
# ``max_output_tokens``; forward under the right key when set (#1204).
max_tokens = config.get("max_tokens")
if max_tokens is not None and max_tokens != "":
key = "max_output_tokens" if provider == "google" else "max_tokens"
kwargs[key] = _coerce_max_tokens(max_tokens)
return kwargs