feat: add MiniMax as alternative LLM provider for Finogrid agents

Add MiniMax MiniMax-M2.5 (204K context) as a cost-effective alternative
to OpenAI for sentiment analysis and agent LLM operations. Uses the
existing openai SDK via MiniMax's OpenAI-compatible API endpoint.

Changes:
- MiniMaxSentimentProvider: drop-in replacement for OpenAISentimentFallback
- MiniMaxLLMClient: generic async LLM client for agent use
- Updated factory to support FINGPT_LLM_PROVIDER=minimax
- Added MINIMAX_API_KEY to .env.example and docs
- 24 unit tests + 4 integration tests (all passing)
This commit is contained in:
PR Bot
2026-03-17 11:47:46 +08:00
parent d8d10ffae8
commit 74417db064
10 changed files with 605 additions and 9 deletions
+10
View File
@@ -202,6 +202,16 @@ The datasets we used, and the **multi-task financial LLM** models are available
## Cloud LLM Providers for FinGPT Inference
For the Finogrid platform (finogrid/), FinGPT supports multiple cloud LLM providers as alternatives to running local models. Set `FINGPT_LLM_PROVIDER` in your environment:
| Provider | Model | Context Length | Use Case |
|----------|-------|---------------|----------|
| OpenAI | GPT-3.5-turbo | 16K | Default fallback for sentiment & agents |
| [MiniMax](https://platform.minimaxi.com/) | MiniMax-M2.5 | 204K | Cost-effective alternative with large context window |
| FinGPT (local) | Llama-2-13B LoRA | 4K | Full local inference (requires GPU) |
## Open-Source Base Model used in the LLMs layer of FinGPT
* Feel free to contribute more open-source base models tailored for various language-specific financial markets.
+3
View File
@@ -26,7 +26,10 @@ JWT_ALGORITHM=HS256
JWT_ACCESS_TOKEN_EXPIRE_MINUTES=60
# ── FinGPT / AI (agents only — NOT in hot path) ───────────────────────────────
# LLM provider for sentiment analysis and agent clients: "openai" | "minimax" | "fingpt"
FINGPT_LLM_PROVIDER=openai
OPENAI_API_KEY=your_openai_api_key_here
MINIMAX_API_KEY=your_minimax_api_key_here
HUGGINGFACE_TOKEN=your_huggingface_token_here
FINGPT_MODEL_PATH=FinGPT/fingpt-sentiment_llama2-13b_lora
FINGPT_FORECASTER_MODEL=FinGPT/fingpt-forecaster_dow30_llama2-7b_lora
+3 -1
View File
@@ -173,7 +173,7 @@ const tx = await finogrid.micropay.pay({
| Database | AlloyDB (PostgreSQL) / SQLAlchemy 2.0 async |
| Messaging | GCP Pub/Sub |
| On-chain | Base L2 (native USDC, ~$0.007/tx, 210s confirmation) |
| AI (inference) | FinGPT Llama-2 LoRA + OpenAI fallback |
| AI (inference) | FinGPT Llama-2 LoRA + OpenAI / [MiniMax](https://platform.minimaxi.com/) fallback |
| SDK | TypeScript 5.3 (`@finogrid/agent-ledger-sdk`) |
| Infrastructure | GCP (Cloud Run, BigQuery, Secret Manager, IAM) |
@@ -272,7 +272,9 @@ python -m mcp.plaid.server &
| `PLAID_SECRET` | Plaid MCP | Plaid secret |
| `KYA_VALIDATOR_BACKEND` | KYA MCP | internal \| sardine \| persona |
| `OPS_API_KEY` | ops console | Ops-level auth key |
| `FINGPT_LLM_PROVIDER` | agents | LLM provider: `openai` \| `minimax` \| `fingpt` |
| `OPENAI_API_KEY` | agents | FinGPT OpenAI fallback |
| `MINIMAX_API_KEY` | agents | [MiniMax](https://platform.minimaxi.com/) API key (when provider=minimax) |
| `PUBSUB_PROJECT_ID` | workers | GCP Pub/Sub project |
See `.env.example` for the full list.
+9 -1
View File
@@ -13,9 +13,17 @@ What we do NOT use:
- Trading strategies (FinGPT_Others)
- v1 sentiment models (superseded by v3)
For MVP: set USE_OPENAI_FALLBACK=true to use GPT-3.5 with FinGPT prompts
For MVP: set FINGPT_USE_OPENAI_FALLBACK=true to use GPT-3.5 with FinGPT prompts
instead of loading the full 13B model locally. Same quality for early stage.
LLM provider selection (for sentiment analysis and agent LLM client):
FINGPT_LLM_PROVIDER=openai → OpenAI GPT-3.5-turbo (default)
FINGPT_LLM_PROVIDER=minimax → MiniMax MiniMax-M2.5 (204K context, cost-effective)
FINGPT_LLM_PROVIDER=fingpt → Local FinGPT model (requires GPU)
"""
import os
USE_OPENAI_FALLBACK = os.getenv("FINGPT_USE_OPENAI_FALLBACK", "true").lower() == "true"
# Provider selection: "openai" (default), "minimax", or "fingpt" (local model)
LLM_PROVIDER = os.getenv("FINGPT_LLM_PROVIDER", "openai").lower()
@@ -0,0 +1,70 @@
"""
MiniMax LLM client for Finogrid agents.
Provides a generic ``await client.chat(prompt)`` interface that any Finogrid
agent can use (InternalSupport, AuditGovernance, etc.).
Uses MiniMax's OpenAI-compatible API so the existing ``openai`` dependency
is reused — no new packages required.
"""
from __future__ import annotations
import os
import structlog
from openai import AsyncOpenAI
log = structlog.get_logger()
MINIMAX_BASE_URL = "https://api.minimax.io/v1"
class MiniMaxLLMClient:
"""
Async LLM client backed by MiniMax's API.
Compatible with the ``llm_client`` parameter accepted by all Finogrid
agents (``InternalSupportAgent``, ``AuditGovernanceAgent``, etc.).
Usage::
from finogrid.fingpt_integration.minimax_llm_client import MiniMaxLLMClient
client = MiniMaxLLMClient()
agent = InternalSupportAgent(knowledge_base=kb, llm_client=client)
"""
def __init__(
self,
model: str = "MiniMax-M2.5",
temperature: float = 0.7,
max_tokens: int = 1024,
):
self.model = model
# MiniMax requires temperature in (0.0, 1.0]
self.temperature = max(0.01, min(temperature, 1.0))
self.max_tokens = max_tokens
api_key = os.getenv("MINIMAX_API_KEY")
if not api_key:
raise ValueError(
"MINIMAX_API_KEY environment variable is required. "
"Get your API key at https://platform.minimaxi.com/"
)
self.client = AsyncOpenAI(
api_key=api_key,
base_url=MINIMAX_BASE_URL,
)
async def chat(self, prompt: str) -> str:
"""Send a prompt and return the assistant's reply text."""
try:
response = await self.client.chat.completions.create(
model=self.model,
messages=[{"role": "user", "content": prompt}],
temperature=self.temperature,
max_tokens=self.max_tokens,
)
return response.choices[0].message.content.strip()
except Exception as e:
log.error("minimax_llm_chat_failed", error=str(e))
raise
@@ -0,0 +1,86 @@
"""
MiniMax provider for FinGPT sentiment — cost-effective alternative to OpenAI.
Uses MiniMax's OpenAI-compatible API with MiniMax-M2.5 (204K context).
Same interface as OpenAISentimentFallback — drop-in replacement.
MiniMax API docs: https://platform.minimaxi.com/
"""
from __future__ import annotations
import os
import structlog
from openai import AsyncOpenAI
log = structlog.get_logger()
SENTIMENT_PROMPT = (
"Instruction: What is the sentiment of this news? "
"Please choose an answer from {{positive/negative/neutral}}.\n"
"Input: {text}\n"
"Answer:"
)
SENTIMENT_MAP = {"positive": 1, "negative": -1, "neutral": 0}
MINIMAX_BASE_URL = "https://api.minimax.io/v1"
class MiniMaxSentimentProvider:
"""
Drop-in replacement for OpenAISentimentFallback using MiniMax API.
Same interface — swap by setting FINGPT_LLM_PROVIDER=minimax.
MiniMax's API is OpenAI-compatible, so we reuse the openai SDK
with a custom base_url.
"""
def __init__(self, model: str = "MiniMax-M2.5"):
self.model = model
api_key = os.getenv("MINIMAX_API_KEY")
if not api_key:
raise ValueError(
"MINIMAX_API_KEY environment variable is required. "
"Get your API key at https://platform.minimaxi.com/"
)
self.client = AsyncOpenAI(
api_key=api_key,
base_url=MINIMAX_BASE_URL,
)
def load(self):
pass # Nothing to load for MiniMax
async def score(self, text: str) -> dict:
prompt = SENTIMENT_PROMPT.format(text=text[:512])
try:
response = await self.client.chat.completions.create(
model=self.model,
messages=[{"role": "user", "content": prompt}],
max_tokens=5,
# MiniMax requires temperature in (0.0, 1.0]; use 0.01 for near-deterministic output
temperature=0.01,
)
answer = response.choices[0].message.content.strip().lower()
label = "neutral"
for key in SENTIMENT_MAP:
if key in answer:
label = key
break
return {"label": label, "score": SENTIMENT_MAP[label], "raw": answer}
except Exception as e:
log.error("minimax_sentiment_failed", error=str(e))
return {"label": "neutral", "score": 0, "error": str(e)}
async def score_corridor_news(self, news_items: list[dict], corridor_code: str) -> list[dict]:
results = []
for item in news_items:
text = f"{item.get('headline', '')}. {item.get('summary', '')}"
sentiment = await self.score(text)
results.append({
**item,
"corridor": corridor_code,
"sentiment_label": sentiment["label"],
"sentiment_score": sentiment["score"],
})
return results
@@ -71,16 +71,31 @@ class OpenAISentimentFallback:
def get_sentiment_analyzer():
"""
Factory: returns OpenAI fallback for MVP, full FinGPT model for production.
Controlled by FINGPT_USE_OPENAI_FALLBACK env var.
Factory: returns the configured sentiment provider.
Provider selection (in order of precedence):
1. FINGPT_LLM_PROVIDER env var: "openai" | "minimax" | "fingpt"
2. FINGPT_USE_OPENAI_FALLBACK env var (legacy): "true" → OpenAI, "false" → FinGPT model
Examples:
FINGPT_LLM_PROVIDER=minimax → MiniMax MiniMax-M2.5
FINGPT_LLM_PROVIDER=openai → OpenAI GPT-3.5-turbo (default)
FINGPT_LLM_PROVIDER=fingpt → Local FinGPT Llama-2 model (requires GPU)
"""
from .. import USE_OPENAI_FALLBACK
if USE_OPENAI_FALLBACK:
log.info("sentiment_using_openai_fallback")
return OpenAISentimentFallback()
else:
from .. import LLM_PROVIDER, USE_OPENAI_FALLBACK
if LLM_PROVIDER == "minimax":
from .minimax_provider import MiniMaxSentimentProvider
log.info("sentiment_using_minimax")
return MiniMaxSentimentProvider()
if LLM_PROVIDER == "fingpt" or not USE_OPENAI_FALLBACK:
from .crypto_sentiment import FinoGridSentimentAnalyzer
log.info("sentiment_using_fingpt_model")
analyzer = FinoGridSentimentAnalyzer()
analyzer.load()
return analyzer
# Default: OpenAI
log.info("sentiment_using_openai_fallback")
return OpenAISentimentFallback()
@@ -0,0 +1,79 @@
"""
Integration tests for MiniMax provider.
These tests call the real MiniMax API and require:
- MINIMAX_API_KEY environment variable set
Run with:
MINIMAX_API_KEY=your_key pytest finogrid/tests/integration/test_minimax_integration.py -v
"""
import os
import pytest
pytestmark = pytest.mark.skipif(
not os.getenv("MINIMAX_API_KEY"),
reason="MINIMAX_API_KEY not set — skipping live integration tests",
)
@pytest.mark.asyncio
async def test_minimax_sentiment_live():
"""Call MiniMax API to score a financial headline and verify the response shape."""
from finogrid.fingpt_integration.sentiment.minimax_provider import MiniMaxSentimentProvider
provider = MiniMaxSentimentProvider()
result = await provider.score("Apple stock surges to all-time high on strong earnings")
assert "label" in result
assert result["label"] in ("positive", "negative", "neutral")
assert "score" in result
assert result["score"] in (1, 0, -1)
assert "raw" in result
@pytest.mark.asyncio
async def test_minimax_sentiment_corridor_news_live():
"""Score a batch of corridor news items via the MiniMax API."""
from finogrid.fingpt_integration.sentiment.minimax_provider import MiniMaxSentimentProvider
provider = MiniMaxSentimentProvider()
news = [
{"headline": "Brazil GDP grows 3%", "summary": "Economy beats expectations"},
{"headline": "PIX outage nationwide", "summary": "Central bank investigates"},
]
results = await provider.score_corridor_news(news, "BR")
assert len(results) == 2
for r in results:
assert r["corridor"] == "BR"
assert r["sentiment_label"] in ("positive", "negative", "neutral")
assert r["sentiment_score"] in (1, 0, -1)
@pytest.mark.asyncio
async def test_minimax_llm_client_live():
"""Call the MiniMax LLM client and verify it returns a non-empty string."""
from finogrid.fingpt_integration.minimax_llm_client import MiniMaxLLMClient
client = MiniMaxLLMClient()
response = await client.chat("What is 2 + 2? Answer with just the number.")
assert isinstance(response, str)
assert len(response) > 0
assert "4" in response
@pytest.mark.asyncio
async def test_minimax_llm_client_with_agent():
"""Verify MiniMaxLLMClient works as an llm_client for InternalSupportAgent."""
from finogrid.fingpt_integration.minimax_llm_client import MiniMaxLLMClient
from finogrid.agents.internal_support.agent import InternalSupportAgent
client = MiniMaxLLMClient()
agent = InternalSupportAgent(knowledge_base=None, llm_client=client)
result = await agent.answer("What is Finogrid?")
assert "answer" in result
assert isinstance(result["answer"], str)
assert len(result["answer"]) > 0
@@ -0,0 +1,323 @@
"""Unit tests for MiniMax sentiment provider and LLM client."""
import os
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
# ---------------------------------------------------------------------------
# MiniMaxSentimentProvider tests
# ---------------------------------------------------------------------------
class TestMiniMaxSentimentProvider:
"""Tests for MiniMaxSentimentProvider."""
@patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key"})
def test_init_with_api_key(self):
from finogrid.fingpt_integration.sentiment.minimax_provider import MiniMaxSentimentProvider
provider = MiniMaxSentimentProvider()
assert provider.model == "MiniMax-M2.5"
assert provider.client is not None
@patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key"})
def test_init_custom_model(self):
from finogrid.fingpt_integration.sentiment.minimax_provider import MiniMaxSentimentProvider
provider = MiniMaxSentimentProvider(model="MiniMax-M2.5-highspeed")
assert provider.model == "MiniMax-M2.5-highspeed"
@patch.dict(os.environ, {}, clear=True)
def test_init_missing_api_key(self):
# Remove MINIMAX_API_KEY if set
os.environ.pop("MINIMAX_API_KEY", None)
from finogrid.fingpt_integration.sentiment.minimax_provider import MiniMaxSentimentProvider
with pytest.raises(ValueError, match="MINIMAX_API_KEY"):
MiniMaxSentimentProvider()
@patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key"})
def test_load_is_noop(self):
from finogrid.fingpt_integration.sentiment.minimax_provider import MiniMaxSentimentProvider
provider = MiniMaxSentimentProvider()
provider.load() # Should not raise
@pytest.mark.asyncio
@patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key"})
async def test_score_positive(self):
from finogrid.fingpt_integration.sentiment.minimax_provider import MiniMaxSentimentProvider
provider = MiniMaxSentimentProvider()
mock_response = MagicMock()
mock_response.choices = [MagicMock()]
mock_response.choices[0].message.content = "positive"
provider.client.chat.completions.create = AsyncMock(return_value=mock_response)
result = await provider.score("Apple stock surges to all-time high")
assert result["label"] == "positive"
assert result["score"] == 1
assert result["raw"] == "positive"
@pytest.mark.asyncio
@patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key"})
async def test_score_negative(self):
from finogrid.fingpt_integration.sentiment.minimax_provider import MiniMaxSentimentProvider
provider = MiniMaxSentimentProvider()
mock_response = MagicMock()
mock_response.choices = [MagicMock()]
mock_response.choices[0].message.content = "negative"
provider.client.chat.completions.create = AsyncMock(return_value=mock_response)
result = await provider.score("Market crashes amid recession fears")
assert result["label"] == "negative"
assert result["score"] == -1
@pytest.mark.asyncio
@patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key"})
async def test_score_neutral(self):
from finogrid.fingpt_integration.sentiment.minimax_provider import MiniMaxSentimentProvider
provider = MiniMaxSentimentProvider()
mock_response = MagicMock()
mock_response.choices = [MagicMock()]
mock_response.choices[0].message.content = "neutral"
provider.client.chat.completions.create = AsyncMock(return_value=mock_response)
result = await provider.score("Fed holds rates steady as expected")
assert result["label"] == "neutral"
assert result["score"] == 0
@pytest.mark.asyncio
@patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key"})
async def test_score_api_error_returns_neutral(self):
from finogrid.fingpt_integration.sentiment.minimax_provider import MiniMaxSentimentProvider
provider = MiniMaxSentimentProvider()
provider.client.chat.completions.create = AsyncMock(side_effect=Exception("API error"))
result = await provider.score("Some news text")
assert result["label"] == "neutral"
assert result["score"] == 0
assert "error" in result
@pytest.mark.asyncio
@patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key"})
async def test_score_temperature_is_low(self):
"""Verify MiniMax uses near-zero temperature (0.01) for deterministic output."""
from finogrid.fingpt_integration.sentiment.minimax_provider import MiniMaxSentimentProvider
provider = MiniMaxSentimentProvider()
mock_response = MagicMock()
mock_response.choices = [MagicMock()]
mock_response.choices[0].message.content = "positive"
provider.client.chat.completions.create = AsyncMock(return_value=mock_response)
await provider.score("test")
call_kwargs = provider.client.chat.completions.create.call_args[1]
assert call_kwargs["temperature"] == 0.01
assert call_kwargs["max_tokens"] == 5
@pytest.mark.asyncio
@patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key"})
async def test_score_corridor_news(self):
from finogrid.fingpt_integration.sentiment.minimax_provider import MiniMaxSentimentProvider
provider = MiniMaxSentimentProvider()
mock_response = MagicMock()
mock_response.choices = [MagicMock()]
mock_response.choices[0].message.content = "positive"
provider.client.chat.completions.create = AsyncMock(return_value=mock_response)
news = [
{"headline": "Brazil economy grows", "summary": "GDP up 3%"},
{"headline": "PIX adoption soars", "summary": "50M new users"},
]
results = await provider.score_corridor_news(news, "BR")
assert len(results) == 2
assert results[0]["corridor"] == "BR"
assert results[0]["sentiment_label"] == "positive"
assert results[0]["sentiment_score"] == 1
assert results[0]["headline"] == "Brazil economy grows"
@pytest.mark.asyncio
@patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key"})
async def test_score_truncates_long_text(self):
from finogrid.fingpt_integration.sentiment.minimax_provider import MiniMaxSentimentProvider
provider = MiniMaxSentimentProvider()
mock_response = MagicMock()
mock_response.choices = [MagicMock()]
mock_response.choices[0].message.content = "neutral"
provider.client.chat.completions.create = AsyncMock(return_value=mock_response)
long_text = "x" * 1000
await provider.score(long_text)
call_kwargs = provider.client.chat.completions.create.call_args[1]
prompt = call_kwargs["messages"][0]["content"]
# The prompt should contain at most 512 chars of the input text
assert len(prompt) < 1000
@patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key"})
def test_base_url_is_minimax(self):
from finogrid.fingpt_integration.sentiment.minimax_provider import MiniMaxSentimentProvider
provider = MiniMaxSentimentProvider()
assert str(provider.client.base_url).rstrip("/").endswith("api.minimax.io/v1")
# ---------------------------------------------------------------------------
# MiniMaxLLMClient tests
# ---------------------------------------------------------------------------
class TestMiniMaxLLMClient:
"""Tests for MiniMaxLLMClient."""
@patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key"})
def test_init_defaults(self):
from finogrid.fingpt_integration.minimax_llm_client import MiniMaxLLMClient
client = MiniMaxLLMClient()
assert client.model == "MiniMax-M2.5"
assert client.temperature == 0.7
assert client.max_tokens == 1024
@patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key"})
def test_init_custom_params(self):
from finogrid.fingpt_integration.minimax_llm_client import MiniMaxLLMClient
client = MiniMaxLLMClient(
model="MiniMax-M2.5-highspeed",
temperature=0.5,
max_tokens=2048,
)
assert client.model == "MiniMax-M2.5-highspeed"
assert client.temperature == 0.5
assert client.max_tokens == 2048
@patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key"})
def test_temperature_clamped_to_min(self):
"""MiniMax requires temperature > 0; verify clamping to 0.01."""
from finogrid.fingpt_integration.minimax_llm_client import MiniMaxLLMClient
client = MiniMaxLLMClient(temperature=0.0)
assert client.temperature == 0.01
@patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key"})
def test_temperature_clamped_to_max(self):
"""MiniMax requires temperature <= 1.0; verify clamping."""
from finogrid.fingpt_integration.minimax_llm_client import MiniMaxLLMClient
client = MiniMaxLLMClient(temperature=2.0)
assert client.temperature == 1.0
@patch.dict(os.environ, {}, clear=True)
def test_init_missing_api_key(self):
os.environ.pop("MINIMAX_API_KEY", None)
from finogrid.fingpt_integration.minimax_llm_client import MiniMaxLLMClient
with pytest.raises(ValueError, match="MINIMAX_API_KEY"):
MiniMaxLLMClient()
@pytest.mark.asyncio
@patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key"})
async def test_chat_returns_text(self):
from finogrid.fingpt_integration.minimax_llm_client import MiniMaxLLMClient
client = MiniMaxLLMClient()
mock_response = MagicMock()
mock_response.choices = [MagicMock()]
mock_response.choices[0].message.content = "This is a test response."
client.client.chat.completions.create = AsyncMock(return_value=mock_response)
result = await client.chat("Hello, world!")
assert result == "This is a test response."
@pytest.mark.asyncio
@patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key"})
async def test_chat_strips_whitespace(self):
from finogrid.fingpt_integration.minimax_llm_client import MiniMaxLLMClient
client = MiniMaxLLMClient()
mock_response = MagicMock()
mock_response.choices = [MagicMock()]
mock_response.choices[0].message.content = " response with spaces \n"
client.client.chat.completions.create = AsyncMock(return_value=mock_response)
result = await client.chat("prompt")
assert result == "response with spaces"
@pytest.mark.asyncio
@patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key"})
async def test_chat_api_error_raises(self):
from finogrid.fingpt_integration.minimax_llm_client import MiniMaxLLMClient
client = MiniMaxLLMClient()
client.client.chat.completions.create = AsyncMock(side_effect=Exception("API error"))
with pytest.raises(Exception, match="API error"):
await client.chat("prompt")
@pytest.mark.asyncio
@patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key"})
async def test_chat_passes_correct_params(self):
from finogrid.fingpt_integration.minimax_llm_client import MiniMaxLLMClient
client = MiniMaxLLMClient(model="MiniMax-M2.5-highspeed", temperature=0.3, max_tokens=512)
mock_response = MagicMock()
mock_response.choices = [MagicMock()]
mock_response.choices[0].message.content = "ok"
client.client.chat.completions.create = AsyncMock(return_value=mock_response)
await client.chat("test prompt")
call_kwargs = client.client.chat.completions.create.call_args[1]
assert call_kwargs["model"] == "MiniMax-M2.5-highspeed"
assert call_kwargs["temperature"] == 0.3
assert call_kwargs["max_tokens"] == 512
assert call_kwargs["messages"] == [{"role": "user", "content": "test prompt"}]
@patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key"})
def test_base_url_is_minimax(self):
from finogrid.fingpt_integration.minimax_llm_client import MiniMaxLLMClient
client = MiniMaxLLMClient()
assert str(client.client.base_url).rstrip("/").endswith("api.minimax.io/v1")
# ---------------------------------------------------------------------------
# Factory function tests
# ---------------------------------------------------------------------------
class TestGetSentimentAnalyzerFactory:
"""Tests for the get_sentiment_analyzer() factory."""
@patch.dict(os.environ, {
"MINIMAX_API_KEY": "test-key",
"FINGPT_LLM_PROVIDER": "minimax",
"FINGPT_USE_OPENAI_FALLBACK": "true",
})
def test_factory_returns_minimax_provider(self):
# Need to reload modules to pick up env changes
import importlib
import finogrid.fingpt_integration
importlib.reload(finogrid.fingpt_integration)
from finogrid.fingpt_integration.sentiment.minimax_provider import MiniMaxSentimentProvider
from finogrid.fingpt_integration.sentiment.openai_fallback import get_sentiment_analyzer
analyzer = get_sentiment_analyzer()
assert isinstance(analyzer, MiniMaxSentimentProvider)
@patch.dict(os.environ, {
"OPENAI_API_KEY": "test-key",
"FINGPT_LLM_PROVIDER": "openai",
"FINGPT_USE_OPENAI_FALLBACK": "true",
})
def test_factory_returns_openai_provider(self):
import importlib
import finogrid.fingpt_integration
importlib.reload(finogrid.fingpt_integration)
from finogrid.fingpt_integration.sentiment.openai_fallback import (
get_sentiment_analyzer, OpenAISentimentFallback,
)
analyzer = get_sentiment_analyzer()
assert isinstance(analyzer, OpenAISentimentFallback)