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
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ OPENAI_API_KEY=
ANTHROPIC_API_KEY=
ANTHROPIC_ENDPOINT=https://api.anthropic.com

MINIMAX_API_KEY=
MINIMAX_OPENAI_ENDPOINT=https://api.minimax.io/v1
MINIMAX_CN_OPENAI_ENDPOINT=https://api.minimaxi.com/v1
MINIMAX_ANTHROPIC_ENDPOINT=https://api.minimax.io/anthropic
MINIMAX_CN_ANTHROPIC_ENDPOINT=https://api.minimaxi.com/anthropic

GOOGLE_API_KEY=

AZURE_OPENAI_ENDPOINT=
Expand Down
5 changes: 5 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ services:
- OPENAI_API_KEY=${OPENAI_API_KEY:-}
- ANTHROPIC_ENDPOINT=${ANTHROPIC_ENDPOINT:-https://api.anthropic.com}
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}
- MINIMAX_API_KEY=${MINIMAX_API_KEY:-}
- MINIMAX_OPENAI_ENDPOINT=${MINIMAX_OPENAI_ENDPOINT:-https://api.minimax.io/v1}
- MINIMAX_CN_OPENAI_ENDPOINT=${MINIMAX_CN_OPENAI_ENDPOINT:-https://api.minimaxi.com/v1}
- MINIMAX_ANTHROPIC_ENDPOINT=${MINIMAX_ANTHROPIC_ENDPOINT:-https://api.minimax.io/anthropic}
- MINIMAX_CN_ANTHROPIC_ENDPOINT=${MINIMAX_CN_ANTHROPIC_ENDPOINT:-https://api.minimaxi.com/anthropic}
- GOOGLE_API_KEY=${GOOGLE_API_KEY:-}
- AZURE_OPENAI_ENDPOINT=${AZURE_OPENAI_ENDPOINT:-}
- AZURE_OPENAI_API_KEY=${AZURE_OPENAI_API_KEY:-}
Expand Down
33 changes: 33 additions & 0 deletions src/utils/config.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,28 @@
MINIMAX_MODELS = ["MiniMax-M3", "MiniMax-M2.7"]

MINIMAX_PROVIDER_CONFIGS = {
"minimax": {
"protocol": "openai",
"endpoint_env": "MINIMAX_OPENAI_ENDPOINT",
"base_url": "https://api.minimax.io/v1",
},
"minimax_cn": {
"protocol": "openai",
"endpoint_env": "MINIMAX_CN_OPENAI_ENDPOINT",
"base_url": "https://api.minimaxi.com/v1",
},
"minimax_anthropic": {
"protocol": "anthropic",
"endpoint_env": "MINIMAX_ANTHROPIC_ENDPOINT",
"base_url": "https://api.minimax.io/anthropic",
},
"minimax_anthropic_cn": {
"protocol": "anthropic",
"endpoint_env": "MINIMAX_CN_ANTHROPIC_ENDPOINT",
"base_url": "https://api.minimaxi.com/anthropic",
},
}

PROVIDER_DISPLAY_NAMES = {
"openai": "OpenAI",
"azure_openai": "Azure OpenAI",
Expand All @@ -9,12 +34,20 @@
"unbound": "Unbound AI",
"ibm": "IBM",
"grok": "Grok",
"minimax": "MiniMax (Global, OpenAI-compatible)",
"minimax_cn": "MiniMax (China, OpenAI-compatible)",
"minimax_anthropic": "MiniMax (Global, Anthropic-compatible)",
"minimax_anthropic_cn": "MiniMax (China, Anthropic-compatible)",
}

# Predefined model names for common providers
model_names = {
"anthropic": ["claude-3-5-sonnet-20241022", "claude-3-5-sonnet-20240620", "claude-3-opus-20240229"],
"openai": ["gpt-4o", "gpt-4", "gpt-3.5-turbo", "o3-mini"],
"minimax": MINIMAX_MODELS,
"minimax_cn": MINIMAX_MODELS,
"minimax_anthropic": MINIMAX_MODELS,
"minimax_anthropic_cn": MINIMAX_MODELS,
"deepseek": ["deepseek-chat", "deepseek-reasoner"],
"google": ["gemini-2.0-flash", "gemini-2.0-flash-thinking-exp", "gemini-1.5-flash-latest",
"gemini-1.5-flash-8b-latest", "gemini-2.0-flash-thinking-exp-01-21", "gemini-2.0-pro-exp-02-05",
Expand Down
26 changes: 24 additions & 2 deletions src/utils/llm_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,16 +156,38 @@ def get_llm_model(provider: str, **kwargs):
:param kwargs:
:return:
"""
provider_config = config.MINIMAX_PROVIDER_CONFIGS.get(provider)

if provider not in ["ollama", "bedrock"]:
env_var = f"{provider.upper()}_API_KEY"
env_var = "MINIMAX_API_KEY" if provider_config else f"{provider.upper()}_API_KEY"
api_key = kwargs.get("api_key", "") or os.getenv(env_var, "")
if not api_key:
provider_display = config.PROVIDER_DISPLAY_NAMES.get(provider, provider.upper())
error_msg = f"💥 {provider_display} API key not found! 🔑 Please set the `{env_var}` environment variable or provide it in the UI."
raise ValueError(error_msg)
kwargs["api_key"] = api_key

if provider == "anthropic":
if provider_config:
base_url = kwargs.get("base_url", "") or os.getenv(
provider_config["endpoint_env"], provider_config["base_url"]
)
model_name = kwargs.get("model_name", config.MINIMAX_MODELS[0])

if provider_config["protocol"] == "anthropic":
return ChatAnthropic(
model=model_name,
temperature=kwargs.get("temperature", 0.0),
base_url=base_url,
api_key=api_key,
)

return ChatOpenAI(
model=model_name,
temperature=kwargs.get("temperature", 0.0),
base_url=base_url,
api_key=api_key,
)
elif provider == "anthropic":
if not kwargs.get("base_url", ""):
base_url = "https://api.anthropic.com"
else:
Expand Down
107 changes: 107 additions & 0 deletions tests/test_minimax_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import httpx
import pytest

from src.utils import config, llm_provider


EXPECTED_MODELS = ["MiniMax-M3", "MiniMax-M2.7"]


@pytest.mark.parametrize("provider", config.MINIMAX_PROVIDER_CONFIGS)
def test_minimax_models_are_registered(provider):
assert config.model_names[provider] == EXPECTED_MODELS


@pytest.mark.parametrize(
("provider", "expected_adapter", "expected_base_url"),
[
("minimax", "openai", "https://api.minimax.io/v1"),
("minimax_cn", "openai", "https://api.minimaxi.com/v1"),
("minimax_anthropic", "anthropic", "https://api.minimax.io/anthropic"),
("minimax_anthropic_cn", "anthropic", "https://api.minimaxi.com/anthropic"),
],
)
def test_minimax_provider_uses_configured_adapter_and_endpoint(
monkeypatch, provider, expected_adapter, expected_base_url
):
created = {}

def fake_openai(**kwargs):
created.update(adapter="openai", kwargs=kwargs)
return object()

def fake_anthropic(**kwargs):
created.update(adapter="anthropic", kwargs=kwargs)
return object()

provider_config = config.MINIMAX_PROVIDER_CONFIGS[provider]
monkeypatch.setattr(llm_provider, "ChatOpenAI", fake_openai)
monkeypatch.setattr(llm_provider, "ChatAnthropic", fake_anthropic)
monkeypatch.setenv("MINIMAX_API_KEY", "unit-test-key")
monkeypatch.delenv(provider_config["endpoint_env"], raising=False)

llm_provider.get_llm_model(provider=provider, model_name=EXPECTED_MODELS[0])

assert created["adapter"] == expected_adapter
assert created["kwargs"]["model"] == EXPECTED_MODELS[0]
assert created["kwargs"]["base_url"] == expected_base_url
assert created["kwargs"]["api_key"] == "unit-test-key"


def test_minimax_clients_append_expected_request_paths(monkeypatch):
captured_urls = []

def fake_send(self, request, *args, **kwargs):
captured_urls.append(str(request.url))
if request.url.path.endswith("/chat/completions"):
payload = {
"id": "chatcmpl-test",
"object": "chat.completion",
"created": 0,
"model": EXPECTED_MODELS[0],
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": "ok"},
"finish_reason": "stop",
}
],
"usage": {
"prompt_tokens": 1,
"completion_tokens": 1,
"total_tokens": 2,
},
}
else:
payload = {
"id": "msg_test",
"type": "message",
"role": "assistant",
"model": EXPECTED_MODELS[0],
"content": [{"type": "text", "text": "ok"}],
"stop_reason": "end_turn",
"stop_sequence": None,
"usage": {"input_tokens": 1, "output_tokens": 1},
}
return httpx.Response(200, json=payload, request=request)

monkeypatch.setattr(httpx.Client, "send", fake_send)
expected_urls = {
"minimax": "https://api.minimax.io/v1/chat/completions",
"minimax_cn": "https://api.minimaxi.com/v1/chat/completions",
"minimax_anthropic": "https://api.minimax.io/anthropic/v1/messages",
"minimax_anthropic_cn": "https://api.minimaxi.com/anthropic/v1/messages",
}

for provider, expected_url in expected_urls.items():
captured_urls.clear()
model = llm_provider.get_llm_model(
provider=provider,
model_name=EXPECTED_MODELS[0],
api_key="unit-test-key",
temperature=0.0,
)

model.invoke("hello")

assert captured_urls == [expected_url]