Compare commits

..
Author SHA1 Message Date
kartik-mem0 bd401dcae7 Merge branch 'main' into fix/vertexai-py-batch-size 2026-08-14 17:22:49 +05:30
kartik-mem0 493a62cb03 fix(embeddings): size Vertex AI batches by model family
gemini-embedding-* accepts one input text per predict request, but
embed_batch() chunked at 250 for every model while defaulting to
gemini-embedding-001. Derive the chunk size from the model instead.
2026-07-09 21:30:39 +05:30
9 changed files with 46 additions and 154 deletions
-5
View File
@@ -20,8 +20,6 @@ class BaseEmbedderConfig(ABC):
ollama_base_url: Optional[str] = None,
# Openai specific
openai_base_url: Optional[str] = None,
# Together specific
together_base_url: Optional[str] = None,
# Huggingface specific
model_kwargs: Optional[dict] = None,
huggingface_base_url: Optional[str] = None,
@@ -60,8 +58,6 @@ class BaseEmbedderConfig(ABC):
:type huggingface_base_url: Optional[str], optional
:param openai_base_url: Openai base URL to be use, defaults to "https://api.openai.com/v1"
:type openai_base_url: Optional[str], optional
:param together_base_url: Together base URL to be use, defaults to None
:type together_base_url: Optional[str], optional
:param azure_kwargs: key-value arguments for the AzureOpenAI embedding model, defaults a dict inside init
:type azure_kwargs: Optional[Dict[str, Any]], defaults a dict inside init
:param http_client_proxies: The proxy server settings used to create self.http_client, defaults to None
@@ -81,7 +77,6 @@ class BaseEmbedderConfig(ABC):
self.model = model
self.api_key = api_key
self.openai_base_url = openai_base_url
self.together_base_url = together_base_url
self.embedding_dims = embedding_dims
# AzureOpenAI specific
-56
View File
@@ -1,56 +0,0 @@
from typing import Optional
from mem0.configs.llms.base import BaseLlmConfig
class TogetherConfig(BaseLlmConfig):
"""
Configuration class for Together-specific parameters.
Inherits from BaseLlmConfig and adds Together-specific settings.
"""
def __init__(
self,
# Base parameters
model: Optional[str] = None,
temperature: float = 0.1,
api_key: Optional[str] = None,
max_tokens: int = 2000,
top_p: float = 0.1,
top_k: int = 1,
enable_vision: bool = False,
vision_details: Optional[str] = "auto",
http_client_proxies: Optional[dict] = None,
# Together-specific parameters
together_base_url: Optional[str] = None,
):
"""
Initialize Together configuration.
Args:
model: Together model to use, defaults to None
temperature: Controls randomness, defaults to 0.1
api_key: Together API key, defaults to None
max_tokens: Maximum tokens to generate, defaults to 2000
top_p: Nucleus sampling parameter, defaults to 0.1
top_k: Top-k sampling parameter, defaults to 1
enable_vision: Enable vision capabilities, defaults to False
vision_details: Vision detail level, defaults to "auto"
http_client_proxies: HTTP client proxy settings, defaults to None
together_base_url: Together API base URL, defaults to None
"""
# Initialize base parameters
super().__init__(
model=model,
temperature=temperature,
api_key=api_key,
max_tokens=max_tokens,
top_p=top_p,
top_k=top_k,
enable_vision=enable_vision,
vision_details=vision_details,
http_client_proxies=http_client_proxies,
)
# Together-specific parameters
self.together_base_url = together_base_url
+1 -2
View File
@@ -13,9 +13,8 @@ class TogetherEmbedding(EmbeddingBase):
self.config.model = self.config.model or "intfloat/multilingual-e5-large-instruct"
api_key = self.config.api_key or os.getenv("TOGETHER_API_KEY")
base_url = self.config.together_base_url or os.getenv("TOGETHER_API_BASE") or "https://api.together.ai/v1"
self.config.embedding_dims = self.config.embedding_dims or 1024
self.client = Together(api_key=api_key, base_url=base_url)
self.client = Together(api_key=api_key)
def embed(self, text, memory_action: Optional[Literal["add", "search", "update"]] = None):
"""
+11 -2
View File
@@ -8,6 +8,14 @@ from mem0.embeddings.base import EmbeddingBase
from mem0.utils.gcp_auth import GCPAuthenticator
def _max_instances_per_request(model: str) -> int:
# Vertex AI's per-request input cap depends on the model family: gemini-embedding-*
# accepts exactly one input text per predict() call, while text-embedding-004/005 and
# text-multilingual-embedding-002 accept up to 250.
# https://docs.cloud.google.com/vertex-ai/docs/samples/aiplatform-sdk-embedding
return 1 if model.startswith("gemini-embedding") else 250
class VertexAIEmbedding(EmbeddingBase):
def __init__(self, config: Optional[BaseEmbedderConfig] = None):
super().__init__(config)
@@ -72,8 +80,9 @@ class VertexAIEmbedding(EmbeddingBase):
raise ValueError(f"Invalid memory action: {memory_action}")
embedding_type = self.embedding_types[memory_action]
all_embeddings = []
for i in range(0, len(texts), 250):
chunk = texts[i : i + 250]
chunk_size = _max_instances_per_request(self.config.model)
for i in range(0, len(texts), chunk_size):
chunk = texts[i : i + chunk_size]
inputs = [TextEmbeddingInput(text=t, task_type=embedding_type) for t in chunk]
results = self.model.get_embeddings(texts=inputs, output_dimensionality=self.config.embedding_dims)
all_embeddings.extend(r.values for r in results)
+3 -24
View File
@@ -1,6 +1,6 @@
import json
import os
from typing import Dict, List, Optional, Union
from typing import Dict, List, Optional
try:
from together import Together
@@ -8,40 +8,19 @@ except ImportError:
raise ImportError("The 'together' library is required. Please install it using 'pip install together'.")
from mem0.configs.llms.base import BaseLlmConfig
from mem0.configs.llms.together import TogetherConfig
from mem0.llms.base import LLMBase
from mem0.memory.utils import extract_json
class TogetherLLM(LLMBase):
def __init__(self, config: Optional[Union[BaseLlmConfig, TogetherConfig, Dict]] = None):
# Convert to TogetherConfig if needed
if config is None:
config = TogetherConfig()
elif isinstance(config, dict):
config = TogetherConfig(**config)
elif isinstance(config, BaseLlmConfig) and not isinstance(config, TogetherConfig):
# Convert BaseLlmConfig to TogetherConfig
config = TogetherConfig(
model=config.model,
temperature=config.temperature,
api_key=config.api_key,
max_tokens=config.max_tokens,
top_p=config.top_p,
top_k=config.top_k,
enable_vision=config.enable_vision,
vision_details=config.vision_details,
http_client_proxies=config.http_client_proxies,
)
def __init__(self, config: Optional[BaseLlmConfig] = None):
super().__init__(config)
if not self.config.model:
self.config.model = "MiniMaxAI/MiniMax-M3"
api_key = self.config.api_key or os.getenv("TOGETHER_API_KEY")
base_url = self.config.together_base_url or os.getenv("TOGETHER_API_BASE") or "https://api.together.ai/v1"
self.client = Together(api_key=api_key, base_url=base_url)
self.client = Together(api_key=api_key)
def _parse_response(self, response, tools):
"""
+1 -2
View File
@@ -13,7 +13,6 @@ from mem0.configs.llms.lmstudio import LMStudioConfig
from mem0.configs.llms.minimax import MinimaxConfig
from mem0.configs.llms.ollama import OllamaConfig
from mem0.configs.llms.openai import OpenAIConfig
from mem0.configs.llms.together import TogetherConfig
from mem0.configs.llms.vllm import VllmConfig
from mem0.configs.llms.xai import XAIConfig
from mem0.configs.rerankers.base import BaseRerankerConfig
@@ -44,7 +43,7 @@ class LlmFactory:
"ollama": ("mem0.llms.ollama.OllamaLLM", OllamaConfig),
"openai": ("mem0.llms.openai.OpenAILLM", OpenAIConfig),
"groq": ("mem0.llms.groq.GroqLLM", BaseLlmConfig),
"together": ("mem0.llms.together.TogetherLLM", TogetherConfig),
"together": ("mem0.llms.together.TogetherLLM", BaseLlmConfig),
"aws_bedrock": ("mem0.llms.aws_bedrock.AWSBedrockLLM", AWSBedrockConfig),
"litellm": ("mem0.llms.litellm.LiteLLM", BaseLlmConfig),
"azure_openai": ("mem0.llms.azure_openai.AzureOpenAILLM", AzureOpenAIConfig),
@@ -85,29 +85,3 @@ def test_explicit_config_overrides_defaults(mock_together_client):
mock_together_client.embeddings.create.return_value = Mock(data=[Mock(embedding=[0.0] * 768)])
embedder.embed("hello")
mock_together_client.embeddings.create.assert_called_once_with(model="BAAI/bge-base-en-v1.5", input="hello")
def test_together_embedding_base_url(monkeypatch):
# case1: default config uses Together's official base url
monkeypatch.delenv("TOGETHER_API_BASE", raising=False)
config = BaseEmbedderConfig(model=DEFAULT_MODEL, embedding_dims=DEFAULT_EMBEDDING_DIMS, api_key="api_key")
embedder = TogetherEmbedding(config)
assert str(embedder.client.base_url) == "https://api.together.ai/v1/"
# case2: with env variable TOGETHER_API_BASE
provider_base_url = "https://api.provider.com/v1/"
monkeypatch.setenv("TOGETHER_API_BASE", provider_base_url)
config = BaseEmbedderConfig(model=DEFAULT_MODEL, embedding_dims=DEFAULT_EMBEDDING_DIMS, api_key="api_key")
embedder = TogetherEmbedding(config)
assert str(embedder.client.base_url) == provider_base_url
# case3: with config.together_base_url (explicit config beats env var)
config_base_url = "https://api.config.com/v1/"
config = BaseEmbedderConfig(
model=DEFAULT_MODEL,
embedding_dims=DEFAULT_EMBEDDING_DIMS,
api_key="api_key",
together_base_url=config_base_url,
)
embedder = TogetherEmbedding(config)
assert str(embedder.client.base_url) == config_base_url
+30 -4
View File
@@ -163,7 +163,9 @@ def test_invalid_memory_action(mock_text_embedding_model, mock_config):
@patch("mem0.embeddings.vertexai.TextEmbeddingModel")
def test_embed_batch_single_call(mock_text_embedding_model, mock_os_environ, mock_config):
mock_config.return_value.model = "gemini-embedding-001"
# text-embedding-005 accepts up to 250 inputs per request, so 2 texts fit in one call.
# (gemini-embedding-* only accepts 1 input per request; see test_embed_batch_default_model_sends_one_text_per_call.)
mock_config.return_value.model = "text-embedding-005"
mock_config.return_value.embedding_dims = 256
config = mock_config()
@@ -198,7 +200,9 @@ def test_embed_batch_empty_list(mock_text_embedding_model, mock_os_environ, mock
@patch("mem0.embeddings.vertexai.TextEmbeddingModel")
def test_embed_batch_count_mismatch_raises(mock_text_embedding_model, mock_os_environ, mock_config):
mock_config.return_value.model = "gemini-embedding-001"
# Use a 250-cap model so both texts land in a single chunk/call, keeping the mismatch
# (1 embedding returned for that one call) triggered by that single call.
mock_config.return_value.model = "text-embedding-005"
mock_config.return_value.embedding_dims = 256
config = mock_config()
@@ -261,8 +265,8 @@ def test_embed_batch_invalid_memory_action_raises(mock_text_embedding_model, moc
@patch("mem0.embeddings.vertexai.TextEmbeddingModel")
def test_embed_batch_chunking_triggers_two_api_calls(mock_text_embedding_model, mock_os_environ, mock_config):
"""300 texts must produce exactly 2 get_embeddings calls (chunks of 250 and 50)."""
mock_config.return_value.model = "gemini-embedding-001"
"""text-embedding-005 accepts up to 250 inputs/request: 300 texts -> 2 calls (chunks of 250 and 50)."""
mock_config.return_value.model = "text-embedding-005"
mock_config.return_value.embedding_dims = 256
config = mock_config()
@@ -278,3 +282,25 @@ def test_embed_batch_chunking_triggers_two_api_calls(mock_text_embedding_model,
assert mock_text_embedding_model.from_pretrained.return_value.get_embeddings.call_count == 2
assert len(result) == 300
@patch("mem0.embeddings.vertexai.TextEmbeddingModel")
def test_embed_batch_default_model_sends_one_text_per_call(mock_text_embedding_model, mock_os_environ, mock_config):
"""gemini-embedding-001 (default) accepts 1 input/request: 300 texts -> 300 calls of 1 text each."""
mock_config.return_value.model = "gemini-embedding-001"
mock_config.return_value.embedding_dims = 256
config = mock_config()
embedder = VertexAIEmbedding(config)
def make_single_response(texts, output_dimensionality):
assert len(texts) == 1
return [Mock(values=[0.1, 0.2]) for _ in texts]
mock_text_embedding_model.from_pretrained.return_value.get_embeddings.side_effect = make_single_response
texts = [f"text {i}" for i in range(300)]
result = embedder.embed_batch(texts)
assert mock_text_embedding_model.from_pretrained.return_value.get_embeddings.call_count == 300
assert len(result) == 300
-33
View File
@@ -3,7 +3,6 @@ from unittest.mock import Mock, patch
import pytest
from mem0.configs.llms.base import BaseLlmConfig
from mem0.configs.llms.together import TogetherConfig
from mem0.llms.together import TogetherLLM
@@ -15,38 +14,6 @@ def mock_together_client():
yield mock_client
def test_together_llm_base_url(monkeypatch):
# case1: default config uses Together's official base url
monkeypatch.delenv("TOGETHER_API_BASE", raising=False)
config = BaseLlmConfig(
model="mistralai/Mixtral-8x7B-Instruct-v0.1", temperature=0.7, max_tokens=100, top_p=1.0, api_key="api_key"
)
llm = TogetherLLM(config)
assert str(llm.client.base_url) == "https://api.together.ai/v1/"
# case2: with env variable TOGETHER_API_BASE
provider_base_url = "https://api.provider.com/v1/"
monkeypatch.setenv("TOGETHER_API_BASE", provider_base_url)
config = TogetherConfig(
model="mistralai/Mixtral-8x7B-Instruct-v0.1", temperature=0.7, max_tokens=100, top_p=1.0, api_key="api_key"
)
llm = TogetherLLM(config)
assert str(llm.client.base_url) == provider_base_url
# case3: with config.together_base_url (explicit config beats env var)
config_base_url = "https://api.config.com/v1/"
config = TogetherConfig(
model="mistralai/Mixtral-8x7B-Instruct-v0.1",
temperature=0.7,
max_tokens=100,
top_p=1.0,
api_key="api_key",
together_base_url=config_base_url,
)
llm = TogetherLLM(config)
assert str(llm.client.base_url) == config_base_url
def test_generate_response_without_tools(mock_together_client):
config = BaseLlmConfig(model="mistralai/Mixtral-8x7B-Instruct-v0.1", temperature=0.7, max_tokens=100, top_p=1.0)
llm = TogetherLLM(config)