Skip to content
Merged
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
21 changes: 21 additions & 0 deletions docs/en/guides/01-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,27 @@ With `input: "multimodal"`, OpenViking can embed text, images (PNG, JPG, etc.),
- `vikingdb`: VikingDB Embedding API
- `jina`: Jina AI Embedding API
- `voyage`: Voyage AI Embedding API
- `minimax`: MiniMax Embedding API

**minimax provider example:**

```json
{
"embedding": {
"dense": {
"provider": "minimax",
"api_key": "your-minimax-api-key",
"model": "embo-01",
"dimension": 1536,
"query_param": "query",
"document_param": "db",
"extra_headers": {
"GroupId": "your-group-id"
}
}
}
}
```

**vikingdb provider example:**

Expand Down
22 changes: 22 additions & 0 deletions docs/zh/guides/01-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,28 @@ OpenViking 使用 JSON 配置文件(`ov.conf`)进行设置。配置文件支
- `volcengine`: 火山引擎 Embedding API
- `vikingdb`: VikingDB Embedding API
- `jina`: Jina AI Embedding API
- `voyage`: Voyage AI Embedding API
- `minimax`: MiniMax Embedding API

**minimax provider 配置示例:**

```json
{
"embedding": {
"dense": {
"provider": "minimax",
"api_key": "your-minimax-api-key",
"model": "embo-01",
"dimension": 1536,
"query_param": "query",
"document_param": "db",
"extra_headers": {
"GroupId": "your-group-id"
}
}
}
}
```

**vikingdb provider 配置示例:**

Expand Down
5 changes: 4 additions & 1 deletion openviking/models/embedder/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@
SparseEmbedderBase,
)
from openviking.models.embedder.jina_embedders import JinaDenseEmbedder
from openviking.models.embedder.minimax_embedders import MinimaxDenseEmbedder
from openviking.models.embedder.openai_embedders import OpenAIDenseEmbedder
from openviking.models.embedder.voyage_embedders import VoyageDenseEmbedder
from openviking.models.embedder.vikingdb_embedders import (
VikingDBDenseEmbedder,
VikingDBHybridEmbedder,
Expand All @@ -36,6 +36,7 @@
VolcengineHybridEmbedder,
VolcengineSparseEmbedder,
)
from openviking.models.embedder.voyage_embedders import VoyageDenseEmbedder

__all__ = [
# Base classes
Expand All @@ -47,6 +48,8 @@
"CompositeHybridEmbedder",
# Jina AI implementations
"JinaDenseEmbedder",
# MiniMax implementations
"MinimaxDenseEmbedder",
# OpenAI implementations
"OpenAIDenseEmbedder",
# Voyage implementations
Expand Down
181 changes: 181 additions & 0 deletions openviking/models/embedder/minimax_embedders.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd.
# SPDX-License-Identifier: Apache-2.0
"""MiniMax Embedder Implementation via HTTP API"""

from typing import Any, Dict, List, Optional

import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

from openviking.models.embedder.base import DenseEmbedderBase, EmbedResult
from openviking_cli.utils.logger import default_logger as logger


class MinimaxDenseEmbedder(DenseEmbedderBase):
"""MiniMax Dense Embedder Implementation

Supports MiniMax embedding models via official HTTP API.
API Docs: https://platform.minimaxi.com/docs/api-reference/api-overview

Example:
>>> embedder = MinimaxDenseEmbedder(
... model_name="embo-01",
... api_key="your-api-key",
... group_id="your-group-id",
... type="db" # or "query"
... )
"""

DEFAULT_API_BASE = "https://api.minimax.chat/v1/embeddings"
DEFAULT_MODEL = "embo-01"

def __init__(
self,
model_name: str = DEFAULT_MODEL,
api_key: Optional[str] = None,
api_base: Optional[str] = None,
dimension: Optional[int] = None,
query_param: Optional[str] = None,
document_param: Optional[str] = None,
config: Optional[Dict[str, Any]] = None,
extra_headers: Optional[Dict[str, str]] = None,
):
"""Initialize MiniMax Dense Embedder

Args:
model_name: Model name, defaults to embo-01
api_key: API key
api_base: API base URL, defaults to https://api.minimax.chat/v1/embeddings
dimension: Dimension (Optional, MiniMax embo-01 is usually 1536 but docs don't specify, we'll detect)
query_param: Type for query-side embeddings. Default: "query" if not provided.
document_param: Type for document-side embeddings. Default: "db" if not provided.
config: Additional configuration dict
extra_headers: Extra headers, useful for passing GroupId for MiniMax API
"""
super().__init__(model_name, config)

self.api_key = api_key
self.api_base = api_base or self.DEFAULT_API_BASE
self.query_param = query_param
self.document_param = document_param
self._dimension = dimension

# Get group_id from extra_headers if present, since MiniMax API may require it
self.group_id = None
self.extra_headers = {}
if extra_headers:
self.extra_headers = extra_headers
# Case-insensitive extraction of GroupId
for k, v in extra_headers.items():
if k.lower() == "groupid" or k.lower() == "group_id":
self.group_id = v
break

if not self.api_key:
raise ValueError("api_key is required for MiniMax embedder")

# Initialize session with retry logic
self.session = self._create_session()

# Auto-detect dimension if not provided
if self._dimension is None:
try:
self._dimension = self._detect_dimension()
except Exception as e:
logger.warning(f"Failed to detect MiniMax dimension: {e}. Defaulting to 1536.")
self._dimension = 1536

def _create_session(self) -> requests.Session:
"""Create a requests session with retry logic"""
session = requests.Session()
retry_strategy = Retry(
total=6,
backoff_factor=1, # 1s, 2s, 4s, 8s, 16s, 32s
status_forcelist=[429, 500, 502, 503, 504],
allowed_methods=["POST"],
)
adapter = HTTPAdapter(max_retries=retry_strategy)
session.mount("https://", adapter)
session.mount("http://", adapter)
return session

def _detect_dimension(self) -> int:
"""Detect dimension by making an actual API call"""
result = self.embed("test")
return len(result.dense_vector) if result.dense_vector else 1536

def _call_api(self, texts: List[str], is_query: bool = False) -> List[List[float]]:
"""Call MiniMax API"""
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json",
}

# Merge extra headers
if self.extra_headers:
for k, v in self.extra_headers.items():
if k.lower() not in ["authorization", "content-type", "groupid", "group_id"]:
headers[k] = v

params = {}
if self.group_id:
params["GroupId"] = self.group_id

embed_type = "db"
if is_query:
embed_type = self.query_param if self.query_param is not None else "query"
else:
embed_type = self.document_param if self.document_param is not None else "db"

payload = {
"model": self.model_name,
"type": embed_type,
"texts": texts,
}

try:
response = self.session.post(
self.api_base,
headers=headers,
params=params,
json=payload,
timeout=60, # 60s timeout
)
response.raise_for_status()
data = response.json()

# Check for business error code
base_resp = data.get("base_resp", {})
if base_resp.get("status_code") != 0:
raise RuntimeError(f"MiniMax API error: {base_resp.get('status_msg')}")

vectors = data.get("vectors", [])
if not vectors:
raise RuntimeError("MiniMax API returned empty vectors")

return vectors

except requests.exceptions.RequestException as e:
raise RuntimeError(f"MiniMax network error: {str(e)}") from e
except Exception as e:
raise RuntimeError(f"MiniMax embedding failed: {str(e)}") from e

def embed(self, text: str, is_query: bool = False) -> EmbedResult:
"""Perform dense embedding on text"""
vectors = self._call_api([text], is_query=is_query)
return EmbedResult(dense_vector=vectors[0])

def embed_batch(self, texts: List[str], is_query: bool = False) -> List[EmbedResult]:
"""Batch embedding"""
if not texts:
return []

# MiniMax might have batch size limits, but let's assume the caller handles batching or use safe defaults
# For now, we pass through. If needed, we can implement internal chunking.
vectors = self._call_api(texts, is_query=is_query)
return [EmbedResult(dense_vector=v) for v in vectors]

def get_dimension(self) -> int:
"""Get embedding dimension"""
return self._dimension
29 changes: 27 additions & 2 deletions openviking_cli/utils/config/embedding_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,18 @@ def validate_config(self):
if not self.provider:
raise ValueError("Embedding provider is required")

if self.provider not in ["openai", "volcengine", "vikingdb", "jina", "ollama", "voyage"]:
if self.provider not in [
"openai",
"volcengine",
"vikingdb",
"jina",
"ollama",
"voyage",
"minimax",
]:
raise ValueError(
f"Invalid embedding provider: '{self.provider}'. Must be one of: "
"'openai', 'volcengine', 'vikingdb', 'jina', 'ollama', 'voyage'"
"'openai', 'volcengine', 'vikingdb', 'jina', 'ollama', 'voyage', 'minimax'"
)

# Provider-specific validation
Expand Down Expand Up @@ -135,6 +143,10 @@ def validate_config(self):
if not self.api_key:
raise ValueError("Voyage provider requires 'api_key' to be set")

elif self.provider == "minimax":
if not self.api_key:
raise ValueError("MiniMax provider requires 'api_key' to be set")

return self

def get_effective_dimension(self) -> int:
Expand Down Expand Up @@ -205,6 +217,7 @@ def _create_embedder(
"""
from openviking.models.embedder import (
JinaDenseEmbedder,
MinimaxDenseEmbedder,
OpenAIDenseEmbedder,
VikingDBDenseEmbedder,
VikingDBHybridEmbedder,
Expand Down Expand Up @@ -328,6 +341,18 @@ def _create_embedder(
"dimension": cfg.dimension,
},
),
("minimax", "dense"): (
MinimaxDenseEmbedder,
lambda cfg: {
"model_name": cfg.model,
"api_key": cfg.api_key,
"api_base": cfg.api_base,
"dimension": cfg.dimension,
**({"query_param": cfg.query_param} if cfg.query_param else {}),
**({"document_param": cfg.document_param} if cfg.document_param else {}),
**({"extra_headers": cfg.extra_headers} if cfg.extra_headers else {}),
},
),
}

key = (provider, embedder_type)
Expand Down
74 changes: 74 additions & 0 deletions tests/unit/test_minimax_embedder_simple.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd.
# SPDX-License-Identifier: Apache-2.0
"""Tests for MiniMax Embedder (Simple)"""

import os
import unittest

from openviking.models.embedder.minimax_embedders import MinimaxDenseEmbedder
from openviking_cli.utils.config.embedding_config import EmbeddingModelConfig


class TestMinimaxRealCall(unittest.TestCase):
"""Test cases for MinimaxDenseEmbedder with REAL API calls"""

def setUp(self):
# Retrieve API key and Group ID from environment variables
self.api_key = os.environ.get("MINIMAX_API_KEY")
self.group_id = os.environ.get("MINIMAX_GROUP_ID")

if not self.api_key:
self.skipTest("MINIMAX_API_KEY not set")

def test_real_embedding(self):
"""Test real embedding call to MiniMax API"""
print("\n[Real API] Testing MiniMax Embedder (embo-01)")

embedder = MinimaxDenseEmbedder(
model_name="embo-01",
api_key=self.api_key,
extra_headers={"GroupId": self.group_id} if self.group_id else None,
document_param="db",
)

text = "OpenViking integration test for MiniMax."

try:
result = embedder.embed(text)

# Verify result
self.assertIsNotNone(result.dense_vector)
dim = len(result.dense_vector)

self.assertEqual(dim, 1536, "Expected dimension 1536")

except Exception as e:
self.fail(f"Real API call failed: {e}")


class TestEmbeddingModelConfig(unittest.TestCase):
def test_minimax_provider_valid(self):
config = EmbeddingModelConfig(provider="minimax", model="embo-01", api_key="test-key")
self.assertEqual(config.provider, "minimax")
self.assertEqual(config.model, "embo-01")

def test_minimax_provider_requires_api_key(self):
with self.assertRaisesRegex(ValueError, "MiniMax provider requires 'api_key'"):
EmbeddingModelConfig(provider="minimax", model="embo-01")

def test_extra_headers_and_param_fields(self):
config = EmbeddingModelConfig(
provider="minimax",
model="embo-01",
api_key="test-key",
extra_headers={"GroupId": "group-123"},
query_param="query",
document_param="db",
)
self.assertEqual(config.extra_headers, {"GroupId": "group-123"})
self.assertEqual(config.query_param, "query")
self.assertEqual(config.document_param, "db")


if __name__ == "__main__":
unittest.main()
Loading