-
-
Notifications
You must be signed in to change notification settings - Fork 107
feat: add MiniMax model and endpoint support #166
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
base: main
Are you sure you want to change the base?
Changes from 1 commit
c951f13
f39da68
a2f5faf
36962f6
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 |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| # SPDX-License-Identifier: LGPL-3.0-only | ||
| # Copyright (c) 2026 Mirrowel | ||
|
|
||
| """MiniMax endpoint and model configuration.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import os | ||
| from typing import Any, Dict, Optional | ||
|
|
||
|
|
||
| GLOBAL_EN = "global_en" | ||
| CN_ZH = "cn_zh" | ||
| OPENAI_PROTOCOL = "openai" | ||
| ANTHROPIC_PROTOCOL = "anthropic" | ||
|
|
||
| MINIMAX_ENDPOINTS: Dict[str, Dict[str, str]] = { | ||
| GLOBAL_EN: { | ||
| OPENAI_PROTOCOL: "https://api.minimax.io/v1", | ||
| ANTHROPIC_PROTOCOL: "https://api.minimax.io/anthropic", | ||
| }, | ||
| CN_ZH: { | ||
| OPENAI_PROTOCOL: "https://api.minimaxi.com/v1", | ||
| ANTHROPIC_PROTOCOL: "https://api.minimaxi.com/anthropic", | ||
| }, | ||
| } | ||
|
|
||
| MINIMAX_MODEL_DEFINITIONS: Dict[str, Dict[str, Any]] = { | ||
| "MiniMax-M3": { | ||
| "context_window": 1_000_000, | ||
| "pricing_usd_per_million_tokens": { | ||
| "input": 0.3, | ||
| "output": 1.2, | ||
| "cache_read": 0.06, | ||
| "cache_write": None, | ||
| }, | ||
| "input_modalities": ["text", "image", "video"], | ||
| "thinking": ["adaptive", "disabled"], | ||
| }, | ||
| "MiniMax-M2.7": { | ||
| "context_window": 204_800, | ||
| "pricing_usd_per_million_tokens": { | ||
| "input": 0.3, | ||
| "output": 1.2, | ||
| "cache_read": 0.06, | ||
| "cache_write": 0.375, | ||
| }, | ||
| "input_modalities": ["text"], | ||
| "thinking": ["always_on"], | ||
| }, | ||
| } | ||
|
|
||
| MINIMAX_DEFAULT_MODELS = tuple(MINIMAX_MODEL_DEFINITIONS) | ||
|
|
||
| _REGION_ENV_VARS = { | ||
| GLOBAL_EN: { | ||
| OPENAI_PROTOCOL: "MINIMAX_GLOBAL_OPENAI_BASE_URL", | ||
| ANTHROPIC_PROTOCOL: "MINIMAX_GLOBAL_ANTHROPIC_BASE_URL", | ||
| }, | ||
| CN_ZH: { | ||
| OPENAI_PROTOCOL: "MINIMAX_CN_OPENAI_BASE_URL", | ||
| ANTHROPIC_PROTOCOL: "MINIMAX_CN_ANTHROPIC_BASE_URL", | ||
| }, | ||
| } | ||
|
|
||
|
|
||
| def get_minimax_region() -> str: | ||
| """Return the configured endpoint region, defaulting to the global service.""" | ||
| region = os.getenv("MINIMAX_API_REGION", GLOBAL_EN).strip().lower() | ||
| return region if region in MINIMAX_ENDPOINTS else GLOBAL_EN | ||
|
|
||
|
|
||
| def get_minimax_protocol() -> str: | ||
| """Return the configured upstream protocol.""" | ||
| protocol = os.getenv("MINIMAX_API_PROTOCOL", OPENAI_PROTOCOL).strip().lower() | ||
| return ( | ||
| protocol | ||
| if protocol in (OPENAI_PROTOCOL, ANTHROPIC_PROTOCOL) | ||
| else OPENAI_PROTOCOL | ||
| ) | ||
|
|
||
|
|
||
| def get_minimax_endpoint( | ||
| region: Optional[str] = None, | ||
| protocol: Optional[str] = None, | ||
| ) -> str: | ||
| """Resolve a user-configured or default MiniMax endpoint.""" | ||
| selected_region = region or get_minimax_region() | ||
| selected_protocol = protocol or get_minimax_protocol() | ||
|
|
||
| if selected_region not in MINIMAX_ENDPOINTS: | ||
| selected_region = GLOBAL_EN | ||
| if selected_protocol not in (OPENAI_PROTOCOL, ANTHROPIC_PROTOCOL): | ||
| selected_protocol = OPENAI_PROTOCOL | ||
|
|
||
| env_var = _REGION_ENV_VARS[selected_region][selected_protocol] | ||
| override = os.getenv(env_var, "").strip() | ||
| if not override: | ||
| if selected_protocol == OPENAI_PROTOCOL: | ||
| override = os.getenv("MINIMAX_API_BASE", "").strip() | ||
| else: | ||
| override = os.getenv("MINIMAX_ANTHROPIC_BASE_URL", "").strip() | ||
|
|
||
| endpoint = override or MINIMAX_ENDPOINTS[selected_region][selected_protocol] | ||
| endpoint = endpoint.rstrip("/") | ||
| if selected_protocol == ANTHROPIC_PROTOCOL and not endpoint.endswith("/anthropic"): | ||
| raise ValueError("MiniMax Anthropic base URL must end with /anthropic") | ||
|
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. This Consider validating at config/startup time, or having the provider log a |
||
| return endpoint | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,8 @@ | |
| from urllib.request import Request, urlopen | ||
| from urllib.error import URLError | ||
|
|
||
| from .minimax_config import MINIMAX_MODEL_DEFINITIONS | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
|
|
@@ -45,6 +47,7 @@ | |
| "meta-llama", | ||
| "nvidia", | ||
| "moonshotai", # Used in nvidia_nim/moonshotai/model format | ||
| "minimax", | ||
| # These are aggregators/proxies - lower priority | ||
| "openrouter", | ||
| "azure", | ||
|
|
@@ -86,6 +89,46 @@ | |
| } | ||
|
|
||
|
|
||
| def _build_minimax_model_catalog() -> Dict[str, Dict[str, Any]]: | ||
| """Build native metadata records for the supported MiniMax models.""" | ||
| catalog = {} | ||
| for model_id, definition in MINIMAX_MODEL_DEFINITIONS.items(): | ||
| pricing = definition["pricing_usd_per_million_tokens"] | ||
| input_types = definition["input_modalities"] | ||
| catalog[f"minimax/{model_id}"] = { | ||
| "name": model_id, | ||
| "original_id": model_id, | ||
| "provider": "minimax", | ||
| "source": "native", | ||
| "category": "chat", | ||
| "prompt_cost": pricing["input"] / 1_000_000, | ||
| "completion_cost": pricing["output"] / 1_000_000, | ||
| "cache_read_cost": pricing["cache_read"] / 1_000_000, | ||
| "cache_write_cost": ( | ||
| pricing["cache_write"] / 1_000_000 | ||
| if pricing["cache_write"] is not None | ||
| else None | ||
| ), | ||
| "context": definition["context_window"], | ||
| "max_out": 0, | ||
| "inputs": input_types, | ||
| "outputs": ["text"], | ||
| "has_tools": False, | ||
| "has_functions": False, | ||
|
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.
|
||
| "has_reasoning": bool(definition["thinking"]), | ||
| "has_vision": "image" in input_types, | ||
| "has_structured_output": False, | ||
| "has_temperature": True, | ||
| "has_attachments": "video" in input_types, | ||
| "has_interleaved": False, | ||
| "supported_parameters": ["thinking"], | ||
| } | ||
| return catalog | ||
|
|
||
|
|
||
| MINIMAX_MODEL_CATALOG = _build_minimax_model_catalog() | ||
|
|
||
|
|
||
| def _get_provider_priority(provider: str) -> int: | ||
| """ | ||
| Get priority score for a provider (lower = better). | ||
|
|
@@ -922,6 +965,7 @@ def __init__( | |
| # Raw data stores | ||
| self._openrouter_store: Dict[str, Dict] = {} | ||
| self._modelsdev_store: Dict[str, Dict] = {} | ||
| self._native_store: Dict[str, Dict] = MINIMAX_MODEL_CATALOG.copy() | ||
|
|
||
| # Lookup infrastructure | ||
| self._index = ModelIndex() | ||
|
|
@@ -1022,6 +1066,9 @@ def _rebuild_index(self): | |
| for model_id in self._modelsdev_store: | ||
| self._index.add(model_id) | ||
|
|
||
| for model_id in self._native_store: | ||
| self._index.add(model_id) | ||
|
|
||
| # ---------- Query API ---------- | ||
|
|
||
| def lookup(self, model_id: str) -> Optional[ModelMetadata]: | ||
|
|
@@ -1064,6 +1111,13 @@ def _resolve_model(self, model_id: str) -> Optional[ModelMetadata]: | |
| ) | ||
| quality = "exact" | ||
|
|
||
| if model_id in self._native_store: | ||
| records.insert( | ||
| 0, | ||
| (self._native_store[model_id], f"native:exact:{model_id}"), | ||
| ) | ||
|
greptile-apps[bot] marked this conversation as resolved.
|
||
| quality = "exact" | ||
|
|
||
| # Step 2: Try provider alias substitution for direct match | ||
| # This handles cases like nvidia_nim/org/model -> nvidia/org/model | ||
| if not records: | ||
|
|
@@ -1215,6 +1269,7 @@ def all_raw_models(self) -> Dict[str, Dict]: | |
| combined = {} | ||
| combined.update(self._openrouter_store) | ||
| combined.update(self._modelsdev_store) | ||
| combined.update(self._native_store) | ||
| return combined | ||
|
|
||
| def diagnostics(self) -> Dict[str, Any]: | ||
|
|
@@ -1224,6 +1279,7 @@ def diagnostics(self) -> Dict[str, Any]: | |
| "last_refresh": self._last_refresh, | ||
| "openrouter_count": len(self._openrouter_store), | ||
| "modelsdev_count": len(self._modelsdev_store), | ||
| "native_count": len(self._native_store), | ||
| "cached_lookups": len(self._result_cache), | ||
| "index_entries": self._index.entry_count(), | ||
| "refresh_interval": self._refresh_interval, | ||
|
|
||
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.
MINIMAX_ANTHROPIC_BASE_URLis read here but isn't documented anywhere (.env.example,README.md,DOCUMENTATION.md, or theprovider_config.pyextra_varslist), unlike its OpenAI counterpartMINIMAX_API_BASE. The naming is also asymmetric (MINIMAX_API_BASEvsMINIMAX_ANTHROPIC_BASE_URL).Either document both fallback vars consistently, or drop them in favor of the four region/protocol-specific vars that are already documented.