-
-
Notifications
You must be signed in to change notification settings - Fork 2.7k
feat(minimax): expose MiniMax-M2.7 in Token Plan model picker #9429
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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"] | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| @register_provider_adapter( | ||||||||||||||||||||||||
| "minimax_token_plan", | ||||||||||||||||||||||||
|
|
@@ -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__( | ||||||||||||||||||||||||
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
|
||||||||||||||||||||||||
|
|
@@ -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() | ||||||||||||||||||||||||
| 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"] |
There was a problem hiding this comment.
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.: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:
Search for all usages of
_MINIMAX_TOKEN_PLAN_FALLBACK_MODELSin the codebase and:_MINIMAX_TOKEN_PLAN_FALLBACK_MODELS.copy(), replace it withlist(_MINIMAX_TOKEN_PLAN_FALLBACK_MODELS)..append,.extend, item assignment), change them to operate on a list copy instead, for example:models = _MINIMAX_TOKEN_PLAN_FALLBACK_MODELS.copy(); models.append("...")models = list(_MINIMAX_TOKEN_PLAN_FALLBACK_MODELS); models.append("...")