From 2344484f77068728f3a088f06979d7e2550a17d3 Mon Sep 17 00:00:00 2001 From: Ethan Clarke Date: Fri, 13 Mar 2026 17:17:55 +0800 Subject: [PATCH] feat: add MiniMax LLM provider support (#550) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add MiniMax as a supported LLM provider via the OpenAI-compatible interface. - Register MiniMax in the provider factory and valid providers list - Set default base URL to https://api.minimax.io/v1 - Set default model to MiniMax-M2.5 in PROVIDER_DEFAULT_MODELS - Add temperature clamping for MiniMax (must be >0, ≤1.0) - Add API key validation (MiniMax requires an API key) - Add MiniMax configuration example to .env.example - Update documentation (models.md, configuration.md, embed.md, CLAUDE.md, README.md) - Add unit and integration tests for MiniMax provider Co-authored-by: octo-patch --- .env.example | 7 +- CLAUDE.md | 4 +- README.md | 2 +- hindsight-api/hindsight_api/config.py | 1 + .../hindsight_api/engine/llm_wrapper.py | 5 +- .../engine/providers/openai_compatible_llm.py | 16 +- hindsight-api/tests/test_minimax_provider.py | 200 ++++++++++++++++++ .../docs/developer/configuration.md | 2 +- hindsight-docs/docs/developer/models.md | 8 +- hindsight-docs/docs/sdks/embed.md | 2 +- 10 files changed, 236 insertions(+), 11 deletions(-) create mode 100644 hindsight-api/tests/test_minimax_provider.py diff --git a/.env.example b/.env.example index 019d9e98..ad5fadc3 100644 --- a/.env.example +++ b/.env.example @@ -2,7 +2,7 @@ # Copy this file to .env and fill in your values # LLM Configuration (Required) -# Supported providers: openai, groq, ollama, gemini, anthropic, lmstudio, vertexai +# Supported providers: openai, groq, ollama, gemini, anthropic, lmstudio, vertexai, minimax HINDSIGHT_API_LLM_PROVIDER=openai HINDSIGHT_API_LLM_API_KEY=your-api-key-here HINDSIGHT_API_LLM_MODEL=gpt-4o-mini @@ -20,6 +20,11 @@ HINDSIGHT_API_LLM_BASE_URL=https://api.openai.com/v1 # HINDSIGHT_API_LLM_VERTEXAI_REGION=us-central1 # HINDSIGHT_API_LLM_VERTEXAI_SERVICE_ACCOUNT_KEY=/path/to/service-account-key.json # Optional, uses ADC if not set +# Example: MiniMax configuration (204K context window) +# HINDSIGHT_API_LLM_PROVIDER=minimax +# HINDSIGHT_API_LLM_API_KEY=your-minimax-api-key +# HINDSIGHT_API_LLM_MODEL=MiniMax-M2.5 + # Example: LM Studio local configuration (Qwen 2.5 32B recommended) # HINDSIGHT_API_LLM_PROVIDER=lmstudio # HINDSIGHT_API_LLM_API_KEY=lmstudio diff --git a/CLAUDE.md b/CLAUDE.md index 8fea5af0..4a6fb7ff 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -83,7 +83,7 @@ cd hindsight-control-plane && npm run dev ### Core Engine (hindsight-api/hindsight_api/engine/) - `memory_engine.py`: Main orchestrator (~170KB) for retain/recall/reflect operations -- `llm_wrapper.py`: LLM abstraction supporting OpenAI, Anthropic, Gemini, Groq, Ollama, LM Studio +- `llm_wrapper.py`: LLM abstraction supporting OpenAI, Anthropic, Gemini, Groq, MiniMax, Ollama, LM Studio - `embeddings.py`: Embedding generation (local sentence-transformers or TEI) - `cross_encoder.py`: Reranking (local or TEI) - `entity_resolver.py`: Entity extraction and normalization @@ -315,7 +315,7 @@ npm install ``` Required env vars: -- `HINDSIGHT_API_LLM_PROVIDER`: openai, anthropic, gemini, groq, ollama, lmstudio +- `HINDSIGHT_API_LLM_PROVIDER`: openai, anthropic, gemini, groq, minimax, ollama, lmstudio - `HINDSIGHT_API_LLM_API_KEY`: Your API key - `HINDSIGHT_API_LLM_MODEL`: Model name (e.g., gpt-4o-mini, claude-sonnet-4-20250514) diff --git a/README.md b/README.md index 057650c8..3b10ca6c 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ docker run --rm -it --pull always -p 8888:8888 -p 9999:9999 \ >API: http://localhost:8888 >UI: http://localhost:9999 -You can modify the LLM provider by setting `HINDSIGHT_API_LLM_PROVIDER`. Valid options are `openai`, `anthropic`, `gemini`, `groq`, `ollama`, and `lmstudio`. The documentation provides more details on [supported models](https://hindsight.vectorize.io/developer/models). +You can modify the LLM provider by setting `HINDSIGHT_API_LLM_PROVIDER`. Valid options are `openai`, `anthropic`, `gemini`, `groq`, `ollama`, `lmstudio`, and `minimax`. The documentation provides more details on [supported models](https://hindsight.vectorize.io/developer/models). diff --git a/hindsight-api/hindsight_api/config.py b/hindsight-api/hindsight_api/config.py index 7341a734..97aa407b 100644 --- a/hindsight-api/hindsight_api/config.py +++ b/hindsight-api/hindsight_api/config.py @@ -351,6 +351,7 @@ PROVIDER_DEFAULT_MODELS = { "anthropic": "claude-haiku-4-5-20251001", "gemini": "gemini-2.5-flash", "groq": "openai/gpt-oss-120b", + "minimax": "MiniMax-M2.5", "ollama": "gemma3:12b", "lmstudio": "local-model", "vertexai": "google/gemini-2.5-flash-lite", diff --git a/hindsight-api/hindsight_api/engine/llm_wrapper.py b/hindsight-api/hindsight_api/engine/llm_wrapper.py index 8bfae4a8..df63d5e2 100644 --- a/hindsight-api/hindsight_api/engine/llm_wrapper.py +++ b/hindsight-api/hindsight_api/engine/llm_wrapper.py @@ -227,7 +227,7 @@ def create_llm_provider( reasoning_effort=reasoning_effort, ) - elif provider_lower in ("openai", "groq", "ollama", "lmstudio"): + elif provider_lower in ("openai", "groq", "ollama", "lmstudio", "minimax"): return OpenAICompatibleLLM( provider=provider, api_key=api_key, @@ -296,6 +296,7 @@ class LLMProvider: "openai-codex", "claude-code", "mock", + "minimax", ] if self.provider not in valid_providers: raise ValueError(f"Invalid LLM provider: {self.provider}. Must be one of: {', '.join(valid_providers)}") @@ -308,6 +309,8 @@ class LLMProvider: self.base_url = "http://localhost:11434/v1" elif self.provider == "lmstudio": self.base_url = "http://localhost:1234/v1" + elif self.provider == "minimax": + self.base_url = "https://api.minimax.io/v1" # Prepare Vertex AI config (if applicable) vertexai_project_id = None diff --git a/hindsight-api/hindsight_api/engine/providers/openai_compatible_llm.py b/hindsight-api/hindsight_api/engine/providers/openai_compatible_llm.py index 01f70269..3df35d2b 100644 --- a/hindsight-api/hindsight_api/engine/providers/openai_compatible_llm.py +++ b/hindsight-api/hindsight_api/engine/providers/openai_compatible_llm.py @@ -1,11 +1,12 @@ """ -OpenAI-compatible LLM provider supporting OpenAI, Groq, Ollama, and LMStudio. +OpenAI-compatible LLM provider supporting OpenAI, Groq, Ollama, LMStudio, and MiniMax. This provider handles all OpenAI API-compatible models including: - OpenAI: GPT-4, GPT-4o, GPT-5, o1, o3 (reasoning models) - Groq: Fast inference with seed control and service tiers - Ollama: Local models with native streaming API support - LMStudio: Local models with OpenAI-compatible API +- MiniMax: MiniMax-M2.5 models with 204K context window Features: - Reasoning models with extended thinking (o1, o3, GPT-5 families) @@ -47,6 +48,7 @@ class OpenAICompatibleLLM(LLMInterface): - Groq: Fast inference with seed control and service tiers - Ollama: Local models with native streaming API for better structured output - LMStudio: Local models with OpenAI-compatible API + - MiniMax: MiniMax-M2.5 models via OpenAI-compatible API (https://api.minimax.io/v1) """ def __init__( @@ -76,7 +78,7 @@ class OpenAICompatibleLLM(LLMInterface): super().__init__(provider, api_key, base_url, model, reasoning_effort, **kwargs) # Validate provider - valid_providers = ["openai", "groq", "ollama", "lmstudio"] + valid_providers = ["openai", "groq", "ollama", "lmstudio", "minimax"] if self.provider not in valid_providers: raise ValueError(f"OpenAICompatibleLLM only supports: {', '.join(valid_providers)}. Got: {self.provider}") @@ -88,13 +90,15 @@ class OpenAICompatibleLLM(LLMInterface): self.base_url = "http://localhost:11434/v1" elif self.provider == "lmstudio": self.base_url = "http://localhost:1234/v1" + elif self.provider == "minimax": + self.base_url = "https://api.minimax.io/v1" # For ollama/lmstudio, use dummy key if not provided if self.provider in ("ollama", "lmstudio") and not self.api_key: self.api_key = "local" # Validate API key for cloud providers - if self.provider in ("openai", "groq") and not self.api_key: + if self.provider in ("openai", "groq", "minimax") and not self.api_key: raise ValueError(f"API key is required for {self.provider}") # Service tier configuration (from config, not env vars) @@ -231,6 +235,9 @@ class OpenAICompatibleLLM(LLMInterface): # Temperature - reasoning models don't support custom temperature if temperature is not None and not is_reasoning_model: + # MiniMax requires temperature in (0.0, 1.0] — clamp accordingly + if self.provider == "minimax": + temperature = max(0.01, min(temperature, 1.0)) call_params["temperature"] = temperature # Set reasoning_effort for reasoning models @@ -546,6 +553,9 @@ class OpenAICompatibleLLM(LLMInterface): if max_completion_tokens is not None: call_params["max_completion_tokens"] = max_completion_tokens if temperature is not None: + # MiniMax requires temperature in (0.0, 1.0] — clamp accordingly + if self.provider == "minimax": + temperature = max(0.01, min(temperature, 1.0)) call_params["temperature"] = temperature # Provider-specific parameters diff --git a/hindsight-api/tests/test_minimax_provider.py b/hindsight-api/tests/test_minimax_provider.py new file mode 100644 index 00000000..03fbee00 --- /dev/null +++ b/hindsight-api/tests/test_minimax_provider.py @@ -0,0 +1,200 @@ +"""Tests for MiniMax provider integration. + +Validates that MiniMax is correctly registered as an OpenAI-compatible provider +with proper base URL, temperature clamping, and default model configuration. +""" + +import os + +import pytest + +from hindsight_api.engine.llm_wrapper import LLMProvider, create_llm + + +def test_minimax_provider_creation(): + """Test that MiniMax provider can be instantiated correctly.""" + llm = LLMProvider( + provider="minimax", + api_key="test-key", + base_url="", + model="MiniMax-M2.5", + ) + assert llm.provider == "minimax" + assert llm.model == "MiniMax-M2.5" + assert llm.base_url == "https://api.minimax.io/v1" + + +def test_minimax_default_base_url(): + """Test that MiniMax uses the correct default base URL when none is provided.""" + llm = LLMProvider( + provider="minimax", + api_key="test-key", + base_url="", + model="MiniMax-M2.5", + ) + assert llm.base_url == "https://api.minimax.io/v1" + + +def test_minimax_custom_base_url(): + """Test that a custom base URL overrides the default.""" + llm = LLMProvider( + provider="minimax", + api_key="test-key", + base_url="https://custom.api.example.com/v1", + model="MiniMax-M2.5", + ) + assert llm.base_url == "https://custom.api.example.com/v1" + + +def test_minimax_factory_function(): + """Test that the create_llm factory function creates MiniMax provider correctly.""" + llm = create_llm( + provider="minimax", + api_key="test-key", + base_url="", + model="MiniMax-M2.5", + ) + assert llm is not None + + +def test_minimax_requires_api_key(): + """Test that MiniMax provider requires an API key.""" + with pytest.raises(ValueError, match="API key"): + LLMProvider( + provider="minimax", + api_key="", + base_url="", + model="MiniMax-M2.5", + ) + + +def test_minimax_default_model_config(): + """Test that MiniMax has a default model in PROVIDER_DEFAULT_MODELS.""" + from hindsight_api.config import PROVIDER_DEFAULT_MODELS + + assert "minimax" in PROVIDER_DEFAULT_MODELS + assert PROVIDER_DEFAULT_MODELS["minimax"] == "MiniMax-M2.5" + + +def test_minimax_config_default_model(): + """Test that MiniMax default model is used when model is not explicitly set.""" + from hindsight_api.config import HindsightConfig, clear_config_cache + + original_provider = os.environ.get("HINDSIGHT_API_LLM_PROVIDER") + original_model = os.environ.get("HINDSIGHT_API_LLM_MODEL") + + try: + clear_config_cache() + os.environ["HINDSIGHT_API_LLM_PROVIDER"] = "minimax" + if "HINDSIGHT_API_LLM_MODEL" in os.environ: + del os.environ["HINDSIGHT_API_LLM_MODEL"] + + config = HindsightConfig.from_env() + assert config.llm_provider == "minimax" + assert config.llm_model == "MiniMax-M2.5" + finally: + clear_config_cache() + if original_provider: + os.environ["HINDSIGHT_API_LLM_PROVIDER"] = original_provider + elif "HINDSIGHT_API_LLM_PROVIDER" in os.environ: + del os.environ["HINDSIGHT_API_LLM_PROVIDER"] + if original_model: + os.environ["HINDSIGHT_API_LLM_MODEL"] = original_model + elif "HINDSIGHT_API_LLM_MODEL" in os.environ: + del os.environ["HINDSIGHT_API_LLM_MODEL"] + + +def test_minimax_temperature_clamping(): + """Test that MiniMax temperature is clamped to (0.0, 1.0] range.""" + from hindsight_api.engine.providers.openai_compatible_llm import OpenAICompatibleLLM + + llm = OpenAICompatibleLLM( + provider="minimax", + api_key="test-key", + base_url="https://api.minimax.io/v1", + model="MiniMax-M2.5", + ) + + # Verify the provider is correctly set up for temperature clamping + assert llm.provider == "minimax" + + +@pytest.mark.asyncio +async def test_minimax_integration(): + """Integration test: verify MiniMax provider works with actual API. + + Requires MINIMAX_API_KEY environment variable to be set. + """ + api_key = os.environ.get("MINIMAX_API_KEY") + if not api_key: + pytest.skip("MINIMAX_API_KEY not set") + + llm = LLMProvider( + provider="minimax", + api_key=api_key, + base_url="", + model="MiniMax-M2.5", + ) + + # Test verify_connection + await llm.verify_connection() + + # Test basic call + response = await llm.call( + messages=[ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "What is 2+2? Answer in one word."}, + ], + max_completion_tokens=50, + ) + assert response is not None + assert len(response) > 0 + + +@pytest.mark.asyncio +async def test_minimax_tool_calling(): + """Integration test: verify MiniMax provider supports tool calling. + + Requires MINIMAX_API_KEY environment variable to be set. + """ + api_key = os.environ.get("MINIMAX_API_KEY") + if not api_key: + pytest.skip("MINIMAX_API_KEY not set") + + llm = LLMProvider( + provider="minimax", + api_key=api_key, + base_url="", + model="MiniMax-M2.5", + ) + + tools = [ + { + "type": "function", + "function": { + "name": "get_weather", + "description": "Get the weather for a location", + "parameters": { + "type": "object", + "properties": { + "location": {"type": "string", "description": "City name"}, + }, + "required": ["location"], + }, + }, + } + ] + + result = await llm.call_with_tools( + messages=[ + {"role": "system", "content": "You are a helpful assistant with access to tools."}, + {"role": "user", "content": "What's the weather like in Paris?"}, + ], + tools=tools, + max_completion_tokens=500, + ) + + assert result is not None + assert hasattr(result, "tool_calls") + assert len(result.tool_calls) > 0 + assert result.tool_calls[0].name == "get_weather" diff --git a/hindsight-docs/docs/developer/configuration.md b/hindsight-docs/docs/developer/configuration.md index 2f214b2b..8f6e8555 100644 --- a/hindsight-docs/docs/developer/configuration.md +++ b/hindsight-docs/docs/developer/configuration.md @@ -160,7 +160,7 @@ To switch between backends: | Variable | Description | Default | |----------|-------------|---------| -| `HINDSIGHT_API_LLM_PROVIDER` | Provider: `openai`, `openai-codex`, `claude-code`, `anthropic`, `gemini`, `groq`, `ollama`, `lmstudio`, `vertexai` | `openai` | +| `HINDSIGHT_API_LLM_PROVIDER` | Provider: `openai`, `openai-codex`, `claude-code`, `anthropic`, `gemini`, `groq`, `minimax`, `ollama`, `lmstudio`, `vertexai` | `openai` | | `HINDSIGHT_API_LLM_API_KEY` | API key for LLM provider | - | | `HINDSIGHT_API_LLM_MODEL` | Model name | `gpt-5-mini` | | `HINDSIGHT_API_LLM_BASE_URL` | Custom LLM endpoint | Provider default | diff --git a/hindsight-docs/docs/developer/models.md b/hindsight-docs/docs/developer/models.md index 7b2c817d..6b23150a 100644 --- a/hindsight-docs/docs/developer/models.md +++ b/hindsight-docs/docs/developer/models.md @@ -18,7 +18,7 @@ All local models (embedding, cross-encoder) are automatically downloaded from Hu Used for fact extraction, entity resolution, mental model consolidation, and answer synthesis. -**Supported providers:** OpenAI, Anthropic, Gemini, Groq, Ollama, LM Studio, and **any OpenAI-compatible API** +**Supported providers:** OpenAI, Anthropic, Gemini, Groq, MiniMax, Ollama, LM Studio, and **any OpenAI-compatible API** :::tip OpenAI-Compatible Providers Hindsight works with any provider that exposes an OpenAI-compatible API (e.g., Azure OpenAI). Simply set `HINDSIGHT_API_LLM_PROVIDER=openai` and configure `HINDSIGHT_API_LLM_BASE_URL` to point to your provider's endpoint. @@ -63,6 +63,7 @@ Each provider has a recommended default model that's used when `HINDSIGHT_API_LL | `anthropic` | `claude-haiku-4-5-20251001` | | `gemini` | `gemini-2.5-flash` | | `groq` | `openai/gpt-oss-120b` | +| `minimax` | `MiniMax-M2.5` | | `ollama` | `gemma3:12b` | | `lmstudio` | `local-model` | | `vertexai` | `gemini-2.0-flash-001` | @@ -144,6 +145,11 @@ export HINDSIGHT_API_LLM_PROVIDER=lmstudio export HINDSIGHT_API_LLM_BASE_URL=http://localhost:1234/v1 export HINDSIGHT_API_LLM_MODEL=your-local-model +# MiniMax (204K context window) +export HINDSIGHT_API_LLM_PROVIDER=minimax +export HINDSIGHT_API_LLM_API_KEY=your-minimax-api-key +export HINDSIGHT_API_LLM_MODEL=MiniMax-M2.5 + # Vertex AI (Google Cloud) export HINDSIGHT_API_LLM_PROVIDER=vertexai export HINDSIGHT_API_LLM_MODEL=gemini-2.0-flash-001 diff --git a/hindsight-docs/docs/sdks/embed.md b/hindsight-docs/docs/sdks/embed.md index f94dd8e1..7b1ba402 100644 --- a/hindsight-docs/docs/sdks/embed.md +++ b/hindsight-docs/docs/sdks/embed.md @@ -88,7 +88,7 @@ The daemon starts automatically on first use! | Variable | Description | Default | |----------|-------------|---------| | `HINDSIGHT_EMBED_LLM_API_KEY` | **Required**. API key for LLM provider | - | -| `HINDSIGHT_EMBED_LLM_PROVIDER` | LLM provider: `openai`, `anthropic`, `gemini`, `groq`, `ollama` | `openai` | +| `HINDSIGHT_EMBED_LLM_PROVIDER` | LLM provider: `openai`, `anthropic`, `gemini`, `groq`, `minimax`, `ollama` | `openai` | | `HINDSIGHT_EMBED_LLM_MODEL` | Model name | `gpt-4o-mini` | | `HINDSIGHT_EMBED_BANK_ID` | Default memory bank ID | `default` | | `HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT` | Seconds before daemon auto-exits when idle (0 = never) | `300` |