Skip to content
Merged
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.
99 changes: 97 additions & 2 deletions unstract/sdk1/src/unstract/sdk1/adapters/base1.py
Original file line number Diff line number Diff line change
Expand Up @@ -530,7 +530,35 @@

_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/"
_MINIMAX_ANTHROPIC_PROVIDER_PREFIX = "anthropic/"
_MINIMAX_CONTEXT_WINDOWS = {
"MiniMax-M3": 1_000_000,
}
# All M2.x variants share one window. Kept local because LiteLLM overstates it.
# REF: https://platform.minimax.io/docs/api-reference/text-openai-api
_MINIMAX_M2_CONTEXT_WINDOW = 204_800


def _minimax_provider_prefix(api_base: str) -> str:
path = urlparse(api_base).path.rstrip("/").lower()
if path.endswith("/anthropic"):
return _MINIMAX_ANTHROPIC_PROVIDER_PREFIX
return _MINIMAX_PROVIDER_PREFIX


def _is_minimax_m2_model(model_id: str) -> bool:
return re.match(r"^minimax-m2(?:$|[.-])", model_id, re.IGNORECASE) is not None


def _minimax_context_window(model_id: str) -> int | None:
if context_window := _MINIMAX_CONTEXT_WINDOWS.get(model_id):
return context_window
if _is_minimax_m2_model(model_id):
return _MINIMAX_M2_CONTEXT_WINDOW
return None


class NvidiaBuildLLMParameters(OpenAICompatibleLLMParameters):
Expand All @@ -546,6 +574,74 @@
)


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

api_key: str
api_base: str = _MINIMAX_API_BASE
temperature: float | None = Field(default=1, ge=0, le=2)
thinking: dict[str, str] | None = None
service_tier: str | None = None

@staticmethod
def validate(adapter_metadata: dict[str, "Any"]) -> dict[str, "Any"]:

Check failure on line 587 in unstract/sdk1/src/unstract/sdk1/adapters/base1.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 18 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=Zipstack_unstract&issues=AZ9hLO49nQ6LQh7VP5r6&open=AZ9hLO49nQ6LQh7VP5r6&pullRequest=2166
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"].split("/", 1)[-1]

service_tier = adapter_metadata.get("service_tier")
if service_tier not in {None, "standard", "priority"}:
raise ValueError("service_tier must be standard or priority.")

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 None and _is_minimax_m2_model(model_id):
thinking = {"type": "adaptive"}
adapter_metadata["thinking"] = 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 _is_minimax_m2_model(model_id) and thinking["type"] == "disabled":
raise ValueError(f"{model_id} does not support disabling thinking.")

validated = MiniMaxLLMParameters(**adapter_metadata).model_dump()
validated["cost_model"] = f"{_MINIMAX_PROVIDER_PREFIX}{model_id}"
if context_window := _minimax_context_window(model_id):
validated["context_window"] = context_window
validated["allowed_openai_params"] = ["service_tier", "thinking"]
return validated

@staticmethod
def validate_model(adapter_metadata: dict[str, "Any"]) -> str:
raw_model = adapter_metadata.get("model")
model = str(raw_model).strip() if raw_model is not None else ""
if not model:
raise ValueError("model is required for the MiniMax adapter.")
for prefix in (
_MINIMAX_PROVIDER_PREFIX,
_MINIMAX_ANTHROPIC_PROVIDER_PREFIX,
):
if model.startswith(prefix):
model = model[len(prefix) :]
break
api_base = str(adapter_metadata.get("api_base") or _MINIMAX_API_BASE)
return f"{_minimax_provider_prefix(api_base)}{model}"


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

Expand Down Expand Up @@ -884,8 +980,7 @@
token = validated.pop(_BEDROCK_BEARER_TOKEN_FIELD, None)
if not isinstance(token, str) or not token.strip():
raise ValueError(
f"{_BEDROCK_BEARER_TOKEN_FIELD} is required when "
"auth_type is 'bearer_token'."
f"{_BEDROCK_BEARER_TOKEN_FIELD} is required when auth_type is 'bearer_token'."
)
validated[_BEDROCK_LITELLM_BEARER_KWARG] = token.strip()

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- and Anthropic-compatible APIs. "
"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
79 changes: 79 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,79 @@
{
"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": "The model name as listed in [MiniMax's model docs](https://platform.minimax.io/docs/api-reference/text-openai-api). Examples: MiniMax-M3, MiniMax-M2.7. See [MiniMax pricing](https://platform.minimax.io/docs/guides/pricing-paygo) for rates and context windows."
},
"api_base": {
"type": "string",
"format": "url",
"title": "API Base",
"default": "https://api.minimax.io/v1",
"description": "MiniMax endpoint. Pre-filled with the global OpenAI-compatible base. A base whose path ends in `/anthropic` is sent using the Anthropic-compatible protocol; any other base uses the OpenAI-compatible protocol. China accounts use the `api.minimaxi.com` host. See [MiniMax API docs](https://platform.minimax.io/docs/api-reference/text-openai-api)."
},
"service_tier": {
"type": "string",
"title": "Service Tier",
"enum": [
"standard",
"priority"
],
"default": "standard",
"description": "Priority requests receive faster admission at a higher price. See [MiniMax pricing](https://platform.minimax.io/docs/guides/pricing-paygo). Note that cost tracking records standard-tier rates."
},
"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",
"description": "Override the protocol default for MiniMax-M3: OpenAI-compatible requests default to adaptive thinking, while Anthropic-compatible requests default to disabled thinking. MiniMax-M2.x models always keep thinking enabled. See [MiniMax API docs](https://platform.minimax.io/docs/api-reference/text-openai-api)."
}
}
}
33 changes: 25 additions & 8 deletions unstract/sdk1/src/unstract/sdk1/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ def __init__( # noqa: C901

self.kwargs = self.adapter.validate(self._adapter_metadata)
self._cost_model = self.kwargs.pop("cost_model", None)
self.kwargs.pop("context_window", None)

# REF: https://docs.litellm.ai/docs/completion/input#translated-openai-params
# supported = get_supported_openai_params(model=self.kwargs["model"],
Expand Down Expand Up @@ -328,6 +329,7 @@ def complete(self, prompt: str, **kwargs: object) -> dict[str, object]:

completion_kwargs = self.adapter.validate({**self.kwargs, **kwargs})
completion_kwargs.pop("cost_model", None)
completion_kwargs.pop("context_window", None)

# if hasattr(self, "model") and self.model not in O1_MODELS:
# completion_kwargs["temperature"] = 0.003
Expand Down Expand Up @@ -445,12 +447,12 @@ def complete_vision(
litellm.drop_params = True

logger.debug(
f"[sdk1][LLM]Invoking {self.adapter.get_provider()} "
f"vision completion API"
f"[sdk1][LLM]Invoking {self.adapter.get_provider()} vision completion API"
)

completion_kwargs = self.adapter.validate({**self.kwargs, **kwargs})
completion_kwargs.pop("cost_model", None)
completion_kwargs.pop("context_window", None)

response: dict[str, object] = litellm.completion(
messages=messages,
Expand Down Expand Up @@ -518,6 +520,7 @@ def stream_complete(

completion_kwargs = self.adapter.validate({**self.kwargs, **kwargs})
completion_kwargs.pop("cost_model", None)
completion_kwargs.pop("context_window", None)

max_retries = pop_litellm_retry_kwargs(
completion_kwargs, self._get_adapter_info()
Expand Down Expand Up @@ -589,6 +592,7 @@ async def acomplete(self, prompt: str, **kwargs: object) -> dict[str, object]:

completion_kwargs = self.adapter.validate({**self.kwargs, **kwargs})
completion_kwargs.pop("cost_model", None)
completion_kwargs.pop("context_window", None)

max_retries = pop_litellm_retry_kwargs(
completion_kwargs, self._get_adapter_info()
Expand Down Expand Up @@ -652,8 +656,21 @@ def get_context_window_size(
) -> int:
"""Returns the context window size of the LLM."""
try:
model = adapters[adapter_id][Common.MODULE].validate_model(adapter_metadata)
return get_max_tokens(model)
validated = adapters[adapter_id][Common.MODULE].validate(
dict(adapter_metadata)
)
context_window = validated.get("context_window")
if isinstance(context_window, int):
return context_window
model = cast("str", validated.get("cost_model") or validated["model"])
model_info = litellm.get_model_info(model)
context_window = model_info.get("max_input_tokens")
if isinstance(context_window, int):
return context_window
fallback = get_max_tokens(model)
if isinstance(fallback, int):
return fallback
raise ValueError(f"Context window is unavailable for model {model}.")
except Exception as e:
logger.warning(f"Failed to get context window size for {adapter_id}: {e}")
return cls.MAX_TOKENS
Expand All @@ -667,10 +684,10 @@ def get_max_tokens(
llm_config = PlatformHelper.get_adapter_config(tool, adapter_instance_id)
adapter_id = llm_config[Common.ADAPTER_ID]
adapter_metadata = llm_config[Common.ADAPTER_METADATA]

model = adapters[adapter_id][Common.MODULE].validate_model(adapter_metadata)

return get_max_tokens(model) - reserved_for_output
return (
cls.get_context_window_size(adapter_id, adapter_metadata)
- reserved_for_output
)
except Exception as e:
logger.warning(
f"Failed to get context window size for {adapter_instance_id}: {e}"
Expand Down
Loading
Loading