Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,21 @@ v = Vektori(embedding_model="bge:BAAI/bge-m3")
v = Vektori(extraction_model="litellm:groq/llama3-8b-8192")
```

## NVIDIA NIM - GPU-optimized models via [NVIDIA NIM](https://build.nvidia.com).
```python
# NVIDIA embedding models (Matryoshka: 384-2048 dimensions)
v = Vektori(
embedding_model="nvidia:llama-nemotron-embed-1b-v2",
embedding_dimension=1024, # Optional: 384, 512, 768, 1024, or 2048
Comment on lines +303 to +306

Copilot AI Apr 13, 2026

Copy link

Choose a reason for hiding this comment

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

The README suggests embedding_dimension=1024 will control NVIDIA Matryoshka output size, but the current code path (Vektori -> create_embedder) doesn’t pass embedding_dimension into NvidiaEmbedder(dimensions=...). Either update the wiring so this example works as written, or adjust the docs to show how to set dimensions (e.g., via provider kwargs/config).

Suggested change
# NVIDIA embedding models (Matryoshka: 384-2048 dimensions)
v = Vektori(
embedding_model="nvidia:llama-nemotron-embed-1b-v2",
embedding_dimension=1024, # Optional: 384, 512, 768, 1024, or 2048
# NVIDIA embedding models
# Note: llama-nemotron-embed-1b-v2 supports Matryoshka output sizes,
# but `embedding_dimension=` is not currently forwarded by this Vektori code path.
v = Vektori(
embedding_model="nvidia:llama-nemotron-embed-1b-v2",

Copilot uses AI. Check for mistakes.
)

# NVIDIA LLM models (nvidia/ prefix auto-added)
v = Vektori(extraction_model="nvidia:llama-3.3-nemotron-super-49b-v1")

# Third-party models hosted on NVIDIA NIM (use full path)
v = Vektori(extraction_model="nvidia:z-ai/glm5")

```
---

## Why Not Mem0 / Zep?
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ anthropic = ["anthropic>=0.20", "voyageai>=0.2"]
sentence-transformers = ["sentence-transformers>=2.6"]
bge = ["FlagEmbedding>=1.2"]
litellm = ["litellm>=1.30"]
nvidia = ["openai>=1.12"] # NVIDIA NIM uses OpenAI-compatible API
Comment on lines 35 to +43

Copilot AI Apr 13, 2026

Copy link

Choose a reason for hiding this comment

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

openai>=1.12 is already a required base dependency, so adding it again under the nvidia extra (and again in the all extra) is redundant and can mislead readers into thinking OpenAI is optional unless .[nvidia] is installed. Consider removing the nvidia extra entirely (or, if the goal is to make OpenAI optional, moving it out of base deps and updating providers accordingly).

Copilot uses AI. Check for mistakes.
dev = [
"pytest>=8.0",
"pytest-asyncio>=0.23",
Expand All @@ -57,6 +58,7 @@ all = [
"sentence-transformers>=2.6",
"FlagEmbedding>=1.2",
"litellm>=1.30",
"openai>=1.12", # NVIDIA NIM support
]

[project.scripts]
Expand Down
7 changes: 7 additions & 0 deletions tests/unit/test_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,10 @@ def test_create_anthropic_llm():
def test_unknown_llm_provider_raises():
with pytest.raises(ValueError, match="Unknown LLM provider"):
create_llm("nonexistent:model")

def test_create_nvidia_embedder_default_model():
from vektori.models.nvidia import NvidiaEmbedder, DEFAULT_EMBEDDING_MODEL

embedder = create_embedder("nvidia")
assert isinstance(embedder, NvidiaEmbedder)
assert embedder.model == DEFAULT_EMBEDDING_MODEL # "nvidia/llama-nemotron-embed-1b-v2"

Copilot AI Apr 13, 2026

Copy link

Choose a reason for hiding this comment

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

The PR description claims “comprehensive NVIDIA tests” (including integration tests and Matryoshka dimension verification), but in this PR’s changes the only NVIDIA test added is a factory default-model check. Either add the missing unit/integration tests that exercise the new provider behavior, or update the PR description/testing notes to match what’s actually included.

Copilot uses AI. Check for mistakes.
2 changes: 2 additions & 0 deletions vektori/models/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"cloudflare": "vektori.models.cloudflare.CloudflareEmbedder",
# LiteLLM: 100+ providers — Together AI, Cohere, Azure, Ollama, etc.
"litellm": "vektori.models.litellm_embedder.LiteLLMEmbedder",
"nvidia": "vektori.models.nvidia.NvidiaEmbedder",
}

LLM_REGISTRY: dict[str, str] = {
Expand All @@ -31,6 +32,7 @@
"gemini": "vektori.models.gemini.GeminiLLM", # Direct Gemini API
# LiteLLM: single interface for 100+ providers — recommended for extraction
"litellm": "vektori.models.litellm_provider.LiteLLMProvider",
"nvidia": "vektori.models.nvidia.NvidiaLLM",
}


Expand Down
301 changes: 301 additions & 0 deletions vektori/models/nvidia.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
"""NVIDIA NIM embedding and LLM providers.

NVIDIA NIM (NVIDIA Inference Microservices) provides GPU-optimized models
for embedding and text generation via an OpenAI-compatible API.

Get API key: https://build.nvidia.com

Usage:
v = Vektori(
embedding_model="nvidia:llama-nemotron-embed-1b-v2",
extraction_model="nvidia:llama-3.3-nemotron-super-49b-v1",
)

Environment:
NVIDIA_API_KEY - Your NVIDIA API key (required)

Models:
Embeddings (llama-nemotron-embed-1b-v2 is recommended default):
- llama-nemotron-embed-1b-v2: 2048 dim, 8192 tokens, multilingual, Matryoshka
- llama-3.2-nv-embedqa-1b-v2: 2048 dim, 8192 tokens, QA-optimized
- baai/bge-m3: 1024 dim, 8192 tokens, multilingual, dense+sparse+multi-vector
- nv-embed-v1: 4096 dim, 32k tokens, generalist
- nv-embedqa-e5-v5: 1024 dim, 512 tokens, fast

LLMs:
- llama-3.3-nemotron-super-49b-v1: High quality extraction (default)
- llama-3.1-nemotron-ultra-253b-v1: Largest NVIDIA model
- nemotron-4-mini-hindi-4b-instruct: Fast, efficient
- z-ai/glm5: GLM-5, multilingual LLM
- z-ai/glm4.7: GLM-4.7, multilingual LLM
- google/gemma-4-31b-it: Google's Gemma 4 31B instruction tuned
- minimaxai/minimax-m2.7: MiniMax M2.7 model
- moonshotai/kimi-k2.5: Moonshot Kimi K2.5
- moonshotai/kimi-k2-instruct: Moonshot Kimi K2 instruction
- deepseek-ai/deepseek-v3.1: DeepSeek V3.1
- qwen/qwen3-coder-480b-a35b-instruct: Qwen3 Coder 480B
"""

from __future__ import annotations

import logging
import os
from typing import Any

from vektori.models.base import EmbeddingProvider, LLMProvider

logger = logging.getLogger(__name__)

NVIDIA_BASE_URL = "https://integrate.api.nvidia.com/v1"

# Embedding dimensions for known NVIDIA models
# Matryoshka models support multiple dimensions; max is shown
EMBEDDING_DIMENSIONS = {
# Nemotron family (Matryoshka: 384, 512, 768, 1024, 2048)
"nvidia/llama-nemotron-embed-1b-v2": 2048,
"nvidia/llama-nemotron-embed-vl-1b-v2": 2048,
# NeMo Retriever family (Matryoshka: 384, 512, 768, 1024, 2048)
"nvidia/llama-3.2-nv-embedqa-1b-v2": 2048,
"nvidia/llama-3.2-nv-embedqa-1b-v1": 2048,
"nvidia/llama-3.2-nemoretriever-300m-embed-v2": 1024,
"nvidia/llama-3.2-nemoretriever-300m-embed-v1": 1024,
"nvidia/llama-3.2-nemoretriever-1b-vlm-embed-v1": 2048,
# BAAI BGE-M3 (hosted on NVIDIA NIM)
"baai/bge-m3": 1024,
# Other models
"nvidia/nv-embed-v1": 4096,
"nvidia/nv-embedqa-e5-v5": 1024,
"nvidia/nv-embedcode-7b-v1": 4096,
"nvidia/nv-embedqa-e5-v4": 1024,
"nvidia/nvclip": 512,
}

# Models that support Matryoshka embeddings (configurable dimensions)
MATRYOSHKA_MODELS = {
"nvidia/llama-nemotron-embed-1b-v2",
"nvidia/llama-nemotron-embed-vl-1b-v2",
"nvidia/llama-3.2-nv-embedqa-1b-v2",
"nvidia/llama-3.2-nv-embedqa-1b-v1",
}

DEFAULT_EMBEDDING_MODEL = "nvidia/llama-nemotron-embed-1b-v2"
DEFAULT_LLM_MODEL = "nvidia/llama-3.3-nemotron-super-49b-v1"


class NvidiaEmbedder(EmbeddingProvider):
"""NVIDIA NIM embedding models via OpenAI-compatible API.

Supports all NVIDIA embedding models including:
- llama-nemotron-embed-1b-v2 (recommended): multilingual, 2048-dim, Matryoshka
- llama-3.2-nv-embedqa-1b-v2: QA-optimized, 2048-dim, Matryoshka
- nv-embed-v1: generalist, 4096-dim, 32k context
- nv-embedqa-e5-v5: fast, 1024-dim, 512 context

Matryoshka models support configurable output dimensions:
384, 512, 768, 1024, or 2048. Use the dimensions parameter or
embedding_dimension config to set custom dimensions.
Comment on lines +95 to +96

Copilot AI Apr 13, 2026

Copy link

Choose a reason for hiding this comment

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

The docstring says Matryoshka dimensions can be set via the embedding_dimension config, but the embedder only accepts dimensions and nothing in Vektori/create_embedder() passes embedding_dimension through. Either plumb VektoriConfig.embedding_dimension into this provider (or factory) or adjust the docstring to avoid implying it works today.

Suggested change
384, 512, 768, 1024, or 2048. Use the dimensions parameter or
embedding_dimension config to set custom dimensions.
384, 512, 768, 1024, or 2048. Use the dimensions parameter to
set custom dimensions.

Copilot uses AI. Check for mistakes.

Args:
model: Model name from NVIDIA catalog. Defaults to llama-nemotron-embed-1b-v2.
api_key: NVIDIA API key. Falls back to NVIDIA_API_KEY env var.
dimensions: Optional custom embedding dimensions for Matryoshka models.
"""

def __init__(
self,
model: str | None = None,
api_key: str | None = None,
dimensions: int | None = None,
) -> None:
raw_model = model or DEFAULT_EMBEDDING_MODEL
# NVIDIA NIM requires full path with namespace prefix (e.g., "nvidia/" or "baai/")
# Add "nvidia/" prefix if model doesn't already have a namespace
if "/" not in raw_model:
raw_model = f"nvidia/{raw_model}"
self.model = raw_model
self._api_key = api_key
self._custom_dims = dimensions
self._client = None
Comment thread
coderabbitai[bot] marked this conversation as resolved.

def _get_client(self):
"""Lazy initialization of OpenAI client configured for NVIDIA API."""
if self._client is None:
try:
from openai import AsyncOpenAI
except ImportError as e:
raise ImportError(
"openai package required for NVIDIA NIM: pip install openai>=1.12"
) from e

api_key = self._api_key or os.environ.get("NVIDIA_API_KEY")
if not api_key:
raise ValueError(
"NVIDIA API key required. Set NVIDIA_API_KEY environment variable "
"or pass api_key parameter."
)

self._client = AsyncOpenAI(
api_key=api_key,
base_url=NVIDIA_BASE_URL,
)
return self._client

@property
def dimension(self) -> int:
"""Return embedding dimension.

If custom dimensions were specified and the model supports Matryoshka
embeddings, returns the custom dimension. Otherwise returns the
model's default dimension.
"""
if self._custom_dims and self.model in MATRYOSHKA_MODELS:
return self._custom_dims
return EMBEDDING_DIMENSIONS.get(self.model, 2048)

async def embed(self, text: str) -> list[float]:
"""Embed a single text string.

Args:
text: Text to embed.

Returns:
Embedding vector as list of floats.
"""
return (await self.embed_batch([text]))[0]

async def embed_batch(self, texts: list[str]) -> list[list[float]]:
"""Embed multiple texts in a single API call.

Args:
texts: List of text strings to embed.

Returns:
List of embedding vectors, preserving input order.
"""
if not texts:
return []

client = self._get_client()

# Build extra_body for NVIDIA-specific parameters
extra_body: dict[str, Any] = {}

# Matryoshka models support custom dimensions
if self._custom_dims and self.model in MATRYOSHKA_MODELS:
extra_body["dimensions"] = self._custom_dims

# Some NVIDIA embedding models require input_type parameter
# "passage" is used for indexing, "query" for querying
# We default to "passage" for general embedding use
if self.model in {
"nvidia/llama-nemotron-embed-1b-v2",
"nvidia/llama-nemotron-embed-vl-1b-v2",
}:
extra_body["input_type"] = "passage"

Comment on lines +226 to +234

Copilot AI Apr 13, 2026

Copy link

Choose a reason for hiding this comment

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

input_type is hard-coded to "passage" for models that distinguish between indexing (passage) vs querying (query). Vektori uses the same embedder for both ingestion and retrieval queries, so this will also embed user queries as "passage" and can degrade retrieval quality for asymmetric models. Consider making input_type configurable (e.g., constructor arg) and/or exposing separate methods/instances for passage vs query embeddings.

Copilot uses AI. Check for mistakes.
response = await client.embeddings.create(
input=texts,
model=self.model,
extra_body=extra_body if extra_body else None,
)

# Sort by index to preserve order
return [item.embedding for item in sorted(response.data, key=lambda x: x.index)]


class NvidiaLLM(LLMProvider):
"""NVIDIA NIM LLM for fact and episode extraction.

Supports all NVIDIA chat completion models and third-party models hosted
on NVIDIA NIM, including:
- nvidia/llama-3.3-nemotron-super-49b-v1 (default): High quality extraction
- nvidia/llama-3.1-nemotron-ultra-253b-v1: Largest NVIDIA model
- nvidia/nemotron-4-mini-hindi-4b-instruct: Fast, efficient
- z-ai/glm5: GLM-5 multilingual model
- z-ai/glm4.7: GLM-4.7 multilingual model
- google/gemma-4-31b-it: Google's Gemma 4 31B instruction tuned
- minimaxai/minimax-m2.7: MiniMax M2.7 model
- moonshotai/kimi-k2.5: Moonshot Kimi K2.5
- moonshotai/kimi-k2-instruct: Moonshot Kimi K2 instruction
- deepseek-ai/deepseek-v3.1: DeepSeek V3.1
- qwen/qwen3-coder-480b-a35b-instruct: Qwen3 Coder 480B

Uses OpenAI-compatible API with NVIDIA-specific base URL.

Args:
model: Model name from NVIDIA catalog (e.g., "llama-3.3-nemotron-super-49b-v1"
for NVIDIA models, or full path like "z-ai/glm5" for third-party models).
api_key: NVIDIA API key. Falls back to NVIDIA_API_KEY env var.
"""

def __init__(
self,
model: str | None = None,
api_key: str | None = None,
) -> None:
raw_model = model or DEFAULT_LLM_MODEL
# NVIDIA NIM requires full path with namespace prefix (e.g., "nvidia/")
# Add "nvidia/" prefix if model doesn't already have a namespace
if "/" not in raw_model:
raw_model = f"nvidia/{raw_model}"
self.model = raw_model
self._api_key = api_key
self._client = None

def _get_client(self):
"""Lazy initialization of OpenAI client configured for NVIDIA API."""
if self._client is None:
try:
from openai import AsyncOpenAI
except ImportError as e:
raise ImportError(
"openai package required for NVIDIA NIM: pip install openai>=1.12"
) from e

api_key = self._api_key or os.environ.get("NVIDIA_API_KEY")
if not api_key:
raise ValueError(
"NVIDIA API key required. Set NVIDIA_API_KEY environment variable "
"or pass api_key parameter."
)

self._client = AsyncOpenAI(
api_key=api_key,
base_url=NVIDIA_BASE_URL,
)
return self._client

async def generate(self, prompt: str, max_tokens: int | None = None) -> str:
"""Generate text completion for the given prompt.

Attempts to use JSON mode for structured outputs. Falls back to
regular chat completion if the model doesn't support JSON mode.

Args:
prompt: The prompt text to send to the model.
max_tokens: Optional maximum tokens in the response.

Returns:
Generated text response.
"""
client = self._get_client()

kwargs: dict[str, Any] = {
"model": self.model,
"messages": [{"role": "user", "content": prompt}],
"temperature": 0.1,
}

if max_tokens is not None:
kwargs["max_tokens"] = max_tokens

# Try JSON mode first (for structured extraction)
try:
kwargs["response_format"] = {"type": "json_object"}
response = await client.chat.completions.create(**kwargs)
except Exception:
# Fallback if model doesn't support JSON mode
del kwargs["response_format"]
response = await client.chat.completions.create(**kwargs)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment on lines +331 to +334

Copilot AI Apr 13, 2026

Copy link

Choose a reason for hiding this comment

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

generate() retries without response_format on any exception. This can mask real failures (auth/rate limit/network) and can double-request/cost on transient errors. Prefer only falling back for the specific "response_format not supported"/400-type error (similar to the GitHub provider’s pattern) and re-raise unexpected exceptions.

Copilot uses AI. Check for mistakes.

return response.choices[0].message.content or ""
Loading