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
Binary file added frontend/public/icons/adapter-icons/MiniMax.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
53 changes: 53 additions & 0 deletions unstract/sdk1/src/unstract/sdk1/adapters/base1.py
Original file line number Diff line number Diff line change
Expand Up @@ -512,7 +512,9 @@ def _validate_branded_openai_compatible(

_NVIDIA_BUILD_API_BASE = "https://integrate.api.nvidia.com/v1"
_OPENROUTER_API_BASE = "https://openrouter.ai/api/v1"
_MINIMAX_API_BASE = "https://api.minimax.io/v1"
_OPENROUTER_PROVIDER_PREFIX = "openrouter/"
_MINIMAX_PROVIDER_PREFIX = "minimax/"


class NvidiaBuildLLMParameters(OpenAICompatibleLLMParameters):
Expand All @@ -528,6 +530,57 @@ def validate(adapter_metadata: dict[str, "Any"]) -> dict[str, "Any"]:
)


class MiniMaxLLMParameters(BaseChatCompletionParameters):
"""Adapter for MiniMax's OpenAI-compatible API."""

api_key: str
api_base: str = _MINIMAX_API_BASE
temperature: float | None = 1

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Temperature field override drops validation constraints from the base class.

BaseChatCompletionParameters defines temperature: float | None = Field(default=0.1, ge=0, le=2), but the MiniMax subclass redeclares it as temperature: float | None = 1 without Field(...). In Pydantic v2, redeclaring a field fully replaces the parent definition, so the ge=0, le=2 bounds are lost. Invalid temperatures (e.g., -1, 5) would pass validation and be sent to the API.

💚 Proposed fix to preserve constraints
-    temperature: float | None = 1
+    temperature: float | None = Field(default=1, ge=0, le=2)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
temperature: float | None = 1
temperature: float | None = Field(default=1, ge=0, le=2)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@unstract/sdk1/src/unstract/sdk1/adapters/base1.py` at line 539, Update the
MiniMax subclass’s temperature declaration to retain the base class’s Pydantic
Field validation, including default 0.1 and ge=0/le=2 constraints, rather than
redeclaring it as an unconstrained scalar. Preserve the existing nullable float
type.

thinking: dict[str, str] | None = None

@staticmethod
def validate(adapter_metadata: dict[str, "Any"]) -> dict[str, "Any"]:
adapter_metadata = dict(adapter_metadata)
api_base = adapter_metadata.get("api_base")
if not (isinstance(api_base, str) and api_base.strip()):
adapter_metadata["api_base"] = _MINIMAX_API_BASE

adapter_metadata["model"] = MiniMaxLLMParameters.validate_model(adapter_metadata)
model_id = adapter_metadata["model"][len(_MINIMAX_PROVIDER_PREFIX) :]

if "enable_thinking" in adapter_metadata:
enable_thinking = adapter_metadata.pop("enable_thinking")
if not isinstance(enable_thinking, bool):
raise ValueError("enable_thinking must be a boolean.")
adapter_metadata["thinking"] = {
"type": "adaptive" if enable_thinking else "disabled"
}

thinking = adapter_metadata.get("thinking")
if thinking is not None:
if not isinstance(thinking, dict) or thinking.get("type") not in {
"adaptive",
"disabled",
}:
raise ValueError("thinking.type must be adaptive or disabled.")
if (
model_id.lower().startswith("minimax-m2")
and thinking["type"] == "disabled"
):
raise ValueError(f"{model_id} does not support disabling thinking.")

return MiniMaxLLMParameters(**adapter_metadata).model_dump()

@staticmethod
def validate_model(adapter_metadata: dict[str, "Any"]) -> str:
model = str(adapter_metadata.get("model", "")).strip()
if not model:
raise ValueError("model is required for the MiniMax adapter.")
if model.startswith(_MINIMAX_PROVIDER_PREFIX):
return model
return f"{_MINIMAX_PROVIDER_PREFIX}{model}"


class OpenRouterLLMParameters(BaseChatCompletionParameters):
"""Adapter for OpenRouter (openrouter.ai).

Expand Down
2 changes: 2 additions & 0 deletions unstract/sdk1/src/unstract/sdk1/adapters/llm1/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from unstract.sdk1.adapters.llm1.anyscale import AnyscaleLLMAdapter
from unstract.sdk1.adapters.llm1.azure_openai import AzureOpenAILLMAdapter
from unstract.sdk1.adapters.llm1.bedrock import AWSBedrockLLMAdapter
from unstract.sdk1.adapters.llm1.minimax import MiniMaxLLMAdapter
from unstract.sdk1.adapters.llm1.nvidia_build import NvidiaBuildLLMAdapter
from unstract.sdk1.adapters.llm1.ollama import OllamaLLMAdapter
from unstract.sdk1.adapters.llm1.openai import OpenAILLMAdapter
Expand All @@ -23,6 +24,7 @@
"AnyscaleLLMAdapter",
"AWSBedrockLLMAdapter",
"AzureOpenAILLMAdapter",
"MiniMaxLLMAdapter",
"NvidiaBuildLLMAdapter",
"OllamaLLMAdapter",
"OpenAILLMAdapter",
Expand Down
45 changes: 45 additions & 0 deletions unstract/sdk1/src/unstract/sdk1/adapters/llm1/minimax.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from typing import Any

from unstract.sdk1.adapters.base1 import BaseAdapter, MiniMaxLLMParameters
from unstract.sdk1.adapters.enums import AdapterTypes

DESCRIPTION = (
"Adapter for MiniMax's OpenAI-compatible API. "
"Supply a model name and your MiniMax API key; the endpoint is preconfigured."
)


class MiniMaxLLMAdapter(MiniMaxLLMParameters, BaseAdapter):
@staticmethod
def get_id() -> str:
return "minimax|4f0e4241-2430-4921-81bf-8b2c6040d8d2"

@staticmethod
def get_metadata() -> dict[str, Any]:
return {
"name": "MiniMax",
"version": "1.0.0",
"adapter": MiniMaxLLMAdapter,
"description": DESCRIPTION,
"is_active": True,
}

@staticmethod
def get_name() -> str:
return "MiniMax"

@staticmethod
def get_description() -> str:
return DESCRIPTION

@staticmethod
def get_provider() -> str:
return "minimax"

@staticmethod
def get_icon() -> str:
return "/icons/adapter-icons/MiniMax.png"

@staticmethod
def get_adapter_type() -> AdapterTypes:
return AdapterTypes.LLM
70 changes: 70 additions & 0 deletions unstract/sdk1/src/unstract/sdk1/adapters/llm1/static/minimax.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
{
"title": "MiniMax",
"type": "object",
"required": [
"adapter_name",
"api_key",
"model"
],
"properties": {
"adapter_name": {
"type": "string",
"title": "Name",
"default": "",
"description": "Provide a unique name for this adapter instance. Example: minimax-1"
},
"api_key": {
"type": "string",
"title": "API Key",
"format": "password",
"description": "Your MiniMax API key from [platform.minimax.io](https://platform.minimax.io)."
},
"model": {
"type": "string",
"title": "Model",
"default": "MiniMax-M3",
"examples": [
"MiniMax-M3",
"MiniMax-M2.7"
],
"description": "MiniMax-M3 supports a 1,000,000-token context window and multimodal input. MiniMax-M2.7 supports a 204,800-token context window and text input."
},
"api_base": {
"type": "string",
"format": "url",
"title": "API Base",
"default": "https://api.minimax.io/v1",
"description": "MiniMax OpenAI-compatible endpoint. The default is for global accounts; use https://api.minimaxi.com/v1 for China accounts."
},
"max_tokens": {
"type": "number",
"minimum": 0,
"multipleOf": 1,
"title": "Maximum Output Tokens",
"default": 4096,
"description": "Maximum number of output tokens to limit LLM replies. Leave it empty to use the provider default."
},
"max_retries": {
"type": "number",
"minimum": 0,
"multipleOf": 1,
"title": "Max Retries",
"default": 5,
"description": "The maximum number of times to retry a request if it fails."
},
"timeout": {
"type": "number",
"minimum": 0,
"multipleOf": 1,
"title": "Timeout",
"default": 900,
"description": "Timeout in seconds."
},
"enable_thinking": {
"type": "boolean",
"title": "Enable Thinking",
"default": true,
"description": "Enable adaptive thinking for MiniMax-M3. MiniMax-M2.7 always keeps thinking enabled."
}
}
}
72 changes: 70 additions & 2 deletions unstract/sdk1/tests/test_branded_openai_adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import pytest
from unstract.sdk1.adapters.base1 import (
MiniMaxLLMParameters,
NvidiaBuildEmbeddingParameters,
NvidiaBuildLLMParameters,
OpenAICompatibleEmbeddingParameters,
Expand All @@ -14,19 +15,21 @@
OpenAICompatibleEmbeddingAdapter,
)
from unstract.sdk1.adapters.llm1 import adapters as llm_adapters
from unstract.sdk1.adapters.llm1.minimax import MiniMaxLLMAdapter
from unstract.sdk1.adapters.llm1.nvidia_build import NvidiaBuildLLMAdapter
from unstract.sdk1.adapters.llm1.openrouter import OpenRouterLLMAdapter

_NVIDIA_BUILD_API_BASE = "https://integrate.api.nvidia.com/v1"
_OPENROUTER_API_BASE = "https://openrouter.ai/api/v1"
_MINIMAX_API_BASE = "https://api.minimax.io/v1"


# --- Branded LLM adapters -------------------------------------------------


@pytest.mark.parametrize(
"adapter",
[NvidiaBuildLLMAdapter, OpenRouterLLMAdapter],
[MiniMaxLLMAdapter, NvidiaBuildLLMAdapter, OpenRouterLLMAdapter],
)
def test_branded_llm_adapter_is_registered(adapter: type) -> None:
adapter_id = adapter.get_id()
Expand All @@ -41,6 +44,55 @@ def test_nvidia_llm_prefixes_model_via_custom_openai() -> None:
assert validated["api_base"] == _NVIDIA_BUILD_API_BASE


@pytest.mark.parametrize("model", ["MiniMax-M3", "MiniMax-M2.7"])
def test_minimax_llm_routes_via_native_provider(model: str) -> None:
from litellm import get_llm_provider

validated = MiniMaxLLMParameters.validate({"model": model, "api_key": "k"})

assert validated["model"] == f"minimax/{model}"
assert validated["api_base"] == _MINIMAX_API_BASE
assert get_llm_provider(validated["model"])[1] == "minimax"


def test_minimax_model_prefix_is_idempotent() -> None:
once = MiniMaxLLMParameters.validate({"model": "MiniMax-M3", "api_key": "k"})
twice = MiniMaxLLMParameters.validate(dict(once))

assert twice["model"] == once["model"] == "minimax/MiniMax-M3"


def test_minimax_native_routing_resolves_usage_cost() -> None:
from litellm import cost_per_token

prompt_cost, completion_cost = cost_per_token(
"minimax/MiniMax-M3", prompt_tokens=1, completion_tokens=1
)

assert prompt_cost > 0
assert completion_cost > 0


def test_minimax_maps_thinking_toggle_to_native_parameter() -> None:
enabled = MiniMaxLLMParameters.validate(
{"model": "MiniMax-M3", "api_key": "k", "enable_thinking": True}
)
disabled = MiniMaxLLMParameters.validate(
{"model": "MiniMax-M3", "api_key": "k", "enable_thinking": False}
)

assert enabled["thinking"] == {"type": "adaptive"}
assert disabled["thinking"] == {"type": "disabled"}
assert "enable_thinking" not in enabled


def test_minimax_m2_rejects_disabling_thinking() -> None:
with pytest.raises(ValueError, match="does not support disabling thinking"):
MiniMaxLLMParameters.validate(
{"model": "MiniMax-M2.7", "api_key": "k", "enable_thinking": False}
)


def test_openrouter_llm_routes_via_native_openrouter_provider() -> None:
from litellm import get_llm_provider

Expand Down Expand Up @@ -106,6 +158,7 @@ def test_openrouter_reasoning_survives_revalidation() -> None:
@pytest.mark.parametrize(
("params", "default_base"),
[
(MiniMaxLLMParameters, _MINIMAX_API_BASE),
(NvidiaBuildLLMParameters, _NVIDIA_BUILD_API_BASE),
(OpenRouterLLMParameters, _OPENROUTER_API_BASE),
],
Expand All @@ -120,7 +173,7 @@ def test_branded_llm_blank_api_base_falls_back_to_default(

@pytest.mark.parametrize(
"params",
[NvidiaBuildLLMParameters, OpenRouterLLMParameters],
[MiniMaxLLMParameters, NvidiaBuildLLMParameters, OpenRouterLLMParameters],
)
def test_branded_llm_honours_api_base_override(params: type) -> None:
validated = params.validate(
Expand All @@ -133,6 +186,7 @@ def test_branded_llm_honours_api_base_override(params: type) -> None:
@pytest.mark.parametrize(
("adapter", "default_base"),
[
(MiniMaxLLMAdapter, _MINIMAX_API_BASE),
(NvidiaBuildLLMAdapter, _NVIDIA_BUILD_API_BASE),
(OpenRouterLLMAdapter, _OPENROUTER_API_BASE),
],
Expand All @@ -147,6 +201,20 @@ def test_branded_llm_schema_exposes_api_base_with_default(
assert "model" in schema["required"]


def test_minimax_schema_covers_models_thinking_and_regions() -> None:
schema = json.loads(MiniMaxLLMAdapter.get_json_schema())

assert schema["properties"]["model"]["examples"] == [
"MiniMax-M3",
"MiniMax-M2.7",
]
assert schema["properties"]["enable_thinking"]["default"] is True
assert (
"https://api.minimaxi.com/v1" in schema["properties"]["api_base"]["description"]
)
assert "reasoning_effort" not in json.dumps(schema)


# --- Branded / generic embedding adapters ---------------------------------


Expand Down