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
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,28 @@ Plexe uses LLMs via [LiteLLM](https://docs.litellm.ai/docs/providers), so you ca
hypothesiser_llm: "openai/gpt-5-mini"
feature_processor_llm: "anthropic/claude-sonnet-4-5-20250929"
model_definer_llm: "ollama/llama3"
planner_llm: "minimax/MiniMax-M2.7"
```

#### MiniMax

[MiniMax](https://www.minimaxi.com) models are supported as a first-class provider via the `minimax/` prefix.
Set your API key and use MiniMax models directly:

```bash
export MINIMAX_API_KEY=<your-key>
```

```yaml
# config.yaml
hypothesiser_llm: "minimax/MiniMax-M2.7"
planner_llm: "minimax/MiniMax-M2.7"
model_definer_llm: "minimax/MiniMax-M2.5-highspeed"
litellm_drop_params: true
```

Available models: `MiniMax-M2.7`, `MiniMax-M2.7-highspeed` (1M context), `MiniMax-M2.5`, `MiniMax-M2.5-highspeed` (204K context).

> [!NOTE]
> Plexe *should* work with most LiteLLM providers, but we actively test only with `openai/*` and `anthropic/*`
> models. If you encounter issues with other providers, please let us know.
Expand Down Expand Up @@ -198,6 +218,7 @@ Requires Python >= 3.10, < 3.13.
```bash
export OPENAI_API_KEY=<your-key>
export ANTHROPIC_API_KEY=<your-key>
export MINIMAX_API_KEY=<your-key> # Optional: for MiniMax models
```
See [LiteLLM providers](https://docs.litellm.ai/docs/providers) for all supported providers.

Expand Down
22 changes: 22 additions & 0 deletions config.yaml.template
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,28 @@
# models:
# anthropic/claude-sonnet-4-5-20250929: my-proxy

# ============================================================
# Example: Using MiniMax as LLM Provider
# ============================================================
#
# MiniMax models are supported via the minimax/ prefix.
# Set MINIMAX_API_KEY in your environment, then use:
#
# hypothesiser_llm: "minimax/MiniMax-M2.7"
# planner_llm: "minimax/MiniMax-M2.7"
# model_definer_llm: "minimax/MiniMax-M2.5-highspeed"
#
# Available MiniMax models:
# minimax/MiniMax-M2.7 (1M context)
# minimax/MiniMax-M2.7-highspeed (1M context, faster)
# minimax/MiniMax-M2.5 (204K context)
# minimax/MiniMax-M2.5-highspeed (204K context, faster)
#
# Recommended: enable litellm_drop_params when using MiniMax
# to silently ignore any unsupported parameters:
#
# litellm_drop_params: true

# ============================================================
# Example: Minimal Config for Quick Testing
# ============================================================
Expand Down
55 changes: 40 additions & 15 deletions plexe/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,25 @@ class StandardMetric(str, Enum):
]


# ============================================
# MiniMax Provider Support
# ============================================

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

MINIMAX_MODELS = {
"MiniMax-M2.7": {"context_window": 1_000_000},
"MiniMax-M2.7-highspeed": {"context_window": 1_000_000},
"MiniMax-M2.5": {"context_window": 204_000},
"MiniMax-M2.5-highspeed": {"context_window": 204_000},
}

Comment on lines +178 to +184
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.

P2 MINIMAX_MODELS is dead code — never consumed by production code

MINIMAX_MODELS is defined and exported but is only referenced in the test test_minimax_models_constant, which just asserts the constant has certain keys. No production code path (routing, validation, context-window capping, etc.) reads from it. Either wire it into get_routing_for_model / PlexeLiteLLMModel to serve a real purpose (e.g. rejecting unknown model IDs or surfacing context-window metadata), or remove it to keep the codebase lean per the project's "prefer deleting code over adding code" principle.

Prompt To Fix With AI
This is a comment left during a code review.
Path: plexe/config.py
Line: 178-184

Comment:
**`MINIMAX_MODELS` is dead code — never consumed by production code**

`MINIMAX_MODELS` is defined and exported but is only referenced in the test `test_minimax_models_constant`, which just asserts the constant has certain keys. No production code path (routing, validation, context-window capping, etc.) reads from it. Either wire it into `get_routing_for_model` / `PlexeLiteLLMModel` to serve a real purpose (e.g. rejecting unknown model IDs or surfacing context-window metadata), or remove it to keep the codebase lean per the project's "prefer deleting code over adding code" principle.

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


def _is_minimax_model(model_id: str) -> bool:
"""Check if a model ID uses the ``minimax/`` provider prefix."""
return model_id.startswith("minimax/")


# ============================================
# Configuration Helpers
# ============================================
Expand Down Expand Up @@ -512,8 +531,9 @@ def get_routing_for_model(config: RoutingConfig | None, model_id: str) -> tuple[

Lookup order:
1. Check if model_id is in 'models' mapping → use that provider's config
2. Else use 'default' config if present
3. Else return (None, {}) for LiteLLM's default routing
2. If model_id uses ``minimax/`` prefix → auto-route to MiniMax API
3. Else use 'default' config if present
4. Else return (None, {}) for LiteLLM's default routing

Args:
config: Routing configuration (or None if no config loaded)
Expand All @@ -524,29 +544,34 @@ def get_routing_for_model(config: RoutingConfig | None, model_id: str) -> tuple[
- api_base: Base URL for API requests (None = use LiteLLM default)
- headers: Dict of HTTP headers to include in requests
"""
# If no config provided, use LiteLLM defaults
if config is None:
return None, {}

# Check if model has explicit provider mapping
if model_id in config.models:
# Check if model has explicit provider mapping (highest priority)
if config is not None and model_id in config.models:
provider_name = config.models[model_id]

if provider_name not in config.providers:
# This should have been caught by validation, but handle gracefully
logging.getLogger(__name__).warning(
f"Model '{model_id}' references non-existent provider '{provider_name}'. Using default routing."
)
Comment on lines 551 to 554
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.

P2 Misleading warning message when an explicit mapping references a non-existent provider

When model_id is a minimax/ model AND its explicit provider mapping resolves to a non-existent provider, the warning says "Using default routing" but execution then falls through to the MiniMax auto-routing branch — not the config.default path. The log message will mislead anyone debugging the routing decision.

Suggested change
if provider_name not in config.providers:
# This should have been caught by validation, but handle gracefully
logging.getLogger(__name__).warning(
f"Model '{model_id}' references non-existent provider '{provider_name}'. Using default routing."
)
logging.getLogger(__name__).warning(
f"Model '{model_id}' references non-existent provider '{provider_name}'. "
"Falling through to next routing step."
)
Prompt To Fix With AI
This is a comment left during a code review.
Path: plexe/config.py
Line: 551-554

Comment:
**Misleading warning message when an explicit mapping references a non-existent provider**

When `model_id` is a `minimax/` model AND its explicit provider mapping resolves to a non-existent provider, the warning says **"Using default routing"** but execution then falls through to the MiniMax auto-routing branch — not the `config.default` path. The log message will mislead anyone debugging the routing decision.

```suggestion
            logging.getLogger(__name__).warning(
                f"Model '{model_id}' references non-existent provider '{provider_name}'. "
                "Falling through to next routing step."
            )
```

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

provider_config = config.default
else:
provider_config = config.providers[provider_name]
logging.getLogger(__name__).debug(f"Model '{model_id}' → provider '{provider_name}'")
else:
# No explicit mapping, use default
provider_config = config.default
logging.getLogger(__name__).debug(f"Model '{model_id}' → default routing")
return provider_config.api_base, provider_config.headers

# Auto-route minimax/ prefix to MiniMax API
if _is_minimax_model(model_id):
api_key = os.getenv("MINIMAX_API_KEY", "")
headers = {"Authorization": f"Bearer {api_key}"} if api_key else {}
logging.getLogger(__name__).debug(f"Model '{model_id}' → MiniMax auto-routing")
return MINIMAX_API_BASE, headers

# If no config provided, use LiteLLM defaults
if config is None:
return None, {}

# Use default routing config
provider_config = config.default
logging.getLogger(__name__).debug(f"Model '{model_id}' → default routing")

# If no applicable config found, use LiteLLM defaults
if provider_config is None:
return None, {}

Expand Down
9 changes: 9 additions & 0 deletions plexe/utils/litellm_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
from smolagents import LiteLLMModel
from tenacity import retry, stop_after_attempt, wait_exponential, wait_random, retry_if_exception_type

from plexe.config import _is_minimax_model

logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -70,6 +72,13 @@ def __init__(
on_llm_call: Callable[[str, Any, int], None] | None = None,
**kwargs,
):
# Rewrite minimax/ prefix to openai/ for LiteLLM compatibility
if _is_minimax_model(model_id):
model_id = "openai/" + model_id[len("minimax/"):]
# Clamp temperature to MiniMax's supported range [0, 1.0]
if "temperature" in kwargs:
kwargs["temperature"] = max(0.0, min(1.0, kwargs["temperature"]))

super().__init__(model_id=model_id, **kwargs)
self.extra_headers = extra_headers or {}
self.on_llm_call = on_llm_call
Expand Down
133 changes: 132 additions & 1 deletion tests/unit/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,16 @@
import pytest
import yaml

from plexe.config import Config, RoutingConfig, RoutingProviderConfig, get_routing_for_model, setup_logging
from plexe.config import (
Config,
MINIMAX_API_BASE,
MINIMAX_MODELS,
RoutingConfig,
RoutingProviderConfig,
_is_minimax_model,
get_routing_for_model,
setup_logging,
)


def test_get_routing_for_model_mapping_and_default():
Expand Down Expand Up @@ -83,3 +92,125 @@ def test_setup_logging_disables_propagation():
assert logger.name == "plexe"
assert logger.propagate is False
assert any(isinstance(h, logging.StreamHandler) for h in logger.handlers)


# ============================================
# MiniMax Provider Tests
# ============================================


def test_is_minimax_model():
"""minimax/ prefix is correctly detected."""
assert _is_minimax_model("minimax/MiniMax-M2.7") is True
assert _is_minimax_model("minimax/MiniMax-M2.5-highspeed") is True
assert _is_minimax_model("openai/gpt-4") is False
assert _is_minimax_model("anthropic/claude-sonnet-4-5-20250929") is False


def test_minimax_models_constant():
"""MINIMAX_MODELS should contain expected model entries."""
assert "MiniMax-M2.7" in MINIMAX_MODELS
assert "MiniMax-M2.7-highspeed" in MINIMAX_MODELS
assert "MiniMax-M2.5" in MINIMAX_MODELS
assert "MiniMax-M2.5-highspeed" in MINIMAX_MODELS
assert MINIMAX_MODELS["MiniMax-M2.7"]["context_window"] == 1_000_000


def test_minimax_auto_routing_no_config(monkeypatch):
"""minimax/ models auto-route even without routing_config."""
monkeypatch.setenv("MINIMAX_API_KEY", "test-key-123")

api_base, headers = get_routing_for_model(None, "minimax/MiniMax-M2.7")

assert api_base == MINIMAX_API_BASE
assert headers == {"Authorization": "Bearer test-key-123"}


def test_minimax_auto_routing_with_config(monkeypatch):
"""minimax/ models auto-route when not explicitly mapped in config."""
monkeypatch.setenv("MINIMAX_API_KEY", "test-key-456")
config = RoutingConfig(
default=RoutingProviderConfig(api_base="https://default", headers={"x": "1"}),
)

api_base, headers = get_routing_for_model(config, "minimax/MiniMax-M2.7")

assert api_base == MINIMAX_API_BASE
assert headers == {"Authorization": "Bearer test-key-456"}


def test_minimax_explicit_mapping_overrides_auto_routing(monkeypatch):
"""Explicit routing_config mapping takes priority over minimax auto-routing."""
monkeypatch.setenv("MINIMAX_API_KEY", "should-not-be-used")
config = RoutingConfig(
providers={
"my-proxy": RoutingProviderConfig(api_base="https://proxy.example.com/v1", headers={"auth": "proxy-key"}),
},
models={"minimax/MiniMax-M2.7": "my-proxy"},
)

api_base, headers = get_routing_for_model(config, "minimax/MiniMax-M2.7")

assert api_base == "https://proxy.example.com/v1"
assert headers == {"auth": "proxy-key"}


def test_minimax_auto_routing_without_api_key(monkeypatch):
"""minimax/ models auto-route without API key (headers empty)."""
monkeypatch.delenv("MINIMAX_API_KEY", raising=False)

api_base, headers = get_routing_for_model(None, "minimax/MiniMax-M2.5-highspeed")

assert api_base == MINIMAX_API_BASE
assert headers == {}


def test_minimax_llm_from_yaml(tmp_path, monkeypatch):
"""MiniMax model IDs can be loaded from YAML config."""
monkeypatch.delenv("HYPOTHESISER_LLM", raising=False)
monkeypatch.delenv("PLANNER_LLM", raising=False)

config_path = tmp_path / "config.yaml"
config_path.write_text(
yaml.safe_dump(
{
"hypothesiser_llm": "minimax/MiniMax-M2.7",
"planner_llm": "minimax/MiniMax-M2.5-highspeed",
}
)
)
monkeypatch.setenv("CONFIG_FILE", str(config_path))

config = Config()

assert config.hypothesiser_llm == "minimax/MiniMax-M2.7"
assert config.planner_llm == "minimax/MiniMax-M2.5-highspeed"


def test_minimax_llm_from_env(monkeypatch):
"""MiniMax model IDs can be set via environment variables."""
monkeypatch.setenv("HYPOTHESISER_LLM", "minimax/MiniMax-M2.7")

config = Config()

assert config.hypothesiser_llm == "minimax/MiniMax-M2.7"


def test_non_minimax_routing_unchanged():
"""Non-minimax models still use default routing behavior."""
config = RoutingConfig(
default=RoutingProviderConfig(api_base="https://default", headers={"x": "1"}),
)

api_base, headers = get_routing_for_model(config, "openai/gpt-4")

assert api_base == "https://default"
assert headers == {"x": "1"}


def test_non_minimax_no_config_returns_none():
"""Non-minimax models without config return (None, {})."""
api_base, headers = get_routing_for_model(None, "openai/gpt-4")

assert api_base is None
assert headers == {}
Loading