diff --git a/README.md b/README.md index ac00fe301..e600c9d5a 100644 --- a/README.md +++ b/README.md @@ -366,8 +366,8 @@ The configuration file is saved in `~/.clawcodex/config.json`. Example structure }, "minimax": { "api_key": "your-api-key", - "base_url": "https://api.minimaxi.com/anthropic", - "default_model": "MiniMax-M2.7" + "base_url": "https://api.minimax.io/anthropic", + "default_model": "MiniMax-M3" }, "openrouter": { "api_key": "your-api-key", @@ -396,6 +396,14 @@ The configuration file is saved in `~/.clawcodex/config.json`. Example structure } ``` +The built-in Minimax provider passes an SDK base URL to the Anthropic SDK. Use +`https://api.minimax.io/anthropic` globally or +`https://api.minimaxi.com/anthropic` in China; the SDK appends `/v1/messages`. +The final Messages request URLs are +`https://api.minimax.io/anthropic/v1/messages` and +`https://api.minimaxi.com/anthropic/v1/messages`. The OpenAI-compatible API roots are +`https://api.minimax.io/v1` globally and `https://api.minimaxi.com/v1` in China. + - **`session`** — REPL session persistence: `auto_save` writes each session automatically; `max_history` caps retained turns. - **`settings`** — the advisor (reviewer) feature. **`advisor_enabled` is the master switch — `false` by default, so the advisor is OFF unless you opt in** (set it to `true`, or run `/advisor :` which flips it on). `advisor_provider` / `advisor_model` pick the reviewer model; `advisor_client_mode` routes the call via the client. - **`env`** — secrets and environment values injected at startup (e.g. `TAVILY_API_KEY` for web search). Managed via `clawcodex config`; keys here are exported into the process environment without overriding anything you already set in your shell. @@ -660,4 +668,4 @@ If you find this useful, please **star** ⭐ the repo! *** -*** \ No newline at end of file +*** diff --git a/docs/i18n/README_ZH.md b/docs/i18n/README_ZH.md index 6e9295291..e373c7329 100644 --- a/docs/i18n/README_ZH.md +++ b/docs/i18n/README_ZH.md @@ -326,8 +326,8 @@ clawcodex login }, "minimax": { "api_key": "your-api-key", - "base_url": "https://api.minimaxi.com/anthropic", - "default_model": "MiniMax-M2.7" + "base_url": "https://api.minimax.io/anthropic", + "default_model": "MiniMax-M3" }, "openrouter": { "api_key": "your-api-key", @@ -355,6 +355,13 @@ clawcodex login } ``` +内置 Minimax provider 会把 SDK base URL 传给 Anthropic SDK:全球区域使用 +`https://api.minimax.io/anthropic`,中国区域使用 +`https://api.minimaxi.com/anthropic`,SDK 会自动追加 `/v1/messages`。最终的 +Messages 请求 URL 分别为 `https://api.minimax.io/anthropic/v1/messages` 和 +`https://api.minimaxi.com/anthropic/v1/messages`。OpenAI 兼容 API root 分别为全球区域的 +`https://api.minimax.io/v1` 和中国区域的 `https://api.minimaxi.com/v1`。 + - **`session`** —— REPL 会话持久化:`auto_save` 自动保存每个会话;`max_history` 限制保留的对话轮数。 - **`settings`** —— 后台辅助功能所用的 advisor 模型(`advisor_provider` / `advisor_model`,以及控制是否经由客户端路由的 `advisor_client_mode`)。 - **`env`** —— 启动时注入的密钥与环境变量(例如用于 Web 搜索的 `TAVILY_API_KEY`)。通过 `clawcodex config` 管理;这里的键会被导出到进程环境,但不会覆盖你在 shell 中已设置的值。 diff --git a/src/context_system/context_analyzer.py b/src/context_system/context_analyzer.py index 9df690fa1..47fe52d85 100644 --- a/src/context_system/context_analyzer.py +++ b/src/context_system/context_analyzer.py @@ -36,7 +36,9 @@ # DeepSeek V4 ships a 1M context window, like glm-5.2 (legacy deepseek-chat/ # -reasoner are intentionally NOT matched here — see src/models/configs.py). "deepseek-v4": 1_000_000, - # Minimax defaults + # MiniMax model-specific windows must precede the family fallback. + "minimax-m3": 1_000_000, + "minimax-m2.7": 204_800, "minimax": 128_000, "abab": 128_000, } diff --git a/src/models/configs.py b/src/models/configs.py index 04e67352f..3eb47dd6b 100644 --- a/src/models/configs.py +++ b/src/models/configs.py @@ -187,6 +187,21 @@ class ModelConfig: max_output_tokens=8_192, supports_cache=True, ), + # MiniMax pricing is maintained in services/pricing.py. + "MiniMax-M2.7": ModelConfig( + model_id="MiniMax-M2.7", + display_name="MiniMax M2.7", + context_window=204_800, + max_output_tokens=8_192, + supports_cache=True, + ), + "MiniMax-M3": ModelConfig( + model_id="MiniMax-M3", + display_name="MiniMax M3", + context_window=1_000_000, + max_output_tokens=8_192, + supports_cache=True, + ), # Meta Muse Spark 1.1 (api.meta.ai, OpenAI-compatible). Muse Spark is a # server-side reasoning model (usage reports ``reasoning_tokens``); like # DeepSeek/GLM it exposes no Anthropic-style thinking blocks (the diff --git a/src/providers/__init__.py b/src/providers/__init__.py index 4dde5bd87..385caa61a 100644 --- a/src/providers/__init__.py +++ b/src/providers/__init__.py @@ -81,10 +81,10 @@ class ProviderInfo(TypedDict): }, "minimax": { "label": "Minimax AI", - "default_base_url": "https://api.minimaxi.com/anthropic", - "default_model": "MiniMax-M2.7", + "default_base_url": "https://api.minimax.io/anthropic", + "default_model": "MiniMax-M3", "available_models": [ - # M2 series (latest) + "MiniMax-M3", "MiniMax-M2.7", "MiniMax-M2.7-highspeed", "MiniMax-M2.5", diff --git a/src/providers/minimax_provider.py b/src/providers/minimax_provider.py index 03e7d477b..37a69d987 100644 --- a/src/providers/minimax_provider.py +++ b/src/providers/minimax_provider.py @@ -22,11 +22,11 @@ def __init__(self, *args, **kwargs): class MinimaxProvider(BaseProvider): """Minimax AI provider using Anthropic-compatible API. - Minimax provides an Anthropic-compatible endpoint at api.minimaxi.com/anthropic. + Minimax provides an Anthropic-compatible endpoint at api.minimax.io/anthropic. Uses the Anthropic SDK with Minimax-specific models. """ - DEFAULT_BASE_URL = "https://api.minimaxi.com/anthropic" + DEFAULT_BASE_URL = "https://api.minimax.io/anthropic" def __init__( self, api_key: str, base_url: Optional[str] = None, model: Optional[str] = None @@ -36,10 +36,10 @@ def __init__( Args: api_key: Minimax API key base_url: Base URL (optional, defaults to Minimax Anthropic-compatible endpoint) - model: Default model (default: MiniMax-M2.7) + model: Default model (default: MiniMax-M3) """ resolved_base_url = base_url or self.DEFAULT_BASE_URL - super().__init__(api_key, resolved_base_url, model or "MiniMax-M2.7") + super().__init__(api_key, resolved_base_url, model or "MiniMax-M3") self._client_kwargs: dict[str, Any] = {"api_key": api_key} if resolved_base_url: @@ -239,7 +239,7 @@ def get_available_models(self) -> list[str]: List of model names """ return [ - # M2 series (latest) + "MiniMax-M3", "MiniMax-M2.7", "MiniMax-M2.7-highspeed", "MiniMax-M2.5", diff --git a/src/services/pricing.py b/src/services/pricing.py index 90c1187a5..607c187d6 100644 --- a/src/services/pricing.py +++ b/src/services/pricing.py @@ -78,6 +78,20 @@ "cache_creation": 0.435 / 1_000_000, "cache_read": 0.003625 / 1_000_000, } +# MiniMax standard pay-as-you-go rates in USD per million tokens. The static +# pricing table uses M3's <=512k input tier; longer requests have a higher tier. +_TIER_MINIMAX_M3 = { + "input": 0.30 / 1_000_000, + "output": 1.20 / 1_000_000, + "cache_creation": 0.30 / 1_000_000, + "cache_read": 0.06 / 1_000_000, +} +_TIER_MINIMAX_M27 = { + "input": 0.30 / 1_000_000, + "output": 1.20 / 1_000_000, + "cache_creation": 0.375 / 1_000_000, + "cache_read": 0.06 / 1_000_000, +} # Meta Muse Spark 1.1 (api.meta.ai, OpenAI-compatible). Meta's published rates: # $1.25/M input, $4.25/M output, $0.15/M cached input. OpenAI-style caching has # no separate cache-write charge, so ``cache_creation`` mirrors ``input``. @@ -124,6 +138,8 @@ # every proxied model is priced at its upstream rate. "deepseek-v4-flash": _TIER_DEEPSEEK_FLASH, "deepseek-v4-pro": _TIER_DEEPSEEK_PRO, + "MiniMax-M3": _TIER_MINIMAX_M3, + "MiniMax-M2.7": _TIER_MINIMAX_M27, # Meta Muse Spark (api.meta.ai) "muse-spark-1.1": _TIER_MUSE_SPARK, } diff --git a/tests/test_context_window_1m.py b/tests/test_context_window_1m.py index 34690b6bd..c4ee74e32 100644 --- a/tests/test_context_window_1m.py +++ b/tests/test_context_window_1m.py @@ -27,6 +27,13 @@ def test_glm_4_legacy_not_promoted_to_1m(): assert canonical_window("glm-4") == 128_000 +def test_minimax_current_context_windows(): + assert display_window("MiniMax-M3") == 1_000_000 + assert canonical_window("MiniMax-M3") == 1_000_000 + assert display_window("MiniMax-M2.7") == 204_800 + assert canonical_window("MiniMax-M2.7") == 204_800 + + def test_legacy_deepseek_chat_not_promoted_to_1m(): # Only deepseek-v4* gets 1M; legacy deepseek-chat/-reasoner do not. assert display_window("deepseek-chat") != 1_000_000 diff --git a/tests/test_pricing_status_bar.py b/tests/test_pricing_status_bar.py index 232163464..15f6f0b65 100644 --- a/tests/test_pricing_status_bar.py +++ b/tests/test_pricing_status_bar.py @@ -35,6 +35,18 @@ def test_exact_match_sonnet_4_6(self) -> None: self.assertEqual(p["input"], 3.0 / 1_000_000) self.assertEqual(p["output"], 15.0 / 1_000_000) + def test_exact_match_minimax_models(self) -> None: + m3 = get_pricing("MiniMax-M3") + self.assertEqual(m3["input"], 0.30 / 1_000_000) + self.assertEqual(m3["output"], 1.20 / 1_000_000) + self.assertEqual(m3["cache_read"], 0.06 / 1_000_000) + + m27 = get_pricing("MiniMax-M2.7") + self.assertEqual(m27["input"], 0.30 / 1_000_000) + self.assertEqual(m27["output"], 1.20 / 1_000_000) + self.assertEqual(m27["cache_creation"], 0.375 / 1_000_000) + self.assertEqual(m27["cache_read"], 0.06 / 1_000_000) + def test_family_prefix_falls_back_for_future_opus_variant(self) -> None: # A model name not in the exact table but matching the # ``claude-opus-4-7`` family prefix → 5/25 tier. diff --git a/tests/test_provider_registry.py b/tests/test_provider_registry.py index 4dcf421f4..36e694823 100644 --- a/tests/test_provider_registry.py +++ b/tests/test_provider_registry.py @@ -20,6 +20,7 @@ resolve_api_key, ) from src.providers.base import ChatMessage +from src.providers.minimax_provider import MinimaxProvider from src.providers.openai_compatible import OpenAICompatibleProvider from src.providers.openai_compatible_specs import ( SPECS_BY_ID, @@ -86,6 +87,17 @@ def test_default_model_is_in_available_models(self): for spec in SPECS_BY_ID.values(): self.assertIn(spec.default_model, spec.available_models, spec.id) + def test_minimax_current_defaults_and_models(self): + info = PROVIDER_INFO["minimax"] + self.assertEqual(info["default_base_url"], "https://api.minimax.io/anthropic") + self.assertEqual(info["default_model"], "MiniMax-M3") + self.assertEqual(info["available_models"][:2], ["MiniMax-M3", "MiniMax-M2.7"]) + + provider = MinimaxProvider(api_key="k") + self.assertEqual(provider.base_url, "https://api.minimax.io/anthropic") + self.assertEqual(provider.model, "MiniMax-M3") + self.assertEqual(provider.get_available_models()[:2], ["MiniMax-M3", "MiniMax-M2.7"]) + def test_hand_written_providers_not_shadowed(self): # The registry must hold only the *new* providers — never override the # bespoke hand-written ones (deepseek cache logic, zai GLM aliasing …).