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
9 changes: 9 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,15 @@
# --- Chutes ---
#CHUTES_API_KEY_1="YOUR_CHUTES_API_KEY"

# --- MiniMax ---
# Supports MiniMax-M2.7, MiniMax-M2.7-highspeed, MiniMax-M2.5, MiniMax-M2.5-highspeed.
# All models share 204K context. Obtain your key at https://platform.minimaxi.com
# Note: MiniMax requires temperature > 0. Set OVERRIDE_TEMPERATURE_ZERO=set (below)
# to automatically convert temperature=0 requests to temperature=1.0.
Comment on lines +58 to +59
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Incorrect temperature value in comment

The comment says OVERRIDE_TEMPERATURE_ZERO will convert temperature=0 requests to 1.0, but the MiniMax provider actually clamps to 0.01 (_MINIMAX_TEMPERATURE_MIN). Users relying on this comment will be surprised when they observe 0.01 in practice.

Suggested change
# Note: MiniMax requires temperature > 0. Set OVERRIDE_TEMPERATURE_ZERO=set (below)
# to automatically convert temperature=0 requests to temperature=1.0.
# Note: MiniMax requires temperature > 0. Set OVERRIDE_TEMPERATURE_ZERO=set (below)
# to automatically convert temperature=0 requests to 0.01.

#MINIMAX_API_KEY_1="YOUR_MINIMAX_API_KEY"
Comment on lines +58 to +60
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Make the temperature override hint actionable.

This note tells users to set OVERRIDE_TEMPERATURE_ZERO "below", but the example file never actually shows that variable. As written, the workaround is not discoverable, and it does not explain how the global override relates to the provider's 0.01 clamp.

Suggested fix
 # Note: MiniMax requires temperature > 0. Set OVERRIDE_TEMPERATURE_ZERO=set (below)
 # to automatically convert temperature=0 requests to temperature=1.0.
+#OVERRIDE_TEMPERATURE_ZERO=set
 `#MINIMAX_API_KEY_1`="YOUR_MINIMAX_API_KEY"
📝 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
# Note: MiniMax requires temperature > 0. Set OVERRIDE_TEMPERATURE_ZERO=set (below)
# to automatically convert temperature=0 requests to temperature=1.0.
#MINIMAX_API_KEY_1="YOUR_MINIMAX_API_KEY"
# Note: MiniMax requires temperature > 0. Set OVERRIDE_TEMPERATURE_ZERO=set (below)
# to automatically convert temperature=0 requests to temperature=1.0.
`#OVERRIDE_TEMPERATURE_ZERO`=set
`#MINIMAX_API_KEY_1`="YOUR_MINIMAX_API_KEY"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.env.example around lines 58 - 60, The .env.example currently references
OVERRIDE_TEMPERATURE_ZERO but doesn't define it or explain how it interacts with
the provider's 0.01 clamp; add a commented example line for
OVERRIDE_TEMPERATURE_ZERO (e.g., OVERRIDE_TEMPERATURE_ZERO=set or =1) near the
other provider keys (like MINIMAX_API_KEY_1) and include a short comment
explaining that when set it will convert incoming temperature=0 requests to
temperature=1.0 (override) while providers still clamp minimum values to 0.01,
so setting this enables the MiniMax-specific workaround; update the surrounding
note to point to that example line and clarify the relationship to the provider
clamp.

# Optional: override the API base URL (default: https://api.minimax.io/v1)
#MINIMAX_API_BASE="https://api.minimax.io/v1"

# ------------------------------------------------------------------------------
# | [OAUTH] Provider OAuth 2.0 Credentials |
# ------------------------------------------------------------------------------
Expand Down
4 changes: 4 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[pytest]
asyncio_mode = strict
markers =
integration: marks tests as integration tests requiring a live MINIMAX_API_KEY (deselect with '-m "not integration"')
1 change: 1 addition & 0 deletions src/proxy_app/provider_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"cohere": "https://api.cohere.ai/v1",
"bedrock": "https://bedrock-runtime.us-east-1.amazonaws.com",
"openrouter": "https://openrouter.ai/api/v1",
"minimax": "https://api.minimax.io/v1",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Honor MINIMAX_API_BASE before the hardcoded default.

get_provider_endpoint() resolves PROVIDER_URL_MAP before checking <PROVIDER>_API_BASE, so adding minimax here makes the documented MINIMAX_API_BASE setting unreachable. That conflicts with .env.example and src/rotator_library/litellm_providers.py:593-602, and it can send requests to the wrong host whenever a custom MiniMax base is configured.

Suggested fix
-    # First, check the hardcoded map
-    base_url = PROVIDER_URL_MAP.get(provider)
-
-    # If not found, check for custom provider via environment variable
-    if not base_url:
-        api_base_env = f"{provider.upper()}_API_BASE"
-        base_url = os.getenv(api_base_env)
-        if not base_url:
-            return None
+    api_base_env = f"{provider.upper()}_API_BASE"
+    base_url = os.getenv(api_base_env) or PROVIDER_URL_MAP.get(provider)
+    if not base_url:
+        return None
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/proxy_app/provider_urls.py` at line 33, PROVIDER_URL_MAP currently
hardcodes "minimax", which prevents honoring the MINIMAX_API_BASE env var;
change the logic so get_provider_endpoint checks for an environment override
(key f"{provider.upper()}_API_BASE" or specifically MINIMAX_API_BASE for
provider "minimax") before falling back to PROVIDER_URL_MAP, or remove the
"minimax" hardcoded entry and instead have PROVIDER_URL_MAP supply only defaults
used when the env var is absent; update get_provider_endpoint (and any lookup
code that uses PROVIDER_URL_MAP) to prefer
os.environ.get(f"{provider.upper()}_API_BASE") and return that if present,
otherwise use PROVIDER_URL_MAP["minimax"].

}

def get_provider_endpoint(provider: str, model_name: str, incoming_path: str) -> Optional[str]:
Expand Down
142 changes: 142 additions & 0 deletions src/rotator_library/providers/minimax_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# SPDX-License-Identifier: LGPL-3.0-only
# Copyright (c) 2026 Mirrowel

import httpx
import logging
from typing import List, Dict, Any, AsyncGenerator, Union
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Unused Dict import

Dict is imported from typing but never referenced in this file.

Suggested change
from typing import List, Dict, Any, AsyncGenerator, Union
from typing import List, Any, AsyncGenerator, Union

import litellm

from .provider_interface import ProviderInterface

lib_logger = logging.getLogger("rotator_library")
lib_logger.propagate = False
if not lib_logger.handlers:
lib_logger.addHandler(logging.NullHandler())

# Static fallback models used when the MiniMax /v1/models endpoint is unavailable.
# MiniMax-M2.7 is the latest generation; all models share 204K context.
_MINIMAX_STATIC_MODELS = [
"MiniMax-M2.7",
"MiniMax-M2.7-highspeed",
"MiniMax-M2.5",
"MiniMax-M2.5-highspeed",
]

# MiniMax requires temperature strictly greater than 0.
# Values at or below 0 are clamped to this minimum before forwarding the request.
_MINIMAX_TEMPERATURE_MIN = 0.01


class MiniMaxProvider(ProviderInterface):
"""
Provider implementation for the MiniMax API.

MiniMax exposes an OpenAI-compatible chat/completions endpoint at
https://api.minimax.io/v1. LiteLLM routes requests using the
``minimax/`` prefix, so no custom acompletion logic is required for the
happy path.

The only provider-specific behaviour handled here is:

* **Model discovery** – fetches the live model list from the MiniMax
``/v1/models`` endpoint and falls back to a static list of known
M2.7 / M2.5 models when the endpoint is unreachable.

* **Temperature clamping** – MiniMax rejects ``temperature=0`` (or any
value ≤ 0). Requests that carry such a temperature are silently
adjusted to ``_MINIMAX_TEMPERATURE_MIN`` (0.01) before being forwarded
to LiteLLM.

Configuration
-------------
Set one or more API keys using the numbered ``_API_KEY`` suffix pattern::

MINIMAX_API_KEY_1=your_key_here
MINIMAX_API_KEY_2=another_key_here

Optionally override the API base URL (defaults to
``https://api.minimax.io/v1``)::

MINIMAX_API_BASE=https://api.minimax.io/v1

To avoid sending ``temperature=0`` to MiniMax from any client, you may
also set the global proxy option::

OVERRIDE_TEMPERATURE_ZERO=set
"""

async def get_models(self, api_key: str, client: httpx.AsyncClient) -> List[str]:
"""
Returns available MiniMax model names (with ``minimax/`` prefix).

Tries to discover models dynamically from the ``/v1/models`` endpoint.
Falls back to the static list when the endpoint is unavailable or
returns an unexpected payload.
"""
try:
response = await client.get(
"https://api.minimax.io/v1/models",
headers={"Authorization": f"Bearer {api_key}"},
Comment on lines +77 to +79
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 MINIMAX_API_BASE ignored in model discovery

get_models hardcodes https://api.minimax.io/v1/models as the endpoint URL. This means that when a user sets MINIMAX_API_BASE to a custom URL (e.g. a private deployment or staging endpoint), model discovery will still hit the public MiniMax API rather than the overridden base. The resulting 404 / connection error silently falls back to the static model list, making the env-var override appear broken for model discovery.

Suggested change
response = await client.get(
"https://api.minimax.io/v1/models",
headers={"Authorization": f"Bearer {api_key}"},
base_url = os.getenv("MINIMAX_API_BASE", "https://api.minimax.io/v1").rstrip("/")
response = await client.get(
f"{base_url}/models",
headers={"Authorization": f"Bearer {api_key}"},
)

You'll also need to add import os at the top if it is not already present.

)
Comment on lines +77 to +80
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Model discovery still ignores MINIMAX_API_BASE.

The provider advertises an overridable base URL, but Lines 77-80 always call the public https://api.minimax.io/v1/models endpoint. With a custom gateway/base URL, model discovery uses a different host than completions and sends the bearer token to that default endpoint.

Suggested fix
+import os
 import httpx
 import logging
 from typing import List, Dict, Any, AsyncGenerator, Union
 import litellm
@@
         try:
+            api_base = os.getenv("MINIMAX_API_BASE", "https://api.minimax.io/v1").rstrip("/")
             response = await client.get(
-                "https://api.minimax.io/v1/models",
+                f"{api_base}/models",
                 headers={"Authorization": f"Bearer {api_key}"},
             )
📝 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
response = await client.get(
"https://api.minimax.io/v1/models",
headers={"Authorization": f"Bearer {api_key}"},
)
import os
import httpx
import logging
from typing import List, Dict, Any, AsyncGenerator, Union
import litellm
try:
api_base = os.getenv("MINIMAX_API_BASE", "https://api.minimax.io/v1").rstrip("/")
response = await client.get(
f"{api_base}/models",
headers={"Authorization": f"Bearer {api_key}"},
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/rotator_library/providers/minimax_provider.py` around lines 77 - 80, The
model discovery call currently hardcodes "https://api.minimax.io/v1/models";
update the code that performs the GET (the call to client.get in
minimax_provider.py that fetches models) to build the URL from the provider's
overridable base (MINIMAX_API_BASE or the provider config variable used
elsewhere for completions) instead of the hardcoded host—e.g. derive api_base =
MINIMAX_API_BASE or the existing config value, normalize to strip a trailing
slash, then call f"{api_base}/v1/models" with the same Authorization header so
discovery uses the same gateway as completions and does not leak the bearer
token to the public endpoint.

response.raise_for_status()
data = response.json().get("data", [])
if data:
models = [f"minimax/{model['id']}" for model in data]
lib_logger.debug(
f"MiniMaxProvider: discovered {len(models)} models from API"
)
return models
except httpx.RequestError as e:
lib_logger.warning(
f"MiniMaxProvider: failed to fetch models from API, using static list: {e}"
)
except Exception as e:
lib_logger.warning(
f"MiniMaxProvider: unexpected error fetching models, using static list: {e}"
)

# Return the static fallback list
static = [f"minimax/{m}" for m in _MINIMAX_STATIC_MODELS]
lib_logger.info(
f"MiniMaxProvider: returning {len(static)} static fallback models"
)
return static

# ------------------------------------------------------------------
# Temperature clamping
# ------------------------------------------------------------------

def has_custom_logic(self) -> bool:
"""
Enable custom acompletion so we can clamp temperature before the
request reaches the MiniMax API.
"""
return True

async def acompletion(
self,
client: httpx.AsyncClient,
**kwargs: Any,
) -> Union[litellm.ModelResponse, AsyncGenerator[litellm.ModelResponse, None]]:
"""
Forward the completion request to LiteLLM after clamping temperature.

MiniMax rejects any ``temperature`` value that is not strictly
positive. When the caller sends ``temperature=0`` (common for
deterministic / tool-use requests) this method silently raises the
value to ``_MINIMAX_TEMPERATURE_MIN``.
"""
temperature = kwargs.get("temperature")
if temperature is not None and temperature <= 0:
lib_logger.debug(
f"MiniMaxProvider: clamping temperature {temperature!r} → "
f"{_MINIMAX_TEMPERATURE_MIN} (MiniMax requires temperature > 0)"
)
kwargs = dict(kwargs)
kwargs["temperature"] = _MINIMAX_TEMPERATURE_MIN

# Remove internal proxy keys that must not reach LiteLLM
kwargs.pop("credential_identifier", None)
kwargs.pop("transaction_context", None)

return await litellm.acompletion(**kwargs)
5 changes: 5 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import sys
import os

# Allow imports from the src directory without installing the package
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
Loading
Loading