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.
12 changes: 12 additions & 0 deletions unstract/sdk1/src/unstract/sdk1/adapters/base1.py
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,7 @@ 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/"


Expand All @@ -528,6 +529,17 @@ def validate(adapter_metadata: dict[str, "Any"]) -> dict[str, "Any"]:
)


class MiniMaxLLMParameters(OpenAICompatibleLLMParameters):

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.

Routing through custom_openai/ silently zeroes out cost tracking for MiniMax.

_validate_branded_openai_compatible delegates to OpenAICompatibleLLMParameters.validate(), whose validate_model() unconditionally prepends custom_openai/. cost_model is only populated when the endpoint is OpenAI's own (base1.py:483), so it stays unset here, and audit.py:98 ends up calling cost_per_token(model="custom_openai/MiniMax-M3") — which raises, gets swallowed by the bare except, and records cost_in_dollars = 0.0 at debug level.

LiteLLM already ships a native MiniMax provider, and it prices M3:

litellm.get_llm_provider("minimax/MiniMax-M3")  -> ('MiniMax-M3', 'minimax', None, None)

cost_per_token("custom_openai/MiniMax-M3") -> raises        => cost = 0.0
cost_per_token("minimax/MiniMax-M3")       -> $0.30 / $1.20 per 1M tokens

So every MiniMax token would bill as $0 in usage tracking, silently.

This is why OpenRouterLLMParameters extends BaseChatCompletionParameters instead of OpenAICompatibleLLMParameters — its own docstring says so: "Routed through LiteLLM's native openrouter/ provider so per-token costs resolve and reasoning params map without provider-specific workarounds." MiniMax should follow OpenRouter's template, not NVIDIA Build's.

Worth noting NVIDIA Build's custom_openai/ routing is correct for NVIDIA — LiteLLM prices zero nvidia_nim/ chat models (3 entries, all rerankers, all $0.0), so there's nothing to forfeit there. That reason doesn't carry over to MiniMax.

Bonus: MinimaxChatConfig.get_api_base() already returns https://api.minimax.io/v1, so going native lets you drop the hardcoded _MINIMAX_API_BASE constant entirely.

"""OpenAI-compatible adapter for MiniMax models."""

# Required str so a directly-constructed instance stays valid.
api_base: str = _MINIMAX_API_BASE

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


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"

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.

The shipped MiniMax.png is the OpenRouter logo.

frontend/public/icons/adapter-icons/MiniMax.png is byte-for-byte identical to OpenRouter.png:

0980b0d3102c84da9d332862c563c44f  MiniMax.png
0980b0d3102c84da9d332862c563c44f  OpenRouter.png
676be9d2fc5cec78d5be80af03eb9fd4  NvidiaBuild.png

Both 29953 bytes. Rendering it confirms it's the OpenRouter fork-arrow mark. NvidiaBuild.png differs, so this isn't a shared-placeholder convention — it looks like the icon was copied along with the adapter template and never swapped.

Users would see OpenRouter's branding on the MiniMax card in the adapter picker. Needs the real MiniMax logo.


@staticmethod
def get_adapter_type() -> AdapterTypes:
return AdapterTypes.LLM
113 changes: 113 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,113 @@
{
"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",
"description": "The MiniMax model name. Example: MiniMax-M3"
},
"api_base": {
"type": "string",
"format": "url",
"title": "API Base",
"default": "https://api.minimax.io/v1",
"description": "MiniMax OpenAI-compatible endpoint. Pre-filled with the default; change only if MiniMax moves the base URL or you use a regional endpoint."
},
"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_reasoning": {
"type": "boolean",
"title": "Enable Reasoning",
"default": false,
"description": "Toggle on for reasoning models to route reasoning_effort via the OpenAI-compatible request body."
}
Comment on lines +59 to +64

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.

P1 enable_reasoning toggle silently has no effect on MiniMax

The adapter routes reasoning through OpenAICompatibleLLMParameters.validate(), which sends reasoning_effort in extra_body. MiniMax's OpenAI-compatible API does not recognise reasoning_effort; it exposes deep thinking via a thinking: {type: "adaptive"} field instead (MiniMax docs). A user who turns this toggle on will have their temperature stripped from the request (potentially changing generation behaviour) while MiniMax silently ignores the unknown reasoning_effort key — no actual reasoning is activated. Is there a plan to map enable_reasoningthinking: {type: "adaptive"} in MiniMax's request body, or should the enable_reasoning section be removed from minimax.json until that mapping is implemented?

Prompt To Fix With AI
This is a comment left during a code review.
Path: unstract/sdk1/src/unstract/sdk1/adapters/llm1/static/minimax.json
Line: 59-64

Comment:
**`enable_reasoning` toggle silently has no effect on MiniMax**

The adapter routes reasoning through `OpenAICompatibleLLMParameters.validate()`, which sends `reasoning_effort` in `extra_body`. MiniMax's OpenAI-compatible API does not recognise `reasoning_effort`; it exposes deep thinking via a `thinking: {type: "adaptive"}` field instead ([MiniMax docs](https://platform.minimax.io/docs/guides/text-generation)). A user who turns this toggle on will have their `temperature` stripped from the request (potentially changing generation behaviour) while MiniMax silently ignores the unknown `reasoning_effort` key — no actual reasoning is activated. Is there a plan to map `enable_reasoning``thinking: {type: "adaptive"}` in MiniMax's request body, or should the `enable_reasoning` section be removed from `minimax.json` until that mapping is implemented?

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

Fix in Claude Code

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.

Confirmed — this is a real issue, not just a doc mismatch.

The reasoning path in OpenAICompatibleLLMParameters.validate() (base1.py) does two things when enable_reasoning is on: it puts reasoning_effort into extra_body and pops temperature (validated.pop("temperature", None)). MiniMax's OpenAI-compatible endpoint doesn't recognise reasoning_effort — it gates deep thinking via thinking: {type: "adaptive"} — so the key is silently ignored while the user's temperature is stripped. The toggle activates no reasoning but changes generation behaviour.

Severity is bounded to the opt-in case: it only fires if a user flips the toggle. It won't auto-enable, since _is_openai_reasoning_model matches only o1|o3|o4|gpt-5, not MiniMax-M3.

Fix: dropping the enable_reasoning block (and its allOf conditional) from minimax.json for now, since the shared OpenAI-compatible validate path has no way to emit MiniMax's thinking field. A proper enable_reasoning → thinking:{type:"adaptive"} mapping would be a follow-up in the validate layer.

},
"allOf": [
{
"if": {
"properties": {
"enable_reasoning": {
"const": true
}
},
"required": [
"enable_reasoning"
]
},
"then": {
"properties": {
"reasoning_effort": {
"type": "string",
"enum": [
"low",
"medium",
"high"
],
"default": "medium",
"title": "Reasoning Effort",
"description": "Sets the reasoning strength when Enable Reasoning is on."
}
},
"required": [
"reasoning_effort"
]
}
},
{
"if": {
"properties": {
"enable_reasoning": {
"const": false
}
},
"required": [
"enable_reasoning"
]
},
"then": {
"properties": {}
}
}
]
}
16 changes: 14 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,13 @@ def test_nvidia_llm_prefixes_model_via_custom_openai() -> None:
assert validated["api_base"] == _NVIDIA_BUILD_API_BASE


def test_minimax_llm_prefixes_model_via_custom_openai() -> None:
validated = MiniMaxLLMParameters.validate({"model": "MiniMax-M3", "api_key": "k"})

assert validated["model"] == "custom_openai/MiniMax-M3"
assert validated["api_base"] == _MINIMAX_API_BASE


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

Expand Down Expand Up @@ -106,6 +116,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 +131,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 +144,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 Down