From d085dea770cb0153d3d63c36d8c0b0aae9dd8a97 Mon Sep 17 00:00:00 2001 From: "xiaogang.zhou" Date: Sun, 15 Mar 2026 15:46:36 +0800 Subject: [PATCH 1/2] feat: enable minimax embedding and adapt to separate query/document parameter configuration --- docs/en/guides/01-configuration.md | 21 ++ docs/zh/guides/01-configuration.md | 22 +++ openviking/models/embedder/__init__.py | 3 + .../models/embedder/minimax_embedders.py | 182 ++++++++++++++++++ .../utils/config/embedding_config.py | 29 ++- tests/unit/test_minimax_embedder_simple.py | 76 ++++++++ 6 files changed, 331 insertions(+), 2 deletions(-) create mode 100644 openviking/models/embedder/minimax_embedders.py create mode 100644 tests/unit/test_minimax_embedder_simple.py diff --git a/docs/en/guides/01-configuration.md b/docs/en/guides/01-configuration.md index 7c2779b6..7d08dee2 100644 --- a/docs/en/guides/01-configuration.md +++ b/docs/en/guides/01-configuration.md @@ -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:** diff --git a/docs/zh/guides/01-configuration.md b/docs/zh/guides/01-configuration.md index 889c737a..4b6eb862 100644 --- a/docs/zh/guides/01-configuration.md +++ b/docs/zh/guides/01-configuration.md @@ -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 配置示例:** diff --git a/openviking/models/embedder/__init__.py b/openviking/models/embedder/__init__.py index b418b809..682755f1 100644 --- a/openviking/models/embedder/__init__.py +++ b/openviking/models/embedder/__init__.py @@ -24,6 +24,7 @@ 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 ( @@ -47,6 +48,8 @@ "CompositeHybridEmbedder", # Jina AI implementations "JinaDenseEmbedder", + # MiniMax implementations + "MinimaxDenseEmbedder", # OpenAI implementations "OpenAIDenseEmbedder", # Voyage implementations diff --git a/openviking/models/embedder/minimax_embedders.py b/openviking/models/embedder/minimax_embedders.py new file mode 100644 index 00000000..3c5e2194 --- /dev/null +++ b/openviking/models/embedder/minimax_embedders.py @@ -0,0 +1,182 @@ +# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd. +# SPDX-License-Identifier: Apache-2.0 +"""MiniMax Embedder Implementation via HTTP API""" + +import time +from typing import Any, Dict, List, Optional, Union + +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 diff --git a/openviking_cli/utils/config/embedding_config.py b/openviking_cli/utils/config/embedding_config.py index 3f504d40..3950561b 100644 --- a/openviking_cli/utils/config/embedding_config.py +++ b/openviking_cli/utils/config/embedding_config.py @@ -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 @@ -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: @@ -205,6 +217,7 @@ def _create_embedder( """ from openviking.models.embedder import ( JinaDenseEmbedder, + MinimaxDenseEmbedder, OpenAIDenseEmbedder, VikingDBDenseEmbedder, VikingDBHybridEmbedder, @@ -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) diff --git a/tests/unit/test_minimax_embedder_simple.py b/tests/unit/test_minimax_embedder_simple.py new file mode 100644 index 00000000..c47fd0d1 --- /dev/null +++ b/tests/unit/test_minimax_embedder_simple.py @@ -0,0 +1,76 @@ +# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd. +# SPDX-License-Identifier: Apache-2.0 +"""Tests for MiniMax Embedder (Simple)""" + +import unittest +from unittest.mock import MagicMock, patch +import sys +import os + +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(f"\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() From 3bbe016eb87d251aa4af222b126cfe40b074e4bf Mon Sep 17 00:00:00 2001 From: "xiaogang.zhou" Date: Wed, 18 Mar 2026 21:38:13 +0800 Subject: [PATCH 2/2] feat: enable minimax embedding and adapt to separate query/document parameter configuration --- openviking/models/embedder/__init__.py | 2 +- openviking/models/embedder/minimax_embedders.py | 3 +-- tests/unit/test_minimax_embedder_simple.py | 6 ++---- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/openviking/models/embedder/__init__.py b/openviking/models/embedder/__init__.py index 682755f1..c7a670d7 100644 --- a/openviking/models/embedder/__init__.py +++ b/openviking/models/embedder/__init__.py @@ -26,7 +26,6 @@ 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, @@ -37,6 +36,7 @@ VolcengineHybridEmbedder, VolcengineSparseEmbedder, ) +from openviking.models.embedder.voyage_embedders import VoyageDenseEmbedder __all__ = [ # Base classes diff --git a/openviking/models/embedder/minimax_embedders.py b/openviking/models/embedder/minimax_embedders.py index 3c5e2194..faec9b91 100644 --- a/openviking/models/embedder/minimax_embedders.py +++ b/openviking/models/embedder/minimax_embedders.py @@ -2,8 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 """MiniMax Embedder Implementation via HTTP API""" -import time -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Optional import requests from requests.adapters import HTTPAdapter diff --git a/tests/unit/test_minimax_embedder_simple.py b/tests/unit/test_minimax_embedder_simple.py index c47fd0d1..5602e2cb 100644 --- a/tests/unit/test_minimax_embedder_simple.py +++ b/tests/unit/test_minimax_embedder_simple.py @@ -2,10 +2,8 @@ # SPDX-License-Identifier: Apache-2.0 """Tests for MiniMax Embedder (Simple)""" -import unittest -from unittest.mock import MagicMock, patch -import sys import os +import unittest from openviking.models.embedder.minimax_embedders import MinimaxDenseEmbedder from openviking_cli.utils.config.embedding_config import EmbeddingModelConfig @@ -24,7 +22,7 @@ def setUp(self): def test_real_embedding(self): """Test real embedding call to MiniMax API""" - print(f"\n[Real API] Testing MiniMax Embedder (embo-01)") + print("\n[Real API] Testing MiniMax Embedder (embo-01)") embedder = MinimaxDenseEmbedder( model_name="embo-01",