Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 30 additions & 6 deletions pageindex/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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/")
Expand All @@ -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
Expand All @@ -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 = {}
Expand Down
8 changes: 7 additions & 1 deletion pageindex/config.yaml
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
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
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"
if_add_node_text: "no"
106 changes: 106 additions & 0 deletions tests/test_minimax_provider.py
Original file line number Diff line number Diff line change
@@ -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",
]