Skip to content

Commit f6d4ea2

Browse files
authored
Merge pull request #42 from rajatvarna/feat/deepseek-max-tokens-1205
feat: DeepSeek max_tokens and streaming reasoning round-trip (TauricResearch#1205)
2 parents 3a91f1e + f2dcbc7 commit f6d4ea2

6 files changed

Lines changed: 43 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ Breaking changes within the 0.x line are called out explicitly.
2121
- **Unparseable ratings surface as REVIEW (#1189 / #1170)**: `SignalProcessor` and the SQLite memory log now tag decisions with `RATING_REVIEW` when no 5-tier rating can be extracted, instead of silently defaulting to Hold/Unknown. (`tradingagents/agents/utils/rating.py`, `tradingagents/graph/signal_processing.py`, `tradingagents/agents/utils/memory.py`, `tests/test_signal_processing.py`)
2222

2323
- **Reddit RSS hardening (#1218 / #1219)**: RSS Atom parsing uses `defusedxml` to block XXE, and 429 responses retry up to three times with exponential backoff (honouring `Retry-After`). (`tradingagents/dataflows/reddit.py`, `pyproject.toml`)
24+
25+
- **DeepSeek max_tokens and streaming round-trip (#1205 / #1204)**: default `max_tokens` (8192) with `TRADINGAGENTS_MAX_TOKENS` override; forward through graph provider kwargs; capture `reasoning_content` on the streaming path for DeepSeek/opencode.ai. (`tradingagents/default_config.py`, `tradingagents/config_schema.py`, `tradingagents/graph/trading_graph.py`, `tradingagents/llm_clients/openai_client.py`, `docs/CONFIG_REFERENCE.md`)
2426
`ops/broker/mcp_client.py` now imports `streamablehttp_client` with a fallback to the mcp 2.0 rename (`streamable_http_client`). Pin `mcp>=1.28.1,<2` to keep CI ops tests collecting reliably. (`ops/broker/mcp_client.py`, `pyproject.toml`)
2527

2628
- **API DeepSeek/MiniMax provider support**: REST API worker and console now support `deepseek`, `minimax`, and `minimax-cn` (not just ollama/google/openrouter). Default provider follows `TRADINGAGENTS_LLM_PROVIDER` (defaults to `deepseek`). Local path defaults use `./output/` instead of `/data/*`. Worker respects `DEFAULT_CONFIG` data vendors (Finnhub/yfinance via env) instead of forcing yfinance-only. (`api/worker.py`, `api/main.py`, `api/schemas.py`, `api/db.py`, `tests/test_api_worker_local_deploy.py`, `tests/test_provider_model_routing.py`)

docs/CONFIG_REFERENCE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ Autogenerated from `tradingagents/config_schema.py::TradingAgentsConfig` by `pyt
6363
| `deepseek_reasoning_effort` | `str \| None` | `'max'` | `TRADINGAGENTS_DEEPSEEK_REASONING_EFFORT` |
6464
| `temperature` | `float \| None` | `None` | `TRADINGAGENTS_TEMPERATURE` |
6565
| `llm_max_retries` | `int \| None` | `None` | `TRADINGAGENTS_LLM_MAX_RETRIES` |
66+
| `max_tokens` | `int \| None` | `8192` | `TRADINGAGENTS_MAX_TOKENS` |
6667
| `llm_cache_enabled` | `bool` | `True` | `TRADINGAGENTS_LLM_CACHE_ENABLED` |
6768
| `llm_cache_ttl_hours` | `int` | `24` | `TRADINGAGENTS_LLM_CACHE_TTL_HOURS` |
6869
| `llm_cache_providers` | `list` | `[]` | `TRADINGAGENTS_LLM_CACHE_PROVIDERS` |

tradingagents/config_schema.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ class TradingAgentsConfig(BaseModel):
125125
deepseek_reasoning_effort: str | None = "max"
126126
temperature: float | None = Field(default=None, ge=0, le=2)
127127
llm_max_retries: int | None = Field(default=None, ge=0)
128+
max_tokens: int | None = Field(default=8192, ge=1)
128129
llm_cache_enabled: bool = True
129130
llm_cache_ttl_hours: int = Field(default=24, ge=0)
130131
llm_cache_providers: list[str] = Field(default_factory=list)

tradingagents/default_config.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"TRADINGAGENTS_LLM_BACKEND_URL": "backend_url",
2727
"TRADINGAGENTS_OUTPUT_LANGUAGE": "output_language",
2828
"TRADINGAGENTS_LLM_MAX_RETRIES": "llm_max_retries",
29+
"TRADINGAGENTS_MAX_TOKENS": "max_tokens",
2930
"TRADINGAGENTS_MAX_DEBATE_ROUNDS": "max_debate_rounds",
3031
"TRADINGAGENTS_MAX_RISK_ROUNDS": "max_risk_discuss_rounds",
3132
"TRADINGAGENTS_CHECKPOINT_ENABLED": "checkpoint_enabled",
@@ -247,6 +248,12 @@ def _apply_env_overrides(config: dict) -> dict:
247248
# provider/SDK at its own default (usually 2). Raise it to ride out bursty
248249
# 429 throttling on rate-limited deployments instead of aborting a run (#1091).
249250
"llm_max_retries": None,
251+
# Max output tokens per LLM call. DeepSeek V4 thinking models emit long
252+
# reasoning chains; without an explicit cap the backend can stream the
253+
# chain indefinitely (gateway idle timeout) or truncate content to empty
254+
# (#1204). Default 8192 leaves room for reasoning + content; override via
255+
# TRADINGAGENTS_MAX_TOKENS.
256+
"max_tokens": 8192,
250257
# LLM response cache — local file-based cache of LLM API responses.
251258
# Enabled by default; set TRADINGAGENTS_LLM_CACHE_ENABLED=false to disable.
252259
"llm_cache_enabled": True,

tradingagents/graph/trading_graph.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,10 @@ def _get_provider_kwargs(self) -> dict[str, Any]:
371371
if max_retries is not None and max_retries != "":
372372
kwargs["max_retries"] = _coerce_max_retries(max_retries)
373373

374+
max_tokens = self.config.get("max_tokens")
375+
if max_tokens is not None and max_tokens != "":
376+
kwargs["max_tokens"] = int(max_tokens)
377+
374378
# Determinism keys (T0.1)
375379
llm_temp = self.config.get("llm_temperature")
376380
if llm_temp is not None and llm_temp != "":

tradingagents/llm_clients/openai_client.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -158,8 +158,9 @@ class DeepSeekChatOpenAI(NormalizedChatOpenAI):
158158
stays here. When DeepSeek's thinking models return a response with
159159
``reasoning_content``, that field must be echoed back as part of the
160160
assistant message on the next turn or the API fails with HTTP 400.
161-
``_create_chat_result`` captures it on receive and
162-
``_get_request_payload`` re-attaches it on send.
161+
``_create_chat_result`` captures it on the non-streaming path and
162+
``_convert_chunk_to_generation_chunk`` captures it on the streaming
163+
path; ``_get_request_payload`` re-attaches it on send.
163164
164165
Tool-choice handling for V4 and reasoner — those models reject the
165166
``tool_choice`` parameter — is handled by the capability dispatch in
@@ -177,6 +178,22 @@ def _get_request_payload(self, input_, *, stop=None, **kwargs):
177178
message_dict["reasoning_content"] = reasoning
178179
return payload
179180

181+
def _convert_chunk_to_generation_chunk(self, chunk, default_chunk_class, base_generation_info):
182+
# langchain-openai's stream path drops ``reasoning_content`` from
183+
# deltas. Rescue it into ``additional_kwargs`` so the round-trip on
184+
# the next turn — see ``_get_request_payload`` — has it.
185+
gen_chunk = super()._convert_chunk_to_generation_chunk(
186+
chunk, default_chunk_class, base_generation_info
187+
)
188+
if gen_chunk is None:
189+
return None
190+
choices = chunk.get("choices") or chunk.get("chunk", {}).get("choices") or []
191+
if choices:
192+
reasoning = (choices[0].get("delta") or {}).get("reasoning_content")
193+
if reasoning:
194+
gen_chunk.message.additional_kwargs["reasoning_content"] = reasoning
195+
return gen_chunk
196+
180197
def _create_chat_result(self, response, generation_info=None):
181198
chat_result = super()._create_chat_result(response, generation_info)
182199
response_dict = (
@@ -357,7 +374,7 @@ def _create_chat_result(self, response, generation_info=None):
357374
_PASSTHROUGH_KWARGS = (
358375
"timeout", "max_retries", "reasoning_effort", "temperature",
359376
"api_key", "callbacks", "http_client", "http_async_client",
360-
"default_headers",
377+
"default_headers", "max_tokens",
361378
)
362379

363380
# OpenAI's ``reasoning_effort`` is only accepted by reasoning models — the GPT-5
@@ -634,6 +651,14 @@ def get_llm(self) -> Any:
634651
if hasattr(chat_cls, "__name__") and chat_cls.__name__ in globals():
635652
chat_cls = globals()[chat_cls.__name__]
636653

654+
base_url_for_check = llm_kwargs.get("base_url", "") or ""
655+
is_opencode = "opencode.ai" in base_url_for_check
656+
if is_opencode:
657+
llm_kwargs.setdefault("streaming", True)
658+
is_deepseek_model = "deepseek" in self.model.lower()
659+
if self.provider == "deepseek" or (is_opencode and is_deepseek_model):
660+
chat_cls = DeepSeekChatOpenAI
661+
637662
llm = chat_cls(**llm_kwargs)
638663
# Tag the LLM with the provider name so the cache layer can filter
639664
# by provider (e.g. cache DeepSeek but skip Ollama).

0 commit comments

Comments
 (0)