From b961d0ac31381fc15a8acca0fed95f46140fbcbe Mon Sep 17 00:00:00 2001 From: octo-patch <266937838+octo-patch@users.noreply.github.com> Date: Tue, 14 Jul 2026 20:55:28 +0800 Subject: [PATCH] Add MiniMax provider configuration --- README.md | 42 +++++++++++++ pageindex/client.py | 36 +++++++++-- pageindex/config.yaml | 8 ++- tests/test_minimax_provider.py | 106 +++++++++++++++++++++++++++++++++ 4 files changed, 185 insertions(+), 7 deletions(-) create mode 100644 tests/test_minimax_provider.py diff --git a/README.md b/README.md index 594b70e8e..8a31f9038 100644 --- a/README.md +++ b/README.md @@ -163,6 +163,48 @@ Create a `.env` file in the root directory with your LLM API key. Multi-LLM is s OPENAI_API_KEY=your_openai_key_here ``` +#### MiniMax + +Both `MiniMax-M3` and `MiniMax-M2.7` can be used through LiteLLM. Choose the +API mode and region that match your account: + +| API mode | Region | Base URL | +| --- | --- | --- | +| OpenAI-compatible | Global | `https://api.minimax.io/v1` | +| OpenAI-compatible | China | `https://api.minimaxi.com/v1` | +| Anthropic-compatible | Global | `https://api.minimax.io/anthropic` | +| Anthropic-compatible | China | `https://api.minimaxi.com/anthropic` | + +For the OpenAI-compatible API, set the matching regional base URL: + +```bash +MINIMAX_API_KEY=your_minimax_key_here +MINIMAX_API_BASE=https://api.minimax.io/v1 +``` + +Then select either model in `pageindex/config.yaml` or with `--model`: + +```yaml +model: "minimax/MiniMax-M3" +retrieve_model: "minimax/MiniMax-M2.7" +``` + +For the Anthropic-compatible API, keep the public base URL ending in +`/anthropic`: + +```bash +ANTHROPIC_API_KEY=your_minimax_key_here +ANTHROPIC_API_BASE=https://api.minimax.io/anthropic +``` + +```yaml +model: "anthropic/MiniMax-M3" +retrieve_model: "anthropic/MiniMax-M2.7" +``` + +When using `PageIndexClient`, the optional `api_base` argument configures the +matching base URL variable for the selected model prefix. + ### 3. Generate PageIndex structure for your PDF ```bash diff --git a/pageindex/client.py b/pageindex/client.py index 894dab181..0351cc642 100644 --- a/pageindex/client.py +++ b/pageindex/client.py @@ -15,6 +15,16 @@ META_INDEX = "_meta.json" +def _provider_environment(model: str) -> tuple[str, str]: + """Return the LiteLLM credential and base URL variables for a model.""" + normalized_model = (model or "").removeprefix("litellm/") + if normalized_model.startswith("anthropic/"): + return "ANTHROPIC_API_KEY", "ANTHROPIC_API_BASE" + if normalized_model.startswith("minimax/"): + return "MINIMAX_API_KEY", "MINIMAX_API_BASE" + return "OPENAI_API_KEY", "OPENAI_API_BASE" + + def _normalize_retrieve_model(model: str) -> str: """Preserve supported Agents SDK prefixes and route other provider paths via LiteLLM.""" passthrough_prefixes = ("litellm/", "openai/") @@ -32,12 +42,14 @@ class PageIndexClient: For agent-based QA, see examples/agentic_vectorless_rag_demo.py. """ - def __init__(self, api_key: str = None, model: str = None, retrieve_model: str = None, workspace: str = None): - if api_key: - os.environ["OPENAI_API_KEY"] = api_key - elif not os.getenv("OPENAI_API_KEY") and os.getenv("CHATGPT_API_KEY"): - os.environ["OPENAI_API_KEY"] = os.getenv("CHATGPT_API_KEY") - self.workspace = Path(workspace).expanduser() if workspace else None + def __init__( + self, + api_key: str = None, + model: str = None, + retrieve_model: str = None, + workspace: str = None, + api_base: str = None, + ): overrides = {} if model: overrides["model"] = model @@ -46,6 +58,18 @@ def __init__(self, api_key: str = None, model: str = None, retrieve_model: str = opt = ConfigLoader().load(overrides or None) self.model = opt.model self.retrieve_model = _normalize_retrieve_model(opt.retrieve_model or self.model) + api_key_env, api_base_env = _provider_environment(self.model) + if api_key: + os.environ[api_key_env] = api_key + elif ( + api_key_env == "OPENAI_API_KEY" + and not os.getenv(api_key_env) + and os.getenv("CHATGPT_API_KEY") + ): + os.environ[api_key_env] = os.getenv("CHATGPT_API_KEY") + if api_base: + os.environ[api_base_env] = api_base + self.workspace = Path(workspace).expanduser() if workspace else None if self.workspace: self.workspace.mkdir(parents=True, exist_ok=True) self.documents = {} diff --git a/pageindex/config.yaml b/pageindex/config.yaml index 591fe9331..76cb135d5 100644 --- a/pageindex/config.yaml +++ b/pageindex/config.yaml @@ -1,5 +1,11 @@ model: "gpt-4o-2024-11-20" # model: "anthropic/claude-sonnet-4-6" +# MiniMax OpenAI-compatible examples: +# model: "minimax/MiniMax-M3" +# retrieve_model: "minimax/MiniMax-M2.7" +# MiniMax Anthropic-compatible examples: +# model: "anthropic/MiniMax-M3" +# retrieve_model: "anthropic/MiniMax-M2.7" retrieve_model: "gpt-5.4" # defaults to `model` if not set toc_check_page_num: 20 max_page_num_each_node: 10 @@ -7,4 +13,4 @@ max_token_num_each_node: 20000 if_add_node_id: "yes" if_add_node_summary: "yes" if_add_doc_description: "no" -if_add_node_text: "no" \ No newline at end of file +if_add_node_text: "no" diff --git a/tests/test_minimax_provider.py b/tests/test_minimax_provider.py new file mode 100644 index 000000000..ade8280e6 --- /dev/null +++ b/tests/test_minimax_provider.py @@ -0,0 +1,106 @@ +import os +from unittest.mock import patch + +import httpx +import litellm +import pytest + +from pageindex.client import PageIndexClient + + +@pytest.mark.parametrize( + ("model", "api_base", "api_key_env", "api_base_env"), + [ + ( + "minimax/MiniMax-M3", + "https://api.minimax.io/v1", + "MINIMAX_API_KEY", + "MINIMAX_API_BASE", + ), + ( + "minimax/MiniMax-M2.7", + "https://api.minimaxi.com/v1", + "MINIMAX_API_KEY", + "MINIMAX_API_BASE", + ), + ( + "anthropic/MiniMax-M3", + "https://api.minimax.io/anthropic", + "ANTHROPIC_API_KEY", + "ANTHROPIC_API_BASE", + ), + ( + "anthropic/MiniMax-M2.7", + "https://api.minimaxi.com/anthropic", + "ANTHROPIC_API_KEY", + "ANTHROPIC_API_BASE", + ), + ], +) +def test_client_configures_provider_environment(model, api_base, api_key_env, api_base_env): + with patch.dict(os.environ, {}, clear=True): + PageIndexClient(model=model, api_key="test-key", api_base=api_base) + + assert os.environ[api_key_env] == "test-key" + assert os.environ[api_base_env] == api_base + + +def test_litellm_routes_compatible_endpoints(): + captured_urls = [] + + def fake_send(_client, request, *args, **kwargs): + captured_urls.append(str(request.url)) + if "/anthropic/" in str(request.url): + payload = { + "id": "msg_test", + "type": "message", + "role": "assistant", + "model": "MiniMax-M3", + "content": [{"type": "text", "text": "ok"}], + "stop_reason": "end_turn", + "usage": {"input_tokens": 1, "output_tokens": 1}, + } + else: + payload = { + "id": "chatcmpl-test", + "object": "chat.completion", + "created": 0, + "model": "MiniMax-M3", + "choices": [ + { + "index": 0, + "message": {"role": "assistant", "content": "ok"}, + "finish_reason": "stop", + } + ], + "usage": { + "prompt_tokens": 1, + "completion_tokens": 1, + "total_tokens": 2, + }, + } + return httpx.Response(200, json=payload, request=request) + + cases = [ + ("minimax/MiniMax-M3", "https://api.minimax.io/v1"), + ("minimax/MiniMax-M2.7", "https://api.minimaxi.com/v1"), + ("anthropic/MiniMax-M3", "https://api.minimax.io/anthropic"), + ("anthropic/MiniMax-M2.7", "https://api.minimaxi.com/anthropic"), + ] + + with patch.object(httpx.Client, "send", fake_send): + for model, api_base in cases: + response = litellm.completion( + model=model, + api_base=api_base, + api_key="test-key", + messages=[{"role": "user", "content": "hello"}], + ) + assert response.choices[0].message.content == "ok" + + assert captured_urls == [ + "https://api.minimax.io/v1/chat/completions", + "https://api.minimaxi.com/v1/chat/completions", + "https://api.minimax.io/anthropic/v1/messages", + "https://api.minimaxi.com/anthropic/v1/messages", + ]