feat: Add OpenRouter as LLM/embedding provider (#162)
* feat: Add OpenRouter as LLM/embedding provider Add OpenRouter provider support for Graphiti memory integration, enabling access to multiple LLM providers through a single API. Changes: Backend: - Created openrouter_llm.py: OpenRouter LLM provider using OpenAI-compatible API - Created openrouter_embedder.py: OpenRouter embedder provider - Updated config.py: Added OpenRouter to provider enums and configuration - New fields: openrouter_api_key, openrouter_base_url, openrouter_llm_model, openrouter_embedding_model - Validation methods updated for OpenRouter - Updated factory.py: Added OpenRouter to LLM and embedder factories - Updated provider __init__.py files: Exported new OpenRouter functions Frontend: - Updated project.ts types: Added 'openrouter' to provider type unions - GraphitiProviderConfig extended with OpenRouter fields - Updated GraphitiStep.tsx: Added OpenRouter to provider arrays - LLM_PROVIDERS: 'Multi-provider aggregator' - EMBEDDING_PROVIDERS: 'OpenAI-compatible embeddings' - Added OpenRouter API key input field with show/hide toggle - Link to https://openrouter.ai/keys - Updated env-handlers.ts: OpenRouter .env generation and parsing - Template generation for OPENROUTER_* variables - Parsing from .env files with proper type casting Documentation: - Updated .env.example with OpenRouter section - Configuration examples - Popular model recommendations - Example configuration (#6) Fixes #92 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * refactor: address CodeRabbit review comments for OpenRouter - Add globalOpenRouterApiKey to settings types and store updates - Initialize openrouterApiKey from global settings - Update documentation to include OpenRouter in provider lists - Add OpenRouter handling to get_embedding_dimension() method - Add openrouter to provider cleanup list - Add OpenRouter to get_available_providers() function - Clarify Legacy comment for openrouterLlmModel These changes complete the OpenRouter integration by ensuring proper settings persistence and provider detection across the application. * fix: apply ruff formatting to OpenRouter code - Break long error message across multiple lines - Format provider list with one item per line - Fixes lint CI failure 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
f168bdc3ac
commit
02bef954f3
@@ -153,10 +153,10 @@ GRAPHITI_ENABLED=true
|
|||||||
# Choose which providers to use for LLM and embeddings.
|
# Choose which providers to use for LLM and embeddings.
|
||||||
# Default is "openai" for both.
|
# Default is "openai" for both.
|
||||||
|
|
||||||
# LLM provider: openai | anthropic | azure_openai | ollama | google
|
# LLM provider: openai | anthropic | azure_openai | ollama | google | openrouter
|
||||||
# GRAPHITI_LLM_PROVIDER=openai
|
# GRAPHITI_LLM_PROVIDER=openai
|
||||||
|
|
||||||
# Embedder provider: openai | voyage | azure_openai | ollama | google
|
# Embedder provider: openai | voyage | azure_openai | ollama | google | openrouter
|
||||||
# GRAPHITI_EMBEDDER_PROVIDER=openai
|
# GRAPHITI_EMBEDDER_PROVIDER=openai
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@@ -221,6 +221,28 @@ GRAPHITI_ENABLED=true
|
|||||||
# Google Embedding Model (default: text-embedding-004)
|
# Google Embedding Model (default: text-embedding-004)
|
||||||
# GOOGLE_EMBEDDING_MODEL=text-embedding-004
|
# GOOGLE_EMBEDDING_MODEL=text-embedding-004
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# GRAPHITI: OpenRouter Provider (Multi-provider aggregator)
|
||||||
|
# =============================================================================
|
||||||
|
# Use OpenRouter to access multiple LLM providers through a single API.
|
||||||
|
# OpenRouter provides access to Anthropic, OpenAI, Google, and many other models.
|
||||||
|
# Get API key from: https://openrouter.ai/keys
|
||||||
|
#
|
||||||
|
# Required: OPENROUTER_API_KEY
|
||||||
|
|
||||||
|
# OpenRouter API Key
|
||||||
|
# OPENROUTER_API_KEY=sk-or-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||||
|
|
||||||
|
# OpenRouter Base URL (default: https://openrouter.ai/api/v1)
|
||||||
|
# OPENROUTER_BASE_URL=https://openrouter.ai/api/v1
|
||||||
|
|
||||||
|
# OpenRouter LLM Model (default: anthropic/claude-3.5-sonnet)
|
||||||
|
# Popular choices: anthropic/claude-3.5-sonnet, openai/gpt-4o, google/gemini-2.0-flash
|
||||||
|
# OPENROUTER_LLM_MODEL=anthropic/claude-3.5-sonnet
|
||||||
|
|
||||||
|
# OpenRouter Embedding Model (default: openai/text-embedding-3-small)
|
||||||
|
# OPENROUTER_EMBEDDING_MODEL=openai/text-embedding-3-small
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# GRAPHITI: Azure OpenAI Provider
|
# GRAPHITI: Azure OpenAI Provider
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@@ -307,3 +329,11 @@ GRAPHITI_ENABLED=true
|
|||||||
# GRAPHITI_LLM_PROVIDER=google
|
# GRAPHITI_LLM_PROVIDER=google
|
||||||
# GRAPHITI_EMBEDDER_PROVIDER=google
|
# GRAPHITI_EMBEDDER_PROVIDER=google
|
||||||
# GOOGLE_API_KEY=AIzaSyxxxxxxxx
|
# GOOGLE_API_KEY=AIzaSyxxxxxxxx
|
||||||
|
#
|
||||||
|
# --- Example 6: OpenRouter (multi-provider aggregator) ---
|
||||||
|
# GRAPHITI_ENABLED=true
|
||||||
|
# GRAPHITI_LLM_PROVIDER=openrouter
|
||||||
|
# GRAPHITI_EMBEDDER_PROVIDER=openrouter
|
||||||
|
# OPENROUTER_API_KEY=sk-or-xxxxxxxx
|
||||||
|
# OPENROUTER_LLM_MODEL=anthropic/claude-3.5-sonnet
|
||||||
|
# OPENROUTER_EMBEDDING_MODEL=openai/text-embedding-3-small
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ Follows the same patterns as linear_config.py for consistency.
|
|||||||
Uses LadybugDB as the embedded graph database (no Docker required, requires Python 3.12+).
|
Uses LadybugDB as the embedded graph database (no Docker required, requires Python 3.12+).
|
||||||
|
|
||||||
Multi-Provider Support (V2):
|
Multi-Provider Support (V2):
|
||||||
- LLM Providers: OpenAI, Anthropic, Azure OpenAI, Ollama, Google AI
|
- LLM Providers: OpenAI, Anthropic, Azure OpenAI, Ollama, Google AI, OpenRouter
|
||||||
- Embedder Providers: OpenAI, Voyage AI, Azure OpenAI, Ollama, Google AI
|
- Embedder Providers: OpenAI, Voyage AI, Azure OpenAI, Ollama, Google AI, OpenRouter
|
||||||
|
|
||||||
Environment Variables:
|
Environment Variables:
|
||||||
# Core
|
# Core
|
||||||
@@ -89,6 +89,7 @@ class LLMProvider(str, Enum):
|
|||||||
AZURE_OPENAI = "azure_openai"
|
AZURE_OPENAI = "azure_openai"
|
||||||
OLLAMA = "ollama"
|
OLLAMA = "ollama"
|
||||||
GOOGLE = "google"
|
GOOGLE = "google"
|
||||||
|
OPENROUTER = "openrouter"
|
||||||
|
|
||||||
|
|
||||||
class EmbedderProvider(str, Enum):
|
class EmbedderProvider(str, Enum):
|
||||||
@@ -99,6 +100,7 @@ class EmbedderProvider(str, Enum):
|
|||||||
AZURE_OPENAI = "azure_openai"
|
AZURE_OPENAI = "azure_openai"
|
||||||
OLLAMA = "ollama"
|
OLLAMA = "ollama"
|
||||||
GOOGLE = "google"
|
GOOGLE = "google"
|
||||||
|
OPENROUTER = "openrouter"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -141,6 +143,12 @@ class GraphitiConfig:
|
|||||||
google_llm_model: str = "gemini-2.0-flash"
|
google_llm_model: str = "gemini-2.0-flash"
|
||||||
google_embedding_model: str = "text-embedding-004"
|
google_embedding_model: str = "text-embedding-004"
|
||||||
|
|
||||||
|
# OpenRouter settings (multi-provider aggregator)
|
||||||
|
openrouter_api_key: str = ""
|
||||||
|
openrouter_base_url: str = "https://openrouter.ai/api/v1"
|
||||||
|
openrouter_llm_model: str = "anthropic/claude-3.5-sonnet"
|
||||||
|
openrouter_embedding_model: str = "openai/text-embedding-3-small"
|
||||||
|
|
||||||
# Ollama settings (local)
|
# Ollama settings (local)
|
||||||
ollama_base_url: str = DEFAULT_OLLAMA_BASE_URL
|
ollama_base_url: str = DEFAULT_OLLAMA_BASE_URL
|
||||||
ollama_llm_model: str = ""
|
ollama_llm_model: str = ""
|
||||||
@@ -196,6 +204,18 @@ class GraphitiConfig:
|
|||||||
"GOOGLE_EMBEDDING_MODEL", "text-embedding-004"
|
"GOOGLE_EMBEDDING_MODEL", "text-embedding-004"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# OpenRouter settings
|
||||||
|
openrouter_api_key = os.environ.get("OPENROUTER_API_KEY", "")
|
||||||
|
openrouter_base_url = os.environ.get(
|
||||||
|
"OPENROUTER_BASE_URL", "https://openrouter.ai/api/v1"
|
||||||
|
)
|
||||||
|
openrouter_llm_model = os.environ.get(
|
||||||
|
"OPENROUTER_LLM_MODEL", "anthropic/claude-3.5-sonnet"
|
||||||
|
)
|
||||||
|
openrouter_embedding_model = os.environ.get(
|
||||||
|
"OPENROUTER_EMBEDDING_MODEL", "openai/text-embedding-3-small"
|
||||||
|
)
|
||||||
|
|
||||||
# Ollama settings
|
# Ollama settings
|
||||||
ollama_base_url = os.environ.get("OLLAMA_BASE_URL", DEFAULT_OLLAMA_BASE_URL)
|
ollama_base_url = os.environ.get("OLLAMA_BASE_URL", DEFAULT_OLLAMA_BASE_URL)
|
||||||
ollama_llm_model = os.environ.get("OLLAMA_LLM_MODEL", "")
|
ollama_llm_model = os.environ.get("OLLAMA_LLM_MODEL", "")
|
||||||
@@ -227,6 +247,10 @@ class GraphitiConfig:
|
|||||||
google_api_key=google_api_key,
|
google_api_key=google_api_key,
|
||||||
google_llm_model=google_llm_model,
|
google_llm_model=google_llm_model,
|
||||||
google_embedding_model=google_embedding_model,
|
google_embedding_model=google_embedding_model,
|
||||||
|
openrouter_api_key=openrouter_api_key,
|
||||||
|
openrouter_base_url=openrouter_base_url,
|
||||||
|
openrouter_llm_model=openrouter_llm_model,
|
||||||
|
openrouter_embedding_model=openrouter_embedding_model,
|
||||||
ollama_base_url=ollama_base_url,
|
ollama_base_url=ollama_base_url,
|
||||||
ollama_llm_model=ollama_llm_model,
|
ollama_llm_model=ollama_llm_model,
|
||||||
ollama_embedding_model=ollama_embedding_model,
|
ollama_embedding_model=ollama_embedding_model,
|
||||||
@@ -267,6 +291,8 @@ class GraphitiConfig:
|
|||||||
return bool(self.ollama_embedding_model)
|
return bool(self.ollama_embedding_model)
|
||||||
elif self.embedder_provider == "google":
|
elif self.embedder_provider == "google":
|
||||||
return bool(self.google_api_key)
|
return bool(self.google_api_key)
|
||||||
|
elif self.embedder_provider == "openrouter":
|
||||||
|
return bool(self.openrouter_api_key)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_validation_errors(self) -> list[str]:
|
def get_validation_errors(self) -> list[str]:
|
||||||
@@ -309,6 +335,11 @@ class GraphitiConfig:
|
|||||||
elif self.embedder_provider == "google":
|
elif self.embedder_provider == "google":
|
||||||
if not self.google_api_key:
|
if not self.google_api_key:
|
||||||
errors.append("Google embedder provider requires GOOGLE_API_KEY")
|
errors.append("Google embedder provider requires GOOGLE_API_KEY")
|
||||||
|
elif self.embedder_provider == "openrouter":
|
||||||
|
if not self.openrouter_api_key:
|
||||||
|
errors.append(
|
||||||
|
"OpenRouter embedder provider requires OPENROUTER_API_KEY"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
errors.append(f"Unknown embedder provider: {self.embedder_provider}")
|
errors.append(f"Unknown embedder provider: {self.embedder_provider}")
|
||||||
|
|
||||||
@@ -367,6 +398,18 @@ class GraphitiConfig:
|
|||||||
elif self.embedder_provider == "azure_openai":
|
elif self.embedder_provider == "azure_openai":
|
||||||
# Depends on the deployment, default to 1536
|
# Depends on the deployment, default to 1536
|
||||||
return 1536
|
return 1536
|
||||||
|
elif self.embedder_provider == "openrouter":
|
||||||
|
# OpenRouter uses provider/model format
|
||||||
|
# Extract underlying provider to determine dimension
|
||||||
|
model = self.openrouter_embedding_model.lower()
|
||||||
|
if model.startswith("openai/"):
|
||||||
|
return 1536 # OpenAI text-embedding-3-small
|
||||||
|
elif model.startswith("voyage/"):
|
||||||
|
return 1024 # Voyage-3
|
||||||
|
elif model.startswith("google/"):
|
||||||
|
return 768 # Google text-embedding-004
|
||||||
|
# Add more providers as needed
|
||||||
|
return 1536 # Default for unknown OpenRouter models
|
||||||
return 768 # Safe default
|
return 768 # Safe default
|
||||||
|
|
||||||
def get_provider_signature(self) -> str:
|
def get_provider_signature(self) -> str:
|
||||||
@@ -403,7 +446,14 @@ class GraphitiConfig:
|
|||||||
base_name = self.database
|
base_name = self.database
|
||||||
|
|
||||||
# Remove existing provider suffix if present
|
# Remove existing provider suffix if present
|
||||||
for provider in ["openai", "ollama", "voyage", "google", "azure_openai"]:
|
for provider in [
|
||||||
|
"openai",
|
||||||
|
"ollama",
|
||||||
|
"voyage",
|
||||||
|
"google",
|
||||||
|
"azure_openai",
|
||||||
|
"openrouter",
|
||||||
|
]:
|
||||||
if f"_{provider}_" in base_name:
|
if f"_{provider}_" in base_name:
|
||||||
base_name = base_name.split(f"_{provider}_")[0]
|
base_name = base_name.split(f"_{provider}_")[0]
|
||||||
break
|
break
|
||||||
@@ -617,6 +667,11 @@ def get_available_providers() -> dict:
|
|||||||
available_llm.append("google")
|
available_llm.append("google")
|
||||||
available_embedder.append("google")
|
available_embedder.append("google")
|
||||||
|
|
||||||
|
# Check OpenRouter
|
||||||
|
if config.openrouter_api_key:
|
||||||
|
available_llm.append("openrouter")
|
||||||
|
available_embedder.append("openrouter")
|
||||||
|
|
||||||
# Check Ollama
|
# Check Ollama
|
||||||
if config.ollama_llm_model:
|
if config.ollama_llm_model:
|
||||||
available_llm.append("ollama")
|
available_llm.append("ollama")
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ from .ollama_embedder import (
|
|||||||
get_embedding_dim_for_model,
|
get_embedding_dim_for_model,
|
||||||
)
|
)
|
||||||
from .openai_embedder import create_openai_embedder
|
from .openai_embedder import create_openai_embedder
|
||||||
|
from .openrouter_embedder import create_openrouter_embedder
|
||||||
from .voyage_embedder import create_voyage_embedder
|
from .voyage_embedder import create_voyage_embedder
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@@ -26,6 +27,7 @@ __all__ = [
|
|||||||
"create_azure_openai_embedder",
|
"create_azure_openai_embedder",
|
||||||
"create_ollama_embedder",
|
"create_ollama_embedder",
|
||||||
"create_google_embedder",
|
"create_google_embedder",
|
||||||
|
"create_openrouter_embedder",
|
||||||
"KNOWN_OLLAMA_EMBEDDING_MODELS",
|
"KNOWN_OLLAMA_EMBEDDING_MODELS",
|
||||||
"get_embedding_dim_for_model",
|
"get_embedding_dim_for_model",
|
||||||
]
|
]
|
||||||
|
|||||||
+60
@@ -0,0 +1,60 @@
|
|||||||
|
"""
|
||||||
|
OpenRouter Embedder Provider
|
||||||
|
=============================
|
||||||
|
|
||||||
|
OpenRouter embedder implementation for Graphiti.
|
||||||
|
Uses OpenAI-compatible embedding API.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ...config import GraphitiConfig
|
||||||
|
|
||||||
|
from ..exceptions import ProviderError, ProviderNotInstalled
|
||||||
|
|
||||||
|
|
||||||
|
def create_openrouter_embedder(config: "GraphitiConfig") -> Any:
|
||||||
|
"""
|
||||||
|
Create OpenRouter embedder client.
|
||||||
|
|
||||||
|
OpenRouter uses OpenAI-compatible API, so we use the OpenAI embedder
|
||||||
|
with custom base URL.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: GraphitiConfig with OpenRouter settings
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
OpenAI-compatible embedder instance
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ProviderNotInstalled: If graphiti-core is not installed
|
||||||
|
ProviderError: If API key is missing
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> from auto_claude.integrations.graphiti.config import GraphitiConfig
|
||||||
|
>>> config = GraphitiConfig(
|
||||||
|
... openrouter_api_key="sk-or-...",
|
||||||
|
... openrouter_embedding_model="openai/text-embedding-3-small"
|
||||||
|
... )
|
||||||
|
>>> embedder = create_openrouter_embedder(config)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from graphiti_core.embedder import EmbedderConfig, OpenAIEmbedder
|
||||||
|
except ImportError as e:
|
||||||
|
raise ProviderNotInstalled(
|
||||||
|
f"OpenRouter provider requires graphiti-core. "
|
||||||
|
f"Install with: pip install graphiti-core\n"
|
||||||
|
f"Error: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not config.openrouter_api_key:
|
||||||
|
raise ProviderError("OpenRouter provider requires OPENROUTER_API_KEY")
|
||||||
|
|
||||||
|
embedder_config = EmbedderConfig(
|
||||||
|
api_key=config.openrouter_api_key,
|
||||||
|
model=config.openrouter_embedding_model,
|
||||||
|
base_url=config.openrouter_base_url,
|
||||||
|
)
|
||||||
|
|
||||||
|
return OpenAIEmbedder(config=embedder_config)
|
||||||
@@ -16,6 +16,7 @@ from .embedder_providers import (
|
|||||||
create_google_embedder,
|
create_google_embedder,
|
||||||
create_ollama_embedder,
|
create_ollama_embedder,
|
||||||
create_openai_embedder,
|
create_openai_embedder,
|
||||||
|
create_openrouter_embedder,
|
||||||
create_voyage_embedder,
|
create_voyage_embedder,
|
||||||
)
|
)
|
||||||
from .exceptions import ProviderError
|
from .exceptions import ProviderError
|
||||||
@@ -25,6 +26,7 @@ from .llm_providers import (
|
|||||||
create_google_llm_client,
|
create_google_llm_client,
|
||||||
create_ollama_llm_client,
|
create_ollama_llm_client,
|
||||||
create_openai_llm_client,
|
create_openai_llm_client,
|
||||||
|
create_openrouter_llm_client,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -58,6 +60,8 @@ def create_llm_client(config: "GraphitiConfig") -> Any:
|
|||||||
return create_ollama_llm_client(config)
|
return create_ollama_llm_client(config)
|
||||||
elif provider == "google":
|
elif provider == "google":
|
||||||
return create_google_llm_client(config)
|
return create_google_llm_client(config)
|
||||||
|
elif provider == "openrouter":
|
||||||
|
return create_openrouter_llm_client(config)
|
||||||
else:
|
else:
|
||||||
raise ProviderError(f"Unknown LLM provider: {provider}")
|
raise ProviderError(f"Unknown LLM provider: {provider}")
|
||||||
|
|
||||||
@@ -90,5 +94,7 @@ def create_embedder(config: "GraphitiConfig") -> Any:
|
|||||||
return create_ollama_embedder(config)
|
return create_ollama_embedder(config)
|
||||||
elif provider == "google":
|
elif provider == "google":
|
||||||
return create_google_embedder(config)
|
return create_google_embedder(config)
|
||||||
|
elif provider == "openrouter":
|
||||||
|
return create_openrouter_embedder(config)
|
||||||
else:
|
else:
|
||||||
raise ProviderError(f"Unknown embedder provider: {provider}")
|
raise ProviderError(f"Unknown embedder provider: {provider}")
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from .azure_openai_llm import create_azure_openai_llm_client
|
|||||||
from .google_llm import create_google_llm_client
|
from .google_llm import create_google_llm_client
|
||||||
from .ollama_llm import create_ollama_llm_client
|
from .ollama_llm import create_ollama_llm_client
|
||||||
from .openai_llm import create_openai_llm_client
|
from .openai_llm import create_openai_llm_client
|
||||||
|
from .openrouter_llm import create_openrouter_llm_client
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"create_openai_llm_client",
|
"create_openai_llm_client",
|
||||||
@@ -22,4 +23,5 @@ __all__ = [
|
|||||||
"create_azure_openai_llm_client",
|
"create_azure_openai_llm_client",
|
||||||
"create_ollama_llm_client",
|
"create_ollama_llm_client",
|
||||||
"create_google_llm_client",
|
"create_google_llm_client",
|
||||||
|
"create_openrouter_llm_client",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
"""
|
||||||
|
OpenRouter LLM Provider
|
||||||
|
=======================
|
||||||
|
|
||||||
|
OpenRouter LLM client implementation for Graphiti.
|
||||||
|
Uses OpenAI-compatible API.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ...config import GraphitiConfig
|
||||||
|
|
||||||
|
from ..exceptions import ProviderError, ProviderNotInstalled
|
||||||
|
|
||||||
|
|
||||||
|
def create_openrouter_llm_client(config: "GraphitiConfig") -> Any:
|
||||||
|
"""
|
||||||
|
Create OpenRouter LLM client.
|
||||||
|
|
||||||
|
OpenRouter uses OpenAI-compatible API, so we use the OpenAI client
|
||||||
|
with custom base URL.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: GraphitiConfig with OpenRouter settings
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
OpenAI-compatible LLM client instance
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ProviderNotInstalled: If graphiti-core is not installed
|
||||||
|
ProviderError: If API key is missing
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> from auto_claude.integrations.graphiti.config import GraphitiConfig
|
||||||
|
>>> config = GraphitiConfig(
|
||||||
|
... openrouter_api_key="sk-or-...",
|
||||||
|
... openrouter_llm_model="anthropic/claude-3.5-sonnet"
|
||||||
|
... )
|
||||||
|
>>> client = create_openrouter_llm_client(config)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from graphiti_core.llm_client.config import LLMConfig
|
||||||
|
from graphiti_core.llm_client.openai_client import OpenAIClient
|
||||||
|
except ImportError as e:
|
||||||
|
raise ProviderNotInstalled(
|
||||||
|
f"OpenRouter provider requires graphiti-core. "
|
||||||
|
f"Install with: pip install graphiti-core\n"
|
||||||
|
f"Error: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not config.openrouter_api_key:
|
||||||
|
raise ProviderError("OpenRouter provider requires OPENROUTER_API_KEY")
|
||||||
|
|
||||||
|
llm_config = LLMConfig(
|
||||||
|
api_key=config.openrouter_api_key,
|
||||||
|
model=config.openrouter_llm_model,
|
||||||
|
base_url=config.openrouter_base_url,
|
||||||
|
)
|
||||||
|
|
||||||
|
# OpenRouter uses OpenAI-compatible API
|
||||||
|
# Disable reasoning/verbosity for compatibility
|
||||||
|
return OpenAIClient(config=llm_config, reasoning=None, verbosity=None)
|
||||||
@@ -44,6 +44,7 @@ const LLM_PROVIDERS: Array<{
|
|||||||
{ id: 'anthropic', name: 'Anthropic', description: 'Claude models', requiresApiKey: true },
|
{ id: 'anthropic', name: 'Anthropic', description: 'Claude models', requiresApiKey: true },
|
||||||
{ id: 'google', name: 'Google AI', description: 'Gemini models', requiresApiKey: true },
|
{ id: 'google', name: 'Google AI', description: 'Gemini models', requiresApiKey: true },
|
||||||
{ id: 'groq', name: 'Groq', description: 'Llama models (fast inference)', requiresApiKey: true },
|
{ id: 'groq', name: 'Groq', description: 'Llama models (fast inference)', requiresApiKey: true },
|
||||||
|
{ id: 'openrouter', name: 'OpenRouter', description: 'Multi-provider aggregator', requiresApiKey: true },
|
||||||
{ id: 'azure_openai', name: 'Azure OpenAI', description: 'Enterprise Azure deployment', requiresApiKey: true },
|
{ id: 'azure_openai', name: 'Azure OpenAI', description: 'Enterprise Azure deployment', requiresApiKey: true },
|
||||||
{ id: 'ollama', name: 'Ollama', description: 'Local models (free)', requiresApiKey: false }
|
{ id: 'ollama', name: 'Ollama', description: 'Local models (free)', requiresApiKey: false }
|
||||||
];
|
];
|
||||||
@@ -58,6 +59,7 @@ const EMBEDDING_PROVIDERS: Array<{
|
|||||||
{ id: 'openai', name: 'OpenAI', description: 'text-embedding-3-small (recommended)', requiresApiKey: true },
|
{ id: 'openai', name: 'OpenAI', description: 'text-embedding-3-small (recommended)', requiresApiKey: true },
|
||||||
{ id: 'voyage', name: 'Voyage AI', description: 'voyage-3 (great with Anthropic)', requiresApiKey: true },
|
{ id: 'voyage', name: 'Voyage AI', description: 'voyage-3 (great with Anthropic)', requiresApiKey: true },
|
||||||
{ id: 'google', name: 'Google AI', description: 'Gemini text-embedding-004', requiresApiKey: true },
|
{ id: 'google', name: 'Google AI', description: 'Gemini text-embedding-004', requiresApiKey: true },
|
||||||
|
{ id: 'openrouter', name: 'OpenRouter', description: 'OpenAI-compatible embeddings', requiresApiKey: true },
|
||||||
{ id: 'azure_openai', name: 'Azure OpenAI', description: 'Enterprise Azure embeddings', requiresApiKey: true }
|
{ id: 'azure_openai', name: 'Azure OpenAI', description: 'Enterprise Azure embeddings', requiresApiKey: true }
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -82,6 +84,11 @@ interface GraphitiConfig {
|
|||||||
googleApiKey: string;
|
googleApiKey: string;
|
||||||
// Groq
|
// Groq
|
||||||
groqApiKey: string;
|
groqApiKey: string;
|
||||||
|
// OpenRouter
|
||||||
|
openrouterApiKey: string;
|
||||||
|
openrouterBaseUrl: string;
|
||||||
|
openrouterLlmModel: string;
|
||||||
|
openrouterEmbeddingModel: string;
|
||||||
// HuggingFace
|
// HuggingFace
|
||||||
huggingfaceApiKey: string;
|
huggingfaceApiKey: string;
|
||||||
// Ollama
|
// Ollama
|
||||||
@@ -118,6 +125,10 @@ export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) {
|
|||||||
voyageApiKey: '',
|
voyageApiKey: '',
|
||||||
googleApiKey: settings.globalGoogleApiKey || '',
|
googleApiKey: settings.globalGoogleApiKey || '',
|
||||||
groqApiKey: settings.globalGroqApiKey || '',
|
groqApiKey: settings.globalGroqApiKey || '',
|
||||||
|
openrouterApiKey: settings.globalOpenRouterApiKey || '',
|
||||||
|
openrouterBaseUrl: 'https://openrouter.ai/api/v1',
|
||||||
|
openrouterLlmModel: 'anthropic/claude-3.5-sonnet',
|
||||||
|
openrouterEmbeddingModel: 'openai/text-embedding-3-small',
|
||||||
huggingfaceApiKey: '',
|
huggingfaceApiKey: '',
|
||||||
ollamaBaseUrl: settings.ollamaBaseUrl || 'http://localhost:11434',
|
ollamaBaseUrl: settings.ollamaBaseUrl || 'http://localhost:11434',
|
||||||
ollamaLlmModel: '',
|
ollamaLlmModel: '',
|
||||||
@@ -194,6 +205,9 @@ export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) {
|
|||||||
if (llmProvider === 'groq') {
|
if (llmProvider === 'groq') {
|
||||||
if (!config.groqApiKey.trim()) return 'Groq API key';
|
if (!config.groqApiKey.trim()) return 'Groq API key';
|
||||||
}
|
}
|
||||||
|
if (llmProvider === 'openrouter' || embeddingProvider === 'openrouter') {
|
||||||
|
if (!config.openrouterApiKey.trim()) return 'OpenRouter API key';
|
||||||
|
}
|
||||||
if (llmProvider === 'ollama') {
|
if (llmProvider === 'ollama') {
|
||||||
if (!config.ollamaLlmModel.trim()) return 'Ollama LLM model name';
|
if (!config.ollamaLlmModel.trim()) return 'Ollama LLM model name';
|
||||||
}
|
}
|
||||||
@@ -221,9 +235,11 @@ export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) {
|
|||||||
config.llmProvider === 'anthropic' ? config.anthropicApiKey :
|
config.llmProvider === 'anthropic' ? config.anthropicApiKey :
|
||||||
config.llmProvider === 'google' ? config.googleApiKey :
|
config.llmProvider === 'google' ? config.googleApiKey :
|
||||||
config.llmProvider === 'groq' ? config.groqApiKey :
|
config.llmProvider === 'groq' ? config.groqApiKey :
|
||||||
|
config.llmProvider === 'openrouter' ? config.openrouterApiKey :
|
||||||
config.llmProvider === 'azure_openai' ? config.azureOpenaiApiKey :
|
config.llmProvider === 'azure_openai' ? config.azureOpenaiApiKey :
|
||||||
config.llmProvider === 'ollama' ? '' : // Ollama doesn't need API key
|
config.llmProvider === 'ollama' ? '' : // Ollama doesn't need API key
|
||||||
config.embeddingProvider === 'openai' ? config.openaiApiKey : '';
|
config.embeddingProvider === 'openai' ? config.openaiApiKey :
|
||||||
|
config.embeddingProvider === 'openrouter' ? config.openrouterApiKey : '';
|
||||||
|
|
||||||
const result = await window.electronAPI.testGraphitiConnection({
|
const result = await window.electronAPI.testGraphitiConnection({
|
||||||
dbPath: config.dbPath || undefined,
|
dbPath: config.dbPath || undefined,
|
||||||
@@ -303,6 +319,9 @@ export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) {
|
|||||||
if (config.groqApiKey.trim()) {
|
if (config.groqApiKey.trim()) {
|
||||||
settingsToSave.globalGroqApiKey = config.groqApiKey.trim();
|
settingsToSave.globalGroqApiKey = config.groqApiKey.trim();
|
||||||
}
|
}
|
||||||
|
if (config.openrouterApiKey.trim()) {
|
||||||
|
settingsToSave.globalOpenRouterApiKey = config.openrouterApiKey.trim();
|
||||||
|
}
|
||||||
if (config.ollamaBaseUrl.trim()) {
|
if (config.ollamaBaseUrl.trim()) {
|
||||||
settingsToSave.ollamaBaseUrl = config.ollamaBaseUrl.trim();
|
settingsToSave.ollamaBaseUrl = config.ollamaBaseUrl.trim();
|
||||||
}
|
}
|
||||||
@@ -311,11 +330,12 @@ export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) {
|
|||||||
|
|
||||||
if (result?.success) {
|
if (result?.success) {
|
||||||
// Update local settings store with API key settings
|
// Update local settings store with API key settings
|
||||||
const storeUpdate: Partial<Pick<AppSettings, 'globalOpenAIApiKey' | 'globalAnthropicApiKey' | 'globalGoogleApiKey' | 'globalGroqApiKey' | 'ollamaBaseUrl'>> = {};
|
const storeUpdate: Partial<Pick<AppSettings, 'globalOpenAIApiKey' | 'globalAnthropicApiKey' | 'globalGoogleApiKey' | 'globalGroqApiKey' | 'globalOpenRouterApiKey' | 'ollamaBaseUrl'>> = {};
|
||||||
if (config.openaiApiKey.trim()) storeUpdate.globalOpenAIApiKey = config.openaiApiKey.trim();
|
if (config.openaiApiKey.trim()) storeUpdate.globalOpenAIApiKey = config.openaiApiKey.trim();
|
||||||
if (config.anthropicApiKey.trim()) storeUpdate.globalAnthropicApiKey = config.anthropicApiKey.trim();
|
if (config.anthropicApiKey.trim()) storeUpdate.globalAnthropicApiKey = config.anthropicApiKey.trim();
|
||||||
if (config.googleApiKey.trim()) storeUpdate.globalGoogleApiKey = config.googleApiKey.trim();
|
if (config.googleApiKey.trim()) storeUpdate.globalGoogleApiKey = config.googleApiKey.trim();
|
||||||
if (config.groqApiKey.trim()) storeUpdate.globalGroqApiKey = config.groqApiKey.trim();
|
if (config.groqApiKey.trim()) storeUpdate.globalGroqApiKey = config.groqApiKey.trim();
|
||||||
|
if (config.openrouterApiKey.trim()) storeUpdate.globalOpenRouterApiKey = config.openrouterApiKey.trim();
|
||||||
if (config.ollamaBaseUrl.trim()) storeUpdate.ollamaBaseUrl = config.ollamaBaseUrl.trim();
|
if (config.ollamaBaseUrl.trim()) storeUpdate.ollamaBaseUrl = config.ollamaBaseUrl.trim();
|
||||||
updateSettings(storeUpdate);
|
updateSettings(storeUpdate);
|
||||||
onNext();
|
onNext();
|
||||||
@@ -355,6 +375,7 @@ export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) {
|
|||||||
const needsVoyage = embeddingProvider === 'voyage';
|
const needsVoyage = embeddingProvider === 'voyage';
|
||||||
const needsGoogle = llmProvider === 'google' || embeddingProvider === 'google';
|
const needsGoogle = llmProvider === 'google' || embeddingProvider === 'google';
|
||||||
const needsGroq = llmProvider === 'groq';
|
const needsGroq = llmProvider === 'groq';
|
||||||
|
const needsOpenRouter = llmProvider === 'openrouter' || embeddingProvider === 'openrouter';
|
||||||
const needsOllama = llmProvider === 'ollama' || embeddingProvider === 'ollama';
|
const needsOllama = llmProvider === 'ollama' || embeddingProvider === 'ollama';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -606,6 +627,39 @@ export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* OpenRouter API Key */}
|
||||||
|
{needsOpenRouter && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="openrouter-key" className="text-sm font-medium text-foreground">
|
||||||
|
OpenRouter API Key
|
||||||
|
</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
id="openrouter-key"
|
||||||
|
type={showApiKey['openrouter'] ? 'text' : 'password'}
|
||||||
|
value={config.openrouterApiKey}
|
||||||
|
onChange={(e) => setConfig(prev => ({ ...prev, openrouterApiKey: e.target.value }))}
|
||||||
|
placeholder="sk-or-..."
|
||||||
|
className="pr-10 font-mono text-sm"
|
||||||
|
disabled={isSaving || isValidating}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => toggleShowApiKey('openrouter')}
|
||||||
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||||||
|
>
|
||||||
|
{showApiKey['openrouter'] ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Get your key from{' '}
|
||||||
|
<a href="https://openrouter.ai/keys" target="_blank" rel="noopener noreferrer" className="text-primary hover:text-primary/80">
|
||||||
|
OpenRouter Dashboard
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Ollama Settings */}
|
{/* Ollama Settings */}
|
||||||
{needsOllama && (
|
{needsOllama && (
|
||||||
<div className="space-y-3 p-3 rounded-md bg-muted/50">
|
<div className="space-y-3 p-3 rounded-md bg-muted/50">
|
||||||
|
|||||||
@@ -176,12 +176,12 @@ export interface GraphitiConnectionTestResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Memory Provider Types
|
// Memory Provider Types
|
||||||
// Embedding Providers: OpenAI, Voyage AI, Azure OpenAI, Ollama (local), Google
|
// Embedding Providers: OpenAI, Voyage AI, Azure OpenAI, Ollama (local), Google, OpenRouter
|
||||||
// Note: LLM provider removed - Claude SDK handles RAG queries
|
// Note: LLM provider removed - Claude SDK handles RAG queries
|
||||||
export type GraphitiEmbeddingProvider = 'openai' | 'voyage' | 'azure_openai' | 'ollama' | 'google';
|
export type GraphitiEmbeddingProvider = 'openai' | 'voyage' | 'azure_openai' | 'ollama' | 'google' | 'openrouter';
|
||||||
|
|
||||||
// Legacy type aliases for backward compatibility
|
// Legacy type aliases for backward compatibility
|
||||||
export type GraphitiLLMProvider = 'openai' | 'anthropic' | 'azure_openai' | 'ollama' | 'google' | 'groq';
|
export type GraphitiLLMProvider = 'openai' | 'anthropic' | 'azure_openai' | 'ollama' | 'google' | 'groq' | 'openrouter';
|
||||||
export type GraphitiProviderType = GraphitiLLMProvider;
|
export type GraphitiProviderType = GraphitiLLMProvider;
|
||||||
|
|
||||||
export interface GraphitiProviderConfig {
|
export interface GraphitiProviderConfig {
|
||||||
@@ -206,6 +206,12 @@ export interface GraphitiProviderConfig {
|
|||||||
googleApiKey?: string;
|
googleApiKey?: string;
|
||||||
googleEmbeddingModel?: string;
|
googleEmbeddingModel?: string;
|
||||||
|
|
||||||
|
// OpenRouter (multi-provider aggregator)
|
||||||
|
openrouterApiKey?: string;
|
||||||
|
openrouterBaseUrl?: string; // Default: https://openrouter.ai/api/v1
|
||||||
|
openrouterLlmModel?: string; // LLM model selection (e.g., 'anthropic/claude-3.5-sonnet')
|
||||||
|
openrouterEmbeddingModel?: string;
|
||||||
|
|
||||||
// Ollama Embeddings (local, no API key required)
|
// Ollama Embeddings (local, no API key required)
|
||||||
ollamaBaseUrl?: string; // Default: http://localhost:11434
|
ollamaBaseUrl?: string; // Default: http://localhost:11434
|
||||||
ollamaEmbeddingModel?: string;
|
ollamaEmbeddingModel?: string;
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ export interface AppSettings {
|
|||||||
globalAnthropicApiKey?: string;
|
globalAnthropicApiKey?: string;
|
||||||
globalGoogleApiKey?: string;
|
globalGoogleApiKey?: string;
|
||||||
globalGroqApiKey?: string;
|
globalGroqApiKey?: string;
|
||||||
|
globalOpenRouterApiKey?: string;
|
||||||
// Graphiti LLM provider settings
|
// Graphiti LLM provider settings
|
||||||
graphitiLlmProvider?: 'openai' | 'anthropic' | 'google' | 'groq' | 'ollama';
|
graphitiLlmProvider?: 'openai' | 'anthropic' | 'google' | 'groq' | 'ollama';
|
||||||
ollamaBaseUrl?: string;
|
ollamaBaseUrl?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user