feat(semantic_search): support local OpenAI-compatible embedding servers (#22)
Adds a LocalEmbedder that targets any OpenAI-compatible embedding endpoint (Ollama, llama.cpp, vLLM, LM Studio, ...). Zero new Python dependencies — reuses the existing openai SDK with a custom base_url. Defaults to Ollama at http://localhost:11434/v1 with nomic-embed-text @ 768 dims; override via env vars for other servers/models (e.g. bge-m3 @ 1024 dims for better Turkish). Refactors the shared encode/similarity logic into a private base class so OpenRouterEmbedder and LocalEmbedder don't duplicate ~50 lines. OpenRouter keeps its ranking headers; local sends none. Adds get_embedder() factory selecting the provider based on EMBEDDING_PROVIDER (local) or OPENROUTER_API_KEY presence, and is_semantic_search_available() that returns True for either path. mcp_server_main now uses these so the semantic_search tool is exposed when only a local server is configured. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
42731a2c03
commit
fb29146755
+17
-4
@@ -74,18 +74,31 @@ JWT_SECRET_KEY=your_jwt_secret_key_here
|
|||||||
# SEMANTIC SEARCH SETTINGS (Optional)
|
# SEMANTIC SEARCH SETTINGS (Optional)
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
# OpenRouter API Key for semantic search functionality
|
# Embedding provider for the semantic_search tool.
|
||||||
|
# Pick exactly one of: OpenRouter (hosted) or Local (your own server).
|
||||||
|
|
||||||
|
# --- Option A: OpenRouter (hosted, default) -----------------------------------
|
||||||
# Get your API key from: https://openrouter.ai/keys
|
# Get your API key from: https://openrouter.ai/keys
|
||||||
# If not set, semantic search tool will be disabled
|
# If neither this nor EMBEDDING_PROVIDER=local is set, semantic search is off.
|
||||||
OPENROUTER_API_KEY=sk-or-v1-your_openrouter_api_key_here
|
OPENROUTER_API_KEY=sk-or-v1-your_openrouter_api_key_here
|
||||||
|
|
||||||
# Optional: override the embedding model and dimension.
|
# Optional: override the OpenRouter embedding model and dimension.
|
||||||
# Defaults: google/gemini-embedding-001 at 3072 dims (paid on OpenRouter).
|
# Defaults: google/gemini-embedding-001 at 3072 dims (paid on OpenRouter).
|
||||||
# Pick any embedding model from https://openrouter.ai/models?modality=embedding
|
# Pick any model from https://openrouter.ai/models?modality=embedding
|
||||||
# and set the dimension to that model's output size — they must match.
|
# and set the dimension to that model's output size — they must match.
|
||||||
# OPENROUTER_EMBEDDING_MODEL=google/gemini-embedding-001
|
# OPENROUTER_EMBEDDING_MODEL=google/gemini-embedding-001
|
||||||
# OPENROUTER_EMBEDDING_DIMENSION=3072
|
# OPENROUTER_EMBEDDING_DIMENSION=3072
|
||||||
|
|
||||||
|
# --- Option B: Local OpenAI-compatible server (Ollama / llama.cpp / vLLM) -----
|
||||||
|
# Uncomment to use your own server instead of OpenRouter (no API key required).
|
||||||
|
# Defaults target Ollama with nomic-embed-text. For Turkish, bge-m3 (1024 dims)
|
||||||
|
# tends to work better — pull it with: `ollama pull bge-m3`
|
||||||
|
# EMBEDDING_PROVIDER=local
|
||||||
|
# LOCAL_EMBEDDING_BASE_URL=http://localhost:11434/v1
|
||||||
|
# LOCAL_EMBEDDING_MODEL=nomic-embed-text
|
||||||
|
# LOCAL_EMBEDDING_DIMENSION=768
|
||||||
|
# LOCAL_EMBEDDING_API_KEY= # most local servers ignore this
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# USAGE INSTRUCTIONS
|
# USAGE INSTRUCTIONS
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
+10
-8
@@ -265,17 +265,18 @@ from bedesten_mcp_module.models import (
|
|||||||
)
|
)
|
||||||
from bedesten_mcp_module.enums import BirimAdiEnum
|
from bedesten_mcp_module.enums import BirimAdiEnum
|
||||||
|
|
||||||
# Semantic Search Module Imports (conditional based on OPENROUTER_API_KEY)
|
# Semantic Search Module Imports (enabled if any embedding provider is configured)
|
||||||
from semantic_search.embedder import is_openrouter_available
|
from semantic_search.embedder import is_semantic_search_available, is_local_embedding_configured
|
||||||
SEMANTIC_SEARCH_AVAILABLE = is_openrouter_available()
|
SEMANTIC_SEARCH_AVAILABLE = is_semantic_search_available()
|
||||||
|
|
||||||
if SEMANTIC_SEARCH_AVAILABLE:
|
if SEMANTIC_SEARCH_AVAILABLE:
|
||||||
from semantic_search.embedder import OpenRouterEmbedder
|
from semantic_search.embedder import get_embedder
|
||||||
from semantic_search.vector_store import VectorStore
|
from semantic_search.vector_store import VectorStore
|
||||||
from semantic_search.processor import DocumentProcessor
|
from semantic_search.processor import DocumentProcessor
|
||||||
logger.info("Semantic search enabled (OPENROUTER_API_KEY found)")
|
provider = "local" if is_local_embedding_configured() else "openrouter"
|
||||||
|
logger.info(f"Semantic search enabled (provider={provider})")
|
||||||
else:
|
else:
|
||||||
logger.info("Semantic search disabled (OPENROUTER_API_KEY not set)")
|
logger.info("Semantic search disabled (no embedding provider configured)")
|
||||||
|
|
||||||
from danistay_mcp_module.client import DanistayApiClient
|
from danistay_mcp_module.client import DanistayApiClient
|
||||||
from emsal_mcp_module.client import EmsalApiClient
|
from emsal_mcp_module.client import EmsalApiClient
|
||||||
@@ -1279,8 +1280,9 @@ YANLIŞ KULLANIM:
|
|||||||
logger.info(f"Semantic search tool called with initial_keyword: {initial_keyword}, query: {query}")
|
logger.info(f"Semantic search tool called with initial_keyword: {initial_keyword}, query: {query}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Initialize components
|
# Initialize components (provider chosen via EMBEDDING_PROVIDER /
|
||||||
embedder = OpenRouterEmbedder()
|
# OPENROUTER_API_KEY env vars)
|
||||||
|
embedder = get_embedder()
|
||||||
vector_store = VectorStore(dimension=embedder.dimension)
|
vector_store = VectorStore(dimension=embedder.dimension)
|
||||||
processor = DocumentProcessor(chunk_size=1500, chunk_overlap=300)
|
processor = DocumentProcessor(chunk_size=1500, chunk_overlap=300)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,23 @@
|
|||||||
# semantic_search/__init__.py
|
# semantic_search/__init__.py
|
||||||
|
|
||||||
from .embedder import OpenRouterEmbedder, is_openrouter_available
|
from .embedder import (
|
||||||
|
OpenRouterEmbedder,
|
||||||
|
LocalEmbedder,
|
||||||
|
get_embedder,
|
||||||
|
is_openrouter_available,
|
||||||
|
is_local_embedding_configured,
|
||||||
|
is_semantic_search_available,
|
||||||
|
)
|
||||||
from .vector_store import VectorStore
|
from .vector_store import VectorStore
|
||||||
from .processor import DocumentProcessor
|
from .processor import DocumentProcessor
|
||||||
|
|
||||||
__all__ = ['OpenRouterEmbedder', 'is_openrouter_available', 'VectorStore', 'DocumentProcessor']
|
__all__ = [
|
||||||
|
'OpenRouterEmbedder',
|
||||||
|
'LocalEmbedder',
|
||||||
|
'get_embedder',
|
||||||
|
'is_openrouter_available',
|
||||||
|
'is_local_embedding_configured',
|
||||||
|
'is_semantic_search_available',
|
||||||
|
'VectorStore',
|
||||||
|
'DocumentProcessor',
|
||||||
|
]
|
||||||
|
|||||||
+182
-81
@@ -2,87 +2,70 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from typing import List, Optional
|
from typing import Dict, List, Optional
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# OpenRouter defaults (preserve backward compatibility)
|
||||||
DEFAULT_MODEL = "google/gemini-embedding-001"
|
DEFAULT_MODEL = "google/gemini-embedding-001"
|
||||||
DEFAULT_DIMENSION = 3072
|
DEFAULT_DIMENSION = 3072
|
||||||
|
|
||||||
|
# Local provider defaults — Ollama with nomic-embed-text out of the box.
|
||||||
|
# Override via LOCAL_EMBEDDING_BASE_URL / LOCAL_EMBEDDING_MODEL /
|
||||||
|
# LOCAL_EMBEDDING_DIMENSION when using a different server or model
|
||||||
|
# (e.g. llama.cpp's server, vLLM, LM Studio, or a different Ollama model
|
||||||
|
# such as bge-m3 — better for Turkish — at 1024 dimensions).
|
||||||
|
LOCAL_DEFAULT_BASE_URL = "http://localhost:11434/v1"
|
||||||
|
LOCAL_DEFAULT_MODEL = "nomic-embed-text"
|
||||||
|
LOCAL_DEFAULT_DIMENSION = 768
|
||||||
|
|
||||||
|
|
||||||
def is_openrouter_available() -> bool:
|
def is_openrouter_available() -> bool:
|
||||||
"""Check if OpenRouter API key is available."""
|
"""Check if OpenRouter API key is available."""
|
||||||
return bool(os.getenv("OPENROUTER_API_KEY"))
|
return bool(os.getenv("OPENROUTER_API_KEY"))
|
||||||
|
|
||||||
|
|
||||||
class OpenRouterEmbedder:
|
def is_local_embedding_configured() -> bool:
|
||||||
|
"""Check if the user opted into a local embedding endpoint."""
|
||||||
|
return os.getenv("EMBEDDING_PROVIDER", "").strip().lower() == "local"
|
||||||
|
|
||||||
|
|
||||||
|
def is_semantic_search_available() -> bool:
|
||||||
|
"""Returns True if any embedding provider is configured."""
|
||||||
|
return is_local_embedding_configured() or is_openrouter_available()
|
||||||
|
|
||||||
|
|
||||||
|
def _coerce_dimension(value, env_name: str, default: int) -> int:
|
||||||
|
"""Parse a dimension value (int or str) with clear error messages."""
|
||||||
|
if value is None:
|
||||||
|
return default
|
||||||
|
try:
|
||||||
|
parsed = int(value)
|
||||||
|
except (TypeError, ValueError) as e:
|
||||||
|
raise ValueError(
|
||||||
|
f"{env_name} must be an integer, got {value!r}"
|
||||||
|
) from e
|
||||||
|
if parsed <= 0:
|
||||||
|
raise ValueError(f"Embedding dimension must be positive, got {parsed}")
|
||||||
|
return parsed
|
||||||
|
|
||||||
|
|
||||||
|
class _BaseOpenAICompatibleEmbedder:
|
||||||
"""
|
"""
|
||||||
Embedder using OpenRouter's embedding API.
|
Shared encode/similarity logic for embedders backed by the OpenAI Python
|
||||||
|
SDK. Subclasses configure ``client``, ``model``, ``dimension``, and
|
||||||
The model and dimension are configurable so users can pick any OpenRouter
|
optionally ``_extra_headers`` (e.g. OpenRouter ranking headers).
|
||||||
embedding model (e.g. when one becomes paid or when a different model fits
|
|
||||||
the budget better). Configuration precedence: explicit constructor args >
|
|
||||||
environment variables > defaults.
|
|
||||||
|
|
||||||
Environment variables:
|
|
||||||
OPENROUTER_API_KEY (required): OpenRouter credential
|
|
||||||
OPENROUTER_EMBEDDING_MODEL (optional): override the embedding model id
|
|
||||||
OPENROUTER_EMBEDDING_DIMENSION (optional): override the vector size
|
|
||||||
|
|
||||||
Defaults preserve backward compatibility: ``google/gemini-embedding-001``
|
|
||||||
at 3072 dimensions.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, model: Optional[str] = None, dimension: Optional[int] = None):
|
# Subclasses may override; sent on every embeddings.create call when set.
|
||||||
"""
|
_extra_headers: Dict[str, str] = {}
|
||||||
Initialize OpenRouter Embedder.
|
|
||||||
|
|
||||||
Args:
|
# Set by subclasses
|
||||||
model: OpenRouter embedding model id. Falls back to
|
client = None
|
||||||
OPENROUTER_EMBEDDING_MODEL env var, then DEFAULT_MODEL.
|
model: str = ""
|
||||||
dimension: Output vector size. Falls back to
|
dimension: int = 0
|
||||||
OPENROUTER_EMBEDDING_DIMENSION env var, then DEFAULT_DIMENSION.
|
|
||||||
Must match the chosen model's actual output size — the vector
|
|
||||||
store and similarity math rely on it.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If OPENROUTER_API_KEY is not set or dimension is invalid
|
|
||||||
ImportError: If openai package is not installed
|
|
||||||
"""
|
|
||||||
api_key = os.getenv("OPENROUTER_API_KEY")
|
|
||||||
if not api_key:
|
|
||||||
raise ValueError("OPENROUTER_API_KEY environment variable is not set")
|
|
||||||
|
|
||||||
try:
|
|
||||||
from openai import OpenAI
|
|
||||||
except ImportError:
|
|
||||||
raise ImportError("openai package is required. Install with: pip install openai")
|
|
||||||
|
|
||||||
self.client = OpenAI(
|
|
||||||
base_url="https://openrouter.ai/api/v1",
|
|
||||||
api_key=api_key,
|
|
||||||
)
|
|
||||||
self.model = model or os.getenv("OPENROUTER_EMBEDDING_MODEL") or DEFAULT_MODEL
|
|
||||||
|
|
||||||
dim_value = dimension if dimension is not None else os.getenv("OPENROUTER_EMBEDDING_DIMENSION")
|
|
||||||
if dim_value is None:
|
|
||||||
self.dimension = DEFAULT_DIMENSION
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
self.dimension = int(dim_value)
|
|
||||||
except (TypeError, ValueError) as e:
|
|
||||||
raise ValueError(
|
|
||||||
f"OPENROUTER_EMBEDDING_DIMENSION must be an integer, got {dim_value!r}"
|
|
||||||
) from e
|
|
||||||
if self.dimension <= 0:
|
|
||||||
raise ValueError(f"Embedding dimension must be positive, got {self.dimension}")
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
f"OpenRouter Embedder initialized with model: {self.model} "
|
|
||||||
f"(dimension={self.dimension})"
|
|
||||||
)
|
|
||||||
|
|
||||||
def encode_query(self, query: str, task: str = "search result") -> np.ndarray:
|
def encode_query(self, query: str, task: str = "search result") -> np.ndarray:
|
||||||
"""
|
"""
|
||||||
@@ -95,7 +78,6 @@ class OpenRouterEmbedder:
|
|||||||
Returns:
|
Returns:
|
||||||
Numpy array of embeddings (``self.dimension`` elements).
|
Numpy array of embeddings (``self.dimension`` elements).
|
||||||
"""
|
"""
|
||||||
# Apply query prompt template
|
|
||||||
text = f"task: {task} | query: {query}"
|
text = f"task: {task} | query: {query}"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -103,10 +85,7 @@ class OpenRouterEmbedder:
|
|||||||
model=self.model,
|
model=self.model,
|
||||||
input=text,
|
input=text,
|
||||||
encoding_format="float",
|
encoding_format="float",
|
||||||
extra_headers={
|
extra_headers=self._extra_headers or None,
|
||||||
"HTTP-Referer": "https://yargimcp.com",
|
|
||||||
"X-Title": "Yargi MCP Server",
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
embedding = np.array(response.data[0].embedding, dtype=np.float32)
|
embedding = np.array(response.data[0].embedding, dtype=np.float32)
|
||||||
@@ -125,7 +104,7 @@ class OpenRouterEmbedder:
|
|||||||
|
|
||||||
def encode_documents(self, documents: List[str], titles: Optional[List[str]] = None) -> np.ndarray:
|
def encode_documents(self, documents: List[str], titles: Optional[List[str]] = None) -> np.ndarray:
|
||||||
"""
|
"""
|
||||||
Encode multiple documents with batch API call.
|
Encode multiple documents with a batch API call.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
documents: List of document texts
|
documents: List of document texts
|
||||||
@@ -137,28 +116,22 @@ class OpenRouterEmbedder:
|
|||||||
if not documents:
|
if not documents:
|
||||||
return np.array([])
|
return np.array([])
|
||||||
|
|
||||||
# Apply document prompt template
|
|
||||||
texts = []
|
texts = []
|
||||||
for i, doc in enumerate(documents):
|
for i, doc in enumerate(documents):
|
||||||
title = titles[i] if titles and i < len(titles) else "none"
|
title = titles[i] if titles and i < len(titles) else "none"
|
||||||
text = f"title: {title} | text: {doc}"
|
texts.append(f"title: {title} | text: {doc}")
|
||||||
texts.append(text)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = self.client.embeddings.create(
|
response = self.client.embeddings.create(
|
||||||
model=self.model,
|
model=self.model,
|
||||||
input=texts,
|
input=texts,
|
||||||
encoding_format="float",
|
encoding_format="float",
|
||||||
extra_headers={
|
extra_headers=self._extra_headers or None,
|
||||||
"HTTP-Referer": "https://yargimcp.com",
|
|
||||||
"X-Title": "Yargi MCP Server",
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Extract embeddings in order
|
|
||||||
embeddings = np.array(
|
embeddings = np.array(
|
||||||
[d.embedding for d in sorted(response.data, key=lambda x: x.index)],
|
[d.embedding for d in sorted(response.data, key=lambda x: x.index)],
|
||||||
dtype=np.float32
|
dtype=np.float32,
|
||||||
)
|
)
|
||||||
|
|
||||||
# L2 normalize each embedding for cosine similarity
|
# L2 normalize each embedding for cosine similarity
|
||||||
@@ -183,11 +156,139 @@ class OpenRouterEmbedder:
|
|||||||
Returns:
|
Returns:
|
||||||
Similarity scores (N,)
|
Similarity scores (N,)
|
||||||
"""
|
"""
|
||||||
# Ensure query is 2D for matrix multiplication
|
|
||||||
if len(query_embedding.shape) == 1:
|
if len(query_embedding.shape) == 1:
|
||||||
query_embedding = query_embedding.reshape(1, -1)
|
query_embedding = query_embedding.reshape(1, -1)
|
||||||
|
|
||||||
# Compute cosine similarity (embeddings are already normalized)
|
# Embeddings are already L2-normalized.
|
||||||
similarities = np.dot(document_embeddings, query_embedding.T).squeeze()
|
similarities = np.dot(document_embeddings, query_embedding.T).squeeze()
|
||||||
|
|
||||||
return similarities
|
return similarities
|
||||||
|
|
||||||
|
|
||||||
|
class OpenRouterEmbedder(_BaseOpenAICompatibleEmbedder):
|
||||||
|
"""
|
||||||
|
Embedder using OpenRouter's embedding API.
|
||||||
|
|
||||||
|
The model and dimension are configurable so users can pick any OpenRouter
|
||||||
|
embedding model (e.g. when one becomes paid). Configuration precedence:
|
||||||
|
explicit constructor args > environment variables > defaults.
|
||||||
|
|
||||||
|
Environment variables:
|
||||||
|
OPENROUTER_API_KEY (required): OpenRouter credential
|
||||||
|
OPENROUTER_EMBEDDING_MODEL (optional): override the embedding model id
|
||||||
|
OPENROUTER_EMBEDDING_DIMENSION (optional): override the vector size
|
||||||
|
|
||||||
|
Defaults preserve backward compatibility: ``google/gemini-embedding-001``
|
||||||
|
at 3072 dimensions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_extra_headers = {
|
||||||
|
"HTTP-Referer": "https://yargimcp.com",
|
||||||
|
"X-Title": "Yargi MCP Server",
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, model: Optional[str] = None, dimension: Optional[int] = None):
|
||||||
|
api_key = os.getenv("OPENROUTER_API_KEY")
|
||||||
|
if not api_key:
|
||||||
|
raise ValueError("OPENROUTER_API_KEY environment variable is not set")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from openai import OpenAI
|
||||||
|
except ImportError:
|
||||||
|
raise ImportError("openai package is required. Install with: pip install openai")
|
||||||
|
|
||||||
|
self.client = OpenAI(
|
||||||
|
base_url="https://openrouter.ai/api/v1",
|
||||||
|
api_key=api_key,
|
||||||
|
)
|
||||||
|
self.model = model or os.getenv("OPENROUTER_EMBEDDING_MODEL") or DEFAULT_MODEL
|
||||||
|
self.dimension = _coerce_dimension(
|
||||||
|
dimension if dimension is not None else os.getenv("OPENROUTER_EMBEDDING_DIMENSION"),
|
||||||
|
"OPENROUTER_EMBEDDING_DIMENSION",
|
||||||
|
DEFAULT_DIMENSION,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"OpenRouter Embedder initialized with model: {self.model} "
|
||||||
|
f"(dimension={self.dimension})"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class LocalEmbedder(_BaseOpenAICompatibleEmbedder):
|
||||||
|
"""
|
||||||
|
Embedder for a local OpenAI-compatible embedding server — Ollama,
|
||||||
|
llama.cpp, vLLM, LM Studio, etc. Zero new Python dependencies; just
|
||||||
|
point the existing OpenAI SDK at a local base URL.
|
||||||
|
|
||||||
|
Environment variables:
|
||||||
|
EMBEDDING_PROVIDER=local (selects this provider)
|
||||||
|
LOCAL_EMBEDDING_BASE_URL (default: http://localhost:11434/v1)
|
||||||
|
LOCAL_EMBEDDING_MODEL (default: nomic-embed-text)
|
||||||
|
LOCAL_EMBEDDING_DIMENSION (default: 768)
|
||||||
|
LOCAL_EMBEDDING_API_KEY (optional; ignored by most local servers)
|
||||||
|
|
||||||
|
Setup (Ollama):
|
||||||
|
$ ollama serve
|
||||||
|
$ ollama pull nomic-embed-text # or bge-m3 for better Turkish
|
||||||
|
|
||||||
|
The dimension MUST match the model's actual output size (e.g. 768 for
|
||||||
|
nomic-embed-text, 1024 for bge-m3, 1024 for mxbai-embed-large).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
base_url: Optional[str] = None,
|
||||||
|
model: Optional[str] = None,
|
||||||
|
dimension: Optional[int] = None,
|
||||||
|
api_key: Optional[str] = None,
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
from openai import OpenAI
|
||||||
|
except ImportError:
|
||||||
|
raise ImportError("openai package is required. Install with: pip install openai")
|
||||||
|
|
||||||
|
self.base_url = (
|
||||||
|
base_url
|
||||||
|
or os.getenv("LOCAL_EMBEDDING_BASE_URL")
|
||||||
|
or LOCAL_DEFAULT_BASE_URL
|
||||||
|
)
|
||||||
|
# Most local servers don't validate the key — use a placeholder so
|
||||||
|
# the OpenAI SDK doesn't error on the missing-key check.
|
||||||
|
effective_key = (
|
||||||
|
api_key
|
||||||
|
or os.getenv("LOCAL_EMBEDDING_API_KEY")
|
||||||
|
or "no-key-needed"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.client = OpenAI(base_url=self.base_url, api_key=effective_key)
|
||||||
|
self.model = model or os.getenv("LOCAL_EMBEDDING_MODEL") or LOCAL_DEFAULT_MODEL
|
||||||
|
self.dimension = _coerce_dimension(
|
||||||
|
dimension if dimension is not None else os.getenv("LOCAL_EMBEDDING_DIMENSION"),
|
||||||
|
"LOCAL_EMBEDDING_DIMENSION",
|
||||||
|
LOCAL_DEFAULT_DIMENSION,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Local Embedder initialized: model={self.model} "
|
||||||
|
f"base_url={self.base_url} dimension={self.dimension}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_embedder():
|
||||||
|
"""
|
||||||
|
Factory that picks the embedder based on EMBEDDING_PROVIDER.
|
||||||
|
|
||||||
|
- ``EMBEDDING_PROVIDER=local`` -> ``LocalEmbedder``
|
||||||
|
- otherwise -> ``OpenRouterEmbedder`` (requires OPENROUTER_API_KEY)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If no provider is configured (neither local nor OpenRouter).
|
||||||
|
"""
|
||||||
|
if is_local_embedding_configured():
|
||||||
|
return LocalEmbedder()
|
||||||
|
if is_openrouter_available():
|
||||||
|
return OpenRouterEmbedder()
|
||||||
|
raise ValueError(
|
||||||
|
"No embedding provider configured. Set OPENROUTER_API_KEY for hosted "
|
||||||
|
"embeddings, or EMBEDDING_PROVIDER=local (with LOCAL_EMBEDDING_* "
|
||||||
|
"env vars) for a local OpenAI-compatible server like Ollama."
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user