From a0bf4f2e5c3cb2e4abb593b9158fc651acb37f63 Mon Sep 17 00:00:00 2001 From: PR Bot Date: Tue, 17 Mar 2026 13:21:29 +0800 Subject: [PATCH 1/4] feat: add MiniMax as first-class LLM provider for data cleaning - Add LLMProvider enum and preset system (OpenAI, DeepSeek, MiniMax, Custom) that auto-fills base_url and model_name in MakeDatasetArgs - Auto-detect MiniMax endpoints and disable response_format (unsupported) - Clamp temperature to 0.01 for MiniMax (rejects zero) - Support both api.minimax.io and api.minimaxi.com (China) endpoints - Update config templates with llm_provider documentation - Add 25 unit tests + 4 integration tests Co-Authored-By: Octopus --- README.md | 25 ++ README_zh.md | 25 ++ examples/mllm.template.jsonc | 5 +- examples/tg.template.jsonc | 6 +- settings.template.jsonc | 6 +- tests/test_minimax_provider.py | 362 +++++++++++++++++++++++++ weclone/core/inference/online_infer.py | 26 +- weclone/utils/config_models.py | 42 +++ 8 files changed, 493 insertions(+), 4 deletions(-) create mode 100644 tests/test_minimax_provider.py diff --git a/README.md b/README.md index 7b378ad..866f8f8 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,31 @@ weclone-cli make-dataset ``` More Parameter Details: [Data Preprocessing](https://docs.weclone.love/docs/deploy/data_preprocessing.html#related-parameters) +#### Online LLM Providers for Data Cleaning + +When using `online_llm_clear`, you can set `llm_provider` to quickly configure a supported provider. The provider presets automatically populate `base_url` and `model_name` (which can still be overridden explicitly): + +| Provider | `llm_provider` | Default Model | API Docs | +|----------|----------------|---------------|----------| +| OpenAI | `"openai"` | `gpt-4o-mini` | [platform.openai.com](https://platform.openai.com/docs) | +| DeepSeek | `"deepseek"` | `deepseek-chat` | [platform.deepseek.com](https://platform.deepseek.com/docs) | +| MiniMax | `"minimax"` | `MiniMax-M2.5` | [platform.minimax.io](https://platform.minimax.io/docs/api-reference/text-openai-api) | +| Custom | `"custom"` | *(manual)* | Any OpenAI-compatible API | + +Example using MiniMax for data cleaning: +```jsonc +"make_dataset_args": { + "online_llm_clear": true, + "llm_provider": "minimax", + "llm_api_key": "your-minimax-api-key", + // base_url and model_name are auto-filled: + // base_url → https://api.minimax.io/v1 + // model_name → MiniMax-M2.5 +} +``` + +> You can also use the China endpoint by explicitly setting `"base_url": "https://api.minimaxi.com/v1"`. + ## Configure Parameters and Fine-tune Model - (Optional) Modify `model_name_or_path`, `template`, `lora_target` in `settings.jsonc` to select other locally downloaded models. diff --git a/README_zh.md b/README_zh.md index e15e79a..fc50718 100644 --- a/README_zh.md +++ b/README_zh.md @@ -134,6 +134,31 @@ weclone-cli make-dataset ``` 数据处理更多参数说明:[数据预处理](https://docs.weclone.love/zh/docs/deploy/data_preprocessing.html#%E7%9B%B8%E5%85%B3%E5%8F%82%E6%95%B0) +#### 在线 LLM 供应商(数据清洗) + +使用 `online_llm_clear` 时,可通过 `llm_provider` 快速配置供应商,自动填充 `base_url` 和 `model_name`(仍可手动覆盖): + +| 供应商 | `llm_provider` | 默认模型 | API 文档 | +|--------|----------------|----------|----------| +| OpenAI | `"openai"` | `gpt-4o-mini` | [platform.openai.com](https://platform.openai.com/docs) | +| DeepSeek | `"deepseek"` | `deepseek-chat` | [platform.deepseek.com](https://platform.deepseek.com/docs) | +| MiniMax | `"minimax"` | `MiniMax-M2.5` | [platform.minimax.io](https://platform.minimax.io/docs/api-reference/text-openai-api) | +| 自定义 | `"custom"` | *(手动填写)* | 任何 OpenAI 兼容 API | + +使用 MiniMax 进行数据清洗示例: +```jsonc +"make_dataset_args": { + "online_llm_clear": true, + "llm_provider": "minimax", + "llm_api_key": "your-minimax-api-key", + // base_url 和 model_name 自动填充: + // base_url → https://api.minimax.io/v1 + // model_name → MiniMax-M2.5 +} +``` + +> 国内用户可手动设置 `"base_url": "https://api.minimaxi.com/v1"`(注意多一个 i)。 + ## 配置参数并微调模型 - (可选)修改 `settings.jsonc` 的 `model_name_or_path` 、`template`、 `lora_target`选择本地下载好的其他模型。 diff --git a/examples/mllm.template.jsonc b/examples/mllm.template.jsonc index 6e65cb9..eca0778 100644 --- a/examples/mllm.template.jsonc +++ b/examples/mllm.template.jsonc @@ -41,9 +41,12 @@ } }, "online_llm_clear": false, + // llm_provider: 可选 "openai", "deepseek", "minimax", "custom" + // 设置后会自动填充 base_url 和 model_name(若未显式指定) + // "llm_provider": "minimax", "base_url": "https://xxx/v1", "llm_api_key": "xxxxx", - "model_name": "xxx", //建议使用参数较大的模型,例如DeepSeek-V3 + "model_name": "xxx", //建议使用参数较大的模型,例如DeepSeek-V3, MiniMax-M2.5 "clean_batch_size": 10, "vision_api": { "enable": false, // 设置为 true 来开启此功能 diff --git a/examples/tg.template.jsonc b/examples/tg.template.jsonc index c750774..a8a7377 100644 --- a/examples/tg.template.jsonc +++ b/examples/tg.template.jsonc @@ -46,9 +46,13 @@ } }, "online_llm_clear": false, + // llm_provider: "openai", "deepseek", "minimax", or "custom" + // When set, auto-fills base_url and model_name if not explicitly provided. + // e.g. "minimax" → base_url: https://api.minimax.io/v1, model_name: MiniMax-M2.5 + // "llm_provider": "minimax", "base_url": "https://xxx/v1", "llm_api_key": "xxxxx", - "model_name": "xxx", // Recommend using models with larger parameters, e.g. DeepSeek-V3 + "model_name": "xxx", // Recommend using models with larger parameters, e.g. DeepSeek-V3, MiniMax-M2.5 "clean_batch_size": 10, "vision_api": { "enable": false, // Set to true to enable this feature diff --git a/settings.template.jsonc b/settings.template.jsonc index 11907cb..43ec89d 100644 --- a/settings.template.jsonc +++ b/settings.template.jsonc @@ -46,9 +46,13 @@ } }, "online_llm_clear": false, + // llm_provider: 可选 "openai", "deepseek", "minimax", "custom" + // 设置后会自动填充 base_url 和 model_name(若未显式指定) + // 例如设为 "minimax" 时,base_url 默认 https://api.minimax.io/v1,model_name 默认 MiniMax-M2.5 + // "llm_provider": "minimax", "base_url": "https://xxx/v1", "llm_api_key": "xxxxx", - "model_name": "xxx", //建议使用参数较大的模型,例如DeepSeek-V3 + "model_name": "xxx", //建议使用参数较大的模型,例如DeepSeek-V3, MiniMax-M2.5 "clean_batch_size": 50, "vision_api": { "enable": false, // 设置为 true 来开启此功能 diff --git a/tests/test_minimax_provider.py b/tests/test_minimax_provider.py new file mode 100644 index 0000000..6f2a7f0 --- /dev/null +++ b/tests/test_minimax_provider.py @@ -0,0 +1,362 @@ +"""Tests for MiniMax LLM provider integration. + +Unit tests validate provider presets, temperature clamping, and response_format +handling. Integration tests (skipped without MINIMAX_API_KEY) verify actual +MiniMax API calls. +""" + +import os +import re +import sys +from unittest.mock import MagicMock, patch + +import pytest + + +def _extract_json_from_text(text: str) -> str: + """Re-implementation of extract_json_from_text for test use. + + Strips ```` blocks and markdown code fences, then finds the + first JSON object so that the payload can be parsed by Pydantic in + integration tests. + """ + # Strip blocks (MiniMax M2.5 thinking output) + cleaned = re.sub(r".*?", "", text, flags=re.DOTALL).strip() + # Strip markdown code fences + m = re.search(r"```json\s*(.*?)\s*```", cleaned, re.DOTALL) + if m: + return m.group(1).strip() + if cleaned: + return cleaned + # Fallback: find first JSON object in original text (may be inside ) + m = re.search(r"\{[^{}]*\}", text) + if m: + return m.group(0) + return text.strip() + + +# Mock heavy dependencies (torch, llamafactory, vllm, pyjson5) that are not +# installed in the test environment. We only need OnlineLLM which lives in +# online_infer.py; its sole dependency on offline_infer is for +# extract_json_from_text which we provide a working re-implementation for. +_mock_offline = MagicMock() +_mock_offline.extract_json_from_text = _extract_json_from_text +sys.modules.setdefault("torch", MagicMock()) +sys.modules.setdefault("pyjson5", MagicMock()) +sys.modules.setdefault("vllm", MagicMock()) +sys.modules.setdefault("vllm.lora", MagicMock()) +sys.modules.setdefault("vllm.lora.request", MagicMock()) +sys.modules.setdefault("vllm.outputs", MagicMock()) +sys.modules.setdefault("vllm.sampling_params", MagicMock()) +sys.modules.setdefault("llamafactory", MagicMock()) +sys.modules.setdefault("llamafactory.data", MagicMock()) +sys.modules.setdefault("llamafactory.extras", MagicMock()) +sys.modules.setdefault("llamafactory.extras.misc", MagicMock()) +sys.modules.setdefault("llamafactory.hparams", MagicMock()) +sys.modules.setdefault("llamafactory.model", MagicMock()) +sys.modules.setdefault("weclone.core.inference.offline_infer", _mock_offline) + +from weclone.core.inference.online_infer import OnlineLLM +from weclone.utils.config_models import ( + LLM_PROVIDER_PRESETS, + LLMProvider, + MakeDatasetArgs, +) + + +# --------------------------------------------------------------------------- +# Unit Tests – Provider presets +# --------------------------------------------------------------------------- + +class TestLLMProviderPresets: + """Test provider preset configuration.""" + + def test_minimax_preset_values(self): + preset = LLM_PROVIDER_PRESETS[LLMProvider.MINIMAX] + assert preset["base_url"] == "https://api.minimax.io/v1" + assert preset["model_name"] == "MiniMax-M2.5" + + def test_openai_preset_values(self): + preset = LLM_PROVIDER_PRESETS[LLMProvider.OPENAI] + assert preset["base_url"] == "https://api.openai.com/v1" + assert preset["model_name"] == "gpt-4o-mini" + + def test_deepseek_preset_values(self): + preset = LLM_PROVIDER_PRESETS[LLMProvider.DEEPSEEK] + assert preset["base_url"] == "https://api.deepseek.com/v1" + assert preset["model_name"] == "deepseek-chat" + + def test_custom_provider_not_in_presets(self): + assert LLMProvider.CUSTOM not in LLM_PROVIDER_PRESETS + + +class TestMakeDatasetArgsProviderPresets: + """Test that MakeDatasetArgs applies provider presets correctly.""" + + _base_args = { + "platform": "chat", + "language": "zh", + } + + def test_minimax_provider_fills_base_url_and_model(self): + args = MakeDatasetArgs(llm_provider="minimax", **self._base_args) + assert args.base_url == "https://api.minimax.io/v1" + assert args.model_name == "MiniMax-M2.5" + + def test_explicit_base_url_overrides_preset(self): + args = MakeDatasetArgs( + llm_provider="minimax", + base_url="https://api.minimaxi.com/v1", + **self._base_args, + ) + assert args.base_url == "https://api.minimaxi.com/v1" + assert args.model_name == "MiniMax-M2.5" + + def test_explicit_model_name_overrides_preset(self): + args = MakeDatasetArgs( + llm_provider="minimax", + model_name="MiniMax-M2.5-highspeed", + **self._base_args, + ) + assert args.base_url == "https://api.minimax.io/v1" + assert args.model_name == "MiniMax-M2.5-highspeed" + + def test_no_provider_leaves_fields_none(self): + args = MakeDatasetArgs(**self._base_args) + assert args.base_url is None + assert args.model_name is None + assert args.llm_provider is None + + def test_custom_provider_does_not_fill_defaults(self): + args = MakeDatasetArgs( + llm_provider="custom", + base_url="https://my-server/v1", + model_name="my-model", + **self._base_args, + ) + assert args.base_url == "https://my-server/v1" + assert args.model_name == "my-model" + + +# --------------------------------------------------------------------------- +# Unit Tests – Temperature clamping +# --------------------------------------------------------------------------- + +class TestTemperatureClamping: + """Test OnlineLLM.clamp_temperature for MiniMax constraints.""" + + def test_zero_temperature_clamped_for_minimax(self): + result = OnlineLLM.clamp_temperature(0.0, "https://api.minimax.io/v1") + assert result == 0.01 + + def test_negative_temperature_clamped_for_minimax(self): + result = OnlineLLM.clamp_temperature(-1.0, "https://api.minimax.io/v1") + assert result == 0.01 + + def test_positive_temperature_unchanged_for_minimax(self): + result = OnlineLLM.clamp_temperature(0.7, "https://api.minimax.io/v1") + assert result == 0.7 + + def test_temperature_one_unchanged_for_minimax(self): + result = OnlineLLM.clamp_temperature(1.0, "https://api.minimax.io/v1") + assert result == 1.0 + + def test_zero_temperature_unchanged_for_openai(self): + result = OnlineLLM.clamp_temperature(0.0, "https://api.openai.com/v1") + assert result == 0.0 + + def test_china_endpoint_also_clamped(self): + result = OnlineLLM.clamp_temperature(0.0, "https://api.minimaxi.com/v1") + assert result == 0.01 + + +# --------------------------------------------------------------------------- +# Unit Tests – response_format handling +# --------------------------------------------------------------------------- + +class TestResponseFormatHandling: + """Test that response_format is disabled for MiniMax.""" + + @patch("weclone.core.inference.online_infer.OpenAI") + def test_minimax_disables_response_format(self, mock_openai_cls): + llm = OnlineLLM( + api_key="test-key", + base_url="https://api.minimax.io/v1", + model_name="MiniMax-M2.5", + ) + assert llm.response_format == "" + assert llm._supports_response_format is False + + @patch("weclone.core.inference.online_infer.OpenAI") + def test_openai_keeps_response_format(self, mock_openai_cls): + llm = OnlineLLM( + api_key="test-key", + base_url="https://api.openai.com/v1", + model_name="gpt-4o-mini", + ) + assert llm.response_format == "json_object" + assert llm._supports_response_format is True + + @patch("weclone.core.inference.online_infer.OpenAI") + def test_china_minimax_disables_response_format(self, mock_openai_cls): + llm = OnlineLLM( + api_key="test-key", + base_url="https://api.minimaxi.com/v1", + model_name="MiniMax-M2.5", + ) + assert llm.response_format == "" + + @patch("weclone.core.inference.online_infer.OpenAI") + def test_chat_omits_response_format_for_minimax(self, mock_openai_cls): + mock_client = MagicMock() + mock_openai_cls.return_value = mock_client + mock_response = MagicMock() + mock_client.chat.completions.create.return_value = mock_response + + llm = OnlineLLM( + api_key="test-key", + base_url="https://api.minimax.io/v1", + model_name="MiniMax-M2.5", + ) + llm.chat("Hello") + + call_kwargs = mock_client.chat.completions.create.call_args + # response_format should NOT be in the call params + assert "response_format" not in call_kwargs.kwargs and ( + not call_kwargs.args or "response_format" not in str(call_kwargs) + ) + + @patch("weclone.core.inference.online_infer.OpenAI") + def test_chat_includes_response_format_for_openai(self, mock_openai_cls): + mock_client = MagicMock() + mock_openai_cls.return_value = mock_client + mock_response = MagicMock() + mock_client.chat.completions.create.return_value = mock_response + + llm = OnlineLLM( + api_key="test-key", + base_url="https://api.openai.com/v1", + model_name="gpt-4o-mini", + ) + llm.chat("Hello") + + call_kwargs = mock_client.chat.completions.create.call_args[1] + assert "response_format" in call_kwargs + + +# --------------------------------------------------------------------------- +# Unit Tests – Provider detection +# --------------------------------------------------------------------------- + +class TestProviderDetection: + """Test _is_no_response_format_provider detection.""" + + def test_detects_minimax_io(self): + assert OnlineLLM._is_no_response_format_provider("https://api.minimax.io/v1") is True + + def test_detects_minimaxi_com(self): + assert OnlineLLM._is_no_response_format_provider("https://api.minimaxi.com/v1") is True + + def test_not_detected_for_openai(self): + assert OnlineLLM._is_no_response_format_provider("https://api.openai.com/v1") is False + + def test_not_detected_for_empty(self): + assert OnlineLLM._is_no_response_format_provider("") is False + + def test_case_insensitive(self): + assert OnlineLLM._is_no_response_format_provider("https://API.MINIMAX.IO/v1") is True + + +# --------------------------------------------------------------------------- +# Integration Tests – MiniMax API (skipped without API key) +# --------------------------------------------------------------------------- + +MINIMAX_API_KEY = os.environ.get("MINIMAX_API_KEY", "") + + +@pytest.mark.skipif(not MINIMAX_API_KEY, reason="MINIMAX_API_KEY not set") +class TestMiniMaxIntegration: + """Integration tests that call the real MiniMax API.""" + + def test_basic_chat_completion(self): + """Send a simple message and verify a response is returned.""" + llm = OnlineLLM( + api_key=MINIMAX_API_KEY, + base_url="https://api.minimax.io/v1", + model_name="MiniMax-M2.5", + response_format="", + ) + response = llm.chat( + 'Respond with exactly: {"status": "ok"}', + temperature=0.5, + max_tokens=30, + ) + assert response is not None + assert response.choices[0].message.content is not None + assert len(response.choices[0].message.content) > 0 + + def test_json_extraction_without_response_format(self): + """Verify JSON can be extracted from MiniMax response without response_format.""" + llm = OnlineLLM( + api_key=MINIMAX_API_KEY, + base_url="https://api.minimax.io/v1", + model_name="MiniMax-M2.5", + response_format="", + ) + response = llm.chat( + 'You must respond with valid JSON only, no other text. ' + 'Output: {"id": 1, "score": 4}', + temperature=0.5, + max_tokens=50, + ) + content = response.choices[0].message.content + assert content is not None + json_text = _extract_json_from_text(content) + assert '"id"' in json_text or '"score"' in json_text + + def test_temperature_clamping_in_api_call(self): + """Verify that temperature=0 is clamped and doesn't cause API error.""" + llm = OnlineLLM( + api_key=MINIMAX_API_KEY, + base_url="https://api.minimax.io/v1", + model_name="MiniMax-M2.5", + response_format="", + ) + # This would fail without clamping since MiniMax rejects temperature=0 + response = llm.chat( + "Say hello", + temperature=0, + max_tokens=10, + ) + assert response is not None + assert response.choices[0].message.content is not None + + def test_batch_chat_with_guided_decoding(self): + """Test batch processing with JSON validation (mimics data cleaning).""" + from pydantic import BaseModel as PydanticBaseModel + + class SimpleScore(PydanticBaseModel): + id: int + score: int + + llm = OnlineLLM( + api_key=MINIMAX_API_KEY, + base_url="https://api.minimax.io/v1", + model_name="MiniMax-M2.5", + response_format="", + ) + prompts = [ + 'You must respond with valid JSON only. Output: {"id": 1, "score": 4}', + 'You must respond with valid JSON only. Output: {"id": 2, "score": 3}', + ] + parsed_results, failed = llm.chat_batch( + prompts, + temperature=0.5, + max_tokens=50, + guided_decoding_class=SimpleScore, + ) + # At least one result should parse successfully + successful = [r for r in parsed_results if r is not None] + assert len(successful) > 0 + assert hasattr(successful[0], "id") + assert hasattr(successful[0], "score") diff --git a/weclone/core/inference/online_infer.py b/weclone/core/inference/online_infer.py index 9d0c661..f1665a0 100644 --- a/weclone/core/inference/online_infer.py +++ b/weclone/core/inference/online_infer.py @@ -15,6 +15,9 @@ class OnlineLLM: + # Providers that do not support the response_format parameter + _NO_RESPONSE_FORMAT_PROVIDERS = {"api.minimax.io", "api.minimaxi.com"} + def __init__( self, api_key: str, @@ -33,7 +36,26 @@ def __init__( self.client = OpenAI(api_key=self.api_key, base_url=self.base_url, max_retries=0) self.executor = ThreadPoolExecutor(max_workers=max_workers) self.prompt_with_system = prompt_with_system - self.response_format = response_format + self._supports_response_format = not self._is_no_response_format_provider(base_url) + self.response_format = response_format if self._supports_response_format else "" + + @staticmethod + def _is_no_response_format_provider(base_url: str) -> bool: + """Check if the provider does not support the response_format parameter.""" + if not base_url: + return False + base_url_lower = base_url.lower() + return any(host in base_url_lower for host in OnlineLLM._NO_RESPONSE_FORMAT_PROVIDERS) + + @staticmethod + def clamp_temperature(temperature: float, base_url: str) -> float: + """Clamp temperature for providers that require it to be in (0.0, 1.0]. + + MiniMax API rejects temperature=0; use a small positive value instead. + """ + if OnlineLLM._is_no_response_format_provider(base_url) and temperature <= 0: + return 0.01 + return temperature @retry_openai_api(max_retries=200, base_delay=30.0, max_delay=180.0) def chat( @@ -53,6 +75,8 @@ def chat( {"role": "user", "content": prompt_text}, ] + temperature = self.clamp_temperature(temperature, self.base_url) + params = { "model": self.model_name, "messages": messages, diff --git a/weclone/utils/config_models.py b/weclone/utils/config_models.py index 085402c..73c2a43 100644 --- a/weclone/utils/config_models.py +++ b/weclone/utils/config_models.py @@ -62,6 +62,32 @@ class CombineStrategy(StrEnum): TIME_WINDOW = "time_window" +class LLMProvider(StrEnum): + """LLM provider for online API calls""" + + OPENAI = "openai" + DEEPSEEK = "deepseek" + MINIMAX = "minimax" + CUSTOM = "custom" + + +# Provider preset configurations: base_url and default model +LLM_PROVIDER_PRESETS = { + LLMProvider.OPENAI: { + "base_url": "https://api.openai.com/v1", + "model_name": "gpt-4o-mini", + }, + LLMProvider.DEEPSEEK: { + "base_url": "https://api.deepseek.com/v1", + "model_name": "deepseek-chat", + }, + LLMProvider.MINIMAX: { + "base_url": "https://api.minimax.io/v1", + "model_name": "MiniMax-M2.5", + }, +} + + class CleanStrategy(StrEnum): """Data cleaning strategy""" @@ -156,6 +182,11 @@ class MakeDatasetArgs(BaseConfigModel): ) clean_dataset: CleanDatasetConfig = Field(CleanDatasetConfig(), description="Data cleaning configuration") online_llm_clear: bool = Field(False) + llm_provider: Optional[LLMProvider] = Field( + None, + description="LLM provider preset (openai, deepseek, minimax, custom). " + "When set, auto-fills base_url and model_name if not explicitly provided.", + ) base_url: Optional[str] = Field(None, description="Base URL for online LLM") llm_api_key: Optional[str] = Field(None, description="API key for online LLM") model_name: Optional[str] = Field( @@ -164,6 +195,17 @@ class MakeDatasetArgs(BaseConfigModel): clean_batch_size: int = Field(10, description="Batch size for data cleaning") vision_api: VisionApiConfig = Field(VisionApiConfig()) + @model_validator(mode="after") + def apply_provider_presets(self): + """Apply provider presets when llm_provider is set.""" + if self.llm_provider and self.llm_provider in LLM_PROVIDER_PRESETS: + preset = LLM_PROVIDER_PRESETS[self.llm_provider] + if not self.base_url: + self.base_url = preset["base_url"] + if not self.model_name: + self.model_name = preset["model_name"] + return self + class TrainSftArgs(BaseConfigModel): stage: str = Field("sft", description="Training stage") From 8a2cdc1db11769f0ef390d6d82f0f35b8e2a6870 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 05:22:11 +0000 Subject: [PATCH 2/4] :balloon: auto fixes by pre-commit hooks --- tests/test_minimax_provider.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_minimax_provider.py b/tests/test_minimax_provider.py index 6f2a7f0..7da159b 100644 --- a/tests/test_minimax_provider.py +++ b/tests/test_minimax_provider.py @@ -63,7 +63,6 @@ def _extract_json_from_text(text: str) -> str: MakeDatasetArgs, ) - # --------------------------------------------------------------------------- # Unit Tests – Provider presets # --------------------------------------------------------------------------- From 8fe697393fcf0fb7234b20bc1f45205115098f3f Mon Sep 17 00:00:00 2001 From: PR Bot Date: Tue, 17 Mar 2026 14:11:29 +0800 Subject: [PATCH 3/4] Address code review feedback from sourcery-ai - Use is None checks instead of truthiness for preset fields - Clamp MiniMax temperature upper bound to 1.0 (was only lower) - Add tests for None/empty base_url passthrough behavior - Add test for explicit response_format override on MiniMax - Simplify response_format assertion using call_args unpacking --- tests/test_minimax_provider.py | 36 +++++++++++++++++++++++--- weclone/core/inference/online_infer.py | 7 ++--- weclone/utils/config_models.py | 4 +-- 3 files changed, 38 insertions(+), 9 deletions(-) diff --git a/tests/test_minimax_provider.py b/tests/test_minimax_provider.py index 7da159b..02558e9 100644 --- a/tests/test_minimax_provider.py +++ b/tests/test_minimax_provider.py @@ -168,6 +168,24 @@ def test_china_endpoint_also_clamped(self): result = OnlineLLM.clamp_temperature(0.0, "https://api.minimaxi.com/v1") assert result == 0.01 + def test_above_one_clamped_for_minimax(self): + result = OnlineLLM.clamp_temperature(1.5, "https://api.minimax.io/v1") + assert result == 1.0 + + def test_above_one_unchanged_for_openai(self): + result = OnlineLLM.clamp_temperature(1.5, "https://api.openai.com/v1") + assert result == 1.5 + + def test_clamp_temperature_none_url(self): + """Temperature passes through unchanged when base_url is None.""" + result = OnlineLLM.clamp_temperature(0.0, None) + assert result == 0.0 + + def test_clamp_temperature_empty_url(self): + """Temperature passes through unchanged when base_url is empty string.""" + result = OnlineLLM.clamp_temperature(0.0, "") + assert result == 0.0 + # --------------------------------------------------------------------------- # Unit Tests – response_format handling @@ -205,6 +223,18 @@ def test_china_minimax_disables_response_format(self, mock_openai_cls): ) assert llm.response_format == "" + @patch("weclone.core.inference.online_infer.OpenAI") + def test_minimax_overrides_explicit_response_format(self, mock_openai_cls): + """Even when response_format is explicitly passed, MiniMax should clear it.""" + llm = OnlineLLM( + api_key="test-key", + base_url="https://api.minimax.io/v1", + model_name="MiniMax-M2.5", + response_format="json_object", + ) + assert llm.response_format == "" + assert llm._supports_response_format is False + @patch("weclone.core.inference.online_infer.OpenAI") def test_chat_omits_response_format_for_minimax(self, mock_openai_cls): mock_client = MagicMock() @@ -219,11 +249,9 @@ def test_chat_omits_response_format_for_minimax(self, mock_openai_cls): ) llm.chat("Hello") - call_kwargs = mock_client.chat.completions.create.call_args + _, kwargs = mock_client.chat.completions.create.call_args # response_format should NOT be in the call params - assert "response_format" not in call_kwargs.kwargs and ( - not call_kwargs.args or "response_format" not in str(call_kwargs) - ) + assert "response_format" not in kwargs @patch("weclone.core.inference.online_infer.OpenAI") def test_chat_includes_response_format_for_openai(self, mock_openai_cls): diff --git a/weclone/core/inference/online_infer.py b/weclone/core/inference/online_infer.py index f1665a0..194bed1 100644 --- a/weclone/core/inference/online_infer.py +++ b/weclone/core/inference/online_infer.py @@ -51,10 +51,11 @@ def _is_no_response_format_provider(base_url: str) -> bool: def clamp_temperature(temperature: float, base_url: str) -> float: """Clamp temperature for providers that require it to be in (0.0, 1.0]. - MiniMax API rejects temperature=0; use a small positive value instead. + MiniMax API rejects temperature=0 and values above 1.0; + clamp to (0.01, 1.0] for MiniMax providers. """ - if OnlineLLM._is_no_response_format_provider(base_url) and temperature <= 0: - return 0.01 + if OnlineLLM._is_no_response_format_provider(base_url): + return min(max(temperature, 0.01), 1.0) return temperature @retry_openai_api(max_retries=200, base_delay=30.0, max_delay=180.0) diff --git a/weclone/utils/config_models.py b/weclone/utils/config_models.py index 73c2a43..b0e2825 100644 --- a/weclone/utils/config_models.py +++ b/weclone/utils/config_models.py @@ -200,9 +200,9 @@ def apply_provider_presets(self): """Apply provider presets when llm_provider is set.""" if self.llm_provider and self.llm_provider in LLM_PROVIDER_PRESETS: preset = LLM_PROVIDER_PRESETS[self.llm_provider] - if not self.base_url: + if self.base_url is None: self.base_url = preset["base_url"] - if not self.model_name: + if self.model_name is None: self.model_name = preset["model_name"] return self From b345bc6a942cef9b89ea4f66c707429ea221eefb Mon Sep 17 00:00:00 2001 From: PR Bot Date: Thu, 19 Mar 2026 02:37:48 +0800 Subject: [PATCH 4/4] feat: upgrade MiniMax default model to M2.7 - Update default model from MiniMax-M2.5 to MiniMax-M2.7 - Update all config templates, README docs, and tests - Keep all previous models as alternatives --- README.md | 4 ++-- README_zh.md | 4 ++-- examples/mllm.template.jsonc | 2 +- examples/tg.template.jsonc | 4 ++-- settings.template.jsonc | 4 ++-- tests/test_minimax_provider.py | 28 ++++++++++++++-------------- weclone/utils/config_models.py | 2 +- 7 files changed, 24 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 866f8f8..29d904b 100644 --- a/README.md +++ b/README.md @@ -145,7 +145,7 @@ When using `online_llm_clear`, you can set `llm_provider` to quickly configure a |----------|----------------|---------------|----------| | OpenAI | `"openai"` | `gpt-4o-mini` | [platform.openai.com](https://platform.openai.com/docs) | | DeepSeek | `"deepseek"` | `deepseek-chat` | [platform.deepseek.com](https://platform.deepseek.com/docs) | -| MiniMax | `"minimax"` | `MiniMax-M2.5` | [platform.minimax.io](https://platform.minimax.io/docs/api-reference/text-openai-api) | +| MiniMax | `"minimax"` | `MiniMax-M2.7` | [platform.minimax.io](https://platform.minimax.io/docs/api-reference/text-openai-api) | | Custom | `"custom"` | *(manual)* | Any OpenAI-compatible API | Example using MiniMax for data cleaning: @@ -156,7 +156,7 @@ Example using MiniMax for data cleaning: "llm_api_key": "your-minimax-api-key", // base_url and model_name are auto-filled: // base_url → https://api.minimax.io/v1 - // model_name → MiniMax-M2.5 + // model_name → MiniMax-M2.7 } ``` diff --git a/README_zh.md b/README_zh.md index fc50718..bc98ffd 100644 --- a/README_zh.md +++ b/README_zh.md @@ -142,7 +142,7 @@ weclone-cli make-dataset |--------|----------------|----------|----------| | OpenAI | `"openai"` | `gpt-4o-mini` | [platform.openai.com](https://platform.openai.com/docs) | | DeepSeek | `"deepseek"` | `deepseek-chat` | [platform.deepseek.com](https://platform.deepseek.com/docs) | -| MiniMax | `"minimax"` | `MiniMax-M2.5` | [platform.minimax.io](https://platform.minimax.io/docs/api-reference/text-openai-api) | +| MiniMax | `"minimax"` | `MiniMax-M2.7` | [platform.minimax.io](https://platform.minimax.io/docs/api-reference/text-openai-api) | | 自定义 | `"custom"` | *(手动填写)* | 任何 OpenAI 兼容 API | 使用 MiniMax 进行数据清洗示例: @@ -153,7 +153,7 @@ weclone-cli make-dataset "llm_api_key": "your-minimax-api-key", // base_url 和 model_name 自动填充: // base_url → https://api.minimax.io/v1 - // model_name → MiniMax-M2.5 + // model_name → MiniMax-M2.7 } ``` diff --git a/examples/mllm.template.jsonc b/examples/mllm.template.jsonc index eca0778..8f548f7 100644 --- a/examples/mllm.template.jsonc +++ b/examples/mllm.template.jsonc @@ -46,7 +46,7 @@ // "llm_provider": "minimax", "base_url": "https://xxx/v1", "llm_api_key": "xxxxx", - "model_name": "xxx", //建议使用参数较大的模型,例如DeepSeek-V3, MiniMax-M2.5 + "model_name": "xxx", //建议使用参数较大的模型,例如DeepSeek-V3, MiniMax-M2.7 "clean_batch_size": 10, "vision_api": { "enable": false, // 设置为 true 来开启此功能 diff --git a/examples/tg.template.jsonc b/examples/tg.template.jsonc index a8a7377..bae195f 100644 --- a/examples/tg.template.jsonc +++ b/examples/tg.template.jsonc @@ -48,11 +48,11 @@ "online_llm_clear": false, // llm_provider: "openai", "deepseek", "minimax", or "custom" // When set, auto-fills base_url and model_name if not explicitly provided. - // e.g. "minimax" → base_url: https://api.minimax.io/v1, model_name: MiniMax-M2.5 + // e.g. "minimax" → base_url: https://api.minimax.io/v1, model_name: MiniMax-M2.7 // "llm_provider": "minimax", "base_url": "https://xxx/v1", "llm_api_key": "xxxxx", - "model_name": "xxx", // Recommend using models with larger parameters, e.g. DeepSeek-V3, MiniMax-M2.5 + "model_name": "xxx", // Recommend using models with larger parameters, e.g. DeepSeek-V3, MiniMax-M2.7 "clean_batch_size": 10, "vision_api": { "enable": false, // Set to true to enable this feature diff --git a/settings.template.jsonc b/settings.template.jsonc index 43ec89d..1489203 100644 --- a/settings.template.jsonc +++ b/settings.template.jsonc @@ -48,11 +48,11 @@ "online_llm_clear": false, // llm_provider: 可选 "openai", "deepseek", "minimax", "custom" // 设置后会自动填充 base_url 和 model_name(若未显式指定) - // 例如设为 "minimax" 时,base_url 默认 https://api.minimax.io/v1,model_name 默认 MiniMax-M2.5 + // 例如设为 "minimax" 时,base_url 默认 https://api.minimax.io/v1,model_name 默认 MiniMax-M2.7 // "llm_provider": "minimax", "base_url": "https://xxx/v1", "llm_api_key": "xxxxx", - "model_name": "xxx", //建议使用参数较大的模型,例如DeepSeek-V3, MiniMax-M2.5 + "model_name": "xxx", //建议使用参数较大的模型,例如DeepSeek-V3, MiniMax-M2.7 "clean_batch_size": 50, "vision_api": { "enable": false, // 设置为 true 来开启此功能 diff --git a/tests/test_minimax_provider.py b/tests/test_minimax_provider.py index 02558e9..aa33a6d 100644 --- a/tests/test_minimax_provider.py +++ b/tests/test_minimax_provider.py @@ -20,7 +20,7 @@ def _extract_json_from_text(text: str) -> str: first JSON object so that the payload can be parsed by Pydantic in integration tests. """ - # Strip blocks (MiniMax M2.5 thinking output) + # Strip blocks (MiniMax M2.7 thinking output) cleaned = re.sub(r".*?", "", text, flags=re.DOTALL).strip() # Strip markdown code fences m = re.search(r"```json\s*(.*?)\s*```", cleaned, re.DOTALL) @@ -73,7 +73,7 @@ class TestLLMProviderPresets: def test_minimax_preset_values(self): preset = LLM_PROVIDER_PRESETS[LLMProvider.MINIMAX] assert preset["base_url"] == "https://api.minimax.io/v1" - assert preset["model_name"] == "MiniMax-M2.5" + assert preset["model_name"] == "MiniMax-M2.7" def test_openai_preset_values(self): preset = LLM_PROVIDER_PRESETS[LLMProvider.OPENAI] @@ -100,7 +100,7 @@ class TestMakeDatasetArgsProviderPresets: def test_minimax_provider_fills_base_url_and_model(self): args = MakeDatasetArgs(llm_provider="minimax", **self._base_args) assert args.base_url == "https://api.minimax.io/v1" - assert args.model_name == "MiniMax-M2.5" + assert args.model_name == "MiniMax-M2.7" def test_explicit_base_url_overrides_preset(self): args = MakeDatasetArgs( @@ -109,16 +109,16 @@ def test_explicit_base_url_overrides_preset(self): **self._base_args, ) assert args.base_url == "https://api.minimaxi.com/v1" - assert args.model_name == "MiniMax-M2.5" + assert args.model_name == "MiniMax-M2.7" def test_explicit_model_name_overrides_preset(self): args = MakeDatasetArgs( llm_provider="minimax", - model_name="MiniMax-M2.5-highspeed", + model_name="MiniMax-M2.7-highspeed", **self._base_args, ) assert args.base_url == "https://api.minimax.io/v1" - assert args.model_name == "MiniMax-M2.5-highspeed" + assert args.model_name == "MiniMax-M2.7-highspeed" def test_no_provider_leaves_fields_none(self): args = MakeDatasetArgs(**self._base_args) @@ -199,7 +199,7 @@ def test_minimax_disables_response_format(self, mock_openai_cls): llm = OnlineLLM( api_key="test-key", base_url="https://api.minimax.io/v1", - model_name="MiniMax-M2.5", + model_name="MiniMax-M2.7", ) assert llm.response_format == "" assert llm._supports_response_format is False @@ -219,7 +219,7 @@ def test_china_minimax_disables_response_format(self, mock_openai_cls): llm = OnlineLLM( api_key="test-key", base_url="https://api.minimaxi.com/v1", - model_name="MiniMax-M2.5", + model_name="MiniMax-M2.7", ) assert llm.response_format == "" @@ -229,7 +229,7 @@ def test_minimax_overrides_explicit_response_format(self, mock_openai_cls): llm = OnlineLLM( api_key="test-key", base_url="https://api.minimax.io/v1", - model_name="MiniMax-M2.5", + model_name="MiniMax-M2.7", response_format="json_object", ) assert llm.response_format == "" @@ -245,7 +245,7 @@ def test_chat_omits_response_format_for_minimax(self, mock_openai_cls): llm = OnlineLLM( api_key="test-key", base_url="https://api.minimax.io/v1", - model_name="MiniMax-M2.5", + model_name="MiniMax-M2.7", ) llm.chat("Hello") @@ -310,7 +310,7 @@ def test_basic_chat_completion(self): llm = OnlineLLM( api_key=MINIMAX_API_KEY, base_url="https://api.minimax.io/v1", - model_name="MiniMax-M2.5", + model_name="MiniMax-M2.7", response_format="", ) response = llm.chat( @@ -327,7 +327,7 @@ def test_json_extraction_without_response_format(self): llm = OnlineLLM( api_key=MINIMAX_API_KEY, base_url="https://api.minimax.io/v1", - model_name="MiniMax-M2.5", + model_name="MiniMax-M2.7", response_format="", ) response = llm.chat( @@ -346,7 +346,7 @@ def test_temperature_clamping_in_api_call(self): llm = OnlineLLM( api_key=MINIMAX_API_KEY, base_url="https://api.minimax.io/v1", - model_name="MiniMax-M2.5", + model_name="MiniMax-M2.7", response_format="", ) # This would fail without clamping since MiniMax rejects temperature=0 @@ -369,7 +369,7 @@ class SimpleScore(PydanticBaseModel): llm = OnlineLLM( api_key=MINIMAX_API_KEY, base_url="https://api.minimax.io/v1", - model_name="MiniMax-M2.5", + model_name="MiniMax-M2.7", response_format="", ) prompts = [ diff --git a/weclone/utils/config_models.py b/weclone/utils/config_models.py index b0e2825..a9d8ab8 100644 --- a/weclone/utils/config_models.py +++ b/weclone/utils/config_models.py @@ -83,7 +83,7 @@ class LLMProvider(StrEnum): }, LLMProvider.MINIMAX: { "base_url": "https://api.minimax.io/v1", - "model_name": "MiniMax-M2.5", + "model_name": "MiniMax-M2.7", }, }