Files
Claude-Code-Workflow/ccw-litellm/src/ccw_litellm/clients/litellm_embedder.py
catlog22 bf66b095c7 feat: Add unified LiteLLM API management with dashboard UI and CLI integration
- Create ccw-litellm Python package with AbstractEmbedder and AbstractLLMClient interfaces
- Add BaseEmbedder abstraction and factory pattern to codex-lens for pluggable backends
- Implement API Settings dashboard page for provider credentials and custom endpoints
- Add REST API routes for CRUD operations on providers and endpoints
- Extend CLI with --model parameter for custom endpoint routing
- Integrate existing context-cache for @pattern file resolution
- Add provider model registry with predefined models per provider type
- Include i18n translations (en/zh) for all new UI elements

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-23 20:36:32 +08:00

171 lines
5.1 KiB
Python

"""LiteLLM embedder implementation for text embeddings."""
from __future__ import annotations
import logging
from typing import Any, Sequence
import litellm
import numpy as np
from numpy.typing import NDArray
from ..config import LiteLLMConfig, get_config
from ..interfaces.embedder import AbstractEmbedder
logger = logging.getLogger(__name__)
class LiteLLMEmbedder(AbstractEmbedder):
"""LiteLLM embedder implementation.
Supports multiple embedding providers (OpenAI, etc.) through LiteLLM's unified interface.
Example:
embedder = LiteLLMEmbedder(model="default")
vectors = embedder.embed(["Hello world", "Another text"])
print(vectors.shape) # (2, 1536)
"""
def __init__(
self,
model: str = "default",
config: LiteLLMConfig | None = None,
**litellm_kwargs: Any,
) -> None:
"""Initialize LiteLLM embedder.
Args:
model: Model name from configuration (default: "default")
config: Configuration instance (default: use global config)
**litellm_kwargs: Additional arguments to pass to litellm.embedding()
"""
self._config = config or get_config()
self._model_name = model
self._litellm_kwargs = litellm_kwargs
# Get embedding model configuration
try:
self._model_config = self._config.get_embedding_model(model)
except ValueError as e:
logger.error(f"Failed to get embedding model configuration: {e}")
raise
# Get provider configuration
try:
self._provider_config = self._config.get_provider(self._model_config.provider)
except ValueError as e:
logger.error(f"Failed to get provider configuration: {e}")
raise
# Set up LiteLLM environment
self._setup_litellm()
def _setup_litellm(self) -> None:
"""Configure LiteLLM with provider settings."""
provider = self._model_config.provider
# Set API key
if self._provider_config.api_key:
litellm.api_key = self._provider_config.api_key
# Also set environment-specific keys
if provider == "openai":
litellm.openai_key = self._provider_config.api_key
elif provider == "anthropic":
litellm.anthropic_key = self._provider_config.api_key
# Set API base
if self._provider_config.api_base:
litellm.api_base = self._provider_config.api_base
def _format_model_name(self) -> str:
"""Format model name for LiteLLM.
Returns:
Formatted model name (e.g., "text-embedding-3-small")
"""
provider = self._model_config.provider
model = self._model_config.model
# For some providers, LiteLLM expects explicit prefix
if provider in ["azure", "vertex_ai", "bedrock"]:
return f"{provider}/{model}"
return model
@property
def dimensions(self) -> int:
"""Embedding vector size."""
return self._model_config.dimensions
def embed(
self,
texts: str | Sequence[str],
*,
batch_size: int | None = None,
**kwargs: Any,
) -> NDArray[np.floating]:
"""Embed one or more texts.
Args:
texts: Single text or sequence of texts
batch_size: Batch size for processing (currently unused, LiteLLM handles batching)
**kwargs: Additional arguments for litellm.embedding()
Returns:
A numpy array of shape (n_texts, dimensions).
Raises:
Exception: If LiteLLM embedding fails
"""
# Normalize input to list
if isinstance(texts, str):
text_list = [texts]
single_input = True
else:
text_list = list(texts)
single_input = False
if not text_list:
# Return empty array with correct shape
return np.empty((0, self.dimensions), dtype=np.float32)
# Merge kwargs
embedding_kwargs = {**self._litellm_kwargs, **kwargs}
try:
# Call LiteLLM embedding
response = litellm.embedding(
model=self._format_model_name(),
input=text_list,
**embedding_kwargs,
)
# Extract embeddings
embeddings = [item["embedding"] for item in response.data]
# Convert to numpy array
result = np.array(embeddings, dtype=np.float32)
# Validate dimensions
if result.shape[1] != self.dimensions:
logger.warning(
f"Expected {self.dimensions} dimensions, got {result.shape[1]}. "
f"Configuration may be incorrect."
)
return result
except Exception as e:
logger.error(f"LiteLLM embedding failed: {e}")
raise
@property
def model_name(self) -> str:
"""Get configured model name."""
return self._model_name
@property
def provider(self) -> str:
"""Get configured provider name."""
return self._model_config.provider