Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ cohere = ["tokenizers>=0.16.0", "cohere>=5.13.0"]
jina = ["tokenizers>=0.16.0"]
catsu = ["catsu>=0.0.1"]
litellm = ["litellm>=1.0.0", "tiktoken>=0.5.0", "tokenizers>=0.16.0"]
minimax = [] # uses httpx (core dep) for native MiniMax API

# Optional dependencies for the friends
chroma = ["chromadb>=1.0.0"]
Expand Down Expand Up @@ -156,6 +157,7 @@ all = [
"chonkie[jina]",
"chonkie[catsu]",
"chonkie[litellm]",
"chonkie[minimax]",
"chonkie[chroma]",
"chonkie[qdrant]",
"chonkie[tpuf]",
Expand Down
5 changes: 4 additions & 1 deletion src/chonkie/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,14 @@
GeminiEmbeddings,
JinaEmbeddings,
LiteLLMEmbeddings,
MiniMaxEmbeddings,
Model2VecEmbeddings,
OpenAIEmbeddings,
SentenceTransformerEmbeddings,
VoyageAIEmbeddings,
)
from .fetcher import BaseFetcher, FileFetcher
from .genie import AzureOpenAIGenie, BaseGenie, CerebrasGenie, GeminiGenie, GroqGenie, OpenAIGenie
from .genie import AzureOpenAIGenie, BaseGenie, CerebrasGenie, GeminiGenie, GroqGenie, MiniMaxGenie, OpenAIGenie
from .handshakes import (
BaseHandshake,
ChromaHandshake,
Expand Down Expand Up @@ -99,6 +100,7 @@
"GeminiEmbeddings",
"JinaEmbeddings",
"LiteLLMEmbeddings",
"MiniMaxEmbeddings",
"Model2VecEmbeddings",
"OpenAIEmbeddings",
"SentenceTransformerEmbeddings",
Expand All @@ -112,6 +114,7 @@
"CerebrasGenie",
"GeminiGenie",
"GroqGenie",
"MiniMaxGenie",
"OpenAIGenie",
# handshakes
"BaseHandshake",
Expand Down
2 changes: 2 additions & 0 deletions src/chonkie/embeddings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from .gemini import GeminiEmbeddings
from .jina import JinaEmbeddings
from .litellm import LiteLLMEmbeddings
from .minimax import MiniMaxEmbeddings
from .mistral import MistralEmbeddings
from .mixedbread import MixedbreadEmbeddings
from .model2vec import Model2VecEmbeddings
Expand Down Expand Up @@ -41,4 +42,5 @@
"NomicEmbeddings",
"DeepInfraEmbeddings",
"CloudflareEmbeddings",
"MiniMaxEmbeddings",
]
287 changes: 287 additions & 0 deletions src/chonkie/embeddings/minimax.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
"""MiniMax embeddings via native API.

MiniMax provides the embo-01 embedding model with 1536 dimensions.
The API uses a non-OpenAI-compatible format with ``texts`` and ``type`` fields.
"""

import importlib.util as importutil
import os
from typing import Any, List, Optional

import httpx
import numpy as np
from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_exponential

from .base import BaseEmbeddings

# MiniMax embedding API endpoint
_MINIMAX_EMBEDDINGS_URL = "https://api.minimax.io/v1/embeddings"


class MiniMaxEmbeddings(BaseEmbeddings):
"""MiniMax embeddings using the native MiniMax Embedding API.

Uses the embo-01 model (1536 dimensions) via MiniMax's native endpoint,
which expects ``texts`` and ``type`` fields instead of the OpenAI format.

Args:
model: MiniMax embedding model name (default: "embo-01").
api_key: MiniMax API key (or set MINIMAX_API_KEY env var).
embedding_type: Embedding type — "db" for storage, "query" for search
(default: "db").
batch_size: Maximum texts per API call (default: 64).
timeout: Request timeout in seconds (default: 30).

Examples:
>>> embeddings = MiniMaxEmbeddings(api_key="your-key")
>>> vector = embeddings.embed("hello world")
>>> vectors = embeddings.embed_batch(["text1", "text2"])

"""

DEFAULT_MODEL = "embo-01"

AVAILABLE_MODELS = {
"embo-01": 1536,
}

def __init__(
self,
model: str = DEFAULT_MODEL,
api_key: Optional[str] = None,
embedding_type: str = "db",
batch_size: int = 64,
timeout: float = 30.0,
):
"""Initialize MiniMax embeddings.

Args:
model: MiniMax embedding model name.
api_key: MiniMax API key (falls back to MINIMAX_API_KEY env var).
embedding_type: "db" for storage or "query" for search queries.
batch_size: Maximum texts per API call.
timeout: Request timeout in seconds.

Raises:
ImportError: If httpx is not installed.
ValueError: If no API key is provided or embedding_type is invalid.

"""
super().__init__()

if not self._is_available():
raise ImportError(
"The httpx package is required for MiniMaxEmbeddings. "
"Please install it via `pip install httpx`"
)

self.model = model
self.api_key = api_key or os.environ.get("MINIMAX_API_KEY")
if not self.api_key:
raise ValueError(
"MiniMaxEmbeddings requires an API key. "
"Either pass the `api_key` parameter or set the `MINIMAX_API_KEY` "
"environment variable.",
)

if embedding_type not in ("db", "query"):
raise ValueError("embedding_type must be 'db' or 'query'")

self.embedding_type = embedding_type
self._batch_size = batch_size
self._timeout = timeout
self._dimension = self.AVAILABLE_MODELS.get(model, 1536)

self._client = httpx.Client(timeout=self._timeout)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

# -- core methods ---------------------------------------------------------

@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=2, max=30),
retry=retry_if_exception_type((
httpx.HTTPStatusError,
httpx.ConnectError,
httpx.TimeoutException,
)),
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
def _call_api(self, texts: List[str]) -> List[List[float]]:
"""Call the MiniMax embedding API.

Args:
texts: List of text strings to embed.

Returns:
List of embedding vectors (each a list of floats).

Raises:
ValueError: If the API returns an error.

"""
response = self._client.post(
_MINIMAX_EMBEDDINGS_URL,
headers={
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json",
},
json={
"model": self.model,
"texts": texts,
"type": self.embedding_type,
},
)
response.raise_for_status()
data = response.json()

# Check for API-level errors
base_resp = data.get("base_resp", {})
if base_resp.get("status_code", 0) != 0:
raise ValueError(f"MiniMax API error: {base_resp.get('status_msg', 'unknown error')}")

vectors = data.get("vectors")
if vectors is None:
raise ValueError("MiniMax API response missing 'vectors' field")
return vectors

@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=2, max=30),
retry=retry_if_exception_type((
httpx.HTTPStatusError,
httpx.ConnectError,
httpx.TimeoutException,
)),
)
async def _acall_api(self, texts: List[str]) -> List[List[float]]:
"""Call the MiniMax embedding API asynchronously.

Args:
texts: List of text strings to embed.

Returns:
List of embedding vectors (each a list of floats).

Raises:
ValueError: If the API returns an error.

"""
async with httpx.AsyncClient(timeout=self._timeout) as client:
response = await client.post(
_MINIMAX_EMBEDDINGS_URL,
headers={
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json",
},
json={
"model": self.model,
"texts": texts,
"type": self.embedding_type,
},
)
response.raise_for_status()
data = response.json()

base_resp = data.get("base_resp", {})
if base_resp.get("status_code", 0) != 0:
raise ValueError(
f"MiniMax API error: {base_resp.get('status_msg', 'unknown error')}"
Comment on lines +163 to +188

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For better performance, the httpx.AsyncClient should be instantiated once in the __init__ method and reused across all asynchronous calls, rather than being created for each call within _acall_api. This avoids the overhead of setting up a new client and connection pool for every API request, which is especially important for batch operations.

This pattern is already used for the synchronous httpx.Client and is also consistent with how the MiniMaxGenie class handles its AsyncOpenAI client.

You should add self._async_client = httpx.AsyncClient(timeout=self._timeout) to __init__ and then use self._async_client here.

        response = await self._async_client.post(
            _MINIMAX_EMBEDDINGS_URL,
            headers={
                "Authorization": f"Bearer {self.api_key}",
                "Content-Type": "application/json",
            },
            json={
                "model": self.model,
                "texts": texts,
                "type": self.embedding_type,
            },
        )
        response.raise_for_status()
        data = response.json()

        base_resp = data.get("base_resp", {})
        if base_resp.get("status_code", 0) != 0:
            raise ValueError(
                f"MiniMax API error: {base_resp.get('status_msg', 'unknown error')}"
            )

        vectors = data.get("vectors")
        if vectors is None:
            raise ValueError("MiniMax API response missing 'vectors' field")
        return vectors

)

vectors = data.get("vectors")
if vectors is None:
raise ValueError("MiniMax API response missing 'vectors' field")
return vectors

def embed(self, text: str) -> np.ndarray:
"""Embed a single text string.

Args:
text: Text string to embed.

Returns:
Embedding vector as a numpy array.

"""
vectors = self._call_api([text])
return np.array(vectors[0], dtype=np.float32)

def embed_batch(self, texts: List[str]) -> List[np.ndarray]:
"""Embed multiple texts using batched API calls.

Args:
texts: List of text strings to embed.

Returns:
List of embedding vectors as numpy arrays.

"""
if not texts:
return []

results: List[np.ndarray] = []
for i in range(0, len(texts), self._batch_size):
batch = texts[i : i + self._batch_size]
vectors = self._call_api(batch)
results.extend(np.array(v, dtype=np.float32) for v in vectors)
return results

async def aembed(self, text: str) -> np.ndarray:
"""Embed a single text string asynchronously.

Args:
text: Text string to embed.

Returns:
Embedding vector as a numpy array.

"""
vectors = await self._acall_api([text])
return np.array(vectors[0], dtype=np.float32)

async def aembed_batch(self, texts: List[str]) -> List[np.ndarray]:
"""Embed multiple texts asynchronously using batched API calls.

Args:
texts: List of text strings to embed.

Returns:
List of embedding vectors as numpy arrays.

"""
if not texts:
return []

results: List[np.ndarray] = []
for i in range(0, len(texts), self._batch_size):
batch = texts[i : i + self._batch_size]
vectors = await self._acall_api(batch)
results.extend(np.array(v, dtype=np.float32) for v in vectors)
return results

@property
def dimension(self) -> int:
"""Return the embedding dimension (1536 for embo-01)."""
return self._dimension

def get_tokenizer(self) -> Any:
"""Return a basic tokenizer for token counting.

MiniMax does not expose a dedicated tokenizer API, so we use
chonkie's built-in WordTokenizer as an approximation.

Returns:
WordTokenizer instance.

"""
from chonkie.tokenizer import WordTokenizer

return WordTokenizer()

@classmethod
def _is_available(cls) -> bool:
"""Check if httpx is available."""
return importutil.find_spec("httpx") is not None

def __repr__(self) -> str:
"""Return a string representation."""
return f"MiniMaxEmbeddings(model={self.model})"
6 changes: 6 additions & 0 deletions src/chonkie/embeddings/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from .gemini import GeminiEmbeddings
from .jina import JinaEmbeddings
from .litellm import LiteLLMEmbeddings
from .minimax import MiniMaxEmbeddings
from .mistral import MistralEmbeddings
from .mixedbread import MixedbreadEmbeddings
from .model2vec import Model2VecEmbeddings
Expand Down Expand Up @@ -307,5 +308,10 @@ def wrap(cls, object: Any, **kwargs: Any) -> BaseEmbeddings:
# Register Cloudflare embeddings
EmbeddingsRegistry.register_provider("cloudflare", CloudflareEmbeddings)

# Register MiniMax embeddings
EmbeddingsRegistry.register_provider("minimax", MiniMaxEmbeddings)
EmbeddingsRegistry.register_pattern(r"^embo-", MiniMaxEmbeddings)
EmbeddingsRegistry.register_model("embo-01", MiniMaxEmbeddings)

# Register LiteLLM embeddings
EmbeddingsRegistry.register_provider("litellm", LiteLLMEmbeddings)
2 changes: 2 additions & 0 deletions src/chonkie/genie/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from .cerebras import CerebrasGenie
from .gemini import GeminiGenie
from .groq import GroqGenie
from .minimax import MiniMaxGenie
from .openai import OpenAIGenie

# Add all genie classes to __all__
Expand All @@ -14,5 +15,6 @@
"CerebrasGenie",
"GeminiGenie",
"GroqGenie",
"MiniMaxGenie",
"OpenAIGenie",
]
Loading