From 5f1e17de7c8e370651a8de38de0b92df6c07352b Mon Sep 17 00:00:00 2001 From: octo-patch Date: Tue, 24 Mar 2026 15:48:19 +0800 Subject: [PATCH] feat: add MiniMax AI as LLM provider example Add MiniMax M2.7 as an alternative LLM provider for Bindu agents via OpenAI-compatible API (agno OpenAILike). MiniMax offers models with up to 1M context window. Changes: - New example: examples/beginner/minimax_example.py using Agno + MiniMax - Update .env.example with MINIMAX_API_KEY configuration - Update README.md with MiniMax in prerequisites and LLM providers list - Update examples/README.md with MiniMax example entry - Add 24 unit tests + 3 integration tests --- README.md | 7 +- examples/README.md | 4 +- examples/beginner/.env.example | 7 + examples/beginner/minimax_example.py | 75 +++++++++ tests/unit/test_minimax_example.py | 239 +++++++++++++++++++++++++++ 5 files changed, 330 insertions(+), 2 deletions(-) create mode 100644 examples/beginner/minimax_example.py create mode 100644 tests/unit/test_minimax_example.py diff --git a/README.md b/README.md index 22aff803..8a5270c8 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ Before installing Bindu, ensure you have: - **Python 3.12 or higher** - [Download here](https://www.python.org/downloads/) - **UV package manager** - [Installation guide](https://github.com/astral-sh/uv) -- **API Key Required**: Set `OPENROUTER_API_KEY` or `OPENAI_API_KEY` in your environment variables. Free OpenRouter models are available for testing. +- **API Key Required**: Set `OPENROUTER_API_KEY`, `OPENAI_API_KEY`, or `MINIMAX_API_KEY` in your environment variables. Free OpenRouter models are available for testing. [MiniMax AI](https://platform.minimaxi.com) offers M2.7 with 1M context window. ### Verify Your Setup @@ -522,6 +522,11 @@ Bindu is **framework-agnostic** and tested with: Bindu is language-agnostic via gRPC — see [docs/grpc/](docs/grpc/) for how it works and how to add new languages. +**Compatible LLM Providers:** +- **OpenRouter** — Access 100+ models through a single API +- **OpenAI** — GPT-4o, GPT-5, and more +- **[MiniMax AI](https://platform.minimaxi.com)** — M2.7 (1M context), M2.5, M2.5-highspeed (204K context) via OpenAI-compatible API + Want integration with your favorite framework? [Let us know on Discord](https://discord.gg/3w5zuYUuwt)! --- diff --git a/examples/README.md b/examples/README.md index 2c079001..ed542449 100644 --- a/examples/README.md +++ b/examples/README.md @@ -48,6 +48,7 @@ For full URL override, use `BINDU_DEPLOYMENT_URL` (e.g. `http://127.0.0.1:5001`) - `beginner/faq_agent.py` - Documentation search agent - `beginner/agno_notion_agent.py` - Notion integration - `beginner/ag2_simple_example.py` - AG2 (AutoGen) simple agent +- `beginner/minimax_example.py` - MiniMax AI research agent (OpenAI-compatible) - `beginner/dspy_agent.py` - DSPy framework integration - `beginner/agno_paywall_example.py` - Paywall-protected agent - `beginner/echo_agent_behind_paywall.py` - Echo agent with payment requirement @@ -81,8 +82,9 @@ For full URL override, use `BINDU_DEPLOYMENT_URL` (e.g. `http://127.0.0.1:5001`) ## Environment Variables ```bash -# Required +# Required (at least one LLM provider) OPENROUTER_API_KEY=sk-or-v1-your-api-key-here +MINIMAX_API_KEY=your-minimax-api-key # Alternative: MiniMax AI (https://platform.minimaxi.com) # Optional PORT=4000 diff --git a/examples/beginner/.env.example b/examples/beginner/.env.example index cc729737..0d14f206 100644 --- a/examples/beginner/.env.example +++ b/examples/beginner/.env.example @@ -4,6 +4,13 @@ # Used for LLM API calls OPENROUTER_API_KEY=sk-or-v1- +# ---------------------------------------------------------------------------- +# MiniMax AI Configuration +# ---------------------------------------------------------------------------- +# Used for MiniMax LLM API calls (OpenAI-compatible) +# Get your API key at https://platform.minimaxi.com +MINIMAX_API_KEY= + # ---------------------------------------------------------------------------- # Storage Configuration # ---------------------------------------------------------------------------- diff --git a/examples/beginner/minimax_example.py b/examples/beginner/minimax_example.py new file mode 100644 index 00000000..fec7c828 --- /dev/null +++ b/examples/beginner/minimax_example.py @@ -0,0 +1,75 @@ +"""MiniMax AI Research Agent + +A Bindu agent powered by MiniMax's M2.7 model via OpenAI-compatible API. +MiniMax offers high-performance models with up to 1M context window. + +Features: +- MiniMax M2.7 model (1M context) +- Web search integration via DuckDuckGo +- Research and summarization capabilities + +Usage: + python minimax_example.py + +Environment: + Requires MINIMAX_API_KEY in .env file + Get your API key at https://platform.minimaxi.com +""" + +import os +from bindu.penguin.bindufy import bindufy +from agno.agent import Agent +from agno.tools.duckduckgo import DuckDuckGoTools +from agno.models.openai import OpenAILike + +from dotenv import load_dotenv + +load_dotenv() + +# MiniMax API configuration +MINIMAX_API_KEY = os.getenv("MINIMAX_API_KEY") +MINIMAX_BASE_URL = "https://api.minimax.io/v1" + +# Define your agent with MiniMax M2.7 +agent = Agent( + instructions="You are a research assistant that finds and summarizes information.", + model=OpenAILike( + id="MiniMax-M2.7", + api_key=MINIMAX_API_KEY, + base_url=MINIMAX_BASE_URL, + ), + tools=[DuckDuckGoTools()], +) + + +# Configuration +config = { + "author": "your.email@example.com", + "name": "minimax_research_agent", + "description": "A research assistant agent powered by MiniMax AI", + "deployment": { + "url": os.getenv("BINDU_DEPLOYMENT_URL", "http://localhost:3773"), + "expose": True, + "cors_origins": ["http://localhost:5173"] + }, + "skills": ["skills/question-answering", "skills/pdf-processing"] +} + + +# Handler function +def handler(messages: list[dict[str, str]]): + """Process messages and return agent response. + + Args: + messages: List of message dictionaries containing conversation history + + Returns: + Agent response result + """ + result = agent.run(input=messages) + return result + + +# Bindu-fy it +if __name__ == "__main__": + bindufy(config, handler) diff --git a/tests/unit/test_minimax_example.py b/tests/unit/test_minimax_example.py new file mode 100644 index 00000000..a3556d0e --- /dev/null +++ b/tests/unit/test_minimax_example.py @@ -0,0 +1,239 @@ +"""Tests for MiniMax AI example agent configuration. + +Validates the MiniMax example follows Bindu patterns correctly and +that the OpenAI-compatible integration is properly configured. +""" + +import ast +import os +from pathlib import Path + +import pytest + +# Path to the example file +EXAMPLE_PATH = Path(__file__).parent.parent.parent / "examples" / "beginner" / "minimax_example.py" +ENV_EXAMPLE_PATH = Path(__file__).parent.parent.parent / "examples" / "beginner" / ".env.example" + + +class TestMiniMaxExampleFile: + """Test MiniMax example file structure and content.""" + + def test_example_file_exists(self): + """Verify the MiniMax example file exists.""" + assert EXAMPLE_PATH.exists(), f"Missing: {EXAMPLE_PATH}" + + def test_example_is_valid_python(self): + """Verify the example is valid Python syntax.""" + source = EXAMPLE_PATH.read_text() + tree = ast.parse(source, filename=str(EXAMPLE_PATH)) + assert tree is not None + + def test_example_has_docstring(self): + """Verify the example has a module docstring.""" + source = EXAMPLE_PATH.read_text() + tree = ast.parse(source) + docstring = ast.get_docstring(tree) + assert docstring is not None + assert "MiniMax" in docstring + + def test_example_imports_bindufy(self): + """Verify the example imports bindufy.""" + source = EXAMPLE_PATH.read_text() + assert "from bindu.penguin.bindufy import bindufy" in source + + def test_example_imports_openailike(self): + """Verify the example uses OpenAILike for MiniMax.""" + source = EXAMPLE_PATH.read_text() + assert "from agno.models.openai import OpenAILike" in source + + def test_example_has_minimax_base_url(self): + """Verify the example uses the correct MiniMax API URL.""" + source = EXAMPLE_PATH.read_text() + assert "https://api.minimax.io/v1" in source + + def test_example_uses_minimax_m27(self): + """Verify the example uses MiniMax-M2.7 model.""" + source = EXAMPLE_PATH.read_text() + assert "MiniMax-M2.7" in source + + def test_example_reads_api_key_from_env(self): + """Verify the example reads MINIMAX_API_KEY from env.""" + source = EXAMPLE_PATH.read_text() + assert 'MINIMAX_API_KEY' in source + assert 'os.getenv("MINIMAX_API_KEY")' in source + + def test_example_has_config_dict(self): + """Verify the example has a config dictionary with required fields.""" + source = EXAMPLE_PATH.read_text() + assert '"name"' in source + assert '"author"' in source + assert '"description"' in source + assert '"deployment"' in source + + def test_example_has_handler_function(self): + """Verify the example defines a handler function.""" + source = EXAMPLE_PATH.read_text() + assert "def handler(" in source + + def test_example_calls_bindufy(self): + """Verify the example calls bindufy.""" + source = EXAMPLE_PATH.read_text() + assert "bindufy(config, handler)" in source + + def test_example_has_main_guard(self): + """Verify the example has __main__ guard.""" + source = EXAMPLE_PATH.read_text() + assert 'if __name__ == "__main__":' in source + + def test_example_uses_duckduckgo_tools(self): + """Verify the example includes search tools.""" + source = EXAMPLE_PATH.read_text() + assert "DuckDuckGoTools" in source + + def test_example_loads_dotenv(self): + """Verify the example loads .env file.""" + source = EXAMPLE_PATH.read_text() + assert "load_dotenv()" in source + + +class TestEnvExampleFile: + """Test that .env.example includes MiniMax configuration.""" + + def test_env_example_has_minimax_key(self): + """Verify .env.example includes MINIMAX_API_KEY.""" + content = ENV_EXAMPLE_PATH.read_text() + assert "MINIMAX_API_KEY" in content + + def test_env_example_has_minimax_section(self): + """Verify .env.example has a MiniMax section header.""" + content = ENV_EXAMPLE_PATH.read_text() + assert "MiniMax" in content + + def test_env_example_has_platform_url(self): + """Verify .env.example references the MiniMax platform.""" + content = ENV_EXAMPLE_PATH.read_text() + assert "platform.minimaxi.com" in content + + +class TestMiniMaxModelConstants: + """Test MiniMax model constants and values.""" + + def test_valid_minimax_models(self): + """Verify known MiniMax model identifiers.""" + valid_models = { + "MiniMax-M2.7", + "MiniMax-M2.7-highspeed", + "MiniMax-M2.5", + "MiniMax-M2.5-highspeed", + } + # Parse the example to check the model used + source = EXAMPLE_PATH.read_text() + for model in valid_models: + # At least M2.7 should be referenced + if model == "MiniMax-M2.7": + assert model in source + + def test_minimax_api_url_format(self): + """Verify MiniMax API URL follows OpenAI-compatible format.""" + source = EXAMPLE_PATH.read_text() + # Should end with /v1 (OpenAI-compatible pattern) + assert "api.minimax.io/v1" in source + + def test_example_does_not_hardcode_api_key(self): + """Verify no API key is hardcoded in the example.""" + source = EXAMPLE_PATH.read_text() + # Should use os.getenv, not a hardcoded string + assert "sk-" not in source.replace("sk-or-v1", "") # exclude OpenRouter pattern + assert 'api_key="' not in source + + +class TestReadmeUpdates: + """Test that README files mention MiniMax.""" + + def test_main_readme_mentions_minimax(self): + """Verify main README mentions MiniMax.""" + readme = (Path(__file__).parent.parent.parent / "README.md").read_text() + assert "MiniMax" in readme + + def test_main_readme_has_minimax_api_key(self): + """Verify main README mentions MINIMAX_API_KEY.""" + readme = (Path(__file__).parent.parent.parent / "README.md").read_text() + assert "MINIMAX_API_KEY" in readme + + def test_examples_readme_mentions_minimax(self): + """Verify examples README lists the MiniMax example.""" + readme = (Path(__file__).parent.parent.parent / "examples" / "README.md").read_text() + assert "minimax_example.py" in readme + + def test_examples_readme_mentions_minimax_env(self): + """Verify examples README mentions MINIMAX_API_KEY in env vars.""" + readme = (Path(__file__).parent.parent.parent / "examples" / "README.md").read_text() + assert "MINIMAX_API_KEY" in readme + + +class TestMiniMaxIntegration: + """Integration tests for MiniMax API (require MINIMAX_API_KEY).""" + + @pytest.fixture + def api_key(self): + key = os.getenv("MINIMAX_API_KEY") + if not key: + pytest.skip("MINIMAX_API_KEY not set") + return key + + def test_minimax_api_connection(self, api_key): + """Test that MiniMax API is reachable with valid key.""" + import httpx + resp = httpx.post( + "https://api.minimax.io/v1/chat/completions", + headers={ + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + }, + json={ + "model": "MiniMax-M2.7", + "messages": [{"role": "user", "content": "Hi"}], + "max_tokens": 5, + }, + timeout=30, + ) + assert resp.status_code == 200 + + def test_minimax_chat_completion(self, api_key): + """Test a simple chat completion via MiniMax API.""" + import httpx + resp = httpx.post( + "https://api.minimax.io/v1/chat/completions", + headers={ + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + }, + json={ + "model": "MiniMax-M2.7", + "messages": [{"role": "user", "content": "Say hello in one word."}], + "max_tokens": 10, + }, + timeout=30, + ) + assert resp.status_code == 200 + data = resp.json() + assert "choices" in data + assert len(data["choices"]) > 0 + + def test_minimax_m27_highspeed_model(self, api_key): + """Test that M2.7-highspeed model is also accessible.""" + import httpx + resp = httpx.post( + "https://api.minimax.io/v1/chat/completions", + headers={ + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + }, + json={ + "model": "MiniMax-M2.7-highspeed", + "messages": [{"role": "user", "content": "Hi"}], + "max_tokens": 5, + }, + timeout=30, + ) + assert resp.status_code == 200