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
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,29 @@ print("Invoice Number:", result.invoice_number)
print("Invoice Date:", result.invoice_date)
```

### MiniMax Integration Example

ExtractThinker supports [MiniMax](https://www.minimax.io/) models via the `minimax/` prefix. Set your API key and use MiniMax models for document extraction:

```python
import os
from extract_thinker import Extractor, DocumentLoaderPyPdf, Contract

os.environ["MINIMAX_API_KEY"] = "your-minimax-api-key"

class InvoiceContract(Contract):
invoice_number: str
invoice_date: str

extractor = Extractor()
extractor.load_document_loader(DocumentLoaderPyPdf())
extractor.load_llm("minimax/MiniMax-M2.7") # or "minimax/MiniMax-M2.7-highspeed"

result = extractor.extract("invoice.pdf", InvoiceContract)
print("Invoice Number:", result.invoice_number)
print("Invoice Date:", result.invoice_date)
```

## 📚 Documentation and Resources

- **Examples**: Check out the examples directory for Jupyter notebooks and scripts demonstrating various use cases.
Expand All @@ -271,6 +294,7 @@ ExtractThinker supports integration with multiple LLM providers:
- **Anthropic**: Integrate with Claude models.
- **Cohere**: Utilize Cohere's language models.
- **Azure OpenAI**: Connect with Azure's OpenAI services.
- **MiniMax**: Use MiniMax M2.7 / M2.5 models with up to 1M context.
- **Local Models**: Ollama compatible models.

## ⚙️ How It Works
Expand Down
12 changes: 11 additions & 1 deletion extract_thinker/global_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,14 @@ def get_gpt_mini_model():

def get_gpt_o4_model():
"""Return the GPT-4o model."""
return "gpt-4o"
return "gpt-4o"


def get_minimax_model():
"""Return the MiniMax M2.7 model (1M context)."""
return "minimax/MiniMax-M2.7"


def get_minimax_highspeed_model():
"""Return the MiniMax M2.7-highspeed model (1M context, faster)."""
return "minimax/MiniMax-M2.7-highspeed"
62 changes: 51 additions & 11 deletions extract_thinker/llm.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import asyncio
import os
from typing import List, Dict, Any, Optional
import instructor
import litellm
from litellm import Router
from extract_thinker.llm_engine import LLMEngine
from extract_thinker.utils import add_classification_structure, extract_thinking_json
from extract_thinker.utils import (
add_classification_structure,
extract_thinking_json,
is_minimax_model,
resolve_minimax_model,
MINIMAX_API_BASE,
)

# Helper to build the dynamic prompt used when `is_dynamic=True`.
# We expose it as a standalone function so that callers (or subclasses)
Expand Down Expand Up @@ -56,16 +63,30 @@ def __init__(
self,
model: str,
token_limit: int = None,
backend: LLMEngine = LLMEngine.DEFAULT
backend: LLMEngine = LLMEngine.DEFAULT,
api_base: Optional[str] = None,
api_key: Optional[str] = None,
):
"""Initialize LLM with specified backend.

Args:
model: The model name (e.g. "gpt-4", "claude-3")
model: The model name (e.g. "gpt-4", "claude-3", "minimax/MiniMax-M2.7")
token_limit: Optional maximum tokens
backend: LLMBackend enum (default: LITELLM)
api_base: Optional custom API base URL (auto-detected for MiniMax)
api_key: Optional API key (auto-detected from env for MiniMax)
"""
self.model = model
# Auto-detect MiniMax models and configure accordingly
self._is_minimax = is_minimax_model(model)
if self._is_minimax:
self.model = resolve_minimax_model(model)
self.api_base = api_base or MINIMAX_API_BASE
self.api_key = api_key or os.environ.get("MINIMAX_API_KEY")
else:
self.model = model
self.api_base = api_base
self.api_key = api_key

self.token_limit = token_limit
self.router = None
self.is_dynamic = False
Expand Down Expand Up @@ -124,9 +145,25 @@ def load_router(self, router: Router) -> None:
raise ValueError("Router is only supported with LITELLM backend")
self.router = router

def _get_extra_params(self) -> Dict[str, Any]:
"""Return provider-specific extra parameters (api_base, api_key)."""
params: Dict[str, Any] = {}
if self.api_base:
params["api_base"] = self.api_base
if self.api_key:
params["api_key"] = self.api_key
return params

def _effective_temperature(self) -> float:
"""Return the temperature value, clamped for MiniMax models."""
temp = self.temperature
if self._is_minimax:
temp = max(0.0, min(temp, 1.0))
return temp

def set_temperature(self, temperature: float) -> None:
"""Set the temperature for LLM requests.

Args:
temperature (float): Temperature value between 0 and 1
"""
Expand Down Expand Up @@ -242,14 +279,15 @@ def _request_with_router(self, messages: List[Dict[str, str]], response_model: O
max_tokens = min(self.token_limit, max_tokens)
elif self.is_thinking:
max_tokens = min(self.thinking_token_limit, max_tokens) if self.thinking_token_limit else max_tokens

params = {
"model": self.model,
"messages": messages,
"response_model": response_model,
"temperature": self.temperature,
"temperature": self._effective_temperature(),
"timeout": self.TIMEOUT,
"max_completion_tokens": max_tokens,
**self._get_extra_params(),
}
if self.is_thinking:
if litellm.supports_reasoning(self.model):
Expand All @@ -275,13 +313,14 @@ def _request_direct(self, messages: List[Dict[str, str]], response_model: Option
base_params = {
"model": self.model,
"messages": messages,
"temperature": self.temperature,
"temperature": self._effective_temperature(),
"response_model": response_model,
"max_retries": 1,
"max_completion_tokens": max_tokens,
"timeout": self.TIMEOUT,
**self._get_extra_params(),
}

if self.is_thinking:
if litellm.supports_reasoning(self.model):
# Try with thinking parameter
Expand Down Expand Up @@ -321,6 +360,7 @@ def raw_completion(self, messages: List[Dict[str, str]]) -> str:
"model": self.model,
"messages": messages,
"max_completion_tokens": max_tokens,
**self._get_extra_params(),
}

if self.is_thinking:
Expand All @@ -333,7 +373,7 @@ def raw_completion(self, messages: List[Dict[str, str]]) -> str:
params["thinking"] = thinking_param
else:
print(f"Warning: Model {self.model} doesn't support thinking parameter, proceeding without it.")

if self.router:
raw_response = self.router.completion(**params)
else:
Expand Down
40 changes: 40 additions & 0 deletions extract_thinker/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,46 @@ def extract_thinking_json(thinking_text: str, response_model: type[BaseModel]) -
except Exception as e:
raise ValueError(f"Failed to parse thinking output: {str(e)}\nInput text was: {thinking_text[:200]}...")

MINIMAX_API_BASE = "https://api.minimax.io/v1"
MINIMAX_MODELS = [
"MiniMax-M2.7",
"MiniMax-M2.7-highspeed",
"MiniMax-M2.5",
"MiniMax-M2.5-highspeed",
]


def is_minimax_model(model: str) -> bool:
"""Check if a model string refers to a MiniMax model.

Recognises both the ``minimax/`` convenience prefix and bare model names
that belong to the MiniMax family (e.g. ``MiniMax-M2.7``).
"""
model_lower = model.lower()
if model_lower.startswith("minimax/"):
return True
# Also match openai/MiniMax-* (user may have already rewritten)
for m in MINIMAX_MODELS:
if m.lower() in model_lower:
return True
return False


def resolve_minimax_model(model: str) -> str:
"""Rewrite a ``minimax/...`` model string to ``openai/...`` for litellm.

If the model already uses the ``openai/`` prefix or is bare, it is
normalised to ``openai/<model_name>`` so that litellm routes the request
through its OpenAI-compatible provider.
"""
if model.lower().startswith("minimax/"):
return "openai/" + model[len("minimax/"):]
if model.lower().startswith("openai/"):
return model
# Bare model name
return "openai/" + model


def is_vision_error(error: Exception) -> bool:
if isinstance(error.args[0], litellm.BadRequestError):
return True
Expand Down
Loading