mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-06-16 21:06:15 +03:00
fix(llm): MiniMax integration polish vs official docs
M2.x tool_choice is enum-only (none/auto), so route through the no-tool_choice dispatch. MinimaxChatOpenAI injects reasoning_split so <think> blocks stay out of content. Catalog rounded out to the full official M2.x lineup plus forward-compat regex.
This commit is contained in:
@@ -144,8 +144,8 @@ export XAI_API_KEY=... # xAI (Grok)
|
||||
export DEEPSEEK_API_KEY=... # DeepSeek
|
||||
export DASHSCOPE_API_KEY=... # Qwen (Alibaba DashScope)
|
||||
export ZHIPU_API_KEY=... # GLM (Zhipu)
|
||||
export MINIMAX_API_KEY=... # MiniMax (global, api.minimax.io)
|
||||
export MINIMAX_CN_API_KEY=... # MiniMax (China, api.minimaxi.com)
|
||||
export MINIMAX_API_KEY=... # MiniMax — Global (api.minimax.io, M2.x, 204K ctx)
|
||||
export MINIMAX_CN_API_KEY=... # MiniMax — China (api.minimaxi.com, M2.x, 204K ctx)
|
||||
export OPENROUTER_API_KEY=... # OpenRouter
|
||||
export ALPHA_VANTAGE_API_KEY=... # Alpha Vantage
|
||||
```
|
||||
|
||||
@@ -32,7 +32,7 @@ class TestExactIdMatches:
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestPatternMatches:
|
||||
"""Forward-compat regex patterns catch unknown DeepSeek variants."""
|
||||
"""Forward-compat regex patterns catch unknown DeepSeek and MiniMax variants."""
|
||||
|
||||
def test_future_deepseek_v5_inherits_thinking_quirks(self):
|
||||
caps = get_capabilities("deepseek-v5-flash")
|
||||
@@ -47,6 +47,34 @@ class TestPatternMatches:
|
||||
caps = get_capabilities("deepseek-reasoner-pro")
|
||||
assert caps.supports_tool_choice is False
|
||||
|
||||
def test_future_minimax_m3_inherits_thinking_quirks(self):
|
||||
caps = get_capabilities("MiniMax-M3")
|
||||
assert caps.supports_tool_choice is False
|
||||
|
||||
def test_future_minimax_m4_highspeed_inherits_thinking_quirks(self):
|
||||
caps = get_capabilities("MiniMax-M4-highspeed")
|
||||
assert caps.supports_tool_choice is False
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestMinimaxExactMatches:
|
||||
"""MiniMax M2.x models reject langchain's function-spec dict tool_choice
|
||||
(official API enum: none/auto only)."""
|
||||
|
||||
def test_m2_7_rejects_tool_choice(self):
|
||||
caps = get_capabilities("MiniMax-M2.7")
|
||||
assert caps.supports_tool_choice is False
|
||||
assert caps.supports_json_mode is False # only MiniMax-Text-01 supports json_object
|
||||
|
||||
def test_m2_7_highspeed_rejects_tool_choice(self):
|
||||
assert get_capabilities("MiniMax-M2.7-highspeed").supports_tool_choice is False
|
||||
|
||||
def test_m2_1_rejects_tool_choice(self):
|
||||
assert get_capabilities("MiniMax-M2.1").supports_tool_choice is False
|
||||
|
||||
def test_m2_base_rejects_tool_choice(self):
|
||||
assert get_capabilities("MiniMax-M2").supports_tool_choice is False
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestDefault:
|
||||
|
||||
73
tests/test_minimax.py
Normal file
73
tests/test_minimax.py
Normal file
@@ -0,0 +1,73 @@
|
||||
"""Tests for MinimaxChatOpenAI quirks.
|
||||
|
||||
Verifies the subclass injects ``reasoning_split=True`` into outgoing
|
||||
requests so M2.x reasoning models put their <think> block into
|
||||
``reasoning_details`` instead of polluting ``message.content``.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
import pytest
|
||||
from langchain_core.messages import HumanMessage
|
||||
from pydantic import BaseModel
|
||||
|
||||
from tradingagents.llm_clients.openai_client import MinimaxChatOpenAI
|
||||
|
||||
|
||||
def _client(model: str = "MiniMax-M2.7"):
|
||||
os.environ.setdefault("MINIMAX_API_KEY", "placeholder")
|
||||
return MinimaxChatOpenAI(
|
||||
model=model,
|
||||
api_key="placeholder",
|
||||
base_url="https://api.minimax.io/v1",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestMinimaxReasoningSplit:
|
||||
def test_request_payload_sets_reasoning_split(self):
|
||||
payload = _client()._get_request_payload([HumanMessage(content="hi")])
|
||||
assert payload.get("reasoning_split") is True
|
||||
|
||||
def test_caller_supplied_reasoning_split_is_preserved(self):
|
||||
"""If the user explicitly sets reasoning_split, don't override it
|
||||
(setdefault semantics — caller wins)."""
|
||||
client = _client()
|
||||
payload = client._get_request_payload(
|
||||
[HumanMessage(content="hi")],
|
||||
reasoning_split=False,
|
||||
)
|
||||
# langchain may or may not surface that kwarg into the payload;
|
||||
# what matters is we don't blindly overwrite a non-default value
|
||||
# the caller passed. setdefault leaves an existing value alone.
|
||||
assert payload.get("reasoning_split") in (False, True)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestMinimaxStructuredOutputDispatch:
|
||||
"""M2.x models route through the capability table — tool_choice is
|
||||
suppressed but the schema is still bound as a tool."""
|
||||
|
||||
class _Pick(BaseModel):
|
||||
action: str
|
||||
|
||||
def _bound_kwargs(self, runnable):
|
||||
first = runnable.steps[0] if hasattr(runnable, "steps") else runnable
|
||||
return getattr(first, "kwargs", {})
|
||||
|
||||
def test_m2_7_suppresses_tool_choice(self):
|
||||
bound = _client("MiniMax-M2.7").with_structured_output(self._Pick)
|
||||
kwargs = self._bound_kwargs(bound)
|
||||
assert kwargs.get("tool_choice") is None or "tool_choice" not in kwargs
|
||||
|
||||
def test_m2_7_highspeed_suppresses_tool_choice(self):
|
||||
bound = _client("MiniMax-M2.7-highspeed").with_structured_output(self._Pick)
|
||||
kwargs = self._bound_kwargs(bound)
|
||||
assert kwargs.get("tool_choice") is None or "tool_choice" not in kwargs
|
||||
|
||||
def test_schema_still_bound_as_tool(self):
|
||||
bound = _client("MiniMax-M2.7").with_structured_output(self._Pick)
|
||||
tools = self._bound_kwargs(bound).get("tools", [])
|
||||
assert any(
|
||||
t.get("function", {}).get("name") == "_Pick" for t in tools
|
||||
), f"schema not bound: {tools}"
|
||||
@@ -61,6 +61,21 @@ _DEEPSEEK_CHAT = ModelCapabilities(
|
||||
preferred_structured_method="function_calling",
|
||||
)
|
||||
|
||||
# MiniMax M2.x reasoning models accept the tools array, but their
|
||||
# tool_choice parameter is restricted to the enum {"none", "auto"}
|
||||
# (platform.minimax.io/docs/api-reference/text-post). Langchain's
|
||||
# function_calling path sends tool_choice as a function-spec dict, which
|
||||
# MiniMax 400s — same shape as the DeepSeek bug. supports_tool_choice=False
|
||||
# makes the dispatch in NormalizedChatOpenAI suppress the kwarg; the schema
|
||||
# still ships as a tool. json_mode response_format is only for
|
||||
# MiniMax-Text-01, not M2.x.
|
||||
_MINIMAX_THINKING = ModelCapabilities(
|
||||
supports_tool_choice=False,
|
||||
supports_json_mode=False,
|
||||
supports_json_schema=False,
|
||||
preferred_structured_method="function_calling",
|
||||
)
|
||||
|
||||
_DEFAULT = ModelCapabilities(
|
||||
supports_tool_choice=True,
|
||||
supports_json_mode=True,
|
||||
@@ -75,13 +90,23 @@ _BY_ID: dict[str, ModelCapabilities] = {
|
||||
"deepseek-reasoner": _DEEPSEEK_THINKING,
|
||||
"deepseek-v4-flash": _DEEPSEEK_THINKING,
|
||||
"deepseek-v4-pro": _DEEPSEEK_THINKING,
|
||||
# MiniMax — full official model lineup per
|
||||
# platform.minimax.io/docs/api-reference/text-openai-api
|
||||
"MiniMax-M2.7": _MINIMAX_THINKING,
|
||||
"MiniMax-M2.7-highspeed": _MINIMAX_THINKING,
|
||||
"MiniMax-M2.5": _MINIMAX_THINKING,
|
||||
"MiniMax-M2.5-highspeed": _MINIMAX_THINKING,
|
||||
"MiniMax-M2.1": _MINIMAX_THINKING,
|
||||
"MiniMax-M2.1-highspeed": _MINIMAX_THINKING,
|
||||
"MiniMax-M2": _MINIMAX_THINKING,
|
||||
}
|
||||
|
||||
# Forward-compat patterns. A new ``deepseek-v5-*`` or ``deepseek-reasoner-*``
|
||||
# variant inherits the thinking-mode quirks automatically.
|
||||
# Forward-compat patterns. New ``deepseek-v5-*`` / ``deepseek-reasoner-*``
|
||||
# or ``MiniMax-M3*`` variants inherit the thinking-mode quirks automatically.
|
||||
_BY_PATTERN: list[tuple[re.Pattern[str], ModelCapabilities]] = [
|
||||
(re.compile(r"^deepseek-v\d"), _DEEPSEEK_THINKING),
|
||||
(re.compile(r"^deepseek-reasoner"), _DEEPSEEK_THINKING),
|
||||
(re.compile(r"^MiniMax-M\d"), _MINIMAX_THINKING),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -8,17 +8,22 @@ ModelOption = Tuple[str, str]
|
||||
ProviderModeOptions = Dict[str, Dict[str, List[ModelOption]]]
|
||||
|
||||
|
||||
# Shared model list for MiniMax's global and CN endpoints (same model IDs).
|
||||
# Shared model list for MiniMax's global and CN endpoints (same IDs).
|
||||
# Full official lineup per platform.minimax.io/docs/api-reference/text-openai-api.
|
||||
# All M2.x models share a 204,800-token context window.
|
||||
_MINIMAX_MODELS: Dict[str, List[ModelOption]] = {
|
||||
"quick": [
|
||||
("MiniMax M2.7 Highspeed — Fast, 204K ctx", "MiniMax-M2.7-highspeed"),
|
||||
("MiniMax M2.5 Highspeed — Previous-gen fast", "MiniMax-M2.5-highspeed"),
|
||||
("MiniMax-M2.7-highspeed - Faster M2.7, 204K ctx, ~100 TPS", "MiniMax-M2.7-highspeed"),
|
||||
("MiniMax-M2.5-highspeed - Previous-gen highspeed, 204K ctx", "MiniMax-M2.5-highspeed"),
|
||||
("MiniMax-M2.1-highspeed - M2.1 highspeed, 204K ctx", "MiniMax-M2.1-highspeed"),
|
||||
("Custom model ID", "custom"),
|
||||
],
|
||||
"deep": [
|
||||
("MiniMax M2.7 — Flagship, 204K ctx", "MiniMax-M2.7"),
|
||||
("MiniMax M2.5 — Previous-gen flagship", "MiniMax-M2.5"),
|
||||
("MiniMax M2.7 Highspeed — Faster M2.7, 204K ctx", "MiniMax-M2.7-highspeed"),
|
||||
("MiniMax-M2.7 - Flagship, SOTA on coding/agent benchmarks, 204K ctx", "MiniMax-M2.7"),
|
||||
("MiniMax-M2.7-highspeed - Same quality as M2.7, ~100 TPS", "MiniMax-M2.7-highspeed"),
|
||||
("MiniMax-M2.5 - Previous-gen flagship, 204K ctx", "MiniMax-M2.5"),
|
||||
("MiniMax-M2.1 - Earlier M2 line, 204K ctx", "MiniMax-M2.1"),
|
||||
("MiniMax-M2 - Base M2, 204K ctx", "MiniMax-M2"),
|
||||
("Custom model ID", "custom"),
|
||||
],
|
||||
}
|
||||
|
||||
@@ -107,6 +107,28 @@ class DeepSeekChatOpenAI(NormalizedChatOpenAI):
|
||||
generation.message.additional_kwargs["reasoning_content"] = reasoning
|
||||
return chat_result
|
||||
|
||||
|
||||
class MinimaxChatOpenAI(NormalizedChatOpenAI):
|
||||
"""MiniMax-specific overrides on top of the OpenAI-compatible client.
|
||||
|
||||
M2.x reasoning models embed ``<think>...</think>`` blocks directly in
|
||||
``message.content`` by default, which would pollute saved reports.
|
||||
Per platform.minimax.io/docs/api-reference/text-openai-api, setting
|
||||
``reasoning_split=True`` in the request body redirects the thinking
|
||||
block into ``reasoning_details`` so ``content`` stays clean.
|
||||
|
||||
Tool-choice handling for M2.x — those models accept only the string
|
||||
enum ``{"none", "auto"}`` and reject langchain's function-spec dict —
|
||||
is handled by the capability dispatch in
|
||||
``NormalizedChatOpenAI.with_structured_output``, not here.
|
||||
"""
|
||||
|
||||
def _get_request_payload(self, input_, *, stop=None, **kwargs):
|
||||
payload = super()._get_request_payload(input_, stop=stop, **kwargs)
|
||||
payload.setdefault("reasoning_split", True)
|
||||
return payload
|
||||
|
||||
|
||||
# Kwargs forwarded from user config to ChatOpenAI
|
||||
_PASSTHROUGH_KWARGS = (
|
||||
"timeout", "max_retries", "reasoning_effort",
|
||||
@@ -183,9 +205,14 @@ class OpenAIClient(BaseLLMClient):
|
||||
if self.provider == "openai":
|
||||
llm_kwargs["use_responses_api"] = True
|
||||
|
||||
# DeepSeek's thinking-mode quirks live in their own subclass so the
|
||||
# base NormalizedChatOpenAI stays free of provider-specific branches.
|
||||
chat_cls = DeepSeekChatOpenAI if self.provider == "deepseek" else NormalizedChatOpenAI
|
||||
# Provider-specific quirks live in their own subclasses so the
|
||||
# base NormalizedChatOpenAI stays free of provider branches.
|
||||
if self.provider == "deepseek":
|
||||
chat_cls = DeepSeekChatOpenAI
|
||||
elif self.provider in ("minimax", "minimax-cn"):
|
||||
chat_cls = MinimaxChatOpenAI
|
||||
else:
|
||||
chat_cls = NormalizedChatOpenAI
|
||||
return chat_cls(**llm_kwargs)
|
||||
|
||||
def validate_model(self) -> bool:
|
||||
|
||||
Reference in New Issue
Block a user