diff --git a/.env.example b/.env.example index be9bf13e..ade505f0 100644 --- a/.env.example +++ b/.env.example @@ -7,3 +7,4 @@ DEEPSEEK_API_KEY= DASHSCOPE_API_KEY= ZHIPU_API_KEY= OPENROUTER_API_KEY= +MINIMAX_API_KEY= diff --git a/cli/utils.py b/cli/utils.py index 85c282ed..76421c6c 100644 --- a/cli/utils.py +++ b/cli/utils.py @@ -239,6 +239,7 @@ def select_llm_provider() -> tuple[str, str | None]: ("DeepSeek", "deepseek", "https://api.deepseek.com"), ("Qwen", "qwen", "https://dashscope.aliyuncs.com/compatible-mode/v1"), ("GLM", "glm", "https://open.bigmodel.cn/api/paas/v4/"), + ("MiniMax", "minimax", "https://api.minimaxi.chat/v1"), ("OpenRouter", "openrouter", "https://openrouter.ai/api/v1"), ("Azure OpenAI", "azure", None), ("Ollama", "ollama", "http://localhost:11434/v1"), diff --git a/tradingagents/llm_clients/factory.py b/tradingagents/llm_clients/factory.py index a9a7e83d..c8ec2084 100644 --- a/tradingagents/llm_clients/factory.py +++ b/tradingagents/llm_clients/factory.py @@ -8,7 +8,7 @@ from .azure_client import AzureOpenAIClient # Providers that use the OpenAI-compatible chat completions API _OPENAI_COMPATIBLE = ( - "openai", "xai", "deepseek", "qwen", "glm", "ollama", "openrouter", + "openai", "xai", "deepseek", "qwen", "glm", "ollama", "openrouter", "minimax", ) diff --git a/tradingagents/llm_clients/model_catalog.py b/tradingagents/llm_clients/model_catalog.py index a2c57ed8..a86e1b02 100644 --- a/tradingagents/llm_clients/model_catalog.py +++ b/tradingagents/llm_clients/model_catalog.py @@ -99,6 +99,20 @@ MODEL_OPTIONS: ProviderModeOptions = { ("Custom model ID", "custom"), ], }, + "minimax": { + "quick": [ + ("MiniMax-M2.7", "MiniMax-M2.7"), + ("MiniMax-M2.7-highspeed", "MiniMax-M2.7-highspeed"), + ("MiniMax-M2.5", "MiniMax-M2.5"), + ("MiniMax-M2.5-highspeed", "MiniMax-M2.5-highspeed"), + ], + "deep": [ + ("MiniMax-M2.7", "MiniMax-M2.7"), + ("MiniMax-M2.7-highspeed", "MiniMax-M2.7-highspeed"), + ("MiniMax-M2.5", "MiniMax-M2.5"), + ("MiniMax-M2.5-highspeed", "MiniMax-M2.5-highspeed"), + ], + }, # OpenRouter: fetched dynamically. Azure: any deployed model name. "ollama": { "quick": [ diff --git a/tradingagents/llm_clients/openai_client.py b/tradingagents/llm_clients/openai_client.py index f943124a..f793be65 100644 --- a/tradingagents/llm_clients/openai_client.py +++ b/tradingagents/llm_clients/openai_client.py @@ -1,4 +1,6 @@ import os +import time +import logging from typing import Any, Optional from langchain_openai import ChatOpenAI @@ -6,6 +8,11 @@ from langchain_openai import ChatOpenAI from .base_client import BaseLLMClient, normalize_content from .validators import validate_model +logger = logging.getLogger(__name__) + +_NULL_CHOICES_RETRIES = 3 +_NULL_CHOICES_DELAY = 2 # seconds + class NormalizedChatOpenAI(ChatOpenAI): """ChatOpenAI with normalized content output. @@ -16,7 +23,28 @@ class NormalizedChatOpenAI(ChatOpenAI): """ def invoke(self, input, config=None, **kwargs): - return normalize_content(super().invoke(input, config, **kwargs)) + for attempt in range(1, _NULL_CHOICES_RETRIES + 1): + try: + return normalize_content(super().invoke(input, config, **kwargs)) + except TypeError as e: + if "null value for 'choices'" in str(e): + if attempt < _NULL_CHOICES_RETRIES: + logger.warning( + "Received null choices from API (content filter or transient error). " + "Retrying in %ds (attempt %d/%d)...", + _NULL_CHOICES_DELAY, + attempt, + _NULL_CHOICES_RETRIES, + ) + time.sleep(_NULL_CHOICES_DELAY) + else: + raise RuntimeError( + "API returned null choices after retries. " + "The request may have been blocked by content moderation. " + "Try rephrasing the prompt or check the provider's content policy." + ) from e + else: + raise # Kwargs forwarded from user config to ChatOpenAI _PASSTHROUGH_KWARGS = ( @@ -32,6 +60,7 @@ _PROVIDER_CONFIG = { "glm": ("https://api.z.ai/api/paas/v4/", "ZHIPU_API_KEY"), "openrouter": ("https://openrouter.ai/api/v1", "OPENROUTER_API_KEY"), "ollama": ("http://localhost:11434/v1", None), + "minimax": ("https://api.minimaxi.chat/v1", "MINIMAX_API_KEY"), }