orchestrata_ultimate.semantic_graph.embedders
Reference › orchestrata_ultimate › semantic_graph › embedders
The embedding backends for the semantic graph: a small protocol and two implementations behind one async interface, so ingestion does not need to know which is in use. Embedder is a runtime-checkable protocol with a dimension property and embed(texts, prompt_type), which returns one vector per text for a document or query prompt.
OpenRouterEmbedder is the cloud backend over OpenRouter's embeddings endpoint (default model mistralai/codestral-embed-2505, dimension 1536, 60-second timeout, batches of up to 96 texts). embed splits inputs with _split_batches, bounded by item count and estimated tokens, then _request_embeddings sends each batch through the recorded-cassette contract; only when that allows a live call does _request_embeddings_live reach the provider. _post_with_retry retries transient failures only.
SentenceTransformerEmbedder is the local backend (default Qwen/Qwen3-Embedding-0.6B, dimension 1024). _load_model loads the model lazily, _prompt_kwargs_for supplies the query or document prompt, and embed runs the model in a worker thread so the event loop is not blocked. Which backend a process gets is decided in embedder_factory. The module does not touch the graph.
At a glance
| Source | src/orchestrata_ultimate/semantic_graph/embedders.py |
| Lines of code | 400 |
| Classes | 3 |
| Functions | 0 |
| Imports (internal) | orchestrata_ultimate.llm._cassette, orchestrata_ultimate.semantic_graph.exceptions |
| Imported by | orchestrata_ultimate.semantic_graph.architecture_twin, orchestrata_ultimate.semantic_graph.embedder_factory, orchestrata_ultimate.semantic_graph.ingestion, scripts.ingest_orchestrata_ultimate, scripts.sync_architecture_semantic_twin |
| Tests that import it | tests/llm/test_payload_cassette.py, tests/scripts/test_ingest_orchestrata_ultimate.py, tests/scripts/test_sync_architecture_semantic_twin.py, tests/semantic_graph/test_architecture_twin.py, tests/semantic_graph/test_embedder_factory.py, tests/semantic_graph/test_embedders.py, tests/semantic_graph/test_ingestion.py |
Class diagram
Dependency diagram
API
Classes
Embedder (protocol)
Bases: Protocol · Decorators: runtime_checkable · Line 39
Protocol for text embedding backends.
| Method | Signature | Summary |
|---|---|---|
dimension property |
dimension() -> int |
Return the vector dimension produced by this embedder. |
embed |
async embed(texts: list[str], prompt_type: str = 'document') -> list[list[float]] |
Return one embedding vector for each input text. |
OpenRouterEmbedder
Line 65
Cloud embedding backend using the OpenRouter embeddings endpoint.
| Method | Signature | Summary |
|---|---|---|
__init__ |
__init__(api_key: str, model: str = 'mistralai/codestral-embed-2505', dimension: int = 1536, base_url: str = 'https://openrouter.ai/api/v1', timeout: float = 60.0, batch_size: int = 96, max_tokens_per_batch: int = 100000, env: Mapping[str, str] | None = None, retry_attempts: int = RETRY_ATTEMPTS, r… |
Initialize the embedder. |
dimension property |
dimension() -> int |
|
embed |
async embed(texts: list[str], prompt_type: str = 'document') -> list[list[float]] |
Embed texts via OpenRouter in bounded batches. |
_split_batches |
_split_batches(texts: list[str]) -> list[list[str]] |
Split texts into batches bounded by count and estimated tokens. |
_request_embeddings |
async _request_embeddings(texts: list[str]) -> list[list[float]] |
Embeddings for texts, through the cassette contract. |
_post_with_retry |
async _post_with_retry(url: str, headers: dict[str, str], payload: dict[str, Any]) -> Any |
POST the embeddings request, retrying only transient failures. |
_request_embeddings_live |
async _request_embeddings_live(texts: list[str]) -> list[list[float]] |
The real provider call. |
SentenceTransformerEmbedder
Line 301
Local embedding backend using sentence-transformers.
| Method | Signature | Summary |
|---|---|---|
__init__ |
__init__(model_name: str = 'Qwen/Qwen3-Embedding-0.6B', dimension: int = 1024, device: str | None = None, query_prompt: str | None = None, document_prompt: str = '', trust_remote_code: bool = True, batch_size: int = 32, normalize_embeddings: bool = True) -> None |
Initialize the embedder. |
dimension property |
dimension() -> int |
|
embed |
async embed(texts: list[str], prompt_type: str = 'document') -> list[list[float]] |
Embed texts locally, running the model in a worker thread. |
_load_model |
_load_model() -> Any |
Lazy-load the sentence-transformers model. |
_prompt_kwargs_for |
_prompt_kwargs_for(prompt_type: str) -> dict[str, str] |
Return sentence-transformers prompt arguments for prompt_type. |
Source
"""Embedding backends for the semantic graph.
Provides a small protocol plus two concrete implementations:
* :class:`OpenRouterEmbedder` - cloud embeddings via OpenRouter (Codestral).
* :class:`SentenceTransformerEmbedder` - local GPU embeddings via
``sentence-transformers`` (Qwen3-Embedding, etc.).
Both expose the same async interface so ingestion code does not need to know
which backend is in use.
"""
from __future__ import annotations
import asyncio
import logging
from collections.abc import Mapping
from typing import Any, Protocol, runtime_checkable
import anyio
import anyio.to_thread
import httpx
from orchestrata_ultimate.semantic_graph.exceptions import VectorSearchError
logger = logging.getLogger(__name__)
#: Attempts per embeddings request, including the first. A corpus ingest issues
#: thousands of requests, so a single un-retried transient ends the whole job:
#: request 330 of a 2026-08-09 run failed on an empty-message
#: httpx.RequestError (a read timeout) after 329 consecutive 200s, discarding
#: everything embedded up to that point.
RETRY_ATTEMPTS = 4
#: Seconds before the first retry; doubles after each attempt.
RETRY_BACKOFF_SECONDS = 1.0
@runtime_checkable
class Embedder(Protocol):
"""Protocol for text embedding backends."""
@property
def dimension(self) -> int:
"""Return the vector dimension produced by this embedder."""
...
async def embed(
self, texts: list[str], prompt_type: str = "document"
) -> list[list[float]]:
"""Return one embedding vector for each input text.
Args:
texts: Input strings to embed. Empty strings are valid and must
return a vector of ``self.dimension`` floats.
prompt_type: Semantic role of the texts. Typical values are
``"document"`` and ``"query"``. Backends that do not use
prompt prefixes ignore this value.
Returns:
A list of embedding vectors parallel to ``texts``.
"""
...
class OpenRouterEmbedder:
"""Cloud embedding backend using the OpenRouter embeddings endpoint.
Defaults to ``mistralai/codestral-embed-2505`` (1536 dimensions). The
caller must supply an API key; the class never reads environment variables
directly.
"""
def __init__(
self,
api_key: str,
model: str = "mistralai/codestral-embed-2505",
dimension: int = 1536,
base_url: str = "https://openrouter.ai/api/v1",
timeout: float = 60.0,
batch_size: int = 96,
max_tokens_per_batch: int = 100_000,
env: Mapping[str, str] | None = None,
retry_attempts: int = RETRY_ATTEMPTS,
retry_backoff_seconds: float = RETRY_BACKOFF_SECONDS,
) -> None:
"""Initialize the embedder.
Args:
api_key: OpenRouter API key.
model: Model identifier on OpenRouter.
dimension: Expected dimension of returned vectors.
base_url: OpenRouter API base URL.
timeout: HTTP request timeout in seconds.
batch_size: Maximum texts per embedding request.
max_tokens_per_batch: Approximate token ceiling per request. Batches
are split further when the estimated total exceeds this value.
retry_attempts: Attempts per request, including the first. Only
transient failures consume one; a refusal fails at once.
retry_backoff_seconds: Delay before the first retry, doubling
after each. Tests pass a small value rather than reassigning a
class attribute.
env: Environment the cassette contract is read from. Defaults to
the process environment. Pass an explicit mapping WITHOUT
``EMBEDDING_CASSETTE_DIR`` to force the real HTTP path — which
is what the error-path tests need, since their subject is this
client's own failure handling against a local dead port and
never a provider.
"""
if not api_key:
raise VectorSearchError("OpenRouter API key is required")
self._api_key = api_key
self._model = model
self._dimension = dimension
self._base_url = base_url.rstrip("/")
self._timeout = timeout
self._batch_size = max(1, batch_size)
self._max_tokens_per_batch = max(1, max_tokens_per_batch)
self._env = env
self._retry_attempts = max(1, retry_attempts)
self._retry_backoff_seconds = max(0.0, retry_backoff_seconds)
@property
def dimension(self) -> int:
return self._dimension
async def embed(
self, texts: list[str], prompt_type: str = "document"
) -> list[list[float]]:
"""Embed texts via OpenRouter in bounded batches."""
del prompt_type # Codestral does not use prompt prefixes.
if not texts:
return []
results: list[list[float] | None] = [None] * len(texts)
semaphore = asyncio.Semaphore(4)
batches = self._split_batches(texts)
async def _embed_batch(offset: int, batch: list[str]) -> None:
async with semaphore:
embeddings = await self._request_embeddings(batch)
for idx, embedding in enumerate(embeddings):
results[offset + idx] = embedding
offset = 0
tasks = []
for batch in batches:
tasks.append(_embed_batch(offset, batch))
offset += len(batch)
await asyncio.gather(*tasks)
return [embedding for embedding in results if embedding is not None]
def _split_batches(self, texts: list[str]) -> list[list[str]]:
"""Split texts into batches bounded by count and estimated tokens.
The initial split is by ``batch_size``. Each chunk is then subdivided if
the estimated token count exceeds ``max_tokens_per_batch``. Token counts
are estimated as ``max(1, len(text) // 4)`` — conservative for code and
English prose and good enough to avoid provider batch limits.
"""
batches: list[list[str]] = []
for i in range(0, len(texts), self._batch_size):
chunk = texts[i : i + self._batch_size]
current: list[str] = []
current_tokens = 0
for text in chunk:
tokens = max(1, len(text) // 4)
if current and current_tokens + tokens > self._max_tokens_per_batch:
batches.append(current)
current = [text]
current_tokens = tokens
else:
current.append(text)
current_tokens += tokens
if current:
batches.append(current)
return batches
async def _request_embeddings(self, texts: list[str]) -> list[list[float]]:
"""Embeddings for *texts*, through the cassette contract.
This path holds its own httpx client and never reaches
``llm/client.py``, so until the cassette grew a non-chat form every
embedding the suite computed was a real billed OpenRouter request.
"""
from orchestrata_ultimate.llm._cassette import ( # noqa: PLC0415
cassette_exchange,
synthetic_embeddings,
)
embeddings: list[list[float]] = await cassette_exchange(
kind="embeddings",
payload={"model": self._model, "input": list(texts)},
synth=lambda: synthetic_embeddings(texts, self._dimension),
live=lambda: self._request_embeddings_live(texts),
env=self._env,
)
return embeddings
#: Status codes worth trying again. Everything else — 400, 401, 402, 403 —
#: is a refusal that a retry reproduces exactly, at the cost of the wait.
RETRY_STATUS = frozenset({429, 500, 502, 503, 504})
async def _post_with_retry(
self,
url: str,
headers: dict[str, str],
payload: dict[str, Any],
) -> Any:
"""POST the embeddings request, retrying only transient failures.
Raises:
VectorSearchError: On a non-transient refusal, or once the
transient attempts are exhausted. The message names which of
the two happened, so a quota problem is never reported as
flakiness.
"""
delay = self._retry_backoff_seconds
for attempt in range(1, self._retry_attempts + 1):
last = attempt == self._retry_attempts
try:
async with httpx.AsyncClient(timeout=self._timeout) as client:
response = await client.post(url, headers=headers, json=payload)
response.raise_for_status()
return response.json()
except httpx.HTTPStatusError as exc:
status = exc.response.status_code if exc.response else 0
body = exc.response.text if exc.response else ""
if status not in self.RETRY_STATUS or last:
logger.error("OpenRouter embeddings request failed: %s", body)
raise VectorSearchError(
f"OpenRouter embeddings HTTP error {status or 'unknown'}"
) from exc
logger.warning(
"OpenRouter embeddings HTTP %s (attempt %s/%s); retrying in %.1fs",
status,
attempt,
self._retry_attempts,
delay,
)
except httpx.RequestError as exc:
if last:
logger.error("OpenRouter embeddings request failed: %s", exc)
raise VectorSearchError(
f"OpenRouter embeddings request failed after "
f"{self._retry_attempts} attempts: {exc!r}"
) from exc
logger.warning(
"OpenRouter embeddings transport error %r (attempt %s/%s); "
"retrying in %.1fs",
exc,
attempt,
self._retry_attempts,
delay,
)
await asyncio.sleep(delay)
delay *= 2
# Unreachable: the final attempt either returns or raises above.
raise VectorSearchError("OpenRouter embeddings retry loop exhausted")
async def _request_embeddings_live(self, texts: list[str]) -> list[list[float]]:
"""The real provider call. Reached only when the cassette contract
says so — an unset LLM_CASSETTE_DIR, or an explicit re-record."""
headers = {
"Authorization": f"Bearer {self._api_key}",
"Content-Type": "application/json",
}
payload = {
"model": self._model,
"input": texts,
}
url = f"{self._base_url}/embeddings"
data = await self._post_with_retry(url, headers, payload)
try:
items = data["data"]
except KeyError as exc:
raise VectorSearchError(
f"OpenRouter embeddings response missing 'data': {data}"
) from exc
# Sort by index to preserve input order; some providers return results
# out of order under load.
indexed = sorted(
((item.get("index", 0), item["embedding"]) for item in items),
key=lambda pair: pair[0],
)
embeddings = [embedding for _, embedding in indexed]
if len(embeddings) != len(texts):
raise VectorSearchError(
f"OpenRouter returned {len(embeddings)} embeddings for {len(texts)} texts"
)
for embedding in embeddings:
if len(embedding) != self._dimension:
raise VectorSearchError(
f"Expected dimension {self._dimension}, got {len(embedding)}"
)
return embeddings
class SentenceTransformerEmbedder:
"""Local embedding backend using ``sentence-transformers``.
Defaults to ``Qwen/Qwen3-Embedding-0.6B`` on CUDA if available. The model
is loaded lazily on first use so import-time overhead stays low.
Query texts may use an optional prompt prefix; documents are embedded
without a prefix by default. Both prompts are configurable.
"""
def __init__(
self,
model_name: str = "Qwen/Qwen3-Embedding-0.6B",
dimension: int = 1024,
device: str | None = None,
query_prompt: str | None = None,
document_prompt: str = "",
trust_remote_code: bool = True,
batch_size: int = 32,
normalize_embeddings: bool = True,
) -> None:
"""Initialize the embedder.
Args:
model_name: Hugging Face model identifier or local path.
dimension: Expected vector dimension.
device: Torch device (``"cuda"``, ``"cpu"``, etc.). ``None`` picks
CUDA when available.
query_prompt: Prompt prefix for ``prompt_type="query"``. ``None``
uses ``prompt_name="query"`` if the model provides it.
document_prompt: Prompt prefix for ``prompt_type="document"``.
trust_remote_code: Passed to ``SentenceTransformer``.
batch_size: Encoding batch size.
normalize_embeddings: L2-normalize output vectors.
"""
self._model_name = model_name
self._dimension = dimension
self._device = device
self._query_prompt = query_prompt
self._document_prompt = document_prompt
self._trust_remote_code = trust_remote_code
self._batch_size = max(1, batch_size)
self._normalize_embeddings = normalize_embeddings
self._model: Any | None = None
@property
def dimension(self) -> int:
return self._dimension
async def embed(
self, texts: list[str], prompt_type: str = "document"
) -> list[list[float]]:
"""Embed texts locally, running the model in a worker thread."""
if not texts:
return []
model = self._load_model()
prompt_kwargs = self._prompt_kwargs_for(prompt_type)
def _encode() -> Any:
kwargs: dict[str, Any] = {
"batch_size": self._batch_size,
"normalize_embeddings": self._normalize_embeddings,
"show_progress_bar": False,
}
kwargs.update(prompt_kwargs)
return model.encode(texts, **kwargs)
embeddings = await anyio.to_thread.run_sync(_encode)
return [list(vector) for vector in embeddings]
def _load_model(self) -> Any:
"""Lazy-load the sentence-transformers model."""
if self._model is None:
from sentence_transformers import SentenceTransformer
device = self._device
if device is None:
import torch
device = "cuda" if torch.cuda.is_available() else "cpu"
self._model = SentenceTransformer(
self._model_name,
device=device,
trust_remote_code=self._trust_remote_code,
)
return self._model
def _prompt_kwargs_for(self, prompt_type: str) -> dict[str, str]:
"""Return sentence-transformers prompt arguments for ``prompt_type``."""
if prompt_type == "query":
if self._query_prompt is not None:
return {"prompt": self._query_prompt}
model = self._load_model()
if hasattr(model, "prompts") and "query" in model.prompts:
return {"prompt_name": "query"}
return {}
if prompt_type == "document" and self._document_prompt:
return {"prompt": self._document_prompt}
return {}