From 29e28eff90a734fce1c6a50a4bee9e40b0798763 Mon Sep 17 00:00:00 2001 From: octo-patch Date: Wed, 13 May 2026 16:24:05 +0800 Subject: [PATCH] feat: add MiniMax provider support - Add MiniMax (minimax) as an OpenAI-compatible provider in both core and nanobot registries - Register MINIMAX_API_KEY environment variable - Default base URL: https://api.minimax.io/v1 - Models: MiniMax-M2.7, MiniMax-M2.7-highspeed - Add unit tests for registry, config, and provider instantiation - Update deepcode_config.json.example and README API docs: https://platform.minimax.io/docs/api-reference/text-openai-api --- README.md | 3 +- core/config.py | 1 + core/providers/registry.py | 8 ++ deepcode_config.json.example | 3 + nanobot/nanobot/config/schema.py | 1 + nanobot/nanobot/providers/registry.py | 17 +++ tests/minimax_provider_test.py | 155 ++++++++++++++++++++++++++ 7 files changed, 187 insertions(+), 1 deletion(-) create mode 100644 tests/minimax_provider_test.py diff --git a/README.md b/README.md index 0f0505cb..7ce99fe3 100644 --- a/README.md +++ b/README.md @@ -714,7 +714,8 @@ Edit `deepcode_config.json` and fill in at least one provider key. Inline string "providers": { "openai": { "apiKey": "your_openai_api_key" }, "anthropic": { "apiKey": "${ANTHROPIC_API_KEY}" }, - "gemini": { "apiKey": "" } + "gemini": { "apiKey": "" }, + "minimax": { "apiKey": "${MINIMAX_API_KEY}" } } } ``` diff --git a/core/config.py b/core/config.py index cf10568f..f976f7dc 100644 --- a/core/config.py +++ b/core/config.py @@ -147,6 +147,7 @@ class ProvidersConfig(_Base): gemini: ProviderConfig = Field(default_factory=ProviderConfig) zhipu: ProviderConfig = Field(default_factory=ProviderConfig) dashscope: ProviderConfig = Field(default_factory=ProviderConfig) + minimax: ProviderConfig = Field(default_factory=ProviderConfig) vllm: ProviderConfig = Field(default_factory=ProviderConfig) ollama: ProviderConfig = Field(default_factory=ProviderConfig) diff --git a/core/providers/registry.py b/core/providers/registry.py index 36c2bc94..1be96f12 100644 --- a/core/providers/registry.py +++ b/core/providers/registry.py @@ -140,6 +140,14 @@ def label(self) -> str: backend="openai_compat", is_local=True, ), + ProviderSpec( + name="minimax", + keywords=("minimax", "abab"), + env_key="MINIMAX_API_KEY", + display_name="MiniMax", + backend="openai_compat", + default_api_base="https://api.minimax.io/v1", + ), ProviderSpec( name="ollama", keywords=("ollama", "nemotron"), diff --git a/deepcode_config.json.example b/deepcode_config.json.example index b08d2806..b8e92d1b 100644 --- a/deepcode_config.json.example +++ b/deepcode_config.json.example @@ -46,6 +46,9 @@ "dashscope": { "apiKey": "" }, + "minimax": { + "apiKey": "${MINIMAX_API_KEY}" + }, "ollama": { "apiBase": "http://localhost:11434/v1" }, diff --git a/nanobot/nanobot/config/schema.py b/nanobot/nanobot/config/schema.py index af7fa99c..b143bf37 100644 --- a/nanobot/nanobot/config/schema.py +++ b/nanobot/nanobot/config/schema.py @@ -169,6 +169,7 @@ class ProvidersConfig(BaseModel): groq: ProviderConfig = Field(default_factory=ProviderConfig) zhipu: ProviderConfig = Field(default_factory=ProviderConfig) dashscope: ProviderConfig = Field(default_factory=ProviderConfig) # 阿里云通义千问 + minimax: ProviderConfig = Field(default_factory=ProviderConfig) # MiniMax vllm: ProviderConfig = Field(default_factory=ProviderConfig) gemini: ProviderConfig = Field(default_factory=ProviderConfig) moonshot: ProviderConfig = Field(default_factory=ProviderConfig) diff --git a/nanobot/nanobot/providers/registry.py b/nanobot/nanobot/providers/registry.py index 58e29d60..b8c9fee2 100644 --- a/nanobot/nanobot/providers/registry.py +++ b/nanobot/nanobot/providers/registry.py @@ -223,6 +223,23 @@ def label(self) -> str: strip_model_prefix=False, model_overrides=(("kimi-k2.5", {"temperature": 1.0}),), ), + # MiniMax: OpenAI-compatible API. Needs "openai/" prefix for LiteLLM. + ProviderSpec( + name="minimax", + keywords=("minimax", "abab"), + env_key="MINIMAX_API_KEY", + display_name="MiniMax", + litellm_prefix="openai", + skip_prefixes=("openai/", "minimax/"), + env_extras=(), + is_gateway=False, + is_local=False, + detect_by_key_prefix="", + detect_by_base_keyword="minimax", + default_api_base="https://api.minimax.io/v1", + strip_model_prefix=False, + model_overrides=(), + ), # === Local deployment (matched by config key, NOT by api_base) ========= # vLLM / any OpenAI-compatible local server. # Detected when config key is "vllm" (provider_name="vllm"). diff --git a/tests/minimax_provider_test.py b/tests/minimax_provider_test.py new file mode 100644 index 00000000..c9f65d71 --- /dev/null +++ b/tests/minimax_provider_test.py @@ -0,0 +1,155 @@ +"""Tests for MiniMax (minimax) provider integration.""" + +from __future__ import annotations + +import sys +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[1] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) + +from core.providers.registry import ( # noqa: E402 + PROVIDERS, + find_by_model, + find_by_name, +) + + +# --------------------------------------------------------------------------- +# Core registry tests +# --------------------------------------------------------------------------- + + +class TestCoreRegistry: + """Tests for the core provider registry.""" + + def test_find_by_name(self): + spec = find_by_name("minimax") + assert spec is not None + assert spec.name == "minimax" + assert spec.display_name == "MiniMax" + + def test_env_key(self): + spec = find_by_name("minimax") + assert spec is not None + assert spec.env_key == "MINIMAX_API_KEY" + + def test_backend_is_openai_compat(self): + spec = find_by_name("minimax") + assert spec is not None + assert spec.backend == "openai_compat" + + def test_default_api_base(self): + spec = find_by_name("minimax") + assert spec is not None + assert spec.default_api_base == "https://api.minimax.io/v1" + + def test_find_by_model_minimax(self): + spec = find_by_model("minimax/MiniMax-M2.7") + assert spec is not None + assert spec.name == "minimax" + + def test_find_by_model_abab(self): + spec = find_by_model("abab-7") + assert spec is not None + assert spec.name == "minimax" + + def test_not_gateway_or_local(self): + spec = find_by_name("minimax") + assert spec is not None + assert spec.is_gateway is False + assert spec.is_local is False + + def test_provider_in_registry_list(self): + names = [s.name for s in PROVIDERS] + assert "minimax" in names + + +# --------------------------------------------------------------------------- +# Config model tests +# --------------------------------------------------------------------------- + + +class TestConfigModel: + """Tests for the config model with the new provider field.""" + + def test_providers_config_has_minimax_field(self): + from core.config import ProvidersConfig + + cfg = ProvidersConfig() + assert hasattr(cfg, "minimax") + assert cfg.minimax.api_key is None + + def test_providers_config_with_api_key(self): + from core.config import ProvidersConfig, ProviderConfig + + cfg = ProvidersConfig(minimax=ProviderConfig(api_key="test-key")) + assert cfg.minimax.api_key == "test-key" + + def test_providers_config_with_custom_base(self): + from core.config import ProvidersConfig, ProviderConfig + + cfg = ProvidersConfig( + minimax=ProviderConfig( + api_key="test-key", + api_base="https://api.minimaxi.com/v1", + ) + ) + assert cfg.minimax.api_base == "https://api.minimaxi.com/v1" + + +# --------------------------------------------------------------------------- +# Nanobot registry tests +# --------------------------------------------------------------------------- + + +class TestNanobotRegistry: + """Tests for the nanobot provider registry.""" + + @pytest.fixture(autouse=True) + def _setup_nanobot_path(self): + nanobot_root = ROOT / "nanobot" + if str(nanobot_root) not in sys.path: + sys.path.insert(0, str(nanobot_root)) + + def test_find_by_name(self): + nanobot_reg = pytest.importorskip("nanobot.providers.registry") + spec = nanobot_reg.find_by_name("minimax") + assert spec is not None + assert spec.name == "minimax" + assert spec.display_name == "MiniMax" + + def test_litellm_prefix(self): + nanobot_reg = pytest.importorskip("nanobot.providers.registry") + spec = nanobot_reg.find_by_name("minimax") + assert spec is not None + assert spec.litellm_prefix == "openai" + + def test_detect_by_base_keyword(self): + nanobot_reg = pytest.importorskip("nanobot.providers.registry") + spec = nanobot_reg.find_by_name("minimax") + assert spec is not None + assert spec.detect_by_base_keyword == "minimax" + + +# --------------------------------------------------------------------------- +# Nanobot config schema tests +# --------------------------------------------------------------------------- + + +class TestNanobotConfigSchema: + """Tests for the nanobot config schema.""" + + @pytest.fixture(autouse=True) + def _setup_nanobot_path(self): + nanobot_root = ROOT / "nanobot" + if str(nanobot_root) not in sys.path: + sys.path.insert(0, str(nanobot_root)) + + def test_providers_config_has_minimax_field(self): + schema = pytest.importorskip("nanobot.config.schema") + cfg = schema.ProvidersConfig() + assert hasattr(cfg, "minimax")