Spaces:
Running
Running
| from __future__ import annotations | |
| from time import sleep | |
| from typing import Iterable | |
| from ..config import config | |
| MAX_EMBEDDING_ATTEMPTS = 3 | |
| class EmbeddingError(RuntimeError): | |
| pass | |
| class OpenRouterEmbeddingClient: | |
| def __init__(self) -> None: | |
| self._client = None | |
| def client(self): | |
| if self._client is None: | |
| if not config.processing.EMBEDDING_API_KEY: | |
| raise EmbeddingError("OPEN_ROUTER_API_KEY is not configured for embeddings") | |
| from openai import OpenAI | |
| self._client = OpenAI( | |
| api_key=config.processing.EMBEDDING_API_KEY, | |
| base_url=config.processing.EMBEDDING_BASE_URL, | |
| ) | |
| return self._client | |
| def embed_query(self, text: str) -> list[float]: | |
| return self.embed_documents([text])[0] | |
| def embed_documents(self, texts: Iterable[str]) -> list[list[float]]: | |
| clean_texts = [(text or " ").strip() or " " for text in texts] | |
| if not clean_texts: | |
| return [] | |
| last_error: Exception | None = None | |
| for attempt in range(1, MAX_EMBEDDING_ATTEMPTS + 1): | |
| try: | |
| response = self.client.embeddings.create( | |
| model=config.processing.EMBEDDING_MODEL, | |
| input=clean_texts, | |
| ) | |
| embeddings = self._extract_embeddings(response) | |
| self._validate_embeddings(embeddings, expected_count=len(clean_texts)) | |
| return embeddings | |
| except Exception as exc: | |
| last_error = exc | |
| if attempt == MAX_EMBEDDING_ATTEMPTS or not self._is_retryable(exc): | |
| break | |
| sleep(min(2 ** (attempt - 1), 8)) | |
| raise EmbeddingError(f"Failed to generate OpenRouter embeddings: {last_error}") from last_error | |
| def _extract_embeddings(response) -> list[list[float]]: | |
| data = list(getattr(response, "data", []) or []) | |
| data.sort(key=lambda item: getattr(item, "index", 0)) | |
| return [list(getattr(item, "embedding", []) or []) for item in data] | |
| def _validate_embeddings(self, embeddings: list[list[float]], expected_count: int) -> None: | |
| if len(embeddings) != expected_count: | |
| raise EmbeddingError( | |
| f"Embedding response count mismatch: expected {expected_count}, got {len(embeddings)}" | |
| ) | |
| for idx, embedding in enumerate(embeddings): | |
| if len(embedding) != config.processing.EMBEDDING_DIMENSIONS: | |
| raise EmbeddingError( | |
| f"Embedding {idx} has dimension {len(embedding)}; " | |
| f"expected {config.processing.EMBEDDING_DIMENSIONS}" | |
| ) | |
| def _is_retryable(error: Exception) -> bool: | |
| status_code = getattr(error, "status_code", None) | |
| if status_code in {408, 409, 425, 429, 500, 502, 503, 504}: | |
| return True | |
| text = str(error).lower() | |
| return any( | |
| signal in text | |
| for signal in [ | |
| "rate limit", | |
| "timeout", | |
| "temporarily unavailable", | |
| "connection", | |
| "server error", | |
| ] | |
| ) | |