Skip to content
Closed
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
25 changes: 21 additions & 4 deletions astrbot/core/provider/sources/minimax_token_plan_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@

from ..register import register_provider_adapter

# Current MiniMax Token Plan model IDs. Returned as a fallback when the
# dynamic /v1/models fetch is unavailable (no API key configured yet or a
# transient network error) so the model picker can still surface the current
# models. The dynamic list remains authoritative whenever it succeeds.
_MINIMAX_TOKEN_PLAN_FALLBACK_MODELS = ["MiniMax-M3", "MiniMax-M2.7"]

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.

suggestion: Consider freezing the fallback model IDs to avoid accidental runtime mutation.

Defining this as a tuple (or another immutable sequence) would better signal that it’s a constant and remove the need to return .copy() elsewhere, e.g.:

_MINIMAX_TOKEN_PLAN_FALLBACK_MODELS = ("MiniMax-M3", "MiniMax-M2.7")

Callers can then construct a list if needed: return list(_MINIMAX_TOKEN_PLAN_FALLBACK_MODELS) (or use the tuple directly if a generic sequence is sufficient).

Suggested implementation:

# Current MiniMax Token Plan model IDs. Returned as a fallback when the
# dynamic /v1/models fetch is unavailable (no API key configured yet or a
# transient network error) so the model picker can still surface the current
# models. The dynamic list remains authoritative whenever it succeeds.
_MINIMAX_TOKEN_PLAN_FALLBACK_MODELS = ("MiniMax-M3", "MiniMax-M2.7")

Search for all usages of _MINIMAX_TOKEN_PLAN_FALLBACK_MODELS in the codebase and:

  1. If any code currently calls _MINIMAX_TOKEN_PLAN_FALLBACK_MODELS.copy(), replace it with list(_MINIMAX_TOKEN_PLAN_FALLBACK_MODELS).
  2. If callers mutate the sequence (e.g. .append, .extend, item assignment), change them to operate on a list copy instead, for example:
    • Before: models = _MINIMAX_TOKEN_PLAN_FALLBACK_MODELS.copy(); models.append("...")
    • After: models = list(_MINIMAX_TOKEN_PLAN_FALLBACK_MODELS); models.append("...")
  3. If callers only iterate or treat it as a generic sequence, no change is needed; they can use the tuple directly.



@register_provider_adapter(
"minimax_token_plan",
Expand All @@ -16,6 +22,8 @@ class ProviderMiniMaxTokenPlan(ProviderAnthropic):
The model list is fetched dynamically from the MiniMax API's /v1/models
endpoint, so newly released models are automatically discovered without
a code change. The default model is MiniMax-M3, the current flagship.
When the dynamic fetch is unavailable, a small fallback list of current
model IDs is returned so the dashboard is never empty.
"""

def __init__(
Expand All @@ -41,11 +49,16 @@ def __init__(
self.set_model(configured_model)

async def get_models(self) -> list[str]:
"""Dynamically fetch available models from the MiniMax API."""
"""Fetch available models from the MiniMax API.

Returns the current fallback model IDs when the dynamic fetch is
unavailable (no API key or a network/API error) so the model picker
is never empty. The dynamic list is authoritative when it succeeds.
"""
key = self.chosen_api_key
if not key:
logger.warning("No API key configured for MiniMax Token Plan.")
return []
return _MINIMAX_TOKEN_PLAN_FALLBACK_MODELS.copy()
try:
async with httpx.AsyncClient() as client:
resp = await client.get(
Comment on lines 62 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.

suggestion (bug_risk): Lack of explicit timeout on the HTTP client can lead to hanging calls.

Using httpx.AsyncClient() without an explicit timeout risks tasks hanging if the MiniMax endpoint is slow or unresponsive. Please set a sensible timeout (or reuse a shared client with a central timeout config) so these calls can fail fast instead of blocking indefinitely.

Suggested change
try:
async with httpx.AsyncClient() as client:
resp = await client.get(
try:
# Explicit timeout to avoid hanging calls if MiniMax endpoint is slow/unresponsive.
# Adjust the timeout value if you need stricter or more lenient behavior.
async with httpx.AsyncClient(timeout=10.0) as client:
resp = await client.get(
)
resp.raise_for_status()
data = resp.json()

Expand All @@ -55,7 +68,11 @@ async def get_models(self) -> list[str]:
)
resp.raise_for_status()
data = resp.json()
return [m["id"] for m in data.get("data", [])]
models = [m["id"] for m in data.get("data", [])]
if models:
return models
logger.warning("MiniMax /v1/models returned an empty list.")
return _MINIMAX_TOKEN_PLAN_FALLBACK_MODELS.copy()
except Exception as e:
logger.error(f"Failed to fetch MiniMax model list: {e}")
return []
return _MINIMAX_TOKEN_PLAN_FALLBACK_MODELS.copy()
126 changes: 126 additions & 0 deletions tests/test_minimax_token_plan_source.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
from typing import Any
from unittest.mock import patch

import httpx
import pytest

import astrbot.core.provider.sources.anthropic_source as anthropic_source
from astrbot.core.provider.sources.minimax_token_plan_source import (
ProviderMiniMaxTokenPlan,
)


class _FakeAsyncAnthropic:
"""Minimal AsyncAnthropic stand-in for provider construction in tests."""

def __init__(self, **kwargs: Any) -> None:
self.kwargs = kwargs

async def close(self) -> None:
return None


def _make_provider(monkeypatch, key=None):
monkeypatch.setattr(anthropic_source, "AsyncAnthropic", _FakeAsyncAnthropic)
provider_config: dict[str, Any] = {
"id": "minimax-token-plan-test",
"type": "minimax_token_plan",
}
if key is not None:
provider_config["key"] = key
return ProviderMiniMaxTokenPlan(
provider_config=provider_config, provider_settings={}
)


@pytest.mark.asyncio
async def test_get_models_returns_fallback_when_no_api_key(monkeypatch):
provider = _make_provider(monkeypatch, key=[""])
models = await provider.get_models()
assert models == ["MiniMax-M3", "MiniMax-M2.7"]


@pytest.mark.asyncio
async def test_get_models_returns_dynamic_list_when_available(monkeypatch):
provider = _make_provider(monkeypatch, key=["test-key"])
payload = {
"data": [{"id": "MiniMax-M3"}, {"id": "MiniMax-M2.7"}, {"id": "MiniMax-M2"}]
}

class _FakeResponse:
def raise_for_status(self) -> None:
return None

def json(self) -> dict:
return payload

class _FakeClient:
async def __aenter__(self):
return self

async def __aexit__(self, exc_type, exc, tb):
return None

async def get(self, url, headers=None, timeout=None):
return _FakeResponse()

with patch(
"astrbot.core.provider.sources.minimax_token_plan_source.httpx.AsyncClient",
return_value=_FakeClient(),
):
models = await provider.get_models()

assert models == ["MiniMax-M3", "MiniMax-M2.7", "MiniMax-M2"]


@pytest.mark.asyncio
async def test_get_models_returns_fallback_on_empty_dynamic_list(monkeypatch):
provider = _make_provider(monkeypatch, key=["test-key"])

class _FakeResponse:
def raise_for_status(self) -> None:
return None

def json(self) -> dict:
return {"data": []}

class _FakeClient:
async def __aenter__(self):
return self

async def __aexit__(self, exc_type, exc, tb):
return None

async def get(self, url, headers=None, timeout=None):
return _FakeResponse()

with patch(
"astrbot.core.provider.sources.minimax_token_plan_source.httpx.AsyncClient",
return_value=_FakeClient(),
):
models = await provider.get_models()

assert models == ["MiniMax-M3", "MiniMax-M2.7"]


@pytest.mark.asyncio
async def test_get_models_returns_fallback_on_fetch_error(monkeypatch):
provider = _make_provider(monkeypatch, key=["test-key"])

class _FakeClient:
async def __aenter__(self):
return self

async def __aexit__(self, exc_type, exc, tb):
return None

async def get(self, url, headers=None, timeout=None):
raise httpx.ConnectError("connection refused")

with patch(
"astrbot.core.provider.sources.minimax_token_plan_source.httpx.AsyncClient",
return_value=_FakeClient(),
):
models = await provider.get_models()

assert models == ["MiniMax-M3", "MiniMax-M2.7"]
Loading