feat(providers): add Mistral embedding provider, route registry through dynaconf

Adds a hosted Mistral embedding option (mistral-embed, 1024-dim) alongside
the existing Bedrock / OpenAI / Ollama / Simple providers. Implementation
mirrors OpenAIProvider: lazy dimension detection with a known-models lookup,
chunked batch requests, defensive index sort, and a 429-aware retry decorator.

In the same change, ProviderRegistry switches from os.getenv to the
dynaconf-backed Settings dataclass so all five providers share a single
configuration path. config.py gains the previously-uncovered Bedrock keys,
the new Mistral keys, the missing OPENAI_GENERATION_MODEL /
OLLAMA_GENERATION_MODEL, and SIMPLE_EMBEDDING_DIMENSION.

Auto-detection priority: Bedrock → OpenAI → Mistral → Ollama → Simple.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-05-08 17:25:24 +02:00
co-authored by Claude Opus 4.7
parent 61cadf7935
commit 3268a13d11
10 changed files with 862 additions and 138 deletions
+19 -2
View File
@@ -118,10 +118,16 @@ class ProviderRegistry:
@staticmethod @staticmethod
def create_provider() -> Provider: def create_provider() -> Provider:
# 1. Bedrock (AWS_REGION or BEDROCK_*_MODEL) # 1. Bedrock (AWS_REGION or BEDROCK_*_MODEL)
# 2. Ollama (OLLAMA_BASE_URL) # 2. OpenAI (OPENAI_API_KEY)
# 3. Simple (fallback) # 3. Mistral (MISTRAL_API_KEY)
# 4. Ollama (OLLAMA_BASE_URL)
# 5. Simple (fallback)
``` ```
Configuration is sourced via the dynaconf-backed `Settings` dataclass in
`config.py`; the registry reads `get_settings()` rather than `os.getenv`
directly, so settings files and env vars share one resolution path.
**Environment Variables:** **Environment Variables:**
**Bedrock:** **Bedrock:**
@@ -131,6 +137,17 @@ class ProviderRegistry:
- `BEDROCK_EMBEDDING_MODEL`: Model ID for embeddings (e.g., "amazon.titan-embed-text-v2:0") - `BEDROCK_EMBEDDING_MODEL`: Model ID for embeddings (e.g., "amazon.titan-embed-text-v2:0")
- `BEDROCK_GENERATION_MODEL`: Model ID for text generation (e.g., "anthropic.claude-3-sonnet-20240229-v1:0") - `BEDROCK_GENERATION_MODEL`: Model ID for text generation (e.g., "anthropic.claude-3-sonnet-20240229-v1:0")
**OpenAI:**
- `OPENAI_API_KEY`: OpenAI API key (or `GITHUB_TOKEN` for GitHub Models)
- `OPENAI_BASE_URL`: Optional base URL override for OpenAI-compatible APIs
- `OPENAI_EMBEDDING_MODEL`: Embedding model (default: "text-embedding-3-small")
- `OPENAI_GENERATION_MODEL`: Generation model (e.g., "gpt-4o-mini")
**Mistral (embeddings only):**
- `MISTRAL_API_KEY`: Mistral API key from console.mistral.ai
- `MISTRAL_EMBEDDING_MODEL`: Embedding model (default: "mistral-embed", 1024-dim)
- `MISTRAL_BASE_URL`: Optional server URL override (proxies, on-prem)
**Ollama:** **Ollama:**
- `OLLAMA_BASE_URL`: Ollama API base URL (e.g., "http://localhost:11434") - `OLLAMA_BASE_URL`: Ollama API base URL (e.g., "http://localhost:11434")
- `OLLAMA_EMBEDDING_MODEL`: Model for embeddings (default: "nomic-embed-text") - `OLLAMA_EMBEDDING_MODEL`: Model for embeddings (default: "nomic-embed-text")
+63 -3
View File
@@ -410,9 +410,16 @@ DOCUMENT_CHUNK_OVERLAP=50 # Overlapping words between chunks (defaul
### Embedding Service Configuration ### Embedding Service Configuration
The server uses an embedding service to generate vector representations. Two options are available: The server picks an embedding provider via auto-detection. Priority order
(see `nextcloud_mcp_server/providers/registry.py`):
#### Ollama (Recommended) 1. **Bedrock** — if `AWS_REGION` or `BEDROCK_EMBEDDING_MODEL` is set
2. **OpenAI** — if `OPENAI_API_KEY` is set
3. **Mistral** — if `MISTRAL_API_KEY` is set
4. **Ollama** — if `OLLAMA_BASE_URL` is set
5. **Simple** — fallback when nothing else is configured
#### Ollama (Recommended for self-hosted)
Use a local Ollama instance for embeddings: Use a local Ollama instance for embeddings:
@@ -422,9 +429,52 @@ OLLAMA_EMBEDDING_MODEL=nomic-embed-text # Default model
OLLAMA_VERIFY_SSL=true # Verify SSL certificates OLLAMA_VERIFY_SSL=true # Verify SSL certificates
``` ```
#### OpenAI
Hosted OpenAI embeddings (or any OpenAI-compatible API via `OPENAI_BASE_URL`):
```dotenv
OPENAI_API_KEY=sk-...
OPENAI_EMBEDDING_MODEL=text-embedding-3-small # default
# OPENAI_BASE_URL=https://models.github.ai/inference # optional
```
#### Mistral
Hosted Mistral embeddings. Requires a Mistral API key from
[console.mistral.ai](https://console.mistral.ai). Currently embeddings only
(no text generation).
```dotenv
MISTRAL_API_KEY=...
MISTRAL_EMBEDDING_MODEL=mistral-embed # default; produces 1024-dim vectors
# MISTRAL_BASE_URL=https://api.mistral.ai # optional override (proxies, on-prem)
```
Switching to or from Mistral forces a new Qdrant collection because the
collection name encodes the model (see "Qdrant Collection Naming" above).
#### Amazon Bedrock
Bedrock provides hosted embedding models (Titan, Cohere) and uses the AWS
credential chain (env vars, profiles, or IAM role):
```dotenv
AWS_REGION=us-east-1
BEDROCK_EMBEDDING_MODEL=amazon.titan-embed-text-v2:0
# AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY are optional — boto3 will use
# the standard credential chain if not set.
```
#### Simple Embedding Provider (Fallback) #### Simple Embedding Provider (Fallback)
If `OLLAMA_BASE_URL` is not set, the server uses a simple random embedding provider for testing. This is **not suitable for production** as it generates random embeddings with no semantic meaning. If no provider env var is set, the server falls back to a simple deterministic
embedding provider for testing. This is **not suitable for production** as
its embeddings have no semantic meaning.
```dotenv
SIMPLE_EMBEDDING_DIMENSION=384 # optional; default 384
```
### Document Chunking Configuration ### Document Chunking Configuration
@@ -534,6 +584,16 @@ equivalent.** Operators who need a runtime toggle should open an issue.
| `OLLAMA_BASE_URL` | ⚠️ Optional | - | Ollama API endpoint for embeddings | | `OLLAMA_BASE_URL` | ⚠️ Optional | - | Ollama API endpoint for embeddings |
| `OLLAMA_EMBEDDING_MODEL` | ⚠️ Optional | `nomic-embed-text` | Embedding model to use | | `OLLAMA_EMBEDDING_MODEL` | ⚠️ Optional | `nomic-embed-text` | Embedding model to use |
| `OLLAMA_VERIFY_SSL` | ⚠️ Optional | `true` | Verify SSL certificates | | `OLLAMA_VERIFY_SSL` | ⚠️ Optional | `true` | Verify SSL certificates |
| `OPENAI_API_KEY` | ⚠️ Optional | - | OpenAI API key (selects OpenAI provider) |
| `OPENAI_BASE_URL` | ⚠️ Optional | - | OpenAI base URL override (for compatible APIs) |
| `OPENAI_EMBEDDING_MODEL` | ⚠️ Optional | `text-embedding-3-small` | OpenAI embedding model |
| `MISTRAL_API_KEY` | ⚠️ Optional | - | Mistral API key (selects Mistral provider) |
| `MISTRAL_EMBEDDING_MODEL` | ⚠️ Optional | `mistral-embed` | Mistral embedding model (1024-dim) |
| `MISTRAL_BASE_URL` | ⚠️ Optional | - | Mistral base URL override (proxies, on-prem) |
| `AWS_REGION` | ⚠️ Optional | - | AWS region (selects Bedrock provider) |
| `BEDROCK_EMBEDDING_MODEL` | ⚠️ Optional | - | Bedrock embedding model ID |
| `BEDROCK_GENERATION_MODEL` | ⚠️ Optional | - | Bedrock generation model ID |
| `SIMPLE_EMBEDDING_DIMENSION` | ⚠️ Optional | `384` | Dimension for the fallback Simple provider |
| `DOCUMENT_CHUNK_SIZE` | ⚠️ Optional | `512` | Words per chunk for document embedding | | `DOCUMENT_CHUNK_SIZE` | ⚠️ Optional | `512` | Words per chunk for document embedding |
| `DOCUMENT_CHUNK_OVERLAP` | ⚠️ Optional | `50` | Overlapping words between chunks (must be < chunk size) | | `DOCUMENT_CHUNK_OVERLAP` | ⚠️ Optional | `50` | Overlapping words between chunks (must be < chunk size) |
+59 -9
View File
@@ -77,11 +77,25 @@ _DEFAULTS: dict[str, Any] = {
# Ollama # Ollama
"ollama_base_url": None, "ollama_base_url": None,
"ollama_embedding_model": "nomic-embed-text", "ollama_embedding_model": "nomic-embed-text",
"ollama_generation_model": None,
"ollama_verify_ssl": True, "ollama_verify_ssl": True,
# OpenAI # OpenAI
"openai_api_key": None, "openai_api_key": None,
"openai_base_url": None, "openai_base_url": None,
"openai_embedding_model": "text-embedding-3-small", "openai_embedding_model": "text-embedding-3-small",
"openai_generation_model": None,
# Bedrock (AWS)
"aws_region": None,
"aws_access_key_id": None,
"aws_secret_access_key": None,
"bedrock_embedding_model": None,
"bedrock_generation_model": None,
# Mistral
"mistral_api_key": None,
"mistral_embedding_model": "mistral-embed",
"mistral_base_url": None,
# Simple (fallback) embedding dimension
"simple_embedding_dimension": 384,
# Document chunking # Document chunking
"document_chunk_size": 2048, "document_chunk_size": 2048,
"document_chunk_overlap": 200, "document_chunk_overlap": 200,
@@ -486,15 +500,32 @@ class Settings:
qdrant_api_key: str | None = None qdrant_api_key: str | None = None
qdrant_collection: str = "nextcloud_content" qdrant_collection: str = "nextcloud_content"
# Ollama settings (for embeddings) # Ollama settings (embeddings + optional generation)
ollama_base_url: str | None = None ollama_base_url: str | None = None
ollama_embedding_model: str = "nomic-embed-text" ollama_embedding_model: str = "nomic-embed-text"
ollama_generation_model: str | None = None
ollama_verify_ssl: bool = True ollama_verify_ssl: bool = True
# OpenAI settings (for embeddings) # OpenAI settings (embeddings + optional generation)
openai_api_key: str | None = None openai_api_key: str | None = None
openai_base_url: str | None = None openai_base_url: str | None = None
openai_embedding_model: str = "text-embedding-3-small" openai_embedding_model: str = "text-embedding-3-small"
openai_generation_model: str | None = None
# Bedrock (AWS) settings — boto3 also reads these from its credential chain
aws_region: str | None = None
aws_access_key_id: str | None = None
aws_secret_access_key: str | None = None
bedrock_embedding_model: str | None = None
bedrock_generation_model: str | None = None
# Mistral settings (embeddings only)
mistral_api_key: str | None = None
mistral_embedding_model: str = "mistral-embed"
mistral_base_url: str | None = None
# Simple (fallback) provider — dimension when no real provider configured
simple_embedding_dimension: int = 384
# Document chunking settings (for vector embeddings) # Document chunking settings (for vector embeddings)
document_chunk_size: int = 2048 # Characters per chunk document_chunk_size: int = 2048 # Characters per chunk
@@ -573,23 +604,28 @@ class Settings:
Get the active embedding model name based on provider priority. Get the active embedding model name based on provider priority.
Priority order (same as ProviderRegistry): Priority order (same as ProviderRegistry):
1. OpenAI - if OPENAI_API_KEY is set 1. Bedrock - if AWS_REGION or BEDROCK_EMBEDDING_MODEL is set
2. Ollama - if OLLAMA_BASE_URL is set 2. OpenAI - if OPENAI_API_KEY is set
3. Simple - fallback (returns "simple-384") 3. Mistral - if MISTRAL_API_KEY is set
4. Ollama - if OLLAMA_BASE_URL is set
5. Simple - fallback (returns "simple-{dimension}")
Returns: Returns:
Active embedding model name Active embedding model name
""" """
# Check OpenAI first (higher priority than Ollama in registry) if self.aws_region or self.bedrock_embedding_model:
return self.bedrock_embedding_model or "bedrock-default"
if self.openai_api_key: if self.openai_api_key:
return self.openai_embedding_model return self.openai_embedding_model
# Check Ollama if self.mistral_api_key:
return self.mistral_embedding_model
if self.ollama_base_url: if self.ollama_base_url:
return self.ollama_embedding_model return self.ollama_embedding_model
# Fallback to simple provider indicator return f"simple-{self.simple_embedding_dimension}"
return "simple-384"
def get_collection_name(self) -> str: def get_collection_name(self) -> str:
""" """
@@ -835,11 +871,25 @@ def get_settings() -> Settings:
# Ollama settings # Ollama settings
"ollama_base_url": "OLLAMA_BASE_URL", "ollama_base_url": "OLLAMA_BASE_URL",
"ollama_embedding_model": "OLLAMA_EMBEDDING_MODEL", "ollama_embedding_model": "OLLAMA_EMBEDDING_MODEL",
"ollama_generation_model": "OLLAMA_GENERATION_MODEL",
"ollama_verify_ssl": "OLLAMA_VERIFY_SSL", "ollama_verify_ssl": "OLLAMA_VERIFY_SSL",
# OpenAI settings # OpenAI settings
"openai_api_key": "OPENAI_API_KEY", "openai_api_key": "OPENAI_API_KEY",
"openai_base_url": "OPENAI_BASE_URL", "openai_base_url": "OPENAI_BASE_URL",
"openai_embedding_model": "OPENAI_EMBEDDING_MODEL", "openai_embedding_model": "OPENAI_EMBEDDING_MODEL",
"openai_generation_model": "OPENAI_GENERATION_MODEL",
# Bedrock (AWS) settings
"aws_region": "AWS_REGION",
"aws_access_key_id": "AWS_ACCESS_KEY_ID",
"aws_secret_access_key": "AWS_SECRET_ACCESS_KEY",
"bedrock_embedding_model": "BEDROCK_EMBEDDING_MODEL",
"bedrock_generation_model": "BEDROCK_GENERATION_MODEL",
# Mistral settings
"mistral_api_key": "MISTRAL_API_KEY",
"mistral_embedding_model": "MISTRAL_EMBEDDING_MODEL",
"mistral_base_url": "MISTRAL_BASE_URL",
# Simple provider
"simple_embedding_dimension": "SIMPLE_EMBEDDING_DIMENSION",
# Document chunking settings # Document chunking settings
"document_chunk_size": "DOCUMENT_CHUNK_SIZE", "document_chunk_size": "DOCUMENT_CHUNK_SIZE",
"document_chunk_overlap": "DOCUMENT_CHUNK_OVERLAP", "document_chunk_overlap": "DOCUMENT_CHUNK_OVERLAP",
@@ -3,6 +3,7 @@
from .anthropic import AnthropicProvider from .anthropic import AnthropicProvider
from .base import Provider from .base import Provider
from .bedrock import BedrockProvider from .bedrock import BedrockProvider
from .mistral import MistralProvider
from .ollama import OllamaProvider from .ollama import OllamaProvider
from .openai import OpenAIProvider from .openai import OpenAIProvider
from .registry import get_provider, reset_provider from .registry import get_provider, reset_provider
@@ -13,6 +14,7 @@ __all__ = [
"OllamaProvider", "OllamaProvider",
"OpenAIProvider", "OpenAIProvider",
"AnthropicProvider", "AnthropicProvider",
"MistralProvider",
"SimpleProvider", "SimpleProvider",
"BedrockProvider", "BedrockProvider",
"get_provider", "get_provider",
+231
View File
@@ -0,0 +1,231 @@
"""Mistral provider for embeddings.
Currently supports embeddings only (``mistral-embed``, 1024-dim). Generation
can be added later if needed; see ADR-015.
"""
import logging
from functools import wraps
import anyio
from mistralai.client import Mistral
from mistralai.client.errors.sdkerror import SDKError
from .base import Provider
logger = logging.getLogger(__name__)
MAX_RETRIES = 5
INITIAL_RETRY_DELAY = 2.0
MAX_RETRY_DELAY = 60.0
def retry_on_rate_limit(func):
"""Retry on Mistral 429 (rate limit) responses with exponential backoff."""
@wraps(func)
async def wrapper(*args, **kwargs):
retry_delay = INITIAL_RETRY_DELAY
last_error: Exception | None = None
for attempt in range(1, MAX_RETRIES + 1):
try:
return await func(*args, **kwargs)
except SDKError as e:
# SDKError carries a status_code attribute populated from the
# raw response. Only 429 is retryable here.
status = getattr(e, "status_code", None)
if status != 429:
raise
last_error = e
if attempt < MAX_RETRIES:
logger.warning(
"Mistral rate limit hit (attempt %d/%d), retrying in %.1fs...",
attempt,
MAX_RETRIES,
retry_delay,
)
await anyio.sleep(retry_delay)
retry_delay = min(retry_delay * 2, MAX_RETRY_DELAY)
logger.error("Mistral rate limit exceeded after %d attempts", MAX_RETRIES)
raise last_error # type: ignore[misc]
return wrapper
# Well-known Mistral embedding model dimensions
MISTRAL_EMBEDDING_DIMENSIONS: dict[str, int] = {
"mistral-embed": 1024,
}
# Conservative chunk size for batch embeddings. Mistral allows large batches,
# but we keep this in line with sibling providers (OpenAI=100, Ollama=32).
BATCH_SIZE = 64
class MistralProvider(Provider):
"""
Mistral provider — embeddings only.
Uses the official ``mistralai`` SDK. Lazy dimension detection mirrors the
OpenAI provider: known models populate the cached dimension at construction
time; unknown models get their dimension detected on the first ``embed()``
call.
"""
def __init__(
self,
api_key: str,
embedding_model: str | None = "mistral-embed",
base_url: str | None = None,
):
"""
Initialize the Mistral provider.
Args:
api_key: Mistral API key.
embedding_model: Embedding model ID (default: ``mistral-embed``).
Pass ``None`` to disable embeddings (the provider will then
support no capabilities, which is mostly useful for tests).
base_url: Optional base URL override (e.g. proxies, on-prem).
"""
self.embedding_model = embedding_model
self._dimension: int | None = None
self.client = Mistral(api_key=api_key, server_url=base_url)
if embedding_model and embedding_model in MISTRAL_EMBEDDING_DIMENSIONS:
self._dimension = MISTRAL_EMBEDDING_DIMENSIONS[embedding_model]
logger.info(
"Initialized Mistral provider: base_url=%s, embedding_model=%s, "
"dimension=%s",
base_url or "default",
embedding_model,
self._dimension,
)
@property
def supports_embeddings(self) -> bool:
return self.embedding_model is not None
@property
def supports_generation(self) -> bool:
return False
@retry_on_rate_limit
async def embed(self, text: str) -> list[float]:
"""Generate an embedding for a single text."""
if not self.supports_embeddings:
raise NotImplementedError(
"Embedding not supported - no embedding_model configured"
)
assert self.embedding_model is not None
response = await self.client.embeddings.create_async(
model=self.embedding_model,
inputs=[text],
)
if not response.data or response.data[0].embedding is None:
raise RuntimeError(
f"Mistral embeddings API returned no embedding for model "
f"{self.embedding_model}"
)
embedding = response.data[0].embedding
if self._dimension is None:
self._dimension = len(embedding)
logger.info(
"Detected embedding dimension: %d for model %s",
self._dimension,
self.embedding_model,
)
return embedding
async def embed_batch(self, texts: list[str]) -> list[list[float]]:
"""Generate embeddings for multiple texts, chunking by ``BATCH_SIZE``."""
if not self.supports_embeddings:
raise NotImplementedError(
"Embedding not supported - no embedding_model configured"
)
if not texts:
return []
all_embeddings: list[list[float]] = []
for i in range(0, len(texts), BATCH_SIZE):
batch = texts[i : i + BATCH_SIZE]
batch_embeddings = await self._embed_batch_request(batch)
all_embeddings.extend(batch_embeddings)
if self._dimension is None and batch_embeddings:
self._dimension = len(batch_embeddings[0])
logger.info(
"Detected embedding dimension: %d for model %s",
self._dimension,
self.embedding_model,
)
return all_embeddings
@retry_on_rate_limit
async def _embed_batch_request(self, batch: list[str]) -> list[list[float]]:
"""Single batch request with rate-limit retry."""
assert self.embedding_model is not None
response = await self.client.embeddings.create_async(
model=self.embedding_model,
inputs=batch,
)
# Defensive: response.data items have Optional fields. Sort by index
# (default 0 if missing) and reject None embeddings explicitly.
sorted_data = sorted(response.data or [], key=lambda x: x.index or 0)
result: list[list[float]] = []
for item in sorted_data:
if item.embedding is None:
raise RuntimeError(
f"Mistral embeddings API returned a null embedding for "
f"model {self.embedding_model}"
)
result.append(item.embedding)
if len(result) != len(batch):
raise RuntimeError(
f"Mistral embeddings API returned {len(result)} embeddings "
f"for {len(batch)} inputs"
)
return result
def get_dimension(self) -> int:
if not self.supports_embeddings:
raise NotImplementedError(
"Embedding not supported - no embedding_model configured"
)
if self._dimension is None:
raise RuntimeError(
f"Embedding dimension not detected yet for model "
f"{self.embedding_model}. Call embed() first or use a known "
"model."
)
return self._dimension
async def generate(self, prompt: str, max_tokens: int = 500) -> str:
raise NotImplementedError(
"MistralProvider does not support generation. "
"Use OpenAI, Anthropic, or Bedrock for text generation."
)
async def close(self) -> None:
# The Mistral SDK manages its own httpx client lifecycle; close it
# via the SDK's context-manager hook if present, otherwise no-op.
close = getattr(self.client, "__aexit__", None)
if close is not None:
try:
await close(None, None, None)
except Exception: # pragma: no cover - best-effort cleanup
logger.debug("Mistral client close raised; ignoring", exc_info=True)
+78 -82
View File
@@ -1,10 +1,11 @@
"""Provider registry and factory for auto-detection and instantiation.""" """Provider registry and factory for auto-detection and instantiation."""
import logging import logging
import os
from ..config import get_settings
from .base import Provider from .base import Provider
from .bedrock import BedrockProvider from .bedrock import BedrockProvider
from .mistral import MistralProvider
from .ollama import OllamaProvider from .ollama import OllamaProvider
from .openai import OpenAIProvider from .openai import OpenAIProvider
from .simple import SimpleProvider from .simple import SimpleProvider
@@ -16,117 +17,112 @@ class ProviderRegistry:
""" """
Registry for provider auto-detection and instantiation. Registry for provider auto-detection and instantiation.
Checks environment variables in priority order and creates appropriate provider: Reads configuration via dynaconf-backed Settings (see ``config.py``).
1. Bedrock (AWS_REGION + BEDROCK_*_MODEL) Checks provider settings in priority order and creates the appropriate
2. OpenAI (OPENAI_API_KEY) provider:
3. Ollama (OLLAMA_BASE_URL)
4. Simple (fallback for testing/development) 1. Bedrock (``AWS_REGION`` or ``BEDROCK_*_MODEL``)
2. OpenAI (``OPENAI_API_KEY``)
3. Mistral (``MISTRAL_API_KEY``)
4. Ollama (``OLLAMA_BASE_URL``)
5. Simple (fallback for testing/development)
""" """
@staticmethod @staticmethod
def create_provider() -> Provider: def create_provider() -> Provider:
""" """
Auto-detect and create provider based on environment variables. Auto-detect and create provider based on configured settings.
Settings are sourced via :func:`nextcloud_mcp_server.config.get_settings`,
which reads from settings files and environment variables (env vars
always win, see ADR-024/025).
Priority order: Priority order:
1. Bedrock - if AWS_REGION or BEDROCK_EMBEDDING_MODEL is set
2. OpenAI - if OPENAI_API_KEY is set 1. Bedrock - if ``aws_region`` or ``bedrock_embedding_model`` is set
3. Ollama - if OLLAMA_BASE_URL is set 2. OpenAI - if ``openai_api_key`` is set
4. Simple - fallback for testing/development 3. Mistral - if ``mistral_api_key`` is set
4. Ollama - if ``ollama_base_url`` is set
5. Simple - fallback for testing/development
Returns: Returns:
Provider instance Provider instance
Environment Variables:
Bedrock:
- AWS_REGION: AWS region (e.g., "us-east-1")
- AWS_ACCESS_KEY_ID: AWS access key (optional, uses credential chain)
- AWS_SECRET_ACCESS_KEY: AWS secret key (optional)
- BEDROCK_EMBEDDING_MODEL: Model ID for embeddings (e.g., "amazon.titan-embed-text-v2:0")
- BEDROCK_GENERATION_MODEL: Model ID for text generation (e.g., "anthropic.claude-3-sonnet-20240229-v1:0")
OpenAI:
- OPENAI_API_KEY: OpenAI API key (or GITHUB_TOKEN for GitHub Models)
- OPENAI_BASE_URL: Base URL override (e.g., "https://models.github.ai/inference")
- OPENAI_EMBEDDING_MODEL: Model for embeddings (default: "text-embedding-3-small")
- OPENAI_GENERATION_MODEL: Model for text generation (e.g., "gpt-4o-mini")
Ollama:
- OLLAMA_BASE_URL: Ollama API base URL (e.g., "http://localhost:11434")
- OLLAMA_EMBEDDING_MODEL: Model for embeddings (default: "nomic-embed-text")
- OLLAMA_GENERATION_MODEL: Model for text generation (e.g., "llama3.2:1b")
- OLLAMA_VERIFY_SSL: Verify SSL certificates (default: "true")
Simple (no configuration needed, fallback):
- SIMPLE_EMBEDDING_DIMENSION: Embedding dimension (default: 384)
""" """
# 1. Check for Bedrock settings = get_settings()
aws_region = os.getenv("AWS_REGION")
bedrock_embedding_model = os.getenv("BEDROCK_EMBEDDING_MODEL")
bedrock_generation_model = os.getenv("BEDROCK_GENERATION_MODEL")
if aws_region or bedrock_embedding_model or bedrock_generation_model: # 1. Bedrock
if (
settings.aws_region
or settings.bedrock_embedding_model
or settings.bedrock_generation_model
):
logger.info( logger.info(
f"Using Bedrock provider: region={aws_region}, " "Using Bedrock provider: region=%s, embedding_model=%s, "
f"embedding_model={bedrock_embedding_model}, " "generation_model=%s",
f"generation_model={bedrock_generation_model}" settings.aws_region,
settings.bedrock_embedding_model,
settings.bedrock_generation_model,
) )
return BedrockProvider( return BedrockProvider(
region_name=aws_region, region_name=settings.aws_region,
embedding_model=bedrock_embedding_model, embedding_model=settings.bedrock_embedding_model,
generation_model=bedrock_generation_model, generation_model=settings.bedrock_generation_model,
aws_access_key_id=os.getenv("AWS_ACCESS_KEY_ID"), aws_access_key_id=settings.aws_access_key_id,
aws_secret_access_key=os.getenv("AWS_SECRET_ACCESS_KEY"), aws_secret_access_key=settings.aws_secret_access_key,
) )
# 2. Check for OpenAI # 2. OpenAI
openai_api_key = os.getenv("OPENAI_API_KEY") if settings.openai_api_key:
if openai_api_key:
base_url = os.getenv("OPENAI_BASE_URL")
embedding_model = os.getenv(
"OPENAI_EMBEDDING_MODEL", "text-embedding-3-small"
)
generation_model = os.getenv("OPENAI_GENERATION_MODEL")
logger.info( logger.info(
f"Using OpenAI provider: base_url={base_url or 'default'}, " "Using OpenAI provider: base_url=%s, embedding_model=%s, "
f"embedding_model={embedding_model}, " "generation_model=%s",
f"generation_model={generation_model}" settings.openai_base_url or "default",
settings.openai_embedding_model,
settings.openai_generation_model,
) )
return OpenAIProvider( return OpenAIProvider(
api_key=openai_api_key, api_key=settings.openai_api_key,
base_url=base_url, base_url=settings.openai_base_url,
embedding_model=embedding_model, embedding_model=settings.openai_embedding_model,
generation_model=generation_model, generation_model=settings.openai_generation_model,
) )
# 3. Check for Ollama (local LLM) # 3. Mistral
ollama_url = os.getenv("OLLAMA_BASE_URL") if settings.mistral_api_key:
if ollama_url:
embedding_model = os.getenv("OLLAMA_EMBEDDING_MODEL", "nomic-embed-text")
generation_model = os.getenv("OLLAMA_GENERATION_MODEL")
verify_ssl = os.getenv("OLLAMA_VERIFY_SSL", "true").lower() == "true"
logger.info( logger.info(
f"Using Ollama provider: {ollama_url}, " "Using Mistral provider: base_url=%s, embedding_model=%s",
f"embedding_model={embedding_model}, " settings.mistral_base_url or "default",
f"generation_model={generation_model}" settings.mistral_embedding_model,
)
return MistralProvider(
api_key=settings.mistral_api_key,
base_url=settings.mistral_base_url,
embedding_model=settings.mistral_embedding_model,
)
# 4. Ollama
if settings.ollama_base_url:
logger.info(
"Using Ollama provider: %s, embedding_model=%s, generation_model=%s",
settings.ollama_base_url,
settings.ollama_embedding_model,
settings.ollama_generation_model,
) )
return OllamaProvider( return OllamaProvider(
base_url=ollama_url, base_url=settings.ollama_base_url,
embedding_model=embedding_model, embedding_model=settings.ollama_embedding_model,
generation_model=generation_model, generation_model=settings.ollama_generation_model,
verify_ssl=verify_ssl, verify_ssl=settings.ollama_verify_ssl,
) )
# 4. Fallback to Simple provider for development/testing # 5. Simple (fallback)
dimension = int(os.getenv("SIMPLE_EMBEDDING_DIMENSION", "384"))
logger.warning( logger.warning(
"No provider configured (AWS_REGION, OPENAI_API_KEY, OLLAMA_BASE_URL not set). " "No provider configured (AWS_REGION, OPENAI_API_KEY, "
"MISTRAL_API_KEY, OLLAMA_BASE_URL not set). "
"Using SimpleProvider for testing/development. " "Using SimpleProvider for testing/development. "
"For production, configure Bedrock, OpenAI, or Ollama." "For production, configure Bedrock, OpenAI, Mistral, or Ollama."
) )
return SimpleProvider(dimension=dimension) return SimpleProvider(dimension=settings.simple_embedding_dimension)
# Singleton instance # Singleton instance
+1
View File
@@ -43,6 +43,7 @@ dependencies = [
"pymupdf4llm>=0.2.2", "pymupdf4llm>=0.2.2",
"openai>=2.8.1", "openai>=2.8.1",
"dynaconf>=3.2.13,<4.0", "dynaconf>=3.2.13,<4.0",
"mistralai>=2.4.5",
] ]
classifiers = [ classifiers = [
"Development Status :: 4 - Beta", "Development Status :: 4 - Beta",
+198
View File
@@ -0,0 +1,198 @@
"""Unit tests for Mistral provider."""
from unittest.mock import AsyncMock, MagicMock
import pytest
from nextcloud_mcp_server.providers.mistral import (
BATCH_SIZE,
MISTRAL_EMBEDDING_DIMENSIONS,
MistralProvider,
)
def _make_data(embedding: list[float], index: int) -> MagicMock:
"""Build a mock EmbeddingResponseData entry."""
item = MagicMock()
item.embedding = embedding
item.index = index
return item
def _make_response(embeddings: list[list[float]]) -> MagicMock:
"""Build a mock EmbeddingResponse with `embeddings` indexed in order."""
response = MagicMock()
response.data = [_make_data(emb, i) for i, emb in enumerate(embeddings)]
return response
@pytest.fixture
def mock_mistral_client(mocker):
"""Mock the Mistral SDK constructor."""
mock_client = MagicMock()
mock_client.embeddings = MagicMock()
mocker.patch(
"nextcloud_mcp_server.providers.mistral.Mistral", return_value=mock_client
)
return mock_client
@pytest.mark.unit
async def test_mistral_embedding_single(mock_mistral_client):
"""Single text embed: round-trip through SDK with correct kwargs."""
mock_mistral_client.embeddings.create_async = AsyncMock(
return_value=_make_response([[0.1, 0.2, 0.3]])
)
provider = MistralProvider(api_key="test-key", embedding_model="mistral-embed")
embedding = await provider.embed("hello world")
assert embedding == [0.1, 0.2, 0.3]
mock_mistral_client.embeddings.create_async.assert_awaited_once_with(
model="mistral-embed",
inputs=["hello world"],
)
@pytest.mark.unit
async def test_mistral_embedding_batch_single_call(mock_mistral_client):
"""Batch smaller than BATCH_SIZE issues a single API call."""
mock_mistral_client.embeddings.create_async = AsyncMock(
return_value=_make_response([[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]])
)
provider = MistralProvider(api_key="test-key", embedding_model="mistral-embed")
embeddings = await provider.embed_batch(["a", "b", "c"])
assert embeddings == [[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]]
assert mock_mistral_client.embeddings.create_async.await_count == 1
@pytest.mark.unit
async def test_mistral_embedding_batch_chunking(mock_mistral_client):
"""Batches exceeding BATCH_SIZE are split into multiple API calls."""
# Each call returns one embedding per input it received; capture by side
# effect so we can inspect lengths per chunk.
def _side_effect(*, model, inputs, **_kwargs):
return _make_response([[float(i)] for i in range(len(inputs))])
mock_mistral_client.embeddings.create_async = AsyncMock(side_effect=_side_effect)
provider = MistralProvider(api_key="test-key", embedding_model="mistral-embed")
total = BATCH_SIZE * 2 + 5 # forces three chunks: 64, 64, 5 (with default)
embeddings = await provider.embed_batch([f"text-{i}" for i in range(total)])
assert len(embeddings) == total
assert mock_mistral_client.embeddings.create_async.await_count == 3
# Verify the chunk sizes the SDK was actually called with.
chunk_sizes = [
len(call.kwargs["inputs"])
for call in mock_mistral_client.embeddings.create_async.await_args_list
]
assert chunk_sizes == [BATCH_SIZE, BATCH_SIZE, 5]
@pytest.mark.unit
async def test_mistral_embedding_batch_order_preserved(mock_mistral_client):
"""Out-of-order index in response data is sorted before returning."""
response = MagicMock()
response.data = [
_make_data([0.3, 0.3], 2),
_make_data([0.1, 0.1], 0),
_make_data([0.2, 0.2], 1),
]
mock_mistral_client.embeddings.create_async = AsyncMock(return_value=response)
provider = MistralProvider(api_key="test-key", embedding_model="mistral-embed")
embeddings = await provider.embed_batch(["x", "y", "z"])
assert embeddings == [[0.1, 0.1], [0.2, 0.2], [0.3, 0.3]]
@pytest.mark.unit
async def test_mistral_supports_capabilities(mock_mistral_client):
"""Mistral provider advertises embeddings only."""
provider = MistralProvider(api_key="test-key", embedding_model="mistral-embed")
assert provider.supports_embeddings is True
assert provider.supports_generation is False
@pytest.mark.unit
async def test_mistral_generate_not_implemented(mock_mistral_client):
"""generate() always raises NotImplementedError."""
provider = MistralProvider(api_key="test-key", embedding_model="mistral-embed")
with pytest.raises(NotImplementedError, match="does not support generation"):
await provider.generate("test prompt")
@pytest.mark.unit
async def test_mistral_get_dimension_known_model(mock_mistral_client):
"""Known model: dimension available without an API call."""
provider = MistralProvider(api_key="test-key", embedding_model="mistral-embed")
assert provider.get_dimension() == MISTRAL_EMBEDDING_DIMENSIONS["mistral-embed"]
mock_mistral_client.embeddings.create_async.assert_not_called()
@pytest.mark.unit
async def test_mistral_get_dimension_unknown_model_detected(mock_mistral_client):
"""Unknown model: dimension detected on first embed() call."""
mock_mistral_client.embeddings.create_async = AsyncMock(
return_value=_make_response([[0.1] * 768])
)
provider = MistralProvider(api_key="test-key", embedding_model="custom-mistral")
with pytest.raises(RuntimeError, match="not detected yet"):
provider.get_dimension()
await provider.embed("test")
assert provider.get_dimension() == 768
@pytest.mark.unit
async def test_mistral_no_embeddings_disabled():
"""Setting embedding_model=None disables the embedding capability."""
provider = MistralProvider(api_key="test-key", embedding_model=None)
assert provider.supports_embeddings is False
with pytest.raises(NotImplementedError, match="no embedding_model configured"):
await provider.embed("test")
with pytest.raises(NotImplementedError, match="no embedding_model configured"):
await provider.embed_batch(["test"])
with pytest.raises(NotImplementedError, match="no embedding_model configured"):
provider.get_dimension()
@pytest.mark.unit
async def test_mistral_empty_batch(mock_mistral_client):
"""An empty batch returns [] without calling the API."""
provider = MistralProvider(api_key="test-key", embedding_model="mistral-embed")
assert await provider.embed_batch([]) == []
mock_mistral_client.embeddings.create_async.assert_not_called()
@pytest.mark.unit
async def test_mistral_close_no_error(mock_mistral_client):
"""close() is best-effort and does not raise."""
provider = MistralProvider(api_key="test-key", embedding_model="mistral-embed")
# No __aexit__ on the mock by default → close() should silently no-op.
await provider.close()
@pytest.mark.unit
async def test_mistral_base_url_passed_to_sdk(mocker):
"""base_url is forwarded as server_url to the Mistral SDK constructor."""
mock_ctor = mocker.patch(
"nextcloud_mcp_server.providers.mistral.Mistral", return_value=MagicMock()
)
MistralProvider(
api_key="test-key",
embedding_model="mistral-embed",
base_url="https://example.com/mistral",
)
mock_ctor.assert_called_once_with(
api_key="test-key",
server_url="https://example.com/mistral",
)
+130
View File
@@ -0,0 +1,130 @@
"""Unit tests for ProviderRegistry — dynaconf-driven auto-detection."""
import pytest
from nextcloud_mcp_server.config import _reload_config
from nextcloud_mcp_server.providers import (
BedrockProvider,
MistralProvider,
OllamaProvider,
OpenAIProvider,
SimpleProvider,
get_provider,
reset_provider,
)
from nextcloud_mcp_server.providers.bedrock import BOTO3_AVAILABLE
def _clear_provider_envs(monkeypatch: pytest.MonkeyPatch) -> None:
"""Strip every provider-selection env var so each test starts clean."""
for name in (
"AWS_REGION",
"AWS_ACCESS_KEY_ID",
"AWS_SECRET_ACCESS_KEY",
"BEDROCK_EMBEDDING_MODEL",
"BEDROCK_GENERATION_MODEL",
"OPENAI_API_KEY",
"OPENAI_BASE_URL",
"OPENAI_EMBEDDING_MODEL",
"OPENAI_GENERATION_MODEL",
"MISTRAL_API_KEY",
"MISTRAL_BASE_URL",
"MISTRAL_EMBEDDING_MODEL",
"OLLAMA_BASE_URL",
"OLLAMA_EMBEDDING_MODEL",
"OLLAMA_GENERATION_MODEL",
"OLLAMA_VERIFY_SSL",
"SIMPLE_EMBEDDING_DIMENSION",
):
monkeypatch.delenv(name, raising=False)
@pytest.fixture
def clean_provider_env(monkeypatch):
"""Reset provider singleton + dynaconf cache around each test."""
_clear_provider_envs(monkeypatch)
reset_provider()
_reload_config()
yield monkeypatch
reset_provider()
@pytest.mark.unit
def test_registry_falls_back_to_simple(clean_provider_env):
"""No provider env set → SimpleProvider (with default dimension)."""
provider = get_provider()
assert isinstance(provider, SimpleProvider)
assert provider.get_dimension() == 384
@pytest.mark.unit
def test_registry_picks_simple_with_custom_dimension(clean_provider_env):
"""SIMPLE_EMBEDDING_DIMENSION flows through dynaconf to SimpleProvider."""
clean_provider_env.setenv("SIMPLE_EMBEDDING_DIMENSION", "512")
_reload_config()
provider = get_provider()
assert isinstance(provider, SimpleProvider)
assert provider.get_dimension() == 512
@pytest.mark.unit
def test_registry_picks_mistral_when_api_key_set(clean_provider_env):
"""MISTRAL_API_KEY alone is enough to select MistralProvider."""
clean_provider_env.setenv("MISTRAL_API_KEY", "test-key")
_reload_config()
provider = get_provider()
assert isinstance(provider, MistralProvider)
@pytest.mark.unit
def test_registry_picks_ollama_when_base_url_set(clean_provider_env, mocker):
"""OLLAMA_BASE_URL selects OllamaProvider."""
# OllamaProvider eagerly probes /api/tags in __init__; stub it out.
mocker.patch(
"nextcloud_mcp_server.providers.ollama.OllamaProvider._check_model_is_loaded"
)
clean_provider_env.setenv("OLLAMA_BASE_URL", "http://localhost:11434")
_reload_config()
provider = get_provider()
assert isinstance(provider, OllamaProvider)
@pytest.mark.unit
def test_registry_openai_wins_over_mistral_and_ollama(clean_provider_env):
"""OpenAI takes priority when multiple provider env vars are set."""
clean_provider_env.setenv("OPENAI_API_KEY", "openai-key")
clean_provider_env.setenv("MISTRAL_API_KEY", "mistral-key")
clean_provider_env.setenv("OLLAMA_BASE_URL", "http://localhost:11434")
_reload_config()
provider = get_provider()
assert isinstance(provider, OpenAIProvider)
@pytest.mark.unit
def test_registry_mistral_wins_over_ollama(clean_provider_env):
"""Mistral takes priority over Ollama when both are configured."""
clean_provider_env.setenv("MISTRAL_API_KEY", "mistral-key")
clean_provider_env.setenv("OLLAMA_BASE_URL", "http://localhost:11434")
_reload_config()
provider = get_provider()
assert isinstance(provider, MistralProvider)
@pytest.mark.unit
def test_registry_bedrock_wins_when_aws_region_set(clean_provider_env):
"""AWS_REGION alone routes to Bedrock, even with other providers configured."""
if not BOTO3_AVAILABLE:
pytest.skip("boto3 not installed")
clean_provider_env.setenv("AWS_REGION", "us-east-1")
clean_provider_env.setenv("OPENAI_API_KEY", "openai-key")
clean_provider_env.setenv("MISTRAL_API_KEY", "mistral-key")
_reload_config()
provider = get_provider()
assert isinstance(provider, BedrockProvider)
Generated
+81 -42
View File
@@ -769,6 +769,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/97/43/11d6e5d2c00bf000b5329717c74563bf76a9193f4a41cb0c4ef277dde4fa/dynaconf-3.2.13-py2.py3-none-any.whl", hash = "sha256:4305527aef4834bdba3e39479b23c005186e83fb85f65bcaa4bcea58fa26759b", size = 238041, upload-time = "2026-03-17T19:38:45.337Z" }, { url = "https://files.pythonhosted.org/packages/97/43/11d6e5d2c00bf000b5329717c74563bf76a9193f4a41cb0c4ef277dde4fa/dynaconf-3.2.13-py2.py3-none-any.whl", hash = "sha256:4305527aef4834bdba3e39479b23c005186e83fb85f65bcaa4bcea58fa26759b", size = 238041, upload-time = "2026-03-17T19:38:45.337Z" },
] ]
[[package]]
name = "eval-type-backport"
version = "0.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fb/a3/cafafb4558fd638aadfe4121dc6cefb8d743368c085acb2f521df0f3d9d7/eval_type_backport-0.3.1.tar.gz", hash = "sha256:57e993f7b5b69d271e37482e62f74e76a0276c82490cf8e4f0dffeb6b332d5ed", size = 9445, upload-time = "2025-12-02T11:51:42.987Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cf/22/fdc2e30d43ff853720042fa15baa3e6122722be1a7950a98233ebb55cd71/eval_type_backport-0.3.1-py3-none-any.whl", hash = "sha256:279ab641905e9f11129f56a8a78f493518515b83402b860f6f06dd7c011fdfa8", size = 6063, upload-time = "2025-12-02T11:51:41.665Z" },
]
[[package]] [[package]]
name = "executing" name = "executing"
version = "2.2.1" version = "2.2.1"
@@ -1478,6 +1487,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898, upload-time = "2023-06-16T21:01:28.466Z" }, { url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898, upload-time = "2023-06-16T21:01:28.466Z" },
] ]
[[package]]
name = "jsonpath-python"
version = "1.1.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/98/18/4ca8742534a5993ff383f7602e325ce2d5d7cc93d72ac5e1cdedbea8a458/jsonpath_python-1.1.6.tar.gz", hash = "sha256:dded9932b4ec41fb8726e09c83afa4e6be618f938c2db287cc2a81723c639671", size = 88178, upload-time = "2026-05-07T01:26:34.482Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/55/8a/1270a6803bd821cbfcdda387eaa13cb41a7b1f7b9bd145979b3bfb9d6cb7/jsonpath_python-1.1.6-py3-none-any.whl", hash = "sha256:a1c50afd8d3fbbaf47a4873bc890dcb3c15da96f5c020327977d844d8731a2d4", size = 14453, upload-time = "2026-05-07T01:26:33.306Z" },
]
[[package]] [[package]]
name = "jsonpointer" name = "jsonpointer"
version = "3.0.0" version = "3.0.0"
@@ -1842,6 +1860,25 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
] ]
[[package]]
name = "mistralai"
version = "2.4.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "eval-type-backport" },
{ name = "httpx" },
{ name = "jsonpath-python" },
{ name = "opentelemetry-api" },
{ name = "opentelemetry-semantic-conventions" },
{ name = "pydantic" },
{ name = "python-dateutil" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8e/3f/5624d57c5897c83c55d3e4c7dd4127de42ad14fd3183e26566cdc7dca1bf/mistralai-2.4.5.tar.gz", hash = "sha256:ef165bb004ec4423cbf19a440bf0983ca0c3fc92ab12a35ebca097bdf418e33a", size = 424611, upload-time = "2026-05-07T11:46:43.888Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1b/48/2c5c4f853dec32a625c1a3d23809b80cf2e135c3441fe1764f72910dfea9/mistralai-2.4.5-py3-none-any.whl", hash = "sha256:bf3b6550258ab16dec8547b90e9c18bebf9099f55b7fc25a884bf0bbeffced0f", size = 995999, upload-time = "2026-05-07T11:46:41.915Z" },
]
[[package]] [[package]]
name = "mmh3" name = "mmh3"
version = "5.2.0" version = "5.2.0"
@@ -2104,6 +2141,7 @@ dependencies = [
{ name = "langchain-text-splitters" }, { name = "langchain-text-splitters" },
{ name = "markdownify" }, { name = "markdownify" },
{ name = "mcp", extra = ["cli"] }, { name = "mcp", extra = ["cli"] },
{ name = "mistralai" },
{ name = "openai" }, { name = "openai" },
{ name = "opentelemetry-api" }, { name = "opentelemetry-api" },
{ name = "opentelemetry-exporter-otlp-proto-grpc" }, { name = "opentelemetry-exporter-otlp-proto-grpc" },
@@ -2157,6 +2195,7 @@ requires-dist = [
{ name = "langchain-text-splitters", specifier = ">=1.0.0" }, { name = "langchain-text-splitters", specifier = ">=1.0.0" },
{ name = "markdownify", specifier = ">=0.14.1" }, { name = "markdownify", specifier = ">=0.14.1" },
{ name = "mcp", extras = ["cli"], specifier = ">=1.27,<1.28" }, { name = "mcp", extras = ["cli"], specifier = ">=1.27,<1.28" },
{ name = "mistralai", specifier = ">=2.4.5" },
{ name = "openai", specifier = ">=2.8.1" }, { name = "openai", specifier = ">=2.8.1" },
{ name = "opentelemetry-api", specifier = ">=1.28.2" }, { name = "opentelemetry-api", specifier = ">=1.28.2" },
{ name = "opentelemetry-exporter-otlp-proto-grpc", specifier = ">=1.28.2" }, { name = "opentelemetry-exporter-otlp-proto-grpc", specifier = ">=1.28.2" },
@@ -2341,45 +2380,45 @@ wheels = [
[[package]] [[package]]
name = "opentelemetry-api" name = "opentelemetry-api"
version = "1.39.0" version = "1.39.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "importlib-metadata" }, { name = "importlib-metadata" },
{ name = "typing-extensions" }, { name = "typing-extensions" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/c0/0b/e5428c009d4d9af0515b0a8371a8aaae695371af291f45e702f7969dce6b/opentelemetry_api-1.39.0.tar.gz", hash = "sha256:6130644268c5ac6bdffaf660ce878f10906b3e789f7e2daa5e169b047a2933b9", size = 65763, upload-time = "2025-12-03T13:19:56.378Z" } sdist = { url = "https://files.pythonhosted.org/packages/97/b9/3161be15bb8e3ad01be8be5a968a9237c3027c5be504362ff800fca3e442/opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c", size = 65767, upload-time = "2025-12-11T13:32:39.182Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/05/85/d831a9bc0a9e0e1a304ff3d12c1489a5fbc9bf6690a15dcbdae372bbca45/opentelemetry_api-1.39.0-py3-none-any.whl", hash = "sha256:3c3b3ca5c5687b1b5b37e5c5027ff68eacea8675241b29f13110a8ffbb8f0459", size = 66357, upload-time = "2025-12-03T13:19:33.043Z" }, { url = "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356, upload-time = "2025-12-11T13:32:17.304Z" },
] ]
[[package]] [[package]]
name = "opentelemetry-exporter-otlp" name = "opentelemetry-exporter-otlp"
version = "1.39.0" version = "1.39.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "opentelemetry-exporter-otlp-proto-grpc" }, { name = "opentelemetry-exporter-otlp-proto-grpc" },
{ name = "opentelemetry-exporter-otlp-proto-http" }, { name = "opentelemetry-exporter-otlp-proto-http" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/13/be/0e9d889f47e55cadc4041e5b53d4e0cc688f9a74811134fb0ba7cbee6905/opentelemetry_exporter_otlp-1.39.0.tar.gz", hash = "sha256:b405da0287b895fe4e2450dedb2a5b072debba1dfcfed5bdb3d1d183d8daa296", size = 6146, upload-time = "2025-12-03T13:19:58.381Z" } sdist = { url = "https://files.pythonhosted.org/packages/30/9c/3ab1db90f32da200dba332658f2bbe602369e3d19f6aba394031a42635be/opentelemetry_exporter_otlp-1.39.1.tar.gz", hash = "sha256:7cf7470e9fd0060c8a38a23e4f695ac686c06a48ad97f8d4867bc9b420180b9c", size = 6147, upload-time = "2025-12-11T13:32:40.309Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/fc/35/212d2cae4fa9a2c02e74438612268b640ab577b8ccb04590371eb4e0f542/opentelemetry_exporter_otlp-1.39.0-py3-none-any.whl", hash = "sha256:fe155d6968d581b325574ad6dc267c8de299397b18d11feeda2206d0a47928a9", size = 7017, upload-time = "2025-12-03T13:19:35.686Z" }, { url = "https://files.pythonhosted.org/packages/00/6c/bdc82a066e6fb1dcf9e8cc8d4e026358fe0f8690700cc6369a6bf9bd17a7/opentelemetry_exporter_otlp-1.39.1-py3-none-any.whl", hash = "sha256:68ae69775291f04f000eb4b698ff16ff685fdebe5cb52871bc4e87938a7b00fe", size = 7019, upload-time = "2025-12-11T13:32:19.387Z" },
] ]
[[package]] [[package]]
name = "opentelemetry-exporter-otlp-proto-common" name = "opentelemetry-exporter-otlp-proto-common"
version = "1.39.0" version = "1.39.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "opentelemetry-proto" }, { name = "opentelemetry-proto" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/11/cb/3a29ce606b10c76d413d6edd42d25a654af03e73e50696611e757d2602f3/opentelemetry_exporter_otlp_proto_common-1.39.0.tar.gz", hash = "sha256:a135fceed1a6d767f75be65bd2845da344dd8b9258eeed6bc48509d02b184409", size = 20407, upload-time = "2025-12-03T13:19:59.003Z" } sdist = { url = "https://files.pythonhosted.org/packages/e9/9d/22d241b66f7bbde88a3bfa6847a351d2c46b84de23e71222c6aae25c7050/opentelemetry_exporter_otlp_proto_common-1.39.1.tar.gz", hash = "sha256:763370d4737a59741c89a67b50f9e39271639ee4afc999dadfe768541c027464", size = 20409, upload-time = "2025-12-11T13:32:40.885Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/c6/215edba62d13a3948c718b289539f70e40965bc37fc82ecd55bb0b749c1a/opentelemetry_exporter_otlp_proto_common-1.39.0-py3-none-any.whl", hash = "sha256:3d77be7c4bdf90f1a76666c934368b8abed730b5c6f0547a2ec57feb115849ac", size = 18367, upload-time = "2025-12-03T13:19:36.906Z" }, { url = "https://files.pythonhosted.org/packages/8c/02/ffc3e143d89a27ac21fd557365b98bd0653b98de8a101151d5805b5d4c33/opentelemetry_exporter_otlp_proto_common-1.39.1-py3-none-any.whl", hash = "sha256:08f8a5862d64cc3435105686d0216c1365dc5701f86844a8cd56597d0c764fde", size = 18366, upload-time = "2025-12-11T13:32:20.2Z" },
] ]
[[package]] [[package]]
name = "opentelemetry-exporter-otlp-proto-grpc" name = "opentelemetry-exporter-otlp-proto-grpc"
version = "1.39.0" version = "1.39.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "googleapis-common-protos" }, { name = "googleapis-common-protos" },
@@ -2390,14 +2429,14 @@ dependencies = [
{ name = "opentelemetry-sdk" }, { name = "opentelemetry-sdk" },
{ name = "typing-extensions" }, { name = "typing-extensions" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/7e/62/4db083ee9620da3065eeb559e9fc128f41a1d15e7c48d7c83aafbccd354c/opentelemetry_exporter_otlp_proto_grpc-1.39.0.tar.gz", hash = "sha256:7e7bb3f436006836c0e0a42ac619097746ad5553ad7128a5bd4d3e727f37fc06", size = 24650, upload-time = "2025-12-03T13:20:00.06Z" } sdist = { url = "https://files.pythonhosted.org/packages/53/48/b329fed2c610c2c32c9366d9dc597202c9d1e58e631c137ba15248d8850f/opentelemetry_exporter_otlp_proto_grpc-1.39.1.tar.gz", hash = "sha256:772eb1c9287485d625e4dbe9c879898e5253fea111d9181140f51291b5fec3ad", size = 24650, upload-time = "2025-12-11T13:32:41.429Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/56/e8/d420b94ffddfd8cff85bb4aa5d98da26ce7935dc3cf3eca6b83cd39ab436/opentelemetry_exporter_otlp_proto_grpc-1.39.0-py3-none-any.whl", hash = "sha256:758641278050de9bb895738f35ff8840e4a47685b7e6ef4a201fe83196ba7a05", size = 19765, upload-time = "2025-12-03T13:19:38.143Z" }, { url = "https://files.pythonhosted.org/packages/81/a3/cc9b66575bd6597b98b886a2067eea2693408d2d5f39dad9ab7fc264f5f3/opentelemetry_exporter_otlp_proto_grpc-1.39.1-py3-none-any.whl", hash = "sha256:fa1c136a05c7e9b4c09f739469cbdb927ea20b34088ab1d959a849b5cc589c18", size = 19766, upload-time = "2025-12-11T13:32:21.027Z" },
] ]
[[package]] [[package]]
name = "opentelemetry-exporter-otlp-proto-http" name = "opentelemetry-exporter-otlp-proto-http"
version = "1.39.0" version = "1.39.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "googleapis-common-protos" }, { name = "googleapis-common-protos" },
@@ -2408,14 +2447,14 @@ dependencies = [
{ name = "requests" }, { name = "requests" },
{ name = "typing-extensions" }, { name = "typing-extensions" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/81/dc/1e9bf3f6a28e29eba516bc0266e052996d02bc7e92675f3cd38169607609/opentelemetry_exporter_otlp_proto_http-1.39.0.tar.gz", hash = "sha256:28d78fc0eb82d5a71ae552263d5012fa3ebad18dfd189bf8d8095ba0e65ee1ed", size = 17287, upload-time = "2025-12-03T13:20:01.134Z" } sdist = { url = "https://files.pythonhosted.org/packages/80/04/2a08fa9c0214ae38880df01e8bfae12b067ec0793446578575e5080d6545/opentelemetry_exporter_otlp_proto_http-1.39.1.tar.gz", hash = "sha256:31bdab9745c709ce90a49a0624c2bd445d31a28ba34275951a6a362d16a0b9cb", size = 17288, upload-time = "2025-12-11T13:32:42.029Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/bc/46/e4a102e17205bb05a50dbf24ef0e92b66b648cd67db9a68865af06a242fd/opentelemetry_exporter_otlp_proto_http-1.39.0-py3-none-any.whl", hash = "sha256:5789cb1375a8b82653328c0ce13a054d285f774099faf9d068032a49de4c7862", size = 19639, upload-time = "2025-12-03T13:19:39.536Z" }, { url = "https://files.pythonhosted.org/packages/95/f1/b27d3e2e003cd9a3592c43d099d2ed8d0a947c15281bf8463a256db0b46c/opentelemetry_exporter_otlp_proto_http-1.39.1-py3-none-any.whl", hash = "sha256:d9f5207183dd752a412c4cd564ca8875ececba13be6e9c6c370ffb752fd59985", size = 19641, upload-time = "2025-12-11T13:32:22.248Z" },
] ]
[[package]] [[package]]
name = "opentelemetry-instrumentation" name = "opentelemetry-instrumentation"
version = "0.60b0" version = "0.60b1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "opentelemetry-api" }, { name = "opentelemetry-api" },
@@ -2423,14 +2462,14 @@ dependencies = [
{ name = "packaging" }, { name = "packaging" },
{ name = "wrapt" }, { name = "wrapt" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/55/3c/bd53dbb42eff93d18e3047c7be11224aa9966ce98ac4cc5bfb860a32c95a/opentelemetry_instrumentation-0.60b0.tar.gz", hash = "sha256:4e9fec930f283a2677a2217754b40aaf9ef76edae40499c165bc7f1d15366a74", size = 31707, upload-time = "2025-12-03T13:22:00.352Z" } sdist = { url = "https://files.pythonhosted.org/packages/41/0f/7e6b713ac117c1f5e4e3300748af699b9902a2e5e34c9cf443dde25a01fa/opentelemetry_instrumentation-0.60b1.tar.gz", hash = "sha256:57ddc7974c6eb35865af0426d1a17132b88b2ed8586897fee187fd5b8944bd6a", size = 31706, upload-time = "2025-12-11T13:36:42.515Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/7b/5b5b9f8cfe727a28553acf9cd287b1d7f706f5c0a00d6e482df55b169483/opentelemetry_instrumentation-0.60b0-py3-none-any.whl", hash = "sha256:aaafa1483543a402819f1bdfb06af721c87d60dd109501f9997332862a35c76a", size = 33096, upload-time = "2025-12-03T13:20:51.785Z" }, { url = "https://files.pythonhosted.org/packages/77/d2/6788e83c5c86a2690101681aeef27eeb2a6bf22df52d3f263a22cee20915/opentelemetry_instrumentation-0.60b1-py3-none-any.whl", hash = "sha256:04480db952b48fb1ed0073f822f0ee26012b7be7c3eac1a3793122737c78632d", size = 33096, upload-time = "2025-12-11T13:35:33.067Z" },
] ]
[[package]] [[package]]
name = "opentelemetry-instrumentation-asgi" name = "opentelemetry-instrumentation-asgi"
version = "0.60b0" version = "0.60b1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "asgiref" }, { name = "asgiref" },
@@ -2439,14 +2478,14 @@ dependencies = [
{ name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-semantic-conventions" },
{ name = "opentelemetry-util-http" }, { name = "opentelemetry-util-http" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/b0/0a/715ea7044708d3c215385fb2a1c6ffe429aacb3cd23a348060aaeda52834/opentelemetry_instrumentation_asgi-0.60b0.tar.gz", hash = "sha256:928731218050089dca69f0fe980b8bfe109f384be8b89802d7337372ddb67b91", size = 26083, upload-time = "2025-12-03T13:22:05.672Z" } sdist = { url = "https://files.pythonhosted.org/packages/77/db/851fa88db7441da82d50bd80f2de5ee55213782e25dc858e04d0c9961d60/opentelemetry_instrumentation_asgi-0.60b1.tar.gz", hash = "sha256:16bfbe595cd24cda309a957456d0fc2523f41bc7b076d1f2d7e98a1ad9876d6f", size = 26107, upload-time = "2025-12-11T13:36:47.015Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/9b/8c/c6c59127fd996107243ca45669355665a7daff578ddafb86d6d2d3b01428/opentelemetry_instrumentation_asgi-0.60b0-py3-none-any.whl", hash = "sha256:9d76a541269452c718a0384478f3291feb650c5a3f29e578fdc6613ea3729cf3", size = 16907, upload-time = "2025-12-03T13:20:58.962Z" }, { url = "https://files.pythonhosted.org/packages/76/76/1fb94367cef64420d2171157a6b9509582873bd09a6afe08a78a8d1f59d9/opentelemetry_instrumentation_asgi-0.60b1-py3-none-any.whl", hash = "sha256:d48def2dbed10294c99cfcf41ebbd0c414d390a11773a41f472d20000fcddc25", size = 16933, upload-time = "2025-12-11T13:35:40.462Z" },
] ]
[[package]] [[package]]
name = "opentelemetry-instrumentation-httpx" name = "opentelemetry-instrumentation-httpx"
version = "0.60b0" version = "0.60b1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "opentelemetry-api" }, { name = "opentelemetry-api" },
@@ -2455,70 +2494,70 @@ dependencies = [
{ name = "opentelemetry-util-http" }, { name = "opentelemetry-util-http" },
{ name = "wrapt" }, { name = "wrapt" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/09/71/9dc0bc5ab14122251f520ffe9fc8dd892ca688d7d591482b5b1843d685d2/opentelemetry_instrumentation_httpx-0.60b0.tar.gz", hash = "sha256:fcf349a92fb0b941a2a18bec65141f4ba62cbf7a457a1aa580794bad44dc477c", size = 20612, upload-time = "2025-12-03T13:22:20.5Z" } sdist = { url = "https://files.pythonhosted.org/packages/86/08/11208bcfcab4fc2023252c3f322aa397fd9ad948355fea60f5fc98648603/opentelemetry_instrumentation_httpx-0.60b1.tar.gz", hash = "sha256:a506ebaf28c60112cbe70ad4f0338f8603f148938cb7b6794ce1051cd2b270ae", size = 20611, upload-time = "2025-12-11T13:37:01.661Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/3a/22/a340c8bfad6f31bfd6a15b0b24d5e68e05e3975e4dbdc3cea6ec4f96e060/opentelemetry_instrumentation_httpx-0.60b0-py3-none-any.whl", hash = "sha256:3f5e6fc4ddf1d9de2aaddb5255110827154dbd4de9187da906c8c2a3cc2219e9", size = 15702, upload-time = "2025-12-03T13:21:20.667Z" }, { url = "https://files.pythonhosted.org/packages/43/59/b98e84eebf745ffc75397eaad4763795bff8a30cbf2373a50ed4e70646c5/opentelemetry_instrumentation_httpx-0.60b1-py3-none-any.whl", hash = "sha256:f37636dd742ad2af83d896ba69601ed28da51fa4e25d1ab62fde89ce413e275b", size = 15701, upload-time = "2025-12-11T13:36:04.56Z" },
] ]
[[package]] [[package]]
name = "opentelemetry-instrumentation-logging" name = "opentelemetry-instrumentation-logging"
version = "0.60b0" version = "0.60b1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "opentelemetry-api" }, { name = "opentelemetry-api" },
{ name = "opentelemetry-instrumentation" }, { name = "opentelemetry-instrumentation" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/5b/8b/a1aed0b695a58a1f0bdcf90fae5e468cc0fd7de2b74b9816b17e45c38e43/opentelemetry_instrumentation_logging-0.60b0.tar.gz", hash = "sha256:6d87840666669cbbcd53d2230c7a33476862d0bf7f1adc67a95519c6ccfc8281", size = 9969, upload-time = "2025-12-03T13:22:22.608Z" } sdist = { url = "https://files.pythonhosted.org/packages/60/a6/4515895b383113677fd2ad21813df5e56108a2df14ebb7916c962c9a0234/opentelemetry_instrumentation_logging-0.60b1.tar.gz", hash = "sha256:98f4b9c7aeb9314a30feee7c002c7ea9abea07c90df5f97fb058b850bc45b89a", size = 9968, upload-time = "2025-12-11T13:37:03.974Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/00/89/6d4f5d8d03376637bff5b8d9e91356104a6f1ce9a34a2099bb2f4bf5e2b5/opentelemetry_instrumentation_logging-0.60b0-py3-none-any.whl", hash = "sha256:af75b3020911b9b6a1b4b19819a165eb131ce9bfdd313062d578fc2dc9a5cd0f", size = 12576, upload-time = "2025-12-03T13:21:24.797Z" }, { url = "https://files.pythonhosted.org/packages/f1/f9/8a4ce3901bc52277794e4b18c4ac43dc5929806eff01d22812364132f45f/opentelemetry_instrumentation_logging-0.60b1-py3-none-any.whl", hash = "sha256:f2e18cbc7e1dd3628c80e30d243897fdc93c5b7e0c8ae60abd2b9b6a99f82343", size = 12577, upload-time = "2025-12-11T13:36:08.123Z" },
] ]
[[package]] [[package]]
name = "opentelemetry-proto" name = "opentelemetry-proto"
version = "1.39.0" version = "1.39.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "protobuf" }, { name = "protobuf" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/48/b5/64d2f8c3393cd13ea2092106118f7b98461ba09333d40179a31444c6f176/opentelemetry_proto-1.39.0.tar.gz", hash = "sha256:c1fa48678ad1a1624258698e59be73f990b7fc1f39e73e16a9d08eef65dd838c", size = 46153, upload-time = "2025-12-03T13:20:08.729Z" } sdist = { url = "https://files.pythonhosted.org/packages/49/1d/f25d76d8260c156c40c97c9ed4511ec0f9ce353f8108ca6e7561f82a06b2/opentelemetry_proto-1.39.1.tar.gz", hash = "sha256:6c8e05144fc0d3ed4d22c2289c6b126e03bcd0e6a7da0f16cedd2e1c2772e2c8", size = 46152, upload-time = "2025-12-11T13:32:48.681Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/e3/4d/d500e1862beed68318705732d1976c390f4a72ca8009c4983ff627acff20/opentelemetry_proto-1.39.0-py3-none-any.whl", hash = "sha256:1e086552ac79acb501485ff0ce75533f70f3382d43d0a30728eeee594f7bf818", size = 72534, upload-time = "2025-12-03T13:19:50.251Z" }, { url = "https://files.pythonhosted.org/packages/51/95/b40c96a7b5203005a0b03d8ce8cd212ff23f1793d5ba289c87a097571b18/opentelemetry_proto-1.39.1-py3-none-any.whl", hash = "sha256:22cdc78efd3b3765d09e68bfbd010d4fc254c9818afd0b6b423387d9dee46007", size = 72535, upload-time = "2025-12-11T13:32:33.866Z" },
] ]
[[package]] [[package]]
name = "opentelemetry-sdk" name = "opentelemetry-sdk"
version = "1.39.0" version = "1.39.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "opentelemetry-api" }, { name = "opentelemetry-api" },
{ name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-semantic-conventions" },
{ name = "typing-extensions" }, { name = "typing-extensions" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/51/e3/7cd989003e7cde72e0becfe830abff0df55c69d237ee7961a541e0167833/opentelemetry_sdk-1.39.0.tar.gz", hash = "sha256:c22204f12a0529e07aa4d985f1bca9d6b0e7b29fe7f03e923548ae52e0e15dde", size = 171322, upload-time = "2025-12-03T13:20:09.651Z" } sdist = { url = "https://files.pythonhosted.org/packages/eb/fb/c76080c9ba07e1e8235d24cdcc4d125ef7aa3edf23eb4e497c2e50889adc/opentelemetry_sdk-1.39.1.tar.gz", hash = "sha256:cf4d4563caf7bff906c9f7967e2be22d0d6b349b908be0d90fb21c8e9c995cc6", size = 171460, upload-time = "2025-12-11T13:32:49.369Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/a4/b4/2adc8bc83eb1055ecb592708efb6f0c520cc2eb68970b02b0f6ecda149cf/opentelemetry_sdk-1.39.0-py3-none-any.whl", hash = "sha256:90cfb07600dfc0d2de26120cebc0c8f27e69bf77cd80ef96645232372709a514", size = 132413, upload-time = "2025-12-03T13:19:51.364Z" }, { url = "https://files.pythonhosted.org/packages/7c/98/e91cf858f203d86f4eccdf763dcf01cf03f1dae80c3750f7e635bfa206b6/opentelemetry_sdk-1.39.1-py3-none-any.whl", hash = "sha256:4d5482c478513ecb0a5d938dcc61394e647066e0cc2676bee9f3af3f3f45f01c", size = 132565, upload-time = "2025-12-11T13:32:35.069Z" },
] ]
[[package]] [[package]]
name = "opentelemetry-semantic-conventions" name = "opentelemetry-semantic-conventions"
version = "0.60b0" version = "0.60b1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "opentelemetry-api" }, { name = "opentelemetry-api" },
{ name = "typing-extensions" }, { name = "typing-extensions" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/71/0e/176a7844fe4e3cb5de604212094dffaed4e18b32f1c56b5258bcbcba85c2/opentelemetry_semantic_conventions-0.60b0.tar.gz", hash = "sha256:227d7aa73cbb8a2e418029d6b6465553aa01cf7e78ec9d0bc3255c7b3ac5bf8f", size = 137935, upload-time = "2025-12-03T13:20:12.395Z" } sdist = { url = "https://files.pythonhosted.org/packages/91/df/553f93ed38bf22f4b999d9be9c185adb558982214f33eae539d3b5cd0858/opentelemetry_semantic_conventions-0.60b1.tar.gz", hash = "sha256:87c228b5a0669b748c76d76df6c364c369c28f1c465e50f661e39737e84bc953", size = 137935, upload-time = "2025-12-11T13:32:50.487Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/d0/56/af0306666f91bae47db14d620775604688361f0f76a872e0005277311131/opentelemetry_semantic_conventions-0.60b0-py3-none-any.whl", hash = "sha256:069530852691136018087b52688857d97bba61cd641d0f8628d2d92788c4f78a", size = 219981, upload-time = "2025-12-03T13:19:53.585Z" }, { url = "https://files.pythonhosted.org/packages/7a/5e/5958555e09635d09b75de3c4f8b9cae7335ca545d77392ffe7331534c402/opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl", hash = "sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb", size = 219982, upload-time = "2025-12-11T13:32:36.955Z" },
] ]
[[package]] [[package]]
name = "opentelemetry-util-http" name = "opentelemetry-util-http"
version = "0.60b0" version = "0.60b1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/38/0d/786a713445cf338131fef3a84fab1378e4b2ef3c3ea348eeb0c915eb804a/opentelemetry_util_http-0.60b0.tar.gz", hash = "sha256:e42b7bb49bba43b6f34390327d97e5016eb1c47949ceaf37c4795472a4e3a82d", size = 10576, upload-time = "2025-12-03T13:22:41.224Z" } sdist = { url = "https://files.pythonhosted.org/packages/50/fc/c47bb04a1d8a941a4061307e1eddfa331ed4d0ab13d8a9781e6db256940a/opentelemetry_util_http-0.60b1.tar.gz", hash = "sha256:0d97152ca8c8a41ced7172d29d3622a219317f74ae6bb3027cfbdcf22c3cc0d6", size = 11053, upload-time = "2025-12-11T13:37:25.115Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/53/5d/a448862f6d10c95685ed0e703596b6bd1784074e7ad90bffdc550abb7b68/opentelemetry_util_http-0.60b0-py3-none-any.whl", hash = "sha256:4f366f1a48adb74ffa6f80aee26f96882e767e01b03cd1cfb948b6e1020341fe", size = 8742, upload-time = "2025-12-03T13:21:54.553Z" }, { url = "https://files.pythonhosted.org/packages/16/5c/d3f1733665f7cd582ef0842fb1d2ed0bc1fba10875160593342d22bba375/opentelemetry_util_http-0.60b1-py3-none-any.whl", hash = "sha256:66381ba28550c91bee14dcba8979ace443444af1ed609226634596b4b0faf199", size = 8947, upload-time = "2025-12-11T13:36:37.151Z" },
] ]
[[package]] [[package]]
@@ -3314,7 +3353,7 @@ wheels = [
[[package]] [[package]]
name = "pytest-otel" name = "pytest-otel"
version = "2.0.1" version = "2.0.3"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "opentelemetry-api" }, { name = "opentelemetry-api" },
@@ -3322,9 +3361,9 @@ dependencies = [
{ name = "opentelemetry-sdk" }, { name = "opentelemetry-sdk" },
{ name = "pytest" }, { name = "pytest" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/ca/5e/771f8dbdf55ae57603d89bb26e046c13e8bee3492c74cb3afb6044166e9a/pytest_otel-2.0.1.tar.gz", hash = "sha256:3d529dc34105862cca39fd1258d00dd3d17f3b7d92ebf0953d55326bb017af3d", size = 17880, upload-time = "2025-12-08T14:56:40.393Z" } sdist = { url = "https://files.pythonhosted.org/packages/98/14/e4f7cb90c4c93dbdd85034fd65e7a16da328bd71c6ca5270e4f8d37fa4ba/pytest_otel-2.0.3.tar.gz", hash = "sha256:782985fef50acc6922db5e69da4584b384d785c863a96ebc11eaf2a7fef5189c", size = 18587, upload-time = "2026-01-02T16:12:20.394Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/16/fa/0cfe23bac571f68b4f427ad7ae313ddd8de881ec2ff49dcea196a2a96592/pytest_otel-2.0.1-py2.py3-none-any.whl", hash = "sha256:501f36f02f55578ca34c3ccbe55e81cc1e14bb130da13f7cf0b6480d54a9e5db", size = 14530, upload-time = "2025-12-08T14:56:41.731Z" }, { url = "https://files.pythonhosted.org/packages/ac/f8/d9b93b41b299c7e14b47887eb31fb4cb77d771c2e9f3e0bcac0750f62699/pytest_otel-2.0.3-py2.py3-none-any.whl", hash = "sha256:3c6c331e943609ad7df7c718714090dee91211340f133a8798217e72e3b2cd67", size = 14914, upload-time = "2026-01-02T16:12:21.946Z" },
] ]
[[package]] [[package]]