From 02bef954f3fbbe31a06e4698a7cc099322d493db Mon Sep 17 00:00:00 2001 From: Fernando Possebon Date: Tue, 23 Dec 2025 13:58:55 -0300 Subject: [PATCH] feat: Add OpenRouter as LLM/embedding provider (#162) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 * 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 --------- Co-authored-by: Claude Sonnet 4.5 --- apps/backend/.env.example | 34 +++++++++- apps/backend/integrations/graphiti/config.py | 61 +++++++++++++++++- .../embedder_providers/__init__.py | 2 + .../embedder_providers/openrouter_embedder.py | 60 ++++++++++++++++++ .../graphiti/providers_pkg/factory.py | 6 ++ .../providers_pkg/llm_providers/__init__.py | 2 + .../llm_providers/openrouter_llm.py | 63 +++++++++++++++++++ .../components/onboarding/GraphitiStep.tsx | 58 ++++++++++++++++- apps/frontend/src/shared/types/project.ts | 12 +++- apps/frontend/src/shared/types/settings.ts | 1 + 10 files changed, 289 insertions(+), 10 deletions(-) create mode 100644 apps/backend/integrations/graphiti/providers_pkg/embedder_providers/openrouter_embedder.py create mode 100644 apps/backend/integrations/graphiti/providers_pkg/llm_providers/openrouter_llm.py diff --git a/apps/backend/.env.example b/apps/backend/.env.example index 3d4233e2..a2b98273 100644 --- a/apps/backend/.env.example +++ b/apps/backend/.env.example @@ -153,10 +153,10 @@ GRAPHITI_ENABLED=true # Choose which providers to use for LLM and embeddings. # 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 -# Embedder provider: openai | voyage | azure_openai | ollama | google +# Embedder provider: openai | voyage | azure_openai | ollama | google | openrouter # GRAPHITI_EMBEDDER_PROVIDER=openai # ============================================================================= @@ -221,6 +221,28 @@ GRAPHITI_ENABLED=true # Google Embedding Model (default: 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 # ============================================================================= @@ -307,3 +329,11 @@ GRAPHITI_ENABLED=true # GRAPHITI_LLM_PROVIDER=google # GRAPHITI_EMBEDDER_PROVIDER=google # 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 diff --git a/apps/backend/integrations/graphiti/config.py b/apps/backend/integrations/graphiti/config.py index a168615c..f2af6fd3 100644 --- a/apps/backend/integrations/graphiti/config.py +++ b/apps/backend/integrations/graphiti/config.py @@ -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+). Multi-Provider Support (V2): -- LLM Providers: OpenAI, Anthropic, Azure OpenAI, Ollama, Google AI -- Embedder Providers: OpenAI, Voyage AI, 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, OpenRouter Environment Variables: # Core @@ -89,6 +89,7 @@ class LLMProvider(str, Enum): AZURE_OPENAI = "azure_openai" OLLAMA = "ollama" GOOGLE = "google" + OPENROUTER = "openrouter" class EmbedderProvider(str, Enum): @@ -99,6 +100,7 @@ class EmbedderProvider(str, Enum): AZURE_OPENAI = "azure_openai" OLLAMA = "ollama" GOOGLE = "google" + OPENROUTER = "openrouter" @dataclass @@ -141,6 +143,12 @@ class GraphitiConfig: google_llm_model: str = "gemini-2.0-flash" 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_base_url: str = DEFAULT_OLLAMA_BASE_URL ollama_llm_model: str = "" @@ -196,6 +204,18 @@ class GraphitiConfig: "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_base_url = os.environ.get("OLLAMA_BASE_URL", DEFAULT_OLLAMA_BASE_URL) ollama_llm_model = os.environ.get("OLLAMA_LLM_MODEL", "") @@ -227,6 +247,10 @@ class GraphitiConfig: google_api_key=google_api_key, google_llm_model=google_llm_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_llm_model=ollama_llm_model, ollama_embedding_model=ollama_embedding_model, @@ -267,6 +291,8 @@ class GraphitiConfig: return bool(self.ollama_embedding_model) elif self.embedder_provider == "google": return bool(self.google_api_key) + elif self.embedder_provider == "openrouter": + return bool(self.openrouter_api_key) return False def get_validation_errors(self) -> list[str]: @@ -309,6 +335,11 @@ class GraphitiConfig: elif self.embedder_provider == "google": if not self.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: errors.append(f"Unknown embedder provider: {self.embedder_provider}") @@ -367,6 +398,18 @@ class GraphitiConfig: elif self.embedder_provider == "azure_openai": # Depends on the deployment, default to 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 def get_provider_signature(self) -> str: @@ -403,7 +446,14 @@ class GraphitiConfig: base_name = self.database # 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: base_name = base_name.split(f"_{provider}_")[0] break @@ -617,6 +667,11 @@ def get_available_providers() -> dict: available_llm.append("google") available_embedder.append("google") + # Check OpenRouter + if config.openrouter_api_key: + available_llm.append("openrouter") + available_embedder.append("openrouter") + # Check Ollama if config.ollama_llm_model: available_llm.append("ollama") diff --git a/apps/backend/integrations/graphiti/providers_pkg/embedder_providers/__init__.py b/apps/backend/integrations/graphiti/providers_pkg/embedder_providers/__init__.py index 7c0f7ed6..522c2965 100644 --- a/apps/backend/integrations/graphiti/providers_pkg/embedder_providers/__init__.py +++ b/apps/backend/integrations/graphiti/providers_pkg/embedder_providers/__init__.py @@ -18,6 +18,7 @@ from .ollama_embedder import ( get_embedding_dim_for_model, ) from .openai_embedder import create_openai_embedder +from .openrouter_embedder import create_openrouter_embedder from .voyage_embedder import create_voyage_embedder __all__ = [ @@ -26,6 +27,7 @@ __all__ = [ "create_azure_openai_embedder", "create_ollama_embedder", "create_google_embedder", + "create_openrouter_embedder", "KNOWN_OLLAMA_EMBEDDING_MODELS", "get_embedding_dim_for_model", ] diff --git a/apps/backend/integrations/graphiti/providers_pkg/embedder_providers/openrouter_embedder.py b/apps/backend/integrations/graphiti/providers_pkg/embedder_providers/openrouter_embedder.py new file mode 100644 index 00000000..61b21c29 --- /dev/null +++ b/apps/backend/integrations/graphiti/providers_pkg/embedder_providers/openrouter_embedder.py @@ -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) diff --git a/apps/backend/integrations/graphiti/providers_pkg/factory.py b/apps/backend/integrations/graphiti/providers_pkg/factory.py index 29f1daba..06eb2b66 100644 --- a/apps/backend/integrations/graphiti/providers_pkg/factory.py +++ b/apps/backend/integrations/graphiti/providers_pkg/factory.py @@ -16,6 +16,7 @@ from .embedder_providers import ( create_google_embedder, create_ollama_embedder, create_openai_embedder, + create_openrouter_embedder, create_voyage_embedder, ) from .exceptions import ProviderError @@ -25,6 +26,7 @@ from .llm_providers import ( create_google_llm_client, create_ollama_llm_client, create_openai_llm_client, + create_openrouter_llm_client, ) logger = logging.getLogger(__name__) @@ -58,6 +60,8 @@ def create_llm_client(config: "GraphitiConfig") -> Any: return create_ollama_llm_client(config) elif provider == "google": return create_google_llm_client(config) + elif provider == "openrouter": + return create_openrouter_llm_client(config) else: raise ProviderError(f"Unknown LLM provider: {provider}") @@ -90,5 +94,7 @@ def create_embedder(config: "GraphitiConfig") -> Any: return create_ollama_embedder(config) elif provider == "google": return create_google_embedder(config) + elif provider == "openrouter": + return create_openrouter_embedder(config) else: raise ProviderError(f"Unknown embedder provider: {provider}") diff --git a/apps/backend/integrations/graphiti/providers_pkg/llm_providers/__init__.py b/apps/backend/integrations/graphiti/providers_pkg/llm_providers/__init__.py index eb210859..be335f5f 100644 --- a/apps/backend/integrations/graphiti/providers_pkg/llm_providers/__init__.py +++ b/apps/backend/integrations/graphiti/providers_pkg/llm_providers/__init__.py @@ -15,6 +15,7 @@ from .azure_openai_llm import create_azure_openai_llm_client from .google_llm import create_google_llm_client from .ollama_llm import create_ollama_llm_client from .openai_llm import create_openai_llm_client +from .openrouter_llm import create_openrouter_llm_client __all__ = [ "create_openai_llm_client", @@ -22,4 +23,5 @@ __all__ = [ "create_azure_openai_llm_client", "create_ollama_llm_client", "create_google_llm_client", + "create_openrouter_llm_client", ] diff --git a/apps/backend/integrations/graphiti/providers_pkg/llm_providers/openrouter_llm.py b/apps/backend/integrations/graphiti/providers_pkg/llm_providers/openrouter_llm.py new file mode 100644 index 00000000..162b87aa --- /dev/null +++ b/apps/backend/integrations/graphiti/providers_pkg/llm_providers/openrouter_llm.py @@ -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) diff --git a/apps/frontend/src/renderer/components/onboarding/GraphitiStep.tsx b/apps/frontend/src/renderer/components/onboarding/GraphitiStep.tsx index d72d485c..b9f57654 100644 --- a/apps/frontend/src/renderer/components/onboarding/GraphitiStep.tsx +++ b/apps/frontend/src/renderer/components/onboarding/GraphitiStep.tsx @@ -44,6 +44,7 @@ const LLM_PROVIDERS: Array<{ { id: 'anthropic', name: 'Anthropic', description: 'Claude 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: 'openrouter', name: 'OpenRouter', description: 'Multi-provider aggregator', requiresApiKey: true }, { id: 'azure_openai', name: 'Azure OpenAI', description: 'Enterprise Azure deployment', requiresApiKey: true }, { 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: '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: 'openrouter', name: 'OpenRouter', description: 'OpenAI-compatible embeddings', requiresApiKey: true }, { id: 'azure_openai', name: 'Azure OpenAI', description: 'Enterprise Azure embeddings', requiresApiKey: true } ]; @@ -82,6 +84,11 @@ interface GraphitiConfig { googleApiKey: string; // Groq groqApiKey: string; + // OpenRouter + openrouterApiKey: string; + openrouterBaseUrl: string; + openrouterLlmModel: string; + openrouterEmbeddingModel: string; // HuggingFace huggingfaceApiKey: string; // Ollama @@ -118,6 +125,10 @@ export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) { voyageApiKey: '', googleApiKey: settings.globalGoogleApiKey || '', 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: '', ollamaBaseUrl: settings.ollamaBaseUrl || 'http://localhost:11434', ollamaLlmModel: '', @@ -194,6 +205,9 @@ export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) { if (llmProvider === 'groq') { 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 (!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 === 'google' ? config.googleApiKey : config.llmProvider === 'groq' ? config.groqApiKey : + config.llmProvider === 'openrouter' ? config.openrouterApiKey : config.llmProvider === 'azure_openai' ? config.azureOpenaiApiKey : 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({ dbPath: config.dbPath || undefined, @@ -303,6 +319,9 @@ export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) { if (config.groqApiKey.trim()) { settingsToSave.globalGroqApiKey = config.groqApiKey.trim(); } + if (config.openrouterApiKey.trim()) { + settingsToSave.globalOpenRouterApiKey = config.openrouterApiKey.trim(); + } if (config.ollamaBaseUrl.trim()) { settingsToSave.ollamaBaseUrl = config.ollamaBaseUrl.trim(); } @@ -311,11 +330,12 @@ export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) { if (result?.success) { // Update local settings store with API key settings - const storeUpdate: Partial> = {}; + const storeUpdate: Partial> = {}; if (config.openaiApiKey.trim()) storeUpdate.globalOpenAIApiKey = config.openaiApiKey.trim(); if (config.anthropicApiKey.trim()) storeUpdate.globalAnthropicApiKey = config.anthropicApiKey.trim(); if (config.googleApiKey.trim()) storeUpdate.globalGoogleApiKey = config.googleApiKey.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(); updateSettings(storeUpdate); onNext(); @@ -355,6 +375,7 @@ export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) { const needsVoyage = embeddingProvider === 'voyage'; const needsGoogle = llmProvider === 'google' || embeddingProvider === 'google'; const needsGroq = llmProvider === 'groq'; + const needsOpenRouter = llmProvider === 'openrouter' || embeddingProvider === 'openrouter'; const needsOllama = llmProvider === 'ollama' || embeddingProvider === 'ollama'; return ( @@ -606,6 +627,39 @@ export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) { )} + {/* OpenRouter API Key */} + {needsOpenRouter && ( +
+ +
+ setConfig(prev => ({ ...prev, openrouterApiKey: e.target.value }))} + placeholder="sk-or-..." + className="pr-10 font-mono text-sm" + disabled={isSaving || isValidating} + /> + +
+

+ Get your key from{' '} + + OpenRouter Dashboard + +

+
+ )} + {/* Ollama Settings */} {needsOllama && (
diff --git a/apps/frontend/src/shared/types/project.ts b/apps/frontend/src/shared/types/project.ts index 75aa5a46..f5847322 100644 --- a/apps/frontend/src/shared/types/project.ts +++ b/apps/frontend/src/shared/types/project.ts @@ -176,12 +176,12 @@ export interface GraphitiConnectionTestResult { } // 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 -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 -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 interface GraphitiProviderConfig { @@ -206,6 +206,12 @@ export interface GraphitiProviderConfig { googleApiKey?: 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) ollamaBaseUrl?: string; // Default: http://localhost:11434 ollamaEmbeddingModel?: string; diff --git a/apps/frontend/src/shared/types/settings.ts b/apps/frontend/src/shared/types/settings.ts index a217878b..c81d53d6 100644 --- a/apps/frontend/src/shared/types/settings.ts +++ b/apps/frontend/src/shared/types/settings.ts @@ -89,6 +89,7 @@ export interface AppSettings { globalAnthropicApiKey?: string; globalGoogleApiKey?: string; globalGroqApiKey?: string; + globalOpenRouterApiKey?: string; // Graphiti LLM provider settings graphitiLlmProvider?: 'openai' | 'anthropic' | 'google' | 'groq' | 'ollama'; ollamaBaseUrl?: string;