Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,8 +230,9 @@ export STRIX_REASONING_EFFORT="high" # control thinking effort (default: high,
- [OpenAI GPT-5.4](https://openai.com/api/) — `openai/gpt-5.4`
- [Anthropic Claude Sonnet 4.6](https://claude.com/platform/api) — `anthropic/claude-sonnet-4-6`
- [Google Gemini 3 Pro Preview](https://cloud.google.com/vertex-ai) — `vertex_ai/gemini-3-pro-preview`
- [MiniMax-M2.7](https://platform.minimax.io) — `openai/MiniMax-M2.7` (set `LLM_API_BASE=https://api.minimax.io/v1`)

See the [LLM Providers documentation](https://docs.strix.ai/llm-providers/overview) for all supported providers including Vertex AI, Bedrock, Azure, and local models.
See the [LLM Providers documentation](https://docs.strix.ai/llm-providers/overview) for all supported providers including Vertex AI, Bedrock, Azure, MiniMax, and local models.

## Enterprise

Expand Down
43 changes: 43 additions & 0 deletions docs/llm-providers/minimax.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
title: "MiniMax"
description: "Configure Strix with MiniMax models"
---

[MiniMax](https://www.minimax.io) provides powerful large language models with up to 1M token context windows through an OpenAI-compatible API.

## Setup

```bash
export STRIX_LLM="openai/MiniMax-M2.7"
export LLM_API_KEY="your-minimax-api-key"
export LLM_API_BASE="https://api.minimax.io/v1"
```

Or use the shorthand with automatic base URL detection:

```bash
export STRIX_LLM="openai/MiniMax-M2.7"
export MINIMAX_API_KEY="your-minimax-api-key"
```

## Available Models

| Model | Configuration | Context Window |
|-------|---------------|----------------|
| MiniMax-M2.7 | `openai/MiniMax-M2.7` | 1M tokens |
| MiniMax-M2.7-highspeed | `openai/MiniMax-M2.7-highspeed` | 1M tokens |

**MiniMax-M2.7** is the latest flagship model with strong reasoning and coding capabilities.
**MiniMax-M2.7-highspeed** offers faster inference with slightly reduced quality.

## Get API Key

1. Go to [platform.minimax.io](https://platform.minimax.io)
2. Sign up or sign in
3. Navigate to API Keys and create a new key

## Notes

- MiniMax API is fully OpenAI-compatible, so it works via the `openai/` LiteLLM prefix
- Temperature range: 0.0 to 1.0 (inclusive)
- When `MINIMAX_API_KEY` is set and the model name contains "minimax", the API key and base URL are auto-detected
4 changes: 4 additions & 0 deletions docs/llm-providers/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Set your model and API key:
| GPT-5.4 | OpenAI | `openai/gpt-5.4` |
| Claude Sonnet 4.6 | Anthropic | `anthropic/claude-sonnet-4-6` |
| Gemini 3 Pro | Google Vertex | `vertex_ai/gemini-3-pro-preview` |
| MiniMax-M2.7 | MiniMax | `openai/MiniMax-M2.7` |

```bash
export STRIX_LLM="openai/gpt-5.4"
Expand Down Expand Up @@ -52,6 +53,9 @@ See the [Local Models guide](/llm-providers/local) for setup instructions and re
<Card title="Azure OpenAI" href="/llm-providers/azure">
GPT-5.4 via Azure.
</Card>
<Card title="MiniMax" href="/llm-providers/minimax">
MiniMax-M2.7 models with 1M context.
</Card>
<Card title="Local Models" href="/llm-providers/local">
Llama 4, Mistral, and self-hosted models.
</Card>
Expand Down
13 changes: 13 additions & 0 deletions strix/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,4 +212,17 @@ def resolve_llm_config() -> tuple[str | None, str | None, str | None]:
or Config.get("ollama_api_base")
)

# Auto-detect MiniMax provider: use MINIMAX_API_KEY and set base URL
if _is_minimax_model(model):
if not api_key:
api_key = os.getenv("MINIMAX_API_KEY")
if not api_base:
api_base = "https://api.minimax.io/v1"
Comment on lines +215 to +220
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 MiniMax key may be injected into Strix-gateway calls

When STRIX_LLM="strix/minimax-m2.7" is configured, the earlier block sets api_base = STRIX_API_BASE (the Strix gateway). The new MiniMax detection block then runs unconditionally: the api_base override is correctly skipped (since it's already set), but if no LLM_API_KEY is present, MINIMAX_API_KEY is picked up and used as the api_key for a call that goes to the Strix gateway.

Unless the Strix gateway is designed to receive and forward a raw provider key, this will cause authentication failures for strix/minimax-* users who only have MINIMAX_API_KEY set. Consider adding a not model.startswith("strix/") guard before the MiniMax detection block so it only fires for direct provider calls.

Prompt To Fix With AI
This is a comment left during a code review.
Path: strix/config/config.py
Line: 215-220

Comment:
**MiniMax key may be injected into Strix-gateway calls**

When `STRIX_LLM="strix/minimax-m2.7"` is configured, the earlier block sets `api_base = STRIX_API_BASE` (the Strix gateway). The new MiniMax detection block then runs unconditionally: the `api_base` override is correctly skipped (since it's already set), but if no `LLM_API_KEY` is present, `MINIMAX_API_KEY` is picked up and used as the `api_key` for a call that goes to the Strix gateway.

Unless the Strix gateway is designed to receive and forward a raw provider key, this will cause authentication failures for `strix/minimax-*` users who only have `MINIMAX_API_KEY` set. Consider adding a `not model.startswith("strix/")` guard before the MiniMax detection block so it only fires for direct provider calls.

How can I resolve this? If you propose a fix, please make it concise.


return model, api_key, api_base


def _is_minimax_model(model: str) -> bool:
"""Check if the model name refers to a MiniMax model."""
lower = model.lower()
return "minimax" in lower
2 changes: 2 additions & 0 deletions strix/llm/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ def normalize_tool_format(content: str) -> str:
"gemini-3-flash-preview": "gemini/gemini-3-flash-preview",
"glm-5": "openrouter/z-ai/glm-5",
"glm-4.7": "openrouter/z-ai/glm-4.7",
"minimax-m2.7": "openai/MiniMax-M2.7",
"minimax-m2.7-highspeed": "openai/MiniMax-M2.7-highspeed",
}


Expand Down
167 changes: 167 additions & 0 deletions tests/llm/test_minimax.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
"""Tests for MiniMax model integration."""

import os

import pytest

from strix.config.config import Config, _is_minimax_model, resolve_llm_config
from strix.llm.config import LLMConfig
from strix.llm.utils import STRIX_MODEL_MAP, resolve_strix_model


class TestMiniMaxModelMap:
"""Tests for MiniMax entries in STRIX_MODEL_MAP."""

def test_minimax_m27_in_model_map(self):
assert "minimax-m2.7" in STRIX_MODEL_MAP
assert STRIX_MODEL_MAP["minimax-m2.7"] == "openai/MiniMax-M2.7"

def test_minimax_m27_highspeed_in_model_map(self):
assert "minimax-m2.7-highspeed" in STRIX_MODEL_MAP
assert STRIX_MODEL_MAP["minimax-m2.7-highspeed"] == "openai/MiniMax-M2.7-highspeed"


class TestMiniMaxModelResolution:
"""Tests for resolving strix/ MiniMax models."""

def test_resolve_strix_minimax_m27(self):
api_model, canonical = resolve_strix_model("strix/minimax-m2.7")
assert api_model == "openai/minimax-m2.7"
assert canonical == "openai/MiniMax-M2.7"

def test_resolve_strix_minimax_m27_highspeed(self):
api_model, canonical = resolve_strix_model("strix/minimax-m2.7-highspeed")
assert api_model == "openai/minimax-m2.7-highspeed"
assert canonical == "openai/MiniMax-M2.7-highspeed"

def test_resolve_direct_minimax_model_passthrough(self):
api_model, canonical = resolve_strix_model("openai/MiniMax-M2.7")
assert api_model == "openai/MiniMax-M2.7"
assert canonical == "openai/MiniMax-M2.7"


class TestIsMiniMaxModel:
"""Tests for MiniMax model detection."""

def test_detects_minimax_openai_prefix(self):
assert _is_minimax_model("openai/MiniMax-M2.7")

def test_detects_minimax_case_insensitive(self):
assert _is_minimax_model("openai/minimax-m2.7")

def test_detects_minimax_strix_prefix(self):
assert _is_minimax_model("strix/minimax-m2.7")

def test_non_minimax_model(self):
assert not _is_minimax_model("openai/gpt-5.4")

def test_non_minimax_anthropic(self):
assert not _is_minimax_model("anthropic/claude-sonnet-4-6")


class TestMiniMaxConfigResolution:
"""Tests for MiniMax auto-detection in resolve_llm_config."""

def test_auto_detect_minimax_api_key(self, monkeypatch: pytest.MonkeyPatch):
monkeypatch.setenv("STRIX_LLM", "openai/MiniMax-M2.7")
monkeypatch.setenv("MINIMAX_API_KEY", "test-minimax-key")
monkeypatch.delenv("LLM_API_KEY", raising=False)
monkeypatch.delenv("LLM_API_BASE", raising=False)
monkeypatch.delenv("OPENAI_API_BASE", raising=False)
monkeypatch.delenv("LITELLM_BASE_URL", raising=False)
monkeypatch.delenv("OLLAMA_API_BASE", raising=False)

model, api_key, api_base = resolve_llm_config()

assert model == "openai/MiniMax-M2.7"
assert api_key == "test-minimax-key"
assert api_base == "https://api.minimax.io/v1"

def test_llm_api_key_takes_precedence_over_minimax_key(self, monkeypatch: pytest.MonkeyPatch):
monkeypatch.setenv("STRIX_LLM", "openai/MiniMax-M2.7")
monkeypatch.setenv("LLM_API_KEY", "llm-key-takes-precedence")
monkeypatch.setenv("MINIMAX_API_KEY", "minimax-key")
monkeypatch.delenv("LLM_API_BASE", raising=False)
monkeypatch.delenv("OPENAI_API_BASE", raising=False)
monkeypatch.delenv("LITELLM_BASE_URL", raising=False)
monkeypatch.delenv("OLLAMA_API_BASE", raising=False)

model, api_key, api_base = resolve_llm_config()

assert api_key == "llm-key-takes-precedence"
assert api_base == "https://api.minimax.io/v1"

def test_custom_api_base_takes_precedence(self, monkeypatch: pytest.MonkeyPatch):
monkeypatch.setenv("STRIX_LLM", "openai/MiniMax-M2.7")
monkeypatch.setenv("MINIMAX_API_KEY", "test-key")
monkeypatch.setenv("LLM_API_BASE", "https://custom-proxy.com/v1")
monkeypatch.delenv("LLM_API_KEY", raising=False)
monkeypatch.delenv("OPENAI_API_BASE", raising=False)
monkeypatch.delenv("LITELLM_BASE_URL", raising=False)
monkeypatch.delenv("OLLAMA_API_BASE", raising=False)

model, api_key, api_base = resolve_llm_config()

assert api_base == "https://custom-proxy.com/v1"

def test_no_minimax_key_no_auto_detect(self, monkeypatch: pytest.MonkeyPatch):
monkeypatch.setenv("STRIX_LLM", "openai/gpt-5.4")
monkeypatch.delenv("LLM_API_KEY", raising=False)
monkeypatch.delenv("MINIMAX_API_KEY", raising=False)
monkeypatch.delenv("LLM_API_BASE", raising=False)
monkeypatch.delenv("OPENAI_API_BASE", raising=False)
monkeypatch.delenv("LITELLM_BASE_URL", raising=False)
monkeypatch.delenv("OLLAMA_API_BASE", raising=False)

model, api_key, api_base = resolve_llm_config()

assert model == "openai/gpt-5.4"
assert api_key is None
assert api_base is None

def test_minimax_auto_base_url_when_no_base_set(self, monkeypatch: pytest.MonkeyPatch):
monkeypatch.setenv("STRIX_LLM", "openai/MiniMax-M2.7-highspeed")
monkeypatch.setenv("LLM_API_KEY", "some-key")
monkeypatch.delenv("LLM_API_BASE", raising=False)
monkeypatch.delenv("OPENAI_API_BASE", raising=False)
monkeypatch.delenv("LITELLM_BASE_URL", raising=False)
monkeypatch.delenv("OLLAMA_API_BASE", raising=False)

model, api_key, api_base = resolve_llm_config()

assert api_base == "https://api.minimax.io/v1"


class TestMiniMaxLLMConfig:
"""Tests for LLMConfig with MiniMax models."""

def test_llm_config_minimax_direct(self, monkeypatch: pytest.MonkeyPatch):
monkeypatch.setenv("STRIX_LLM", "openai/MiniMax-M2.7")
monkeypatch.setenv("LLM_API_KEY", "test-key")
monkeypatch.delenv("LLM_API_BASE", raising=False)
monkeypatch.delenv("OPENAI_API_BASE", raising=False)
monkeypatch.delenv("LITELLM_BASE_URL", raising=False)
monkeypatch.delenv("OLLAMA_API_BASE", raising=False)

config = LLMConfig()

assert config.model_name == "openai/MiniMax-M2.7"
assert config.litellm_model == "openai/MiniMax-M2.7"
assert config.api_key == "test-key"
assert config.api_base == "https://api.minimax.io/v1"

def test_llm_config_minimax_strix_shortcut(self, monkeypatch: pytest.MonkeyPatch):
monkeypatch.setenv("STRIX_LLM", "strix/minimax-m2.7")
monkeypatch.setenv("MINIMAX_API_KEY", "minimax-key")
monkeypatch.delenv("LLM_API_KEY", raising=False)
monkeypatch.delenv("LLM_API_BASE", raising=False)
monkeypatch.delenv("OPENAI_API_BASE", raising=False)
monkeypatch.delenv("LITELLM_BASE_URL", raising=False)
monkeypatch.delenv("OLLAMA_API_BASE", raising=False)

config = LLMConfig()

assert config.model_name == "strix/minimax-m2.7"
assert config.litellm_model == "openai/minimax-m2.7"
assert config.canonical_model == "openai/MiniMax-M2.7"
assert config.api_key == "minimax-key"
78 changes: 78 additions & 0 deletions tests/llm/test_minimax_integration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""Integration tests for MiniMax provider.

These tests verify end-to-end MiniMax integration by making real API calls.
They require MINIMAX_API_KEY to be set in the environment.
"""

import os

import pytest

from strix.llm.config import LLMConfig
from strix.llm.llm import LLM


pytestmark = pytest.mark.skipif(
not os.environ.get("MINIMAX_API_KEY"),
reason="MINIMAX_API_KEY not set",
)


@pytest.fixture()
def minimax_llm(monkeypatch: pytest.MonkeyPatch) -> LLM:
"""Create an LLM instance configured for MiniMax."""
monkeypatch.setenv("STRIX_LLM", "openai/MiniMax-M2.7")
monkeypatch.setenv("LLM_API_KEY", os.environ.get("MINIMAX_API_KEY", ""))
monkeypatch.setenv("LLM_API_BASE", "https://api.minimax.io/v1")
monkeypatch.setenv("STRIX_TELEMETRY", "0")
config = LLMConfig()
return LLM(config, agent_name=None)


@pytest.mark.asyncio()
async def test_minimax_basic_completion(minimax_llm: LLM):
"""Test that MiniMax can complete a simple prompt."""
messages = [{"role": "user", "content": "Reply with exactly: hello"}]
responses = []
async for response in minimax_llm.generate(messages):
responses.append(response)

assert len(responses) > 0
final = responses[-1]
assert final.content
assert "hello" in final.content.lower()


@pytest.mark.asyncio()
async def test_minimax_streaming(minimax_llm: LLM):
"""Test that MiniMax streaming produces incremental responses."""
messages = [{"role": "user", "content": "Count from 1 to 3, one number per line."}]
responses = []
async for response in minimax_llm.generate(messages):
responses.append(response)

# Streaming should produce multiple intermediate responses
assert len(responses) >= 2
final = responses[-1]
assert "1" in final.content
assert "2" in final.content
assert "3" in final.content


@pytest.mark.asyncio()
async def test_minimax_config_auto_detection():
"""Test that MINIMAX_API_KEY auto-detection works end-to-end."""
api_key = os.environ.get("MINIMAX_API_KEY", "")
orig_llm_key = os.environ.pop("LLM_API_KEY", None)
orig_llm_base = os.environ.pop("LLM_API_BASE", None)
os.environ["STRIX_LLM"] = "openai/MiniMax-M2.7"

try:
config = LLMConfig()
assert config.api_key == api_key
assert config.api_base == "https://api.minimax.io/v1"
finally:
if orig_llm_key is not None:
os.environ["LLM_API_KEY"] = orig_llm_key
Comment on lines +60 to +76
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 STRIX_LLM not restored in test teardown

os.environ["STRIX_LLM"] is set on line 63 but is never restored in the finally block. If this test runs in an environment where MINIMAX_API_KEY is present, STRIX_LLM will remain as "openai/MiniMax-M2.7" for any subsequently executed test, potentially causing failures in other integration tests that rely on a different STRIX_LLM value.

The other two integration tests in this file use a monkeypatch fixture (which auto-restores on teardown), but this function bypasses it and does not capture or restore the original value of STRIX_LLM.

Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/llm/test_minimax_integration.py
Line: 60-76

Comment:
**`STRIX_LLM` not restored in test teardown**

`os.environ["STRIX_LLM"]` is set on line 63 but is never restored in the `finally` block. If this test runs in an environment where `MINIMAX_API_KEY` is present, `STRIX_LLM` will remain as `"openai/MiniMax-M2.7"` for any subsequently executed test, potentially causing failures in other integration tests that rely on a different `STRIX_LLM` value.

The other two integration tests in this file use a `monkeypatch` fixture (which auto-restores on teardown), but this function bypasses it and does not capture or restore the original value of `STRIX_LLM`.

How can I resolve this? If you propose a fix, please make it concise.

if orig_llm_base is not None:
os.environ["LLM_API_BASE"] = orig_llm_base