feat(llm): add a configurable output-token cap

- some model/gateway combinations emit unbounded reasoning/output and hang or
  trip an idle timeout (e.g. some deepseek-v4-flash deployments)
- add an opt-in max_tokens config knob + TRADINGAGENTS_MAX_TOKENS, forwarded to
  every provider when set (Gemini takes it as max_output_tokens); int-coerced,
  rejects non-positive/boolean values #1204
This commit is contained in:
Yijia-Xiao
2026-08-30 06:26:57 +00:00
parent 539eae8fd6
commit 0ef56e6a33
6 changed files with 155 additions and 2 deletions

View File

@@ -19,6 +19,7 @@ _ENV_OVERRIDES = {
"TRADINGAGENTS_BENCHMARK_TICKER": "benchmark_ticker",
"TRADINGAGENTS_TEMPERATURE": "temperature",
"TRADINGAGENTS_LLM_MAX_RETRIES": "llm_max_retries",
"TRADINGAGENTS_MAX_TOKENS": "max_tokens",
# Provider-specific reasoning/thinking knobs (None = each provider's own
# default). Settable here for non-interactive runs; the CLI also offers an
# interactive choice, which is skipped when the matching var is set.
@@ -100,6 +101,11 @@ DEFAULT_CONFIG = _apply_env_overrides({
# provider/SDK at its own default (usually 2). Raise it to ride out bursty
# 429 throttling on rate-limited deployments instead of aborting a run (#1091).
"llm_max_retries": None,
# Cap on output tokens forwarded to every provider chat client. None leaves
# each provider at its own default. Set it to bound a model that emits
# unbounded reasoning/output and hangs or trips a gateway idle timeout
# (e.g. some deepseek-v4-flash deployments, #1204).
"max_tokens": None,
# Checkpoint/resume: when True, LangGraph saves state after each node
# so a crashed run can resume from the last successful step.
"checkpoint_enabled": False,

View File

@@ -62,6 +62,19 @@ def _coerce_max_retries(value):
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."""
@@ -183,6 +196,13 @@ class TradingAgentsGraph:
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 _create_tool_nodes(self) -> dict[str, ToolNode]:

View File

@@ -31,7 +31,8 @@ class GoogleClient(BaseLLMClient):
if self.base_url:
llm_kwargs["base_url"] = self.base_url
for key in ("timeout", "max_retries", "temperature", "callbacks", "http_client", "http_async_client"):
for key in ("timeout", "max_retries", "temperature", "max_output_tokens",
"callbacks", "http_client", "http_async_client"):
if key in self.kwargs:
llm_kwargs[key] = self.kwargs[key]

View File

@@ -164,7 +164,7 @@ class MinimaxChatOpenAI(NormalizedChatOpenAI):
# Kwargs forwarded from user config to ChatOpenAI
_PASSTHROUGH_KWARGS = (
"timeout", "max_retries", "reasoning_effort", "temperature",
"timeout", "max_retries", "reasoning_effort", "temperature", "max_tokens",
"api_key", "callbacks", "http_client", "http_async_client",
)