From 9b2a17f3c03ea1a79056b433f296d1606e7068c8 Mon Sep 17 00:00:00 2001
From: Chillbruhhh
Date: Thu, 18 Sep 2025 04:05:17 -0500
Subject: [PATCH 01/28] Add Anthropic and Grok provider support
---
.../src/components/settings/RAGSettings.tsx | 7 ---
.../src/server/services/credential_service.py | 25 +++++++++++
.../server/services/llm_provider_service.py | 43 +++++++++++++++++++
3 files changed, 68 insertions(+), 7 deletions(-)
diff --git a/archon-ui-main/src/components/settings/RAGSettings.tsx b/archon-ui-main/src/components/settings/RAGSettings.tsx
index 83766b6c3a..d54cd51062 100644
--- a/archon-ui-main/src/components/settings/RAGSettings.tsx
+++ b/archon-ui-main/src/components/settings/RAGSettings.tsx
@@ -842,13 +842,6 @@ export const RAGSettings = ({
);
}
})()}
- {(provider.key === 'anthropic' || provider.key === 'grok' || provider.key === 'openrouter') && (
-
- )}
))}
diff --git a/python/src/server/services/credential_service.py b/python/src/server/services/credential_service.py
index a57c1abbbd..954c8ab7a2 100644
--- a/python/src/server/services/credential_service.py
+++ b/python/src/server/services/credential_service.py
@@ -239,6 +239,14 @@ async def set_credential(
self._rag_cache_timestamp = None
logger.debug(f"Invalidated RAG settings cache due to update of {key}")
+ # Also invalidate provider service cache to ensure immediate effect
+ try:
+ from .llm_provider_service import clear_provider_cache
+ clear_provider_cache()
+ logger.debug("Also cleared LLM provider service cache")
+ except Exception as e:
+ logger.warning(f"Failed to clear provider service cache: {e}")
+
# Also invalidate LLM provider service cache for provider config
try:
from . import llm_provider_service
@@ -281,6 +289,14 @@ async def delete_credential(self, key: str) -> bool:
self._rag_cache_timestamp = None
logger.debug(f"Invalidated RAG settings cache due to deletion of {key}")
+ # Also invalidate provider service cache to ensure immediate effect
+ try:
+ from .llm_provider_service import clear_provider_cache
+ clear_provider_cache()
+ logger.debug("Also cleared LLM provider service cache")
+ except Exception as e:
+ logger.warning(f"Failed to clear provider service cache: {e}")
+
# Also invalidate LLM provider service cache for provider config
try:
from . import llm_provider_service
@@ -464,6 +480,9 @@ async def _get_provider_api_key(self, provider: str) -> str | None:
key_mapping = {
"openai": "OPENAI_API_KEY",
"google": "GOOGLE_API_KEY",
+ "openrouter": "OPENROUTER_API_KEY",
+ "anthropic": "ANTHROPIC_API_KEY",
+ "grok": "GROK_API_KEY",
"ollama": None, # No API key needed
}
@@ -478,6 +497,12 @@ def _get_provider_base_url(self, provider: str, rag_settings: dict) -> str | Non
return rag_settings.get("LLM_BASE_URL", "http://localhost:11434/v1")
elif provider == "google":
return "https://generativelanguage.googleapis.com/v1beta/openai/"
+ elif provider == "openrouter":
+ return "https://openrouter.ai/api/v1"
+ elif provider == "anthropic":
+ return "https://api.anthropic.com/v1"
+ elif provider == "grok":
+ return "https://api.x.ai/v1"
return None # Use default for OpenAI
async def set_active_provider(self, provider: str, service_type: str = "llm") -> bool:
diff --git a/python/src/server/services/llm_provider_service.py b/python/src/server/services/llm_provider_service.py
index f04f0741ba..ca99b08d63 100644
--- a/python/src/server/services/llm_provider_service.py
+++ b/python/src/server/services/llm_provider_service.py
@@ -97,6 +97,15 @@ async def get_llm_client(provider: str | None = None, use_embedding_provider: bo
# For Ollama, don't use the base_url from config - let _get_optimal_ollama_instance decide
base_url = provider_config["base_url"] if provider_name != "ollama" else None
+ # Validate provider name
+ allowed_providers = {"openai", "ollama", "google", "openrouter", "anthropic", "grok"}
+ if provider_name not in allowed_providers:
+ raise ValueError(f"Unsupported provider: {provider_name}. Allowed: {allowed_providers}")
+
+ # Validate API key format for security
+ if api_key and len(api_key.strip()) == 0:
+ api_key = None # Treat empty strings as None
+
logger.info(f"Creating LLM client for provider: {provider_name}")
if provider_name == "openai":
@@ -155,6 +164,35 @@ async def get_llm_client(provider: str | None = None, use_embedding_provider: bo
)
logger.info("Google Gemini client created successfully")
+ elif provider_name == "openrouter":
+ if not api_key:
+ raise ValueError("OpenRouter API key not found")
+
+ client = openai.AsyncOpenAI(
+ api_key=api_key,
+ base_url=base_url or "https://openrouter.ai/api/v1",
+ )
+ logger.info("OpenRouter client created successfully")
+
+ elif provider_name == "anthropic":
+ if not api_key:
+ raise ValueError("Anthropic API key not found")
+
+ client = openai.AsyncOpenAI(
+ api_key=api_key,
+ base_url=base_url or "https://api.anthropic.com/v1",
+ )
+ logger.info("Anthropic client created successfully")
+
+ elif provider_name == "grok":
+ if not api_key:
+ raise ValueError("Grok API key not found")
+
+ client = openai.AsyncOpenAI(
+ api_key=api_key,
+ base_url=base_url or "https://api.x.ai/v1",
+ )
+ logger.info("Grok client created successfully")
else:
raise ValueError(f"Unsupported LLM provider: {provider_name}")
@@ -250,6 +288,11 @@ async def get_embedding_model(provider: str | None = None) -> str:
provider_name = provider_config["provider"]
custom_model = provider_config["embedding_model"]
+ # Validate provider name
+ allowed_providers = {"openai", "ollama", "google", "openrouter", "anthropic", "grok"}
+ if provider_name not in allowed_providers:
+ logger.warning(f"Unknown embedding provider: {provider_name}, falling back to OpenAI")
+ provider_name = "openai"
# Use custom model if specified
if custom_model:
return custom_model
From 791ecd1e4729f20a96d11cf1579e95d54f6fa701 Mon Sep 17 00:00:00 2001
From: Chillbruhhh
Date: Fri, 19 Sep 2025 04:53:49 -0500
Subject: [PATCH 02/28] feat: Add crucial GPT-5 and reasoning model support for
OpenRouter
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Add requires_max_completion_tokens() function for GPT-5, o1, o3, Grok-3 series
- Add prepare_chat_completion_params() for reasoning model compatibility
- Implement max_tokens β max_completion_tokens conversion for reasoning models
- Add temperature handling for reasoning models (must be 1.0 default)
- Enhanced provider validation and API key security in provider endpoints
- Streamlined retry logic (3β2 attempts) for faster issue detection
- Add failure tracking and circuit breaker analysis for debugging
- Support OpenRouter format detection (openai/gpt-5-nano, openai/o1-mini)
- Improved Grok provider empty response handling with structured fallbacks
- Enhanced contextual embedding with provider-aware model selection
Core provider functionality:
- OpenRouter, Grok, Anthropic provider support with full embedding integration
- Provider-specific model defaults and validation
- Secure API connectivity testing endpoints
- Provider context passing for code generation workflows
π€ Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude
---
python/src/server/api_routes/knowledge_api.py | 20 +-
python/src/server/api_routes/providers_api.py | 154 ++++++
.../contextual_embedding_service.py | 34 +-
.../server/services/llm_provider_service.py | 502 ++++++++++++++++--
.../services/storage/code_storage_service.py | 387 +++++++++++++-
5 files changed, 1012 insertions(+), 85 deletions(-)
create mode 100644 python/src/server/api_routes/providers_api.py
diff --git a/python/src/server/api_routes/knowledge_api.py b/python/src/server/api_routes/knowledge_api.py
index 5672583859..3cd0f3dbf3 100644
--- a/python/src/server/api_routes/knowledge_api.py
+++ b/python/src/server/api_routes/knowledge_api.py
@@ -18,6 +18,8 @@
from fastapi import APIRouter, File, Form, HTTPException, UploadFile
from pydantic import BaseModel
+# Basic validation - simplified inline version
+
# Import unified logging
from ..config.logfire_config import get_logger, safe_logfire_error, safe_logfire_info
from ..services.crawler_manager import get_crawler
@@ -62,11 +64,25 @@ async def _validate_provider_api_key(provider: str = None) -> None:
logger.info("π Starting API key validation...")
try:
+ # Basic provider validation
if not provider:
provider = "openai"
+ else:
+ # Simple provider validation
+ allowed_providers = {"openai", "ollama", "google", "openrouter", "anthropic", "grok"}
+ if provider not in allowed_providers:
+ raise HTTPException(
+ status_code=400,
+ detail={
+ "error": "Invalid provider name",
+ "message": f"Provider '{provider}' not supported",
+ "error_type": "validation_error"
+ }
+ )
- logger.info(f"π Testing {provider.title()} API key with minimal embedding request...")
-
+ # Basic sanitization for logging
+ safe_provider = provider[:20] # Limit length
+ logger.info(f"π Testing {safe_provider.title()} API key with minimal embedding request...")
# Test API key with minimal embedding request - this will fail if key is invalid
from ..services.embeddings.embedding_service import create_embedding
test_result = await create_embedding(text="test")
diff --git a/python/src/server/api_routes/providers_api.py b/python/src/server/api_routes/providers_api.py
new file mode 100644
index 0000000000..9c405ecd43
--- /dev/null
+++ b/python/src/server/api_routes/providers_api.py
@@ -0,0 +1,154 @@
+"""
+Provider status API endpoints for testing connectivity
+
+Handles server-side provider connectivity testing without exposing API keys to frontend.
+"""
+
+import httpx
+from fastapi import APIRouter, HTTPException, Path
+
+from ..config.logfire_config import logfire
+from ..services.credential_service import credential_service
+# Provider validation - simplified inline version
+
+router = APIRouter(prefix="/api/providers", tags=["providers"])
+
+
+async def test_openai_connection(api_key: str) -> bool:
+ """Test OpenAI API connectivity"""
+ try:
+ async with httpx.AsyncClient(timeout=10.0) as client:
+ response = await client.get(
+ "https://api.openai.com/v1/models",
+ headers={"Authorization": f"Bearer {api_key}"}
+ )
+ return response.status_code == 200
+ except Exception as e:
+ logfire.warning(f"OpenAI connectivity test failed: {e}")
+ return False
+
+
+async def test_google_connection(api_key: str) -> bool:
+ """Test Google AI API connectivity"""
+ try:
+ async with httpx.AsyncClient(timeout=10.0) as client:
+ response = await client.get(
+ "https://generativelanguage.googleapis.com/v1/models",
+ headers={"x-goog-api-key": api_key}
+ )
+ return response.status_code == 200
+ except Exception:
+ logfire.warning("Google AI connectivity test failed")
+ return False
+
+
+async def test_anthropic_connection(api_key: str) -> bool:
+ """Test Anthropic API connectivity"""
+ try:
+ async with httpx.AsyncClient(timeout=10.0) as client:
+ response = await client.get(
+ "https://api.anthropic.com/v1/models",
+ headers={
+ "x-api-key": api_key,
+ "anthropic-version": "2023-06-01"
+ }
+ )
+ return response.status_code == 200
+ except Exception as e:
+ logfire.warning(f"Anthropic connectivity test failed: {e}")
+ return False
+
+
+async def test_openrouter_connection(api_key: str) -> bool:
+ """Test OpenRouter API connectivity"""
+ try:
+ async with httpx.AsyncClient(timeout=10.0) as client:
+ response = await client.get(
+ "https://openrouter.ai/api/v1/models",
+ headers={"Authorization": f"Bearer {api_key}"}
+ )
+ return response.status_code == 200
+ except Exception as e:
+ logfire.warning(f"OpenRouter connectivity test failed: {e}")
+ return False
+
+
+async def test_grok_connection(api_key: str) -> bool:
+ """Test Grok API connectivity"""
+ try:
+ async with httpx.AsyncClient(timeout=10.0) as client:
+ response = await client.get(
+ "https://api.x.ai/v1/models",
+ headers={"Authorization": f"Bearer {api_key}"}
+ )
+ return response.status_code == 200
+ except Exception as e:
+ logfire.warning(f"Grok connectivity test failed: {e}")
+ return False
+
+
+PROVIDER_TESTERS = {
+ "openai": test_openai_connection,
+ "google": test_google_connection,
+ "anthropic": test_anthropic_connection,
+ "openrouter": test_openrouter_connection,
+ "grok": test_grok_connection,
+}
+
+
+@router.get("/{provider}/status")
+async def get_provider_status(
+ provider: str = Path(
+ ...,
+ description="Provider name to test connectivity for",
+ regex="^[a-z0-9_]+$",
+ max_length=20
+ )
+):
+ """Test provider connectivity using server-side API key (secure)"""
+ try:
+ # Basic provider validation
+ allowed_providers = {"openai", "ollama", "google", "openrouter", "anthropic", "grok"}
+ if provider not in allowed_providers:
+ raise HTTPException(
+ status_code=400,
+ detail=f"Invalid provider '{provider}'. Allowed providers: {sorted(allowed_providers)}"
+ )
+
+ # Basic sanitization for logging
+ safe_provider = provider[:20] # Limit length
+ logfire.info(f"Testing {safe_provider} connectivity server-side")
+
+ if provider not in PROVIDER_TESTERS:
+ raise HTTPException(
+ status_code=400,
+ detail=f"Provider '{provider}' not supported for connectivity testing"
+ )
+
+ # Get API key server-side (never expose to client)
+ key_name = f"{provider.upper()}_API_KEY"
+ api_key = await credential_service.get_credential(key_name, decrypt=True)
+
+ if not api_key or not isinstance(api_key, str) or not api_key.strip():
+ logfire.info(f"No API key configured for {safe_provider}")
+ return {"ok": False, "reason": "no_key"}
+
+ # Test connectivity using server-side key
+ tester = PROVIDER_TESTERS[provider]
+ is_connected = await tester(api_key)
+
+ logfire.info(f"{safe_provider} connectivity test result: {is_connected}")
+ return {
+ "ok": is_connected,
+ "reason": "connected" if is_connected else "connection_failed",
+ "provider": provider # Echo back validated provider name
+ }
+
+ except HTTPException:
+ # Re-raise HTTP exceptions (they're already properly formatted)
+ raise
+ except Exception as e:
+ # Basic error sanitization for logging
+ safe_error = str(e)[:100] # Limit length
+ logfire.error(f"Error testing {provider[:20]} connectivity: {safe_error}")
+ raise HTTPException(status_code=500, detail={"error": "Internal server error during connectivity test"})
diff --git a/python/src/server/services/embeddings/contextual_embedding_service.py b/python/src/server/services/embeddings/contextual_embedding_service.py
index 76f3c59b31..559b7f11b7 100644
--- a/python/src/server/services/embeddings/contextual_embedding_service.py
+++ b/python/src/server/services/embeddings/contextual_embedding_service.py
@@ -12,6 +12,7 @@
from ...config.logfire_config import search_logger
from ..llm_provider_service import get_llm_client
from ..threading_service import get_threading_service
+from ..credential_service import credential_service
async def generate_contextual_embedding(
@@ -32,8 +33,6 @@ async def generate_contextual_embedding(
"""
# Model choice is a RAG setting, get from credential service
try:
- from ...services.credential_service import credential_service
-
model_choice = await credential_service.get_credential("MODEL_CHOICE", "gpt-4.1-nano")
except Exception as e:
# Fallback to environment variable or default
@@ -111,7 +110,7 @@ async def process_chunk_with_context(
async def _get_model_choice(provider: str | None = None) -> str:
- """Get model choice from credential service."""
+ """Get model choice from credential service with centralized defaults."""
from ..credential_service import credential_service
# Get the active provider configuration
@@ -119,31 +118,36 @@ async def _get_model_choice(provider: str | None = None) -> str:
model = provider_config.get("chat_model", "").strip() # Strip whitespace
provider_name = provider_config.get("provider", "openai")
- # Handle empty model case - fallback to provider-specific defaults or explicit config
+ # Handle empty model case - use centralized defaults
if not model:
- search_logger.warning(f"chat_model is empty for provider {provider_name}, using fallback logic")
-
+ search_logger.warning(f"chat_model is empty for provider {provider_name}, using centralized defaults")
+
+ # Special handling for Ollama to check specific credential
if provider_name == "ollama":
- # Try to get OLLAMA_CHAT_MODEL specifically
try:
ollama_model = await credential_service.get_credential("OLLAMA_CHAT_MODEL")
if ollama_model and ollama_model.strip():
model = ollama_model.strip()
search_logger.info(f"Using OLLAMA_CHAT_MODEL fallback: {model}")
else:
- # Use a sensible Ollama default
+ # Use default for Ollama
model = "llama3.2:latest"
- search_logger.info(f"Using Ollama default model: {model}")
+ search_logger.info(f"Using Ollama default: {model}")
except Exception as e:
search_logger.error(f"Error getting OLLAMA_CHAT_MODEL: {e}")
model = "llama3.2:latest"
- search_logger.info(f"Using Ollama fallback model: {model}")
- elif provider_name == "google":
- model = "gemini-1.5-flash"
+ search_logger.info(f"Using Ollama fallback: {model}")
else:
- # OpenAI or other providers
- model = "gpt-4o-mini"
-
+ # Use provider-specific defaults
+ provider_defaults = {
+ "openai": "gpt-4o-mini",
+ "openrouter": "anthropic/claude-3.5-sonnet",
+ "google": "gemini-1.5-flash",
+ "anthropic": "claude-3-5-haiku-20241022",
+ "grok": "grok-3-mini"
+ }
+ model = provider_defaults.get(provider_name, "gpt-4o-mini")
+ search_logger.debug(f"Using default model for provider {provider_name}: {model}")
search_logger.debug(f"Using model from credential service: {model}")
return model
diff --git a/python/src/server/services/llm_provider_service.py b/python/src/server/services/llm_provider_service.py
index ca99b08d63..1161939f41 100644
--- a/python/src/server/services/llm_provider_service.py
+++ b/python/src/server/services/llm_provider_service.py
@@ -16,28 +16,298 @@
logger = get_logger(__name__)
-# Settings cache with TTL
-_settings_cache: dict[str, tuple[Any, float]] = {}
+
+# Basic validation functions to avoid circular imports
+def _is_valid_provider(provider: str) -> bool:
+ """Basic provider validation."""
+ if not provider or not isinstance(provider, str):
+ return False
+ return provider.lower() in {"openai", "ollama", "google", "openrouter", "anthropic", "grok"}
+
+
+def _sanitize_for_log(text: str) -> str:
+ """Basic text sanitization for logging."""
+ if not text:
+ return ""
+ import re
+ sanitized = re.sub(r"sk-[a-zA-Z0-9-_]{20,}", "[REDACTED]", text)
+ sanitized = re.sub(r"xai-[a-zA-Z0-9-_]{20,}", "[REDACTED]", sanitized)
+ return sanitized[:100]
+
+
+# Secure settings cache with TTL and validation
+_settings_cache: dict[str, tuple[Any, float, str]] = {} # value, timestamp, checksum
_CACHE_TTL_SECONDS = 300 # 5 minutes
+_cache_access_log: list[dict] = [] # Track cache access patterns for security monitoring
+
+
+def _calculate_cache_checksum(value: Any) -> str:
+ """Calculate checksum for cache entry integrity validation."""
+ import hashlib
+ import json
+
+ # Convert value to JSON string for consistent hashing
+ try:
+ value_str = json.dumps(value, sort_keys=True, default=str)
+ return hashlib.sha256(value_str.encode()).hexdigest()[:16] # First 16 chars for efficiency
+ except Exception:
+ # Fallback for non-serializable objects
+ return hashlib.sha256(str(value).encode()).hexdigest()[:16]
+
+
+def _log_cache_access(key: str, action: str, hit: bool = None, security_event: str = None) -> None:
+ """Log cache access for security monitoring."""
+
+ access_entry = {
+ "timestamp": time.time(),
+ "key": _sanitize_for_log(key),
+ "action": action, # "get", "set", "invalidate", "clear"
+ "hit": hit, # For get operations
+ "security_event": security_event # "checksum_mismatch", "expired", etc.
+ }
+
+ # Keep only last 100 access entries to prevent memory growth
+ _cache_access_log.append(access_entry)
+ if len(_cache_access_log) > 100:
+ _cache_access_log.pop(0)
+
+ # Log security events at warning level
+ if security_event:
+ safe_key = _sanitize_for_log(key)
+ logger.warning(f"Cache security event: {security_event} for key '{safe_key}'")
def _get_cached_settings(key: str) -> Any | None:
- """Get cached settings if not expired."""
- if key in _settings_cache:
- value, timestamp = _settings_cache[key]
- if time.time() - timestamp < _CACHE_TTL_SECONDS:
+ """Get cached settings if not expired and valid."""
+
+ try:
+ if key in _settings_cache:
+ value, timestamp, stored_checksum = _settings_cache[key]
+ current_time = time.time()
+
+ # Check expiration with strict TTL enforcement
+ if current_time - timestamp >= _CACHE_TTL_SECONDS:
+ # Expired, remove from cache
+ del _settings_cache[key]
+ _log_cache_access(key, "get", hit=False, security_event="expired")
+ return None
+
+ # Verify cache entry integrity
+ current_checksum = _calculate_cache_checksum(value)
+ if current_checksum != stored_checksum:
+ # Cache tampering detected, remove entry
+ del _settings_cache[key]
+ _log_cache_access(key, "get", hit=False, security_event="checksum_mismatch")
+ logger.error(f"Cache integrity violation detected for key: {_sanitize_for_log(key)}")
+ return None
+
+ # Additional validation for provider configurations
+ if "provider_config" in key and isinstance(value, dict):
+ # Basic validation: check required fields
+ if not value.get("provider") or not _is_valid_provider(value.get("provider")):
+ # Invalid configuration in cache, remove it
+ del _settings_cache[key]
+ _log_cache_access(key, "get", hit=False, security_event="invalid_config")
+ return None
+
+ _log_cache_access(key, "get", hit=True)
return value
- else:
- # Expired, remove from cache
- del _settings_cache[key]
- return None
+
+ _log_cache_access(key, "get", hit=False)
+ return None
+
+ except Exception as e:
+ # Cache access error, log and return None for safety
+ _log_cache_access(key, "get", hit=False, security_event=f"access_error: {str(e)}")
+ return None
def _set_cached_settings(key: str, value: Any) -> None:
- """Cache settings with current timestamp."""
- _settings_cache[key] = (value, time.time())
+ """Cache settings with current timestamp and integrity checksum."""
+
+ try:
+ # Validate provider configurations before caching
+ if "provider_config" in key and isinstance(value, dict):
+ # Basic validation: check required fields
+ if not value.get("provider") or not _is_valid_provider(value.get("provider")):
+ _log_cache_access(key, "set", security_event="invalid_config_rejected")
+ logger.warning(f"Rejected caching of invalid provider config for key: {_sanitize_for_log(key)}")
+ return
+
+ # Calculate integrity checksum
+ checksum = _calculate_cache_checksum(value)
+
+ # Store with timestamp and checksum
+ _settings_cache[key] = (value, time.time(), checksum)
+ _log_cache_access(key, "set")
+
+ except Exception as e:
+ _log_cache_access(key, "set", security_event=f"set_error: {str(e)}")
+ logger.error(f"Failed to cache settings for key {_sanitize_for_log(key)}: {e}")
+
+
+def clear_provider_cache() -> None:
+ """Clear the provider configuration cache to force refresh on next request."""
+ global _settings_cache
+
+ cache_size_before = len(_settings_cache)
+ _settings_cache.clear()
+ _log_cache_access("*", "clear")
+ logger.debug(f"Provider configuration cache cleared ({cache_size_before} entries removed)")
+
+
+def invalidate_provider_cache(provider: str = None) -> None:
+ """
+ Invalidate specific provider cache entries or all cache entries.
+
+ Args:
+ provider: Optional provider name to invalidate. If None, clears all cache.
+ """
+ global _settings_cache
+
+ if provider is None:
+ # Clear entire cache
+ cache_size_before = len(_settings_cache)
+ _settings_cache.clear()
+ _log_cache_access("*", "invalidate")
+ logger.debug(f"All provider cache entries invalidated ({cache_size_before} entries)")
+ else:
+ # Validate provider name before processing
+ if not _is_valid_provider(provider):
+ _log_cache_access(provider, "invalidate", security_event="invalid_provider_name")
+ logger.warning(f"Rejected cache invalidation for invalid provider: {_sanitize_for_log(provider)}")
+ return
+
+ # Clear specific provider entries
+ keys_to_remove = []
+ for key in _settings_cache.keys():
+ if provider in key:
+ keys_to_remove.append(key)
+
+ for key in keys_to_remove:
+ del _settings_cache[key]
+ _log_cache_access(key, "invalidate")
+
+ safe_provider = _sanitize_for_log(provider)
+ logger.debug(f"Cache entries for provider '{safe_provider}' invalidated: {len(keys_to_remove)} entries removed")
+
+
+def get_cache_stats() -> dict[str, Any]:
+ """
+ Get cache statistics with security metrics for monitoring and debugging.
+
+ Returns:
+ Dictionary containing cache statistics and security metrics
+ """
+ global _settings_cache, _cache_access_log
+ current_time = time.time()
+
+ stats = {
+ "total_entries": len(_settings_cache),
+ "fresh_entries": 0,
+ "stale_entries": 0,
+ "cache_hit_potential": 0.0,
+ "security_metrics": {
+ "integrity_violations": 0,
+ "expired_access_attempts": 0,
+ "invalid_config_rejections": 0,
+ "access_errors": 0,
+ "total_security_events": 0
+ },
+ "access_patterns": {
+ "recent_cache_hits": 0,
+ "recent_cache_misses": 0,
+ "hit_rate": 0.0
+ }
+ }
+
+ # Analyze cache entries
+ for key, (value, timestamp, checksum) in _settings_cache.items():
+ age = current_time - timestamp
+ if age < _CACHE_TTL_SECONDS:
+ stats["fresh_entries"] += 1
+ else:
+ stats["stale_entries"] += 1
+
+ if stats["total_entries"] > 0:
+ stats["cache_hit_potential"] = stats["fresh_entries"] / stats["total_entries"]
+
+ # Analyze security events from access log
+ recent_threshold = current_time - 3600 # Last hour
+ recent_hits = 0
+ recent_misses = 0
+
+ for access in _cache_access_log:
+ if access["timestamp"] >= recent_threshold:
+ if access["action"] == "get":
+ if access["hit"]:
+ recent_hits += 1
+ else:
+ recent_misses += 1
+ # Count security events
+ if access["security_event"]:
+ stats["security_metrics"]["total_security_events"] += 1
+ if "checksum_mismatch" in access["security_event"]:
+ stats["security_metrics"]["integrity_violations"] += 1
+ elif "expired" in access["security_event"]:
+ stats["security_metrics"]["expired_access_attempts"] += 1
+ elif "invalid_config" in access["security_event"]:
+ stats["security_metrics"]["invalid_config_rejections"] += 1
+ elif "error" in access["security_event"]:
+ stats["security_metrics"]["access_errors"] += 1
+
+ # Calculate hit rate
+ total_recent_access = recent_hits + recent_misses
+ if total_recent_access > 0:
+ stats["access_patterns"]["hit_rate"] = recent_hits / total_recent_access
+
+ stats["access_patterns"]["recent_cache_hits"] = recent_hits
+ stats["access_patterns"]["recent_cache_misses"] = recent_misses
+
+ return stats
+
+
+def get_cache_security_report() -> dict[str, Any]:
+ """
+ Get detailed security report for cache monitoring.
+
+ Returns:
+ Detailed security analysis of cache operations
+ """
+ global _cache_access_log
+ current_time = time.time()
+
+ report = {
+ "timestamp": current_time,
+ "analysis_period_hours": 1,
+ "security_events": [],
+ "recommendations": []
+ }
+
+ # Extract security events from last hour
+ recent_threshold = current_time - 3600
+ security_events = [
+ access for access in _cache_access_log
+ if access["timestamp"] >= recent_threshold and access["security_event"]
+ ]
+
+ report["security_events"] = security_events
+
+ # Generate recommendations based on security events
+ if len(security_events) > 10:
+ report["recommendations"].append("High number of security events detected - investigate potential attacks")
+
+ integrity_violations = sum(1 for event in security_events if "checksum_mismatch" in event.get("security_event", ""))
+ if integrity_violations > 0:
+ report["recommendations"].append(f"Cache integrity violations detected ({integrity_violations}) - check for memory corruption or attacks")
+
+ invalid_configs = sum(1 for event in security_events if "invalid_config" in event.get("security_event", ""))
+ if invalid_configs > 3:
+ report["recommendations"].append(f"Multiple invalid configuration attempts ({invalid_configs}) - validate data sources")
+
+ return report
@asynccontextmanager
async def get_llm_client(provider: str | None = None, use_embedding_provider: bool = False,
instance_type: str | None = None, base_url: str | None = None):
@@ -97,43 +367,68 @@ async def get_llm_client(provider: str | None = None, use_embedding_provider: bo
# For Ollama, don't use the base_url from config - let _get_optimal_ollama_instance decide
base_url = provider_config["base_url"] if provider_name != "ollama" else None
- # Validate provider name
- allowed_providers = {"openai", "ollama", "google", "openrouter", "anthropic", "grok"}
- if provider_name not in allowed_providers:
- raise ValueError(f"Unsupported provider: {provider_name}. Allowed: {allowed_providers}")
+ # Comprehensive provider validation with security checks
+ if not _is_valid_provider(provider_name):
+ raise ValueError(f"Provider validation failed: invalid provider '{provider_name}'")
- # Validate API key format for security
- if api_key and len(api_key.strip()) == 0:
- api_key = None # Treat empty strings as None
+ # Validate API key format for security (prevent injection)
+ if api_key:
+ if len(api_key.strip()) == 0:
+ api_key = None # Treat empty strings as None
+ elif len(api_key) > 500: # Reasonable API key length limit
+ raise ValueError("API key length exceeds security limits")
+ # Additional security: check for suspicious patterns
+ if any(char in api_key for char in ['\n', '\r', '\t', '\0']):
+ raise ValueError("API key contains invalid characters")
- logger.info(f"Creating LLM client for provider: {provider_name}")
+ # Sanitize provider name for logging
+ safe_provider_name = _sanitize_for_log(provider_name)
+ logger.info(f"Creating LLM client for provider: {safe_provider_name}")
if provider_name == "openai":
if not api_key:
- # Check if Ollama instances are available as fallback
- logger.warning("OpenAI API key not found, attempting Ollama fallback")
+ # Check if Ollama fallback is explicitly enabled (fail fast principle)
try:
- # Try to get an optimal Ollama instance for fallback
- ollama_base_url = await _get_optimal_ollama_instance(
- instance_type="embedding" if use_embedding_provider else "chat",
- use_embedding_provider=use_embedding_provider
- )
- if ollama_base_url:
- logger.info(f"Falling back to Ollama instance: {ollama_base_url}")
- provider_name = "ollama"
- api_key = "ollama" # Ollama doesn't need a real API key
- base_url = ollama_base_url
- # Create Ollama client after fallback
- client = openai.AsyncOpenAI(
- api_key="ollama",
- base_url=ollama_base_url,
+ enable_fallback = await credential_service.get_credential("ENABLE_OLLAMA_FALLBACK", "false")
+ enable_fallback = enable_fallback.lower() == "true"
+ except Exception:
+ enable_fallback = False # Default to false for fail-fast behavior
+
+ if enable_fallback:
+ logger.warning("OpenAI API key not found, attempting configured Ollama fallback")
+ try:
+ # Try to get an optimal Ollama instance for fallback
+ ollama_base_url = await _get_optimal_ollama_instance(
+ instance_type="embedding" if use_embedding_provider else "chat",
+ use_embedding_provider=use_embedding_provider
)
- logger.info(f"Ollama fallback client created successfully with base URL: {ollama_base_url}")
- else:
- raise ValueError("OpenAI API key not found and no Ollama instances available")
- except Exception as ollama_error:
- logger.error(f"Ollama fallback failed: {ollama_error}")
- raise ValueError("OpenAI API key not found and Ollama fallback failed") from ollama_error
+ if ollama_base_url:
+ logger.info(f"Falling back to Ollama instance: {ollama_base_url}")
+ provider_name = "ollama"
+ api_key = "ollama" # Ollama doesn't need a real API key
+ base_url = ollama_base_url
+ # Create Ollama client after fallback
+ client = openai.AsyncOpenAI(
+ api_key="ollama",
+ base_url=ollama_base_url,
+ )
+ logger.info(f"Ollama fallback client created successfully with base URL: {ollama_base_url}")
+ else:
+ raise ValueError("OpenAI API key not found and no Ollama instances available for fallback")
+ except Exception as ollama_error:
+ logger.error(f"Configured Ollama fallback failed: {ollama_error}")
+ raise ValueError("OpenAI API key not found and configured Ollama fallback failed") from ollama_error
+ else:
+ # Fail fast and loud - provide clear instructions
+ error_msg = (
+ "OpenAI API key not found. To fix this:\n"
+ "1. Set OPENAI_API_KEY environment variable, OR\n"
+ "2. Configure OpenAI API key in the UI settings, OR\n"
+ "3. Enable Ollama fallback by setting ENABLE_OLLAMA_FALLBACK=true\n"
+ "Current provider configuration requires a valid OpenAI API key."
+ )
+ logger.error(error_msg)
+ raise ValueError(error_msg)
else:
# Only create OpenAI client if we have an API key (didn't fallback to Ollama)
client = openai.AsyncOpenAI(api_key=api_key)
@@ -186,7 +481,19 @@ async def get_llm_client(provider: str | None = None, use_embedding_provider: bo
elif provider_name == "grok":
if not api_key:
- raise ValueError("Grok API key not found")
+ raise ValueError("Grok API key not found - set GROK_API_KEY environment variable")
+
+ # Enhanced Grok API key validation (secure - no key fragments logged)
+ key_format_valid = api_key.startswith("xai-")
+ key_length_valid = len(api_key) >= 20
+
+ if not key_format_valid:
+ logger.warning("Grok API key format validation failed - should start with 'xai-'")
+
+ if not key_length_valid:
+ logger.warning("Grok API key validation failed - insufficient length")
+
+ logger.debug(f"Grok API key validation: format_valid={key_format_valid}, length_valid={key_length_valid}")
client = openai.AsyncOpenAI(
api_key=api_key,
@@ -288,14 +595,20 @@ async def get_embedding_model(provider: str | None = None) -> str:
provider_name = provider_config["provider"]
custom_model = provider_config["embedding_model"]
- # Validate provider name
- allowed_providers = {"openai", "ollama", "google", "openrouter", "anthropic", "grok"}
- if provider_name not in allowed_providers:
- logger.warning(f"Unknown embedding provider: {provider_name}, falling back to OpenAI")
+ # Comprehensive provider validation for embeddings
+ if not _is_valid_provider(provider_name):
+ safe_provider = _sanitize_for_log(provider_name)
+ logger.warning(f"Invalid embedding provider: {safe_provider}, falling back to OpenAI")
provider_name = "openai"
- # Use custom model if specified
- if custom_model:
- return custom_model
+ # Use custom model if specified (with validation)
+ if custom_model and len(custom_model.strip()) > 0:
+ custom_model = custom_model.strip()
+ # Basic model name validation (check length and basic characters)
+ if len(custom_model) <= 100 and not any(char in custom_model for char in ['\n', '\r', '\t', '\0']):
+ return custom_model
+ else:
+ safe_model = _sanitize_for_log(custom_model)
+ logger.warning(f"Invalid custom embedding model '{safe_model}' for provider '{provider_name}', using default")
# Return provider-specific defaults
if provider_name == "openai":
@@ -305,7 +618,16 @@ async def get_embedding_model(provider: str | None = None) -> str:
return "nomic-embed-text"
elif provider_name == "google":
# Google's embedding model
- return "text-embedding-004"
+ return "gemini-embedding-001"
+ elif provider_name == "openrouter":
+ # OpenRouter supports OpenAI embedding models
+ return "text-embedding-3-small"
+ elif provider_name == "anthropic":
+ # Anthropic doesn't have native embedding models, fallback to OpenAI
+ return "text-embedding-3-small"
+ elif provider_name == "grok":
+ # Grok doesn't have embedding models yet, fallback to OpenAI
+ return "text-embedding-3-small"
else:
# Fallback to OpenAI's model
return "text-embedding-3-small"
@@ -426,3 +748,81 @@ async def validate_provider_instance(provider: str, instance_url: str | None = N
"error_message": str(e),
"validation_timestamp": time.time()
}
+
+
+def requires_max_completion_tokens(model_name: str) -> bool:
+ """
+ Check if a model requires max_completion_tokens instead of max_tokens.
+
+ OpenAI changed the parameter for reasoning models (o1, o3, GPT-5 series)
+ introduced in September 2024.
+
+ Args:
+ model_name: The model name to check
+
+ Returns:
+ True if the model requires max_completion_tokens, False otherwise
+ """
+ if not model_name:
+ return False
+
+ # Normalize to lowercase for comparison
+ model_lower = model_name.lower()
+
+ # Models that require max_completion_tokens (reasoning models)
+ reasoning_model_prefixes = [
+ "o1", # o1, o1-mini, o1-preview, etc.
+ "o3", # o3, o3-mini, etc.
+ "gpt-5", # gpt-5, gpt-5-nano, gpt-5-mini, etc.
+ "grok-3", # grok-3 series are reasoning models
+ ]
+
+ # Check for reasoning models (including OpenRouter prefixed models)
+ for prefix in reasoning_model_prefixes:
+ if model_lower.startswith(prefix):
+ return True
+ # Also check for OpenRouter format: "openai/gpt-5-nano", "openai/o1-mini", etc.
+ if f"openai/{prefix}" in model_lower:
+ return True
+
+ return False
+
+
+def prepare_chat_completion_params(model: str, params: dict) -> dict:
+ """
+ Convert parameters for compatibility with reasoning models (GPT-5, o1, o3 series).
+
+ OpenAI made several API changes for reasoning models:
+ 1. max_tokens β max_completion_tokens
+ 2. temperature must be 1.0 (default) - custom values not supported
+
+ This ensures compatibility with OpenAI's API changes for newer models
+ while maintaining backward compatibility for existing models.
+
+ Args:
+ model: The model name being used
+ params: Dictionary of API parameters
+
+ Returns:
+ Updated parameters dictionary with correct parameters for the model
+ """
+ if not model or not params:
+ return params
+
+ # Make a copy to avoid modifying the original
+ updated_params = params.copy()
+
+ is_reasoning_model = requires_max_completion_tokens(model)
+
+ # Convert max_tokens to max_completion_tokens for reasoning models
+ if is_reasoning_model and "max_tokens" in updated_params:
+ max_tokens_value = updated_params.pop("max_tokens")
+ updated_params["max_completion_tokens"] = max_tokens_value
+ logger.debug(f"Converted max_tokens to max_completion_tokens for model {model}")
+
+ # Remove custom temperature for reasoning models (they only support default temperature=1.0)
+ if is_reasoning_model and "temperature" in updated_params:
+ original_temp = updated_params.pop("temperature")
+ logger.debug(f"Removed custom temperature {original_temp} for reasoning model {model} (only supports default temperature=1.0)")
+
+ return updated_params
diff --git a/python/src/server/services/storage/code_storage_service.py b/python/src/server/services/storage/code_storage_service.py
index ece5ea1007..a8518c151f 100644
--- a/python/src/server/services/storage/code_storage_service.py
+++ b/python/src/server/services/storage/code_storage_service.py
@@ -19,23 +19,93 @@
from ...config.logfire_config import search_logger
from ..embeddings.contextual_embedding_service import generate_contextual_embeddings_batch
from ..embeddings.embedding_service import create_embeddings_batch
+from ..llm_provider_service import get_llm_client, prepare_chat_completion_params, requires_max_completion_tokens
+from ..credential_service import credential_service
-def _get_model_choice() -> str:
- """Get MODEL_CHOICE with direct fallback."""
+def _is_reasoning_model(model: str) -> bool:
+ """
+ Check if a model is a reasoning model that may return empty responses.
+
+ Args:
+ model: The model identifier
+
+ Returns:
+ True if the model is a reasoning model (GPT-5, o1, o3 series)
+ """
+ return requires_max_completion_tokens(model)
+
+
+def _supports_response_format(provider: str, model: str) -> bool:
+ """
+ Determine if a specific provider/model combination supports response_format.
+
+ Args:
+ provider: The LLM provider name
+ model: The model identifier
+
+ Returns:
+ True if the model supports structured JSON output via response_format
+ """
+ if not provider:
+ return True # Default to supporting it
+
+ provider = provider.lower()
+
+ if provider == "openai":
+ return True # OpenAI models generally support response_format
+ elif provider == "openrouter":
+ # OpenRouter: "OpenAI models, Nitro models, and some others" support it
+ model_lower = model.lower()
+
+ # Known compatible model patterns on OpenRouter
+ compatible_patterns = [
+ "openai/", # OpenAI models on OpenRouter
+ "gpt-", # GPT models
+ "nitro/", # Nitro models
+ "deepseek/", # DeepSeek models often support JSON
+ "google/", # Some Google models support it
+ ]
+
+ for pattern in compatible_patterns:
+ if pattern in model_lower:
+ search_logger.debug(f"Model {model} supports response_format (pattern: {pattern})")
+ return True
+
+ search_logger.debug(f"Model {model} may not support response_format, skipping")
+ return False
+ else:
+ # Conservative approach for other providers
+ return False
+
+
+async def _get_model_choice() -> str:
+ """Get MODEL_CHOICE with provider-aware defaults from centralized service."""
try:
- # Direct cache/env fallback
- from ..credential_service import credential_service
+ # Get the active provider configuration
+ provider_config = await credential_service.get_active_provider("llm")
+ active_provider = provider_config.get("provider", "openai")
+ model = provider_config.get("chat_model")
+
+ # If no custom model is set, use provider-specific defaults
+ if not model or model.strip() == "":
+ # Provider-specific defaults
+ provider_defaults = {
+ "openai": "gpt-4o-mini",
+ "openrouter": "anthropic/claude-3.5-sonnet",
+ "google": "gemini-1.5-flash",
+ "ollama": "llama3.2:latest",
+ "anthropic": "claude-3-5-haiku-20241022",
+ "grok": "grok-3-mini"
+ }
+ model = provider_defaults.get(active_provider, "gpt-4o-mini")
+ search_logger.debug(f"Using default model for provider {active_provider}: {model}")
- if credential_service._cache_initialized and "MODEL_CHOICE" in credential_service._cache:
- model = credential_service._cache["MODEL_CHOICE"]
- else:
- model = os.getenv("MODEL_CHOICE", "gpt-4.1-nano")
- search_logger.debug(f"Using model choice: {model}")
+ search_logger.debug(f"Using model for provider {active_provider}: {model}")
return model
except Exception as e:
search_logger.warning(f"Error getting model choice: {e}, using default")
- return "gpt-4.1-nano"
+ return "gpt-4o-mini"
def _get_max_workers() -> int:
@@ -155,6 +225,130 @@ def score_block(block):
return best_block
+def _should_attempt_fallback(provider: str, model: str, is_reasoning: bool, error_context: dict) -> bool:
+ """
+ Determine if fallback should be attempted based on error type and configuration.
+
+ Args:
+ provider: The LLM provider name
+ model: The model identifier
+ is_reasoning: Whether this is a reasoning model
+ error_context: Context about the error that occurred
+
+ Returns:
+ True if fallback should be attempted
+ """
+ # Check for environment variable to disable fallbacks (fail-fast mode)
+ if os.getenv("DISABLE_LLM_FALLBACKS", "false").lower() == "true":
+ search_logger.debug("LLM fallbacks disabled by DISABLE_LLM_FALLBACKS environment variable")
+ return False
+
+ # Only attempt fallback for specific provider/model combinations
+ fallback_eligible_providers = ["grok", "openai"] # Providers that support fallback
+
+ if provider not in fallback_eligible_providers:
+ search_logger.debug(f"Provider {provider} not eligible for fallback")
+ return False
+
+ # Only allow fallback for empty responses, not other error types
+ if error_context.get("response_type") != "empty_content":
+ search_logger.debug(f"Error type {error_context.get('response_type')} not eligible for fallback")
+ return False
+
+ # Allow fallback for Grok and reasoning models that commonly have empty responses
+ if provider == "grok" or is_reasoning:
+ search_logger.debug(f"Fallback enabled for {provider}/{model} (reasoning: {is_reasoning})")
+ return True
+
+ return False
+
+
+async def _attempt_single_fallback(
+ original_model: str,
+ original_provider: str,
+ is_reasoning: bool,
+ original_params: dict,
+ error_context: dict
+) -> str | None:
+ """
+ Attempt a single fallback to gpt-4o-mini with structured tracking.
+
+ Args:
+ original_model: The original model that failed
+ original_provider: The original provider that failed
+ is_reasoning: Whether original was a reasoning model
+ original_params: The original request parameters
+ error_context: Context about the original error
+
+ Returns:
+ Response content if fallback succeeded, None if failed
+ """
+ fallback_start_time = time.time()
+
+ # Always fallback to reliable gpt-4o-mini
+ fallback_model = "gpt-4o-mini"
+ fallback_provider = "openai"
+
+ fallback_context = {
+ "original_model": original_model,
+ "original_provider": original_provider,
+ "fallback_model": fallback_model,
+ "fallback_provider": fallback_provider,
+ "original_error": error_context,
+ "fallback_attempt_time": time.time()
+ }
+
+ search_logger.info(f"Attempting single fallback: {original_model} β {fallback_model}")
+
+ try:
+ # Prepare fallback parameters (simplified, no JSON format to avoid issues)
+ fallback_params = {
+ "model": fallback_model,
+ "messages": original_params["messages"],
+ "max_tokens": min(original_params.get("max_tokens", 500), 500), # Cap for reliability
+ "temperature": original_params.get("temperature", 0.3)
+ }
+
+ # No response_format for fallback to maximize reliability
+ if "response_format" in fallback_params:
+ del fallback_params["response_format"]
+
+ async with get_llm_client(provider=fallback_provider) as fallback_client:
+ fallback_response = await fallback_client.chat.completions.create(**fallback_params)
+ fallback_content = fallback_response.choices[0].message.content
+
+ if fallback_content and fallback_content.strip():
+ fallback_time = time.time() - fallback_start_time
+ fallback_success = {
+ **fallback_context,
+ "fallback_succeeded": True,
+ "fallback_time": f"{fallback_time:.2f}s",
+ "fallback_content_length": len(fallback_content.strip())
+ }
+ search_logger.info(f"Fallback success: {fallback_success}")
+ return fallback_content.strip()
+ else:
+ # Fallback returned empty - log and return None
+ fallback_failure = {
+ **fallback_context,
+ "fallback_succeeded": False,
+ "fallback_error": "empty_response"
+ }
+ search_logger.error(f"Fallback returned empty response: {fallback_failure}")
+ return None
+
+ except Exception as e:
+ fallback_time = time.time() - fallback_start_time
+ fallback_error = {
+ **fallback_context,
+ "fallback_succeeded": False,
+ "fallback_error": str(e),
+ "fallback_time": f"{fallback_time:.2f}s"
+ }
+ search_logger.error(f"Fallback exception: {fallback_error}")
+ return None
+
+
def extract_code_blocks(markdown_content: str, min_length: int = None) -> list[dict[str, Any]]:
"""
Extract code blocks from markdown content along with context.
@@ -168,8 +362,6 @@ def extract_code_blocks(markdown_content: str, min_length: int = None) -> list[d
"""
# Load all code extraction settings with direct fallback
try:
- from ...services.credential_service import credential_service
-
def _get_setting_fallback(key: str, default: str) -> str:
if credential_service._cache_initialized and key in credential_service._cache:
return credential_service._cache[key]
@@ -570,7 +762,172 @@ async def _generate_code_example_summary_async(
temperature=0.3,
)
- response_content = response.choices[0].message.content.strip()
+ # Try to use response_format, but handle gracefully if not supported
+ # Note: Grok reasoning models don't work well with response_format
+ if provider in ["openai", "google", "anthropic"] or (provider == "openrouter" and model_choice.startswith("openai/")):
+ request_params["response_format"] = {"type": "json_object"}
+
+ # Grok-specific parameter validation and filtering
+ if provider == "grok":
+ # Remove any parameters that Grok reasoning models don't support
+ # Based on xAI docs: presencePenalty, frequencyPenalty, stop are not supported
+ unsupported_params = ["presence_penalty", "frequency_penalty", "stop", "reasoning_effort"]
+ for param in unsupported_params:
+ if param in request_params:
+ removed_value = request_params.pop(param)
+ search_logger.warning(f"Removed unsupported Grok parameter '{param}': {removed_value}")
+
+ # Validate that we're using supported parameters only
+ supported_params = ["model", "messages", "max_tokens", "temperature", "response_format", "stream", "tools", "tool_choice"]
+ for param in request_params:
+ if param not in supported_params:
+ search_logger.warning(f"Parameter '{param}' may not be supported by Grok reasoning models")
+
+ start_time = time.time() # Initialize for all models
+
+ if provider == "grok" or is_reasoning:
+ model_type = "Grok" if provider == "grok" else f"reasoning model ({model_choice})"
+ search_logger.debug(f"{model_type} request params: {request_params}")
+ search_logger.debug(f"{model_type} prompt length: {len(prompt)} characters")
+ search_logger.debug(f"{model_type} prompt preview: {prompt[:200]}...")
+
+ # Simplified retry logic - reduced from 3 to 2 retries to surface issues faster
+ max_retries = 2 if (provider == "grok" or is_reasoning) else 1
+ retry_delay = 1.0 # Start with 1 second delay
+ failure_reasons = [] # Track failure reasons for circuit breaker analysis
+
+ for attempt in range(max_retries):
+ try:
+ if provider == "grok" and attempt > 0:
+ search_logger.info(f"Grok retry attempt {attempt + 1}/{max_retries} after {retry_delay:.1f}s delay")
+ await asyncio.sleep(retry_delay)
+
+ response = await client.chat.completions.create(**request_params)
+
+ # Check for empty response - handle Grok reasoning models
+ message = response.choices[0].message if response.choices else None
+ response_content = None
+
+ # Enhanced Grok debugging - log both content fields
+ if provider == "grok" and message:
+ content_preview = message.content[:100] if message.content else "None"
+ reasoning_preview = getattr(message, 'reasoning_content', 'N/A')[:100] if hasattr(message, 'reasoning_content') and getattr(message, 'reasoning_content') else "None"
+ search_logger.debug(f"Grok response fields - content: '{content_preview}', reasoning_content: '{reasoning_preview}'")
+
+ if message:
+ # For Grok reasoning models, check content first, then reasoning_content
+ if provider == "grok":
+ # First try content (where final answer should be)
+ if message.content and message.content.strip():
+ response_content = message.content.strip()
+ search_logger.debug(f"Grok using content field: {len(response_content)} chars")
+ # Fallback to reasoning_content if content is empty
+ elif hasattr(message, 'reasoning_content') and message.reasoning_content:
+ response_content = message.reasoning_content.strip()
+ search_logger.debug(f"Grok fallback to reasoning_content: {len(response_content)} chars")
+ else:
+ search_logger.debug(f"Grok no content in either field: content='{message.content}', reasoning_content='{getattr(message, 'reasoning_content', 'N/A')}'")
+ elif message.content:
+ response_content = message.content
+ else:
+ search_logger.debug(f"No content in message: content={message.content}, reasoning_content={getattr(message, 'reasoning_content', 'N/A')}")
+
+ if response_content and response_content.strip():
+ # Success - break out of retry loop
+ if provider == "grok" and attempt > 0:
+ search_logger.info(f"Grok request succeeded on attempt {attempt + 1}")
+ break
+ elif (provider == "grok" or is_reasoning) and attempt < max_retries - 1:
+ # Empty response from Grok or reasoning models - track failure and retry
+ model_type = "Grok" if provider == "grok" else f"reasoning model ({model_choice})"
+ failure_reason = f"empty_response_attempt_{attempt + 1}"
+ failure_reasons.append(failure_reason)
+
+ search_logger.warning(f"{model_type} empty response on attempt {attempt + 1}, retrying...")
+
+ retry_delay *= 2 # Exponential backoff
+ continue
+ else:
+ # Final attempt failed or not Grok - handle below
+ break
+
+ except Exception as e:
+ if (provider == "grok" or is_reasoning) and attempt < max_retries - 1:
+ model_type = "Grok" if provider == "grok" else f"reasoning model ({model_choice})"
+ failure_reason = f"exception_attempt_{attempt + 1}_{type(e).__name__}"
+ failure_reasons.append(failure_reason)
+
+ search_logger.error(f"{model_type} request failed on attempt {attempt + 1}: {e}, retrying...")
+ retry_delay *= 2
+ continue
+ else:
+ # Re-raise on final attempt or non-Grok/reasoning providers
+ if failure_reasons:
+ # Add structured failure analysis for circuit breaker pattern
+ failure_analysis = {
+ "total_attempts": attempt + 1,
+ "failure_pattern": failure_reasons,
+ "final_error": str(e),
+ "model": model_choice,
+ "provider": provider
+ }
+ search_logger.error(f"Circuit breaker analysis: {failure_analysis}")
+ raise
+
+ # Log timing for Grok requests
+ if provider == "grok":
+ elapsed_time = time.time() - start_time
+ search_logger.debug(f"Grok total response time: {elapsed_time:.2f}s")
+
+ # Handle empty response with streamlined fallback logic
+ if not response_content:
+ # Structured error analysis for debugging
+ error_context = {
+ "model": model_choice,
+ "provider": provider,
+ "request_time": f"{elapsed_time:.2f}s" if 'elapsed_time' in locals() else "unknown",
+ "response_type": "empty_content",
+ "response_choices_count": len(response.choices) if response.choices else 0
+ }
+ search_logger.error(f"Empty response from LLM: {error_context}")
+ # Determine if fallback should be attempted based on error type and configuration
+ should_fallback = _should_attempt_fallback(provider, model_choice, is_reasoning, error_context)
+
+ if should_fallback:
+ # Single fallback attempt with tracking
+ fallback_result = await _attempt_single_fallback(
+ model_choice, provider, is_reasoning, request_params, error_context
+ )
+ if fallback_result:
+ response_content = fallback_result
+ search_logger.info(f"Fallback succeeded for {model_choice}")
+ else:
+ # Log fallback failure analysis for circuit breaker patterns
+ fallback_failure = {
+ "original_model": model_choice,
+ "original_provider": provider,
+ "fallback_attempted": True,
+ "fallback_succeeded": False,
+ "error_context": error_context
+ }
+ elif (provider == "grok" or is_reasoning) and attempt < max_retries - 1:
+ # Empty response from Grok or reasoning models - track failure and retry
+ model_type = "Grok" if provider == "grok" else f"reasoning model ({model_choice})"
+ failure_reason = f"empty_response_attempt_{attempt + 1}"
+ failure_reasons.append(failure_reason)
+
+ search_logger.warning(f"{model_type} empty response on attempt {attempt + 1}, retrying...")
+
+ else:
+ # No fallback attempted - fail fast with detailed context
+ search_logger.error(f"No fallback configured for {provider}/{model_choice} - failing fast")
+ raise ValueError(f"Empty response from {model_choice} (provider: {provider}). Check: API key validity, rate limits, model availability")
+
+ if not response_content:
+ # This should not happen after fallback logic, but safety check
+ raise ValueError("No valid response content after all attempts")
+
+ response_content = response_content.strip()
search_logger.debug(f"LLM API response: {repr(response_content[:200])}...")
result = json.loads(response_content)
@@ -627,8 +984,6 @@ async def generate_code_summaries_batch(
# Get max_workers from settings if not provided
if max_workers is None:
try:
- from ...services.credential_service import credential_service
-
if (
credential_service._cache_initialized
and "CODE_SUMMARY_MAX_WORKERS" in credential_service._cache
@@ -759,8 +1114,6 @@ async def add_code_examples_to_supabase(
# Check if contextual embeddings are enabled
try:
- from ..credential_service import credential_service
-
use_contextual_embeddings = credential_service._cache.get("USE_CONTEXTUAL_EMBEDDINGS")
if isinstance(use_contextual_embeddings, str):
use_contextual_embeddings = use_contextual_embeddings.lower() == "true"
From 9a7a34d660bb8cf9719157ce0ad3ffc14d01f977 Mon Sep 17 00:00:00 2001
From: Chillbruhhh
Date: Fri, 19 Sep 2025 00:11:04 -0500
Subject: [PATCH 03/28] fully working model providers, addressing securtiy and
code related concerns, throughly hardening our code
---
.../contextual_embedding_service.py | 32 ++--
.../server/services/llm_provider_service.py | 2 +-
.../services/storage/code_storage_service.py | 160 +++++++++++++-----
3 files changed, 135 insertions(+), 59 deletions(-)
diff --git a/python/src/server/services/embeddings/contextual_embedding_service.py b/python/src/server/services/embeddings/contextual_embedding_service.py
index 559b7f11b7..f9aec36617 100644
--- a/python/src/server/services/embeddings/contextual_embedding_service.py
+++ b/python/src/server/services/embeddings/contextual_embedding_service.py
@@ -10,7 +10,7 @@
import openai
from ...config.logfire_config import search_logger
-from ..llm_provider_service import get_llm_client
+from ..llm_provider_service import get_llm_client, prepare_chat_completion_params, requires_max_completion_tokens
from ..threading_service import get_threading_service
from ..credential_service import credential_service
@@ -64,18 +64,21 @@ async def generate_contextual_embedding(
# Get model from provider configuration
model = await _get_model_choice(provider)
- response = await client.chat.completions.create(
- model=model,
- messages=[
+ # Prepare parameters and convert max_tokens for GPT-5/reasoning models
+ params = {
+ "model": model,
+ "messages": [
{
"role": "system",
"content": "You are a helpful assistant that provides concise contextual information.",
},
{"role": "user", "content": prompt},
],
- temperature=0.3,
- max_tokens=200,
- )
+ "temperature": 0.3,
+ "max_tokens": 1200 if requires_max_completion_tokens(model) else 200, # Much more tokens for reasoning models (GPT-5 needs extra for reasoning process)
+ }
+ final_params = prepare_chat_completion_params(model, params)
+ response = await client.chat.completions.create(**final_params)
context = response.choices[0].message.content.strip()
contextual_text = f"{context}\n---\n{chunk}"
@@ -192,18 +195,21 @@ async def generate_contextual_embeddings_batch(
batch_prompt += "For each chunk, provide a short succinct context to situate it within the overall document for improving search retrieval. Format your response as:\\nCHUNK 1: [context]\\nCHUNK 2: [context]\\netc."
# Make single API call for ALL chunks
- response = await client.chat.completions.create(
- model=model_choice,
- messages=[
+ # Prepare parameters and convert max_tokens for GPT-5/reasoning models
+ batch_params = {
+ "model": model_choice,
+ "messages": [
{
"role": "system",
"content": "You are a helpful assistant that generates contextual information for document chunks.",
},
{"role": "user", "content": batch_prompt},
],
- temperature=0,
- max_tokens=100 * len(chunks), # Limit response size
- )
+ "temperature": 0,
+ "max_tokens": (600 if requires_max_completion_tokens(model_choice) else 100) * len(chunks), # Much more tokens for reasoning models (GPT-5 needs extra reasoning space)
+ }
+ final_batch_params = prepare_chat_completion_params(model_choice, batch_params)
+ response = await client.chat.completions.create(**final_batch_params)
# Parse response
response_text = response.choices[0].message.content
diff --git a/python/src/server/services/llm_provider_service.py b/python/src/server/services/llm_provider_service.py
index 1161939f41..88cfd2219a 100644
--- a/python/src/server/services/llm_provider_service.py
+++ b/python/src/server/services/llm_provider_service.py
@@ -500,6 +500,7 @@ async def get_llm_client(provider: str | None = None, use_embedding_provider: bo
base_url=base_url or "https://api.x.ai/v1",
)
logger.info("Grok client created successfully")
+
else:
raise ValueError(f"Unsupported LLM provider: {provider_name}")
@@ -774,7 +775,6 @@ def requires_max_completion_tokens(model_name: str) -> bool:
"o1", # o1, o1-mini, o1-preview, etc.
"o3", # o3, o3-mini, etc.
"gpt-5", # gpt-5, gpt-5-nano, gpt-5-mini, etc.
- "grok-3", # grok-3 series are reasoning models
]
# Check for reasoning models (including OpenRouter prefixed models)
diff --git a/python/src/server/services/storage/code_storage_service.py b/python/src/server/services/storage/code_storage_service.py
index a8518c151f..6b988c3dee 100644
--- a/python/src/server/services/storage/code_storage_service.py
+++ b/python/src/server/services/storage/code_storage_service.py
@@ -6,6 +6,7 @@
import asyncio
import json
+import time
import os
import re
from collections import defaultdict, deque
@@ -757,14 +758,18 @@ async def _generate_code_example_summary_async(
},
{"role": "user", "content": prompt},
],
- response_format={"type": "json_object"},
- max_tokens=500,
- temperature=0.3,
- )
+ "max_tokens": 2000 if (_is_reasoning_model(model_choice) or provider == "grok") else 500, # 2000 tokens for both reasoning models (GPT-5) and Grok for complex reasoning
+ "temperature": 0.3,
+ }
# Try to use response_format, but handle gracefully if not supported
- # Note: Grok reasoning models don't work well with response_format
- if provider in ["openai", "google", "anthropic"] or (provider == "openrouter" and model_choice.startswith("openai/")):
+ # Note: Grok and reasoning models (GPT-5, o1, o3) don't work well with response_format
+ supports_response_format = (
+ provider in ["openai", "google", "anthropic"] or
+ (provider == "openrouter" and model_choice.startswith("openai/"))
+ )
+ # Exclude reasoning models from using response_format
+ if supports_response_format and not _is_reasoning_model(model_choice):
request_params["response_format"] = {"type": "json_object"}
# Grok-specific parameter validation and filtering
@@ -783,48 +788,71 @@ async def _generate_code_example_summary_async(
if param not in supported_params:
search_logger.warning(f"Parameter '{param}' may not be supported by Grok reasoning models")
- start_time = time.time() # Initialize for all models
+ # Enhanced debugging for Grok provider
+ # Implement retry logic for Grok and reasoning models (GPT-5, o1, o3) empty responses
+ is_reasoning = _is_reasoning_model(model_choice)
+ start_time = time.time() # Initialize for all models
if provider == "grok" or is_reasoning:
model_type = "Grok" if provider == "grok" else f"reasoning model ({model_choice})"
search_logger.debug(f"{model_type} request params: {request_params}")
search_logger.debug(f"{model_type} prompt length: {len(prompt)} characters")
search_logger.debug(f"{model_type} prompt preview: {prompt[:200]}...")
- # Simplified retry logic - reduced from 3 to 2 retries to surface issues faster
- max_retries = 2 if (provider == "grok" or is_reasoning) else 1
+ max_retries = 3 if (provider == "grok" or is_reasoning) else 1
retry_delay = 1.0 # Start with 1 second delay
failure_reasons = [] # Track failure reasons for circuit breaker analysis
for attempt in range(max_retries):
try:
- if provider == "grok" and attempt > 0:
- search_logger.info(f"Grok retry attempt {attempt + 1}/{max_retries} after {retry_delay:.1f}s delay")
+ if (provider == "grok" or is_reasoning) and attempt > 0:
+ model_type = "Grok" if provider == "grok" else f"reasoning model ({model_choice})"
+ search_logger.info(f"{model_type} retry attempt {attempt + 1}/{max_retries} after {retry_delay:.1f}s delay")
await asyncio.sleep(retry_delay)
+ elif is_reasoning and attempt == 0:
+ # Small delay for reasoning models on first attempt to help with cold start
+ search_logger.debug(f"reasoning model ({model_choice}) first attempt - adding 0.5s delay for cold start")
+ await asyncio.sleep(0.5)
- response = await client.chat.completions.create(**request_params)
+ # Convert max_tokens to max_completion_tokens for GPT-5/reasoning models
+ final_params = prepare_chat_completion_params(model_choice, request_params)
+ response = await client.chat.completions.create(**final_params)
# Check for empty response - handle Grok reasoning models
message = response.choices[0].message if response.choices else None
response_content = None
- # Enhanced Grok debugging - log both content fields
- if provider == "grok" and message:
+ # Enhanced debugging for Grok and reasoning models - log both content fields
+ if (provider == "grok" or is_reasoning) and message:
content_preview = message.content[:100] if message.content else "None"
- reasoning_preview = getattr(message, 'reasoning_content', 'N/A')[:100] if hasattr(message, 'reasoning_content') and getattr(message, 'reasoning_content') else "None"
- search_logger.debug(f"Grok response fields - content: '{content_preview}', reasoning_content: '{reasoning_preview}'")
+ reasoning_preview = getattr(message, 'reasoning_content', 'N/A')[:100] if hasattr(message, 'reasoning_content') and message.reasoning_content else "None"
+ model_type = "Grok" if provider == "grok" else f"reasoning model ({model_choice})"
+
+ # Additional debugging for first attempt failures
+ finish_reason = getattr(response.choices[0], 'finish_reason', 'unknown') if response.choices else 'no_choices'
+ usage_info = getattr(response, 'usage', None)
+ if usage_info:
+ completion_tokens = getattr(usage_info, 'completion_tokens', 0)
+ reasoning_tokens = getattr(getattr(usage_info, 'completion_tokens_details', None), 'reasoning_tokens', 0) if hasattr(usage_info, 'completion_tokens_details') else 0
+ search_logger.debug(f"{model_type} attempt {attempt + 1} - finish_reason: {finish_reason}, completion_tokens: {completion_tokens}, reasoning_tokens: {reasoning_tokens}")
+ else:
+ search_logger.debug(f"{model_type} attempt {attempt + 1} - finish_reason: {finish_reason}, no usage info")
+
+ search_logger.debug(f"{model_type} response fields - content: '{content_preview}', reasoning_content: '{reasoning_preview}'")
if message:
- # For Grok reasoning models, check content first, then reasoning_content
- if provider == "grok":
+ # For Grok and reasoning models, check content first, then reasoning_content
+ if provider == "grok" or is_reasoning:
# First try content (where final answer should be)
if message.content and message.content.strip():
response_content = message.content.strip()
- search_logger.debug(f"Grok using content field: {len(response_content)} chars")
+ model_type = "Grok" if provider == "grok" else f"reasoning model ({model_choice})"
+ search_logger.debug(f"{model_type} using content field: {len(response_content)} chars")
# Fallback to reasoning_content if content is empty
elif hasattr(message, 'reasoning_content') and message.reasoning_content:
response_content = message.reasoning_content.strip()
- search_logger.debug(f"Grok fallback to reasoning_content: {len(response_content)} chars")
+ model_type = "Grok" if provider == "grok" else f"reasoning model ({model_choice})"
+ search_logger.debug(f"{model_type} fallback to reasoning_content: {len(response_content)} chars")
else:
search_logger.debug(f"Grok no content in either field: content='{message.content}', reasoning_content='{getattr(message, 'reasoning_content', 'N/A')}'")
elif message.content:
@@ -834,50 +862,35 @@ async def _generate_code_example_summary_async(
if response_content and response_content.strip():
# Success - break out of retry loop
- if provider == "grok" and attempt > 0:
- search_logger.info(f"Grok request succeeded on attempt {attempt + 1}")
+ if (provider == "grok" or is_reasoning) and attempt > 0:
+ model_type = "Grok" if provider == "grok" else f"reasoning model ({model_choice})"
+ search_logger.info(f"{model_type} request succeeded on attempt {attempt + 1}")
break
elif (provider == "grok" or is_reasoning) and attempt < max_retries - 1:
- # Empty response from Grok or reasoning models - track failure and retry
+ # Empty response from Grok or reasoning models - retry with exponential backoff
model_type = "Grok" if provider == "grok" else f"reasoning model ({model_choice})"
- failure_reason = f"empty_response_attempt_{attempt + 1}"
- failure_reasons.append(failure_reason)
-
search_logger.warning(f"{model_type} empty response on attempt {attempt + 1}, retrying...")
-
retry_delay *= 2 # Exponential backoff
continue
else:
- # Final attempt failed or not Grok - handle below
+ # Final attempt failed or not Grok/reasoning model - handle below
break
except Exception as e:
if (provider == "grok" or is_reasoning) and attempt < max_retries - 1:
model_type = "Grok" if provider == "grok" else f"reasoning model ({model_choice})"
- failure_reason = f"exception_attempt_{attempt + 1}_{type(e).__name__}"
- failure_reasons.append(failure_reason)
-
search_logger.error(f"{model_type} request failed on attempt {attempt + 1}: {e}, retrying...")
retry_delay *= 2
continue
else:
# Re-raise on final attempt or non-Grok/reasoning providers
- if failure_reasons:
- # Add structured failure analysis for circuit breaker pattern
- failure_analysis = {
- "total_attempts": attempt + 1,
- "failure_pattern": failure_reasons,
- "final_error": str(e),
- "model": model_choice,
- "provider": provider
- }
- search_logger.error(f"Circuit breaker analysis: {failure_analysis}")
raise
- # Log timing for Grok requests
- if provider == "grok":
+ # Log timing for Grok and reasoning model requests
+ if provider == "grok" or is_reasoning:
elapsed_time = time.time() - start_time
- search_logger.debug(f"Grok total response time: {elapsed_time:.2f}s")
+ model_type = "Grok" if provider == "grok" else f"reasoning model ({model_choice})"
+ search_logger.debug(f"{model_type} total response time: {elapsed_time:.2f}s")
# Handle empty response with streamlined fallback logic
if not response_content:
@@ -916,8 +929,65 @@ async def _generate_code_example_summary_async(
failure_reason = f"empty_response_attempt_{attempt + 1}"
failure_reasons.append(failure_reason)
- search_logger.warning(f"{model_type} empty response on attempt {attempt + 1}, retrying...")
+ async with get_llm_client(provider="openai") as fallback_client:
+ search_logger.info("Using OpenAI fallback for Grok failure")
+ # Convert max_tokens to max_completion_tokens for GPT-5/reasoning models
+ final_fallback_params = prepare_chat_completion_params(fallback_params["model"], fallback_params)
+ fallback_response = await fallback_client.chat.completions.create(**final_fallback_params)
+ fallback_content = fallback_response.choices[0].message.content
+
+ if fallback_content and fallback_content.strip():
+ search_logger.info("OpenAI fallback succeeded")
+ response_content = fallback_content.strip()
+ else:
+ search_logger.error("OpenAI fallback also returned empty response")
+ raise ValueError("Both Grok and OpenAI fallback failed")
+
+ except Exception as fallback_error:
+ search_logger.error(f"OpenAI fallback failed: {fallback_error}")
+ raise ValueError(f"Grok failed and fallback to OpenAI also failed: {fallback_error}") from fallback_error
+ elif is_reasoning:
+ # Implement fallback for reasoning model (GPT-5, o1, o3) failures
+ search_logger.error("Reasoning model empty response debugging:")
+ search_logger.error(f" - Model: {model_choice}")
+ search_logger.error(f" - Provider: {provider}")
+ search_logger.error(f" - Request took: {elapsed_time:.2f}s")
+ search_logger.error(f" - Full response: {response}")
+ search_logger.error(f" - Response choices length: {len(response.choices) if response.choices else 0}")
+ if response.choices:
+ search_logger.error(f" - First choice: {response.choices[0]}")
+ search_logger.error(f" - Message content: '{response.choices[0].message.content}'")
+ search_logger.error(f" - Message role: {response.choices[0].message.role}")
+ search_logger.error("Check: 1) API key validity, 2) rate limits, 3) model availability")
+
+ # Implement fallback to non-reasoning model for reasoning model failures
+ search_logger.warning(f"Attempting fallback to gpt-4o-mini due to {model_choice} failure...")
+ try:
+ # Use a reliable non-reasoning model as fallback
+ fallback_params = {
+ "model": "gpt-4o-mini",
+ "messages": request_params["messages"],
+ "max_tokens": request_params.get("max_tokens", 500),
+ "temperature": request_params.get("temperature", 0.3),
+ "response_format": {"type": "json_object"}
+ }
+
+ async with get_llm_client(provider="openai") as fallback_client:
+ search_logger.info(f"Using gpt-4o-mini fallback for {model_choice} failure")
+ # No parameter conversion needed for non-reasoning model
+ fallback_response = await fallback_client.chat.completions.create(**fallback_params)
+ fallback_content = fallback_response.choices[0].message.content
+
+ if fallback_content and fallback_content.strip():
+ search_logger.info(f"gpt-4o-mini fallback succeeded for {model_choice}")
+ response_content = fallback_content.strip()
+ else:
+ search_logger.error("gpt-4o-mini fallback also returned empty response")
+ raise ValueError(f"Both {model_choice} and gpt-4o-mini fallback failed")
+ except Exception as fallback_error:
+ search_logger.error(f"gpt-4o-mini fallback failed: {fallback_error}")
+ raise ValueError(f"{model_choice} failed and fallback to gpt-4o-mini also failed: {fallback_error}") from fallback_error
else:
# No fallback attempted - fail fast with detailed context
search_logger.error(f"No fallback configured for {provider}/{model_choice} - failing fast")
From e9a78e8a6e01a7342060f1cb4feba83ee89525a2 Mon Sep 17 00:00:00 2001
From: Chillbruhhh
Date: Sat, 20 Sep 2025 06:29:51 -0500
Subject: [PATCH 04/28] added multiprovider support, embeddings model support,
cleaned the pr, need to fix health check, asnyico tasks errors, and
contextual embeddings error
---
.../src/components/settings/RAGSettings.tsx | 371 +++++++++---------
python/src/server/api_routes/knowledge_api.py | 76 +++-
.../crawling/code_extraction_service.py | 11 +-
.../services/crawling/crawling_service.py | 12 +
.../crawling/document_storage_operations.py | 4 +-
.../src/server/services/credential_service.py | 67 +++-
.../services/embeddings/embedding_service.py | 23 +-
.../server/services/llm_provider_service.py | 195 ++++++++-
.../services/provider_discovery_service.py | 54 ++-
.../services/storage/code_storage_service.py | 261 ++----------
10 files changed, 639 insertions(+), 435 deletions(-)
diff --git a/archon-ui-main/src/components/settings/RAGSettings.tsx b/archon-ui-main/src/components/settings/RAGSettings.tsx
index d54cd51062..e472d701dc 100644
--- a/archon-ui-main/src/components/settings/RAGSettings.tsx
+++ b/archon-ui-main/src/components/settings/RAGSettings.tsx
@@ -9,6 +9,82 @@ import { credentialsService } from '../../services/credentialsService';
import OllamaModelDiscoveryModal from './OllamaModelDiscoveryModal';
import OllamaModelSelectionModal from './OllamaModelSelectionModal';
+type ProviderKey = 'openai' | 'google' | 'ollama' | 'anthropic' | 'grok' | 'openrouter';
+
+interface ProviderModels {
+ chatModel: string;
+ embeddingModel: string;
+}
+
+type ProviderModelMap = Record;
+
+// Provider model persistence helpers
+const PROVIDER_MODELS_KEY = 'archon_provider_models';
+
+const getDefaultModels = (provider: ProviderKey): ProviderModels => {
+ const chatDefaults: Record = {
+ openai: 'gpt-4o-mini',
+ anthropic: 'claude-3-5-sonnet-20241022',
+ google: 'gemini-1.5-flash',
+ grok: 'grok-3-mini', // Updated to use grok-3-mini as default
+ openrouter: 'openai/gpt-4o-mini',
+ ollama: 'llama3:8b'
+ };
+
+ const embeddingDefaults: Record = {
+ openai: 'text-embedding-3-small',
+ anthropic: 'text-embedding-3-small', // Fallback to OpenAI
+ google: 'text-embedding-004',
+ grok: 'text-embedding-3-small', // Fallback to OpenAI
+ openrouter: 'text-embedding-3-small',
+ ollama: 'nomic-embed-text'
+ };
+
+ return {
+ chatModel: chatDefaults[provider],
+ embeddingModel: embeddingDefaults[provider]
+ };
+};
+
+const saveProviderModels = (providerModels: ProviderModelMap): void => {
+ try {
+ localStorage.setItem(PROVIDER_MODELS_KEY, JSON.stringify(providerModels));
+ } catch (error) {
+ console.error('Failed to save provider models:', error);
+ }
+};
+
+const loadProviderModels = (): ProviderModelMap => {
+ try {
+ const saved = localStorage.getItem(PROVIDER_MODELS_KEY);
+ if (saved) {
+ return JSON.parse(saved);
+ }
+ } catch (error) {
+ console.error('Failed to load provider models:', error);
+ }
+
+ // Return defaults for all providers if nothing saved
+ const providers: ProviderKey[] = ['openai', 'google', 'openrouter', 'ollama', 'anthropic', 'grok'];
+ const defaultModels: ProviderModelMap = {} as ProviderModelMap;
+
+ providers.forEach(provider => {
+ defaultModels[provider] = getDefaultModels(provider);
+ });
+
+ return defaultModels;
+};
+
+// Static color styles mapping (prevents Tailwind JIT purging)
+const colorStyles: Record = {
+ openai: 'border-green-500 bg-green-500/10',
+ google: 'border-blue-500 bg-blue-500/10',
+ openrouter: 'border-cyan-500 bg-cyan-500/10',
+ ollama: 'border-purple-500 bg-purple-500/10',
+ anthropic: 'border-orange-500 bg-orange-500/10',
+ grok: 'border-yellow-500 bg-yellow-500/10',
+};
+
interface RAGSettingsProps {
ragSettings: {
MODEL_CHOICE: string;
@@ -57,7 +133,10 @@ export const RAGSettings = ({
// Model selection modals state
const [showLLMModelSelectionModal, setShowLLMModelSelectionModal] = useState(false);
const [showEmbeddingModelSelectionModal, setShowEmbeddingModelSelectionModal] = useState(false);
-
+
+ // Provider-specific model persistence state
+ const [providerModels, setProviderModels] = useState(() => loadProviderModels());
+
// Instance configurations
const [llmInstanceConfig, setLLMInstanceConfig] = useState({
name: '',
@@ -113,6 +192,25 @@ export const RAGSettings = ({
}
}, [ragSettings.OLLAMA_EMBEDDING_URL, ragSettings.OLLAMA_EMBEDDING_INSTANCE_NAME]);
+ // Provider model persistence effects
+ useEffect(() => {
+ // Update provider models when current models change
+ const currentProvider = ragSettings.LLM_PROVIDER as ProviderKey;
+ if (currentProvider && ragSettings.MODEL_CHOICE && ragSettings.EMBEDDING_MODEL) {
+ setProviderModels(prev => {
+ const updated = {
+ ...prev,
+ [currentProvider]: {
+ chatModel: ragSettings.MODEL_CHOICE,
+ embeddingModel: ragSettings.EMBEDDING_MODEL
+ }
+ };
+ saveProviderModels(updated);
+ return updated;
+ });
+ }
+ }, [ragSettings.MODEL_CHOICE, ragSettings.EMBEDDING_MODEL, ragSettings.LLM_PROVIDER]);
+
// Load API credentials for status checking
useEffect(() => {
const loadApiCredentials = async () => {
@@ -197,58 +295,27 @@ export const RAGSettings = ({
}>({});
// Test connection to external providers
- const testProviderConnection = async (provider: string, apiKey: string): Promise => {
+ const testProviderConnection = async (provider: string): Promise => {
setProviderConnectionStatus(prev => ({
...prev,
[provider]: { ...prev[provider], checking: true }
}));
try {
- switch (provider) {
- case 'openai':
- // Test OpenAI connection with a simple completion request
- const openaiResponse = await fetch('https://api.openai.com/v1/models', {
- method: 'GET',
- headers: {
- 'Authorization': `Bearer ${apiKey}`,
- 'Content-Type': 'application/json'
- }
- });
-
- if (openaiResponse.ok) {
- setProviderConnectionStatus(prev => ({
- ...prev,
- openai: { connected: true, checking: false, lastChecked: new Date() }
- }));
- return true;
- } else {
- throw new Error(`OpenAI API returned ${openaiResponse.status}`);
- }
+ // Use server-side API endpoint for secure connectivity testing
+ const response = await fetch(`/api/providers/${provider}/status`);
+ const result = await response.json();
- case 'google':
- // Test Google Gemini connection
- const googleResponse = await fetch(`https://generativelanguage.googleapis.com/v1/models?key=${apiKey}`, {
- method: 'GET',
- headers: {
- 'Content-Type': 'application/json'
- }
- });
-
- if (googleResponse.ok) {
- setProviderConnectionStatus(prev => ({
- ...prev,
- google: { connected: true, checking: false, lastChecked: new Date() }
- }));
- return true;
- } else {
- throw new Error(`Google API returned ${googleResponse.status}`);
- }
+ const isConnected = result.ok && result.reason === 'connected';
- default:
- return false;
- }
+ setProviderConnectionStatus(prev => ({
+ ...prev,
+ [provider]: { connected: isConnected, checking: false, lastChecked: new Date() }
+ }));
+
+ return isConnected;
} catch (error) {
- console.error(`Failed to test ${provider} connection:`, error);
+ console.error(`Error testing ${provider} connection:`, error);
setProviderConnectionStatus(prev => ({
...prev,
[provider]: { connected: false, checking: false, lastChecked: new Date() }
@@ -260,37 +327,27 @@ export const RAGSettings = ({
// Test provider connections when API credentials change
useEffect(() => {
const testConnections = async () => {
- const providers = ['openai', 'google'];
-
+ // Test all supported providers
+ const providers = ['openai', 'google', 'anthropic', 'openrouter', 'grok'];
+
for (const provider of providers) {
- const keyName = provider === 'openai' ? 'OPENAI_API_KEY' : 'GOOGLE_API_KEY';
- const apiKey = Object.keys(apiCredentials).find(key => key.toUpperCase() === keyName);
- const keyValue = apiKey ? apiCredentials[apiKey] : undefined;
-
- if (keyValue && keyValue.trim().length > 0) {
- // Don't test if we've already checked recently (within last 30 seconds)
- const lastChecked = providerConnectionStatus[provider]?.lastChecked;
- const now = new Date();
- const timeSinceLastCheck = lastChecked ? now.getTime() - lastChecked.getTime() : Infinity;
-
- if (timeSinceLastCheck > 30000) { // 30 seconds
- console.log(`π Testing ${provider} connection...`);
- await testProviderConnection(provider, keyValue);
- }
- } else {
- // No API key, mark as disconnected
- setProviderConnectionStatus(prev => ({
- ...prev,
- [provider]: { connected: false, checking: false, lastChecked: new Date() }
- }));
+ // Don't test if we've already checked recently (within last 30 seconds)
+ const lastChecked = providerConnectionStatus[provider]?.lastChecked;
+ const now = new Date();
+ const timeSinceLastCheck = lastChecked ? now.getTime() - lastChecked.getTime() : Infinity;
+
+ if (timeSinceLastCheck > 30000) { // 30 seconds
+ console.log(`π Testing ${provider} connection...`);
+ await testProviderConnection(provider);
}
}
};
- // Only test if we have credentials loaded
- if (Object.keys(apiCredentials).length > 0) {
- testConnections();
- }
+ // Test connections periodically (every 60 seconds)
+ testConnections();
+ const interval = setInterval(testConnections, 60000);
+
+ return () => clearInterval(interval);
}, [apiCredentials]); // Test when credentials change
// Ref to track if initial test has been run (will be used after function definitions)
@@ -662,20 +719,23 @@ export const RAGSettings = ({
if (llmStatus.online || embeddingStatus.online) return 'partial';
return 'missing';
case 'anthropic':
- // Check if Anthropic API key is configured (case insensitive)
- const anthropicKey = Object.keys(apiCredentials).find(key => key.toUpperCase() === 'ANTHROPIC_API_KEY');
- const hasAnthropicKey = anthropicKey && apiCredentials[anthropicKey] && apiCredentials[anthropicKey].trim().length > 0;
- return hasAnthropicKey ? 'configured' : 'missing';
+ // Use server-side connection status
+ const anthropicConnected = providerConnectionStatus['anthropic']?.connected || false;
+ const anthropicChecking = providerConnectionStatus['anthropic']?.checking || false;
+ if (anthropicChecking) return 'partial';
+ return anthropicConnected ? 'configured' : 'missing';
case 'grok':
- // Check if Grok API key is configured (case insensitive)
- const grokKey = Object.keys(apiCredentials).find(key => key.toUpperCase() === 'GROK_API_KEY');
- const hasGrokKey = grokKey && apiCredentials[grokKey] && apiCredentials[grokKey].trim().length > 0;
- return hasGrokKey ? 'configured' : 'missing';
+ // Use server-side connection status
+ const grokConnected = providerConnectionStatus['grok']?.connected || false;
+ const grokChecking = providerConnectionStatus['grok']?.checking || false;
+ if (grokChecking) return 'partial';
+ return grokConnected ? 'configured' : 'missing';
case 'openrouter':
- // Check if OpenRouter API key is configured (case insensitive)
- const openRouterKey = Object.keys(apiCredentials).find(key => key.toUpperCase() === 'OPENROUTER_API_KEY');
- const hasOpenRouterKey = openRouterKey && apiCredentials[openRouterKey] && apiCredentials[openRouterKey].trim().length > 0;
- return hasOpenRouterKey ? 'configured' : 'missing';
+ // Use server-side connection status
+ const openRouterConnected = providerConnectionStatus['openrouter']?.connected || false;
+ const openRouterChecking = providerConnectionStatus['openrouter']?.checking || false;
+ if (openRouterChecking) return 'partial';
+ return openRouterConnected ? 'configured' : 'missing';
default:
return 'missing';
}
@@ -750,55 +810,32 @@ export const RAGSettings = ({
{[
{ key: 'openai', name: 'OpenAI', logo: '/img/OpenAI.png', color: 'green' },
{ key: 'google', name: 'Google', logo: '/img/google-logo.svg', color: 'blue' },
+ { key: 'openrouter', name: 'OpenRouter', logo: '/img/OpenRouter.png', color: 'cyan' },
{ key: 'ollama', name: 'Ollama', logo: '/img/Ollama.png', color: 'purple' },
{ key: 'anthropic', name: 'Anthropic', logo: '/img/claude-logo.svg', color: 'orange' },
- { key: 'grok', name: 'Grok', logo: '/img/Grok.png', color: 'yellow' },
- { key: 'openrouter', name: 'OpenRouter', logo: '/img/OpenRouter.png', color: 'cyan' }
+ { key: 'grok', name: 'Grok', logo: '/img/Grok.png', color: 'yellow' }
].map(provider => (
{
+ // Get saved models for this provider, or use defaults
+ const providerKey = provider.key as ProviderKey;
+ const savedModels = providerModels[providerKey] || getDefaultModels(providerKey);
+
const updatedSettings = {
...ragSettings,
- LLM_PROVIDER: provider.key
- };
-
- // Set models to provider-appropriate defaults when switching providers
- // This ensures both LLM and embedding models switch when provider changes
- const getDefaultChatModel = (provider: string): string => {
- switch (provider) {
- case 'openai': return 'gpt-4o-mini';
- case 'anthropic': return 'claude-3-5-sonnet-20241022';
- case 'google': return 'gemini-1.5-flash';
- case 'grok': return 'grok-2-latest';
- case 'ollama': return '';
- case 'openrouter': return 'anthropic/claude-3.5-sonnet';
- default: return 'gpt-4o-mini';
- }
+ LLM_PROVIDER: providerKey,
+ MODEL_CHOICE: savedModels.chatModel,
+ EMBEDDING_MODEL: savedModels.embeddingModel
};
-
- const getDefaultEmbeddingModel = (provider: string): string => {
- switch (provider) {
- case 'openai': return 'text-embedding-3-small';
- case 'google': return 'text-embedding-004';
- case 'ollama': return '';
- case 'openrouter': return 'text-embedding-3-small';
- case 'anthropic':
- case 'grok':
- default: return 'text-embedding-3-small';
- }
- };
-
- updatedSettings.MODEL_CHOICE = getDefaultChatModel(provider.key);
- updatedSettings.EMBEDDING_MODEL = getDefaultEmbeddingModel(provider.key);
-
+
setRagSettings(updatedSettings);
}}
className={`
relative p-3 rounded-lg border-2 transition-all duration-200 text-center
${ragSettings.LLM_PROVIDER === provider.key
- ? `border-${provider.color}-500 bg-${provider.color}-500/10 shadow-[0_0_15px_rgba(34,197,94,0.3)]`
+ ? `${colorStyles[provider.key as ProviderKey]} shadow-[0_0_15px_rgba(34,197,94,0.3)]`
: 'border-gray-300 dark:border-gray-600 hover:border-gray-400 dark:hover:border-gray-500'
}
hover:scale-105 active:scale-95
@@ -813,8 +850,8 @@ export const RAGSettings = ({
: ''
}`}
/>
-
{provider.name}
@@ -1846,94 +1883,56 @@ export const RAGSettings = ({
function getDisplayedChatModel(ragSettings: any): string {
const provider = ragSettings.LLM_PROVIDER || 'openai';
const modelChoice = ragSettings.MODEL_CHOICE;
-
- // Check if the stored model is appropriate for the current provider
- const isModelAppropriate = (model: string, provider: string): boolean => {
- if (!model) return false;
-
- switch (provider) {
- case 'openai':
- return model.startsWith('gpt-') || model.startsWith('o1-') || model.includes('text-davinci') || model.includes('text-embedding');
- case 'anthropic':
- return model.startsWith('claude-');
- case 'google':
- return model.startsWith('gemini-') || model.startsWith('text-embedding-');
- case 'grok':
- return model.startsWith('grok-');
- case 'ollama':
- return !model.startsWith('gpt-') && !model.startsWith('claude-') && !model.startsWith('gemini-') && !model.startsWith('grok-');
- case 'openrouter':
- return model.includes('/') || model.startsWith('anthropic/') || model.startsWith('openai/');
- default:
- return false;
- }
- };
-
- // Use stored model if it's appropriate for the provider, otherwise use default
- const useStoredModel = modelChoice && isModelAppropriate(modelChoice, provider);
-
+
+ // Always prioritize user input to allow editing
+ if (modelChoice !== undefined && modelChoice !== null) {
+ return modelChoice;
+ }
+
+ // Only use defaults when there's no stored value
switch (provider) {
case 'openai':
- return useStoredModel ? modelChoice : 'gpt-4o-mini';
+ return 'gpt-4o-mini';
case 'anthropic':
- return useStoredModel ? modelChoice : 'claude-3-5-sonnet-20241022';
+ return 'claude-3-5-sonnet-20241022';
case 'google':
- return useStoredModel ? modelChoice : 'gemini-1.5-flash';
+ return 'gemini-1.5-flash';
case 'grok':
- return useStoredModel ? modelChoice : 'grok-2-latest';
+ return 'grok-3-mini';
case 'ollama':
- return useStoredModel ? modelChoice : '';
+ return '';
case 'openrouter':
- return useStoredModel ? modelChoice : 'anthropic/claude-3.5-sonnet';
+ return 'anthropic/claude-3.5-sonnet';
default:
- return useStoredModel ? modelChoice : 'gpt-4o-mini';
+ return 'gpt-4o-mini';
}
}
function getDisplayedEmbeddingModel(ragSettings: any): string {
const provider = ragSettings.LLM_PROVIDER || 'openai';
const embeddingModel = ragSettings.EMBEDDING_MODEL;
-
- // Check if the stored embedding model is appropriate for the current provider
- const isEmbeddingModelAppropriate = (model: string, provider: string): boolean => {
- if (!model) return false;
-
- switch (provider) {
- case 'openai':
- return model.startsWith('text-embedding-') || model.includes('ada-');
- case 'anthropic':
- return false; // Claude doesn't provide embedding models
- case 'google':
- return model.startsWith('text-embedding-') || model.startsWith('textembedding-') || model.includes('embedding');
- case 'grok':
- return false; // Grok doesn't provide embedding models
- case 'ollama':
- return !model.startsWith('text-embedding-') || model.includes('embed') || model.includes('arctic');
- case 'openrouter':
- return model.startsWith('text-embedding-') || model.includes('/');
- default:
- return false;
- }
- };
-
- // Use stored model if it's appropriate for the provider, otherwise use default
- const useStoredModel = embeddingModel && isEmbeddingModelAppropriate(embeddingModel, provider);
-
+
+ // Always prioritize user input to allow editing
+ if (embeddingModel !== undefined && embeddingModel !== null && embeddingModel !== '') {
+ return embeddingModel;
+ }
+
+ // Provide appropriate defaults based on LLM provider
switch (provider) {
case 'openai':
- return useStoredModel ? embeddingModel : 'text-embedding-3-small';
- case 'anthropic':
- return 'Not available - Claude does not provide embedding models';
+ return 'text-embedding-3-small';
case 'google':
- return useStoredModel ? embeddingModel : 'text-embedding-004';
- case 'grok':
- return 'Not available - Grok does not provide embedding models';
+ return 'text-embedding-004';
case 'ollama':
- return useStoredModel ? embeddingModel : '';
+ return '';
case 'openrouter':
- return useStoredModel ? embeddingModel : 'text-embedding-3-small';
+ return 'text-embedding-3-small'; // Default to OpenAI embedding for OpenRouter
+ case 'anthropic':
+ return 'text-embedding-3-small'; // Use OpenAI embeddings with Claude
+ case 'grok':
+ return 'text-embedding-3-small'; // Use OpenAI embeddings with Grok
default:
- return useStoredModel ? embeddingModel : 'text-embedding-3-small';
+ return 'text-embedding-3-small';
}
}
diff --git a/python/src/server/api_routes/knowledge_api.py b/python/src/server/api_routes/knowledge_api.py
index 3cd0f3dbf3..1764dbab4e 100644
--- a/python/src/server/api_routes/knowledge_api.py
+++ b/python/src/server/api_routes/knowledge_api.py
@@ -83,21 +83,67 @@ async def _validate_provider_api_key(provider: str = None) -> None:
# Basic sanitization for logging
safe_provider = provider[:20] # Limit length
logger.info(f"π Testing {safe_provider.title()} API key with minimal embedding request...")
- # Test API key with minimal embedding request - this will fail if key is invalid
- from ..services.embeddings.embedding_service import create_embedding
- test_result = await create_embedding(text="test")
-
- if not test_result:
- logger.error(f"β {provider.title()} API key validation failed - no embedding returned")
- raise HTTPException(
- status_code=401,
- detail={
- "error": f"Invalid {provider.title()} API key",
- "message": f"Please verify your {provider.title()} API key in Settings.",
- "error_type": "authentication_failed",
- "provider": provider
- }
- )
+
+ try:
+ # Test API key with minimal embedding request - this will fail if key is invalid
+ from ..services.embeddings.embedding_service import create_embedding
+ test_result = await create_embedding(text="test")
+
+ if not test_result:
+ logger.error(f"β {provider.title()} API key validation failed - no embedding returned")
+ raise HTTPException(
+ status_code=401,
+ detail={
+ "error": f"Invalid {provider.title()} API key",
+ "message": f"Please verify your {provider.title()} API key in Settings.",
+ "error_type": "authentication_failed",
+ "provider": provider
+ }
+ )
+ except Exception as e:
+ # If embedding fails due to model incompatibility, try with provider-compatible model
+ error_str = str(e).lower()
+ if "does not exist" in error_str or "not found" in error_str:
+ logger.warning(f"π Embedding model incompatible with {provider.title()}, retrying with provider-compatible model...")
+ try:
+ # Force provider-compatible embedding model for validation
+ from ..services.embeddings.embedding_service import create_embedding
+ from ..services.llm_provider_service import get_llm_client
+
+ # Use provider-specific compatible embedding model for validation
+ compatible_model = "text-embedding-3-small" # OpenAI-compatible model
+
+ async with get_llm_client(provider=provider, use_embedding_provider=True) as client:
+ response = await client.embeddings.create(
+ model=compatible_model,
+ input="test"
+ )
+ if not response.data[0].embedding:
+ raise Exception("No embedding returned")
+ logger.info(f"β
{provider.title()} API key validation successful with compatible model")
+ except Exception as retry_error:
+ logger.error(f"β {provider.title()} API key validation failed even with compatible model: {retry_error}")
+ raise HTTPException(
+ status_code=401,
+ detail={
+ "error": f"Invalid {provider.title()} API key",
+ "message": f"Please verify your {provider.title()} API key in Settings. Original error: {str(e)[:100]}",
+ "error_type": "authentication_failed",
+ "provider": provider
+ }
+ )
+ else:
+ # Other errors (network, auth, etc.) should fail immediately
+ logger.error(f"β {provider.title()} API key validation failed: {e}")
+ raise HTTPException(
+ status_code=401,
+ detail={
+ "error": f"Invalid {provider.title()} API key",
+ "message": f"Please verify your {provider.title()} API key in Settings. Error: {str(e)[:100]}",
+ "error_type": "authentication_failed",
+ "provider": provider
+ }
+ )
logger.info(f"β
{provider.title()} API key validation successful")
diff --git a/python/src/server/services/crawling/code_extraction_service.py b/python/src/server/services/crawling/code_extraction_service.py
index f52b7e2835..1a540f5732 100644
--- a/python/src/server/services/crawling/code_extraction_service.py
+++ b/python/src/server/services/crawling/code_extraction_service.py
@@ -139,6 +139,7 @@ async def extract_and_store_code_examples(
source_id: str,
progress_callback: Callable | None = None,
cancellation_check: Callable[[], None] | None = None,
+ provider: str | None = None,
) -> int:
"""
Extract code examples from crawled documents and store them.
@@ -204,7 +205,7 @@ async def summary_progress(data: dict):
# Generate summaries for code blocks
summary_results = await self._generate_code_summaries(
- all_code_blocks, summary_callback, cancellation_check
+ all_code_blocks, summary_callback, cancellation_check, provider
)
# Prepare code examples for storage
@@ -223,7 +224,7 @@ async def storage_progress(data: dict):
# Store code examples in database
return await self._store_code_examples(
- storage_data, url_to_full_document, storage_callback
+ storage_data, url_to_full_document, storage_callback, provider
)
async def _extract_code_blocks_from_documents(
@@ -1523,6 +1524,7 @@ async def _generate_code_summaries(
all_code_blocks: list[dict[str, Any]],
progress_callback: Callable | None = None,
cancellation_check: Callable[[], None] | None = None,
+ provider: str | None = None,
) -> list[dict[str, str]]:
"""
Generate summaries for all code blocks.
@@ -1587,7 +1589,7 @@ async def wrapped_callback(data: dict):
try:
results = await generate_code_summaries_batch(
- code_blocks_for_summaries, max_workers, progress_callback=summary_progress_callback
+ code_blocks_for_summaries, max_workers, progress_callback=summary_progress_callback, provider=provider
)
# Ensure all results are valid dicts
@@ -1667,6 +1669,7 @@ async def _store_code_examples(
storage_data: dict[str, list[Any]],
url_to_full_document: dict[str, str],
progress_callback: Callable | None = None,
+ provider: str | None = None,
) -> int:
"""
Store code examples in the database.
@@ -1709,7 +1712,7 @@ async def storage_callback(data: dict):
batch_size=20,
url_to_full_document=url_to_full_document,
progress_callback=storage_progress_callback,
- provider=None, # Use configured provider
+ provider=provider,
)
# Report completion of code extraction/storage phase
diff --git a/python/src/server/services/crawling/crawling_service.py b/python/src/server/services/crawling/crawling_service.py
index e05cd6ec19..6dab85e827 100644
--- a/python/src/server/services/crawling/crawling_service.py
+++ b/python/src/server/services/crawling/crawling_service.py
@@ -474,12 +474,24 @@ async def code_progress_callback(data: dict):
)
try:
+ # Extract provider from request or use credential service default
+ provider = request.get("provider")
+ if not provider:
+ try:
+ from ..credential_service import credential_service
+ provider_config = await credential_service.get_active_provider("llm")
+ provider = provider_config.get("provider", "openai")
+ except Exception as e:
+ logger.warning(f"Failed to get provider from credential service: {e}, defaulting to openai")
+ provider = "openai"
+
code_examples_count = await self.doc_storage_ops.extract_and_store_code_examples(
crawl_results,
storage_results["url_to_full_document"],
storage_results["source_id"],
code_progress_callback,
self._check_cancellation,
+ provider,
)
except RuntimeError as e:
# Code extraction failed, continue crawl with warning
diff --git a/python/src/server/services/crawling/document_storage_operations.py b/python/src/server/services/crawling/document_storage_operations.py
index aaf211a707..88ed8e80df 100644
--- a/python/src/server/services/crawling/document_storage_operations.py
+++ b/python/src/server/services/crawling/document_storage_operations.py
@@ -351,6 +351,7 @@ async def extract_and_store_code_examples(
source_id: str,
progress_callback: Callable | None = None,
cancellation_check: Callable[[], None] | None = None,
+ provider: str | None = None,
) -> int:
"""
Extract code examples from crawled documents and store them.
@@ -361,12 +362,13 @@ async def extract_and_store_code_examples(
source_id: The unique source_id for all documents
progress_callback: Optional callback for progress updates
cancellation_check: Optional function to check for cancellation
+ provider: Optional LLM provider to use for code summaries
Returns:
Number of code examples stored
"""
result = await self.code_extraction_service.extract_and_store_code_examples(
- crawl_results, url_to_full_document, source_id, progress_callback, cancellation_check
+ crawl_results, url_to_full_document, source_id, progress_callback, cancellation_check, provider
)
return result
diff --git a/python/src/server/services/credential_service.py b/python/src/server/services/credential_service.py
index 954c8ab7a2..0c1173bac9 100644
--- a/python/src/server/services/credential_service.py
+++ b/python/src/server/services/credential_service.py
@@ -36,6 +36,44 @@ class CredentialItem:
description: str | None = None
+def _detect_embedding_provider_from_model(embedding_model: str) -> str:
+ """
+ Detect the appropriate embedding provider based on model name.
+
+ Args:
+ embedding_model: The embedding model name
+
+ Returns:
+ Provider name: 'google', 'openai', or 'openai' (default)
+ """
+ if not embedding_model:
+ return "openai" # Default
+
+ model_lower = embedding_model.lower()
+
+ # Google embedding models
+ google_patterns = [
+ "text-embedding-004",
+ "text-embedding-005",
+ "text-multilingual-embedding",
+ "gemini-embedding",
+ "multimodalembedding"
+ ]
+
+ if any(pattern in model_lower for pattern in google_patterns):
+ return "google"
+
+ # OpenAI embedding models (and default for unknown)
+ openai_patterns = [
+ "text-embedding-ada-002",
+ "text-embedding-3-small",
+ "text-embedding-3-large"
+ ]
+
+ # Default to OpenAI for OpenAI models or unknown models
+ return "openai"
+
+
class CredentialService:
"""Service for managing application credentials and configuration."""
@@ -435,8 +473,33 @@ async def get_active_provider(self, service_type: str = "llm") -> dict[str, Any]
# Get RAG strategy settings (where UI saves provider selection)
rag_settings = await self.get_credentials_by_category("rag_strategy")
- # Get the selected provider
- provider = rag_settings.get("LLM_PROVIDER", "openai")
+ # Get the selected provider based on service type
+ if service_type == "embedding":
+ # Get the LLM provider setting to determine embedding provider
+ llm_provider = rag_settings.get("LLM_PROVIDER", "openai")
+ embedding_model = rag_settings.get("EMBEDDING_MODEL", "text-embedding-3-small")
+
+ # Determine embedding provider based on LLM provider
+ if llm_provider == "google":
+ provider = "google"
+ elif llm_provider == "ollama":
+ provider = "ollama"
+ elif llm_provider == "openrouter":
+ # OpenRouter supports both OpenAI and Google embedding models
+ provider = _detect_embedding_provider_from_model(embedding_model)
+ elif llm_provider in ["anthropic", "grok"]:
+ # Anthropic and Grok support both OpenAI and Google embedding models
+ provider = _detect_embedding_provider_from_model(embedding_model)
+ else:
+ # Default case (openai, or unknown providers)
+ provider = "openai"
+
+ logger.debug(f"Determined embedding provider '{provider}' from LLM provider '{llm_provider}' and embedding model '{embedding_model}'")
+ else:
+ provider = rag_settings.get("LLM_PROVIDER", "openai")
+ # Ensure provider is a valid string, not a boolean or other type
+ if not isinstance(provider, str) or provider.lower() in ("true", "false", "none", "null"):
+ provider = "openai"
# Get API key for this provider
api_key = await self._get_provider_api_key(provider)
diff --git a/python/src/server/services/embeddings/embedding_service.py b/python/src/server/services/embeddings/embedding_service.py
index d697abf933..59d9dbfc42 100644
--- a/python/src/server/services/embeddings/embedding_service.py
+++ b/python/src/server/services/embeddings/embedding_service.py
@@ -13,7 +13,7 @@
from ...config.logfire_config import safe_span, search_logger
from ..credential_service import credential_service
-from ..llm_provider_service import get_embedding_model, get_llm_client
+from ..llm_provider_service import get_embedding_model, get_llm_client, is_google_embedding_model, is_openai_embedding_model
from ..threading_service import get_threading_service
from .embedding_exceptions import (
EmbeddingAPIError,
@@ -179,7 +179,23 @@ async def create_embeddings_batch(
"create_embeddings_batch", text_count=len(texts), total_chars=sum(len(t) for t in texts)
) as span:
try:
- async with get_llm_client(provider=provider, use_embedding_provider=True) as client:
+ # Intelligent embedding provider routing based on model type
+ # Get the embedding model first to determine the correct provider
+ embedding_model = await get_embedding_model(provider=provider)
+
+ # Route to correct provider based on model type
+ if is_google_embedding_model(embedding_model):
+ embedding_provider = "google"
+ search_logger.info(f"Routing to Google for embedding model: {embedding_model}")
+ elif is_openai_embedding_model(embedding_model):
+ embedding_provider = "openai"
+ search_logger.info(f"Routing to OpenAI for embedding model: {embedding_model}")
+ else:
+ # Keep original provider for ollama and other providers
+ embedding_provider = provider
+ search_logger.info(f"Using original provider '{provider}' for embedding model: {embedding_model}")
+
+ async with get_llm_client(provider=embedding_provider, use_embedding_provider=True) as client:
# Load batch size and dimensions from settings
try:
rag_settings = await credential_service.get_credentials_by_category(
@@ -220,7 +236,8 @@ async def rate_limit_callback(data: dict):
while retry_count < max_retries:
try:
# Create embeddings for this batch
- embedding_model = await get_embedding_model(provider=provider)
+ embedding_model = await get_embedding_model(provider=embedding_provider)
+
response = await client.embeddings.create(
model=embedding_model,
input=batch,
diff --git a/python/src/server/services/llm_provider_service.py b/python/src/server/services/llm_provider_service.py
index 88cfd2219a..bbbaf7d47b 100644
--- a/python/src/server/services/llm_provider_service.py
+++ b/python/src/server/services/llm_provider_service.py
@@ -618,16 +618,19 @@ async def get_embedding_model(provider: str | None = None) -> str:
# Ollama default embedding model
return "nomic-embed-text"
elif provider_name == "google":
- # Google's embedding model
- return "gemini-embedding-001"
+ # Google's latest embedding model
+ return "text-embedding-004"
elif provider_name == "openrouter":
- # OpenRouter supports OpenAI embedding models
+ # OpenRouter supports both OpenAI and Google embedding models
+ # Default to OpenAI's latest for compatibility
return "text-embedding-3-small"
elif provider_name == "anthropic":
- # Anthropic doesn't have native embedding models, fallback to OpenAI
+ # Anthropic supports OpenAI and Google embedding models through their API
+ # Default to OpenAI's latest for compatibility
return "text-embedding-3-small"
elif provider_name == "grok":
- # Grok doesn't have embedding models yet, fallback to OpenAI
+ # Grok supports OpenAI and Google embedding models through their API
+ # Default to OpenAI's latest for compatibility
return "text-embedding-3-small"
else:
# Fallback to OpenAI's model
@@ -639,6 +642,188 @@ async def get_embedding_model(provider: str | None = None) -> str:
return "text-embedding-3-small"
+def is_openai_embedding_model(model: str) -> bool:
+ """Check if a model is an OpenAI embedding model."""
+ if not model:
+ return False
+
+ openai_models = {
+ "text-embedding-ada-002",
+ "text-embedding-3-small",
+ "text-embedding-3-large"
+ }
+ return model.lower() in openai_models
+
+
+def is_google_embedding_model(model: str) -> bool:
+ """Check if a model is a Google embedding model."""
+ if not model:
+ return False
+
+ model_lower = model.lower()
+ google_patterns = [
+ "text-embedding-004",
+ "text-embedding-005",
+ "text-multilingual-embedding-002",
+ "gemini-embedding-001",
+ "multimodalembedding@001"
+ ]
+
+ return any(pattern in model_lower for pattern in google_patterns)
+
+
+def is_valid_embedding_model_for_provider(model: str, provider: str) -> bool:
+ """
+ Validate if an embedding model is compatible with a provider.
+
+ Args:
+ model: The embedding model name
+ provider: The provider name
+
+ Returns:
+ bool: True if the model is compatible with the provider
+ """
+ if not model or not provider:
+ return False
+
+ provider_lower = provider.lower()
+
+ if provider_lower == "openai":
+ return is_openai_embedding_model(model)
+ elif provider_lower == "google":
+ return is_google_embedding_model(model)
+ elif provider_lower in ["openrouter", "anthropic", "grok"]:
+ # These providers support both OpenAI and Google models
+ return is_openai_embedding_model(model) or is_google_embedding_model(model)
+ elif provider_lower == "ollama":
+ # Ollama has its own models, check common ones
+ model_lower = model.lower()
+ ollama_patterns = ["nomic-embed", "all-minilm", "mxbai-embed", "embed"]
+ return any(pattern in model_lower for pattern in ollama_patterns)
+ else:
+ # For unknown providers, assume OpenAI compatibility
+ return is_openai_embedding_model(model)
+
+
+def get_supported_embedding_models(provider: str) -> list[str]:
+ """
+ Get list of supported embedding models for a provider.
+
+ Args:
+ provider: The provider name
+
+ Returns:
+ List of supported embedding model names
+ """
+ if not provider:
+ return []
+
+ provider_lower = provider.lower()
+
+ openai_models = [
+ "text-embedding-ada-002",
+ "text-embedding-3-small",
+ "text-embedding-3-large"
+ ]
+
+ google_models = [
+ "text-embedding-004",
+ "text-embedding-005",
+ "text-multilingual-embedding-002",
+ "gemini-embedding-001",
+ "multimodalembedding@001"
+ ]
+
+ if provider_lower == "openai":
+ return openai_models
+ elif provider_lower == "google":
+ return google_models
+ elif provider_lower in ["openrouter", "anthropic", "grok"]:
+ # These providers support both OpenAI and Google models
+ return openai_models + google_models
+ elif provider_lower == "ollama":
+ return ["nomic-embed-text", "all-minilm", "mxbai-embed-large"]
+ else:
+ # For unknown providers, assume OpenAI compatibility
+ return openai_models
+
+
+def requires_max_completion_tokens(model_name: str) -> bool:
+ """
+ Check if a model requires max_completion_tokens instead of max_tokens.
+
+ OpenAI changed the parameter for reasoning models (o1, o3, GPT-5 series)
+ introduced in September 2024.
+
+ Args:
+ model_name: The model name to check
+
+ Returns:
+ True if the model requires max_completion_tokens, False otherwise
+ """
+ if not model_name:
+ return False
+
+ model_lower = model_name.lower()
+
+ # GPT-5 series (all variants)
+ if "gpt-5" in model_lower:
+ return True
+
+ # o1 and o3 series (reasoning models)
+ reasoning_patterns = [
+ "o1-mini", "o1-preview", "o1-pro",
+ "o3-mini", "o3-medium", "o3-large", "o3-pro",
+ "o1", "o3" # Base patterns
+ ]
+
+ for pattern in reasoning_patterns:
+ if pattern in model_lower:
+ return True
+
+ return False
+
+
+def prepare_chat_completion_params(model: str, params: dict) -> dict:
+ """
+ Convert parameters for compatibility with reasoning models (GPT-5, o1, o3 series).
+
+ OpenAI made several API changes for reasoning models:
+ 1. max_tokens β max_completion_tokens
+ 2. temperature must be 1.0 (default) - custom values not supported
+
+ This ensures compatibility with OpenAI's API changes for newer models
+ while maintaining backward compatibility for existing models.
+
+ Args:
+ model: The model name being used
+ params: Dictionary of API parameters
+
+ Returns:
+ Dictionary with converted parameters for the model
+ """
+ if not model or not params:
+ return params
+
+ # Make a copy to avoid modifying the original
+ updated_params = params.copy()
+
+ is_reasoning_model = requires_max_completion_tokens(model)
+
+ # Convert max_tokens to max_completion_tokens for reasoning models
+ if is_reasoning_model and "max_tokens" in updated_params:
+ max_tokens_value = updated_params.pop("max_tokens")
+ updated_params["max_completion_tokens"] = max_tokens_value
+ logger.debug(f"Converted max_tokens to max_completion_tokens for model {model}")
+
+ # Remove custom temperature for reasoning models (they only support default temperature=1.0)
+ if is_reasoning_model and "temperature" in updated_params:
+ original_temp = updated_params.pop("temperature")
+ logger.debug(f"Removed custom temperature {original_temp} for reasoning model {model} (only supports default temperature=1.0)")
+
+ return updated_params
+
+
async def get_embedding_model_with_routing(provider: str | None = None, instance_url: str | None = None) -> tuple[str, str]:
"""
Get the embedding model with intelligent routing for multi-instance setups.
diff --git a/python/src/server/services/provider_discovery_service.py b/python/src/server/services/provider_discovery_service.py
index e49341cf77..9bc94d5e58 100644
--- a/python/src/server/services/provider_discovery_service.py
+++ b/python/src/server/services/provider_discovery_service.py
@@ -2,7 +2,7 @@
Provider Discovery Service
Discovers available models, checks provider health, and provides model specifications
-for OpenAI, Google Gemini, Ollama, and Anthropic providers.
+for OpenAI, Google Gemini, Ollama, Anthropic, and Grok providers.
"""
import time
@@ -359,6 +359,36 @@ async def discover_anthropic_models(self, api_key: str) -> list[ModelSpec]:
return models
+ async def discover_grok_models(self, api_key: str) -> list[ModelSpec]:
+ """Discover available Grok models."""
+ cache_key = f"grok_models_{hash(api_key)}"
+ cached = self._get_cached_result(cache_key)
+ if cached:
+ return cached
+
+ models = []
+ try:
+ # Grok model specifications
+ model_specs = [
+ ModelSpec("grok-3-mini", "grok", 32768, True, True, False, None, 0.15, 0.60, "Fast and efficient Grok model"),
+ ModelSpec("grok-3", "grok", 32768, True, True, False, None, 2.00, 10.00, "Standard Grok model"),
+ ModelSpec("grok-4", "grok", 32768, True, True, False, None, 5.00, 25.00, "Advanced Grok model"),
+ ModelSpec("grok-2-vision", "grok", 8192, True, True, True, None, 3.00, 15.00, "Grok model with vision capabilities"),
+ ModelSpec("grok-2-latest", "grok", 8192, True, True, False, None, 2.00, 10.00, "Latest Grok 2 model"),
+ ]
+
+ # Test connectivity - Grok doesn't have a models list endpoint,
+ # so we'll just return the known models if API key is provided
+ if api_key:
+ models = model_specs
+ self._cache_result(cache_key, models)
+ logger.info(f"Discovered {len(models)} Grok models")
+
+ except Exception as e:
+ logger.error(f"Error discovering Grok models: {e}")
+
+ return models
+
async def check_provider_health(self, provider: str, config: dict[str, Any]) -> ProviderStatus:
"""Check health and connectivity status of a provider."""
start_time = time.time()
@@ -456,6 +486,23 @@ async def check_provider_health(self, provider: str, config: dict[str, Any]) ->
last_checked=time.time()
)
+ elif provider == "grok":
+ api_key = config.get("api_key")
+ if not api_key:
+ return ProviderStatus(provider, False, None, "API key not configured")
+
+ # Grok doesn't have a health check endpoint, so we'll assume it's available
+ # if API key is provided. In a real implementation, you might want to make a
+ # small test request to verify the key is valid.
+ response_time = (time.time() - start_time) * 1000
+ return ProviderStatus(
+ provider="grok",
+ is_available=True,
+ response_time_ms=response_time,
+ models_available=5, # Known model count
+ last_checked=time.time()
+ )
+
else:
return ProviderStatus(provider, False, None, f"Unknown provider: {provider}")
@@ -496,6 +543,11 @@ async def get_all_available_models(self) -> dict[str, list[ModelSpec]]:
if anthropic_key:
providers["anthropic"] = await self.discover_anthropic_models(anthropic_key)
+ # Grok
+ grok_key = await credential_service.get_credential("GROK_API_KEY")
+ if grok_key:
+ providers["grok"] = await self.discover_grok_models(grok_key)
+
except Exception as e:
logger.error(f"Error getting all available models: {e}")
diff --git a/python/src/server/services/storage/code_storage_service.py b/python/src/server/services/storage/code_storage_service.py
index 6b988c3dee..ffbd26afc2 100644
--- a/python/src/server/services/storage/code_storage_service.py
+++ b/python/src/server/services/storage/code_storage_service.py
@@ -226,129 +226,6 @@ def score_block(block):
return best_block
-def _should_attempt_fallback(provider: str, model: str, is_reasoning: bool, error_context: dict) -> bool:
- """
- Determine if fallback should be attempted based on error type and configuration.
-
- Args:
- provider: The LLM provider name
- model: The model identifier
- is_reasoning: Whether this is a reasoning model
- error_context: Context about the error that occurred
-
- Returns:
- True if fallback should be attempted
- """
- # Check for environment variable to disable fallbacks (fail-fast mode)
- if os.getenv("DISABLE_LLM_FALLBACKS", "false").lower() == "true":
- search_logger.debug("LLM fallbacks disabled by DISABLE_LLM_FALLBACKS environment variable")
- return False
-
- # Only attempt fallback for specific provider/model combinations
- fallback_eligible_providers = ["grok", "openai"] # Providers that support fallback
-
- if provider not in fallback_eligible_providers:
- search_logger.debug(f"Provider {provider} not eligible for fallback")
- return False
-
- # Only allow fallback for empty responses, not other error types
- if error_context.get("response_type") != "empty_content":
- search_logger.debug(f"Error type {error_context.get('response_type')} not eligible for fallback")
- return False
-
- # Allow fallback for Grok and reasoning models that commonly have empty responses
- if provider == "grok" or is_reasoning:
- search_logger.debug(f"Fallback enabled for {provider}/{model} (reasoning: {is_reasoning})")
- return True
-
- return False
-
-
-async def _attempt_single_fallback(
- original_model: str,
- original_provider: str,
- is_reasoning: bool,
- original_params: dict,
- error_context: dict
-) -> str | None:
- """
- Attempt a single fallback to gpt-4o-mini with structured tracking.
-
- Args:
- original_model: The original model that failed
- original_provider: The original provider that failed
- is_reasoning: Whether original was a reasoning model
- original_params: The original request parameters
- error_context: Context about the original error
-
- Returns:
- Response content if fallback succeeded, None if failed
- """
- fallback_start_time = time.time()
-
- # Always fallback to reliable gpt-4o-mini
- fallback_model = "gpt-4o-mini"
- fallback_provider = "openai"
-
- fallback_context = {
- "original_model": original_model,
- "original_provider": original_provider,
- "fallback_model": fallback_model,
- "fallback_provider": fallback_provider,
- "original_error": error_context,
- "fallback_attempt_time": time.time()
- }
-
- search_logger.info(f"Attempting single fallback: {original_model} β {fallback_model}")
-
- try:
- # Prepare fallback parameters (simplified, no JSON format to avoid issues)
- fallback_params = {
- "model": fallback_model,
- "messages": original_params["messages"],
- "max_tokens": min(original_params.get("max_tokens", 500), 500), # Cap for reliability
- "temperature": original_params.get("temperature", 0.3)
- }
-
- # No response_format for fallback to maximize reliability
- if "response_format" in fallback_params:
- del fallback_params["response_format"]
-
- async with get_llm_client(provider=fallback_provider) as fallback_client:
- fallback_response = await fallback_client.chat.completions.create(**fallback_params)
- fallback_content = fallback_response.choices[0].message.content
-
- if fallback_content and fallback_content.strip():
- fallback_time = time.time() - fallback_start_time
- fallback_success = {
- **fallback_context,
- "fallback_succeeded": True,
- "fallback_time": f"{fallback_time:.2f}s",
- "fallback_content_length": len(fallback_content.strip())
- }
- search_logger.info(f"Fallback success: {fallback_success}")
- return fallback_content.strip()
- else:
- # Fallback returned empty - log and return None
- fallback_failure = {
- **fallback_context,
- "fallback_succeeded": False,
- "fallback_error": "empty_response"
- }
- search_logger.error(f"Fallback returned empty response: {fallback_failure}")
- return None
-
- except Exception as e:
- fallback_time = time.time() - fallback_start_time
- fallback_error = {
- **fallback_context,
- "fallback_succeeded": False,
- "fallback_error": str(e),
- "fallback_time": f"{fallback_time:.2f}s"
- }
- search_logger.error(f"Fallback exception: {fallback_error}")
- return None
-
def extract_code_blocks(markdown_content: str, min_length: int = None) -> list[dict[str, Any]]:
"""
@@ -714,7 +591,17 @@ async def _generate_code_example_summary_async(
from ..llm_provider_service import get_llm_client
# Get model choice from credential service (RAG setting)
- model_choice = _get_model_choice()
+ model_choice = await _get_model_choice()
+
+ # If provider is not specified, get it from credential service
+ if provider is None:
+ try:
+ provider_config = await credential_service.get_active_provider("llm")
+ provider = provider_config.get("provider", "openai")
+ search_logger.debug(f"Auto-detected provider from credential service: {provider}")
+ except Exception as e:
+ search_logger.warning(f"Failed to get provider from credential service: {e}, defaulting to openai")
+ provider = "openai"
# Create the prompt
prompt = f"""
@@ -749,9 +636,9 @@ async def _generate_code_example_summary_async(
f"Generating summary for {hash(code) & 0xffffff:06x} using model: {model_choice}"
)
- response = await client.chat.completions.create(
- model=model_choice,
- messages=[
+ request_params = {
+ "model": model_choice,
+ "messages": [
{
"role": "system",
"content": "You are a helpful assistant that analyzes code examples and provides JSON responses with example names and summaries.",
@@ -892,66 +779,13 @@ async def _generate_code_example_summary_async(
model_type = "Grok" if provider == "grok" else f"reasoning model ({model_choice})"
search_logger.debug(f"{model_type} total response time: {elapsed_time:.2f}s")
- # Handle empty response with streamlined fallback logic
if not response_content:
- # Structured error analysis for debugging
- error_context = {
- "model": model_choice,
- "provider": provider,
- "request_time": f"{elapsed_time:.2f}s" if 'elapsed_time' in locals() else "unknown",
- "response_type": "empty_content",
- "response_choices_count": len(response.choices) if response.choices else 0
- }
- search_logger.error(f"Empty response from LLM: {error_context}")
- # Determine if fallback should be attempted based on error type and configuration
- should_fallback = _should_attempt_fallback(provider, model_choice, is_reasoning, error_context)
-
- if should_fallback:
- # Single fallback attempt with tracking
- fallback_result = await _attempt_single_fallback(
- model_choice, provider, is_reasoning, request_params, error_context
- )
- if fallback_result:
- response_content = fallback_result
- search_logger.info(f"Fallback succeeded for {model_choice}")
- else:
- # Log fallback failure analysis for circuit breaker patterns
- fallback_failure = {
- "original_model": model_choice,
- "original_provider": provider,
- "fallback_attempted": True,
- "fallback_succeeded": False,
- "error_context": error_context
- }
- elif (provider == "grok" or is_reasoning) and attempt < max_retries - 1:
- # Empty response from Grok or reasoning models - track failure and retry
- model_type = "Grok" if provider == "grok" else f"reasoning model ({model_choice})"
- failure_reason = f"empty_response_attempt_{attempt + 1}"
- failure_reasons.append(failure_reason)
-
- async with get_llm_client(provider="openai") as fallback_client:
- search_logger.info("Using OpenAI fallback for Grok failure")
- # Convert max_tokens to max_completion_tokens for GPT-5/reasoning models
- final_fallback_params = prepare_chat_completion_params(fallback_params["model"], fallback_params)
- fallback_response = await fallback_client.chat.completions.create(**final_fallback_params)
- fallback_content = fallback_response.choices[0].message.content
-
- if fallback_content and fallback_content.strip():
- search_logger.info("OpenAI fallback succeeded")
- response_content = fallback_content.strip()
- else:
- search_logger.error("OpenAI fallback also returned empty response")
- raise ValueError("Both Grok and OpenAI fallback failed")
-
- except Exception as fallback_error:
- search_logger.error(f"OpenAI fallback failed: {fallback_error}")
- raise ValueError(f"Grok failed and fallback to OpenAI also failed: {fallback_error}") from fallback_error
- elif is_reasoning:
- # Implement fallback for reasoning model (GPT-5, o1, o3) failures
- search_logger.error("Reasoning model empty response debugging:")
- search_logger.error(f" - Model: {model_choice}")
- search_logger.error(f" - Provider: {provider}")
+ search_logger.error(f"Empty response from LLM for model: {model_choice} (provider: {provider})")
+ if provider == "grok":
+ search_logger.error("Grok empty response debugging:")
search_logger.error(f" - Request took: {elapsed_time:.2f}s")
+ search_logger.error(f" - Response status: {getattr(response, 'status_code', 'N/A')}")
+ search_logger.error(f" - Response headers: {getattr(response, 'headers', 'N/A')}")
search_logger.error(f" - Full response: {response}")
search_logger.error(f" - Response choices length: {len(response.choices) if response.choices else 0}")
if response.choices:
@@ -960,26 +794,22 @@ async def _generate_code_example_summary_async(
search_logger.error(f" - Message role: {response.choices[0].message.role}")
search_logger.error("Check: 1) API key validity, 2) rate limits, 3) model availability")
- # Implement fallback to non-reasoning model for reasoning model failures
- search_logger.warning(f"Attempting fallback to gpt-4o-mini due to {model_choice} failure...")
+ # Implement fallback for Grok failures
+ search_logger.warning("Attempting fallback to OpenAI due to Grok failure...")
try:
- # Use a reliable non-reasoning model as fallback
+ # Use OpenAI as fallback with similar parameters
fallback_params = {
"model": "gpt-4o-mini",
"messages": request_params["messages"],
+ "temperature": request_params.get("temperature", 0.1),
"max_tokens": request_params.get("max_tokens", 500),
- "temperature": request_params.get("temperature", 0.3),
- "response_format": {"type": "json_object"}
}
async with get_llm_client(provider="openai") as fallback_client:
- search_logger.info(f"Using gpt-4o-mini fallback for {model_choice} failure")
- # No parameter conversion needed for non-reasoning model
fallback_response = await fallback_client.chat.completions.create(**fallback_params)
fallback_content = fallback_response.choices[0].message.content
-
if fallback_content and fallback_content.strip():
- search_logger.info(f"gpt-4o-mini fallback succeeded for {model_choice}")
+ search_logger.info("gpt-4o-mini fallback succeeded")
response_content = fallback_content.strip()
else:
search_logger.error("gpt-4o-mini fallback also returned empty response")
@@ -989,9 +819,8 @@ async def _generate_code_example_summary_async(
search_logger.error(f"gpt-4o-mini fallback failed: {fallback_error}")
raise ValueError(f"{model_choice} failed and fallback to gpt-4o-mini also failed: {fallback_error}") from fallback_error
else:
- # No fallback attempted - fail fast with detailed context
- search_logger.error(f"No fallback configured for {provider}/{model_choice} - failing fast")
- raise ValueError(f"Empty response from {model_choice} (provider: {provider}). Check: API key validity, rate limits, model availability")
+ search_logger.debug(f"Full response object: {response}")
+ raise ValueError("Empty response from LLM")
if not response_content:
# This should not happen after fallback logic, but safety check
@@ -1035,7 +864,7 @@ async def _generate_code_example_summary_async(
async def generate_code_summaries_batch(
- code_blocks: list[dict[str, Any]], max_workers: int = None, progress_callback=None
+ code_blocks: list[dict[str, Any]], max_workers: int = None, progress_callback=None, provider: str = None
) -> list[dict[str, str]]:
"""
Generate summaries for multiple code blocks with rate limiting and proper worker management.
@@ -1044,6 +873,7 @@ async def generate_code_summaries_batch(
code_blocks: List of code block dictionaries
max_workers: Maximum number of concurrent API requests
progress_callback: Optional callback for progress updates (async function)
+ provider: LLM provider to use for generation (e.g., 'grok', 'openai', 'anthropic')
Returns:
List of summary dictionaries
@@ -1088,6 +918,7 @@ async def generate_single_summary_with_limit(block: dict[str, Any]) -> dict[str,
block["context_before"],
block["context_after"],
block.get("language", ""),
+ provider,
)
# Update progress
@@ -1182,27 +1013,21 @@ async def add_code_examples_to_supabase(
except Exception as e:
search_logger.error(f"Error deleting existing code examples for {url}: {e}")
- # Check if contextual embeddings are enabled
+ # Check if contextual embeddings are enabled (use proper async method like document storage)
try:
- use_contextual_embeddings = credential_service._cache.get("USE_CONTEXTUAL_EMBEDDINGS")
- if isinstance(use_contextual_embeddings, str):
- use_contextual_embeddings = use_contextual_embeddings.lower() == "true"
- elif isinstance(use_contextual_embeddings, dict) and use_contextual_embeddings.get(
- "is_encrypted"
- ):
- # Handle encrypted value
- encrypted_value = use_contextual_embeddings.get("encrypted_value")
- if encrypted_value:
- try:
- decrypted = credential_service._decrypt_value(encrypted_value)
- use_contextual_embeddings = decrypted.lower() == "true"
- except:
- use_contextual_embeddings = False
- else:
- use_contextual_embeddings = False
+ raw_value = await credential_service.get_credential(
+ "USE_CONTEXTUAL_EMBEDDINGS", "false", decrypt=True
+ )
+ search_logger.info(f"DEBUG: Raw contextual embeddings value: {raw_value} (type: {type(raw_value)})")
+
+ if isinstance(raw_value, str):
+ use_contextual_embeddings = raw_value.lower() == "true"
else:
- use_contextual_embeddings = bool(use_contextual_embeddings)
- except:
+ use_contextual_embeddings = bool(raw_value)
+
+ search_logger.info(f"DEBUG: Processed contextual embeddings value: {use_contextual_embeddings}")
+ except Exception as e:
+ search_logger.error(f"DEBUG: Error reading contextual embeddings: {e}")
# Fallback to environment variable
use_contextual_embeddings = (
os.getenv("USE_CONTEXTUAL_EMBEDDINGS", "false").lower() == "true"
@@ -1291,7 +1116,7 @@ async def add_code_examples_to_supabase(
llm_chat_model = await credential_service.get_credential("MODEL_CHOICE", "gpt-4o-mini")
else:
# For code summaries, we use MODEL_CHOICE
- llm_chat_model = _get_model_choice()
+ llm_chat_model = await _get_model_choice()
except Exception as e:
search_logger.warning(f"Failed to get LLM chat model: {e}")
llm_chat_model = "gpt-4o-mini" # Default fallback
From 4dc3e9721ea7f20e1ec4e5232f5bff049dffe9eb Mon Sep 17 00:00:00 2001
From: Chillbruhhh
Date: Sat, 20 Sep 2025 06:52:36 -0500
Subject: [PATCH 05/28] fixed contextual embeddings issue
---
python/src/server/services/storage/code_storage_service.py | 5 -----
1 file changed, 5 deletions(-)
diff --git a/python/src/server/services/storage/code_storage_service.py b/python/src/server/services/storage/code_storage_service.py
index ffbd26afc2..6df33292e6 100644
--- a/python/src/server/services/storage/code_storage_service.py
+++ b/python/src/server/services/storage/code_storage_service.py
@@ -1018,14 +1018,10 @@ async def add_code_examples_to_supabase(
raw_value = await credential_service.get_credential(
"USE_CONTEXTUAL_EMBEDDINGS", "false", decrypt=True
)
- search_logger.info(f"DEBUG: Raw contextual embeddings value: {raw_value} (type: {type(raw_value)})")
-
if isinstance(raw_value, str):
use_contextual_embeddings = raw_value.lower() == "true"
else:
use_contextual_embeddings = bool(raw_value)
-
- search_logger.info(f"DEBUG: Processed contextual embeddings value: {use_contextual_embeddings}")
except Exception as e:
search_logger.error(f"DEBUG: Error reading contextual embeddings: {e}")
# Fallback to environment variable
@@ -1099,7 +1095,6 @@ async def add_code_examples_to_supabase(
# Get model information for tracking
from ..llm_provider_service import get_embedding_model
- from ..credential_service import credential_service
# Get embedding model name
embedding_model_name = await get_embedding_model(provider=provider)
From b7b11671c32736f9b27692a59b4bd9d7f10ab011 Mon Sep 17 00:00:00 2001
From: Chillbruhhh
Date: Sat, 20 Sep 2025 07:05:47 -0500
Subject: [PATCH 06/28] - Added inspect-aware shutdown handling so
get_llm_client always closes the underlying AsyncOpenAI / httpx.AsyncClient
while the loop is still alive, with defensive logging if shutdown happens
late (python/src/server/services/llm_provider_service.py:14,
python/src/server/ services/llm_provider_service.py:520).
---
.../server/services/llm_provider_service.py | 42 ++++++++++++++++++-
.../tests/test_async_llm_provider_service.py | 25 +++++++----
2 files changed, 56 insertions(+), 11 deletions(-)
diff --git a/python/src/server/services/llm_provider_service.py b/python/src/server/services/llm_provider_service.py
index bbbaf7d47b..08adb74a04 100644
--- a/python/src/server/services/llm_provider_service.py
+++ b/python/src/server/services/llm_provider_service.py
@@ -5,6 +5,7 @@
Supports OpenAI, Ollama, and Google Gemini.
"""
+import inspect
import time
from contextlib import asynccontextmanager
from typing import Any
@@ -512,8 +513,45 @@ async def get_llm_client(provider: str | None = None, use_embedding_provider: bo
)
raise
finally:
- # Cleanup if needed
- pass
+ if client is not None:
+ provider_for_log = locals().get("provider_name", "unknown")
+ safe_provider = _sanitize_for_log(str(provider_for_log))
+
+ try:
+ close_method = getattr(client, "aclose", None)
+ if callable(close_method):
+ if inspect.iscoroutinefunction(close_method):
+ await close_method()
+ else:
+ maybe_coro = close_method()
+ if inspect.isawaitable(maybe_coro):
+ await maybe_coro
+ else:
+ close_method = getattr(client, "close", None)
+ if callable(close_method):
+ if inspect.iscoroutinefunction(close_method):
+ await close_method()
+ else:
+ close_result = close_method()
+ if inspect.isawaitable(close_result):
+ await close_result
+ logger.debug(f"Closed LLM client for provider: {safe_provider}")
+ except RuntimeError as close_error:
+ if "Event loop is closed" in str(close_error):
+ logger.error(
+ f"Failed to close LLM client cleanly for provider {safe_provider}: event loop already closed",
+ exc_info=True,
+ )
+ else:
+ logger.error(
+ f"Runtime error closing LLM client for provider {safe_provider}: {close_error}",
+ exc_info=True,
+ )
+ except Exception as close_error:
+ logger.error(
+ f"Unexpected error while closing LLM client for provider {safe_provider}: {close_error}",
+ exc_info=True,
+ )
async def _get_optimal_ollama_instance(instance_type: str | None = None,
diff --git a/python/tests/test_async_llm_provider_service.py b/python/tests/test_async_llm_provider_service.py
index 6c0128972f..8905cb7b34 100644
--- a/python/tests/test_async_llm_provider_service.py
+++ b/python/tests/test_async_llm_provider_service.py
@@ -33,6 +33,12 @@ async def __aexit__(self, exc_type, exc_val, exc_tb):
class TestAsyncLLMProviderService:
"""Test suite for async LLM provider service functions"""
+ @staticmethod
+ def _make_mock_client():
+ client = MagicMock()
+ client.aclose = AsyncMock()
+ return client
+
@pytest.fixture(autouse=True)
def clear_cache(self):
"""Clear cache before each test"""
@@ -98,7 +104,7 @@ async def test_get_llm_client_openai_success(
with patch(
"src.server.services.llm_provider_service.openai.AsyncOpenAI"
) as mock_openai:
- mock_client = MagicMock()
+ mock_client = self._make_mock_client()
mock_openai.return_value = mock_client
async with get_llm_client() as client:
@@ -121,7 +127,7 @@ async def test_get_llm_client_ollama_success(
with patch(
"src.server.services.llm_provider_service.openai.AsyncOpenAI"
) as mock_openai:
- mock_client = MagicMock()
+ mock_client = self._make_mock_client()
mock_openai.return_value = mock_client
async with get_llm_client() as client:
@@ -143,7 +149,7 @@ async def test_get_llm_client_google_success(
with patch(
"src.server.services.llm_provider_service.openai.AsyncOpenAI"
) as mock_openai:
- mock_client = MagicMock()
+ mock_client = self._make_mock_client()
mock_openai.return_value = mock_client
async with get_llm_client() as client:
@@ -166,7 +172,7 @@ async def test_get_llm_client_with_provider_override(self, mock_credential_servi
with patch(
"src.server.services.llm_provider_service.openai.AsyncOpenAI"
) as mock_openai:
- mock_client = MagicMock()
+ mock_client = self._make_mock_client()
mock_openai.return_value = mock_client
async with get_llm_client(provider="openai") as client:
@@ -194,7 +200,7 @@ async def test_get_llm_client_use_embedding_provider(self, mock_credential_servi
with patch(
"src.server.services.llm_provider_service.openai.AsyncOpenAI"
) as mock_openai:
- mock_client = MagicMock()
+ mock_client = self._make_mock_client()
mock_openai.return_value = mock_client
async with get_llm_client(use_embedding_provider=True) as client:
@@ -225,7 +231,7 @@ async def test_get_llm_client_missing_openai_key_with_ollama_fallback(self, mock
with patch(
"src.server.services.llm_provider_service.openai.AsyncOpenAI"
) as mock_openai:
- mock_client = MagicMock()
+ mock_client = self._make_mock_client()
mock_openai.return_value = mock_client
# Should fallback to Ollama instead of raising an error
@@ -426,7 +432,7 @@ async def test_cache_usage_in_get_llm_client(
with patch(
"src.server.services.llm_provider_service.openai.AsyncOpenAI"
) as mock_openai:
- mock_client = MagicMock()
+ mock_client = self._make_mock_client()
mock_openai.return_value = mock_client
# First call should hit the credential service
@@ -464,7 +470,7 @@ async def test_context_manager_cleanup(self, mock_credential_service, openai_pro
with patch(
"src.server.services.llm_provider_service.openai.AsyncOpenAI"
) as mock_openai:
- mock_client = MagicMock()
+ mock_client = self._make_mock_client()
mock_openai.return_value = mock_client
client_ref = None
@@ -474,6 +480,7 @@ async def test_context_manager_cleanup(self, mock_credential_service, openai_pro
# After context manager exits, should still have reference to client
assert client_ref == mock_client
+ mock_client.aclose.assert_awaited_once()
@pytest.mark.asyncio
async def test_multiple_providers_in_sequence(self, mock_credential_service):
@@ -494,7 +501,7 @@ async def test_multiple_providers_in_sequence(self, mock_credential_service):
with patch(
"src.server.services.llm_provider_service.openai.AsyncOpenAI"
) as mock_openai:
- mock_client = MagicMock()
+ mock_client = self._make_mock_client()
mock_openai.return_value = mock_client
for config in configs:
From 7cc29ac149ed5819ebe95db623cdc1da2110a197 Mon Sep 17 00:00:00 2001
From: Chillbruhhh
Date: Sat, 20 Sep 2025 07:27:19 -0500
Subject: [PATCH 07/28] - Restructured get_llm_client so client creation and
usage live in separate try/finally blocks; fallback clients now close without
logging spurious Error creating LLM client when downstream code
raises (python/src/server/services/llm_provider_service.py:335-556). -
Close logic now sanitizes provider names consistently and awaits whichever
aclose/close coroutine the SDK exposes, keeping the loop shut down
cleanly (python/src/server/services/llm_provider_service.py:530-559).
Robust JSON Parsing
- Added _extract_json_payload to strip code fences / extra text returned by
Ollama before json.loads runs, averting the markdown-induced decode errors
you saw in logs
(python/src/server/services/storage/code_storage_service.py:40-63).
- Swapped the direct parse call for the
sanitized payload and emit a debug preview when cleanup alters the content
(python/src/server/ services/storage/code_storage_service.py:858-864).
---
.../server/services/llm_provider_service.py | 85 +++++++++++--------
.../services/storage/code_storage_service.py | 34 +++++++-
2 files changed, 82 insertions(+), 37 deletions(-)
diff --git a/python/src/server/services/llm_provider_service.py b/python/src/server/services/llm_provider_service.py
index 08adb74a04..5b13c1a85b 100644
--- a/python/src/server/services/llm_provider_service.py
+++ b/python/src/server/services/llm_provider_service.py
@@ -310,8 +310,12 @@ def get_cache_security_report() -> dict[str, Any]:
return report
@asynccontextmanager
-async def get_llm_client(provider: str | None = None, use_embedding_provider: bool = False,
- instance_type: str | None = None, base_url: str | None = None):
+async def get_llm_client(
+ provider: str | None = None,
+ use_embedding_provider: bool = False,
+ instance_type: str | None = None,
+ base_url: str | None = None,
+):
"""
Create an async OpenAI-compatible client based on the configured provider.
@@ -329,6 +333,8 @@ async def get_llm_client(provider: str | None = None, use_embedding_provider: bo
openai.AsyncOpenAI: An OpenAI-compatible client configured for the selected provider
"""
client = None
+ provider_name: str | None = None
+ api_key = None
try:
# Get provider configuration from database settings
@@ -348,7 +354,11 @@ async def get_llm_client(provider: str | None = None, use_embedding_provider: bo
logger.debug("Using cached rag_strategy settings")
# For Ollama, don't use the base_url from config - let _get_optimal_ollama_instance decide
- base_url = credential_service._get_provider_base_url(provider, rag_settings) if provider != "ollama" else None
+ base_url = (
+ credential_service._get_provider_base_url(provider, rag_settings)
+ if provider != "ollama"
+ else None
+ )
else:
# Get configured provider from database
service_type = "embedding" if use_embedding_provider else "llm"
@@ -383,64 +393,64 @@ async def get_llm_client(provider: str | None = None, use_embedding_provider: bo
raise ValueError("API key contains invalid characters")
# Sanitize provider name for logging
- safe_provider_name = _sanitize_for_log(provider_name)
+ safe_provider_name = _sanitize_for_log(provider_name) if provider_name else "unknown"
logger.info(f"Creating LLM client for provider: {safe_provider_name}")
if provider_name == "openai":
if not api_key:
# Check if Ollama fallback is explicitly enabled (fail fast principle)
try:
- enable_fallback = await credential_service.get_credential("ENABLE_OLLAMA_FALLBACK", "false")
+ enable_fallback = await credential_service.get_credential(
+ "ENABLE_OLLAMA_FALLBACK", "false"
+ )
enable_fallback = enable_fallback.lower() == "true"
except Exception:
enable_fallback = False # Default to false for fail-fast behavior
if enable_fallback:
- logger.warning("OpenAI API key not found, attempting configured Ollama fallback")
+ logger.warning(
+ "OpenAI API key not found, attempting configured Ollama fallback"
+ )
try:
# Try to get an optimal Ollama instance for fallback
ollama_base_url = await _get_optimal_ollama_instance(
instance_type="embedding" if use_embedding_provider else "chat",
- use_embedding_provider=use_embedding_provider
+ use_embedding_provider=use_embedding_provider,
)
if ollama_base_url:
- logger.info(f"Falling back to Ollama instance: {ollama_base_url}")
- provider_name = "ollama"
- api_key = "ollama" # Ollama doesn't need a real API key
- base_url = ollama_base_url
- # Create Ollama client after fallback
+ logger.info(
+ f"Falling back to Ollama instance: {ollama_base_url}"
+ )
client = openai.AsyncOpenAI(
api_key="ollama",
base_url=ollama_base_url,
)
- logger.info(f"Ollama fallback client created successfully with base URL: {ollama_base_url}")
+ logger.info(
+ f"Ollama fallback client created successfully with base URL: {ollama_base_url}"
+ )
+ provider_name = "ollama"
+ api_key = "ollama"
+ base_url = ollama_base_url
else:
- raise ValueError("OpenAI API key not found and no Ollama instances available for fallback")
- except Exception as ollama_error:
- logger.error(f"Configured Ollama fallback failed: {ollama_error}")
- raise ValueError("OpenAI API key not found and configured Ollama fallback failed") from ollama_error
+ raise ValueError(
+ "No suitable Ollama instance available for fallback"
+ )
+ except Exception as fallback_error:
+ raise ValueError(
+ "OpenAI API key not found and Ollama fallback failed"
+ ) from fallback_error
else:
- # Fail fast and loud - provide clear instructions
- error_msg = (
- "OpenAI API key not found. To fix this:\n"
- "1. Set OPENAI_API_KEY environment variable, OR\n"
- "2. Configure OpenAI API key in the UI settings, OR\n"
- "3. Enable Ollama fallback by setting ENABLE_OLLAMA_FALLBACK=true\n"
- "Current provider configuration requires a valid OpenAI API key."
- )
- logger.error(error_msg)
- raise ValueError(error_msg)
+ raise ValueError("OpenAI API key not found")
else:
- # Only create OpenAI client if we have an API key (didn't fallback to Ollama)
client = openai.AsyncOpenAI(api_key=api_key)
logger.info("OpenAI client created successfully")
elif provider_name == "ollama":
- # Enhanced Ollama client creation with multi-instance support
+ # For Ollama, get the optimal instance based on usage
ollama_base_url = await _get_optimal_ollama_instance(
instance_type=instance_type,
use_embedding_provider=use_embedding_provider,
- base_url_override=base_url
+ base_url_override=base_url,
)
# Ollama requires an API key in the client but doesn't actually use it
@@ -494,7 +504,9 @@ async def get_llm_client(provider: str | None = None, use_embedding_provider: bo
if not key_length_valid:
logger.warning("Grok API key validation failed - insufficient length")
- logger.debug(f"Grok API key validation: format_valid={key_format_valid}, length_valid={key_length_valid}")
+ logger.debug(
+ f"Grok API key validation: format_valid={key_format_valid}, length_valid={key_length_valid}"
+ )
client = openai.AsyncOpenAI(
api_key=api_key,
@@ -505,17 +517,17 @@ async def get_llm_client(provider: str | None = None, use_embedding_provider: bo
else:
raise ValueError(f"Unsupported LLM provider: {provider_name}")
- yield client
-
except Exception as e:
logger.error(
- f"Error creating LLM client for provider {provider_name if 'provider_name' in locals() else 'unknown'}: {e}"
+ f"Error creating LLM client for provider {provider_name if provider_name else 'unknown'}: {e}"
)
raise
+
+ try:
+ yield client
finally:
if client is not None:
- provider_for_log = locals().get("provider_name", "unknown")
- safe_provider = _sanitize_for_log(str(provider_for_log))
+ safe_provider = _sanitize_for_log(provider_name) if provider_name else "unknown"
try:
close_method = getattr(client, "aclose", None)
@@ -554,6 +566,7 @@ async def get_llm_client(provider: str | None = None, use_embedding_provider: bo
)
+
async def _get_optimal_ollama_instance(instance_type: str | None = None,
use_embedding_provider: bool = False,
base_url_override: str | None = None) -> str:
diff --git a/python/src/server/services/storage/code_storage_service.py b/python/src/server/services/storage/code_storage_service.py
index 6df33292e6..4e7895d7e8 100644
--- a/python/src/server/services/storage/code_storage_service.py
+++ b/python/src/server/services/storage/code_storage_service.py
@@ -37,6 +37,32 @@ def _is_reasoning_model(model: str) -> bool:
return requires_max_completion_tokens(model)
+def _extract_json_payload(raw_response: str) -> str:
+ """Return the best-effort JSON object from an LLM response."""
+
+ if not raw_response:
+ return raw_response
+
+ cleaned = raw_response.strip()
+
+ if cleaned.startswith("```"):
+ lines = cleaned.splitlines()
+ # Drop opening fence
+ lines = lines[1:]
+ # Drop closing fence if present
+ if lines and lines[-1].strip().startswith("```"):
+ lines = lines[:-1]
+ cleaned = "\n".join(lines).strip()
+
+ # Trim any leading/trailing text outside the outermost JSON braces
+ start = cleaned.find("{")
+ end = cleaned.rfind("}")
+ if start != -1 and end != -1 and end >= start:
+ cleaned = cleaned[start : end + 1]
+
+ return cleaned.strip()
+
+
def _supports_response_format(provider: str, model: str) -> bool:
"""
Determine if a specific provider/model combination supports response_format.
@@ -829,7 +855,13 @@ async def _generate_code_example_summary_async(
response_content = response_content.strip()
search_logger.debug(f"LLM API response: {repr(response_content[:200])}...")
- result = json.loads(response_content)
+ payload = _extract_json_payload(response_content)
+ if payload != response_content:
+ search_logger.debug(
+ f"Sanitized LLM response payload before parsing: {repr(payload[:200])}..."
+ )
+
+ result = json.loads(payload)
# Validate the response has the required fields
if not result.get("example_name") or not result.get("summary"):
From 6fdce7b93afed3309fd9b71687f4fa89b78a93ec Mon Sep 17 00:00:00 2001
From: Chillbruhhh
Date: Sat, 20 Sep 2025 07:44:27 -0500
Subject: [PATCH 08/28] added provider connection support
---
python/src/server/api_routes/__init__.py | 2 ++
python/src/server/main.py | 2 ++
2 files changed, 4 insertions(+)
diff --git a/python/src/server/api_routes/__init__.py b/python/src/server/api_routes/__init__.py
index 04df299256..8a39ef2673 100644
--- a/python/src/server/api_routes/__init__.py
+++ b/python/src/server/api_routes/__init__.py
@@ -14,6 +14,7 @@
from .knowledge_api import router as knowledge_router
from .mcp_api import router as mcp_router
from .projects_api import router as projects_router
+from .providers_api import router as providers_router
from .settings_api import router as settings_router
__all__ = [
@@ -23,4 +24,5 @@
"projects_router",
"agent_chat_router",
"internal_router",
+ "providers_router",
]
diff --git a/python/src/server/main.py b/python/src/server/main.py
index bec14a7180..ba0b19cbf9 100644
--- a/python/src/server/main.py
+++ b/python/src/server/main.py
@@ -26,6 +26,7 @@
from .api_routes.ollama_api import router as ollama_router
from .api_routes.progress_api import router as progress_router
from .api_routes.projects_api import router as projects_router
+from .api_routes.providers_api import router as providers_router
# Import modular API routers
from .api_routes.settings_api import router as settings_router
@@ -186,6 +187,7 @@ async def skip_health_check_logs(request, call_next):
app.include_router(agent_chat_router)
app.include_router(internal_router)
app.include_router(bug_report_router)
+app.include_router(providers_router)
# Root endpoint
From 3ed0281fe948d3e5e972e3c1b3b97cddad8e2d89 Mon Sep 17 00:00:00 2001
From: Chillbruhhh
Date: Sat, 20 Sep 2025 07:56:52 -0500
Subject: [PATCH 09/28] added provider api key not being configured warning
---
.../src/components/settings/RAGSettings.tsx | 55 ++++++++++++++-----
1 file changed, 40 insertions(+), 15 deletions(-)
diff --git a/archon-ui-main/src/components/settings/RAGSettings.tsx b/archon-ui-main/src/components/settings/RAGSettings.tsx
index e472d701dc..7ea3802931 100644
--- a/archon-ui-main/src/components/settings/RAGSettings.tsx
+++ b/archon-ui-main/src/components/settings/RAGSettings.tsx
@@ -85,6 +85,27 @@ const colorStyles: Record = {
grok: 'border-yellow-500 bg-yellow-500/10',
};
+const providerAlertStyles: Record = {
+ openai: 'bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800 text-green-800 dark:text-green-300',
+ google: 'bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800 text-blue-800 dark:text-blue-300',
+ openrouter: 'bg-cyan-50 dark:bg-cyan-900/20 border-cyan-200 dark:border-cyan-800 text-cyan-800 dark:text-cyan-300',
+ ollama: 'bg-purple-50 dark:bg-purple-900/20 border-purple-200 dark:border-purple-800 text-purple-800 dark:text-purple-300',
+ anthropic: 'bg-orange-50 dark:bg-orange-900/20 border-orange-200 dark:border-orange-800 text-orange-800 dark:text-orange-300',
+ grok: 'bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-800 text-yellow-800 dark:text-yellow-300',
+};
+
+const providerAlertMessages: Record = {
+ openai: 'Configure your OpenAI API key in the credentials section to use GPT models.',
+ google: 'Configure your Google API key in the credentials section to use Gemini models.',
+ openrouter: 'Configure your OpenRouter API key in the credentials section to use models.',
+ ollama: 'Configure your Ollama instances in this panel to connect local models.',
+ anthropic: 'Configure your Anthropic API key in the credentials section to use Claude models.',
+ grok: 'Configure your Grok API key in the credentials section to use Grok models.',
+};
+
+const isProviderKey = (value: unknown): value is ProviderKey =>
+ typeof value === 'string' && ['openai', 'google', 'openrouter', 'ollama', 'anthropic', 'grok'].includes(value);
+
interface RAGSettingsProps {
ragSettings: {
MODEL_CHOICE: string;
@@ -739,7 +760,21 @@ export const RAGSettings = ({
default:
return 'missing';
}
- };;
+ };
+
+ const selectedProviderKey = isProviderKey(ragSettings.LLM_PROVIDER)
+ ? (ragSettings.LLM_PROVIDER as ProviderKey)
+ : undefined;
+ const selectedProviderStatus = selectedProviderKey ? getProviderStatus(selectedProviderKey) : undefined;
+ const shouldShowProviderAlert = Boolean(
+ selectedProviderKey && selectedProviderStatus === 'missing'
+ );
+ const providerAlertClassName = shouldShowProviderAlert && selectedProviderKey
+ ? providerAlertStyles[selectedProviderKey]
+ : '';
+ const providerAlertMessage = shouldShowProviderAlert && selectedProviderKey
+ ? providerAlertMessages[selectedProviderKey]
+ : '';
// Test Ollama connectivity when Settings page loads (scenario 4: page load)
// This useEffect is placed after function definitions to ensure access to manualTestConnection
@@ -1253,19 +1288,9 @@ export const RAGSettings = ({
)}
- {ragSettings.LLM_PROVIDER === 'anthropic' && (
-
-
- Configure your Anthropic API key in the credentials section to use Claude models.
-
-
- )}
-
- {ragSettings.LLM_PROVIDER === 'groq' && (
-
-
- Groq provides fast inference with Llama, Mixtral, and Gemma models.
-
+ {shouldShowProviderAlert && (
+
)}
@@ -2027,4 +2052,4 @@ const CustomCheckbox = ({
);
-};
\ No newline at end of file
+};
From c5d5342d5ea706c48441047a1f08e90c88cfcab5 Mon Sep 17 00:00:00 2001
From: Chillbruhhh
Date: Sat, 20 Sep 2025 08:20:28 -0500
Subject: [PATCH 10/28] Updated get_llm_client so missing OpenAI keys
automatically fall back to Ollama (matching existing tests) and so
unsupported providers still raise the legacy ValueError the suite
expects. The fallback now reuses _get_optimal_ollama_instance and rethrows
ValueError(OpenAI API key not found and Ollama fallback failed) when it cant
connect. Adjusted test_code_extraction_source_id.py to accept the new
optional argument on the mocked extractor (and confirm its None when
present).
---
.../server/services/llm_provider_service.py | 70 +++++++------------
.../tests/test_code_extraction_source_id.py | 18 ++---
2 files changed, 36 insertions(+), 52 deletions(-)
diff --git a/python/src/server/services/llm_provider_service.py b/python/src/server/services/llm_provider_service.py
index 5b13c1a85b..d6b50a2574 100644
--- a/python/src/server/services/llm_provider_service.py
+++ b/python/src/server/services/llm_provider_service.py
@@ -380,7 +380,7 @@ async def get_llm_client(
# Comprehensive provider validation with security checks
if not _is_valid_provider(provider_name):
- raise ValueError(f"Provider validation failed: invalid provider '{provider_name}'")
+ raise ValueError(f"Unsupported LLM provider: {provider_name}")
# Validate API key format for security (prevent injection)
if api_key:
@@ -397,53 +397,35 @@ async def get_llm_client(
logger.info(f"Creating LLM client for provider: {safe_provider_name}")
if provider_name == "openai":
- if not api_key:
- # Check if Ollama fallback is explicitly enabled (fail fast principle)
+ if api_key:
+ client = openai.AsyncOpenAI(api_key=api_key)
+ logger.info("OpenAI client created successfully")
+ else:
+ logger.warning("OpenAI API key not found, attempting Ollama fallback")
try:
- enable_fallback = await credential_service.get_credential(
- "ENABLE_OLLAMA_FALLBACK", "false"
+ ollama_base_url = await _get_optimal_ollama_instance(
+ instance_type="embedding" if use_embedding_provider else "chat",
+ use_embedding_provider=use_embedding_provider,
+ base_url_override=base_url,
)
- enable_fallback = enable_fallback.lower() == "true"
- except Exception:
- enable_fallback = False # Default to false for fail-fast behavior
- if enable_fallback:
- logger.warning(
- "OpenAI API key not found, attempting configured Ollama fallback"
+ if not ollama_base_url:
+ raise RuntimeError("No Ollama base URL resolved")
+
+ client = openai.AsyncOpenAI(
+ api_key="ollama",
+ base_url=ollama_base_url,
)
- try:
- # Try to get an optimal Ollama instance for fallback
- ollama_base_url = await _get_optimal_ollama_instance(
- instance_type="embedding" if use_embedding_provider else "chat",
- use_embedding_provider=use_embedding_provider,
- )
- if ollama_base_url:
- logger.info(
- f"Falling back to Ollama instance: {ollama_base_url}"
- )
- client = openai.AsyncOpenAI(
- api_key="ollama",
- base_url=ollama_base_url,
- )
- logger.info(
- f"Ollama fallback client created successfully with base URL: {ollama_base_url}"
- )
- provider_name = "ollama"
- api_key = "ollama"
- base_url = ollama_base_url
- else:
- raise ValueError(
- "No suitable Ollama instance available for fallback"
- )
- except Exception as fallback_error:
- raise ValueError(
- "OpenAI API key not found and Ollama fallback failed"
- ) from fallback_error
- else:
- raise ValueError("OpenAI API key not found")
- else:
- client = openai.AsyncOpenAI(api_key=api_key)
- logger.info("OpenAI client created successfully")
+ logger.info(
+ f"Ollama fallback client created successfully with base URL: {ollama_base_url}"
+ )
+ provider_name = "ollama"
+ api_key = "ollama"
+ base_url = ollama_base_url
+ except Exception as fallback_error:
+ raise ValueError(
+ "OpenAI API key not found and Ollama fallback failed"
+ ) from fallback_error
elif provider_name == "ollama":
# For Ollama, get the optimal instance based on usage
diff --git a/python/tests/test_code_extraction_source_id.py b/python/tests/test_code_extraction_source_id.py
index 7de851f565..05405ee790 100644
--- a/python/tests/test_code_extraction_source_id.py
+++ b/python/tests/test_code_extraction_source_id.py
@@ -104,13 +104,15 @@ async def test_document_storage_passes_source_id(self):
)
# Verify the correct source_id was passed (now with cancellation_check parameter)
- mock_extract.assert_called_once_with(
- crawl_results,
- url_to_full_document,
- source_id, # This should be the third argument
- None,
- None # cancellation_check parameter
- )
+ mock_extract.assert_called_once()
+ args, kwargs = mock_extract.call_args
+ assert args[0] == crawl_results
+ assert args[1] == url_to_full_document
+ assert args[2] == source_id
+ assert args[3] is None
+ assert args[4] is None
+ if len(args) > 5:
+ assert args[5] is None
assert result == 5
@pytest.mark.asyncio
@@ -174,4 +176,4 @@ def test_urlparse_not_imported(self):
import inspect
source = inspect.getsource(module)
assert "from urllib.parse import urlparse" not in source, \
- "Should not import urlparse since we don't extract domain from URL anymore"
\ No newline at end of file
+ "Should not import urlparse since we don't extract domain from URL anymore"
From 2314be7cf1751c8d913ef3bc293ab4693e55ea19 Mon Sep 17 00:00:00 2001
From: Chillbruhhh
Date: Sat, 20 Sep 2025 09:57:51 -0500
Subject: [PATCH 11/28] Resolved a few needed code rabbit suggestion -
Updated the knowledge API key validation to call create_embedding with the
provider argument and removed the hard-coded OpenAI fallback
(python/src/server/api_routes/knowledge_api.py).
- Broadened
embedding provider detection so prefixed OpenRouter/OpenAI model names route
through the correct client (python/src/server/
services/embeddings/embedding_service.py,
python/src/server/services/llm_provider_service.py).
- Removed the duplicate helper definitions from
llm_provider_service.py, eliminating the stray docstring that was causing the
import-time syntax error.
---
python/src/server/api_routes/knowledge_api.py | 93 +++++++-----------
.../services/embeddings/embedding_service.py | 2 +-
.../server/services/llm_provider_service.py | 97 ++++---------------
3 files changed, 52 insertions(+), 140 deletions(-)
diff --git a/python/src/server/api_routes/knowledge_api.py b/python/src/server/api_routes/knowledge_api.py
index 1764dbab4e..1f26dace8f 100644
--- a/python/src/server/api_routes/knowledge_api.py
+++ b/python/src/server/api_routes/knowledge_api.py
@@ -84,66 +84,39 @@ async def _validate_provider_api_key(provider: str = None) -> None:
safe_provider = provider[:20] # Limit length
logger.info(f"π Testing {safe_provider.title()} API key with minimal embedding request...")
- try:
- # Test API key with minimal embedding request - this will fail if key is invalid
- from ..services.embeddings.embedding_service import create_embedding
- test_result = await create_embedding(text="test")
-
- if not test_result:
- logger.error(f"β {provider.title()} API key validation failed - no embedding returned")
- raise HTTPException(
- status_code=401,
- detail={
- "error": f"Invalid {provider.title()} API key",
- "message": f"Please verify your {provider.title()} API key in Settings.",
- "error_type": "authentication_failed",
- "provider": provider
- }
- )
- except Exception as e:
- # If embedding fails due to model incompatibility, try with provider-compatible model
- error_str = str(e).lower()
- if "does not exist" in error_str or "not found" in error_str:
- logger.warning(f"π Embedding model incompatible with {provider.title()}, retrying with provider-compatible model...")
- try:
- # Force provider-compatible embedding model for validation
- from ..services.embeddings.embedding_service import create_embedding
- from ..services.llm_provider_service import get_llm_client
-
- # Use provider-specific compatible embedding model for validation
- compatible_model = "text-embedding-3-small" # OpenAI-compatible model
-
- async with get_llm_client(provider=provider, use_embedding_provider=True) as client:
- response = await client.embeddings.create(
- model=compatible_model,
- input="test"
- )
- if not response.data[0].embedding:
- raise Exception("No embedding returned")
- logger.info(f"β
{provider.title()} API key validation successful with compatible model")
- except Exception as retry_error:
- logger.error(f"β {provider.title()} API key validation failed even with compatible model: {retry_error}")
- raise HTTPException(
- status_code=401,
- detail={
- "error": f"Invalid {provider.title()} API key",
- "message": f"Please verify your {provider.title()} API key in Settings. Original error: {str(e)[:100]}",
- "error_type": "authentication_failed",
- "provider": provider
- }
- )
- else:
- # Other errors (network, auth, etc.) should fail immediately
- logger.error(f"β {provider.title()} API key validation failed: {e}")
- raise HTTPException(
- status_code=401,
- detail={
- "error": f"Invalid {provider.title()} API key",
- "message": f"Please verify your {provider.title()} API key in Settings. Error: {str(e)[:100]}",
- "error_type": "authentication_failed",
- "provider": provider
- }
- )
+ try:
+ # Test API key with minimal embedding request using provider-scoped configuration
+ from ..services.embeddings.embedding_service import create_embedding
+
+ test_result = await create_embedding(text="test", provider=provider)
+
+ if not test_result:
+ logger.error(
+ f"β {provider.title()} API key validation failed - no embedding returned"
+ )
+ raise HTTPException(
+ status_code=401,
+ detail={
+ "error": f"Invalid {provider.title()} API key",
+ "message": f"Please verify your {provider.title()} API key in Settings.",
+ "error_type": "authentication_failed",
+ "provider": provider,
+ },
+ )
+ except Exception as e:
+ logger.error(
+ f"β {provider.title()} API key validation failed: {e}",
+ exc_info=True,
+ )
+ raise HTTPException(
+ status_code=401,
+ detail={
+ "error": f"Invalid {provider.title()} API key",
+ "message": f"Please verify your {provider.title()} API key in Settings. Error: {str(e)[:100]}",
+ "error_type": "authentication_failed",
+ "provider": provider,
+ },
+ )
logger.info(f"β
{provider.title()} API key validation successful")
diff --git a/python/src/server/services/embeddings/embedding_service.py b/python/src/server/services/embeddings/embedding_service.py
index 59d9dbfc42..06a1e8114b 100644
--- a/python/src/server/services/embeddings/embedding_service.py
+++ b/python/src/server/services/embeddings/embedding_service.py
@@ -187,7 +187,7 @@ async def create_embeddings_batch(
if is_google_embedding_model(embedding_model):
embedding_provider = "google"
search_logger.info(f"Routing to Google for embedding model: {embedding_model}")
- elif is_openai_embedding_model(embedding_model):
+ elif is_openai_embedding_model(embedding_model) or "openai/" in embedding_model.lower():
embedding_provider = "openai"
search_logger.info(f"Routing to OpenAI for embedding model: {embedding_model}")
else:
diff --git a/python/src/server/services/llm_provider_service.py b/python/src/server/services/llm_provider_service.py
index d6b50a2574..303e91c9a8 100644
--- a/python/src/server/services/llm_provider_service.py
+++ b/python/src/server/services/llm_provider_service.py
@@ -680,12 +680,27 @@ def is_openai_embedding_model(model: str) -> bool:
if not model:
return False
- openai_models = {
+ model_lower = model.strip().lower()
+
+ # Known OpenAI embeddings
+ base_models = {
"text-embedding-ada-002",
"text-embedding-3-small",
- "text-embedding-3-large"
+ "text-embedding-3-large",
}
- return model.lower() in openai_models
+
+ if model_lower in base_models:
+ return True
+
+ # Strip common vendor prefixes like "openai/" or "openrouter/"
+ for separator in ("/", ":"):
+ if separator in model_lower:
+ candidate = model_lower.split(separator)[-1]
+ if candidate in base_models:
+ return True
+
+ # Fallback substring detection for custom naming conventions
+ return any(base in model_lower for base in base_models)
def is_google_embedding_model(model: str) -> bool:
@@ -968,79 +983,3 @@ async def validate_provider_instance(provider: str, instance_url: str | None = N
"validation_timestamp": time.time()
}
-
-def requires_max_completion_tokens(model_name: str) -> bool:
- """
- Check if a model requires max_completion_tokens instead of max_tokens.
-
- OpenAI changed the parameter for reasoning models (o1, o3, GPT-5 series)
- introduced in September 2024.
-
- Args:
- model_name: The model name to check
-
- Returns:
- True if the model requires max_completion_tokens, False otherwise
- """
- if not model_name:
- return False
-
- # Normalize to lowercase for comparison
- model_lower = model_name.lower()
-
- # Models that require max_completion_tokens (reasoning models)
- reasoning_model_prefixes = [
- "o1", # o1, o1-mini, o1-preview, etc.
- "o3", # o3, o3-mini, etc.
- "gpt-5", # gpt-5, gpt-5-nano, gpt-5-mini, etc.
- ]
-
- # Check for reasoning models (including OpenRouter prefixed models)
- for prefix in reasoning_model_prefixes:
- if model_lower.startswith(prefix):
- return True
- # Also check for OpenRouter format: "openai/gpt-5-nano", "openai/o1-mini", etc.
- if f"openai/{prefix}" in model_lower:
- return True
-
- return False
-
-
-def prepare_chat_completion_params(model: str, params: dict) -> dict:
- """
- Convert parameters for compatibility with reasoning models (GPT-5, o1, o3 series).
-
- OpenAI made several API changes for reasoning models:
- 1. max_tokens β max_completion_tokens
- 2. temperature must be 1.0 (default) - custom values not supported
-
- This ensures compatibility with OpenAI's API changes for newer models
- while maintaining backward compatibility for existing models.
-
- Args:
- model: The model name being used
- params: Dictionary of API parameters
-
- Returns:
- Updated parameters dictionary with correct parameters for the model
- """
- if not model or not params:
- return params
-
- # Make a copy to avoid modifying the original
- updated_params = params.copy()
-
- is_reasoning_model = requires_max_completion_tokens(model)
-
- # Convert max_tokens to max_completion_tokens for reasoning models
- if is_reasoning_model and "max_tokens" in updated_params:
- max_tokens_value = updated_params.pop("max_tokens")
- updated_params["max_completion_tokens"] = max_tokens_value
- logger.debug(f"Converted max_tokens to max_completion_tokens for model {model}")
-
- # Remove custom temperature for reasoning models (they only support default temperature=1.0)
- if is_reasoning_model and "temperature" in updated_params:
- original_temp = updated_params.pop("temperature")
- logger.debug(f"Removed custom temperature {original_temp} for reasoning model {model} (only supports default temperature=1.0)")
-
- return updated_params
From 2878f5d8b5d4e79f2a83664270f50cd1519dba4c Mon Sep 17 00:00:00 2001
From: Chillbruhhh
Date: Sun, 21 Sep 2025 00:37:01 -0500
Subject: [PATCH 12/28] updated via code rabbit PR review, code rabbit in my
IDE found no issues and no nitpicks with the updates! what was done:
Credential service now persists the provider under the uppercase key
LLM_PROVIDER, matching the read path (no new EMBEDDING_PROVIDER usage
introduced).
Embedding batch
creation stops inserting blank strings, logging failures and skipping invalid
items before they ever hit the provider
(python/src/server/services/embeddings/embedding_service.py).
Contextual
embedding prompts use real newline characters everywhereboth when
constructing the batch prompt and when parsing the models response
(python/src/server/services/embeddings/contextual_embedding_service.py).
Embedding provider routing
already recognizes OpenRouter-prefixed OpenAI models via
is_openai_embedding_model; no further change needed there.
Embedding insertion now skips
unsupported vector dimensions instead of forcing them into the 1536-column,
and the backoff loop uses await asyncio.sleep so we no longer block the
event loop (python/src/server/services/storage/code_storage_service.py).
RAG settings props were extended to include LLM_INSTANCE_NAME
and OLLAMA_EMBEDDING_INSTANCE_NAME, and the debug log no longer prints
API-key prefixes (the rest of the TanStack refactor/EMBEDDING_PROVIDER
support remains deferred).
---
.../src/components/settings/RAGSettings.tsx | 2 +
.../src/server/services/credential_service.py | 2 +-
.../contextual_embedding_service.py | 19 ++++----
.../services/embeddings/embedding_service.py | 48 ++++++++++++++-----
.../services/storage/code_storage_service.py | 14 +++---
5 files changed, 55 insertions(+), 30 deletions(-)
diff --git a/archon-ui-main/src/components/settings/RAGSettings.tsx b/archon-ui-main/src/components/settings/RAGSettings.tsx
index 7ea3802931..fba757691f 100644
--- a/archon-ui-main/src/components/settings/RAGSettings.tsx
+++ b/archon-ui-main/src/components/settings/RAGSettings.tsx
@@ -116,8 +116,10 @@ interface RAGSettingsProps {
USE_RERANKING: boolean;
LLM_PROVIDER?: string;
LLM_BASE_URL?: string;
+ LLM_INSTANCE_NAME?: string;
EMBEDDING_MODEL?: string;
OLLAMA_EMBEDDING_URL?: string;
+ OLLAMA_EMBEDDING_INSTANCE_NAME?: string;
// Crawling Performance Settings
CRAWL_BATCH_SIZE?: number;
CRAWL_MAX_CONCURRENT?: number;
diff --git a/python/src/server/services/credential_service.py b/python/src/server/services/credential_service.py
index 0c1173bac9..c55fe1d543 100644
--- a/python/src/server/services/credential_service.py
+++ b/python/src/server/services/credential_service.py
@@ -573,7 +573,7 @@ async def set_active_provider(self, provider: str, service_type: str = "llm") ->
try:
# For now, we'll update the RAG strategy settings
return await self.set_credential(
- "llm_provider",
+ "LLM_PROVIDER",
provider,
category="rag_strategy",
description=f"Active {service_type} provider",
diff --git a/python/src/server/services/embeddings/contextual_embedding_service.py b/python/src/server/services/embeddings/contextual_embedding_service.py
index f9aec36617..afeb4c5299 100644
--- a/python/src/server/services/embeddings/contextual_embedding_service.py
+++ b/python/src/server/services/embeddings/contextual_embedding_service.py
@@ -181,18 +181,19 @@ async def generate_contextual_embeddings_batch(
model_choice = await _get_model_choice(provider)
# Build batch prompt for ALL chunks at once
- batch_prompt = (
- "Process the following chunks and provide contextual information for each:\\n\\n"
- )
+ batch_prompt = "Process the following chunks and provide contextual information for each:\n\n"
for i, (doc, chunk) in enumerate(zip(full_documents, chunks, strict=False)):
# Use only 2000 chars of document context to save tokens
doc_preview = doc[:2000] if len(doc) > 2000 else doc
- batch_prompt += f"CHUNK {i + 1}:\\n"
- batch_prompt += f"\\n{doc_preview}\\n \\n"
- batch_prompt += f"\\n{chunk[:500]}\\n \\n\\n" # Limit chunk preview
+ batch_prompt += f"CHUNK {i + 1}:\n"
+ batch_prompt += f"\n{doc_preview}\n \n"
+ batch_prompt += f"\n{chunk[:500]}\n \n\n" # Limit chunk preview
- batch_prompt += "For each chunk, provide a short succinct context to situate it within the overall document for improving search retrieval. Format your response as:\\nCHUNK 1: [context]\\nCHUNK 2: [context]\\netc."
+ batch_prompt += (
+ "For each chunk, provide a short succinct context to situate it within the overall document for improving search retrieval. "
+ "Format your response as:\nCHUNK 1: [context]\nCHUNK 2: [context]\netc."
+ )
# Make single API call for ALL chunks
# Prepare parameters and convert max_tokens for GPT-5/reasoning models
@@ -215,7 +216,7 @@ async def generate_contextual_embeddings_batch(
response_text = response.choices[0].message.content
# Extract contexts from response
- lines = response_text.strip().split("\\n")
+ lines = response_text.strip().split("\n")
chunk_contexts = {}
for line in lines:
@@ -255,4 +256,4 @@ async def generate_contextual_embeddings_batch(
except Exception as e:
search_logger.error(f"Error in contextual embedding batch: {e}")
# Return non-contextual for all chunks
- return [(chunk, False) for chunk in chunks]
\ No newline at end of file
+ return [(chunk, False) for chunk in chunks]
diff --git a/python/src/server/services/embeddings/embedding_service.py b/python/src/server/services/embeddings/embedding_service.py
index 06a1e8114b..cb14163b51 100644
--- a/python/src/server/services/embeddings/embedding_service.py
+++ b/python/src/server/services/embeddings/embedding_service.py
@@ -152,27 +152,49 @@ async def create_embeddings_batch(
if not texts:
return EmbeddingBatchResult()
+ result = EmbeddingBatchResult()
+
# Validate that all items in texts are strings
validated_texts = []
for i, text in enumerate(texts):
- if not isinstance(text, str):
+ if isinstance(text, str):
+ if text.strip():
+ validated_texts.append(text)
+ else:
+ result.add_failure(
+ text,
+ EmbeddingAPIError("Empty text not allowed"),
+ batch_index=None,
+ )
+ continue
+
+ search_logger.error(
+ f"Invalid text type at index {i}: {type(text)}, value: {text}", exc_info=True
+ )
+ try:
+ converted = str(text)
+ except Exception as conversion_error:
search_logger.error(
- f"Invalid text type at index {i}: {type(text)}, value: {text}", exc_info=True
+ f"Failed to convert text at index {i} to string: {conversion_error}",
+ exc_info=True,
)
- # Try to convert to string
- try:
- validated_texts.append(str(text))
- except Exception as e:
- search_logger.error(
- f"Failed to convert text at index {i} to string: {e}", exc_info=True
- )
- validated_texts.append("") # Use empty string as fallback
+ result.add_failure(
+ repr(text),
+ EmbeddingAPIError("Invalid text type", original_error=conversion_error),
+ batch_index=None,
+ )
+ continue
+
+ if converted.strip():
+ validated_texts.append(converted)
else:
- validated_texts.append(text)
+ result.add_failure(
+ repr(text),
+ EmbeddingAPIError("Empty text not allowed"),
+ batch_index=None,
+ )
texts = validated_texts
-
- result = EmbeddingBatchResult()
threading_service = get_threading_service()
with safe_span(
diff --git a/python/src/server/services/storage/code_storage_service.py b/python/src/server/services/storage/code_storage_service.py
index 4e7895d7e8..0a9c48b8fb 100644
--- a/python/src/server/services/storage/code_storage_service.py
+++ b/python/src/server/services/storage/code_storage_service.py
@@ -1193,10 +1193,12 @@ async def add_code_examples_to_supabase(
elif embedding_dim == 3072:
embedding_column = "embedding_3072"
else:
- # Default to closest supported dimension
- search_logger.warning(f"Unsupported embedding dimension {embedding_dim}, using embedding_1536")
- embedding_column = "embedding_1536"
-
+ # Skip unsupported dimensions to avoid corrupting the schema
+ search_logger.error(
+ f"Unsupported embedding dimension {embedding_dim}; skipping record to prevent column mismatch"
+ )
+ continue
+
batch_data.append({
"url": urls[idx],
"chunk_number": chunk_numbers[idx],
@@ -1229,9 +1231,7 @@ async def add_code_examples_to_supabase(
f"Error inserting batch into Supabase (attempt {retry + 1}/{max_retries}): {e}"
)
search_logger.info(f"Retrying in {retry_delay} seconds...")
- import time
-
- time.sleep(retry_delay)
+ await asyncio.sleep(retry_delay)
retry_delay *= 2 # Exponential backoff
else:
# Final attempt failed
From 6f15225aefe31278d58f9c2d7de143ce27340bda Mon Sep 17 00:00:00 2001
From: Chillbruhhh
Date: Sun, 21 Sep 2025 01:14:53 -0500
Subject: [PATCH 13/28] test fix
---
.../services/embeddings/embedding_service.py | 20 ++-----------------
1 file changed, 2 insertions(+), 18 deletions(-)
diff --git a/python/src/server/services/embeddings/embedding_service.py b/python/src/server/services/embeddings/embedding_service.py
index cb14163b51..4f825f1dc9 100644
--- a/python/src/server/services/embeddings/embedding_service.py
+++ b/python/src/server/services/embeddings/embedding_service.py
@@ -158,14 +158,7 @@ async def create_embeddings_batch(
validated_texts = []
for i, text in enumerate(texts):
if isinstance(text, str):
- if text.strip():
- validated_texts.append(text)
- else:
- result.add_failure(
- text,
- EmbeddingAPIError("Empty text not allowed"),
- batch_index=None,
- )
+ validated_texts.append(text)
continue
search_logger.error(
@@ -173,6 +166,7 @@ async def create_embeddings_batch(
)
try:
converted = str(text)
+ validated_texts.append(converted)
except Exception as conversion_error:
search_logger.error(
f"Failed to convert text at index {i} to string: {conversion_error}",
@@ -183,16 +177,6 @@ async def create_embeddings_batch(
EmbeddingAPIError("Invalid text type", original_error=conversion_error),
batch_index=None,
)
- continue
-
- if converted.strip():
- validated_texts.append(converted)
- else:
- result.add_failure(
- repr(text),
- EmbeddingAPIError("Empty text not allowed"),
- batch_index=None,
- )
texts = validated_texts
threading_service = get_threading_service()
From dca839df5abd10cc0028194dcdb12c0895b7d8c1 Mon Sep 17 00:00:00 2001
From: Chillbruhhh
Date: Sun, 21 Sep 2025 06:01:23 -0500
Subject: [PATCH 14/28] enhanced Openrouters parsing logic to automatically
detect reasoning models and parse regardless of json output or not. this
commit creates a robust way for archons parsing to work throughly with
openrouter automatically, regardless of the model youre using, to ensure
proper functionality with out breaking any generation capabilities!
---
.../contextual_embedding_service.py | 21 +-
.../server/services/llm_provider_service.py | 317 +++++++++++-
.../services/source_management_service.py | 35 +-
.../services/storage/code_storage_service.py | 474 ++++++++++--------
4 files changed, 600 insertions(+), 247 deletions(-)
diff --git a/python/src/server/services/embeddings/contextual_embedding_service.py b/python/src/server/services/embeddings/contextual_embedding_service.py
index afeb4c5299..29b36395c6 100644
--- a/python/src/server/services/embeddings/contextual_embedding_service.py
+++ b/python/src/server/services/embeddings/contextual_embedding_service.py
@@ -10,9 +10,14 @@
import openai
from ...config.logfire_config import search_logger
-from ..llm_provider_service import get_llm_client, prepare_chat_completion_params, requires_max_completion_tokens
-from ..threading_service import get_threading_service
from ..credential_service import credential_service
+from ..llm_provider_service import (
+ extract_message_text,
+ get_llm_client,
+ prepare_chat_completion_params,
+ requires_max_completion_tokens,
+)
+from ..threading_service import get_threading_service
async def generate_contextual_embedding(
@@ -80,7 +85,9 @@ async def generate_contextual_embedding(
final_params = prepare_chat_completion_params(model, params)
response = await client.chat.completions.create(**final_params)
- context = response.choices[0].message.content.strip()
+ choice = response.choices[0] if response.choices else None
+ context, _, _ = extract_message_text(choice)
+ context = context.strip()
contextual_text = f"{context}\n---\n{chunk}"
return contextual_text, True
@@ -213,7 +220,13 @@ async def generate_contextual_embeddings_batch(
response = await client.chat.completions.create(**final_batch_params)
# Parse response
- response_text = response.choices[0].message.content
+ choice = response.choices[0] if response.choices else None
+ response_text, _, _ = extract_message_text(choice)
+ if not response_text:
+ search_logger.error(
+ "Empty response from LLM when generating contextual embeddings batch"
+ )
+ return [(chunk, False) for chunk in chunks]
# Extract contexts from response
lines = response_text.strip().split("\n")
diff --git a/python/src/server/services/llm_provider_service.py b/python/src/server/services/llm_provider_service.py
index 303e91c9a8..a50a9576d3 100644
--- a/python/src/server/services/llm_provider_service.py
+++ b/python/src/server/services/llm_provider_service.py
@@ -223,7 +223,7 @@ def get_cache_stats() -> dict[str, Any]:
}
# Analyze cache entries
- for key, (value, timestamp, checksum) in _settings_cache.items():
+ for _key, (_value, timestamp, _checksum) in _settings_cache.items():
age = current_time - timestamp
if age < _CACHE_TTL_SECONDS:
stats["fresh_entries"] += 1
@@ -796,40 +796,300 @@ def get_supported_embedding_models(provider: str) -> list[str]:
return openai_models
-def requires_max_completion_tokens(model_name: str) -> bool:
+def is_reasoning_model(model_name: str) -> bool:
"""
- Check if a model requires max_completion_tokens instead of max_tokens.
-
- OpenAI changed the parameter for reasoning models (o1, o3, GPT-5 series)
- introduced in September 2024.
-
- Args:
- model_name: The model name to check
+ Unified check for reasoning models across providers.
- Returns:
- True if the model requires max_completion_tokens, False otherwise
+ Normalizes vendor prefixes (openai/, openrouter/, x-ai/, deepseek/) before checking
+ known reasoning families (OpenAI GPT-5, o1, o3; xAI Grok; DeepSeek-R; etc.).
"""
if not model_name:
return False
model_lower = model_name.lower()
- # GPT-5 series (all variants)
- if "gpt-5" in model_lower:
- return True
+ # Normalize vendor prefixes (e.g., openai/gpt-5-nano, openrouter/x-ai/grok-4)
+ if "/" in model_lower:
+ parts = model_lower.split("/")
+ # Drop known vendor prefixes while keeping the final model identifier
+ known_prefixes = {"openai", "openrouter", "x-ai", "deepseek", "anthropic"}
+ filtered_parts = [part for part in parts if part not in known_prefixes]
+ if filtered_parts:
+ model_lower = filtered_parts[-1]
+ else:
+ model_lower = parts[-1]
+
+ if ":" in model_lower:
+ model_lower = model_lower.split(":", 1)[-1]
+
+ reasoning_prefixes = (
+ "gpt-5",
+ "o1",
+ "o3",
+ "o4",
+ "grok",
+ "deepseek-r",
+ "deepseek-reasoner",
+ "deepseek-chat-r",
+ )
+
+ return model_lower.startswith(reasoning_prefixes)
+
+
+def _extract_reasoning_strings(value: Any) -> list[str]:
+ """Convert reasoning payload fragments into plain-text strings."""
+
+ if value is None:
+ return []
+
+ if isinstance(value, str):
+ text = value.strip()
+ return [text] if text else []
+
+ if isinstance(value, (list, tuple, set)):
+ collected: list[str] = []
+ for item in value:
+ collected.extend(_extract_reasoning_strings(item))
+ return collected
+
+ if isinstance(value, dict):
+ candidates = []
+ for key in ("text", "summary", "content", "message", "value"):
+ if value.get(key):
+ candidates.extend(_extract_reasoning_strings(value[key]))
+ # Some providers nest reasoning parts under "parts"
+ if value.get("parts"):
+ candidates.extend(_extract_reasoning_strings(value["parts"]))
+ return candidates
+
+ # Handle pydantic-style objects with attributes
+ for attr in ("text", "summary", "content", "value"):
+ if hasattr(value, attr):
+ attr_value = getattr(value, attr)
+ if attr_value:
+ return _extract_reasoning_strings(attr_value)
+
+ return []
+
+
+def _get_message_attr(message: Any, attribute: str) -> Any:
+ """Safely access message attributes that may be dict keys or properties."""
+
+ if hasattr(message, attribute):
+ return getattr(message, attribute)
+ if isinstance(message, dict):
+ return message.get(attribute)
+ return None
+
+
+def extract_message_text(choice: Any) -> tuple[str, str, bool]:
+ """Extract primary content and reasoning text from a chat completion choice."""
+
+ if not choice:
+ return "", "", False
+
+ message = _get_message_attr(choice, "message")
+ if message is None:
+ return "", "", False
+
+ raw_content = _get_message_attr(message, "content")
+ content_text = raw_content.strip() if isinstance(raw_content, str) else ""
+
+ reasoning_fragments: list[str] = []
+ for attr in ("reasoning", "reasoning_details", "reasoning_content"):
+ reasoning_value = _get_message_attr(message, attr)
+ if reasoning_value:
+ reasoning_fragments.extend(_extract_reasoning_strings(reasoning_value))
+
+ reasoning_text = "\n".join(fragment for fragment in reasoning_fragments if fragment)
+ reasoning_text = reasoning_text.strip()
+
+ # If content looks like reasoning text but no reasoning field, detect it
+ if content_text and not reasoning_text and _is_reasoning_text(content_text):
+ reasoning_text = content_text
+ # Try to extract structured data from reasoning text
+ extracted_json = extract_json_from_reasoning(content_text)
+ if extracted_json:
+ content_text = extracted_json
+ else:
+ content_text = ""
+
+ if not content_text and reasoning_text:
+ content_text = reasoning_text
+
+ has_reasoning = bool(reasoning_text)
+
+ return content_text, reasoning_text, has_reasoning
+
+
+def _is_reasoning_text(text: str) -> bool:
+ """Detect if text appears to be reasoning/thinking output rather than structured content."""
+ if not text or len(text) < 10:
+ return False
+
+ text_lower = text.lower().strip()
+
+ # Common reasoning text patterns
+ reasoning_indicators = [
+ "okay, let's see", "let me think", "first, i need to", "looking at this",
+ "step by step", "analyzing", "breaking this down", "considering",
+ "let me work through", "i should", "thinking about", "examining"
+ ]
+
+ return any(indicator in text_lower for indicator in reasoning_indicators)
+
+
+def extract_json_from_reasoning(reasoning_text: str, context_code: str = "", language: str = "") -> str:
+ """Extract JSON content from reasoning text, with synthesis fallback."""
+ if not reasoning_text:
+ return ""
+
+ import json
+ import re
+
+ # Try to find JSON blocks in markdown
+ json_block_pattern = r'```(?:json)?\s*(\{.*?\})\s*```'
+ json_matches = re.findall(json_block_pattern, reasoning_text, re.DOTALL | re.IGNORECASE)
+
+ for match in json_matches:
+ try:
+ # Validate it's proper JSON
+ json.loads(match.strip())
+ return match.strip()
+ except json.JSONDecodeError:
+ continue
+
+ # Try to find standalone JSON objects
+ json_pattern = r'\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}'
+ json_matches = re.findall(json_pattern, reasoning_text, re.DOTALL)
+
+ for match in json_matches:
+ try:
+ parsed = json.loads(match.strip())
+ # Ensure it has expected structure
+ if isinstance(parsed, dict) and any(key in parsed for key in ["example_name", "summary", "name", "title"]):
+ return match.strip()
+ except json.JSONDecodeError:
+ continue
+
+ # If no JSON found, synthesize from reasoning content
+ return synthesize_json_from_reasoning(reasoning_text, context_code, language)
+
+
+def synthesize_json_from_reasoning(reasoning_text: str, context_code: str = "", language: str = "") -> str:
+ """Generate JSON structure from reasoning text when no JSON is found."""
+ if not reasoning_text and not context_code:
+ return ""
- # o1 and o3 series (reasoning models)
- reasoning_patterns = [
- "o1-mini", "o1-preview", "o1-pro",
- "o3-mini", "o3-medium", "o3-large", "o3-pro",
- "o1", "o3" # Base patterns
+ import json
+ import re
+
+ # Extract key concepts and actions from reasoning text and code context
+ text_lower = reasoning_text.lower() if reasoning_text else ""
+ code_lower = context_code.lower() if context_code else ""
+ combined_text = f"{text_lower} {code_lower}"
+
+ # Common action patterns in reasoning text and code
+ action_patterns = [
+ (r'\b(?:parse|parsing|parsed)\b', 'Parse'),
+ (r'\b(?:create|creating|created)\b', 'Create'),
+ (r'\b(?:analyze|analyzing|analyzed)\b', 'Analyze'),
+ (r'\b(?:extract|extracting|extracted)\b', 'Extract'),
+ (r'\b(?:generate|generating|generated)\b', 'Generate'),
+ (r'\b(?:process|processing|processed)\b', 'Process'),
+ (r'\b(?:load|loading|loaded)\b', 'Load'),
+ (r'\b(?:handle|handling|handled)\b', 'Handle'),
+ (r'\b(?:manage|managing|managed)\b', 'Manage'),
+ (r'\b(?:build|building|built)\b', 'Build'),
+ (r'\b(?:define|defining|defined)\b', 'Define'),
+ (r'\b(?:implement|implementing|implemented)\b', 'Implement'),
+ (r'\b(?:fetch|fetching|fetched)\b', 'Fetch'),
+ (r'\b(?:connect|connecting|connected)\b', 'Connect'),
+ (r'\b(?:validate|validating|validated)\b', 'Validate'),
]
- for pattern in reasoning_patterns:
- if pattern in model_lower:
- return True
+ # Technology/concept patterns
+ tech_patterns = [
+ (r'\bjson\b', 'JSON'),
+ (r'\bapi\b', 'API'),
+ (r'\bfile\b', 'File'),
+ (r'\bdata\b', 'Data'),
+ (r'\bcode\b', 'Code'),
+ (r'\btext\b', 'Text'),
+ (r'\bcontent\b', 'Content'),
+ (r'\bresponse\b', 'Response'),
+ (r'\brequest\b', 'Request'),
+ (r'\bconfig\b', 'Config'),
+ (r'\bllm\b', 'LLM'),
+ (r'\bmodel\b', 'Model'),
+ (r'\bexample\b', 'Example'),
+ (r'\bcontext\b', 'Context'),
+ (r'\basync\b', 'Async'),
+ (r'\bfunction\b', 'Function'),
+ (r'\bclass\b', 'Class'),
+ (r'\bprint\b', 'Output'),
+ (r'\breturn\b', 'Return'),
+ ]
+
+ # Extract actions and technologies from combined text
+ detected_actions = []
+ detected_techs = []
+
+ for pattern, action in action_patterns:
+ if re.search(pattern, combined_text):
+ detected_actions.append(action)
+
+ for pattern, tech in tech_patterns:
+ if re.search(pattern, combined_text):
+ detected_techs.append(tech)
+
+ # Generate example name
+ if detected_actions and detected_techs:
+ example_name = f"{detected_actions[0]} {detected_techs[0]}"
+ elif detected_actions:
+ example_name = f"{detected_actions[0]} Code"
+ elif detected_techs:
+ example_name = f"Handle {detected_techs[0]}"
+ elif language:
+ example_name = f"Process {language.title()}"
+ else:
+ example_name = "Code Processing"
+
+ # Limit to 4 words as per requirements
+ example_name_words = example_name.split()
+ if len(example_name_words) > 4:
+ example_name = " ".join(example_name_words[:4])
+
+ # Generate summary from reasoning content
+ reasoning_lines = reasoning_text.split('\n')
+ meaningful_lines = [line.strip() for line in reasoning_lines if line.strip() and len(line.strip()) > 10]
+
+ if meaningful_lines:
+ # Take first meaningful sentence for summary base
+ first_line = meaningful_lines[0]
+ if len(first_line) > 100:
+ first_line = first_line[:100] + "..."
+
+ # Create contextual summary
+ if context_code and any(tech in text_lower for tech, _ in tech_patterns):
+ summary = f"This code demonstrates {detected_techs[0].lower() if detected_techs else 'data'} processing functionality. {first_line}"
+ else:
+ summary = f"Code example showing {detected_actions[0].lower() if detected_actions else 'processing'} operations. {first_line}"
+ else:
+ # Fallback summary
+ summary = f"Code example demonstrating {example_name.lower()} functionality for {language or 'general'} development."
+
+ # Ensure summary is not too long
+ if len(summary) > 300:
+ summary = summary[:297] + "..."
+
+ # Create JSON structure
+ result = {
+ "example_name": example_name,
+ "summary": summary
+ }
- return False
+ return json.dumps(result)
def prepare_chat_completion_params(model: str, params: dict) -> dict:
@@ -856,16 +1116,16 @@ def prepare_chat_completion_params(model: str, params: dict) -> dict:
# Make a copy to avoid modifying the original
updated_params = params.copy()
- is_reasoning_model = requires_max_completion_tokens(model)
+ reasoning_model = is_reasoning_model(model)
# Convert max_tokens to max_completion_tokens for reasoning models
- if is_reasoning_model and "max_tokens" in updated_params:
+ if reasoning_model and "max_tokens" in updated_params:
max_tokens_value = updated_params.pop("max_tokens")
updated_params["max_completion_tokens"] = max_tokens_value
logger.debug(f"Converted max_tokens to max_completion_tokens for model {model}")
# Remove custom temperature for reasoning models (they only support default temperature=1.0)
- if is_reasoning_model and "temperature" in updated_params:
+ if reasoning_model and "temperature" in updated_params:
original_temp = updated_params.pop("temperature")
logger.debug(f"Removed custom temperature {original_temp} for reasoning model {model} (only supports default temperature=1.0)")
@@ -983,3 +1243,8 @@ async def validate_provider_instance(provider: str, instance_url: str | None = N
"validation_timestamp": time.time()
}
+
+
+def requires_max_completion_tokens(model_name: str) -> bool:
+ """Backward compatible alias for previous API."""
+ return is_reasoning_model(model_name)
diff --git a/python/src/server/services/source_management_service.py b/python/src/server/services/source_management_service.py
index c7bcdb66c5..f8a2702317 100644
--- a/python/src/server/services/source_management_service.py
+++ b/python/src/server/services/source_management_service.py
@@ -11,7 +11,7 @@
from ..config.logfire_config import get_logger, search_logger
from .client_manager import get_supabase_client
-from .llm_provider_service import get_llm_client
+from .llm_provider_service import extract_message_text, get_llm_client
logger = get_logger(__name__)
@@ -72,20 +72,21 @@ async def extract_source_summary(
)
# Extract the generated summary with proper error handling
- if not response or not response.choices or len(response.choices) == 0:
- search_logger.error(f"Empty or invalid response from LLM for {source_id}")
- return default_summary
-
- message_content = response.choices[0].message.content
- if message_content is None:
- search_logger.error(f"LLM returned None content for {source_id}")
- return default_summary
-
- summary = message_content.strip()
-
- # Ensure the summary is not too long
- if len(summary) > max_length:
- summary = summary[:max_length] + "..."
+ if not response or not response.choices or len(response.choices) == 0:
+ search_logger.error(f"Empty or invalid response from LLM for {source_id}")
+ return default_summary
+
+ choice = response.choices[0]
+ summary_text, _, _ = extract_message_text(choice)
+ if not summary_text:
+ search_logger.error(f"LLM returned None content for {source_id}")
+ return default_summary
+
+ summary = summary_text.strip()
+
+ # Ensure the summary is not too long
+ if len(summary) > max_length:
+ summary = summary[:max_length] + "..."
return summary
@@ -187,7 +188,9 @@ async def generate_source_title_and_metadata(
],
)
- generated_title = response.choices[0].message.content.strip()
+ choice = response.choices[0]
+ generated_title, _, _ = extract_message_text(choice)
+ generated_title = generated_title.strip()
# Clean up the title
generated_title = generated_title.strip("\"'")
if len(generated_title) < 50: # Sanity check
diff --git a/python/src/server/services/storage/code_storage_service.py b/python/src/server/services/storage/code_storage_service.py
index 0a9c48b8fb..a993bc70b8 100644
--- a/python/src/server/services/storage/code_storage_service.py
+++ b/python/src/server/services/storage/code_storage_service.py
@@ -6,9 +6,9 @@
import asyncio
import json
-import time
import os
import re
+import time
from collections import defaultdict, deque
from collections.abc import Callable
from difflib import SequenceMatcher
@@ -18,26 +18,19 @@
from supabase import Client
from ...config.logfire_config import search_logger
+from ..credential_service import credential_service
from ..embeddings.contextual_embedding_service import generate_contextual_embeddings_batch
from ..embeddings.embedding_service import create_embeddings_batch
-from ..llm_provider_service import get_llm_client, prepare_chat_completion_params, requires_max_completion_tokens
-from ..credential_service import credential_service
-
-
-def _is_reasoning_model(model: str) -> bool:
- """
- Check if a model is a reasoning model that may return empty responses.
+from ..llm_provider_service import (
+ extract_json_from_reasoning,
+ extract_message_text,
+ get_llm_client,
+ prepare_chat_completion_params,
+ synthesize_json_from_reasoning,
+)
- Args:
- model: The model identifier
-
- Returns:
- True if the model is a reasoning model (GPT-5, o1, o3 series)
- """
- return requires_max_completion_tokens(model)
-
-def _extract_json_payload(raw_response: str) -> str:
+def _extract_json_payload(raw_response: str, context_code: str = "", language: str = "") -> str:
"""Return the best-effort JSON object from an LLM response."""
if not raw_response:
@@ -45,6 +38,20 @@ def _extract_json_payload(raw_response: str) -> str:
cleaned = raw_response.strip()
+ # Check if this looks like reasoning text first
+ if _is_reasoning_text_response(cleaned):
+ # Try intelligent extraction from reasoning text with context
+ extracted = extract_json_from_reasoning(cleaned, context_code, language)
+ if extracted:
+ return extracted
+ # extract_json_from_reasoning may return nothing; synthesize a fallback JSON if so\
+ fallback_json = synthesize_json_from_reasoning("", context_code, language)
+ if fallback_json:
+ return fallback_json
+ # If all else fails, return a minimal valid JSON object to avoid downstream errors
+ return '{"example_name": "Code Example", "summary": "Code example extracted from context."}'
+
+
if cleaned.startswith("```"):
lines = cleaned.splitlines()
# Drop opening fence
@@ -63,49 +70,25 @@ def _extract_json_payload(raw_response: str) -> str:
return cleaned.strip()
-def _supports_response_format(provider: str, model: str) -> bool:
- """
- Determine if a specific provider/model combination supports response_format.
-
- Args:
- provider: The LLM provider name
- model: The model identifier
+REASONING_STARTERS = [
+ "okay, let's see", "okay, let me", "let me think", "first, i need to", "looking at this",
+ "i need to", "analyzing", "let me work through", "thinking about", "let me see"
+]
- Returns:
- True if the model supports structured JSON output via response_format
- """
- if not provider:
- return True # Default to supporting it
-
- provider = provider.lower()
-
- if provider == "openai":
- return True # OpenAI models generally support response_format
- elif provider == "openrouter":
- # OpenRouter: "OpenAI models, Nitro models, and some others" support it
- model_lower = model.lower()
-
- # Known compatible model patterns on OpenRouter
- compatible_patterns = [
- "openai/", # OpenAI models on OpenRouter
- "gpt-", # GPT models
- "nitro/", # Nitro models
- "deepseek/", # DeepSeek models often support JSON
- "google/", # Some Google models support it
- ]
-
- for pattern in compatible_patterns:
- if pattern in model_lower:
- search_logger.debug(f"Model {model} supports response_format (pattern: {pattern})")
- return True
-
- search_logger.debug(f"Model {model} may not support response_format, skipping")
- return False
- else:
- # Conservative approach for other providers
+def _is_reasoning_text_response(text: str) -> bool:
+ """Detect if response is reasoning text rather than direct JSON."""
+ if not text or len(text) < 20:
return False
+ text_lower = text.lower().strip()
+
+ # Check if it's clearly not JSON (starts with reasoning text)
+ starts_with_reasoning = any(text_lower.startswith(starter) for starter in REASONING_STARTERS)
+ # Check if it lacks immediate JSON structure
+ lacks_immediate_json = not text_lower.lstrip().startswith('{')
+
+ return starts_with_reasoning or (lacks_immediate_json and any(pattern in text_lower for pattern in REASONING_STARTERS))
async def _get_model_choice() -> str:
"""Get MODEL_CHOICE with provider-aware defaults from centralized service."""
try:
@@ -603,7 +586,7 @@ def generate_code_example_summary(
A dictionary with 'summary' and 'example_name'
"""
import asyncio
-
+
# Run the async version in the current thread
return asyncio.run(_generate_code_example_summary_async(code, context_before, context_after, language, provider))
@@ -614,8 +597,7 @@ async def _generate_code_example_summary_async(
"""
Async version of generate_code_example_summary using unified LLM provider service.
"""
- from ..llm_provider_service import get_llm_client
-
+
# Get model choice from credential service (RAG setting)
model_choice = await _get_model_choice()
@@ -629,8 +611,8 @@ async def _generate_code_example_summary_async(
search_logger.warning(f"Failed to get provider from credential service: {e}, defaulting to openai")
provider = "openai"
- # Create the prompt
- prompt = f"""
+ # Create the prompt variants: base prompt, guarded prompt (JSON reminder), and strict prompt for retries
+ base_prompt = f"""
{context_before[-500:] if len(context_before) > 500 else context_before}
@@ -654,6 +636,16 @@ async def _generate_code_example_summary_async(
"summary": "2-3 sentence description of what the code demonstrates"
}}
"""
+ guard_prompt = (
+ base_prompt
+ + "\n\nImportant: Respond with a valid JSON object that exactly matches the keys "
+ '{"example_name": string, "summary": string}. Do not include commentary, '
+ "markdown fences, or reasoning notes."
+ )
+ strict_prompt = (
+ guard_prompt
+ + "\n\nSecond attempt enforcement: Return JSON only with the exact schema. No additional text or reasoning content."
+ )
try:
# Use unified LLM provider service
@@ -661,153 +653,207 @@ async def _generate_code_example_summary_async(
search_logger.info(
f"Generating summary for {hash(code) & 0xffffff:06x} using model: {model_choice}"
)
-
- request_params = {
- "model": model_choice,
- "messages": [
- {
- "role": "system",
- "content": "You are a helpful assistant that analyzes code examples and provides JSON responses with example names and summaries.",
- },
- {"role": "user", "content": prompt},
- ],
- "max_tokens": 2000 if (_is_reasoning_model(model_choice) or provider == "grok") else 500, # 2000 tokens for both reasoning models (GPT-5) and Grok for complex reasoning
- "temperature": 0.3,
- }
- # Try to use response_format, but handle gracefully if not supported
- # Note: Grok and reasoning models (GPT-5, o1, o3) don't work well with response_format
- supports_response_format = (
- provider in ["openai", "google", "anthropic"] or
- (provider == "openrouter" and model_choice.startswith("openai/"))
+ provider_lower = provider.lower()
+ is_grok_model = (provider_lower == "grok") or ("grok" in model_choice.lower())
+
+ supports_response_format_base = (
+ provider_lower in {"openai", "google", "anthropic"}
+ or (provider_lower == "openrouter" and model_choice.startswith("openai/"))
)
- # Exclude reasoning models from using response_format
- if supports_response_format and not _is_reasoning_model(model_choice):
- request_params["response_format"] = {"type": "json_object"}
-
- # Grok-specific parameter validation and filtering
- if provider == "grok":
- # Remove any parameters that Grok reasoning models don't support
- # Based on xAI docs: presencePenalty, frequencyPenalty, stop are not supported
- unsupported_params = ["presence_penalty", "frequency_penalty", "stop", "reasoning_effort"]
- for param in unsupported_params:
- if param in request_params:
- removed_value = request_params.pop(param)
- search_logger.warning(f"Removed unsupported Grok parameter '{param}': {removed_value}")
-
- # Validate that we're using supported parameters only
- supported_params = ["model", "messages", "max_tokens", "temperature", "response_format", "stream", "tools", "tool_choice"]
- for param in request_params:
- if param not in supported_params:
- search_logger.warning(f"Parameter '{param}' may not be supported by Grok reasoning models")
-
- # Enhanced debugging for Grok provider
- # Implement retry logic for Grok and reasoning models (GPT-5, o1, o3) empty responses
- is_reasoning = _is_reasoning_model(model_choice)
-
- start_time = time.time() # Initialize for all models
- if provider == "grok" or is_reasoning:
- model_type = "Grok" if provider == "grok" else f"reasoning model ({model_choice})"
- search_logger.debug(f"{model_type} request params: {request_params}")
- search_logger.debug(f"{model_type} prompt length: {len(prompt)} characters")
- search_logger.debug(f"{model_type} prompt preview: {prompt[:200]}...")
-
- max_retries = 3 if (provider == "grok" or is_reasoning) else 1
- retry_delay = 1.0 # Start with 1 second delay
- failure_reasons = [] # Track failure reasons for circuit breaker analysis
-
- for attempt in range(max_retries):
- try:
- if (provider == "grok" or is_reasoning) and attempt > 0:
- model_type = "Grok" if provider == "grok" else f"reasoning model ({model_choice})"
- search_logger.info(f"{model_type} retry attempt {attempt + 1}/{max_retries} after {retry_delay:.1f}s delay")
- await asyncio.sleep(retry_delay)
- elif is_reasoning and attempt == 0:
- # Small delay for reasoning models on first attempt to help with cold start
- search_logger.debug(f"reasoning model ({model_choice}) first attempt - adding 0.5s delay for cold start")
- await asyncio.sleep(0.5)
-
- # Convert max_tokens to max_completion_tokens for GPT-5/reasoning models
- final_params = prepare_chat_completion_params(model_choice, request_params)
- response = await client.chat.completions.create(**final_params)
-
- # Check for empty response - handle Grok reasoning models
- message = response.choices[0].message if response.choices else None
- response_content = None
-
- # Enhanced debugging for Grok and reasoning models - log both content fields
- if (provider == "grok" or is_reasoning) and message:
- content_preview = message.content[:100] if message.content else "None"
- reasoning_preview = getattr(message, 'reasoning_content', 'N/A')[:100] if hasattr(message, 'reasoning_content') and message.reasoning_content else "None"
- model_type = "Grok" if provider == "grok" else f"reasoning model ({model_choice})"
-
- # Additional debugging for first attempt failures
- finish_reason = getattr(response.choices[0], 'finish_reason', 'unknown') if response.choices else 'no_choices'
- usage_info = getattr(response, 'usage', None)
- if usage_info:
- completion_tokens = getattr(usage_info, 'completion_tokens', 0)
- reasoning_tokens = getattr(getattr(usage_info, 'completion_tokens_details', None), 'reasoning_tokens', 0) if hasattr(usage_info, 'completion_tokens_details') else 0
- search_logger.debug(f"{model_type} attempt {attempt + 1} - finish_reason: {finish_reason}, completion_tokens: {completion_tokens}, reasoning_tokens: {reasoning_tokens}")
+
+ last_response_obj = None
+ last_elapsed_time = None
+ last_response_content = ""
+ last_json_error: json.JSONDecodeError | None = None
+
+ for enforce_json, current_prompt in ((False, guard_prompt), (True, strict_prompt)):
+ request_params = {
+ "model": model_choice,
+ "messages": [
+ {
+ "role": "system",
+ "content": "You are a helpful assistant that analyzes code examples and provides JSON responses with example names and summaries.",
+ },
+ {"role": "user", "content": current_prompt},
+ ],
+ "max_tokens": 2000,
+ "temperature": 0.3,
+ }
+
+ should_use_response_format = False
+ if enforce_json:
+ if not is_grok_model and (supports_response_format_base or provider_lower == "openrouter"):
+ should_use_response_format = True
+ else:
+ if supports_response_format_base:
+ should_use_response_format = True
+
+ if should_use_response_format:
+ request_params["response_format"] = {"type": "json_object"}
+
+ if is_grok_model:
+ unsupported_params = ["presence_penalty", "frequency_penalty", "stop", "reasoning_effort"]
+ for param in unsupported_params:
+ if param in request_params:
+ removed_value = request_params.pop(param)
+ search_logger.warning(f"Removed unsupported Grok parameter '{param}': {removed_value}")
+
+ supported_params = ["model", "messages", "max_tokens", "temperature", "response_format", "stream", "tools", "tool_choice"]
+ for param in list(request_params.keys()):
+ if param not in supported_params:
+ search_logger.warning(f"Parameter '{param}' may not be supported by Grok reasoning models")
+
+ start_time = time.time()
+ max_retries = 3 if is_grok_model else 1
+ retry_delay = 1.0
+ response_content_local = ""
+ reasoning_text_local = ""
+ json_error_occurred = False
+
+ for attempt in range(max_retries):
+ try:
+ if is_grok_model and attempt > 0:
+ search_logger.info(f"Grok retry attempt {attempt + 1}/{max_retries} after {retry_delay:.1f}s delay")
+ await asyncio.sleep(retry_delay)
+
+ final_params = prepare_chat_completion_params(model_choice, request_params)
+ response = await client.chat.completions.create(**final_params)
+ last_response_obj = response
+
+ choice = response.choices[0] if response.choices else None
+ message = choice.message if choice and hasattr(choice, "message") else None
+ response_content_local = ""
+ reasoning_text_local = ""
+
+ if choice:
+ response_content_local, reasoning_text_local, _ = extract_message_text(choice)
+
+ # Enhanced logging for response analysis
+ if message and reasoning_text_local:
+ content_preview = response_content_local[:100] if response_content_local else "None"
+ reasoning_preview = reasoning_text_local[:100] if reasoning_text_local else "None"
+ search_logger.debug(
+ f"Response has reasoning content - content: '{content_preview}', reasoning: '{reasoning_preview}'"
+ )
+
+ if response_content_local:
+ last_response_content = response_content_local.strip()
+
+ # Pre-validate response before processing
+ if len(last_response_content) < 20 or (len(last_response_content) < 50 and not last_response_content.strip().startswith('{')):
+ # Very minimal response - likely "Okay\nOkay" type
+ search_logger.debug(f"Minimal response detected: {repr(last_response_content)}")
+ # Generate fallback directly from context
+ fallback_json = synthesize_json_from_reasoning("", code, language)
+ if fallback_json:
+ try:
+ result = json.loads(fallback_json)
+ final_result = {
+ "example_name": result.get("example_name", f"Code Example{f' ({language})' if language else ''}"),
+ "summary": result.get("summary", "Code example for demonstration purposes."),
+ }
+ search_logger.info(f"Generated fallback summary from context - Name: '{final_result['example_name']}', Summary length: {len(final_result['summary'])}")
+ return final_result
+ except json.JSONDecodeError:
+ pass # Continue to normal error handling
+ else:
+ # Even synthesis failed - provide hardcoded fallback for minimal responses
+ final_result = {
+ "example_name": f"Code Example{f' ({language})' if language else ''}",
+ "summary": "Code example extracted from development context.",
+ }
+ search_logger.info(f"Used hardcoded fallback for minimal response - Name: '{final_result['example_name']}', Summary length: {len(final_result['summary'])}")
+ return final_result
+
+ payload = _extract_json_payload(last_response_content, code, language)
+ if payload != last_response_content:
+ search_logger.debug(
+ f"Sanitized LLM response payload before parsing: {repr(payload[:200])}..."
+ )
+
+ try:
+ result = json.loads(payload)
+
+ if not result.get("example_name") or not result.get("summary"):
+ search_logger.warning(f"Incomplete response from LLM: {result}")
+
+ final_result = {
+ "example_name": result.get(
+ "example_name", f"Code Example{f' ({language})' if language else ''}"
+ ),
+ "summary": result.get("summary", "Code example for demonstration purposes."),
+ }
+
+ search_logger.info(
+ f"Generated code example summary - Name: '{final_result['example_name']}', Summary length: {len(final_result['summary'])}"
+ )
+ return final_result
+
+ except json.JSONDecodeError as json_error:
+ last_json_error = json_error
+ json_error_occurred = True
+ snippet = last_response_content[:200]
+ if not enforce_json:
+ # Check if this was reasoning text that couldn't be parsed
+ if _is_reasoning_text_response(last_response_content):
+ search_logger.debug(
+ f"Reasoning text detected but no JSON extracted. Response snippet: {repr(snippet)}"
+ )
+ else:
+ search_logger.warning(
+ f"Failed to parse JSON response from LLM (non-strict attempt). Error: {json_error}. Response snippet: {repr(snippet)}"
+ )
+ break
+ else:
+ search_logger.error(
+ f"Strict JSON enforcement still failed to produce valid JSON: {json_error}. Response snippet: {repr(snippet)}"
+ )
+ break
+
+ elif is_grok_model and attempt < max_retries - 1:
+ search_logger.warning(f"Grok empty response on attempt {attempt + 1}, retrying...")
+ retry_delay *= 2
+ continue
else:
- search_logger.debug(f"{model_type} attempt {attempt + 1} - finish_reason: {finish_reason}, no usage info")
-
- search_logger.debug(f"{model_type} response fields - content: '{content_preview}', reasoning_content: '{reasoning_preview}'")
-
- if message:
- # For Grok and reasoning models, check content first, then reasoning_content
- if provider == "grok" or is_reasoning:
- # First try content (where final answer should be)
- if message.content and message.content.strip():
- response_content = message.content.strip()
- model_type = "Grok" if provider == "grok" else f"reasoning model ({model_choice})"
- search_logger.debug(f"{model_type} using content field: {len(response_content)} chars")
- # Fallback to reasoning_content if content is empty
- elif hasattr(message, 'reasoning_content') and message.reasoning_content:
- response_content = message.reasoning_content.strip()
- model_type = "Grok" if provider == "grok" else f"reasoning model ({model_choice})"
- search_logger.debug(f"{model_type} fallback to reasoning_content: {len(response_content)} chars")
- else:
- search_logger.debug(f"Grok no content in either field: content='{message.content}', reasoning_content='{getattr(message, 'reasoning_content', 'N/A')}'")
- elif message.content:
- response_content = message.content
+ break
+
+ except Exception as e:
+ if is_grok_model and attempt < max_retries - 1:
+ search_logger.error(f"Grok request failed on attempt {attempt + 1}: {e}, retrying...")
+ retry_delay *= 2
+ continue
else:
- search_logger.debug(f"No content in message: content={message.content}, reasoning_content={getattr(message, 'reasoning_content', 'N/A')}")
+ raise
- if response_content and response_content.strip():
- # Success - break out of retry loop
- if (provider == "grok" or is_reasoning) and attempt > 0:
- model_type = "Grok" if provider == "grok" else f"reasoning model ({model_choice})"
- search_logger.info(f"{model_type} request succeeded on attempt {attempt + 1}")
- break
- elif (provider == "grok" or is_reasoning) and attempt < max_retries - 1:
- # Empty response from Grok or reasoning models - retry with exponential backoff
- model_type = "Grok" if provider == "grok" else f"reasoning model ({model_choice})"
- search_logger.warning(f"{model_type} empty response on attempt {attempt + 1}, retrying...")
- retry_delay *= 2 # Exponential backoff
+ if is_grok_model:
+ elapsed_time = time.time() - start_time
+ last_elapsed_time = elapsed_time
+ search_logger.debug(f"Grok total response time: {elapsed_time:.2f}s")
+
+ if json_error_occurred:
+ if not enforce_json:
continue
else:
- # Final attempt failed or not Grok/reasoning model - handle below
break
- except Exception as e:
- if (provider == "grok" or is_reasoning) and attempt < max_retries - 1:
- model_type = "Grok" if provider == "grok" else f"reasoning model ({model_choice})"
- search_logger.error(f"{model_type} request failed on attempt {attempt + 1}: {e}, retrying...")
- retry_delay *= 2
- continue
- else:
- # Re-raise on final attempt or non-Grok/reasoning providers
- raise
+ if response_content_local:
+ # We would have returned already on success; if we reach here, parsing failed but we are not retrying
+ continue
- # Log timing for Grok and reasoning model requests
- if provider == "grok" or is_reasoning:
- elapsed_time = time.time() - start_time
- model_type = "Grok" if provider == "grok" else f"reasoning model ({model_choice})"
- search_logger.debug(f"{model_type} total response time: {elapsed_time:.2f}s")
+ response_content = last_response_content
+ response = last_response_obj
+ elapsed_time = last_elapsed_time if last_elapsed_time is not None else 0.0
+
+ if last_json_error is not None and response_content:
+ search_logger.error(
+ f"LLM response after strict enforcement was still not valid JSON: {last_json_error}. Clearing response to trigger error handling."
+ )
+ response_content = ""
if not response_content:
search_logger.error(f"Empty response from LLM for model: {model_choice} (provider: {provider})")
- if provider == "grok":
+ if is_grok_model:
search_logger.error("Grok empty response debugging:")
search_logger.error(f" - Request took: {elapsed_time:.2f}s")
search_logger.error(f" - Response status: {getattr(response, 'status_code', 'N/A')}")
@@ -855,7 +901,7 @@ async def _generate_code_example_summary_async(
response_content = response_content.strip()
search_logger.debug(f"LLM API response: {repr(response_content[:200])}...")
- payload = _extract_json_payload(response_content)
+ payload = _extract_json_payload(response_content, code, language)
if payload != response_content:
search_logger.debug(
f"Sanitized LLM response payload before parsing: {repr(payload[:200])}..."
@@ -883,12 +929,38 @@ async def _generate_code_example_summary_async(
search_logger.error(
f"Failed to parse JSON response from LLM: {e}, Response: {repr(response_content) if 'response_content' in locals() else 'No response'}"
)
+ # Try to generate context-aware fallback
+ try:
+ fallback_json = synthesize_json_from_reasoning("", code, language)
+ if fallback_json:
+ fallback_result = json.loads(fallback_json)
+ search_logger.info(f"Generated context-aware fallback summary")
+ return {
+ "example_name": fallback_result.get("example_name", f"Code Example{f' ({language})' if language else ''}"),
+ "summary": fallback_result.get("summary", "Code example for demonstration purposes."),
+ }
+ except Exception:
+ pass # Fall through to generic fallback
+
return {
"example_name": f"Code Example{f' ({language})' if language else ''}",
"summary": "Code example for demonstration purposes.",
}
except Exception as e:
search_logger.error(f"Error generating code summary using unified LLM provider: {e}")
+ # Try to generate context-aware fallback
+ try:
+ fallback_json = synthesize_json_from_reasoning("", code, language)
+ if fallback_json:
+ fallback_result = json.loads(fallback_json)
+ search_logger.info(f"Generated context-aware fallback summary after error")
+ return {
+ "example_name": fallback_result.get("example_name", f"Code Example{f' ({language})' if language else ''}"),
+ "summary": fallback_result.get("summary", "Code example for demonstration purposes."),
+ }
+ except Exception:
+ pass # Fall through to generic fallback
+
return {
"example_name": f"Code Example{f' ({language})' if language else ''}",
"summary": "Code example for demonstration purposes.",
@@ -1124,13 +1196,13 @@ async def add_code_examples_to_supabase(
# Use only successful embeddings
valid_embeddings = result.embeddings
successful_texts = result.texts_processed
-
+
# Get model information for tracking
from ..llm_provider_service import get_embedding_model
-
+
# Get embedding model name
embedding_model_name = await get_embedding_model(provider=provider)
-
+
# Get LLM chat model (used for code summaries and contextual embeddings if enabled)
llm_chat_model = None
try:
@@ -1163,7 +1235,7 @@ async def add_code_examples_to_supabase(
positions_by_text[text].append(original_indices[k])
# Map successful texts back to their original indices
- for embedding, text in zip(valid_embeddings, successful_texts, strict=False):
+ for embedding, text in zip(valid_embeddings, successful_texts, strict=True):
# Get the next available index for this text (handles duplicates)
if positions_by_text[text]:
orig_idx = positions_by_text[text].popleft() # Original j index in [i, batch_end)
@@ -1183,7 +1255,7 @@ async def add_code_examples_to_supabase(
# Determine the correct embedding column based on dimension
embedding_dim = len(embedding) if isinstance(embedding, list) else len(embedding.tolist())
embedding_column = None
-
+
if embedding_dim == 768:
embedding_column = "embedding_768"
elif embedding_dim == 1024:
From d15cc31720b003278566171b225e48a410056ea6 Mon Sep 17 00:00:00 2001
From: Chillbruhhh
Date: Thu, 25 Sep 2025 06:36:15 -0500
Subject: [PATCH 15/28] updated ui llm interface, added seprate embeddings
provider, made the system fully capabale of mix and matching llm providers
(local and non local) for chat & embeddings. updated the ragsettings.tsx ui
mainly, along with core functionality
---
archon-ui-main/package-lock.json | 1381 +++++++++--------
archon-ui-main/package.json | 1 +
.../src/components/settings/RAGSettings.tsx | 966 +++++++-----
.../src/services/credentialsService.ts | 41 +-
.../crawling/code_extraction_service.py | 6 +-
.../src/server/services/credential_service.py | 63 +-
.../services/embeddings/embedding_service.py | 156 +-
7 files changed, 1520 insertions(+), 1094 deletions(-)
diff --git a/archon-ui-main/package-lock.json b/archon-ui-main/package-lock.json
index a665375376..b29650ab8e 100644
--- a/archon-ui-main/package-lock.json
+++ b/archon-ui-main/package-lock.json
@@ -30,6 +30,7 @@
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.3.1",
+ "react-icons": "^5.5.0",
"react-markdown": "^10.1.0",
"react-router-dom": "^6.26.2",
"tailwind-merge": "latest",
@@ -62,9 +63,9 @@
}
},
"node_modules/@adobe/css-tools": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.3.tgz",
- "integrity": "sha512-VQKMkwriZbaOgVCby1UDY/LDk5fIjhQicCvVPFqfe+69fWaPWydbWJ3wRt59/YzIwda1I81loas3oCoHxnqvdA==",
+ "version": "4.4.4",
+ "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz",
+ "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==",
"dev": true,
"license": "MIT"
},
@@ -132,9 +133,9 @@
}
},
"node_modules/@babel/compat-data": {
- "version": "7.27.3",
- "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.3.tgz",
- "integrity": "sha512-V42wFfx1ymFte+ecf6iXghnnP8kWTO+ZLXIyZq+1LAXHHvTZdVxicn4yiVYdYMGaCO3tmqub11AorKkv+iodqw==",
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz",
+ "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -142,22 +143,22 @@
}
},
"node_modules/@babel/core": {
- "version": "7.27.4",
- "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz",
- "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==",
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz",
+ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.27.1",
- "@babel/generator": "^7.27.3",
+ "@babel/generator": "^7.28.3",
"@babel/helper-compilation-targets": "^7.27.2",
- "@babel/helper-module-transforms": "^7.27.3",
- "@babel/helpers": "^7.27.4",
- "@babel/parser": "^7.27.4",
+ "@babel/helper-module-transforms": "^7.28.3",
+ "@babel/helpers": "^7.28.4",
+ "@babel/parser": "^7.28.4",
"@babel/template": "^7.27.2",
- "@babel/traverse": "^7.27.4",
- "@babel/types": "^7.27.3",
+ "@babel/traverse": "^7.28.4",
+ "@babel/types": "^7.28.4",
+ "@jridgewell/remapping": "^2.3.5",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
@@ -183,16 +184,16 @@
}
},
"node_modules/@babel/generator": {
- "version": "7.27.3",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.3.tgz",
- "integrity": "sha512-xnlJYj5zepml8NXtjkG0WquFUv8RskFqyFcVgTBp5k+NaA/8uw/K+OSVf8AMGw5e9HKP2ETd5xpK5MLZQD6b4Q==",
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz",
+ "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/parser": "^7.27.3",
- "@babel/types": "^7.27.3",
- "@jridgewell/gen-mapping": "^0.3.5",
- "@jridgewell/trace-mapping": "^0.3.25",
+ "@babel/parser": "^7.28.3",
+ "@babel/types": "^7.28.2",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
"jsesc": "^3.0.2"
},
"engines": {
@@ -226,6 +227,16 @@
"semver": "bin/semver.js"
}
},
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@babel/helper-module-imports": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
@@ -241,15 +252,15 @@
}
},
"node_modules/@babel/helper-module-transforms": {
- "version": "7.27.3",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz",
- "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==",
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz",
+ "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-module-imports": "^7.27.1",
"@babel/helper-validator-identifier": "^7.27.1",
- "@babel/traverse": "^7.27.3"
+ "@babel/traverse": "^7.28.3"
},
"engines": {
"node": ">=6.9.0"
@@ -299,27 +310,27 @@
}
},
"node_modules/@babel/helpers": {
- "version": "7.27.4",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.4.tgz",
- "integrity": "sha512-Y+bO6U+I7ZKaM5G5rDUZiYfUvQPUibYmAFe7EnKdnKBbVXDZxvp+MWOH5gYciY0EPk4EScsuFMQBbEfpdRKSCQ==",
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz",
+ "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/template": "^7.27.2",
- "@babel/types": "^7.27.3"
+ "@babel/types": "^7.28.4"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
- "version": "7.28.0",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz",
- "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==",
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz",
+ "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/types": "^7.28.0"
+ "@babel/types": "^7.28.4"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -361,9 +372,9 @@
}
},
"node_modules/@babel/runtime": {
- "version": "7.27.4",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.4.tgz",
- "integrity": "sha512-t3yaEOuGu9NlIZ+hIeGbBjFtZT7j2cb2tg0fuaJKeGotchRjjLfrBA9Kwf8quhpP1EUuxModQg04q/mBwyg8uA==",
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
+ "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -385,28 +396,28 @@
}
},
"node_modules/@babel/traverse": {
- "version": "7.27.4",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz",
- "integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==",
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz",
+ "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.27.1",
- "@babel/generator": "^7.27.3",
- "@babel/parser": "^7.27.4",
+ "@babel/generator": "^7.28.3",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.28.4",
"@babel/template": "^7.27.2",
- "@babel/types": "^7.27.3",
- "debug": "^4.3.1",
- "globals": "^11.1.0"
+ "@babel/types": "^7.28.4",
+ "debug": "^4.3.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/types": {
- "version": "7.28.1",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.1.tgz",
- "integrity": "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==",
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz",
+ "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -588,9 +599,9 @@
}
},
"node_modules/@codemirror/autocomplete": {
- "version": "6.18.6",
- "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz",
- "integrity": "sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg==",
+ "version": "6.18.7",
+ "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.7.tgz",
+ "integrity": "sha512-8EzdeIoWPJDsMBwz3zdzwXnUpCzMiCyz5/A3FIPpriaclFCGDkAzK13sMcnsu5rowqiyeQN2Vs2TsOcoDPZirQ==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
@@ -662,9 +673,9 @@
}
},
"node_modules/@codemirror/lang-html": {
- "version": "6.4.9",
- "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.9.tgz",
- "integrity": "sha512-aQv37pIMSlueybId/2PVSP6NPnmurFDVmZwzc7jszd2KAF8qd4VBbvNYPXWQq90WIARjsdVkPbw29pszmHws3Q==",
+ "version": "6.4.10",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.10.tgz",
+ "integrity": "sha512-h/SceTVsN5r+WE+TVP2g3KDvNoSzbSrtZXCKo4vkKdbfT5t4otuVgngGdFukOO/rwRD2++pCxoh6xD4TEVMkQA==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
@@ -727,9 +738,9 @@
}
},
"node_modules/@codemirror/lang-liquid": {
- "version": "6.2.3",
- "resolved": "https://registry.npmjs.org/@codemirror/lang-liquid/-/lang-liquid-6.2.3.tgz",
- "integrity": "sha512-yeN+nMSrf/lNii3FJxVVEGQwFG0/2eDyH6gNOj+TGCa0hlNO4bhQnoO5ISnd7JOG+7zTEcI/GOoyraisFVY7jQ==",
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-liquid/-/lang-liquid-6.3.0.tgz",
+ "integrity": "sha512-fY1YsUExcieXRTsCiwX/bQ9+PbCTA/Fumv7C7mTUZHoFkibfESnaXwpr2aKH6zZVwysEunsHHkaIpM/pl3xETQ==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
@@ -743,9 +754,9 @@
}
},
"node_modules/@codemirror/lang-markdown": {
- "version": "6.3.3",
- "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.3.3.tgz",
- "integrity": "sha512-1fn1hQAPWlSSMCvnF810AkhWpNLkJpl66CRfIy3vVl20Sl4NwChkorCHqpMtNbXr1EuMJsrDnhEpjZxKZ2UX3A==",
+ "version": "6.3.4",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.3.4.tgz",
+ "integrity": "sha512-fBm0BO03azXnTAsxhONDYHi/qWSI+uSEIpzKM7h/bkIc9fHnFp9y7KTMXKON0teNT97pFhc1a9DQTtWBYEZ7ug==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.7.1",
@@ -807,9 +818,9 @@
}
},
"node_modules/@codemirror/lang-sql": {
- "version": "6.9.0",
- "resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.9.0.tgz",
- "integrity": "sha512-xmtpWqKSgum1B1J3Ro6rf7nuPqf2+kJQg5SjrofCAcyCThOe0ihSktSoXfXuhQBnwx1QbmreBbLJM5Jru6zitg==",
+ "version": "6.10.0",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.10.0.tgz",
+ "integrity": "sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
@@ -876,9 +887,9 @@
}
},
"node_modules/@codemirror/language": {
- "version": "6.11.2",
- "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.2.tgz",
- "integrity": "sha512-p44TsNArL4IVXDTbapUmEkAlvWs2CFQbcfc0ymDsis1kH2wh0gcY96AS29c/vp2d0y2Tquk1EDSaawpzilUiAw==",
+ "version": "6.11.3",
+ "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz",
+ "integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
@@ -973,9 +984,9 @@
}
},
"node_modules/@codemirror/view": {
- "version": "6.38.1",
- "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.1.tgz",
- "integrity": "sha512-RmTOkE7hRU3OVREqFVITWHz6ocgBjv08GoePscAakgVQfciA3SGCEk7mb9IzwW61cKKmlTpHXG6DUE5Ubx+MGQ==",
+ "version": "6.38.3",
+ "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.3.tgz",
+ "integrity": "sha512-x2t87+oqwB1mduiQZ6huIghjMt4uZKFEdj66IcXw7+a5iBEvv9lh7EWDRHI7crnD4BMGpnyq/RzmCGbiEZLcvQ==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.5.0",
@@ -1064,9 +1075,9 @@
}
},
"node_modules/@csstools/color-helpers": {
- "version": "5.0.2",
- "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz",
- "integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==",
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
+ "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
"dev": true,
"funding": [
{
@@ -1108,9 +1119,9 @@
}
},
"node_modules/@csstools/css-color-parser": {
- "version": "3.0.10",
- "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.10.tgz",
- "integrity": "sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==",
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
+ "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
"dev": true,
"funding": [
{
@@ -1124,7 +1135,7 @@
],
"license": "MIT",
"dependencies": {
- "@csstools/color-helpers": "^5.0.2",
+ "@csstools/color-helpers": "^5.1.0",
"@csstools/css-calc": "^2.1.4"
},
"engines": {
@@ -1570,9 +1581,9 @@
}
},
"node_modules/@eslint-community/eslint-utils": {
- "version": "4.7.0",
- "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
- "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==",
+ "version": "4.9.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
+ "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1622,20 +1633,28 @@
"url": "https://opencollective.com/eslint"
}
},
- "node_modules/@eslint/eslintrc/node_modules/globals": {
- "version": "13.24.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
- "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
+ "node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "type-fest": "^0.20.2"
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
},
"engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "node": "*"
}
},
"node_modules/@eslint/js": {
@@ -1717,6 +1736,30 @@
"node": ">=10.10.0"
}
},
+ "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/@humanwhocodes/config-array/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/@humanwhocodes/module-importer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
@@ -1758,9 +1801,9 @@
}
},
"node_modules/@isaacs/cliui/node_modules/ansi-regex": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
- "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+ "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1771,9 +1814,9 @@
}
},
"node_modules/@isaacs/cliui/node_modules/strip-ansi": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
- "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
+ "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1810,34 +1853,31 @@
}
},
"node_modules/@jridgewell/gen-mapping": {
- "version": "0.3.8",
- "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
- "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@jridgewell/set-array": "^1.2.1",
- "@jridgewell/sourcemap-codec": "^1.4.10",
+ "@jridgewell/sourcemap-codec": "^1.5.0",
"@jridgewell/trace-mapping": "^0.3.24"
- },
- "engines": {
- "node": ">=6.0.0"
}
},
- "node_modules/@jridgewell/resolve-uri": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
- "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
"dev": true,
"license": "MIT",
- "engines": {
- "node": ">=6.0.0"
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
}
},
- "node_modules/@jridgewell/set-array": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
- "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1845,16 +1885,16 @@
}
},
"node_modules/@jridgewell/sourcemap-codec": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
- "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
- "version": "0.3.25",
- "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
- "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1863,41 +1903,41 @@
}
},
"node_modules/@lexical/clipboard": {
- "version": "0.33.1",
- "resolved": "https://registry.npmjs.org/@lexical/clipboard/-/clipboard-0.33.1.tgz",
- "integrity": "sha512-Qd3/Cm3TW2DFQv58kMtLi86u5YOgpBdf+o7ySbXz55C613SLACsYQBB3X5Vu5hTx/t/ugYOpII4HkiatW6d9zA==",
+ "version": "0.35.0",
+ "resolved": "https://registry.npmjs.org/@lexical/clipboard/-/clipboard-0.35.0.tgz",
+ "integrity": "sha512-ko7xSIIiayvDiqjNDX6fgH9RlcM6r9vrrvJYTcfGVBor5httx16lhIi0QJZ4+RNPvGtTjyFv4bwRmsixRRwImg==",
"license": "MIT",
"dependencies": {
- "@lexical/html": "0.33.1",
- "@lexical/list": "0.33.1",
- "@lexical/selection": "0.33.1",
- "@lexical/utils": "0.33.1",
- "lexical": "0.33.1"
+ "@lexical/html": "0.35.0",
+ "@lexical/list": "0.35.0",
+ "@lexical/selection": "0.35.0",
+ "@lexical/utils": "0.35.0",
+ "lexical": "0.35.0"
}
},
"node_modules/@lexical/code": {
- "version": "0.33.1",
- "resolved": "https://registry.npmjs.org/@lexical/code/-/code-0.33.1.tgz",
- "integrity": "sha512-E0Y/+1znkqVpP52Y6blXGAduoZek9SSehJN+vbH+4iQKyFwTA7JB+jd5C5/K0ik55du9X7SN/oTynByg7lbcAA==",
+ "version": "0.35.0",
+ "resolved": "https://registry.npmjs.org/@lexical/code/-/code-0.35.0.tgz",
+ "integrity": "sha512-ox4DZwETQ9IA7+DS6PN8RJNwSAF7RMjL7YTVODIqFZ5tUFIf+5xoCHbz7Fll0Bvixlp12hVH90xnLwTLRGpkKw==",
"license": "MIT",
"dependencies": {
- "@lexical/utils": "0.33.1",
- "lexical": "0.33.1",
+ "@lexical/utils": "0.35.0",
+ "lexical": "0.35.0",
"prismjs": "^1.30.0"
}
},
"node_modules/@lexical/devtools-core": {
- "version": "0.33.1",
- "resolved": "https://registry.npmjs.org/@lexical/devtools-core/-/devtools-core-0.33.1.tgz",
- "integrity": "sha512-3yHu5diNtjwhoe2q/x9as6n6rIfA+QO2CfaVjFRkam8rkAW6zUzQT1D0fQdE8nOfWvXBgY1mH/ZLP4dDXBdG5Q==",
+ "version": "0.35.0",
+ "resolved": "https://registry.npmjs.org/@lexical/devtools-core/-/devtools-core-0.35.0.tgz",
+ "integrity": "sha512-C2wwtsMCR6ZTfO0TqpSM17RLJWyfHmifAfCTjFtOJu15p3M6NO/nHYK5Mt7YMQteuS89mOjB4ng8iwoLEZ6QpQ==",
"license": "MIT",
"dependencies": {
- "@lexical/html": "0.33.1",
- "@lexical/link": "0.33.1",
- "@lexical/mark": "0.33.1",
- "@lexical/table": "0.33.1",
- "@lexical/utils": "0.33.1",
- "lexical": "0.33.1"
+ "@lexical/html": "0.35.0",
+ "@lexical/link": "0.35.0",
+ "@lexical/mark": "0.35.0",
+ "@lexical/table": "0.35.0",
+ "@lexical/utils": "0.35.0",
+ "lexical": "0.35.0"
},
"peerDependencies": {
"react": ">=17.x",
@@ -1905,144 +1945,144 @@
}
},
"node_modules/@lexical/dragon": {
- "version": "0.33.1",
- "resolved": "https://registry.npmjs.org/@lexical/dragon/-/dragon-0.33.1.tgz",
- "integrity": "sha512-UQ6DLkcDAr83wA1vz3sUgtcpYcMifC4sF0MieZAoMzFrna6Ekqj7OJ7g8Lo7m7AeuT4NETRVDsjIEDdrQMKLLA==",
+ "version": "0.35.0",
+ "resolved": "https://registry.npmjs.org/@lexical/dragon/-/dragon-0.35.0.tgz",
+ "integrity": "sha512-SL6mT5pcqrt6hEbJ16vWxip5+r3uvMd0bQV5UUxuk+cxIeuP86iTgRh0HFR7SM2dRTYovL6/tM/O+8QLAUGTIg==",
"license": "MIT",
"dependencies": {
- "lexical": "0.33.1"
+ "lexical": "0.35.0"
}
},
"node_modules/@lexical/hashtag": {
- "version": "0.33.1",
- "resolved": "https://registry.npmjs.org/@lexical/hashtag/-/hashtag-0.33.1.tgz",
- "integrity": "sha512-M3IsDe4cifggMBZgYAVT7hCLWcwQ3dIcUPdr9Xc6wDQQQdEqOQYB0PO//9bSYUVq+BNiiTgysc+TtlM7PiJfiw==",
+ "version": "0.35.0",
+ "resolved": "https://registry.npmjs.org/@lexical/hashtag/-/hashtag-0.35.0.tgz",
+ "integrity": "sha512-LYJWzXuO2ZjKsvQwrLkNZiS2TsjwYkKjlDgtugzejquTBQ/o/nfSn/MmVx6EkYLOYizaJemmZbz3IBh+u732FA==",
"license": "MIT",
"dependencies": {
- "@lexical/utils": "0.33.1",
- "lexical": "0.33.1"
+ "@lexical/utils": "0.35.0",
+ "lexical": "0.35.0"
}
},
"node_modules/@lexical/history": {
- "version": "0.33.1",
- "resolved": "https://registry.npmjs.org/@lexical/history/-/history-0.33.1.tgz",
- "integrity": "sha512-Bk0h3D6cFkJ7w3HKvqQua7n6Xfz7nR7L3gLDBH9L0nsS4MM9+LteSEZPUe0kj4VuEjnxufYstTc9HA2aNLKxnQ==",
+ "version": "0.35.0",
+ "resolved": "https://registry.npmjs.org/@lexical/history/-/history-0.35.0.tgz",
+ "integrity": "sha512-onjDRLLxGbCfHexSxxrQaDaieIHyV28zCDrbxR5dxTfW8F8PxjuNyuaG0z6o468AXYECmclxkP+P4aT6poHEpQ==",
"license": "MIT",
"dependencies": {
- "@lexical/utils": "0.33.1",
- "lexical": "0.33.1"
+ "@lexical/utils": "0.35.0",
+ "lexical": "0.35.0"
}
},
"node_modules/@lexical/html": {
- "version": "0.33.1",
- "resolved": "https://registry.npmjs.org/@lexical/html/-/html-0.33.1.tgz",
- "integrity": "sha512-t14vu4eKa6BWz1N7/rwXgXif1k4dj73dRvllWJgfXum+a36vn1aySNYOlOfqWXF7k1b3uJmoqsWK7n/1ASnimw==",
+ "version": "0.35.0",
+ "resolved": "https://registry.npmjs.org/@lexical/html/-/html-0.35.0.tgz",
+ "integrity": "sha512-rXGFE5S5rKsg3tVnr1s4iEgOfCApNXGpIFI3T2jGEShaCZ5HLaBY9NVBXnE9Nb49e9bkDkpZ8FZd1qokCbQXbw==",
"license": "MIT",
"dependencies": {
- "@lexical/selection": "0.33.1",
- "@lexical/utils": "0.33.1",
- "lexical": "0.33.1"
+ "@lexical/selection": "0.35.0",
+ "@lexical/utils": "0.35.0",
+ "lexical": "0.35.0"
}
},
"node_modules/@lexical/link": {
- "version": "0.33.1",
- "resolved": "https://registry.npmjs.org/@lexical/link/-/link-0.33.1.tgz",
- "integrity": "sha512-JCTu7Fft2J2kgfqJiWnGei+UMIXVKiZKaXzuHCuGQTFu92DeCyd02azBaFazZHEkSqCIFZ0DqVV2SpIJmd0Ygw==",
+ "version": "0.35.0",
+ "resolved": "https://registry.npmjs.org/@lexical/link/-/link-0.35.0.tgz",
+ "integrity": "sha512-+0Wx6cBwO8TfdMzpkYFacsmgFh8X1rkiYbq3xoLvk3qV8upYxaMzK1s8Q1cpKmWyI0aZrU6z7fiK4vUqB7+69w==",
"license": "MIT",
"dependencies": {
- "@lexical/utils": "0.33.1",
- "lexical": "0.33.1"
+ "@lexical/utils": "0.35.0",
+ "lexical": "0.35.0"
}
},
"node_modules/@lexical/list": {
- "version": "0.33.1",
- "resolved": "https://registry.npmjs.org/@lexical/list/-/list-0.33.1.tgz",
- "integrity": "sha512-PXp56dWADSThc9WhwWV4vXhUc3sdtCqsfPD3UQNGUZ9rsAY1479rqYLtfYgEmYPc8JWXikQCAKEejahCJIm8OQ==",
+ "version": "0.35.0",
+ "resolved": "https://registry.npmjs.org/@lexical/list/-/list-0.35.0.tgz",
+ "integrity": "sha512-owsmc8iwgExBX8sFe8fKTiwJVhYULt9hD1RZ/HwfaiEtRZZkINijqReOBnW2mJfRxBzhFSWc4NG3ISB+fHYzqw==",
"license": "MIT",
"dependencies": {
- "@lexical/selection": "0.33.1",
- "@lexical/utils": "0.33.1",
- "lexical": "0.33.1"
+ "@lexical/selection": "0.35.0",
+ "@lexical/utils": "0.35.0",
+ "lexical": "0.35.0"
}
},
"node_modules/@lexical/mark": {
- "version": "0.33.1",
- "resolved": "https://registry.npmjs.org/@lexical/mark/-/mark-0.33.1.tgz",
- "integrity": "sha512-tGdOf1e694lnm/HyWUKEkEWjDyfhCBFG7u8iRKNpsYTpB3M1FsJUXbphE2bb8MyWfhHbaNxnklupSSaSPzO88A==",
+ "version": "0.35.0",
+ "resolved": "https://registry.npmjs.org/@lexical/mark/-/mark-0.35.0.tgz",
+ "integrity": "sha512-W0hwMTAVeexvpk9/+J6n1G/sNkpI/Meq1yeDazahFLLAwXLHtvhIAq2P/klgFknDy1hr8X7rcsQuN/bqKcKHYg==",
"license": "MIT",
"dependencies": {
- "@lexical/utils": "0.33.1",
- "lexical": "0.33.1"
+ "@lexical/utils": "0.35.0",
+ "lexical": "0.35.0"
}
},
"node_modules/@lexical/markdown": {
- "version": "0.33.1",
- "resolved": "https://registry.npmjs.org/@lexical/markdown/-/markdown-0.33.1.tgz",
- "integrity": "sha512-p5zwWNF70pELRx60wxE8YOFVNiNDkw7gjKoYqkED23q5hj4mcqco9fQf6qeeZChjxLKjfyT6F1PpWgxmlBlxBw==",
+ "version": "0.35.0",
+ "resolved": "https://registry.npmjs.org/@lexical/markdown/-/markdown-0.35.0.tgz",
+ "integrity": "sha512-BlNyXZAt4gWidMw0SRWrhBETY1BpPglFBZI7yzfqukFqgXRh7HUQA28OYeI/nsx9pgNob8TiUduUwShqqvOdEA==",
"license": "MIT",
"dependencies": {
- "@lexical/code": "0.33.1",
- "@lexical/link": "0.33.1",
- "@lexical/list": "0.33.1",
- "@lexical/rich-text": "0.33.1",
- "@lexical/text": "0.33.1",
- "@lexical/utils": "0.33.1",
- "lexical": "0.33.1"
+ "@lexical/code": "0.35.0",
+ "@lexical/link": "0.35.0",
+ "@lexical/list": "0.35.0",
+ "@lexical/rich-text": "0.35.0",
+ "@lexical/text": "0.35.0",
+ "@lexical/utils": "0.35.0",
+ "lexical": "0.35.0"
}
},
"node_modules/@lexical/offset": {
- "version": "0.33.1",
- "resolved": "https://registry.npmjs.org/@lexical/offset/-/offset-0.33.1.tgz",
- "integrity": "sha512-3YIlUs43QdKSBLEfOkuciE2tn9loxVmkSs/HgaIiLYl0Edf1W00FP4ItSmYU4De5GopXsHq6+Y3ry4pU/ciUiQ==",
+ "version": "0.35.0",
+ "resolved": "https://registry.npmjs.org/@lexical/offset/-/offset-0.35.0.tgz",
+ "integrity": "sha512-DRE4Df6qYf2XiV6foh6KpGNmGAv2ANqt3oVXpyS6W8hTx3+cUuAA1APhCZmLNuU107um4zmHym7taCu6uXW5Yg==",
"license": "MIT",
"dependencies": {
- "lexical": "0.33.1"
+ "lexical": "0.35.0"
}
},
"node_modules/@lexical/overflow": {
- "version": "0.33.1",
- "resolved": "https://registry.npmjs.org/@lexical/overflow/-/overflow-0.33.1.tgz",
- "integrity": "sha512-3BDq1lOw567FeCk4rN2ellKwoXTM9zGkGuKnSGlXS1JmtGGGSvT+uTANX3KOOfqTNSrOkrwoM+3hlFv7p6VpiQ==",
+ "version": "0.35.0",
+ "resolved": "https://registry.npmjs.org/@lexical/overflow/-/overflow-0.35.0.tgz",
+ "integrity": "sha512-B25YvnJQTGlZcrNv7b0PJBLWq3tl8sql497OHfYYLem7EOMPKKDGJScJAKM/91D4H/mMAsx5gnA/XgKobriuTg==",
"license": "MIT",
"dependencies": {
- "lexical": "0.33.1"
+ "lexical": "0.35.0"
}
},
"node_modules/@lexical/plain-text": {
- "version": "0.33.1",
- "resolved": "https://registry.npmjs.org/@lexical/plain-text/-/plain-text-0.33.1.tgz",
- "integrity": "sha512-2HxdhAx6bwF8y5A9P0q3YHsYbhUo4XXm+GyKJO87an8JClL2W+GYLTSDbfNWTh4TtH95eG+UYLOjNEgyU6tsWA==",
+ "version": "0.35.0",
+ "resolved": "https://registry.npmjs.org/@lexical/plain-text/-/plain-text-0.35.0.tgz",
+ "integrity": "sha512-lwBCUNMJf7Gujp2syVWMpKRahfbTv5Wq+H3HK1Q1gKH1P2IytPRxssCHvexw9iGwprSyghkKBlbF3fGpEdIJvQ==",
"license": "MIT",
"dependencies": {
- "@lexical/clipboard": "0.33.1",
- "@lexical/selection": "0.33.1",
- "@lexical/utils": "0.33.1",
- "lexical": "0.33.1"
+ "@lexical/clipboard": "0.35.0",
+ "@lexical/selection": "0.35.0",
+ "@lexical/utils": "0.35.0",
+ "lexical": "0.35.0"
}
},
"node_modules/@lexical/react": {
- "version": "0.33.1",
- "resolved": "https://registry.npmjs.org/@lexical/react/-/react-0.33.1.tgz",
- "integrity": "sha512-ylnUmom5h8PY+Z14uDmKLQEoikTPN77GRM0NRCIdtbWmOQqOq/5BhuCzMZE1WvpL5C6n3GtK6IFnsMcsKmVOcw==",
+ "version": "0.35.0",
+ "resolved": "https://registry.npmjs.org/@lexical/react/-/react-0.35.0.tgz",
+ "integrity": "sha512-uYAZSqumH8tRymMef+A0f2hQvMwplKK9DXamcefnk3vSNDHHqRWQXpiUo6kD+rKWuQmMbVa5RW4xRQebXEW+1A==",
"license": "MIT",
"dependencies": {
"@floating-ui/react": "^0.27.8",
- "@lexical/devtools-core": "0.33.1",
- "@lexical/dragon": "0.33.1",
- "@lexical/hashtag": "0.33.1",
- "@lexical/history": "0.33.1",
- "@lexical/link": "0.33.1",
- "@lexical/list": "0.33.1",
- "@lexical/mark": "0.33.1",
- "@lexical/markdown": "0.33.1",
- "@lexical/overflow": "0.33.1",
- "@lexical/plain-text": "0.33.1",
- "@lexical/rich-text": "0.33.1",
- "@lexical/table": "0.33.1",
- "@lexical/text": "0.33.1",
- "@lexical/utils": "0.33.1",
- "@lexical/yjs": "0.33.1",
- "lexical": "0.33.1",
+ "@lexical/devtools-core": "0.35.0",
+ "@lexical/dragon": "0.35.0",
+ "@lexical/hashtag": "0.35.0",
+ "@lexical/history": "0.35.0",
+ "@lexical/link": "0.35.0",
+ "@lexical/list": "0.35.0",
+ "@lexical/mark": "0.35.0",
+ "@lexical/markdown": "0.35.0",
+ "@lexical/overflow": "0.35.0",
+ "@lexical/plain-text": "0.35.0",
+ "@lexical/rich-text": "0.35.0",
+ "@lexical/table": "0.35.0",
+ "@lexical/text": "0.35.0",
+ "@lexical/utils": "0.35.0",
+ "@lexical/yjs": "0.35.0",
+ "lexical": "0.35.0",
"react-error-boundary": "^3.1.4"
},
"peerDependencies": {
@@ -2051,67 +2091,67 @@
}
},
"node_modules/@lexical/rich-text": {
- "version": "0.33.1",
- "resolved": "https://registry.npmjs.org/@lexical/rich-text/-/rich-text-0.33.1.tgz",
- "integrity": "sha512-ZBIsj4LwmamRBCGjJiPSLj7N/XkUDv/pnYn5Rp0BL42WpOiQLvOoGLrZxgUJZEmRPQnx42ZgLKVgrWHsyjuoAA==",
+ "version": "0.35.0",
+ "resolved": "https://registry.npmjs.org/@lexical/rich-text/-/rich-text-0.35.0.tgz",
+ "integrity": "sha512-qEHu8g7vOEzz9GUz1VIUxZBndZRJPh9iJUFI+qTDHj+tQqnd5LCs+G9yz6jgNfiuWWpezTp0i1Vz/udNEuDPKQ==",
"license": "MIT",
"dependencies": {
- "@lexical/clipboard": "0.33.1",
- "@lexical/selection": "0.33.1",
- "@lexical/utils": "0.33.1",
- "lexical": "0.33.1"
+ "@lexical/clipboard": "0.35.0",
+ "@lexical/selection": "0.35.0",
+ "@lexical/utils": "0.35.0",
+ "lexical": "0.35.0"
}
},
"node_modules/@lexical/selection": {
- "version": "0.33.1",
- "resolved": "https://registry.npmjs.org/@lexical/selection/-/selection-0.33.1.tgz",
- "integrity": "sha512-KXPkdCDdVfIUXmkwePu9DAd3kLjL0aAqL5G9CMCFsj7RG9lLvvKk7kpivrAIbRbcsDzO44QwsFPisZHbX4ioXA==",
+ "version": "0.35.0",
+ "resolved": "https://registry.npmjs.org/@lexical/selection/-/selection-0.35.0.tgz",
+ "integrity": "sha512-mMtDE7Q0nycXdFTTH/+ta6EBrBwxBB4Tg8QwsGntzQ1Cq//d838dpXpFjJOqHEeVHUqXpiuj+cBG8+bvz/rPRw==",
"license": "MIT",
"dependencies": {
- "lexical": "0.33.1"
+ "lexical": "0.35.0"
}
},
"node_modules/@lexical/table": {
- "version": "0.33.1",
- "resolved": "https://registry.npmjs.org/@lexical/table/-/table-0.33.1.tgz",
- "integrity": "sha512-pzB11i1Y6fzmy0IPUKJyCdhVBgXaNOxJUxrQJWdKNYCh1eMwwMEQvj+8inItd/11aUkjcdHjwDTht8gL2UHKiQ==",
+ "version": "0.35.0",
+ "resolved": "https://registry.npmjs.org/@lexical/table/-/table-0.35.0.tgz",
+ "integrity": "sha512-9jlTlkVideBKwsEnEkqkdg7A3mije1SvmfiqoYnkl1kKJCLA5iH90ywx327PU0p+bdnURAytWUeZPXaEuEl2OA==",
"license": "MIT",
"dependencies": {
- "@lexical/clipboard": "0.33.1",
- "@lexical/utils": "0.33.1",
- "lexical": "0.33.1"
+ "@lexical/clipboard": "0.35.0",
+ "@lexical/utils": "0.35.0",
+ "lexical": "0.35.0"
}
},
"node_modules/@lexical/text": {
- "version": "0.33.1",
- "resolved": "https://registry.npmjs.org/@lexical/text/-/text-0.33.1.tgz",
- "integrity": "sha512-CnyU3q3RytXXWVSvC5StOKISzFAPGK9MuesNDDGyZk7yDK+J98gV6df4RBKfqwcokFMThpkUlvMeKe1+S2y25A==",
+ "version": "0.35.0",
+ "resolved": "https://registry.npmjs.org/@lexical/text/-/text-0.35.0.tgz",
+ "integrity": "sha512-uaMh46BkysV8hK8wQwp5g/ByZW+2hPDt8ahAErxtf8NuzQem1FHG/f5RTchmFqqUDVHO3qLNTv4AehEGmXv8MA==",
"license": "MIT",
"dependencies": {
- "lexical": "0.33.1"
+ "lexical": "0.35.0"
}
},
"node_modules/@lexical/utils": {
- "version": "0.33.1",
- "resolved": "https://registry.npmjs.org/@lexical/utils/-/utils-0.33.1.tgz",
- "integrity": "sha512-eKysPjzEE9zD+2af3WRX5U3XbeNk0z4uv1nXGH3RG15uJ4Huzjht82hzsQpCFUobKmzYlQaQs5y2IYKE2puipQ==",
+ "version": "0.35.0",
+ "resolved": "https://registry.npmjs.org/@lexical/utils/-/utils-0.35.0.tgz",
+ "integrity": "sha512-2H393EYDnFznYCDFOW3MHiRzwEO5M/UBhtUjvTT+9kc+qhX4U3zc8ixQalo5UmZ5B2nh7L/inXdTFzvSRXtsRA==",
"license": "MIT",
"dependencies": {
- "@lexical/list": "0.33.1",
- "@lexical/selection": "0.33.1",
- "@lexical/table": "0.33.1",
- "lexical": "0.33.1"
+ "@lexical/list": "0.35.0",
+ "@lexical/selection": "0.35.0",
+ "@lexical/table": "0.35.0",
+ "lexical": "0.35.0"
}
},
"node_modules/@lexical/yjs": {
- "version": "0.33.1",
- "resolved": "https://registry.npmjs.org/@lexical/yjs/-/yjs-0.33.1.tgz",
- "integrity": "sha512-Zx1rabMm/Zjk7n7YQMIQLUN+tqzcg1xqcgNpEHSfK1GA8QMPXCPvXWFT3ZDC4tfZOSy/YIqpVUyWZAomFqRa+g==",
+ "version": "0.35.0",
+ "resolved": "https://registry.npmjs.org/@lexical/yjs/-/yjs-0.35.0.tgz",
+ "integrity": "sha512-3DSP7QpmTGYU9bN/yljP0PIao4tNIQtsR4ycauWNSawxs/GQCZtSmAPcLRnCm6qpqsDDjUtKjO/1Ej8FRp0m0w==",
"license": "MIT",
"dependencies": {
- "@lexical/offset": "0.33.1",
- "@lexical/selection": "0.33.1",
- "lexical": "0.33.1"
+ "@lexical/offset": "0.35.0",
+ "@lexical/selection": "0.35.0",
+ "lexical": "0.35.0"
},
"peerDependencies": {
"yjs": ">=13.5.22"
@@ -2166,9 +2206,9 @@
}
},
"node_modules/@lezer/html": {
- "version": "1.3.10",
- "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.10.tgz",
- "integrity": "sha512-dqpT8nISx/p9Do3AchvYGV3qYc4/rKr3IBZxlHmpIKam56P47RSHkSF5f13Vu9hebS1jM0HmtJIwLbWz1VIY6w==",
+ "version": "1.3.11",
+ "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.11.tgz",
+ "integrity": "sha512-SV04kK5EHDPPecMCiFNZAnQhUIxktP04yHxgOKK7TZ3+KUAlK9f4dcYbjAWwDx2C2pJmiOeSV05QEbHeQo5JqA==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
@@ -2188,9 +2228,9 @@
}
},
"node_modules/@lezer/javascript": {
- "version": "1.5.1",
- "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.1.tgz",
- "integrity": "sha512-ATOImjeVJuvgm3JQ/bpo2Tmv55HSScE2MTPnKRMRIPx2cLhHGyX2VnqpHhtIV1tVzIjZDbcWQm+NCTF40ggZVw==",
+ "version": "1.5.4",
+ "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz",
+ "integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
@@ -2229,9 +2269,9 @@
}
},
"node_modules/@lezer/php": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/@lezer/php/-/php-1.0.4.tgz",
- "integrity": "sha512-D2dJ0t8Z28/G1guztRczMFvPDUqzeMLSQbdWQmaiHV7urc8NlEOnjYk9UrZ531OcLiRxD4Ihcbv7AsDpNKDRaQ==",
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@lezer/php/-/php-1.0.5.tgz",
+ "integrity": "sha512-W7asp9DhM6q0W6DYNwIkLSKOvxlXRrif+UXBMxzsJUuqmhE7oVU+gS3THO4S/Puh7Xzgm858UNaFi6dxTP8dJA==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
@@ -2301,9 +2341,9 @@
"license": "MIT"
},
"node_modules/@mdxeditor/editor": {
- "version": "3.42.0",
- "resolved": "https://registry.npmjs.org/@mdxeditor/editor/-/editor-3.42.0.tgz",
- "integrity": "sha512-nQN07RkTm842T477IjPqp1FhWCQMpmbLToOVrc6EjSI60aHifwzva+eqYmElHFKE2jyGiD5FsaQXri1SSORJNg==",
+ "version": "3.46.1",
+ "resolved": "https://registry.npmjs.org/@mdxeditor/editor/-/editor-3.46.1.tgz",
+ "integrity": "sha512-TL0Ol88NhlXYfThD6kYGhxIQkUMjBkHZ2OsbvHU6mD2RpqcTp1/tinLmADzmoreKSl/52rcj+lTbrJwBmeiHRw==",
"license": "MIT",
"dependencies": {
"@codemirror/commands": "^6.2.4",
@@ -2313,16 +2353,16 @@
"@codemirror/state": "^6.4.0",
"@codemirror/view": "^6.23.0",
"@codesandbox/sandpack-react": "^2.20.0",
- "@lexical/clipboard": "^0.33.1",
- "@lexical/link": "^0.33.1",
- "@lexical/list": "^0.33.1",
- "@lexical/markdown": "^0.33.1",
- "@lexical/plain-text": "^0.33.1",
- "@lexical/react": "^0.33.1",
- "@lexical/rich-text": "^0.33.1",
- "@lexical/selection": "^0.33.1",
- "@lexical/utils": "^0.33.1",
- "@mdxeditor/gurx": "^1.1.4",
+ "@lexical/clipboard": "^0.35.0",
+ "@lexical/link": "^0.35.0",
+ "@lexical/list": "^0.35.0",
+ "@lexical/markdown": "^0.35.0",
+ "@lexical/plain-text": "^0.35.0",
+ "@lexical/react": "^0.35.0",
+ "@lexical/rich-text": "^0.35.0",
+ "@lexical/selection": "^0.35.0",
+ "@lexical/utils": "^0.35.0",
+ "@mdxeditor/gurx": "^1.2.4",
"@radix-ui/colors": "^3.0.0",
"@radix-ui/react-dialog": "^1.1.11",
"@radix-ui/react-icons": "^1.3.2",
@@ -2337,7 +2377,7 @@
"codemirror": "^6.0.1",
"downshift": "^7.6.0",
"js-yaml": "4.1.0",
- "lexical": "^0.33.1",
+ "lexical": "^0.35.0",
"mdast-util-directive": "^3.0.0",
"mdast-util-from-markdown": "^2.0.0",
"mdast-util-frontmatter": "^2.0.1",
@@ -2372,9 +2412,9 @@
}
},
"node_modules/@mdxeditor/gurx": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/@mdxeditor/gurx/-/gurx-1.2.3.tgz",
- "integrity": "sha512-5DQOlEx46oN9spggrC8husAGAhVoEFBGIYKN48es08XhRUbSU6l5bcIQYwRrQaY8clU1tExIcXzw8/fNnoxjpg==",
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@mdxeditor/gurx/-/gurx-1.2.4.tgz",
+ "integrity": "sha512-9ZykIFYhKaXaaSPCs1cuI+FvYDegJjbKwmA4ASE/zY+hJY6EYqvoye4esiO85CjhOw9aoD/izD/CU78/egVqmg==",
"license": "MIT",
"engines": {
"node": ">=16"
@@ -3436,16 +3476,16 @@
}
},
"node_modules/@rolldown/pluginutils": {
- "version": "1.0.0-beta.9",
- "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.9.tgz",
- "integrity": "sha512-e9MeMtVWo186sgvFFJOPGy7/d2j2mZhLJIdVW0C/xDluuOvymEATqz6zKsP0ZmXGzQtqlyjz5sC1sYQUoJG98w==",
+ "version": "1.0.0-beta.27",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
"dev": true,
"license": "MIT"
},
"node_modules/@rollup/rollup-android-arm-eabi": {
- "version": "4.41.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.1.tgz",
- "integrity": "sha512-NELNvyEWZ6R9QMkiytB4/L4zSEaBC03KIXEghptLGLZWJ6VPrL63ooZQCOnlx36aQPGhzuOMwDerC1Eb2VmrLw==",
+ "version": "4.52.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.2.tgz",
+ "integrity": "sha512-o3pcKzJgSGt4d74lSZ+OCnHwkKBeAbFDmbEm5gg70eA8VkyCuC/zV9TwBnmw6VjDlRdF4Pshfb+WE9E6XY1PoQ==",
"cpu": [
"arm"
],
@@ -3457,9 +3497,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
- "version": "4.41.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.1.tgz",
- "integrity": "sha512-DXdQe1BJ6TK47ukAoZLehRHhfKnKg9BjnQYUu9gzhI8Mwa1d2fzxA1aw2JixHVl403bwp1+/o/NhhHtxWJBgEA==",
+ "version": "4.52.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.2.tgz",
+ "integrity": "sha512-cqFSWO5tX2vhC9hJTK8WAiPIm4Q8q/cU8j2HQA0L3E1uXvBYbOZMhE2oFL8n2pKB5sOCHY6bBuHaRwG7TkfJyw==",
"cpu": [
"arm64"
],
@@ -3471,9 +3511,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
- "version": "4.41.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.1.tgz",
- "integrity": "sha512-5afxvwszzdulsU2w8JKWwY8/sJOLPzf0e1bFuvcW5h9zsEg+RQAojdW0ux2zyYAz7R8HvvzKCjLNJhVq965U7w==",
+ "version": "4.52.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.2.tgz",
+ "integrity": "sha512-vngduywkkv8Fkh3wIZf5nFPXzWsNsVu1kvtLETWxTFf/5opZmflgVSeLgdHR56RQh71xhPhWoOkEBvbehwTlVA==",
"cpu": [
"arm64"
],
@@ -3485,9 +3525,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
- "version": "4.41.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.1.tgz",
- "integrity": "sha512-egpJACny8QOdHNNMZKf8xY0Is6gIMz+tuqXlusxquWu3F833DcMwmGM7WlvCO9sB3OsPjdC4U0wHw5FabzCGZg==",
+ "version": "4.52.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.2.tgz",
+ "integrity": "sha512-h11KikYrUCYTrDj6h939hhMNlqU2fo/X4NB0OZcys3fya49o1hmFaczAiJWVAFgrM1NCP6RrO7lQKeVYSKBPSQ==",
"cpu": [
"x64"
],
@@ -3499,9 +3539,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
- "version": "4.41.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.1.tgz",
- "integrity": "sha512-DBVMZH5vbjgRk3r0OzgjS38z+atlupJ7xfKIDJdZZL6sM6wjfDNo64aowcLPKIx7LMQi8vybB56uh1Ftck/Atg==",
+ "version": "4.52.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.2.tgz",
+ "integrity": "sha512-/eg4CI61ZUkLXxMHyVlmlGrSQZ34xqWlZNW43IAU4RmdzWEx0mQJ2mN/Cx4IHLVZFL6UBGAh+/GXhgvGb+nVxw==",
"cpu": [
"arm64"
],
@@ -3513,9 +3553,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
- "version": "4.41.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.1.tgz",
- "integrity": "sha512-3FkydeohozEskBxNWEIbPfOE0aqQgB6ttTkJ159uWOFn42VLyfAiyD9UK5mhu+ItWzft60DycIN1Xdgiy8o/SA==",
+ "version": "4.52.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.2.tgz",
+ "integrity": "sha512-QOWgFH5X9+p+S1NAfOqc0z8qEpJIoUHf7OWjNUGOeW18Mx22lAUOiA9b6r2/vpzLdfxi/f+VWsYjUOMCcYh0Ng==",
"cpu": [
"x64"
],
@@ -3527,9 +3567,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
- "version": "4.41.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.1.tgz",
- "integrity": "sha512-wC53ZNDgt0pqx5xCAgNunkTzFE8GTgdZ9EwYGVcg+jEjJdZGtq9xPjDnFgfFozQI/Xm1mh+D9YlYtl+ueswNEg==",
+ "version": "4.52.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.2.tgz",
+ "integrity": "sha512-kDWSPafToDd8LcBYd1t5jw7bD5Ojcu12S3uT372e5HKPzQt532vW+rGFFOaiR0opxePyUkHrwz8iWYEyH1IIQA==",
"cpu": [
"arm"
],
@@ -3541,9 +3581,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
- "version": "4.41.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.1.tgz",
- "integrity": "sha512-jwKCca1gbZkZLhLRtsrka5N8sFAaxrGz/7wRJ8Wwvq3jug7toO21vWlViihG85ei7uJTpzbXZRcORotE+xyrLA==",
+ "version": "4.52.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.2.tgz",
+ "integrity": "sha512-gKm7Mk9wCv6/rkzwCiUC4KnevYhlf8ztBrDRT9g/u//1fZLapSRc+eDZj2Eu2wpJ+0RzUKgtNijnVIB4ZxyL+w==",
"cpu": [
"arm"
],
@@ -3555,9 +3595,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
- "version": "4.41.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.1.tgz",
- "integrity": "sha512-g0UBcNknsmmNQ8V2d/zD2P7WWfJKU0F1nu0k5pW4rvdb+BIqMm8ToluW/eeRmxCared5dD76lS04uL4UaNgpNA==",
+ "version": "4.52.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.2.tgz",
+ "integrity": "sha512-66lA8vnj5mB/rtDNwPgrrKUOtCLVQypkyDa2gMfOefXK6rcZAxKLO9Fy3GkW8VkPnENv9hBkNOFfGLf6rNKGUg==",
"cpu": [
"arm64"
],
@@ -3569,9 +3609,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
- "version": "4.41.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.1.tgz",
- "integrity": "sha512-XZpeGB5TKEZWzIrj7sXr+BEaSgo/ma/kCgrZgL0oo5qdB1JlTzIYQKel/RmhT6vMAvOdM2teYlAaOGJpJ9lahg==",
+ "version": "4.52.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.2.tgz",
+ "integrity": "sha512-s+OPucLNdJHvuZHuIz2WwncJ+SfWHFEmlC5nKMUgAelUeBUnlB4wt7rXWiyG4Zn07uY2Dd+SGyVa9oyLkVGOjA==",
"cpu": [
"arm64"
],
@@ -3582,10 +3622,10 @@
"linux"
]
},
- "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
- "version": "4.41.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.1.tgz",
- "integrity": "sha512-bkCfDJ4qzWfFRCNt5RVV4DOw6KEgFTUZi2r2RuYhGWC8WhCA8lCAJhDeAmrM/fdiAH54m0mA0Vk2FGRPyzI+tw==",
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.52.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.2.tgz",
+ "integrity": "sha512-8wTRM3+gVMDLLDdaT6tKmOE3lJyRy9NpJUS/ZRWmLCmOPIJhVyXwjBo+XbrrwtV33Em1/eCTd5TuGJm4+DmYjw==",
"cpu": [
"loong64"
],
@@ -3596,10 +3636,10 @@
"linux"
]
},
- "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
- "version": "4.41.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.1.tgz",
- "integrity": "sha512-3mr3Xm+gvMX+/8EKogIZSIEF0WUu0HL9di+YWlJpO8CQBnoLAEL/roTCxuLncEdgcfJcvA4UMOf+2dnjl4Ut1A==",
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.52.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.2.tgz",
+ "integrity": "sha512-6yqEfgJ1anIeuP2P/zhtfBlDpXUb80t8DpbYwXQ3bQd95JMvUaqiX+fKqYqUwZXqdJDd8xdilNtsHM2N0cFm6A==",
"cpu": [
"ppc64"
],
@@ -3611,9 +3651,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
- "version": "4.41.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.1.tgz",
- "integrity": "sha512-3rwCIh6MQ1LGrvKJitQjZFuQnT2wxfU+ivhNBzmxXTXPllewOF7JR1s2vMX/tWtUYFgphygxjqMl76q4aMotGw==",
+ "version": "4.52.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.2.tgz",
+ "integrity": "sha512-sshYUiYVSEI2B6dp4jMncwxbrUqRdNApF2c3bhtLAU0qA8Lrri0p0NauOsTWh3yCCCDyBOjESHMExonp7Nzc0w==",
"cpu": [
"riscv64"
],
@@ -3625,9 +3665,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
- "version": "4.41.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.1.tgz",
- "integrity": "sha512-LdIUOb3gvfmpkgFZuccNa2uYiqtgZAz3PTzjuM5bH3nvuy9ty6RGc/Q0+HDFrHrizJGVpjnTZ1yS5TNNjFlklw==",
+ "version": "4.52.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.2.tgz",
+ "integrity": "sha512-duBLgd+3pqC4MMwBrKkFxaZerUxZcYApQVC5SdbF5/e/589GwVvlRUnyqMFbM8iUSb1BaoX/3fRL7hB9m2Pj8Q==",
"cpu": [
"riscv64"
],
@@ -3639,9 +3679,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
- "version": "4.41.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.1.tgz",
- "integrity": "sha512-oIE6M8WC9ma6xYqjvPhzZYk6NbobIURvP/lEbh7FWplcMO6gn7MM2yHKA1eC/GvYwzNKK/1LYgqzdkZ8YFxR8g==",
+ "version": "4.52.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.2.tgz",
+ "integrity": "sha512-tzhYJJidDUVGMgVyE+PmxENPHlvvqm1KILjjZhB8/xHYqAGeizh3GBGf9u6WdJpZrz1aCpIIHG0LgJgH9rVjHQ==",
"cpu": [
"s390x"
],
@@ -3653,9 +3693,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
- "version": "4.41.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.1.tgz",
- "integrity": "sha512-cWBOvayNvA+SyeQMp79BHPK8ws6sHSsYnK5zDcsC3Hsxr1dgTABKjMnMslPq1DvZIp6uO7kIWhiGwaTdR4Og9A==",
+ "version": "4.52.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.2.tgz",
+ "integrity": "sha512-opH8GSUuVcCSSyHHcl5hELrmnk4waZoVpgn/4FDao9iyE4WpQhyWJ5ryl5M3ocp4qkRuHfyXnGqg8M9oKCEKRA==",
"cpu": [
"x64"
],
@@ -3667,9 +3707,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
- "version": "4.41.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.41.1.tgz",
- "integrity": "sha512-y5CbN44M+pUCdGDlZFzGGBSKCA4A/J2ZH4edTYSSxFg7ce1Xt3GtydbVKWLlzL+INfFIZAEg1ZV6hh9+QQf9YQ==",
+ "version": "4.52.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.2.tgz",
+ "integrity": "sha512-LSeBHnGli1pPKVJ79ZVJgeZWWZXkEe/5o8kcn23M8eMKCUANejchJbF/JqzM4RRjOJfNRhKJk8FuqL1GKjF5oQ==",
"cpu": [
"x64"
],
@@ -3680,10 +3720,24 @@
"linux"
]
},
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.52.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.2.tgz",
+ "integrity": "sha512-uPj7MQ6/s+/GOpolavm6BPo+6CbhbKYyZHUDvZ/SmJM7pfDBgdGisFX3bY/CBDMg2ZO4utfhlApkSfZ92yXw7Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
"node_modules/@rollup/rollup-win32-arm64-msvc": {
- "version": "4.41.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.1.tgz",
- "integrity": "sha512-lZkCxIrjlJlMt1dLO/FbpZbzt6J/A8p4DnqzSa4PWqPEUUUnzXLeki/iyPLfV0BmHItlYgHUqJe+3KiyydmiNQ==",
+ "version": "4.52.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.2.tgz",
+ "integrity": "sha512-Z9MUCrSgIaUeeHAiNkm3cQyst2UhzjPraR3gYYfOjAuZI7tcFRTOD+4cHLPoS/3qinchth+V56vtqz1Tv+6KPA==",
"cpu": [
"arm64"
],
@@ -3695,9 +3749,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
- "version": "4.41.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.1.tgz",
- "integrity": "sha512-+psFT9+pIh2iuGsxFYYa/LhS5MFKmuivRsx9iPJWNSGbh2XVEjk90fmpUEjCnILPEPJnikAU6SFDiEUyOv90Pg==",
+ "version": "4.52.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.2.tgz",
+ "integrity": "sha512-+GnYBmpjldD3XQd+HMejo+0gJGwYIOfFeoBQv32xF/RUIvccUz20/V6Otdv+57NE70D5pa8W/jVGDoGq0oON4A==",
"cpu": [
"ia32"
],
@@ -3708,10 +3762,24 @@
"win32"
]
},
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.52.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.2.tgz",
+ "integrity": "sha512-ApXFKluSB6kDQkAqZOKXBjiaqdF1BlKi+/eqnYe9Ee7U2K3pUDKsIyr8EYm/QDHTJIM+4X+lI0gJc3TTRhd+dA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
"node_modules/@rollup/rollup-win32-x64-msvc": {
- "version": "4.41.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.1.tgz",
- "integrity": "sha512-Wq2zpapRYLfi4aKxf2Xff0tN+7slj2d4R87WEzqw7ZLsVvO5zwYCIuEGSZYiK41+GlwUo1HiR+GdkLEJnCKTCw==",
+ "version": "4.52.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.2.tgz",
+ "integrity": "sha512-ARz+Bs8kY6FtitYM96PqPEVvPXqEZmPZsSkXvyX19YzDqkCaIlhCieLLMI5hxO9SRZ2XtCtm8wxhy0iJ2jxNfw==",
"cpu": [
"x64"
],
@@ -3736,9 +3804,9 @@
"license": "MIT"
},
"node_modules/@tanstack/query-core": {
- "version": "5.87.0",
- "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.87.0.tgz",
- "integrity": "sha512-gRZig2csRl71i/HEAHlE9TOmMqKKs9WkMAqIUlzagH+sNtgjvqxwaVo2HmfNGe+iDWUak0ratSkiRv0m/Y8ijg==",
+ "version": "5.90.2",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.2.tgz",
+ "integrity": "sha512-k/TcR3YalnzibscALLwxeiLUub6jN5EDLwKDiO7q5f4ICEoptJ+n9+7vcEFy5/x/i6Q+Lb/tXrsKCggf5uQJXQ==",
"license": "MIT",
"funding": {
"type": "github",
@@ -3746,9 +3814,9 @@
}
},
"node_modules/@tanstack/query-devtools": {
- "version": "5.86.0",
- "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.86.0.tgz",
- "integrity": "sha512-/JDw9BP80eambEK/EsDMGAcsL2VFT+8F5KCOwierjPU7QP8Wt1GT32yJpn3qOinBM8/zS3Jy36+F0GiyJp411A==",
+ "version": "5.90.1",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.90.1.tgz",
+ "integrity": "sha512-GtINOPjPUH0OegJExZ70UahT9ykmAhmtNVcmtdnOZbxLwT7R5OmRztR5Ahe3/Cu7LArEmR6/588tAycuaWb1xQ==",
"license": "MIT",
"funding": {
"type": "github",
@@ -3756,12 +3824,12 @@
}
},
"node_modules/@tanstack/react-query": {
- "version": "5.87.0",
- "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.87.0.tgz",
- "integrity": "sha512-3uRCGHo7KWHl6h7ptzLd5CbrjTQP5Q/37aC1cueClkSN4t/OaNFmfGolgs1AoA0kFjP/OZxTY2ytQoifyJzpWQ==",
+ "version": "5.90.2",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.2.tgz",
+ "integrity": "sha512-CLABiR+h5PYfOWr/z+vWFt5VsOA2ekQeRQBFSKlcoW6Ndx/f8rfyVmq4LbgOM4GG2qtxAxjLYLOpCNTYm4uKzw==",
"license": "MIT",
"dependencies": {
- "@tanstack/query-core": "5.87.0"
+ "@tanstack/query-core": "5.90.2"
},
"funding": {
"type": "github",
@@ -3772,26 +3840,26 @@
}
},
"node_modules/@tanstack/react-query-devtools": {
- "version": "5.87.0",
- "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.87.0.tgz",
- "integrity": "sha512-OeOSKsPyLcTVLdn391iNeRqYFEmpYJrY9t+FjKpaC6ql0SyRu2XT3mKYJIfYczhMMlwOIlbJkNaifBveertV8Q==",
+ "version": "5.90.2",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.90.2.tgz",
+ "integrity": "sha512-vAXJzZuBXtCQtrY3F/yUNJCV4obT/A/n81kb3+YqLbro5Z2+phdAbceO+deU3ywPw8B42oyJlp4FhO0SoivDFQ==",
"license": "MIT",
"dependencies": {
- "@tanstack/query-devtools": "5.86.0"
+ "@tanstack/query-devtools": "5.90.1"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
- "@tanstack/react-query": "^5.87.0",
+ "@tanstack/react-query": "^5.90.2",
"react": "^18 || ^19"
}
},
"node_modules/@testing-library/dom": {
- "version": "10.4.0",
- "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
- "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==",
+ "version": "10.4.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
+ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
"dev": true,
"license": "MIT",
"peer": true,
@@ -3800,9 +3868,9 @@
"@babel/runtime": "^7.12.5",
"@types/aria-query": "^5.0.1",
"aria-query": "5.3.0",
- "chalk": "^4.1.0",
"dom-accessibility-api": "^0.5.9",
"lz-string": "^1.5.0",
+ "picocolors": "1.1.1",
"pretty-format": "^27.0.2"
},
"engines": {
@@ -3810,18 +3878,17 @@
}
},
"node_modules/@testing-library/jest-dom": {
- "version": "6.6.3",
- "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz",
- "integrity": "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==",
+ "version": "6.8.0",
+ "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.8.0.tgz",
+ "integrity": "sha512-WgXcWzVM6idy5JaftTVC8Vs83NKRmGJz4Hqs4oyOuO2J4r/y79vvKZsb+CaGyCSEbUPI6OsewfPd0G1A0/TUZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@adobe/css-tools": "^4.4.0",
"aria-query": "^5.0.0",
- "chalk": "^3.0.0",
"css.escape": "^1.5.1",
"dom-accessibility-api": "^0.6.3",
- "lodash": "^4.17.21",
+ "picocolors": "^1.1.1",
"redent": "^3.0.0"
},
"engines": {
@@ -3830,20 +3897,6 @@
"yarn": ">=1"
}
},
- "node_modules/@testing-library/jest-dom/node_modules/chalk": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
- "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^4.1.0",
- "supports-color": "^7.1.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz",
@@ -3985,13 +4038,13 @@
}
},
"node_modules/@types/babel__traverse": {
- "version": "7.20.7",
- "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz",
- "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==",
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/types": "^7.20.7"
+ "@babel/types": "^7.28.2"
}
},
"node_modules/@types/debug": {
@@ -4004,9 +4057,9 @@
}
},
"node_modules/@types/estree": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
- "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"license": "MIT"
},
"node_modules/@types/estree-jsx": {
@@ -4050,9 +4103,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
- "version": "20.19.0",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.0.tgz",
- "integrity": "sha512-hfrc+1tud1xcdVTABC2JiomZJEklMcXYNTVtZLAeqTVWD+qL5jkHKT+1lOtqDdGxt+mB53DTtiz673vfjU8D1Q==",
+ "version": "20.19.17",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.17.tgz",
+ "integrity": "sha512-gfehUI8N1z92kygssiuWvLiwcbOB3IRktR6hTDgJlXMYh5OvkPSRmgfoBUmfZt+vhwJtX7v1Yw4KvvAf7c5QKQ==",
"devOptional": true,
"license": "MIT",
"dependencies": {
@@ -4060,15 +4113,15 @@
}
},
"node_modules/@types/prop-types": {
- "version": "15.7.14",
- "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
- "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==",
+ "version": "15.7.15",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
+ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
"license": "MIT"
},
"node_modules/@types/react": {
- "version": "18.3.23",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz",
- "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==",
+ "version": "18.3.24",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.24.tgz",
+ "integrity": "sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==",
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
@@ -4252,32 +4305,6 @@
}
}
},
- "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
- "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0"
- }
- },
- "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
- "version": "9.0.3",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
- "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "brace-expansion": "^2.0.1"
- },
- "engines": {
- "node": ">=16 || 14 >=14.17"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
"node_modules/@typescript-eslint/utils": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz",
@@ -4329,16 +4356,16 @@
"license": "ISC"
},
"node_modules/@vitejs/plugin-react": {
- "version": "4.5.0",
- "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.5.0.tgz",
- "integrity": "sha512-JuLWaEqypaJmOJPLWwO335Ig6jSgC1FTONCWAxnqcQthLTK/Yc9aH6hr9z/87xciejbQcnP3GnA1FWUSWeXaeg==",
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/core": "^7.26.10",
- "@babel/plugin-transform-react-jsx-self": "^7.25.9",
- "@babel/plugin-transform-react-jsx-source": "^7.25.9",
- "@rolldown/pluginutils": "1.0.0-beta.9",
+ "@babel/core": "^7.28.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.27",
"@types/babel__core": "^7.20.5",
"react-refresh": "^0.17.0"
},
@@ -4346,7 +4373,7 @@
"node": "^14.18.0 || >=16.0.0"
},
"peerDependencies": {
- "vite": "^4.2.0 || ^5.0.0 || ^6.0.0"
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
}
},
"node_modules/@vitest/coverage-v8": {
@@ -4573,9 +4600,9 @@
"license": "MIT"
},
"node_modules/acorn": {
- "version": "8.14.1",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
- "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
+ "version": "8.15.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
+ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
@@ -4607,9 +4634,9 @@
}
},
"node_modules/agent-base": {
- "version": "7.1.3",
- "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
- "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4856,6 +4883,16 @@
],
"license": "MIT"
},
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.8.6",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.6.tgz",
+ "integrity": "sha512-wrH5NNqren/QMtKUEEJf7z86YjfqW/2uw3IL3/xpqZUC95SSVIFXYQeeGjL6FT/X68IROu6RMehZQS5foy2BXw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.js"
+ }
+ },
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@@ -4870,14 +4907,13 @@
}
},
"node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
+ "balanced-match": "^1.0.0"
}
},
"node_modules/braces": {
@@ -4894,9 +4930,9 @@
}
},
"node_modules/browserslist": {
- "version": "4.25.0",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz",
- "integrity": "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==",
+ "version": "4.26.2",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz",
+ "integrity": "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==",
"dev": true,
"funding": [
{
@@ -4914,9 +4950,10 @@
],
"license": "MIT",
"dependencies": {
- "caniuse-lite": "^1.0.30001718",
- "electron-to-chromium": "^1.5.160",
- "node-releases": "^2.0.19",
+ "baseline-browser-mapping": "^2.8.3",
+ "caniuse-lite": "^1.0.30001741",
+ "electron-to-chromium": "^1.5.218",
+ "node-releases": "^2.0.21",
"update-browserslist-db": "^1.1.3"
},
"bin": {
@@ -5031,9 +5068,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001720",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001720.tgz",
- "integrity": "sha512-Ec/2yV2nNPwb4DnTANEV99ZWwm3ZWfdlfkQbWSDDt+PsXEVYwlhPH8tdMaPunYTKKmz7AnHi2oNEi1GcmKCD8g==",
+ "version": "1.0.30001745",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001745.tgz",
+ "integrity": "sha512-ywt6i8FzvdgrrrGbr1jZVObnVv6adj+0if2/omv9cmR2oiZs30zL4DIyaptKcbOrBdOIc74QTMoJvSE2QHh5UQ==",
"dev": true,
"funding": [
{
@@ -5365,13 +5402,13 @@
}
},
"node_modules/cssstyle": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.3.1.tgz",
- "integrity": "sha512-ZgW+Jgdd7i52AaLYCriF8Mxqft0gD/R9i9wi6RWBhs1pqdPEzPjym7rvRKi397WmQFf3SlyUsszhw+VVCbx79Q==",
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
+ "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@asamuzakjp/css-color": "^3.1.2",
+ "@asamuzakjp/css-color": "^3.2.0",
"rrweb-cssom": "^0.8.0"
},
"engines": {
@@ -5429,9 +5466,9 @@
}
},
"node_modules/debug": {
- "version": "4.4.1",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
- "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@@ -5446,16 +5483,16 @@
}
},
"node_modules/decimal.js": {
- "version": "10.5.0",
- "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz",
- "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==",
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
+ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
"dev": true,
"license": "MIT"
},
"node_modules/decode-named-character-reference": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.1.0.tgz",
- "integrity": "sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==",
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz",
+ "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==",
"license": "MIT",
"dependencies": {
"character-entities": "^2.0.0"
@@ -5721,9 +5758,9 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
- "version": "1.5.161",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.161.tgz",
- "integrity": "sha512-hwtetwfKNZo/UlwHIVBlKZVdy7o8bIZxxKs0Mv/ROPiQQQmDgdm5a+KvKtBsxM8ZjFzTaCeLoodZ8jiBE3o9rA==",
+ "version": "1.5.223",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.223.tgz",
+ "integrity": "sha512-qKm55ic6nbEmagFlTFczML33rF90aU+WtrJ9MdTCThrcvDNdUHN4p6QfVN78U06ZmguqXIyMPyYhw2TrbDUwPQ==",
"dev": true,
"license": "ISC"
},
@@ -5735,9 +5772,9 @@
"license": "MIT"
},
"node_modules/entities": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz",
- "integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
@@ -5996,29 +6033,16 @@
}
},
"node_modules/eslint-plugin-react-refresh": {
- "version": "0.4.20",
- "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.20.tgz",
- "integrity": "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==",
+ "version": "0.4.21",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.21.tgz",
+ "integrity": "sha512-MWDWTtNC4voTcWDxXbdmBNe8b/TxfxRFUL6hXgKWJjN9c1AagYEmpiFWBWzDw+5H3SulWUe1pJKTnoSdmk88UA==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"eslint": ">=8.40"
}
},
- "node_modules/eslint-visitor-keys": {
- "version": "3.4.3",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
- "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
- "node_modules/eslint/node_modules/eslint-scope": {
+ "node_modules/eslint-scope": {
"version": "7.2.2",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
"integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
@@ -6035,30 +6059,41 @@
"url": "https://opencollective.com/eslint"
}
},
- "node_modules/eslint/node_modules/estraverse": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
- "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
"dev": true,
- "license": "BSD-2-Clause",
+ "license": "Apache-2.0",
"engines": {
- "node": ">=4.0"
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
}
},
- "node_modules/eslint/node_modules/globals": {
- "version": "13.24.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
- "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
+ "node_modules/eslint/node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "type-fest": "^0.20.2"
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/eslint/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
},
"engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "node": "*"
}
},
"node_modules/esniff": {
@@ -6107,16 +6142,6 @@
"node": ">=0.10"
}
},
- "node_modules/esquery/node_modules/estraverse": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
- "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
- "dev": true,
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">=4.0"
- }
- },
"node_modules/esrecurse": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
@@ -6130,7 +6155,7 @@
"node": ">=4.0"
}
},
- "node_modules/esrecurse/node_modules/estraverse": {
+ "node_modules/estraverse": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
@@ -6412,15 +6437,16 @@
}
},
"node_modules/form-data": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
- "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
+ "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"dev": true,
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
@@ -6643,14 +6669,44 @@
"node": ">=10.13.0"
}
},
+ "node_modules/glob/node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/glob/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/globals": {
- "version": "11.12.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
- "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
+ "version": "13.24.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
+ "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
"dev": true,
"license": "MIT",
+ "dependencies": {
+ "type-fest": "^0.20.2"
+ },
"engines": {
- "node": ">=4"
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/globby": {
@@ -7486,9 +7542,9 @@
}
},
"node_modules/istanbul-reports": {
- "version": "3.1.7",
- "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz",
- "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==",
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
+ "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -7665,9 +7721,9 @@
}
},
"node_modules/lexical": {
- "version": "0.33.1",
- "resolved": "https://registry.npmjs.org/lexical/-/lexical-0.33.1.tgz",
- "integrity": "sha512-+kiCS/GshQmCs/meMb8MQT4AMvw3S3Ef0lSCv2Xi6Itvs59OD+NjQWNfYkDteIbKtVE/w0Yiqh56VyGwIb8UcA==",
+ "version": "0.35.0",
+ "resolved": "https://registry.npmjs.org/lexical/-/lexical-0.35.0.tgz",
+ "integrity": "sha512-3VuV8xXhh5xJA6tzvfDvE0YBCMkIZUmxtRilJQDDdCgJCc+eut6qAv2qbN+pbqvarqcQqPN1UF+8YvsjmyOZpw==",
"license": "MIT"
},
"node_modules/lib0": {
@@ -7745,13 +7801,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/lodash": {
- "version": "4.17.21",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
- "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -7820,13 +7869,13 @@
}
},
"node_modules/magic-string": {
- "version": "0.30.17",
- "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
- "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
+ "version": "0.30.19",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz",
+ "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@jridgewell/sourcemap-codec": "^1.5.0"
+ "@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/magicast": {
@@ -8891,9 +8940,9 @@
}
},
"node_modules/mime-db": {
- "version": "1.52.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
- "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
@@ -8912,6 +8961,16 @@
"node": ">= 0.6"
}
},
+ "node_modules/mime-types/node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/mimic-fn": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
@@ -8936,16 +8995,19 @@
}
},
"node_modules/minimatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "version": "9.0.3",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
+ "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
"dev": true,
"license": "ISC",
"dependencies": {
- "brace-expansion": "^1.1.7"
+ "brace-expansion": "^2.0.1"
},
"engines": {
- "node": "*"
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/minipass": {
@@ -8959,16 +9021,16 @@
}
},
"node_modules/mlly": {
- "version": "1.7.4",
- "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz",
- "integrity": "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==",
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz",
+ "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "acorn": "^8.14.0",
- "pathe": "^2.0.1",
- "pkg-types": "^1.3.0",
- "ufo": "^1.5.4"
+ "acorn": "^8.15.0",
+ "pathe": "^2.0.3",
+ "pkg-types": "^1.3.1",
+ "ufo": "^1.6.1"
}
},
"node_modules/mlly/node_modules/pathe": {
@@ -9031,9 +9093,9 @@
}
},
"node_modules/nanoid": {
- "version": "5.1.5",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.5.tgz",
- "integrity": "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==",
+ "version": "5.1.6",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz",
+ "integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==",
"funding": [
{
"type": "github",
@@ -9062,9 +9124,9 @@
"license": "ISC"
},
"node_modules/node-releases": {
- "version": "2.0.19",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
- "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
+ "version": "2.0.21",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz",
+ "integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==",
"dev": true,
"license": "MIT"
},
@@ -9118,9 +9180,9 @@
}
},
"node_modules/nwsapi": {
- "version": "2.2.20",
- "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz",
- "integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==",
+ "version": "2.2.22",
+ "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.22.tgz",
+ "integrity": "sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==",
"dev": true,
"license": "MIT"
},
@@ -9549,10 +9611,20 @@
}
},
"node_modules/postcss-js": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
- "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
+ "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
"dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
"license": "MIT",
"dependencies": {
"camelcase-css": "^2.0.1"
@@ -9560,10 +9632,6 @@
"engines": {
"node": "^12 || ^14 || >= 16"
},
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
- },
"peerDependencies": {
"postcss": "^8.4.21"
}
@@ -9885,9 +9953,9 @@
}
},
"node_modules/react-hook-form": {
- "version": "7.62.0",
- "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.62.0.tgz",
- "integrity": "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==",
+ "version": "7.63.0",
+ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.63.0.tgz",
+ "integrity": "sha512-ZwueDMvUeucovM2VjkCf7zIHcs1aAlDimZu2Hvel5C5907gUzMpm4xCrQXtRzCvsBqFjonB4m3x4LzCFI1ZKWA==",
"license": "MIT",
"engines": {
"node": ">=18.0.0"
@@ -9900,6 +9968,15 @@
"react": "^16.8.0 || ^17 || ^18 || ^19"
}
},
+ "node_modules/react-icons": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz",
+ "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "*"
+ }
+ },
"node_modules/react-is": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
@@ -10211,13 +10288,13 @@
}
},
"node_modules/rollup": {
- "version": "4.41.1",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.1.tgz",
- "integrity": "sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw==",
+ "version": "4.52.2",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.2.tgz",
+ "integrity": "sha512-I25/2QgoROE1vYV+NQ1En9T9UFB9Cmfm2CJ83zZOlaDpvz29wGQSZXWKw7MiNXau7wYgB/T9fVIdIuEQ+KbiiA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@types/estree": "1.0.7"
+ "@types/estree": "1.0.8"
},
"bin": {
"rollup": "dist/bin/rollup"
@@ -10227,26 +10304,28 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
- "@rollup/rollup-android-arm-eabi": "4.41.1",
- "@rollup/rollup-android-arm64": "4.41.1",
- "@rollup/rollup-darwin-arm64": "4.41.1",
- "@rollup/rollup-darwin-x64": "4.41.1",
- "@rollup/rollup-freebsd-arm64": "4.41.1",
- "@rollup/rollup-freebsd-x64": "4.41.1",
- "@rollup/rollup-linux-arm-gnueabihf": "4.41.1",
- "@rollup/rollup-linux-arm-musleabihf": "4.41.1",
- "@rollup/rollup-linux-arm64-gnu": "4.41.1",
- "@rollup/rollup-linux-arm64-musl": "4.41.1",
- "@rollup/rollup-linux-loongarch64-gnu": "4.41.1",
- "@rollup/rollup-linux-powerpc64le-gnu": "4.41.1",
- "@rollup/rollup-linux-riscv64-gnu": "4.41.1",
- "@rollup/rollup-linux-riscv64-musl": "4.41.1",
- "@rollup/rollup-linux-s390x-gnu": "4.41.1",
- "@rollup/rollup-linux-x64-gnu": "4.41.1",
- "@rollup/rollup-linux-x64-musl": "4.41.1",
- "@rollup/rollup-win32-arm64-msvc": "4.41.1",
- "@rollup/rollup-win32-ia32-msvc": "4.41.1",
- "@rollup/rollup-win32-x64-msvc": "4.41.1",
+ "@rollup/rollup-android-arm-eabi": "4.52.2",
+ "@rollup/rollup-android-arm64": "4.52.2",
+ "@rollup/rollup-darwin-arm64": "4.52.2",
+ "@rollup/rollup-darwin-x64": "4.52.2",
+ "@rollup/rollup-freebsd-arm64": "4.52.2",
+ "@rollup/rollup-freebsd-x64": "4.52.2",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.52.2",
+ "@rollup/rollup-linux-arm-musleabihf": "4.52.2",
+ "@rollup/rollup-linux-arm64-gnu": "4.52.2",
+ "@rollup/rollup-linux-arm64-musl": "4.52.2",
+ "@rollup/rollup-linux-loong64-gnu": "4.52.2",
+ "@rollup/rollup-linux-ppc64-gnu": "4.52.2",
+ "@rollup/rollup-linux-riscv64-gnu": "4.52.2",
+ "@rollup/rollup-linux-riscv64-musl": "4.52.2",
+ "@rollup/rollup-linux-s390x-gnu": "4.52.2",
+ "@rollup/rollup-linux-x64-gnu": "4.52.2",
+ "@rollup/rollup-linux-x64-musl": "4.52.2",
+ "@rollup/rollup-openharmony-arm64": "4.52.2",
+ "@rollup/rollup-win32-arm64-msvc": "4.52.2",
+ "@rollup/rollup-win32-ia32-msvc": "4.52.2",
+ "@rollup/rollup-win32-x64-gnu": "4.52.2",
+ "@rollup/rollup-win32-x64-msvc": "4.52.2",
"fsevents": "~2.3.2"
}
},
@@ -10639,9 +10718,9 @@
"license": "MIT"
},
"node_modules/string-width/node_modules/ansi-regex": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
- "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+ "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -10652,9 +10731,9 @@
}
},
"node_modules/string-width/node_modules/strip-ansi": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
- "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
+ "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -10814,16 +10893,6 @@
"node": ">=16 || 14 >=14.17"
}
},
- "node_modules/sucrase/node_modules/brace-expansion": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
- "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0"
- }
- },
"node_modules/sucrase/node_modules/glob": {
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
@@ -10901,9 +10970,9 @@
"license": "MIT"
},
"node_modules/tailwind-merge": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.0.tgz",
- "integrity": "sha512-fyW/pEfcQSiigd5SNn0nApUOxx0zB/dm6UDU/rEwc2c3sX2smWUNbapHv+QRqLGVp9GWX3THIa7MUGPo+YkDzQ==",
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz",
+ "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==",
"license": "MIT",
"funding": {
"type": "github",
@@ -10963,6 +11032,30 @@
"node": ">=8"
}
},
+ "node_modules/test-exclude/node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/test-exclude/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@@ -11212,9 +11305,9 @@
}
},
"node_modules/typescript": {
- "version": "5.8.3",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
- "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
+ "version": "5.9.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
+ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -11518,9 +11611,9 @@
}
},
"node_modules/vfile-message": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz",
- "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==",
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
+ "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==",
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0",
@@ -11532,9 +11625,9 @@
}
},
"node_modules/vite": {
- "version": "5.4.19",
- "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz",
- "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==",
+ "version": "5.4.20",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.20.tgz",
+ "integrity": "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -11910,9 +12003,9 @@
}
},
"node_modules/wrap-ansi/node_modules/ansi-regex": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
- "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+ "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -11923,9 +12016,9 @@
}
},
"node_modules/wrap-ansi/node_modules/ansi-styles": {
- "version": "6.2.1",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
- "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
+ "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -11936,9 +12029,9 @@
}
},
"node_modules/wrap-ansi/node_modules/strip-ansi": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
- "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
+ "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -11959,9 +12052,9 @@
"license": "ISC"
},
"node_modules/ws": {
- "version": "8.18.2",
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz",
- "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==",
+ "version": "8.18.3",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
+ "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -12005,9 +12098,9 @@
"license": "ISC"
},
"node_modules/yaml": {
- "version": "2.8.0",
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz",
- "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==",
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
+ "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
"dev": true,
"license": "ISC",
"bin": {
@@ -12059,9 +12152,9 @@
}
},
"node_modules/zod": {
- "version": "3.25.46",
- "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.46.tgz",
- "integrity": "sha512-IqRxcHEIjqLd4LNS/zKffB3Jzg3NwqJxQQ0Ns7pdrvgGkwQsEBdEQcOHaBVqvvZArShRzI39+aMST3FBGmTrLQ==",
+ "version": "3.25.76",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
+ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
diff --git a/archon-ui-main/package.json b/archon-ui-main/package.json
index 31c07574f3..a0f86e81d6 100644
--- a/archon-ui-main/package.json
+++ b/archon-ui-main/package.json
@@ -50,6 +50,7 @@
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.3.1",
+ "react-icons": "^5.5.0",
"react-markdown": "^10.1.0",
"react-router-dom": "^6.26.2",
"tailwind-merge": "latest",
diff --git a/archon-ui-main/src/components/settings/RAGSettings.tsx b/archon-ui-main/src/components/settings/RAGSettings.tsx
index ccba61ce94..432040ccdb 100644
--- a/archon-ui-main/src/components/settings/RAGSettings.tsx
+++ b/archon-ui-main/src/components/settings/RAGSettings.tsx
@@ -1,9 +1,12 @@
import React, { useState, useEffect, useRef } from 'react';
-import { Settings, Check, Save, Loader, ChevronDown, ChevronUp, Zap, Database, Trash2 } from 'lucide-react';
+import { Settings, Check, Save, Loader, ChevronDown, ChevronUp, Zap, Database, Trash2, Cog } from 'lucide-react';
import { Card } from '../ui/Card';
import { Input } from '../ui/Input';
import { Select } from '../ui/Select';
import { Button } from '../ui/Button';
+import { Button as GlowButton } from '../../features/ui/primitives/button';
+import { LuBrainCircuit } from 'react-icons/lu';
+import { PiDatabaseThin } from 'react-icons/pi';
import { useToast } from '../../features/shared/hooks/useToast';
import { credentialsService } from '../../services/credentialsService';
import OllamaModelDiscoveryModal from './OllamaModelDiscoveryModal';
@@ -11,6 +14,9 @@ import OllamaModelSelectionModal from './OllamaModelSelectionModal';
type ProviderKey = 'openai' | 'google' | 'ollama' | 'anthropic' | 'grok' | 'openrouter';
+// Providers that support embedding models
+const EMBEDDING_CAPABLE_PROVIDERS: ProviderKey[] = ['openai', 'google', 'ollama'];
+
interface ProviderModels {
chatModel: string;
embeddingModel: string;
@@ -106,6 +112,9 @@ const providerAlertMessages: Record = {
const isProviderKey = (value: unknown): value is ProviderKey =>
typeof value === 'string' && ['openai', 'google', 'openrouter', 'ollama', 'anthropic', 'grok'].includes(value);
+// Default base URL for Ollama instances when not explicitly configured
+const DEFAULT_OLLAMA_URL = 'http://host.docker.internal:11434/v1';
+
interface RAGSettingsProps {
ragSettings: {
MODEL_CHOICE: string;
@@ -118,6 +127,7 @@ interface RAGSettingsProps {
LLM_BASE_URL?: string;
LLM_INSTANCE_NAME?: string;
EMBEDDING_MODEL?: string;
+ EMBEDDING_PROVIDER?: string;
OLLAMA_EMBEDDING_URL?: string;
OLLAMA_EMBEDDING_INSTANCE_NAME?: string;
// Crawling Performance Settings
@@ -148,6 +158,7 @@ export const RAGSettings = ({
const [showCrawlingSettings, setShowCrawlingSettings] = useState(false);
const [showStorageSettings, setShowStorageSettings] = useState(false);
const [showModelDiscoveryModal, setShowModelDiscoveryModal] = useState(false);
+ const [showOllamaConfig, setShowOllamaConfig] = useState(false);
// Edit modals state
const [showEditLLMModal, setShowEditLLMModal] = useState(false);
@@ -160,6 +171,16 @@ export const RAGSettings = ({
// Provider-specific model persistence state
const [providerModels, setProviderModels] = useState(() => loadProviderModels());
+ // Independent provider selection state
+ const [chatProvider, setChatProvider] = useState(() =>
+ (ragSettings.LLM_PROVIDER as ProviderKey) || 'openai'
+ );
+ const [embeddingProvider, setEmbeddingProvider] = useState(() =>
+ // Default to openai if no specific embedding provider is set
+ (ragSettings.EMBEDDING_PROVIDER as ProviderKey) || 'openai'
+ );
+ const [activeSelection, setActiveSelection] = useState<'chat' | 'embedding'>('chat');
+
// Instance configurations
const [llmInstanceConfig, setLLMInstanceConfig] = useState({
name: '',
@@ -215,16 +236,32 @@ export const RAGSettings = ({
}
}, [ragSettings.OLLAMA_EMBEDDING_URL, ragSettings.OLLAMA_EMBEDDING_INSTANCE_NAME]);
- // Provider model persistence effects
+ // Provider model persistence effects - separate for chat and embedding
+ useEffect(() => {
+ // Update chat provider models when chat model changes
+ if (chatProvider && ragSettings.MODEL_CHOICE) {
+ setProviderModels(prev => {
+ const updated = {
+ ...prev,
+ [chatProvider]: {
+ ...prev[chatProvider],
+ chatModel: ragSettings.MODEL_CHOICE
+ }
+ };
+ saveProviderModels(updated);
+ return updated;
+ });
+ }
+ }, [ragSettings.MODEL_CHOICE, chatProvider]);
+
useEffect(() => {
- // Update provider models when current models change
- const currentProvider = ragSettings.LLM_PROVIDER as ProviderKey;
- if (currentProvider && ragSettings.MODEL_CHOICE && ragSettings.EMBEDDING_MODEL) {
+ // Update embedding provider models when embedding model changes
+ if (embeddingProvider && ragSettings.EMBEDDING_MODEL) {
setProviderModels(prev => {
const updated = {
...prev,
- [currentProvider]: {
- chatModel: ragSettings.MODEL_CHOICE,
+ [embeddingProvider]: {
+ ...prev[embeddingProvider],
embeddingModel: ragSettings.EMBEDDING_MODEL
}
};
@@ -232,7 +269,7 @@ export const RAGSettings = ({
return updated;
});
}
- }, [ragSettings.MODEL_CHOICE, ragSettings.EMBEDDING_MODEL, ragSettings.LLM_PROVIDER]);
+ }, [ragSettings.EMBEDDING_MODEL, embeddingProvider]);
// Load API credentials for status checking
useEffect(() => {
@@ -305,10 +342,53 @@ export const RAGSettings = ({
return () => clearInterval(interval);
}, [ragSettings.LLM_PROVIDER]); // Only restart interval if provider changes
-
+
+ // Sync independent provider states with ragSettings (one-way: ragSettings -> local state)
+ useEffect(() => {
+ if (ragSettings.LLM_PROVIDER && ragSettings.LLM_PROVIDER !== chatProvider) {
+ setChatProvider(ragSettings.LLM_PROVIDER as ProviderKey);
+ }
+ }, [ragSettings.LLM_PROVIDER]); // Remove chatProvider dependency to avoid loops
+
+ useEffect(() => {
+ if (ragSettings.EMBEDDING_PROVIDER && ragSettings.EMBEDDING_PROVIDER !== embeddingProvider) {
+ setEmbeddingProvider(ragSettings.EMBEDDING_PROVIDER as ProviderKey);
+ }
+ }, [ragSettings.EMBEDDING_PROVIDER]); // Remove embeddingProvider dependency to avoid loops
+
+ // Update ragSettings when independent providers change (one-way: local state -> ragSettings)
+ // Split the βfirstβrunβ guard into two refs so chat and embedding effects donβt interfere.
+ const updateChatRagSettingsRef = useRef(false);
+ const updateEmbeddingRagSettingsRef = useRef(false);
+
+ useEffect(() => {
+ // Only update if this is a userβinitiated change, not a sync from ragSettings
+ if (updateChatRagSettingsRef.current && chatProvider !== ragSettings.LLM_PROVIDER) {
+ setRagSettings(prev => ({
+ ...prev,
+ LLM_PROVIDER: chatProvider
+ }));
+ }
+ updateChatRagSettingsRef.current = true;
+ }, [chatProvider]);
+
+ useEffect(() => {
+ // Only update if this is a userβinitiated change, not a sync from ragSettings
+ if (updateEmbeddingRagSettingsRef.current && embeddingProvider && embeddingProvider !== ragSettings.EMBEDDING_PROVIDER) {
+ setRagSettings(prev => ({
+ ...prev,
+ EMBEDDING_PROVIDER: embeddingProvider
+ }));
+ }
+ updateEmbeddingRagSettingsRef.current = true;
+ }, [embeddingProvider]);
+
+
// Status tracking
const [llmStatus, setLLMStatus] = useState({ online: false, responseTime: null, checking: false });
const [embeddingStatus, setEmbeddingStatus] = useState({ online: false, responseTime: null, checking: false });
+ const llmRetryTimeoutRef = useRef(null);
+ const embeddingRetryTimeoutRef = useRef(null);
// API key credentials for status checking
const [apiCredentials, setApiCredentials] = useState<{[key: string]: string}>({});
@@ -317,6 +397,19 @@ export const RAGSettings = ({
[key: string]: { connected: boolean; checking: boolean; lastChecked?: Date }
}>({});
+ useEffect(() => {
+ return () => {
+ if (llmRetryTimeoutRef.current) {
+ clearTimeout(llmRetryTimeoutRef.current);
+ llmRetryTimeoutRef.current = null;
+ }
+ if (embeddingRetryTimeoutRef.current) {
+ clearTimeout(embeddingRetryTimeoutRef.current);
+ embeddingRetryTimeoutRef.current = null;
+ }
+ };
+ }, []);
+
// Test connection to external providers
const testProviderConnection = async (provider: string): Promise => {
setProviderConnectionStatus(prev => ({
@@ -444,7 +537,12 @@ export const RAGSettings = ({
};
// Manual test function with user feedback using backend proxy
- const manualTestConnection = async (url: string, setStatus: React.Dispatch>, instanceName: string) => {
+ const manualTestConnection = async (
+ url: string,
+ setStatus: React.Dispatch>,
+ instanceName: string,
+ context?: 'chat' | 'embedding'
+ ): Promise => {
setStatus(prev => ({ ...prev, checking: true }));
const startTime = Date.now();
@@ -471,31 +569,50 @@ export const RAGSettings = ({
if (instanceStatus?.is_healthy) {
const responseTime = Math.round(instanceStatus.response_time_ms || (Date.now() - startTime));
setStatus({ online: true, responseTime, checking: false });
- showToast(`${instanceName} connection successful: ${instanceStatus.models_available || 0} models available (${responseTime}ms)`, 'success');
-
+
+ // Context-aware model count display
+ let modelCount = instanceStatus.models_available || 0;
+ let modelType = 'models';
+
+ if (context === 'chat') {
+ modelCount = ollamaMetrics.llmInstanceModels?.chat || 0;
+ modelType = 'chat models';
+ } else if (context === 'embedding') {
+ modelCount = ollamaMetrics.embeddingInstanceModels?.embedding || 0;
+ modelType = 'embedding models';
+ }
+
+ showToast(`${instanceName} connection successful: ${modelCount} ${modelType} available (${responseTime}ms)`, 'success');
+
// Scenario 2: Manual "Test Connection" button - refresh Ollama metrics if Ollama provider is selected
if (ragSettings.LLM_PROVIDER === 'ollama') {
console.log('π Fetching Ollama metrics - Test Connection button clicked');
fetchOllamaMetrics();
}
+
+ return true;
} else {
setStatus({ online: false, responseTime: null, checking: false });
showToast(`${instanceName} connection failed: ${instanceStatus?.error_message || 'Instance is not healthy'}`, 'error');
+ return false;
}
} else {
setStatus({ online: false, responseTime: null, checking: false });
showToast(`${instanceName} connection failed: Backend proxy error (HTTP ${response.status})`, 'error');
+ return false;
}
} catch (error: any) {
setStatus({ online: false, responseTime: null, checking: false });
-
+
if (error.name === 'AbortError') {
showToast(`${instanceName} connection failed: Request timeout (>15s)`, 'error');
} else {
showToast(`${instanceName} connection failed: ${error.message || 'Unknown error'}`, 'error');
}
+
+ return false;
}
- };;
+ };
// Function to handle LLM instance deletion
const handleDeleteLLMInstance = () => {
@@ -737,7 +854,7 @@ export const RAGSettings = ({
return googleConnected ? 'configured' : 'missing';
case 'ollama':
- // Check if both LLM and embedding instances are configured and online
+ if (llmStatus.checking || embeddingStatus.checking) return 'partial';
if (llmStatus.online && embeddingStatus.online) return 'configured';
if (llmStatus.online || embeddingStatus.online) return 'partial';
return 'missing';
@@ -778,6 +895,90 @@ export const RAGSettings = ({
? providerAlertMessages[selectedProviderKey]
: '';
+ useEffect(() => {
+ if (chatProvider !== 'ollama') {
+ return;
+ }
+
+ const baseUrl = (ragSettings.LLM_BASE_URL && ragSettings.LLM_BASE_URL.trim().length > 0)
+ ? ragSettings.LLM_BASE_URL.trim()
+ : DEFAULT_OLLAMA_URL;
+
+ if (!baseUrl) {
+ return;
+ }
+
+ const instanceName = (ragSettings.LLM_INSTANCE_NAME && ragSettings.LLM_INSTANCE_NAME.trim().length > 0)
+ ? ragSettings.LLM_INSTANCE_NAME.trim()
+ : 'LLM Instance';
+
+ if (llmRetryTimeoutRef.current) {
+ clearTimeout(llmRetryTimeoutRef.current);
+ llmRetryTimeoutRef.current = null;
+ }
+
+ const runTest = async () => {
+ const success = await manualTestConnection(baseUrl, setLLMStatus, instanceName, 'chat');
+
+ if (!success && chatProvider === 'ollama') {
+ llmRetryTimeoutRef.current = window.setTimeout(runTest, 5000);
+ }
+ };
+
+ setLLMStatus(prev => ({ ...prev, checking: true }));
+ llmRetryTimeoutRef.current = window.setTimeout(runTest, 100);
+
+ return () => {
+ if (llmRetryTimeoutRef.current) {
+ clearTimeout(llmRetryTimeoutRef.current);
+ llmRetryTimeoutRef.current = null;
+ }
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [chatProvider, ragSettings.LLM_BASE_URL, ragSettings.LLM_INSTANCE_NAME]);
+
+ useEffect(() => {
+ if (embeddingProvider !== 'ollama') {
+ return;
+ }
+
+ const baseUrl = (ragSettings.OLLAMA_EMBEDDING_URL && ragSettings.OLLAMA_EMBEDDING_URL.trim().length > 0)
+ ? ragSettings.OLLAMA_EMBEDDING_URL.trim()
+ : DEFAULT_OLLAMA_URL;
+
+ if (!baseUrl) {
+ return;
+ }
+
+ const instanceName = (ragSettings.OLLAMA_EMBEDDING_INSTANCE_NAME && ragSettings.OLLAMA_EMBEDDING_INSTANCE_NAME.trim().length > 0)
+ ? ragSettings.OLLAMA_EMBEDDING_INSTANCE_NAME.trim()
+ : 'Embedding Instance';
+
+ if (embeddingRetryTimeoutRef.current) {
+ clearTimeout(embeddingRetryTimeoutRef.current);
+ embeddingRetryTimeoutRef.current = null;
+ }
+
+ const runTest = async () => {
+ const success = await manualTestConnection(baseUrl, setEmbeddingStatus, instanceName, 'embedding');
+
+ if (!success && embeddingProvider === 'ollama') {
+ embeddingRetryTimeoutRef.current = window.setTimeout(runTest, 5000);
+ }
+ };
+
+ setEmbeddingStatus(prev => ({ ...prev, checking: true }));
+ embeddingRetryTimeoutRef.current = window.setTimeout(runTest, 100);
+
+ return () => {
+ if (embeddingRetryTimeoutRef.current) {
+ clearTimeout(embeddingRetryTimeoutRef.current);
+ embeddingRetryTimeoutRef.current = null;
+ }
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [embeddingProvider, ragSettings.OLLAMA_EMBEDDING_URL, ragSettings.OLLAMA_EMBEDDING_INSTANCE_NAME]);
+
// Test Ollama connectivity when Settings page loads (scenario 4: page load)
// This useEffect is placed after function definitions to ensure access to manualTestConnection
useEffect(() => {
@@ -806,7 +1007,7 @@ export const RAGSettings = ({
setTimeout(() => {
const instanceName = llmInstanceConfig.name || 'LLM Instance';
console.log('π Testing LLM instance on page load:', instanceName, llmInstanceConfig.url);
- manualTestConnection(llmInstanceConfig.url, setLLMStatus, instanceName);
+ manualTestConnection(llmInstanceConfig.url, setLLMStatus, instanceName, 'chat');
}, 1000); // Increased delay to ensure component is fully ready
}
@@ -817,7 +1018,7 @@ export const RAGSettings = ({
setTimeout(() => {
const instanceName = embeddingInstanceConfig.name || 'Embedding Instance';
console.log('π Testing Embedding instance on page load:', instanceName, embeddingInstanceConfig.url);
- manualTestConnection(embeddingInstanceConfig.url, setEmbeddingStatus, instanceName);
+ manualTestConnection(embeddingInstanceConfig.url, setEmbeddingStatus, instanceName, 'embedding');
}, 1500); // Stagger the tests
}
@@ -838,12 +1039,63 @@ export const RAGSettings = ({
knowledge retrieval.
- {/* Provider Selection - 6 Button Layout */}
+ {/* LLM Settings Header */}
+
+
+ LLM Settings
+
+
+
+ {/* Provider Selection Buttons */}
+
+
setActiveSelection('chat')}
+ variant="ghost"
+ className={`min-w-[180px] px-5 py-3 font-semibold text-white dark:text-white
+ border border-emerald-400/70 dark:border-emerald-400/40
+ bg-black/40 backdrop-blur-md
+ shadow-[inset_0_0_16px_rgba(15,118,110,0.38)]
+ hover:bg-emerald-500/12 dark:hover:bg-emerald-500/20
+ hover:border-emerald-300/80 hover:shadow-[0_0_22px_rgba(16,185,129,0.5)]
+ ${(activeSelection === 'chat')
+ ? 'shadow-[0_0_25px_rgba(16,185,129,0.5)] ring-2 ring-emerald-400/50'
+ : 'shadow-[0_0_15px_rgba(16,185,129,0.25)]'}
+ `}
+ >
+
+
+ Chat: {chatProvider}
+
+
+
setActiveSelection('embedding')}
+ variant="ghost"
+ className={`min-w-[180px] px-5 py-3 font-semibold text-white dark:text-white
+ border border-purple-400/70 dark:border-purple-400/40
+ bg-black/40 backdrop-blur-md
+ shadow-[inset_0_0_16px_rgba(109,40,217,0.38)]
+ hover:bg-purple-500/12 dark:hover:bg-purple-500/20
+ hover:border-purple-300/80 hover:shadow-[0_0_24px_rgba(168,85,247,0.52)]
+ ${(activeSelection === 'embedding')
+ ? 'shadow-[0_0_26px_rgba(168,85,247,0.55)] ring-2 ring-purple-400/60'
+ : 'shadow-[0_0_15px_rgba(168,85,247,0.25)]'}
+ `}
+ >
+
+
+ Embeddings: {embeddingProvider}
+
+
+
+
+ {/* Context-Aware Provider Grid */}
- LLM Provider
+ Select {activeSelection === 'chat' ? 'Chat' : 'Embedding'} Provider
-
+
{[
{ key: 'openai', name: 'OpenAI', logo: '/img/OpenAI.png', color: 'green' },
{ key: 'google', name: 'Google', logo: '/img/google-logo.svg', color: 'blue' },
@@ -851,39 +1103,50 @@ export const RAGSettings = ({
{ key: 'ollama', name: 'Ollama', logo: '/img/Ollama.png', color: 'purple' },
{ key: 'anthropic', name: 'Anthropic', logo: '/img/claude-logo.svg', color: 'orange' },
{ key: 'grok', name: 'Grok', logo: '/img/Grok.png', color: 'yellow' }
- ].map(provider => (
+ ]
+ .filter(provider =>
+ activeSelection === 'chat' || EMBEDDING_CAPABLE_PROVIDERS.includes(provider.key as ProviderKey)
+ )
+ .map(provider => (
{
- // Get saved models for this provider, or use defaults
const providerKey = provider.key as ProviderKey;
- const savedModels = providerModels[providerKey] || getDefaultModels(providerKey);
- const updatedSettings = {
- ...ragSettings,
- LLM_PROVIDER: providerKey,
- MODEL_CHOICE: savedModels.chatModel,
- EMBEDDING_MODEL: savedModels.embeddingModel
- };
-
- setRagSettings(updatedSettings);
+ if (activeSelection === 'chat') {
+ setChatProvider(providerKey);
+ // Update chat model when switching providers
+ const savedModels = providerModels[providerKey] || getDefaultModels(providerKey);
+ setRagSettings(prev => ({
+ ...prev,
+ MODEL_CHOICE: savedModels.chatModel
+ }));
+ } else {
+ setEmbeddingProvider(providerKey);
+ // Update embedding model when switching providers
+ const savedModels = providerModels[providerKey] || getDefaultModels(providerKey);
+ setRagSettings(prev => ({
+ ...prev,
+ EMBEDDING_MODEL: savedModels.embeddingModel
+ }));
+ }
}}
className={`
relative p-3 rounded-lg border-2 transition-all duration-200 text-center
- ${ragSettings.LLM_PROVIDER === provider.key
+ ${(activeSelection === 'chat' ? chatProvider === provider.key : embeddingProvider === provider.key)
? `${colorStyles[provider.key as ProviderKey]} shadow-[0_0_15px_rgba(34,197,94,0.3)]`
: 'border-gray-300 dark:border-gray-600 hover:border-gray-400 dark:hover:border-gray-500'
}
hover:scale-105 active:scale-95
`}
>
-
@@ -892,10 +1155,9 @@ export const RAGSettings = ({
}`}>
{provider.name}
-{(() => {
+ {(() => {
const status = getProviderStatus(provider.key);
- const isSelected = ragSettings.LLM_PROVIDER === provider.key;
-
+
if (status === 'configured') {
return (
@@ -919,53 +1181,185 @@ export const RAGSettings = ({
))}
+
+ {/* API Key Validation Warnings */}
+ {(() => {
+ const chatStatus = getProviderStatus(chatProvider);
+ const embeddingStatus = getProviderStatus(embeddingProvider);
+ const missingProviders = [];
+
+ if (chatStatus === 'missing') {
+ missingProviders.push({ name: chatProvider, type: 'Chat', color: 'green' });
+ }
+ if (embeddingStatus === 'missing' && embeddingProvider !== chatProvider) {
+ missingProviders.push({ name: embeddingProvider, type: 'Embedding', color: 'purple' });
+ }
+
+ if (missingProviders.length > 0) {
+ return (
+
+
+
+
+
+
+ Missing API Key Configuration
+
+
+
+ Please configure API keys for: {missingProviders.map(p => `${p.name} (${p.type})`).join(', ')}
+
+
+ );
+ }
+ return null;
+ })()}
+
+
+ {shouldShowProviderAlert && (
+
+
{providerAlertMessage}
+
+ )}
- {/* Provider-specific configuration */}
- {ragSettings.LLM_PROVIDER === 'ollama' && (
-
+
+ {/* Context-Aware Model Input */}
+
+ {activeSelection === 'chat' ? (
+ chatProvider !== 'ollama' ? (
+
setRagSettings({
+ ...ragSettings,
+ MODEL_CHOICE: e.target.value
+ })}
+ placeholder={getModelPlaceholder(chatProvider)}
+ accentColor="green"
+ />
+ ) : (
+
+
+ Chat Model
+
+
+ Configured via Ollama instance
+
+
+ Current: {getDisplayedChatModel(ragSettings) || 'Not selected'}
+
+
+ )
+ ) : (
+ embeddingProvider !== 'ollama' ? (
+
setRagSettings({
+ ...ragSettings,
+ EMBEDDING_MODEL: e.target.value
+ })}
+ placeholder={getEmbeddingPlaceholder(embeddingProvider)}
+ accentColor="purple"
+ />
+ ) : (
+
+
+ Embedding Model
+
+
+ Configured via Ollama instance
+
+
+ Current: {getDisplayedEmbeddingModel(ragSettings) || 'Not selected'}
+
+
+ )
+ )}
+
+
+ {/* Ollama Configuration Gear Icon */}
+ {((activeSelection === 'chat' && chatProvider === 'ollama') ||
+ (activeSelection === 'embedding' && embeddingProvider === 'ollama')) && (
+
}
+ className="whitespace-nowrap ml-4 border-green-500 text-green-400 hover:bg-green-500/10"
+ onClick={() => setShowOllamaConfig(!showOllamaConfig)}
+ >
+ {activeSelection === 'chat' ? 'Config' : 'Config'}
+
+ )}
+
+ {/* Save Settings Button */}
+
:
}
+ className="whitespace-nowrap ml-4"
+ size="md"
+ onClick={async () => {
+ try {
+ setSaving(true);
+
+ // Ensure instance configurations are synced with ragSettings before saving
+ const updatedSettings = {
+ ...ragSettings,
+ LLM_BASE_URL: llmInstanceConfig.url,
+ LLM_INSTANCE_NAME: llmInstanceConfig.name,
+ OLLAMA_EMBEDDING_URL: embeddingInstanceConfig.url,
+ OLLAMA_EMBEDDING_INSTANCE_NAME: embeddingInstanceConfig.name
+ };
+
+ await credentialsService.updateRagSettings(updatedSettings);
+
+ // Update local ragSettings state to match what was saved
+ setRagSettings(updatedSettings);
+
+ showToast('RAG settings saved successfully!', 'success');
+ } catch (err) {
+ console.error('Failed to save RAG settings:', err);
+ showToast('Failed to save settings', 'error');
+ } finally {
+ setSaving(false);
+ }
+ }}
+ disabled={saving}
+ >
+ {saving ? 'Saving...' : 'Save Settings'}
+
+
+
+ {/* Expandable Ollama Configuration Container */}
+ {showOllamaConfig && ((activeSelection === 'chat' && chatProvider === 'ollama') ||
+ (activeSelection === 'embedding' && embeddingProvider === 'ollama')) && (
+
-
Ollama Configuration
-
Configure separate Ollama instances for LLM and embedding models
+
+ {activeSelection === 'chat' ? 'LLM Chat Configuration' : 'Embedding Configuration'}
+
+
+ {activeSelection === 'chat'
+ ? 'Configure Ollama instance for chat completions'
+ : 'Configure Ollama instance for text embeddings'}
+
- {(llmStatus.online && embeddingStatus.online) ? "2 / 2 Online" :
- (llmStatus.online || embeddingStatus.online) ? "1 / 2 Online" : "0 / 2 Online"}
+ {(activeSelection === 'chat' ? llmStatus.online : embeddingStatus.online)
+ ? "Online" : "Offline"}
- {/* LLM Instance Card */}
-
-
+ {/* Configuration Content */}
+
+ {activeSelection === 'chat' ? (
+ // Chat Model Configuration
-
LLM Instance
-
For chat completions and text generation
-
-
- {llmStatus.checking ? (
- Checking...
- ) : llmStatus.online ? (
- Online ({llmStatus.responseTime}ms)
- ) : (
- Offline
- )}
- {llmInstanceConfig.name && llmInstanceConfig.url && (
-
-
-
- )}
-
-
-
-
-
{llmInstanceConfig.name && llmInstanceConfig.url ? (
<>
@@ -977,45 +1371,53 @@ export const RAGSettings = ({
Model:
{getDisplayedChatModel(ragSettings)}
-
-
+
+
{llmStatus.checking ? (
) : null}
- {ollamaMetrics.loading ? 'Loading...' : `${ollamaMetrics.llmInstanceModels.total} models available`}
+ {ollamaMetrics.loading ? 'Loading...' : `${ollamaMetrics.llmInstanceModels?.chat || 0} chat models available`}
+
+
+
+ setShowEditLLMModal(true)}
+ >
+ Edit Settings
+
+ manualTestConnection(llmInstanceConfig.url, setLLMStatus, llmInstanceConfig.name, 'chat')}
+ disabled={llmStatus.checking}
+ >
+ {llmStatus.checking ? 'Testing...' : 'Test Connection'}
+
+ setShowLLMModelSelectionModal(true)}
+ >
+ Select Model
+
>
) : (
No LLM instance configured
-
Configure an instance to use LLM features
-
- {/* Quick setup for single host users */}
- {!embeddingInstanceConfig.url && (
-
-
{
- // Quick setup: configure both instances with default values
- const defaultUrl = 'http://host.docker.internal:11434/v1';
- const defaultName = 'Default Ollama';
- setLLMInstanceConfig({ name: defaultName, url: defaultUrl });
- setEmbeddingInstanceConfig({ name: defaultName, url: defaultUrl });
- setShowEditLLMModal(true);
- }}
- >
- β‘ Quick Setup (Single Host)
-
-
Sets up both LLM and Embedding for one host
-
- )}
-
-
Configure an instance to use LLM chat features
+
setShowEditLLMModal(true)}
>
Add LLM Instance
@@ -1023,68 +1425,9 @@ export const RAGSettings = ({
)}
-
- {llmInstanceConfig.name && llmInstanceConfig.url && (
-
- setShowEditLLMModal(true)}
- >
- Edit Settings
-
- manualTestConnection(llmInstanceConfig.url, setLLMStatus, llmInstanceConfig.name)}
- disabled={llmStatus.checking}
- >
- {llmStatus.checking ? 'Testing...' : 'Test Connection'}
-
- setShowLLMModelSelectionModal(true)}
- >
- Select Model
-
-
- )}
-
-
-
- {/* Embedding Instance Card */}
-
-
+ ) : (
+ // Embedding Model Configuration
-
Embedding Instance
-
For generating text embeddings and vector search
-
-
- {embeddingStatus.checking ? (
- Checking...
- ) : embeddingStatus.online ? (
- Online ({embeddingStatus.responseTime}ms)
- ) : (
- Offline
- )}
- {embeddingInstanceConfig.name && embeddingInstanceConfig.url && (
-
-
-
- )}
-
-
-
-
-
{embeddingInstanceConfig.name && embeddingInstanceConfig.url ? (
<>
@@ -1096,22 +1439,50 @@ export const RAGSettings = ({
Model:
{getDisplayedEmbeddingModel(ragSettings)}
-
-
+
+
{embeddingStatus.checking ? (
) : null}
- {ollamaMetrics.loading ? 'Loading...' : `${ollamaMetrics.embeddingInstanceModels.total} models available`}
+ {ollamaMetrics.loading ? 'Loading...' : `${ollamaMetrics.embeddingInstanceModels?.embedding || 0} embedding models available`}
+
+
+
+ setShowEditEmbeddingModal(true)}
+ >
+ Edit Settings
+
+ manualTestConnection(embeddingInstanceConfig.url, setEmbeddingStatus, embeddingInstanceConfig.name, 'embedding')}
+ disabled={embeddingStatus.checking}
+ >
+ {embeddingStatus.checking ? 'Testing...' : 'Test Connection'}
+
+ setShowEmbeddingModelSelectionModal(true)}
+ >
+ Select Model
+
>
) : (
No Embedding instance configured
Configure an instance to use embedding features
-
setShowEditEmbeddingModal(true)}
>
Add Embedding Instance
@@ -1119,99 +1490,65 @@ export const RAGSettings = ({
)}
-
- {embeddingInstanceConfig.name && embeddingInstanceConfig.url && (
-
- setShowEditEmbeddingModal(true)}
- >
- Edit Settings
-
- manualTestConnection(embeddingInstanceConfig.url, setEmbeddingStatus, embeddingInstanceConfig.name)}
- disabled={embeddingStatus.checking}
- >
- {embeddingStatus.checking ? 'Testing...' : 'Test Connection'}
-
- setShowEmbeddingModelSelectionModal(true)}
- >
- Select Model
-
-
- )}
-
+ )}
- {/* Single Host Indicator */}
- {llmInstanceConfig.url && embeddingInstanceConfig.url &&
- llmInstanceConfig.url === embeddingInstanceConfig.url && (
-
-
-
-
-
-
Single Host Setup
-
-
- Both LLM and Embedding instances are using the same Ollama host ({llmInstanceConfig.name})
-
-
- )}
+ {/* Context-Aware Configuration Summary */}
+
+
+ {activeSelection === 'chat' ? 'LLM Instance Summary' : 'Embedding Instance Summary'}
+
- {/* Configuration Summary */}
-
-
Configuration Summary
-
- {/* Instance Comparison Table */}
Configuration
- LLM Instance
- Embedding Instance
+
+ {activeSelection === 'chat' ? 'LLM Instance' : 'Embedding Instance'}
+
Instance Name
- {llmInstanceConfig.name || Not configured }
+ {activeSelection === 'chat'
+ ? (llmInstanceConfig.name || Not configured )
+ : (embeddingInstanceConfig.name || Not configured )
+ }
-
- {embeddingInstanceConfig.name || Not configured }
+
+
+ Instance URL
+
+ {activeSelection === 'chat'
+ ? (llmInstanceConfig.url || Not configured )
+ : (embeddingInstanceConfig.url || Not configured )
+ }
Status
-
- {llmStatus.checking ? "Checking..." : llmStatus.online ? `Online (${llmStatus.responseTime}ms)` : "Offline"}
-
-
-
-
- {embeddingStatus.checking ? "Checking..." : embeddingStatus.online ? `Online (${embeddingStatus.responseTime}ms)` : "Offline"}
-
+ {activeSelection === 'chat' ? (
+
+ {llmStatus.checking ? "Checking..." : llmStatus.online ? `Online (${llmStatus.responseTime}ms)` : "Offline"}
+
+ ) : (
+
+ {embeddingStatus.checking ? "Checking..." : embeddingStatus.online ? `Online (${embeddingStatus.responseTime}ms)` : "Offline"}
+
+ )}
Selected Model
- {getDisplayedChatModel(ragSettings) || No model selected }
-
-
- {getDisplayedEmbeddingModel(ragSettings) || No model selected }
+ {activeSelection === 'chat'
+ ? (getDisplayedChatModel(ragSettings) || No model selected )
+ : (getDisplayedEmbeddingModel(ragSettings) || No model selected )
+ }
@@ -1219,67 +1556,54 @@ export const RAGSettings = ({
{ollamaMetrics.loading ? (
- ) : (
+ ) : activeSelection === 'chat' ? (
-
{ollamaMetrics.llmInstanceModels.total} Total Models
- {ollamaMetrics.llmInstanceModels.total > 0 && (
-
-
- {ollamaMetrics.llmInstanceModels.chat} Chat
-
-
- {ollamaMetrics.llmInstanceModels.embedding} Embedding
-
-
- )}
+
{ollamaMetrics.llmInstanceModels?.chat || 0}
+
chat models
- )}
-
-
- {ollamaMetrics.loading ? (
-
) : (
-
{ollamaMetrics.embeddingInstanceModels.total} Total Models
- {ollamaMetrics.embeddingInstanceModels.total > 0 && (
-
-
- {ollamaMetrics.embeddingInstanceModels.chat} Chat
-
-
- {ollamaMetrics.embeddingInstanceModels.embedding} Embedding
-
-
- )}
+
{ollamaMetrics.embeddingInstanceModels?.embedding || 0}
+
embedding models
)}
-
- {/* System Readiness Summary */}
+
+ {/* Instance-Specific Readiness */}
- System Readiness:
-
- {(llmStatus.online && embeddingStatus.online) ? "β Ready (Both Instances Online)" :
- (llmStatus.online || embeddingStatus.online) ? "β Partial (1 of 2 Online)" : "β Not Ready (No Instances Online)"}
+
+ {activeSelection === 'chat' ? 'LLM Instance Status:' : 'Embedding Instance Status:'}
+
+
+ {activeSelection === 'chat'
+ ? (llmStatus.online ? "β Ready" : "β Not Ready")
+ : (embeddingStatus.online ? "β Ready" : "β Not Ready")
+ }
-
- {/* Overall Model Metrics */}
+
+ {/* Instance-Specific Model Metrics */}
-
Overall Available:
+
Available on this instance:
{ollamaMetrics.loading ? (
+ ) : activeSelection === 'chat' ? (
+ `${ollamaMetrics.llmInstanceModels?.chat || 0} chat models`
) : (
- `${ollamaMetrics.totalModels} total (${ollamaMetrics.chatModels} chat, ${ollamaMetrics.embeddingModels} embedding)`
+ `${ollamaMetrics.embeddingInstanceModels?.embedding || 0} embedding models`
)}
@@ -1289,83 +1613,9 @@ export const RAGSettings = ({
)}
-
- {shouldShowProviderAlert && (
-
-
{providerAlertMessage}
-
- )}
-
-
- : }
- className="whitespace-nowrap"
- size="md"
- onClick={async () => {
- try {
- setSaving(true);
-
- // Ensure instance configurations are synced with ragSettings before saving
- const updatedSettings = {
- ...ragSettings,
- LLM_BASE_URL: llmInstanceConfig.url,
- LLM_INSTANCE_NAME: llmInstanceConfig.name,
- OLLAMA_EMBEDDING_URL: embeddingInstanceConfig.url,
- OLLAMA_EMBEDDING_INSTANCE_NAME: embeddingInstanceConfig.name
- };
-
- await credentialsService.updateRagSettings(updatedSettings);
-
- // Update local ragSettings state to match what was saved
- setRagSettings(updatedSettings);
-
- showToast('RAG settings saved successfully!', 'success');
- } catch (err) {
- console.error('Failed to save RAG settings:', err);
- showToast('Failed to save settings', 'error');
- } finally {
- setSaving(false);
- }
- }}
- disabled={saving}
- >
- {saving ? 'Saving...' : 'Save Settings'}
-
-
- {/* Model Settings Row - Only show for non-Ollama providers */}
- {ragSettings.LLM_PROVIDER !== 'ollama' && (
-
- )}
-
+
{/* Second row: Contextual Embeddings, Max Workers, and description */}
@@ -1778,7 +2028,7 @@ export const RAGSettings = ({
showToast('LLM instance updated successfully', 'success');
// Wait 1 second then automatically test connection and refresh models
setTimeout(() => {
- manualTestConnection(llmInstanceConfig.url, setLLMStatus, llmInstanceConfig.name);
+ manualTestConnection(llmInstanceConfig.url, setLLMStatus, llmInstanceConfig.name, 'chat');
fetchOllamaMetrics(); // Refresh model metrics after saving
}, 1000);
}}
@@ -1829,7 +2079,7 @@ export const RAGSettings = ({
showToast('Embedding instance updated successfully', 'success');
// Wait 1 second then automatically test connection and refresh models
setTimeout(() => {
- manualTestConnection(embeddingInstanceConfig.url, setEmbeddingStatus, embeddingInstanceConfig.name);
+ manualTestConnection(embeddingInstanceConfig.url, setEmbeddingStatus, embeddingInstanceConfig.name, 'embedding');
fetchOllamaMetrics(); // Refresh model metrics after saving
}, 1000);
}}
diff --git a/archon-ui-main/src/services/credentialsService.ts b/archon-ui-main/src/services/credentialsService.ts
index f52d96790e..b2d2da52fa 100644
--- a/archon-ui-main/src/services/credentialsService.ts
+++ b/archon-ui-main/src/services/credentialsService.ts
@@ -23,6 +23,7 @@ export interface RagSettings {
OLLAMA_EMBEDDING_URL?: string;
OLLAMA_EMBEDDING_INSTANCE_NAME?: string;
EMBEDDING_MODEL?: string;
+ EMBEDDING_PROVIDER?: string;
// Crawling Performance Settings
CRAWL_BATCH_SIZE?: number;
CRAWL_MAX_CONCURRENT?: number;
@@ -75,6 +76,16 @@ import { getApiUrl } from "../config/api";
class CredentialsService {
private baseUrl = getApiUrl();
+ private notifyCredentialUpdate(keys: string[]): void {
+ if (typeof window === "undefined") {
+ return;
+ }
+
+ window.dispatchEvent(
+ new CustomEvent("archon:credentials-updated", { detail: { keys } })
+ );
+ }
+
private handleCredentialError(error: any, context: string): Error {
const errorMessage = error instanceof Error ? error.message : String(error);
@@ -182,15 +193,16 @@ class CredentialsService {
USE_CONTEXTUAL_EMBEDDINGS: false,
CONTEXTUAL_EMBEDDINGS_MAX_WORKERS: 3,
USE_HYBRID_SEARCH: true,
- USE_AGENTIC_RAG: true,
- USE_RERANKING: true,
- MODEL_CHOICE: "gpt-4.1-nano",
- LLM_PROVIDER: "openai",
- LLM_BASE_URL: "",
- LLM_INSTANCE_NAME: "",
- OLLAMA_EMBEDDING_URL: "",
- OLLAMA_EMBEDDING_INSTANCE_NAME: "",
- EMBEDDING_MODEL: "",
+ USE_AGENTIC_RAG: true,
+ USE_RERANKING: true,
+ MODEL_CHOICE: "gpt-4.1-nano",
+ LLM_PROVIDER: "openai",
+ LLM_BASE_URL: "",
+ LLM_INSTANCE_NAME: "",
+ OLLAMA_EMBEDDING_URL: "",
+ OLLAMA_EMBEDDING_INSTANCE_NAME: "",
+ EMBEDDING_PROVIDER: "openai",
+ EMBEDDING_MODEL: "",
// Crawling Performance Settings defaults
CRAWL_BATCH_SIZE: 50,
CRAWL_MAX_CONCURRENT: 10,
@@ -221,6 +233,7 @@ class CredentialsService {
"LLM_INSTANCE_NAME",
"OLLAMA_EMBEDDING_URL",
"OLLAMA_EMBEDDING_INSTANCE_NAME",
+ "EMBEDDING_PROVIDER",
"EMBEDDING_MODEL",
"CRAWL_WAIT_STRATEGY",
].includes(cred.key)
@@ -278,7 +291,9 @@ class CredentialsService {
throw new Error(`HTTP ${response.status}: ${errorText}`);
}
- return response.json();
+ const updated = await response.json();
+ this.notifyCredentialUpdate([credential.key]);
+ return updated;
} catch (error) {
throw this.handleCredentialError(
error,
@@ -302,7 +317,9 @@ class CredentialsService {
throw new Error(`HTTP ${response.status}: ${errorText}`);
}
- return response.json();
+ const created = await response.json();
+ this.notifyCredentialUpdate([credential.key]);
+ return created;
} catch (error) {
throw this.handleCredentialError(
error,
@@ -321,6 +338,8 @@ class CredentialsService {
const errorText = await response.text();
throw new Error(`HTTP ${response.status}: ${errorText}`);
}
+
+ this.notifyCredentialUpdate([key]);
} catch (error) {
throw this.handleCredentialError(error, `Deleting credential '${key}'`);
}
diff --git a/python/src/server/services/crawling/code_extraction_service.py b/python/src/server/services/crawling/code_extraction_service.py
index 1a540f5732..21c11b1aaf 100644
--- a/python/src/server/services/crawling/code_extraction_service.py
+++ b/python/src/server/services/crawling/code_extraction_service.py
@@ -159,7 +159,7 @@ async def extract_and_store_code_examples(
if progress_callback:
async def extraction_progress(data: dict):
# Scale progress to 0-20% range
- raw_progress = data.get("progress", 0)
+ raw_progress = data.get("progress", data.get("percentage", 0))
scaled_progress = int(raw_progress * 0.2) # 0-20%
data["progress"] = scaled_progress
await progress_callback(data)
@@ -197,7 +197,7 @@ async def extraction_progress(data: dict):
if progress_callback:
async def summary_progress(data: dict):
# Scale progress to 20-90% range
- raw_progress = data.get("progress", 0)
+ raw_progress = data.get("progress", data.get("percentage", 0))
scaled_progress = 20 + int(raw_progress * 0.7) # 20-90%
data["progress"] = scaled_progress
await progress_callback(data)
@@ -216,7 +216,7 @@ async def summary_progress(data: dict):
if progress_callback:
async def storage_progress(data: dict):
# Scale progress to 90-100% range
- raw_progress = data.get("progress", 0)
+ raw_progress = data.get("progress", data.get("percentage", 0))
scaled_progress = 90 + int(raw_progress * 0.1) # 90-100%
data["progress"] = scaled_progress
await progress_callback(data)
diff --git a/python/src/server/services/credential_service.py b/python/src/server/services/credential_service.py
index 62fbb47ac8..e39f793062 100644
--- a/python/src/server/services/credential_service.py
+++ b/python/src/server/services/credential_service.py
@@ -36,42 +36,6 @@ class CredentialItem:
description: str | None = None
-def _detect_embedding_provider_from_model(embedding_model: str) -> str:
- """
- Detect the appropriate embedding provider based on model name.
-
- Args:
- embedding_model: The embedding model name
-
- Returns:
- Provider name: 'google', 'openai', or 'openai' (default)
- """
- if not embedding_model:
- return "openai" # Default
-
- model_lower = embedding_model.lower()
-
- # Google embedding models
- google_patterns = [
- "text-embedding-004",
- "text-embedding-005",
- "text-multilingual-embedding",
- "gemini-embedding",
- "multimodalembedding"
- ]
-
- if any(pattern in model_lower for pattern in google_patterns):
- return "google"
-
- # OpenAI embedding models (and default for unknown)
- openai_patterns = [
- "text-embedding-ada-002",
- "text-embedding-3-small",
- "text-embedding-3-large"
- ]
-
- # Default to OpenAI for OpenAI models or unknown models
- return "openai"
class CredentialService:
@@ -475,26 +439,17 @@ async def get_active_provider(self, service_type: str = "llm") -> dict[str, Any]
# Get the selected provider based on service type
if service_type == "embedding":
- # Get the LLM provider setting to determine embedding provider
- llm_provider = rag_settings.get("LLM_PROVIDER", "openai")
- embedding_model = rag_settings.get("EMBEDDING_MODEL", "text-embedding-3-small")
-
- # Determine embedding provider based on LLM provider
- if llm_provider == "google":
- provider = "google"
- elif llm_provider == "ollama":
- provider = "ollama"
- elif llm_provider == "openrouter":
- # OpenRouter supports both OpenAI and Google embedding models
- provider = _detect_embedding_provider_from_model(embedding_model)
- elif llm_provider in ["anthropic", "grok"]:
- # Anthropic and Grok support both OpenAI and Google embedding models
- provider = _detect_embedding_provider_from_model(embedding_model)
+ # First check for explicit EMBEDDING_PROVIDER setting (new split provider approach)
+ explicit_embedding_provider = rag_settings.get("EMBEDDING_PROVIDER")
+
+ if explicit_embedding_provider and explicit_embedding_provider != "":
+ # Use the explicitly set embedding provider
+ provider = explicit_embedding_provider
+ logger.debug(f"Using explicit embedding provider: '{provider}'")
else:
- # Default case (openai, or unknown providers)
+ # Fall back to OpenAI as default embedding provider for backward compatibility
provider = "openai"
-
- logger.debug(f"Determined embedding provider '{provider}' from LLM provider '{llm_provider}' and embedding model '{embedding_model}'")
+ logger.debug(f"No explicit embedding provider set, defaulting to OpenAI for backward compatibility")
else:
provider = rag_settings.get("LLM_PROVIDER", "openai")
# Ensure provider is a valid string, not a boolean or other type
diff --git a/python/src/server/services/embeddings/embedding_service.py b/python/src/server/services/embeddings/embedding_service.py
index 4f825f1dc9..b5dc578f95 100644
--- a/python/src/server/services/embeddings/embedding_service.py
+++ b/python/src/server/services/embeddings/embedding_service.py
@@ -6,14 +6,16 @@
import asyncio
import os
+from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Any
+import httpx
import openai
from ...config.logfire_config import safe_span, search_logger
from ..credential_service import credential_service
-from ..llm_provider_service import get_embedding_model, get_llm_client, is_google_embedding_model, is_openai_embedding_model
+from ..llm_provider_service import get_embedding_model, get_llm_client
from ..threading_service import get_threading_service
from .embedding_exceptions import (
EmbeddingAPIError,
@@ -64,6 +66,120 @@ def total_requested(self) -> int:
return self.success_count + self.failure_count
+class EmbeddingProviderAdapter(ABC):
+ """Adapter interface for embedding providers."""
+
+ @abstractmethod
+ async def create_embeddings(
+ self,
+ texts: list[str],
+ model: str,
+ dimensions: int | None = None,
+ ) -> list[list[float]]:
+ """Create embeddings for the given texts."""
+
+
+class OpenAICompatibleEmbeddingAdapter(EmbeddingProviderAdapter):
+ """Adapter for providers using the OpenAI embeddings API shape."""
+
+ def __init__(self, client: Any):
+ self._client = client
+
+ async def create_embeddings(
+ self,
+ texts: list[str],
+ model: str,
+ dimensions: int | None = None,
+ ) -> list[list[float]]:
+ request_args: dict[str, Any] = {
+ "model": model,
+ "input": texts,
+ }
+ if dimensions is not None:
+ request_args["dimensions"] = dimensions
+
+ response = await self._client.embeddings.create(**request_args)
+ return [item.embedding for item in response.data]
+
+
+class GoogleEmbeddingAdapter(EmbeddingProviderAdapter):
+ """Adapter for Google's native embedding endpoint."""
+
+ async def create_embeddings(
+ self,
+ texts: list[str],
+ model: str,
+ dimensions: int | None = None,
+ ) -> list[list[float]]:
+ try:
+ if dimensions is not None:
+ _ = dimensions # Maintains adapter signature; Google controls dimensions server-side.
+
+ google_api_key = await credential_service.get_credential("GOOGLE_API_KEY")
+ if not google_api_key:
+ raise EmbeddingAPIError("Google API key not found")
+
+ async with httpx.AsyncClient(timeout=30.0) as http_client:
+ embeddings = await asyncio.gather(
+ *(
+ self._fetch_single_embedding(http_client, google_api_key, model, text)
+ for text in texts
+ )
+ )
+
+ return embeddings
+
+ except httpx.HTTPStatusError as error:
+ error_content = error.response.text
+ search_logger.error(
+ f"Google embedding API returned {error.response.status_code} - {error_content}",
+ exc_info=True,
+ )
+ raise EmbeddingAPIError(
+ f"Google embedding API error: {error.response.status_code} - {error_content}",
+ original_error=error,
+ ) from error
+ except Exception as error:
+ search_logger.error(f"Error calling Google embedding API: {error}", exc_info=True)
+ raise EmbeddingAPIError(
+ f"Google embedding error: {str(error)}", original_error=error
+ ) from error
+
+ async def _fetch_single_embedding(
+ self,
+ http_client: httpx.AsyncClient,
+ api_key: str,
+ model: str,
+ text: str,
+ ) -> list[float]:
+ url = f"https://generativelanguage.googleapis.com/v1beta/models/{model}:embedContent"
+ headers = {
+ "x-goog-api-key": api_key,
+ "Content-Type": "application/json",
+ }
+ payload = {
+ "model": f"models/{model}",
+ "content": {"parts": [{"text": text}]},
+ }
+
+ response = await http_client.post(url, headers=headers, json=payload)
+ response.raise_for_status()
+
+ result = response.json()
+ embedding = result.get("embedding", {})
+ values = embedding.get("values") if isinstance(embedding, dict) else None
+ if not isinstance(values, list):
+ raise EmbeddingAPIError(f"Invalid embedding payload from Google: {result}")
+
+ return values
+
+
+def _get_embedding_adapter(provider: str, client: Any) -> EmbeddingProviderAdapter:
+ provider_name = (provider or "").lower()
+ if provider_name == "google":
+ return GoogleEmbeddingAdapter()
+ return OpenAICompatibleEmbeddingAdapter(client)
+
# Provider-aware client factory
get_openai_client = get_llm_client
@@ -185,22 +301,14 @@ async def create_embeddings_batch(
"create_embeddings_batch", text_count=len(texts), total_chars=sum(len(t) for t in texts)
) as span:
try:
- # Intelligent embedding provider routing based on model type
- # Get the embedding model first to determine the correct provider
- embedding_model = await get_embedding_model(provider=provider)
-
- # Route to correct provider based on model type
- if is_google_embedding_model(embedding_model):
- embedding_provider = "google"
- search_logger.info(f"Routing to Google for embedding model: {embedding_model}")
- elif is_openai_embedding_model(embedding_model) or "openai/" in embedding_model.lower():
- embedding_provider = "openai"
- search_logger.info(f"Routing to OpenAI for embedding model: {embedding_model}")
- else:
- # Keep original provider for ollama and other providers
- embedding_provider = provider
- search_logger.info(f"Using original provider '{provider}' for embedding model: {embedding_model}")
+ embedding_config = await credential_service.get_active_provider(service_type="embedding")
+ embedding_provider = embedding_config.get("provider")
+
+ if not embedding_provider:
+ search_logger.error("No embedding provider configured")
+ raise ValueError("No embedding provider configured. Please set EMBEDDING_PROVIDER environment variable.")
+ search_logger.info(f"Using embedding provider: '{embedding_provider}' (from EMBEDDING_PROVIDER setting)")
async with get_llm_client(provider=embedding_provider, use_embedding_provider=True) as client:
# Load batch size and dimensions from settings
try:
@@ -215,6 +323,8 @@ async def create_embeddings_batch(
embedding_dimensions = 1536
total_tokens_used = 0
+ adapter = _get_embedding_adapter(embedding_provider, client)
+ dimensions_to_use = embedding_dimensions if embedding_dimensions > 0 else None
for i in range(0, len(texts), batch_size):
batch = texts[i : i + batch_size]
@@ -243,16 +353,14 @@ async def rate_limit_callback(data: dict):
try:
# Create embeddings for this batch
embedding_model = await get_embedding_model(provider=embedding_provider)
-
- response = await client.embeddings.create(
- model=embedding_model,
- input=batch,
- dimensions=embedding_dimensions,
+ embeddings = await adapter.create_embeddings(
+ batch,
+ embedding_model,
+ dimensions=dimensions_to_use,
)
- # Add successful embeddings
- for text, item in zip(batch, response.data, strict=False):
- result.add_success(item.embedding, text)
+ for text, vector in zip(batch, embeddings, strict=False):
+ result.add_success(vector, text)
break # Success, exit retry loop
From 94e323077d2dfd2e8106f495ca3e3ed10a5ed612 Mon Sep 17 00:00:00 2001
From: Chillbruhhh
Date: Thu, 25 Sep 2025 07:50:48 -0500
Subject: [PATCH 16/28] added warning labels and updated ollama health checks
---
.../src/components/settings/RAGSettings.tsx | 338 +++++++++++++++---
1 file changed, 295 insertions(+), 43 deletions(-)
diff --git a/archon-ui-main/src/components/settings/RAGSettings.tsx b/archon-ui-main/src/components/settings/RAGSettings.tsx
index 432040ccdb..f9d3ce5301 100644
--- a/archon-ui-main/src/components/settings/RAGSettings.tsx
+++ b/archon-ui-main/src/components/settings/RAGSettings.tsx
@@ -100,7 +100,10 @@ const providerAlertStyles: Record = {
grok: 'bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-800 text-yellow-800 dark:text-yellow-300',
};
-const providerAlertMessages: Record = {
+const providerWarningAlertStyle = 'bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-800 text-yellow-800 dark:text-yellow-300';
+const providerErrorAlertStyle = 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800 text-red-800 dark:text-red-300';
+
+const defaultProviderAlertMessages: Record = {
openai: 'Configure your OpenAI API key in the credentials section to use GPT models.',
google: 'Configure your Google API key in the credentials section to use Gemini models.',
openrouter: 'Configure your OpenRouter API key in the credentials section to use models.',
@@ -343,6 +346,55 @@ export const RAGSettings = ({
return () => clearInterval(interval);
}, [ragSettings.LLM_PROVIDER]); // Only restart interval if provider changes
+ useEffect(() => {
+ const needsDetection = chatProvider === 'ollama' || embeddingProvider === 'ollama';
+
+ if (!needsDetection) {
+ setOllamaServerStatus('unknown');
+ return;
+ }
+
+ const baseUrl = (
+ ragSettings.LLM_BASE_URL?.trim() ||
+ llmInstanceConfig.url?.trim() ||
+ ragSettings.OLLAMA_EMBEDDING_URL?.trim() ||
+ embeddingInstanceConfig.url?.trim() ||
+ DEFAULT_OLLAMA_URL
+ );
+
+ const normalizedUrl = baseUrl.replace('/v1', '').replace(/\/$/, '');
+
+ let cancelled = false;
+
+ (async () => {
+ try {
+ const response = await fetch(
+ `/api/ollama/instances/health?instance_urls=${encodeURIComponent(normalizedUrl)}`,
+ { method: 'GET', headers: { Accept: 'application/json' } }
+ );
+
+ if (cancelled) return;
+
+ if (!response.ok) {
+ setOllamaServerStatus('offline');
+ return;
+ }
+
+ const data = await response.json();
+ const instanceStatus = data.instance_status?.[normalizedUrl];
+ setOllamaServerStatus(instanceStatus?.is_healthy ? 'online' : 'offline');
+ } catch (error) {
+ if (!cancelled) {
+ setOllamaServerStatus('offline');
+ }
+ }
+ })();
+
+ return () => {
+ cancelled = true;
+ };
+ }, [chatProvider, embeddingProvider, ragSettings.LLM_BASE_URL, ragSettings.OLLAMA_EMBEDDING_URL, llmInstanceConfig.url, embeddingInstanceConfig.url]);
+
// Sync independent provider states with ragSettings (one-way: ragSettings -> local state)
useEffect(() => {
if (ragSettings.LLM_PROVIDER && ragSettings.LLM_PROVIDER !== chatProvider) {
@@ -356,6 +408,11 @@ export const RAGSettings = ({
}
}, [ragSettings.EMBEDDING_PROVIDER]); // Remove embeddingProvider dependency to avoid loops
+ useEffect(() => {
+ setOllamaManualConfirmed(false);
+ setOllamaServerStatus('unknown');
+ }, [ragSettings.LLM_BASE_URL, ragSettings.OLLAMA_EMBEDDING_URL, chatProvider, embeddingProvider]);
+
// Update ragSettings when independent providers change (one-way: local state -> ragSettings)
// Split the βfirstβrunβ guard into two refs so chat and embedding effects donβt interfere.
const updateChatRagSettingsRef = useRef(false);
@@ -396,6 +453,8 @@ export const RAGSettings = ({
const [providerConnectionStatus, setProviderConnectionStatus] = useState<{
[key: string]: { connected: boolean; checking: boolean; lastChecked?: Date }
}>({});
+ const [ollamaServerStatus, setOllamaServerStatus] = useState<'unknown' | 'online' | 'offline'>('unknown');
+ const [ollamaManualConfirmed, setOllamaManualConfirmed] = useState(false);
useEffect(() => {
return () => {
@@ -537,12 +596,14 @@ export const RAGSettings = ({
};
// Manual test function with user feedback using backend proxy
- const manualTestConnection = async (
+const manualTestConnection = async (
url: string,
setStatus: React.Dispatch>,
instanceName: string,
- context?: 'chat' | 'embedding'
+ context?: 'chat' | 'embedding',
+ options?: { suppressToast?: boolean }
): Promise => {
+ const suppressToast = options?.suppressToast ?? false;
setStatus(prev => ({ ...prev, checking: true }));
const startTime = Date.now();
@@ -582,7 +643,9 @@ export const RAGSettings = ({
modelType = 'embedding models';
}
- showToast(`${instanceName} connection successful: ${modelCount} ${modelType} available (${responseTime}ms)`, 'success');
+ if (!suppressToast) {
+ showToast(`${instanceName} connection successful: ${modelCount} ${modelType} available (${responseTime}ms)`, 'success');
+ }
// Scenario 2: Manual "Test Connection" button - refresh Ollama metrics if Ollama provider is selected
if (ragSettings.LLM_PROVIDER === 'ollama') {
@@ -593,21 +656,27 @@ export const RAGSettings = ({
return true;
} else {
setStatus({ online: false, responseTime: null, checking: false });
- showToast(`${instanceName} connection failed: ${instanceStatus?.error_message || 'Instance is not healthy'}`, 'error');
+ if (!suppressToast) {
+ showToast(`${instanceName} connection failed: ${instanceStatus?.error_message || 'Instance is not healthy'}`, 'error');
+ }
return false;
}
} else {
setStatus({ online: false, responseTime: null, checking: false });
- showToast(`${instanceName} connection failed: Backend proxy error (HTTP ${response.status})`, 'error');
+ if (!suppressToast) {
+ showToast(`${instanceName} connection failed: Backend proxy error (HTTP ${response.status})`, 'error');
+ }
return false;
}
} catch (error: any) {
setStatus({ online: false, responseTime: null, checking: false });
- if (error.name === 'AbortError') {
- showToast(`${instanceName} connection failed: Request timeout (>15s)`, 'error');
- } else {
- showToast(`${instanceName} connection failed: ${error.message || 'Unknown error'}`, 'error');
+ if (!suppressToast) {
+ if (error.name === 'AbortError') {
+ showToast(`${instanceName} connection failed: Request timeout (>15s)`, 'error');
+ } else {
+ showToast(`${instanceName} connection failed: ${error.message || 'Unknown error'}`, 'error');
+ }
}
return false;
@@ -854,10 +923,21 @@ export const RAGSettings = ({
return googleConnected ? 'configured' : 'missing';
case 'ollama':
- if (llmStatus.checking || embeddingStatus.checking) return 'partial';
- if (llmStatus.online && embeddingStatus.online) return 'configured';
- if (llmStatus.online || embeddingStatus.online) return 'partial';
- return 'missing';
+ {
+ if (ollamaManualConfirmed || llmStatus.online || embeddingStatus.online) {
+ return 'configured';
+ }
+
+ if (ollamaServerStatus === 'online') {
+ return 'partial';
+ }
+
+ if (ollamaServerStatus === 'offline') {
+ return 'missing';
+ }
+
+ return 'missing';
+ }
case 'anthropic':
// Use server-side connection status
const anthropicConnected = providerConnectionStatus['anthropic']?.connected || false;
@@ -885,18 +965,29 @@ export const RAGSettings = ({
? (ragSettings.LLM_PROVIDER as ProviderKey)
: undefined;
const selectedProviderStatus = selectedProviderKey ? getProviderStatus(selectedProviderKey) : undefined;
- const shouldShowProviderAlert = Boolean(
- selectedProviderKey && selectedProviderStatus === 'missing'
- );
- const providerAlertClassName = shouldShowProviderAlert && selectedProviderKey
- ? providerAlertStyles[selectedProviderKey]
- : '';
- const providerAlertMessage = shouldShowProviderAlert && selectedProviderKey
- ? providerAlertMessages[selectedProviderKey]
- : '';
+
+ let providerAlertMessage: string | null = null;
+ let providerAlertClassName = '';
+
+ if (selectedProviderKey === 'ollama') {
+ if (selectedProviderStatus === 'missing' || ollamaServerStatus === 'offline' || ollamaServerStatus === 'unknown') {
+ providerAlertMessage = 'Local Ollama service is not running. Start the Ollama server and ensure it is reachable at the configured URL.';
+ providerAlertClassName = providerErrorAlertStyle;
+ } else if (selectedProviderStatus === 'partial') {
+ providerAlertMessage = 'Local Ollama service detected. Click "Test Connection" to confirm model availability.';
+ providerAlertClassName = providerWarningAlertStyle;
+ }
+ } else if (selectedProviderKey && selectedProviderStatus === 'missing') {
+ providerAlertMessage = defaultProviderAlertMessages[selectedProviderKey] ?? null;
+ providerAlertClassName = providerAlertStyles[selectedProviderKey] ?? '';
+ }
+
+ const shouldShowProviderAlert = Boolean(providerAlertMessage);
+ const initialChatOllamaTestRef = useRef(false);
useEffect(() => {
if (chatProvider !== 'ollama') {
+ initialChatOllamaTestRef.current = false;
return;
}
@@ -918,7 +1009,13 @@ export const RAGSettings = ({
}
const runTest = async () => {
- const success = await manualTestConnection(baseUrl, setLLMStatus, instanceName, 'chat');
+ const success = await manualTestConnection(
+ baseUrl,
+ setLLMStatus,
+ instanceName,
+ 'chat',
+ { suppressToast: true }
+ );
if (!success && chatProvider === 'ollama') {
llmRetryTimeoutRef.current = window.setTimeout(runTest, 5000);
@@ -937,8 +1034,44 @@ export const RAGSettings = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [chatProvider, ragSettings.LLM_BASE_URL, ragSettings.LLM_INSTANCE_NAME]);
+ useEffect(() => {
+ if (chatProvider !== 'ollama') {
+ initialChatOllamaTestRef.current = false;
+ return;
+ }
+
+ if (initialChatOllamaTestRef.current) {
+ return;
+ }
+
+ initialChatOllamaTestRef.current = true;
+
+ const baseUrl = (ragSettings.LLM_BASE_URL && ragSettings.LLM_BASE_URL.trim().length > 0)
+ ? ragSettings.LLM_BASE_URL.trim()
+ : (llmInstanceConfig.url && llmInstanceConfig.url.trim().length > 0)
+ ? llmInstanceConfig.url.trim()
+ : DEFAULT_OLLAMA_URL;
+
+ const instanceName = llmInstanceConfig.name?.trim().length
+ ? llmInstanceConfig.name
+ : 'LLM Instance';
+
+ setLLMStatus(prev => ({ ...prev, checking: true }));
+ setTimeout(() => {
+ manualTestConnection(
+ baseUrl,
+ setLLMStatus,
+ instanceName,
+ 'chat',
+ { suppressToast: true }
+ );
+ }, 200);
+ }, [chatProvider, ragSettings.LLM_BASE_URL, llmInstanceConfig.url, llmInstanceConfig.name]);
+
+ const initialEmbeddingOllamaTestRef = useRef(false);
useEffect(() => {
if (embeddingProvider !== 'ollama') {
+ initialEmbeddingOllamaTestRef.current = false;
return;
}
@@ -960,7 +1093,13 @@ export const RAGSettings = ({
}
const runTest = async () => {
- const success = await manualTestConnection(baseUrl, setEmbeddingStatus, instanceName, 'embedding');
+ const success = await manualTestConnection(
+ baseUrl,
+ setEmbeddingStatus,
+ instanceName,
+ 'embedding',
+ { suppressToast: true }
+ );
if (!success && embeddingProvider === 'ollama') {
embeddingRetryTimeoutRef.current = window.setTimeout(runTest, 5000);
@@ -979,6 +1118,40 @@ export const RAGSettings = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [embeddingProvider, ragSettings.OLLAMA_EMBEDDING_URL, ragSettings.OLLAMA_EMBEDDING_INSTANCE_NAME]);
+ useEffect(() => {
+ if (embeddingProvider !== 'ollama') {
+ initialEmbeddingOllamaTestRef.current = false;
+ return;
+ }
+
+ if (initialEmbeddingOllamaTestRef.current) {
+ return;
+ }
+
+ initialEmbeddingOllamaTestRef.current = true;
+
+ const baseUrl = (ragSettings.OLLAMA_EMBEDDING_URL && ragSettings.OLLAMA_EMBEDDING_URL.trim().length > 0)
+ ? ragSettings.OLLAMA_EMBEDDING_URL.trim()
+ : (embeddingInstanceConfig.url && embeddingInstanceConfig.url.trim().length > 0)
+ ? embeddingInstanceConfig.url.trim()
+ : DEFAULT_OLLAMA_URL;
+
+ const instanceName = embeddingInstanceConfig.name?.trim().length
+ ? embeddingInstanceConfig.name
+ : 'Embedding Instance';
+
+ setEmbeddingStatus(prev => ({ ...prev, checking: true }));
+ setTimeout(() => {
+ manualTestConnection(
+ baseUrl,
+ setEmbeddingStatus,
+ instanceName,
+ 'embedding',
+ { suppressToast: true }
+ );
+ }, 250);
+ }, [embeddingProvider, ragSettings.OLLAMA_EMBEDDING_URL, embeddingInstanceConfig.url, embeddingInstanceConfig.name]);
+
// Test Ollama connectivity when Settings page loads (scenario 4: page load)
// This useEffect is placed after function definitions to ensure access to manualTestConnection
useEffect(() => {
@@ -995,33 +1168,70 @@ export const RAGSettings = ({
// Only run once when data is properly loaded and not run before
if (!hasRunInitialTestRef.current &&
ragSettings.LLM_PROVIDER === 'ollama' &&
- Object.keys(ragSettings).length > 0 &&
- (llmInstanceConfig.url || embeddingInstanceConfig.url)) {
+ Object.keys(ragSettings).length > 0) {
hasRunInitialTestRef.current = true;
console.log('π Settings page loaded with Ollama - Testing connectivity');
-
- // Test LLM instance if configured (use URL presence as the key indicator)
- // Only test if URL is explicitly set in ragSettings, not just using the default
- if (llmInstanceConfig.url && ragSettings.LLM_BASE_URL) {
+
+ // Test LLM instance if a URL is available (either saved or default)
+ if (llmInstanceConfig.url) {
setTimeout(() => {
const instanceName = llmInstanceConfig.name || 'LLM Instance';
console.log('π Testing LLM instance on page load:', instanceName, llmInstanceConfig.url);
- manualTestConnection(llmInstanceConfig.url, setLLMStatus, instanceName, 'chat');
+ manualTestConnection(
+ llmInstanceConfig.url,
+ setLLMStatus,
+ instanceName,
+ 'chat',
+ { suppressToast: true }
+ );
}, 1000); // Increased delay to ensure component is fully ready
}
-
+ // If no saved URL, run tests against default endpoint
+ else {
+ setTimeout(() => {
+ const defaultInstanceName = 'Local Ollama (Default)';
+ console.log('π Testing default Ollama chat instance on page load:', DEFAULT_OLLAMA_URL);
+ manualTestConnection(
+ DEFAULT_OLLAMA_URL,
+ setLLMStatus,
+ defaultInstanceName,
+ 'chat',
+ { suppressToast: true }
+ );
+ }, 1000);
+ }
+
// Test Embedding instance if configured and different from LLM instance
- // Only test if URL is explicitly set in ragSettings, not just using the default
- if (embeddingInstanceConfig.url && ragSettings.OLLAMA_EMBEDDING_URL &&
+ if (embeddingInstanceConfig.url &&
embeddingInstanceConfig.url !== llmInstanceConfig.url) {
setTimeout(() => {
const instanceName = embeddingInstanceConfig.name || 'Embedding Instance';
console.log('π Testing Embedding instance on page load:', instanceName, embeddingInstanceConfig.url);
- manualTestConnection(embeddingInstanceConfig.url, setEmbeddingStatus, instanceName, 'embedding');
+ manualTestConnection(
+ embeddingInstanceConfig.url,
+ setEmbeddingStatus,
+ instanceName,
+ 'embedding',
+ { suppressToast: true }
+ );
}, 1500); // Stagger the tests
}
-
+ // If embedding provider is also Ollama but no specific URL is set, test default as fallback
+ else if (embeddingProvider === 'ollama' && !embeddingInstanceConfig.url) {
+ setTimeout(() => {
+ const defaultEmbeddingName = 'Local Ollama (Default)';
+ console.log('π Testing default Ollama embedding instance on page load:', DEFAULT_OLLAMA_URL);
+ manualTestConnection(
+ DEFAULT_OLLAMA_URL,
+ setEmbeddingStatus,
+ defaultEmbeddingName,
+ 'embedding',
+ { suppressToast: true }
+ );
+ }, 1500);
+ }
+
// Fetch Ollama metrics after testing connections
setTimeout(() => {
console.log('π Fetching Ollama metrics on page load');
@@ -1188,10 +1398,14 @@ export const RAGSettings = ({
const embeddingStatus = getProviderStatus(embeddingProvider);
const missingProviders = [];
- if (chatStatus === 'missing') {
+ if (chatStatus === 'missing' && chatProvider !== selectedProviderKey) {
missingProviders.push({ name: chatProvider, type: 'Chat', color: 'green' });
}
- if (embeddingStatus === 'missing' && embeddingProvider !== chatProvider) {
+ if (
+ embeddingStatus === 'missing' &&
+ embeddingProvider !== chatProvider &&
+ embeddingProvider !== selectedProviderKey
+ ) {
missingProviders.push({ name: embeddingProvider, type: 'Embedding', color: 'purple' });
}
@@ -1394,7 +1608,17 @@ export const RAGSettings = ({
size="sm"
accentColor="green"
className="text-white border-emerald-400 hover:bg-emerald-500/10"
- onClick={() => manualTestConnection(llmInstanceConfig.url, setLLMStatus, llmInstanceConfig.name, 'chat')}
+ onClick={async () => {
+ const success = await manualTestConnection(
+ llmInstanceConfig.url,
+ setLLMStatus,
+ llmInstanceConfig.name,
+ 'chat'
+ );
+
+ setOllamaManualConfirmed(success);
+ setOllamaServerStatus(success ? 'online' : 'offline');
+ }}
disabled={llmStatus.checking}
>
{llmStatus.checking ? 'Testing...' : 'Test Connection'}
@@ -1460,7 +1684,17 @@ export const RAGSettings = ({
variant="outline"
size="sm"
className="text-purple-300 border-purple-400 hover:bg-purple-500/10"
- onClick={() => manualTestConnection(embeddingInstanceConfig.url, setEmbeddingStatus, embeddingInstanceConfig.name, 'embedding')}
+ onClick={async () => {
+ const success = await manualTestConnection(
+ embeddingInstanceConfig.url,
+ setEmbeddingStatus,
+ embeddingInstanceConfig.name,
+ 'embedding'
+ );
+
+ setOllamaManualConfirmed(success);
+ setOllamaServerStatus(success ? 'online' : 'offline');
+ }}
disabled={embeddingStatus.checking}
>
{embeddingStatus.checking ? 'Testing...' : 'Test Connection'}
@@ -2028,7 +2262,16 @@ export const RAGSettings = ({
showToast('LLM instance updated successfully', 'success');
// Wait 1 second then automatically test connection and refresh models
setTimeout(() => {
- manualTestConnection(llmInstanceConfig.url, setLLMStatus, llmInstanceConfig.name, 'chat');
+ manualTestConnection(
+ llmInstanceConfig.url,
+ setLLMStatus,
+ llmInstanceConfig.name,
+ 'chat',
+ { suppressToast: true }
+ ).then((success) => {
+ setOllamaManualConfirmed(success);
+ setOllamaServerStatus(success ? 'online' : 'offline');
+ });
fetchOllamaMetrics(); // Refresh model metrics after saving
}, 1000);
}}
@@ -2079,7 +2322,16 @@ export const RAGSettings = ({
showToast('Embedding instance updated successfully', 'success');
// Wait 1 second then automatically test connection and refresh models
setTimeout(() => {
- manualTestConnection(embeddingInstanceConfig.url, setEmbeddingStatus, embeddingInstanceConfig.name, 'embedding');
+ manualTestConnection(
+ embeddingInstanceConfig.url,
+ setEmbeddingStatus,
+ embeddingInstanceConfig.name,
+ 'embedding',
+ { suppressToast: true }
+ ).then((success) => {
+ setOllamaManualConfirmed(success);
+ setOllamaServerStatus(success ? 'online' : 'offline');
+ });
fetchOllamaMetrics(); // Refresh model metrics after saving
}, 1000);
}}
From 810c80f761dbffd5b2715f8b12157a4b645e5ab1 Mon Sep 17 00:00:00 2001
From: Chillbruhhh
Date: Thu, 25 Sep 2025 08:01:08 -0500
Subject: [PATCH 17/28] ready for review, fixed som error warnings and
consildated ollama status health checks
---
.../src/components/settings/RAGSettings.tsx | 148 ++++++------------
1 file changed, 48 insertions(+), 100 deletions(-)
diff --git a/archon-ui-main/src/components/settings/RAGSettings.tsx b/archon-ui-main/src/components/settings/RAGSettings.tsx
index f9d3ce5301..9b9c1b2f2a 100644
--- a/archon-ui-main/src/components/settings/RAGSettings.tsx
+++ b/archon-ui-main/src/components/settings/RAGSettings.tsx
@@ -970,10 +970,10 @@ const manualTestConnection = async (
let providerAlertClassName = '';
if (selectedProviderKey === 'ollama') {
- if (selectedProviderStatus === 'missing' || ollamaServerStatus === 'offline' || ollamaServerStatus === 'unknown') {
+ if (ollamaServerStatus === 'offline') {
providerAlertMessage = 'Local Ollama service is not running. Start the Ollama server and ensure it is reachable at the configured URL.';
providerAlertClassName = providerErrorAlertStyle;
- } else if (selectedProviderStatus === 'partial') {
+ } else if (selectedProviderStatus === 'partial' && ollamaServerStatus === 'online') {
providerAlertMessage = 'Local Ollama service detected. Click "Test Connection" to confirm model availability.';
providerAlertClassName = providerWarningAlertStyle;
}
@@ -984,31 +984,34 @@ const manualTestConnection = async (
const shouldShowProviderAlert = Boolean(providerAlertMessage);
- const initialChatOllamaTestRef = useRef(false);
useEffect(() => {
if (chatProvider !== 'ollama') {
- initialChatOllamaTestRef.current = false;
+ if (llmRetryTimeoutRef.current) {
+ clearTimeout(llmRetryTimeoutRef.current);
+ llmRetryTimeoutRef.current = null;
+ }
return;
}
- const baseUrl = (ragSettings.LLM_BASE_URL && ragSettings.LLM_BASE_URL.trim().length > 0)
- ? ragSettings.LLM_BASE_URL.trim()
- : DEFAULT_OLLAMA_URL;
+ const baseUrl = (
+ ragSettings.LLM_BASE_URL?.trim() ||
+ llmInstanceConfig.url?.trim() ||
+ DEFAULT_OLLAMA_URL
+ );
if (!baseUrl) {
return;
}
- const instanceName = (ragSettings.LLM_INSTANCE_NAME && ragSettings.LLM_INSTANCE_NAME.trim().length > 0)
- ? ragSettings.LLM_INSTANCE_NAME.trim()
+ const instanceName = llmInstanceConfig.name?.trim().length
+ ? llmInstanceConfig.name
: 'LLM Instance';
- if (llmRetryTimeoutRef.current) {
- clearTimeout(llmRetryTimeoutRef.current);
- llmRetryTimeoutRef.current = null;
- }
+ let cancelled = false;
const runTest = async () => {
+ if (cancelled) return;
+
const success = await manualTestConnection(
baseUrl,
setLLMStatus,
@@ -1017,82 +1020,56 @@ const manualTestConnection = async (
{ suppressToast: true }
);
- if (!success && chatProvider === 'ollama') {
+ if (!success && chatProvider === 'ollama' && !cancelled) {
llmRetryTimeoutRef.current = window.setTimeout(runTest, 5000);
}
};
+ if (llmRetryTimeoutRef.current) {
+ clearTimeout(llmRetryTimeoutRef.current);
+ llmRetryTimeoutRef.current = null;
+ }
+
setLLMStatus(prev => ({ ...prev, checking: true }));
- llmRetryTimeoutRef.current = window.setTimeout(runTest, 100);
+ runTest();
return () => {
+ cancelled = true;
if (llmRetryTimeoutRef.current) {
clearTimeout(llmRetryTimeoutRef.current);
llmRetryTimeoutRef.current = null;
}
};
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [chatProvider, ragSettings.LLM_BASE_URL, ragSettings.LLM_INSTANCE_NAME]);
-
- useEffect(() => {
- if (chatProvider !== 'ollama') {
- initialChatOllamaTestRef.current = false;
- return;
- }
-
- if (initialChatOllamaTestRef.current) {
- return;
- }
-
- initialChatOllamaTestRef.current = true;
-
- const baseUrl = (ragSettings.LLM_BASE_URL && ragSettings.LLM_BASE_URL.trim().length > 0)
- ? ragSettings.LLM_BASE_URL.trim()
- : (llmInstanceConfig.url && llmInstanceConfig.url.trim().length > 0)
- ? llmInstanceConfig.url.trim()
- : DEFAULT_OLLAMA_URL;
-
- const instanceName = llmInstanceConfig.name?.trim().length
- ? llmInstanceConfig.name
- : 'LLM Instance';
-
- setLLMStatus(prev => ({ ...prev, checking: true }));
- setTimeout(() => {
- manualTestConnection(
- baseUrl,
- setLLMStatus,
- instanceName,
- 'chat',
- { suppressToast: true }
- );
- }, 200);
- }, [chatProvider, ragSettings.LLM_BASE_URL, llmInstanceConfig.url, llmInstanceConfig.name]);
+ }, [chatProvider, ragSettings.LLM_BASE_URL, ragSettings.LLM_INSTANCE_NAME, llmInstanceConfig.url, llmInstanceConfig.name]);
- const initialEmbeddingOllamaTestRef = useRef(false);
useEffect(() => {
if (embeddingProvider !== 'ollama') {
- initialEmbeddingOllamaTestRef.current = false;
+ if (embeddingRetryTimeoutRef.current) {
+ clearTimeout(embeddingRetryTimeoutRef.current);
+ embeddingRetryTimeoutRef.current = null;
+ }
return;
}
- const baseUrl = (ragSettings.OLLAMA_EMBEDDING_URL && ragSettings.OLLAMA_EMBEDDING_URL.trim().length > 0)
- ? ragSettings.OLLAMA_EMBEDDING_URL.trim()
- : DEFAULT_OLLAMA_URL;
+ const baseUrl = (
+ ragSettings.OLLAMA_EMBEDDING_URL?.trim() ||
+ embeddingInstanceConfig.url?.trim() ||
+ DEFAULT_OLLAMA_URL
+ );
if (!baseUrl) {
return;
}
- const instanceName = (ragSettings.OLLAMA_EMBEDDING_INSTANCE_NAME && ragSettings.OLLAMA_EMBEDDING_INSTANCE_NAME.trim().length > 0)
- ? ragSettings.OLLAMA_EMBEDDING_INSTANCE_NAME.trim()
+ const instanceName = embeddingInstanceConfig.name?.trim().length
+ ? embeddingInstanceConfig.name
: 'Embedding Instance';
- if (embeddingRetryTimeoutRef.current) {
- clearTimeout(embeddingRetryTimeoutRef.current);
- embeddingRetryTimeoutRef.current = null;
- }
+ let cancelled = false;
const runTest = async () => {
+ if (cancelled) return;
+
const success = await manualTestConnection(
baseUrl,
setEmbeddingStatus,
@@ -1101,56 +1078,27 @@ const manualTestConnection = async (
{ suppressToast: true }
);
- if (!success && embeddingProvider === 'ollama') {
+ if (!success && embeddingProvider === 'ollama' && !cancelled) {
embeddingRetryTimeoutRef.current = window.setTimeout(runTest, 5000);
}
};
+ if (embeddingRetryTimeoutRef.current) {
+ clearTimeout(embeddingRetryTimeoutRef.current);
+ embeddingRetryTimeoutRef.current = null;
+ }
+
setEmbeddingStatus(prev => ({ ...prev, checking: true }));
- embeddingRetryTimeoutRef.current = window.setTimeout(runTest, 100);
+ runTest();
return () => {
+ cancelled = true;
if (embeddingRetryTimeoutRef.current) {
clearTimeout(embeddingRetryTimeoutRef.current);
embeddingRetryTimeoutRef.current = null;
}
};
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [embeddingProvider, ragSettings.OLLAMA_EMBEDDING_URL, ragSettings.OLLAMA_EMBEDDING_INSTANCE_NAME]);
-
- useEffect(() => {
- if (embeddingProvider !== 'ollama') {
- initialEmbeddingOllamaTestRef.current = false;
- return;
- }
-
- if (initialEmbeddingOllamaTestRef.current) {
- return;
- }
-
- initialEmbeddingOllamaTestRef.current = true;
-
- const baseUrl = (ragSettings.OLLAMA_EMBEDDING_URL && ragSettings.OLLAMA_EMBEDDING_URL.trim().length > 0)
- ? ragSettings.OLLAMA_EMBEDDING_URL.trim()
- : (embeddingInstanceConfig.url && embeddingInstanceConfig.url.trim().length > 0)
- ? embeddingInstanceConfig.url.trim()
- : DEFAULT_OLLAMA_URL;
-
- const instanceName = embeddingInstanceConfig.name?.trim().length
- ? embeddingInstanceConfig.name
- : 'Embedding Instance';
-
- setEmbeddingStatus(prev => ({ ...prev, checking: true }));
- setTimeout(() => {
- manualTestConnection(
- baseUrl,
- setEmbeddingStatus,
- instanceName,
- 'embedding',
- { suppressToast: true }
- );
- }, 250);
- }, [embeddingProvider, ragSettings.OLLAMA_EMBEDDING_URL, embeddingInstanceConfig.url, embeddingInstanceConfig.name]);
+ }, [embeddingProvider, ragSettings.OLLAMA_EMBEDDING_URL, ragSettings.OLLAMA_EMBEDDING_INSTANCE_NAME, embeddingInstanceConfig.url, embeddingInstanceConfig.name]);
// Test Ollama connectivity when Settings page loads (scenario 4: page load)
// This useEffect is placed after function definitions to ensure access to manualTestConnection
From 11388d6db82645a360b1cbd68938934448c649b0 Mon Sep 17 00:00:00 2001
From: Chillbruhhh
Date: Thu, 25 Sep 2025 08:18:50 -0500
Subject: [PATCH 18/28] fixed FAILED test_async_embedding_service.py
---
.../services/embeddings/embedding_service.py | 21 +++++++++++++++----
1 file changed, 17 insertions(+), 4 deletions(-)
diff --git a/python/src/server/services/embeddings/embedding_service.py b/python/src/server/services/embeddings/embedding_service.py
index b5dc578f95..bbd0788270 100644
--- a/python/src/server/services/embeddings/embedding_service.py
+++ b/python/src/server/services/embeddings/embedding_service.py
@@ -5,6 +5,7 @@
"""
import asyncio
+import inspect
import os
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
@@ -180,6 +181,12 @@ def _get_embedding_adapter(provider: str, client: Any) -> EmbeddingProviderAdapt
return GoogleEmbeddingAdapter()
return OpenAICompatibleEmbeddingAdapter(client)
+
+async def _maybe_await(value: Any) -> Any:
+ """Await the value if it is awaitable, otherwise return as-is."""
+
+ return await value if inspect.isawaitable(value) else value
+
# Provider-aware client factory
get_openai_client = get_llm_client
@@ -301,8 +308,14 @@ async def create_embeddings_batch(
"create_embeddings_batch", text_count=len(texts), total_chars=sum(len(t) for t in texts)
) as span:
try:
- embedding_config = await credential_service.get_active_provider(service_type="embedding")
- embedding_provider = embedding_config.get("provider")
+ embedding_config = await _maybe_await(
+ credential_service.get_active_provider(service_type="embedding")
+ )
+
+ embedding_provider = provider or embedding_config.get("provider")
+
+ if not isinstance(embedding_provider, str) or not embedding_provider.strip():
+ embedding_provider = "openai"
if not embedding_provider:
search_logger.error("No embedding provider configured")
@@ -312,8 +325,8 @@ async def create_embeddings_batch(
async with get_llm_client(provider=embedding_provider, use_embedding_provider=True) as client:
# Load batch size and dimensions from settings
try:
- rag_settings = await credential_service.get_credentials_by_category(
- "rag_strategy"
+ rag_settings = await _maybe_await(
+ credential_service.get_credentials_by_category("rag_strategy")
)
batch_size = int(rag_settings.get("EMBEDDING_BATCH_SIZE", "100"))
embedding_dimensions = int(rag_settings.get("EMBEDDING_DIMENSIONS", "1536"))
From aa354caf06667c2161781435c4d264d475d839b3 Mon Sep 17 00:00:00 2001
From: Chillbruhhh
Date: Thu, 25 Sep 2025 09:04:58 -0500
Subject: [PATCH 19/28] code rabbit fixes
---
.../src/components/settings/RAGSettings.tsx | 23 ++++++++-----------
.../src/services/credentialsService.ts | 2 +-
.../crawling/code_extraction_service.py | 22 ++++++++++++++----
.../src/server/services/credential_service.py | 9 +++++++-
.../services/embeddings/embedding_service.py | 10 ++++++--
.../services/storage/code_storage_service.py | 4 ++--
6 files changed, 46 insertions(+), 24 deletions(-)
diff --git a/archon-ui-main/src/components/settings/RAGSettings.tsx b/archon-ui-main/src/components/settings/RAGSettings.tsx
index 9b9c1b2f2a..db195bcec2 100644
--- a/archon-ui-main/src/components/settings/RAGSettings.tsx
+++ b/archon-ui-main/src/components/settings/RAGSettings.tsx
@@ -860,12 +860,14 @@ const manualTestConnection = async (
// Fetch Ollama metrics only when Ollama provider is initially selected (not on URL changes during typing)
React.useEffect(() => {
- if (ragSettings.LLM_PROVIDER === 'ollama') {
+ if (
+ ragSettings.LLM_PROVIDER === 'ollama' || embeddingProvider === 'ollama'
+ ) {
const currentProvider = ragSettings.LLM_PROVIDER;
const lastProvider = lastMetricsFetchRef.current.provider;
-
+
// Only fetch if provider changed to Ollama (scenario 1: user clicks on Ollama Provider)
- if (currentProvider !== lastProvider) {
+ if (currentProvider !== lastProvider || embeddingProvider === 'ollama') {
lastMetricsFetchRef.current = {
provider: currentProvider,
llmUrl: llmInstanceConfig.url,
@@ -877,7 +879,8 @@ const manualTestConnection = async (
fetchOllamaMetrics();
}
}
- }, [ragSettings.LLM_PROVIDER]); // Only watch provider changes, not URL changes
+ }, [ragSettings.LLM_PROVIDER, embeddingProvider, llmInstanceConfig.url, llmInstanceConfig.name,
+ embeddingInstanceConfig.url, embeddingInstanceConfig.name]); // Include embeddingProvider in deps
// Function to check if a provider is properly configured
const getProviderStatus = (providerKey: string): 'configured' | 'missing' | 'partial' => {
@@ -893,15 +896,7 @@ const manualTestConnection = async (
const openAIConnected = providerConnectionStatus['openai']?.connected || false;
const isChecking = providerConnectionStatus['openai']?.checking || false;
- console.log('π OpenAI status check:', {
- openAIKey,
- keyValue: keyValue ? `${keyValue.substring(0, 10)}...` : keyValue,
- hasValue: !!keyValue,
- hasOpenAIKey,
- openAIConnected,
- isChecking,
- allCredentials: Object.keys(apiCredentials)
- });
+ // Intentionally avoid logging API key material.
if (!hasOpenAIKey) return 'missing';
if (isChecking) return 'partial';
@@ -2386,7 +2381,7 @@ function getDisplayedChatModel(ragSettings: any): string {
}
function getDisplayedEmbeddingModel(ragSettings: any): string {
- const provider = ragSettings.LLM_PROVIDER || 'openai';
+ const provider = ragSettings.EMBEDDING_PROVIDER || ragSettings.LLM_PROVIDER || 'openai';
const embeddingModel = ragSettings.EMBEDDING_MODEL;
// Always prioritize user input to allow editing
diff --git a/archon-ui-main/src/services/credentialsService.ts b/archon-ui-main/src/services/credentialsService.ts
index b2d2da52fa..8287be76f8 100644
--- a/archon-ui-main/src/services/credentialsService.ts
+++ b/archon-ui-main/src/services/credentialsService.ts
@@ -195,7 +195,7 @@ class CredentialsService {
USE_HYBRID_SEARCH: true,
USE_AGENTIC_RAG: true,
USE_RERANKING: true,
- MODEL_CHOICE: "gpt-4.1-nano",
+ MODEL_CHOICE: "gpt-4o-mini",
LLM_PROVIDER: "openai",
LLM_BASE_URL: "",
LLM_INSTANCE_NAME: "",
diff --git a/python/src/server/services/crawling/code_extraction_service.py b/python/src/server/services/crawling/code_extraction_service.py
index 21c11b1aaf..f21ee3eee7 100644
--- a/python/src/server/services/crawling/code_extraction_service.py
+++ b/python/src/server/services/crawling/code_extraction_service.py
@@ -197,8 +197,15 @@ async def extraction_progress(data: dict):
if progress_callback:
async def summary_progress(data: dict):
# Scale progress to 20-90% range
- raw_progress = data.get("progress", data.get("percentage", 0))
- scaled_progress = 20 + int(raw_progress * 0.7) # 20-90%
+ raw = data.get("progress", data.get("percentage", 0))
+ try:
+ raw_num = float(raw)
+ except (TypeError, ValueError):
+ raw_num = 0.0
+ if 0.0 <= raw_num <= 1.0:
+ raw_num *= 100.0
+ # 20-90% with clamping
+ scaled_progress = min(90, max(20, 20 + int(raw_num * 0.7)))
data["progress"] = scaled_progress
await progress_callback(data)
summary_callback = summary_progress
@@ -216,8 +223,15 @@ async def summary_progress(data: dict):
if progress_callback:
async def storage_progress(data: dict):
# Scale progress to 90-100% range
- raw_progress = data.get("progress", data.get("percentage", 0))
- scaled_progress = 90 + int(raw_progress * 0.1) # 90-100%
+ raw = data.get("progress", data.get("percentage", 0))
+ try:
+ raw_num = float(raw)
+ except (TypeError, ValueError):
+ raw_num = 0.0
+ if 0.0 <= raw_num <= 1.0:
+ raw_num *= 100.0
+ # 90-100% with clamping
+ scaled_progress = min(100, max(90, 90 + int(raw_num * 0.1)))
data["progress"] = scaled_progress
await progress_callback(data)
storage_callback = storage_progress
diff --git a/python/src/server/services/credential_service.py b/python/src/server/services/credential_service.py
index e39f793062..a8aee8491d 100644
--- a/python/src/server/services/credential_service.py
+++ b/python/src/server/services/credential_service.py
@@ -442,12 +442,19 @@ async def get_active_provider(self, service_type: str = "llm") -> dict[str, Any]
# First check for explicit EMBEDDING_PROVIDER setting (new split provider approach)
explicit_embedding_provider = rag_settings.get("EMBEDDING_PROVIDER")
- if explicit_embedding_provider and explicit_embedding_provider != "":
+ # Validate that embedding provider actually supports embeddings
+ embedding_capable_providers = {"openai", "google", "ollama"}
+
+ if (explicit_embedding_provider and
+ explicit_embedding_provider != "" and
+ explicit_embedding_provider in embedding_capable_providers):
# Use the explicitly set embedding provider
provider = explicit_embedding_provider
logger.debug(f"Using explicit embedding provider: '{provider}'")
else:
# Fall back to OpenAI as default embedding provider for backward compatibility
+ if explicit_embedding_provider and explicit_embedding_provider not in embedding_capable_providers:
+ logger.warning(f"Invalid embedding provider '{explicit_embedding_provider}' doesn't support embeddings, defaulting to OpenAI")
provider = "openai"
logger.debug(f"No explicit embedding provider set, defaulting to OpenAI for backward compatibility")
else:
diff --git a/python/src/server/services/embeddings/embedding_service.py b/python/src/server/services/embeddings/embedding_service.py
index bbd0788270..1f1837d865 100644
--- a/python/src/server/services/embeddings/embedding_service.py
+++ b/python/src/server/services/embeddings/embedding_service.py
@@ -153,13 +153,19 @@ async def _fetch_single_embedding(
model: str,
text: str,
) -> list[float]:
- url = f"https://generativelanguage.googleapis.com/v1beta/models/{model}:embedContent"
+ if model.startswith("models/"):
+ url_model = model[len("models/") :]
+ payload_model = model
+ else:
+ url_model = model
+ payload_model = f"models/{model}"
+ url = f"https://generativelanguage.googleapis.com/v1beta/models/{url_model}:embedContent"
headers = {
"x-goog-api-key": api_key,
"Content-Type": "application/json",
}
payload = {
- "model": f"models/{model}",
+ "model": payload_model,
"content": {"parts": [{"text": text}]},
}
diff --git a/python/src/server/services/storage/code_storage_service.py b/python/src/server/services/storage/code_storage_service.py
index a993bc70b8..2bcbfbdcd4 100644
--- a/python/src/server/services/storage/code_storage_service.py
+++ b/python/src/server/services/storage/code_storage_service.py
@@ -1183,8 +1183,8 @@ async def add_code_examples_to_supabase(
# Use original combined texts
batch_texts = combined_texts
- # Create embeddings for the batch
- result = await create_embeddings_batch(batch_texts, provider=provider)
+ # Create embeddings for the batch (let credential service determine provider)
+ result = await create_embeddings_batch(batch_texts)
# Log any failures
if result.has_failures:
From 64835ec4e690e68f2274c5168fded8fe6a67714f Mon Sep 17 00:00:00 2001
From: Chillbruhhh
Date: Thu, 25 Sep 2025 10:35:33 -0500
Subject: [PATCH 20/28] Separated the code-summary LLM provider from the
embedding provider, so code example storage now forwards a dedicated
embedding provider override end-to-end without hijacking the embedding
pipeline. this fixes code rabbits (Preserve provider override in
create_embeddings_batch) suggesting
---
.../crawling/code_extraction_service.py | 18 +++++++++++++++++-
.../services/crawling/crawling_service.py | 18 ++++++++++++++++--
.../crawling/document_storage_operations.py | 10 +++++++++-
.../services/storage/code_storage_service.py | 9 ++++++---
python/tests/test_code_extraction_source_id.py | 4 ++--
5 files changed, 50 insertions(+), 9 deletions(-)
diff --git a/python/src/server/services/crawling/code_extraction_service.py b/python/src/server/services/crawling/code_extraction_service.py
index f21ee3eee7..d3fa2f07f8 100644
--- a/python/src/server/services/crawling/code_extraction_service.py
+++ b/python/src/server/services/crawling/code_extraction_service.py
@@ -140,6 +140,7 @@ async def extract_and_store_code_examples(
progress_callback: Callable | None = None,
cancellation_check: Callable[[], None] | None = None,
provider: str | None = None,
+ embedding_provider: str | None = None,
) -> int:
"""
Extract code examples from crawled documents and store them.
@@ -150,6 +151,8 @@ async def extract_and_store_code_examples(
source_id: The unique source_id for all documents
progress_callback: Optional async callback for progress updates
cancellation_check: Optional function to check for cancellation
+ provider: Optional LLM provider identifier for summary generation
+ embedding_provider: Optional embedding provider override for vector creation
Returns:
Number of code examples stored
@@ -238,7 +241,11 @@ async def storage_progress(data: dict):
# Store code examples in database
return await self._store_code_examples(
- storage_data, url_to_full_document, storage_callback, provider
+ storage_data,
+ url_to_full_document,
+ storage_callback,
+ provider,
+ embedding_provider,
)
async def _extract_code_blocks_from_documents(
@@ -1684,12 +1691,20 @@ async def _store_code_examples(
url_to_full_document: dict[str, str],
progress_callback: Callable | None = None,
provider: str | None = None,
+ embedding_provider: str | None = None,
) -> int:
"""
Store code examples in the database.
Returns:
Number of code examples stored
+
+ Args:
+ storage_data: Prepared code example payloads
+ url_to_full_document: Mapping of URLs to their full document content
+ progress_callback: Optional callback for progress updates
+ provider: Optional LLM provider identifier for summaries
+ embedding_provider: Optional embedding provider override for vector storage
"""
# Create progress callback for storage phase
storage_progress_callback = None
@@ -1727,6 +1742,7 @@ async def storage_callback(data: dict):
url_to_full_document=url_to_full_document,
progress_callback=storage_progress_callback,
provider=provider,
+ embedding_provider=embedding_provider,
)
# Report completion of code extraction/storage phase
diff --git a/python/src/server/services/crawling/crawling_service.py b/python/src/server/services/crawling/crawling_service.py
index 69c6571909..acafc0a50e 100644
--- a/python/src/server/services/crawling/crawling_service.py
+++ b/python/src/server/services/crawling/crawling_service.py
@@ -14,6 +14,7 @@
from ...config.logfire_config import get_logger, safe_logfire_error, safe_logfire_info
from ...utils import get_supabase_client
from ...utils.progress.progress_tracker import ProgressTracker
+from ..credential_service import credential_service
# Import strategies
# Import operations
@@ -477,15 +478,27 @@ async def code_progress_callback(data: dict):
try:
# Extract provider from request or use credential service default
provider = request.get("provider")
+ embedding_provider = None
+
if not provider:
try:
- from ..credential_service import credential_service
provider_config = await credential_service.get_active_provider("llm")
provider = provider_config.get("provider", "openai")
except Exception as e:
- logger.warning(f"Failed to get provider from credential service: {e}, defaulting to openai")
+ logger.warning(
+ f"Failed to get provider from credential service: {e}, defaulting to openai"
+ )
provider = "openai"
+ try:
+ embedding_config = await credential_service.get_active_provider("embedding")
+ embedding_provider = embedding_config.get("provider")
+ except Exception as e:
+ logger.warning(
+ f"Failed to get embedding provider from credential service: {e}. Using configured default."
+ )
+ embedding_provider = None
+
code_examples_count = await self.doc_storage_ops.extract_and_store_code_examples(
crawl_results,
storage_results["url_to_full_document"],
@@ -493,6 +506,7 @@ async def code_progress_callback(data: dict):
code_progress_callback,
self._check_cancellation,
provider,
+ embedding_provider,
)
except RuntimeError as e:
# Code extraction failed, continue crawl with warning
diff --git a/python/src/server/services/crawling/document_storage_operations.py b/python/src/server/services/crawling/document_storage_operations.py
index 88ed8e80df..8bfa4560a3 100644
--- a/python/src/server/services/crawling/document_storage_operations.py
+++ b/python/src/server/services/crawling/document_storage_operations.py
@@ -352,6 +352,7 @@ async def extract_and_store_code_examples(
progress_callback: Callable | None = None,
cancellation_check: Callable[[], None] | None = None,
provider: str | None = None,
+ embedding_provider: str | None = None,
) -> int:
"""
Extract code examples from crawled documents and store them.
@@ -363,12 +364,19 @@ async def extract_and_store_code_examples(
progress_callback: Optional callback for progress updates
cancellation_check: Optional function to check for cancellation
provider: Optional LLM provider to use for code summaries
+ embedding_provider: Optional embedding provider override for code example embeddings
Returns:
Number of code examples stored
"""
result = await self.code_extraction_service.extract_and_store_code_examples(
- crawl_results, url_to_full_document, source_id, progress_callback, cancellation_check, provider
+ crawl_results,
+ url_to_full_document,
+ source_id,
+ progress_callback,
+ cancellation_check,
+ provider,
+ embedding_provider,
)
return result
diff --git a/python/src/server/services/storage/code_storage_service.py b/python/src/server/services/storage/code_storage_service.py
index 2bcbfbdcd4..8e237f7ea0 100644
--- a/python/src/server/services/storage/code_storage_service.py
+++ b/python/src/server/services/storage/code_storage_service.py
@@ -1091,6 +1091,7 @@ async def add_code_examples_to_supabase(
url_to_full_document: dict[str, str] | None = None,
progress_callback: Callable | None = None,
provider: str | None = None,
+ embedding_provider: str | None = None,
):
"""
Add code examples to the Supabase code_examples table in batches.
@@ -1105,6 +1106,8 @@ async def add_code_examples_to_supabase(
batch_size: Size of each batch for insertion
url_to_full_document: Optional mapping of URLs to full document content
progress_callback: Optional async callback for progress updates
+ provider: Optional LLM provider used for summary generation tracking
+ embedding_provider: Optional embedding provider override for vector generation
"""
if not urls:
return
@@ -1183,8 +1186,8 @@ async def add_code_examples_to_supabase(
# Use original combined texts
batch_texts = combined_texts
- # Create embeddings for the batch (let credential service determine provider)
- result = await create_embeddings_batch(batch_texts)
+ # Create embeddings for the batch (optionally overriding the embedding provider)
+ result = await create_embeddings_batch(batch_texts, provider=embedding_provider)
# Log any failures
if result.has_failures:
@@ -1201,7 +1204,7 @@ async def add_code_examples_to_supabase(
from ..llm_provider_service import get_embedding_model
# Get embedding model name
- embedding_model_name = await get_embedding_model(provider=provider)
+ embedding_model_name = await get_embedding_model(provider=embedding_provider)
# Get LLM chat model (used for code summaries and contextual embeddings if enabled)
llm_chat_model = None
diff --git a/python/tests/test_code_extraction_source_id.py b/python/tests/test_code_extraction_source_id.py
index 05405ee790..7899c7fc58 100644
--- a/python/tests/test_code_extraction_source_id.py
+++ b/python/tests/test_code_extraction_source_id.py
@@ -111,8 +111,8 @@ async def test_document_storage_passes_source_id(self):
assert args[2] == source_id
assert args[3] is None
assert args[4] is None
- if len(args) > 5:
- assert args[5] is None
+ assert args[5] is None
+ assert args[6] is None
assert result == 5
@pytest.mark.asyncio
From fedf5957f68b053b5a5a93940718f261a7bf5e5b Mon Sep 17 00:00:00 2001
From: Chillbruhhh
Date: Thu, 25 Sep 2025 11:21:18 -0500
Subject: [PATCH 21/28] - Swapped API credential storage to booleans so
decrypted keys never sit in React state (archon-ui-main/src/components/
settings/RAGSettings.tsx). - Normalized Ollama instance URLs and gated the
metrics effect on real state changes to avoid mis-counts and duplicate
fetches (RAGSettings.tsx). - Tightened crawl progress scaling and
indented-block parsing to handle min_length=None safely (python/src/server/
services/crawling/code_extraction_service.py:160,
python/src/server/services/crawling/code_extraction_service.py:911). -
Added provider-agnostic embedding rate-limit retries so Google and friends
back off gracefully (python/src/server/
services/embeddings/embedding_service.py:427). - Made the orchestration
registry async + thread-safe and updated every caller to await it
(python/src/server/services/ crawling/crawling_service.py:34,
python/src/server/api_routes/knowledge_api.py:1291).
---
.../src/components/settings/RAGSettings.tsx | 105 ++++++++++--------
python/src/server/api_routes/knowledge_api.py | 4 +-
.../crawling/code_extraction_service.py | 28 ++++-
.../services/crawling/crawling_service.py | 25 +++--
.../services/embeddings/embedding_service.py | 11 ++
5 files changed, 106 insertions(+), 67 deletions(-)
diff --git a/archon-ui-main/src/components/settings/RAGSettings.tsx b/archon-ui-main/src/components/settings/RAGSettings.tsx
index db195bcec2..618c1ccfa2 100644
--- a/archon-ui-main/src/components/settings/RAGSettings.tsx
+++ b/archon-ui-main/src/components/settings/RAGSettings.tsx
@@ -118,6 +118,16 @@ const isProviderKey = (value: unknown): value is ProviderKey =>
// Default base URL for Ollama instances when not explicitly configured
const DEFAULT_OLLAMA_URL = 'http://host.docker.internal:11434/v1';
+const normalizeBaseUrl = (url?: string | null): string | null => {
+ if (!url) return null;
+ const trimmed = url.trim();
+ if (!trimmed) return null;
+
+ let normalized = trimmed.replace(/\/+$/, '');
+ normalized = normalized.replace(/\/v1$/i, '');
+ return normalized || null;
+};
+
interface RAGSettingsProps {
ragSettings: {
MODEL_CHOICE: string;
@@ -282,12 +292,10 @@ export const RAGSettings = ({
const keyNames = ['OPENAI_API_KEY', 'GOOGLE_API_KEY', 'ANTHROPIC_API_KEY'];
const statusResults = await credentialsService.checkCredentialStatus(keyNames);
- const credentials: {[key: string]: string} = {};
-
+ const credentials: {[key: string]: boolean} = {};
+
for (const [key, result] of Object.entries(statusResults)) {
- if (result.has_value && result.value && result.value.trim().length > 0) {
- credentials[key] = result.value;
- }
+ credentials[key] = !!result.has_value;
}
console.log('π Loaded API credentials for status checking:', Object.keys(credentials));
@@ -311,12 +319,10 @@ export const RAGSettings = ({
const keyNames = ['OPENAI_API_KEY', 'GOOGLE_API_KEY', 'ANTHROPIC_API_KEY'];
const statusResults = await credentialsService.checkCredentialStatus(keyNames);
- const credentials: {[key: string]: string} = {};
-
+ const credentials: {[key: string]: boolean} = {};
+
for (const [key, result] of Object.entries(statusResults)) {
- if (result.has_value && result.value && result.value.trim().length > 0) {
- credentials[key] = result.value;
- }
+ credentials[key] = !!result.has_value;
}
console.log('π Reloaded API credentials for status checking:', Object.keys(credentials));
@@ -448,7 +454,7 @@ export const RAGSettings = ({
const embeddingRetryTimeoutRef = useRef(null);
// API key credentials for status checking
- const [apiCredentials, setApiCredentials] = useState<{[key: string]: string}>({});
+ const [apiCredentials, setApiCredentials] = useState<{[key: string]: boolean}>({});
// Provider connection status tracking
const [providerConnectionStatus, setProviderConnectionStatus] = useState<{
[key: string]: { connected: boolean; checking: boolean; lastChecked?: Date }
@@ -732,11 +738,14 @@ const manualTestConnection = async (
try {
setOllamaMetrics(prev => ({ ...prev, loading: true }));
- // Prepare instance URLs for the API call
- const instanceUrls = [];
- if (llmInstanceConfig.url) instanceUrls.push(llmInstanceConfig.url);
- if (embeddingInstanceConfig.url && embeddingInstanceConfig.url !== llmInstanceConfig.url) {
- instanceUrls.push(embeddingInstanceConfig.url);
+ // Prepare normalized instance URLs for the API call
+ const instanceUrls: string[] = [];
+ const llmUrlBase = normalizeBaseUrl(llmInstanceConfig.url);
+ const embUrlBase = normalizeBaseUrl(embeddingInstanceConfig.url);
+
+ if (llmUrlBase) instanceUrls.push(llmUrlBase);
+ if (embUrlBase && embUrlBase !== llmUrlBase) {
+ instanceUrls.push(embUrlBase);
}
if (instanceUrls.length === 0) {
@@ -760,18 +769,18 @@ const manualTestConnection = async (
// Count models for LLM instance
const llmChatModels = allChatModels.filter((model: any) =>
- model.instance_url === llmInstanceConfig.url
+ normalizeBaseUrl(model.instance_url) === llmUrlBase
);
const llmEmbeddingModels = allEmbeddingModels.filter((model: any) =>
- model.instance_url === llmInstanceConfig.url
+ normalizeBaseUrl(model.instance_url) === llmUrlBase
);
-
+
// Count models for Embedding instance
const embChatModels = allChatModels.filter((model: any) =>
- model.instance_url === embeddingInstanceConfig.url
+ normalizeBaseUrl(model.instance_url) === embUrlBase
);
const embEmbeddingModels = allEmbeddingModels.filter((model: any) =>
- model.instance_url === embeddingInstanceConfig.url
+ normalizeBaseUrl(model.instance_url) === embUrlBase
);
// Calculate totals
@@ -810,7 +819,7 @@ const manualTestConnection = async (
// Use refs to prevent infinite connection testing
const lastTestedLLMConfigRef = useRef({ url: '', name: '', provider: '' });
const lastTestedEmbeddingConfigRef = useRef({ url: '', name: '', provider: '' });
- const lastMetricsFetchRef = useRef({ provider: '', llmUrl: '', embUrl: '', llmOnline: false, embOnline: false });
+ const lastMetricsFetchRef = useRef({ provider: '', embProvider: '', llmUrl: '', embUrl: '', llmOnline: false, embOnline: false });
// Auto-testing disabled to prevent API calls on every keystroke per user request
// Connection testing should only happen on manual "Test Connection" or "Save Changes" button clicks
@@ -858,29 +867,31 @@ const manualTestConnection = async (
// }
// }, [embeddingInstanceConfig.url, embeddingInstanceConfig.name, ragSettings.LLM_PROVIDER]);
- // Fetch Ollama metrics only when Ollama provider is initially selected (not on URL changes during typing)
React.useEffect(() => {
- if (
- ragSettings.LLM_PROVIDER === 'ollama' || embeddingProvider === 'ollama'
- ) {
- const currentProvider = ragSettings.LLM_PROVIDER;
- const lastProvider = lastMetricsFetchRef.current.provider;
-
- // Only fetch if provider changed to Ollama (scenario 1: user clicks on Ollama Provider)
- if (currentProvider !== lastProvider || embeddingProvider === 'ollama') {
- lastMetricsFetchRef.current = {
- provider: currentProvider,
- llmUrl: llmInstanceConfig.url,
- embUrl: embeddingInstanceConfig.url,
- llmOnline: llmStatus.online,
- embOnline: embeddingStatus.online
- };
- console.log('π Fetching Ollama metrics - Provider selected');
- fetchOllamaMetrics();
- }
+ const current = {
+ provider: ragSettings.LLM_PROVIDER,
+ embProvider: embeddingProvider,
+ llmUrl: normalizeBaseUrl(llmInstanceConfig.url) ?? '',
+ embUrl: normalizeBaseUrl(embeddingInstanceConfig.url) ?? '',
+ llmOnline: llmStatus.online,
+ embOnline: embeddingStatus.online,
+ };
+ const last = lastMetricsFetchRef.current;
+
+ const meaningfulChange =
+ current.provider !== last.provider ||
+ current.embProvider !== last.embProvider ||
+ current.llmUrl !== last.llmUrl ||
+ current.embUrl !== last.embUrl ||
+ current.llmOnline !== last.llmOnline ||
+ current.embOnline !== last.embOnline;
+
+ if ((current.provider === 'ollama' || current.embProvider === 'ollama') && meaningfulChange) {
+ lastMetricsFetchRef.current = current;
+ console.log('π Fetching Ollama metrics - state changed');
+ fetchOllamaMetrics();
}
- }, [ragSettings.LLM_PROVIDER, embeddingProvider, llmInstanceConfig.url, llmInstanceConfig.name,
- embeddingInstanceConfig.url, embeddingInstanceConfig.name]); // Include embeddingProvider in deps
+ }, [ragSettings.LLM_PROVIDER, embeddingProvider, llmStatus.online, embeddingStatus.online]);
// Function to check if a provider is properly configured
const getProviderStatus = (providerKey: string): 'configured' | 'missing' | 'partial' => {
@@ -888,9 +899,7 @@ const manualTestConnection = async (
case 'openai':
// Check if OpenAI API key is configured (case insensitive)
const openAIKey = Object.keys(apiCredentials).find(key => key.toUpperCase() === 'OPENAI_API_KEY');
- const keyValue = openAIKey ? apiCredentials[openAIKey] : undefined;
- // Don't consider encrypted placeholders as valid API keys for connection testing
- const hasOpenAIKey = openAIKey && keyValue && keyValue.trim().length > 0 && !keyValue.includes('[ENCRYPTED]');
+ const hasOpenAIKey = openAIKey ? !!apiCredentials[openAIKey] : false;
// Only show configured if we have both API key AND confirmed connection
const openAIConnected = providerConnectionStatus['openai']?.connected || false;
@@ -905,9 +914,7 @@ const manualTestConnection = async (
case 'google':
// Check if Google API key is configured (case insensitive)
const googleKey = Object.keys(apiCredentials).find(key => key.toUpperCase() === 'GOOGLE_API_KEY');
- const googleKeyValue = googleKey ? apiCredentials[googleKey] : undefined;
- // Don't consider encrypted placeholders as valid API keys for connection testing
- const hasGoogleKey = googleKey && googleKeyValue && googleKeyValue.trim().length > 0 && !googleKeyValue.includes('[ENCRYPTED]');
+ const hasGoogleKey = googleKey ? !!apiCredentials[googleKey] : false;
// Only show configured if we have both API key AND confirmed connection
const googleConnected = providerConnectionStatus['google']?.connected || false;
diff --git a/python/src/server/api_routes/knowledge_api.py b/python/src/server/api_routes/knowledge_api.py
index 1f26dace8f..47a3d9db2e 100644
--- a/python/src/server/api_routes/knowledge_api.py
+++ b/python/src/server/api_routes/knowledge_api.py
@@ -1288,7 +1288,7 @@ async def stop_crawl_task(progress_id: str):
found = False
# Step 1: Cancel the orchestration service
- orchestration = get_active_orchestration(progress_id)
+ orchestration = await get_active_orchestration(progress_id)
if orchestration:
orchestration.cancel()
found = True
@@ -1306,7 +1306,7 @@ async def stop_crawl_task(progress_id: str):
found = True
# Step 3: Remove from active orchestrations registry
- unregister_orchestration(progress_id)
+ await unregister_orchestration(progress_id)
# Step 4: Update progress tracker to reflect cancellation (only if we found and cancelled something)
if found:
diff --git a/python/src/server/services/crawling/code_extraction_service.py b/python/src/server/services/crawling/code_extraction_service.py
index d3fa2f07f8..b1705b029e 100644
--- a/python/src/server/services/crawling/code_extraction_service.py
+++ b/python/src/server/services/crawling/code_extraction_service.py
@@ -161,9 +161,16 @@ async def extract_and_store_code_examples(
extraction_callback = None
if progress_callback:
async def extraction_progress(data: dict):
- # Scale progress to 0-20% range
- raw_progress = data.get("progress", data.get("percentage", 0))
- scaled_progress = int(raw_progress * 0.2) # 0-20%
+ # Scale progress to 0-20% range with normalization similar to later phases
+ raw = data.get("progress", data.get("percentage", 0))
+ try:
+ raw_num = float(raw)
+ except (TypeError, ValueError):
+ raw_num = 0.0
+ if 0.0 <= raw_num <= 1.0:
+ raw_num *= 100.0
+ # 0-20% with clamping
+ scaled_progress = min(20, max(0, int(raw_num * 0.2)))
data["progress"] = scaled_progress
await progress_callback(data)
extraction_callback = extraction_progress
@@ -901,9 +908,20 @@ async def _extract_text_file_code_blocks(
current_indent = indent
block_start_idx = i
current_block.append(line)
- elif current_block and len("\n".join(current_block)) >= min_length:
+ elif current_block:
+ block_text = "\n".join(current_block)
+ threshold = (
+ min_length
+ if min_length is not None
+ else await self._get_min_code_length()
+ )
+ if len(block_text) < threshold:
+ current_block = []
+ current_indent = None
+ continue
+
# End of indented block, check if it's code
- code_content = "\n".join(current_block)
+ code_content = block_text
# Try to detect language from content
language = self._detect_language_from_content(code_content)
diff --git a/python/src/server/services/crawling/crawling_service.py b/python/src/server/services/crawling/crawling_service.py
index acafc0a50e..e02f43388d 100644
--- a/python/src/server/services/crawling/crawling_service.py
+++ b/python/src/server/services/crawling/crawling_service.py
@@ -33,22 +33,25 @@
# Global registry to track active orchestration services for cancellation support
_active_orchestrations: dict[str, "CrawlingService"] = {}
+_orchestration_lock = asyncio.Lock()
-def get_active_orchestration(progress_id: str) -> Optional["CrawlingService"]:
+async def get_active_orchestration(progress_id: str) -> Optional["CrawlingService"]:
"""Get an active orchestration service by progress ID."""
- return _active_orchestrations.get(progress_id)
+ async with _orchestration_lock:
+ return _active_orchestrations.get(progress_id)
-def register_orchestration(progress_id: str, orchestration: "CrawlingService"):
+async def register_orchestration(progress_id: str, orchestration: "CrawlingService"):
"""Register an active orchestration service."""
- _active_orchestrations[progress_id] = orchestration
+ async with _orchestration_lock:
+ _active_orchestrations[progress_id] = orchestration
-def unregister_orchestration(progress_id: str):
+async def unregister_orchestration(progress_id: str):
"""Unregister an orchestration service."""
- if progress_id in _active_orchestrations:
- del _active_orchestrations[progress_id]
+ async with _orchestration_lock:
+ _active_orchestrations.pop(progress_id, None)
class CrawlingService:
@@ -247,7 +250,7 @@ async def orchestrate_crawl(self, request: dict[str, Any]) -> dict[str, Any]:
# Register this orchestration service for cancellation support
if self.progress_id:
- register_orchestration(self.progress_id, self)
+ await register_orchestration(self.progress_id, self)
# Start the crawl as an async task in the main event loop
# Store the task reference for proper cancellation
@@ -562,7 +565,7 @@ async def code_progress_callback(data: dict):
# Unregister after successful completion
if self.progress_id:
- unregister_orchestration(self.progress_id)
+ await unregister_orchestration(self.progress_id)
safe_logfire_info(
f"Unregistered orchestration service after completion | progress_id={self.progress_id}"
)
@@ -581,7 +584,7 @@ async def code_progress_callback(data: dict):
)
# Unregister on cancellation
if self.progress_id:
- unregister_orchestration(self.progress_id)
+ await unregister_orchestration(self.progress_id)
safe_logfire_info(
f"Unregistered orchestration service on cancellation | progress_id={self.progress_id}"
)
@@ -605,7 +608,7 @@ async def code_progress_callback(data: dict):
await self.progress_tracker.error(error_message)
# Unregister on error
if self.progress_id:
- unregister_orchestration(self.progress_id)
+ await unregister_orchestration(self.progress_id)
safe_logfire_info(
f"Unregistered orchestration service on error | progress_id={self.progress_id}"
)
diff --git a/python/src/server/services/embeddings/embedding_service.py b/python/src/server/services/embeddings/embedding_service.py
index 1f1837d865..1a71cfdcef 100644
--- a/python/src/server/services/embeddings/embedding_service.py
+++ b/python/src/server/services/embeddings/embedding_service.py
@@ -424,6 +424,17 @@ async def rate_limit_callback(data: dict):
await asyncio.sleep(wait_time)
else:
raise # Will be caught by outer try
+ except EmbeddingRateLimitError as e:
+ retry_count += 1
+ if retry_count < max_retries:
+ wait_time = 2**retry_count
+ search_logger.warning(
+ f"Embedding rate limit for batch {batch_index}: {e}. "
+ f"Waiting {wait_time}s before retry {retry_count}/{max_retries}"
+ )
+ await asyncio.sleep(wait_time)
+ else:
+ raise
except Exception as e:
# This batch failed - track failures but continue with next batch
From ec4ceee1044c99dea57a83dec31ffbfc92cdc87f Mon Sep 17 00:00:00 2001
From: Josh
Date: Thu, 25 Sep 2025 11:33:11 -0500
Subject: [PATCH 22/28] Update RAGSettings.tsx - header for 'LLM Settings' is
now 'LLM Provider Settings'
---
archon-ui-main/src/components/settings/RAGSettings.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/archon-ui-main/src/components/settings/RAGSettings.tsx b/archon-ui-main/src/components/settings/RAGSettings.tsx
index 618c1ccfa2..73f6fd057c 100644
--- a/archon-ui-main/src/components/settings/RAGSettings.tsx
+++ b/archon-ui-main/src/components/settings/RAGSettings.tsx
@@ -1199,10 +1199,10 @@ const manualTestConnection = async (
knowledge retrieval.
- {/* LLM Settings Header */}
+ {/* LLM Provider Settings Header */}
- LLM Settings
+ LLM Provider Settings
From 7b8c834410c644d0a11fb27f565cb1450e85c82d Mon Sep 17 00:00:00 2001
From: Chillbruhhh
Date: Thu, 25 Sep 2025 12:02:26 -0500
Subject: [PATCH 23/28] (RAG Settings)
- Ollama Health Checks & Metrics
- Added a 10-second timeout to the health fetch so it doesn't hang.
- Adjusted logic so metric refreshes run for embedding-only Ollama setups too.
- Initial page load now checks Ollama if either chat or embedding provider uses it.
- Metrics and alerts now respect which provider (chat/embedding) is currently selected.
- Provider Sync & Alerts
- Fixed a sync bug so the very first provider change updates settings as expected.
- Alerts now track the active provider (chat vs embedding) rather than only the LLM provider.
- Warnings about missing credentials now skip whichever provider is currently selected.
- Modals & Types
- Normalize URLs before handing them to selection modals to keep consistent data.
- Strengthened helper function types (getDisplayedChatModel, getModelPlaceholder, etc.).
(Crawling Service)
- Made the orchestration registry lock lazy-initialized to avoid issues in Python 3.12 and wrapped registry commands
(register, unregister) in async calls. This keeps things thread-safe even during concurrent crawling and cancellation.
---
.../src/components/settings/RAGSettings.tsx | 52 ++++++++++---------
.../services/crawling/crawling_service.py | 18 +++++--
2 files changed, 42 insertions(+), 28 deletions(-)
diff --git a/archon-ui-main/src/components/settings/RAGSettings.tsx b/archon-ui-main/src/components/settings/RAGSettings.tsx
index 618c1ccfa2..9998e4fd8f 100644
--- a/archon-ui-main/src/components/settings/RAGSettings.tsx
+++ b/archon-ui-main/src/components/settings/RAGSettings.tsx
@@ -376,7 +376,7 @@ export const RAGSettings = ({
try {
const response = await fetch(
`/api/ollama/instances/health?instance_urls=${encodeURIComponent(normalizedUrl)}`,
- { method: 'GET', headers: { Accept: 'application/json' } }
+ { method: 'GET', headers: { Accept: 'application/json' }, signal: AbortSignal.timeout(10000) }
);
if (cancelled) return;
@@ -421,8 +421,8 @@ export const RAGSettings = ({
// Update ragSettings when independent providers change (one-way: local state -> ragSettings)
// Split the βfirstβrunβ guard into two refs so chat and embedding effects donβt interfere.
- const updateChatRagSettingsRef = useRef(false);
- const updateEmbeddingRagSettingsRef = useRef(false);
+ const updateChatRagSettingsRef = useRef(true);
+ const updateEmbeddingRagSettingsRef = useRef(true);
useEffect(() => {
// Only update if this is a userβinitiated change, not a sync from ragSettings
@@ -654,7 +654,7 @@ const manualTestConnection = async (
}
// Scenario 2: Manual "Test Connection" button - refresh Ollama metrics if Ollama provider is selected
- if (ragSettings.LLM_PROVIDER === 'ollama') {
+ if (ragSettings.LLM_PROVIDER === 'ollama' || embeddingProvider === 'ollama' || context === 'embedding') {
console.log('π Fetching Ollama metrics - Test Connection button clicked');
fetchOllamaMetrics();
}
@@ -963,15 +963,16 @@ const manualTestConnection = async (
}
};
- const selectedProviderKey = isProviderKey(ragSettings.LLM_PROVIDER)
- ? (ragSettings.LLM_PROVIDER as ProviderKey)
+ const resolvedProviderForAlert = activeSelection === 'chat' ? chatProvider : embeddingProvider;
+ const activeProviderKey = isProviderKey(resolvedProviderForAlert)
+ ? (resolvedProviderForAlert as ProviderKey)
: undefined;
- const selectedProviderStatus = selectedProviderKey ? getProviderStatus(selectedProviderKey) : undefined;
+ const selectedProviderStatus = activeProviderKey ? getProviderStatus(activeProviderKey) : undefined;
let providerAlertMessage: string | null = null;
let providerAlertClassName = '';
- if (selectedProviderKey === 'ollama') {
+ if (activeProviderKey === 'ollama') {
if (ollamaServerStatus === 'offline') {
providerAlertMessage = 'Local Ollama service is not running. Start the Ollama server and ensure it is reachable at the configured URL.';
providerAlertClassName = providerErrorAlertStyle;
@@ -979,9 +980,9 @@ const manualTestConnection = async (
providerAlertMessage = 'Local Ollama service detected. Click "Test Connection" to confirm model availability.';
providerAlertClassName = providerWarningAlertStyle;
}
- } else if (selectedProviderKey && selectedProviderStatus === 'missing') {
- providerAlertMessage = defaultProviderAlertMessages[selectedProviderKey] ?? null;
- providerAlertClassName = providerAlertStyles[selectedProviderKey] ?? '';
+ } else if (activeProviderKey && selectedProviderStatus === 'missing') {
+ providerAlertMessage = defaultProviderAlertMessages[activeProviderKey] ?? null;
+ providerAlertClassName = providerAlertStyles[activeProviderKey] ?? '';
}
const shouldShowProviderAlert = Boolean(providerAlertMessage);
@@ -1116,9 +1117,11 @@ const manualTestConnection = async (
});
// Only run once when data is properly loaded and not run before
- if (!hasRunInitialTestRef.current &&
- ragSettings.LLM_PROVIDER === 'ollama' &&
- Object.keys(ragSettings).length > 0) {
+ if (
+ !hasRunInitialTestRef.current &&
+ (ragSettings.LLM_PROVIDER === 'ollama' || embeddingProvider === 'ollama') &&
+ Object.keys(ragSettings).length > 0
+ ) {
hasRunInitialTestRef.current = true;
console.log('π Settings page loaded with Ollama - Testing connectivity');
@@ -1199,10 +1202,10 @@ const manualTestConnection = async (
knowledge retrieval.
- {/* LLM Settings Header */}
+ {/* LLM Provider Settings Header */}
- LLM Settings
+ LLM Provider Settings
@@ -1347,14 +1350,15 @@ const manualTestConnection = async (
const chatStatus = getProviderStatus(chatProvider);
const embeddingStatus = getProviderStatus(embeddingProvider);
const missingProviders = [];
+ const providerToIgnore = activeProviderKey;
- if (chatStatus === 'missing' && chatProvider !== selectedProviderKey) {
+ if (chatStatus === 'missing' && (!providerToIgnore || chatProvider !== providerToIgnore)) {
missingProviders.push({ name: chatProvider, type: 'Chat', color: 'green' });
}
if (
embeddingStatus === 'missing' &&
embeddingProvider !== chatProvider &&
- embeddingProvider !== selectedProviderKey
+ (!providerToIgnore || embeddingProvider !== providerToIgnore)
) {
missingProviders.push({ name: embeddingProvider, type: 'Embedding', color: 'purple' });
}
@@ -2306,7 +2310,7 @@ const manualTestConnection = async (
]}
currentModel={ragSettings.MODEL_CHOICE}
modelType="chat"
- selectedInstanceUrl={llmInstanceConfig.url.replace('/v1', '')}
+ selectedInstanceUrl={normalizeBaseUrl(llmInstanceConfig.url) ?? ''}
onSelectModel={(modelName: string) => {
setRagSettings({ ...ragSettings, MODEL_CHOICE: modelName });
showToast(`Selected LLM model: ${modelName}`, 'success');
@@ -2325,7 +2329,7 @@ const manualTestConnection = async (
]}
currentModel={ragSettings.EMBEDDING_MODEL}
modelType="embedding"
- selectedInstanceUrl={embeddingInstanceConfig.url.replace('/v1', '')}
+ selectedInstanceUrl={normalizeBaseUrl(embeddingInstanceConfig.url) ?? ''}
onSelectModel={(modelName: string) => {
setRagSettings({ ...ragSettings, EMBEDDING_MODEL: modelName });
showToast(`Selected embedding model: ${modelName}`, 'success');
@@ -2359,7 +2363,7 @@ const manualTestConnection = async (
};
// Helper functions to get provider-specific model display
-function getDisplayedChatModel(ragSettings: any): string {
+function getDisplayedChatModel(ragSettings: RAGSettingsProps["ragSettings"]): string {
const provider = ragSettings.LLM_PROVIDER || 'openai';
const modelChoice = ragSettings.MODEL_CHOICE;
@@ -2387,7 +2391,7 @@ function getDisplayedChatModel(ragSettings: any): string {
}
}
-function getDisplayedEmbeddingModel(ragSettings: any): string {
+function getDisplayedEmbeddingModel(ragSettings: RAGSettingsProps["ragSettings"]): string {
const provider = ragSettings.EMBEDDING_PROVIDER || ragSettings.LLM_PROVIDER || 'openai';
const embeddingModel = ragSettings.EMBEDDING_MODEL;
@@ -2416,7 +2420,7 @@ function getDisplayedEmbeddingModel(ragSettings: any): string {
}
// Helper functions for model placeholders
-function getModelPlaceholder(provider: string): string {
+function getModelPlaceholder(provider: ProviderKey): string {
switch (provider) {
case 'openai':
return 'e.g., gpt-4o-mini';
@@ -2435,7 +2439,7 @@ function getModelPlaceholder(provider: string): string {
}
}
-function getEmbeddingPlaceholder(provider: string): string {
+function getEmbeddingPlaceholder(provider: ProviderKey): string {
switch (provider) {
case 'openai':
return 'Default: text-embedding-3-small';
diff --git a/python/src/server/services/crawling/crawling_service.py b/python/src/server/services/crawling/crawling_service.py
index e02f43388d..82a98c0c83 100644
--- a/python/src/server/services/crawling/crawling_service.py
+++ b/python/src/server/services/crawling/crawling_service.py
@@ -33,24 +33,34 @@
# Global registry to track active orchestration services for cancellation support
_active_orchestrations: dict[str, "CrawlingService"] = {}
-_orchestration_lock = asyncio.Lock()
+_orchestration_lock: asyncio.Lock | None = None
+
+
+def _ensure_orchestration_lock() -> asyncio.Lock:
+ global _orchestration_lock
+ if _orchestration_lock is None:
+ _orchestration_lock = asyncio.Lock()
+ return _orchestration_lock
async def get_active_orchestration(progress_id: str) -> Optional["CrawlingService"]:
"""Get an active orchestration service by progress ID."""
- async with _orchestration_lock:
+ lock = _ensure_orchestration_lock()
+ async with lock:
return _active_orchestrations.get(progress_id)
async def register_orchestration(progress_id: str, orchestration: "CrawlingService"):
"""Register an active orchestration service."""
- async with _orchestration_lock:
+ lock = _ensure_orchestration_lock()
+ async with lock:
_active_orchestrations[progress_id] = orchestration
async def unregister_orchestration(progress_id: str):
"""Unregister an orchestration service."""
- async with _orchestration_lock:
+ lock = _ensure_orchestration_lock()
+ async with lock:
_active_orchestrations.pop(progress_id, None)
From 21cf54c8204281f61b3e5f77f6999a2227ef649a Mon Sep 17 00:00:00 2001
From: Chillbruhhh
Date: Thu, 25 Sep 2025 17:11:02 -0500
Subject: [PATCH 24/28] - migration/complete_setup.sql:101 seeds
Google/OpenRouter/Anthropic/Grok API key rows so fresh databases expose every
provider by default. -
migration/0.1.0/009_add_provider_placeholders.sql:1 backfills the same rows
for existing Supabase instances and records the migration. -
archon-ui-main/src/components/settings/RAGSettings.tsx:121 introduces a
shared credentialprovider map, reloadApiCredentials runs through all five
providers, and the status poller includes the new keys. -
archon-ui-main/src/components/settings/RAGSettings.tsx:353 subscribes to the
archon:credentials-updated browser event so adding/removing a key
immediately refetches credential status and pings the corresponding
connectivity test. -
archon-ui-main/src/components/settings/RAGSettings.tsx:926 now treats missing
Anthropic/OpenRouter/Grok keys as missing, preventing stale connected
badges when a key is removed.
---
.../src/components/settings/RAGSettings.tsx | 177 +++++++++++-------
.../0.1.0/009_add_provider_placeholders.sql | 18 ++
migration/complete_setup.sql | 5 +-
3 files changed, 131 insertions(+), 69 deletions(-)
create mode 100644 migration/0.1.0/009_add_provider_placeholders.sql
diff --git a/archon-ui-main/src/components/settings/RAGSettings.tsx b/archon-ui-main/src/components/settings/RAGSettings.tsx
index 9998e4fd8f..9e42f9ce28 100644
--- a/archon-ui-main/src/components/settings/RAGSettings.tsx
+++ b/archon-ui-main/src/components/settings/RAGSettings.tsx
@@ -1,4 +1,4 @@
-import React, { useState, useEffect, useRef } from 'react';
+import React, { useState, useEffect, useRef, useCallback } from 'react';
import { Settings, Check, Save, Loader, ChevronDown, ChevronUp, Zap, Database, Trash2, Cog } from 'lucide-react';
import { Card } from '../ui/Card';
import { Input } from '../ui/Input';
@@ -118,6 +118,24 @@ const isProviderKey = (value: unknown): value is ProviderKey =>
// Default base URL for Ollama instances when not explicitly configured
const DEFAULT_OLLAMA_URL = 'http://host.docker.internal:11434/v1';
+const PROVIDER_CREDENTIAL_KEYS = [
+ 'OPENAI_API_KEY',
+ 'GOOGLE_API_KEY',
+ 'ANTHROPIC_API_KEY',
+ 'OPENROUTER_API_KEY',
+ 'GROK_API_KEY',
+] as const;
+
+type ProviderCredentialKey = typeof PROVIDER_CREDENTIAL_KEYS[number];
+
+const CREDENTIAL_PROVIDER_MAP: Record = {
+ OPENAI_API_KEY: 'openai',
+ GOOGLE_API_KEY: 'google',
+ ANTHROPIC_API_KEY: 'anthropic',
+ OPENROUTER_API_KEY: 'openrouter',
+ GROK_API_KEY: 'grok',
+};
+
const normalizeBaseUrl = (url?: string | null): string | null => {
if (!url) return null;
const trimmed = url.trim();
@@ -284,73 +302,53 @@ export const RAGSettings = ({
}
}, [ragSettings.EMBEDDING_MODEL, embeddingProvider]);
- // Load API credentials for status checking
- useEffect(() => {
- const loadApiCredentials = async () => {
- try {
- // Get decrypted values for the API keys we need for status checking
- const keyNames = ['OPENAI_API_KEY', 'GOOGLE_API_KEY', 'ANTHROPIC_API_KEY'];
- const statusResults = await credentialsService.checkCredentialStatus(keyNames);
-
- const credentials: {[key: string]: boolean} = {};
-
- for (const [key, result] of Object.entries(statusResults)) {
- credentials[key] = !!result.has_value;
- }
-
- console.log('π Loaded API credentials for status checking:', Object.keys(credentials));
- setApiCredentials(credentials);
- } catch (error) {
- console.error('Failed to load API credentials for status checking:', error);
- }
- };
-
- loadApiCredentials();
- }, []);
-
- // Reload API credentials when ragSettings change (e.g., after saving)
- // Use a ref to track if we've loaded credentials to prevent infinite loops
const hasLoadedCredentialsRef = useRef(false);
-
- // Manual reload function for external calls
- const reloadApiCredentials = async () => {
+
+ const reloadApiCredentials = useCallback(async () => {
try {
- // Get decrypted values for the API keys we need for status checking
- const keyNames = ['OPENAI_API_KEY', 'GOOGLE_API_KEY', 'ANTHROPIC_API_KEY'];
- const statusResults = await credentialsService.checkCredentialStatus(keyNames);
-
- const credentials: {[key: string]: boolean} = {};
+ const statusResults = await credentialsService.checkCredentialStatus(
+ Array.from(PROVIDER_CREDENTIAL_KEYS),
+ );
+
+ const credentials: { [key: string]: boolean } = {};
- for (const [key, result] of Object.entries(statusResults)) {
- credentials[key] = !!result.has_value;
+ for (const key of PROVIDER_CREDENTIAL_KEYS) {
+ const result = statusResults[key];
+ credentials[key] = !!result?.has_value;
}
-
- console.log('π Reloaded API credentials for status checking:', Object.keys(credentials));
+
+ console.log(
+ 'π Updated API credential status snapshot:',
+ Object.keys(credentials),
+ );
setApiCredentials(credentials);
hasLoadedCredentialsRef.current = true;
} catch (error) {
- console.error('Failed to reload API credentials:', error);
+ console.error('Failed to load API credentials for status checking:', error);
}
- };
-
+ }, []);
+
+ useEffect(() => {
+ void reloadApiCredentials();
+ }, [reloadApiCredentials]);
+
useEffect(() => {
- // Only reload if we have ragSettings and haven't loaded yet, or if LLM_PROVIDER changed
- if (Object.keys(ragSettings).length > 0 && (!hasLoadedCredentialsRef.current || ragSettings.LLM_PROVIDER)) {
- reloadApiCredentials();
+ if (!hasLoadedCredentialsRef.current) {
+ return;
}
- }, [ragSettings.LLM_PROVIDER]); // Only depend on LLM_PROVIDER changes
-
- // Reload credentials periodically to catch updates from other components (like onboarding)
+
+ void reloadApiCredentials();
+ }, [ragSettings.LLM_PROVIDER, reloadApiCredentials]);
+
useEffect(() => {
- // Set up periodic reload every 30 seconds when component is active (reduced from 2s)
const interval = setInterval(() => {
if (Object.keys(ragSettings).length > 0) {
- reloadApiCredentials();
+ void reloadApiCredentials();
}
- }, 30000); // Changed from 2000ms to 30000ms (30 seconds)
+ }, 30000);
return () => clearInterval(interval);
- }, [ragSettings.LLM_PROVIDER]); // Only restart interval if provider changes
+ }, [ragSettings.LLM_PROVIDER, reloadApiCredentials]);
useEffect(() => {
const needsDetection = chatProvider === 'ollama' || embeddingProvider === 'ollama';
@@ -476,7 +474,7 @@ export const RAGSettings = ({
}, []);
// Test connection to external providers
- const testProviderConnection = async (provider: string): Promise => {
+ const testProviderConnection = useCallback(async (provider: string): Promise => {
setProviderConnectionStatus(prev => ({
...prev,
[provider]: { ...prev[provider], checking: true }
@@ -503,7 +501,7 @@ export const RAGSettings = ({
}));
return false;
}
- };
+ }, []);
// Test provider connections when API credentials change
useEffect(() => {
@@ -529,7 +527,39 @@ export const RAGSettings = ({
const interval = setInterval(testConnections, 60000);
return () => clearInterval(interval);
- }, [apiCredentials]); // Test when credentials change
+ }, [apiCredentials, testProviderConnection]); // Test when credentials change
+
+ useEffect(() => {
+ const handleCredentialUpdate = (event: Event) => {
+ const detail = (event as CustomEvent<{ keys?: string[] }>).detail;
+ const updatedKeys = (detail?.keys ?? []).map(key => key.toUpperCase());
+
+ if (updatedKeys.length === 0) {
+ void reloadApiCredentials();
+ return;
+ }
+
+ const touchedProviderKeys = updatedKeys.filter(key => key in CREDENTIAL_PROVIDER_MAP);
+ if (touchedProviderKeys.length === 0) {
+ return;
+ }
+
+ void reloadApiCredentials();
+
+ touchedProviderKeys.forEach(key => {
+ const provider = CREDENTIAL_PROVIDER_MAP[key as ProviderCredentialKey];
+ if (provider) {
+ void testProviderConnection(provider);
+ }
+ });
+ };
+
+ window.addEventListener('archon:credentials-updated', handleCredentialUpdate);
+
+ return () => {
+ window.removeEventListener('archon:credentials-updated', handleCredentialUpdate);
+ };
+ }, [reloadApiCredentials, testProviderConnection]);
// Ref to track if initial test has been run (will be used after function definitions)
const hasRunInitialTestRef = useRef(false);
@@ -893,33 +923,41 @@ const manualTestConnection = async (
}
}, [ragSettings.LLM_PROVIDER, embeddingProvider, llmStatus.online, embeddingStatus.online]);
+ const hasApiCredential = (credentialKey: ProviderCredentialKey): boolean => {
+ if (credentialKey in apiCredentials) {
+ return Boolean(apiCredentials[credentialKey]);
+ }
+
+ const fallbackKey = Object.keys(apiCredentials).find(
+ key => key.toUpperCase() === credentialKey,
+ );
+
+ return fallbackKey ? Boolean(apiCredentials[fallbackKey]) : false;
+ };
+
// Function to check if a provider is properly configured
const getProviderStatus = (providerKey: string): 'configured' | 'missing' | 'partial' => {
switch (providerKey) {
case 'openai':
- // Check if OpenAI API key is configured (case insensitive)
- const openAIKey = Object.keys(apiCredentials).find(key => key.toUpperCase() === 'OPENAI_API_KEY');
- const hasOpenAIKey = openAIKey ? !!apiCredentials[openAIKey] : false;
-
+ const hasOpenAIKey = hasApiCredential('OPENAI_API_KEY');
+
// Only show configured if we have both API key AND confirmed connection
const openAIConnected = providerConnectionStatus['openai']?.connected || false;
const isChecking = providerConnectionStatus['openai']?.checking || false;
-
+
// Intentionally avoid logging API key material.
-
+
if (!hasOpenAIKey) return 'missing';
if (isChecking) return 'partial';
return openAIConnected ? 'configured' : 'missing';
case 'google':
- // Check if Google API key is configured (case insensitive)
- const googleKey = Object.keys(apiCredentials).find(key => key.toUpperCase() === 'GOOGLE_API_KEY');
- const hasGoogleKey = googleKey ? !!apiCredentials[googleKey] : false;
+ const hasGoogleKey = hasApiCredential('GOOGLE_API_KEY');
// Only show configured if we have both API key AND confirmed connection
const googleConnected = providerConnectionStatus['google']?.connected || false;
const googleChecking = providerConnectionStatus['google']?.checking || false;
-
+
if (!hasGoogleKey) return 'missing';
if (googleChecking) return 'partial';
return googleConnected ? 'configured' : 'missing';
@@ -941,21 +979,24 @@ const manualTestConnection = async (
return 'missing';
}
case 'anthropic':
- // Use server-side connection status
+ const hasAnthropicKey = hasApiCredential('ANTHROPIC_API_KEY');
const anthropicConnected = providerConnectionStatus['anthropic']?.connected || false;
const anthropicChecking = providerConnectionStatus['anthropic']?.checking || false;
+ if (!hasAnthropicKey) return 'missing';
if (anthropicChecking) return 'partial';
return anthropicConnected ? 'configured' : 'missing';
case 'grok':
- // Use server-side connection status
+ const hasGrokKey = hasApiCredential('GROK_API_KEY');
const grokConnected = providerConnectionStatus['grok']?.connected || false;
const grokChecking = providerConnectionStatus['grok']?.checking || false;
+ if (!hasGrokKey) return 'missing';
if (grokChecking) return 'partial';
return grokConnected ? 'configured' : 'missing';
case 'openrouter':
- // Use server-side connection status
+ const hasOpenRouterKey = hasApiCredential('OPENROUTER_API_KEY');
const openRouterConnected = providerConnectionStatus['openrouter']?.connected || false;
const openRouterChecking = providerConnectionStatus['openrouter']?.checking || false;
+ if (!hasOpenRouterKey) return 'missing';
if (openRouterChecking) return 'partial';
return openRouterConnected ? 'configured' : 'missing';
default:
diff --git a/migration/0.1.0/009_add_provider_placeholders.sql b/migration/0.1.0/009_add_provider_placeholders.sql
new file mode 100644
index 0000000000..85d526e6c0
--- /dev/null
+++ b/migration/0.1.0/009_add_provider_placeholders.sql
@@ -0,0 +1,18 @@
+-- Migration: 009_add_provider_placeholders.sql
+-- Description: Add placeholder API key rows for OpenRouter, Anthropic, and Grok
+-- Version: 0.1.0
+-- Author: Archon Team
+-- Date: 2025
+
+-- Insert provider API key placeholders (idempotent)
+INSERT INTO archon_settings (key, encrypted_value, is_encrypted, category, description)
+VALUES
+ ('OPENROUTER_API_KEY', NULL, true, 'api_keys', 'OpenRouter API key for hosted community models. Get from: https://openrouter.ai/keys'),
+ ('ANTHROPIC_API_KEY', NULL, true, 'api_keys', 'Anthropic API key for Claude models. Get from: https://console.anthropic.com/account/keys'),
+ ('GROK_API_KEY', NULL, true, 'api_keys', 'Grok API key for xAI models. Get from: https://console.x.ai/')
+ON CONFLICT (key) DO NOTHING;
+
+-- Record migration application for tracking
+INSERT INTO archon_migrations (version, migration_name)
+VALUES ('0.1.0', '009_add_provider_placeholders')
+ON CONFLICT (version, migration_name) DO NOTHING;
diff --git a/migration/complete_setup.sql b/migration/complete_setup.sql
index 1609060cf3..801b07b423 100644
--- a/migration/complete_setup.sql
+++ b/migration/complete_setup.sql
@@ -100,7 +100,10 @@ ON CONFLICT (key) DO NOTHING;
-- Add provider API key placeholders
INSERT INTO archon_settings (key, encrypted_value, is_encrypted, category, description) VALUES
-('GOOGLE_API_KEY', NULL, true, 'api_keys', 'Google API Key for Gemini models. Get from: https://aistudio.google.com/apikey')
+('GOOGLE_API_KEY', NULL, true, 'api_keys', 'Google API key for Gemini models. Get from: https://aistudio.google.com/apikey'),
+('OPENROUTER_API_KEY', NULL, true, 'api_keys', 'OpenRouter API key for hosted community models. Get from: https://openrouter.ai/keys'),
+('ANTHROPIC_API_KEY', NULL, true, 'api_keys', 'Anthropic API key for Claude models. Get from: https://console.anthropic.com/account/keys'),
+('GROK_API_KEY', NULL, true, 'api_keys', 'Grok API key for xAI models. Get from: https://console.x.ai/')
ON CONFLICT (key) DO NOTHING;
-- Code Extraction Settings Migration
From b6906959afbf70897b033276c2ed902867a5e19e Mon Sep 17 00:00:00 2001
From: Chillbruhhh
Date: Thu, 25 Sep 2025 17:29:33 -0500
Subject: [PATCH 25/28] -
archon-ui-main/src/components/settings/RAGSettings.tsx:90 adds a simple
display-name map and reuses one red alert style. -
archon-ui-main/src/components/settings/RAGSettings.tsx:1016 now shows exactly
one red banner when the active provider - Removed the old duplicate Missing
API Key Configuration block, so the panel no longer stacks two warnings.
---
.../src/components/settings/RAGSettings.tsx | 71 ++++---------------
1 file changed, 12 insertions(+), 59 deletions(-)
diff --git a/archon-ui-main/src/components/settings/RAGSettings.tsx b/archon-ui-main/src/components/settings/RAGSettings.tsx
index 9e42f9ce28..62739fc77a 100644
--- a/archon-ui-main/src/components/settings/RAGSettings.tsx
+++ b/archon-ui-main/src/components/settings/RAGSettings.tsx
@@ -91,25 +91,17 @@ const colorStyles: Record = {
grok: 'border-yellow-500 bg-yellow-500/10',
};
-const providerAlertStyles: Record = {
- openai: 'bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800 text-green-800 dark:text-green-300',
- google: 'bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800 text-blue-800 dark:text-blue-300',
- openrouter: 'bg-cyan-50 dark:bg-cyan-900/20 border-cyan-200 dark:border-cyan-800 text-cyan-800 dark:text-cyan-300',
- ollama: 'bg-purple-50 dark:bg-purple-900/20 border-purple-200 dark:border-purple-800 text-purple-800 dark:text-purple-300',
- anthropic: 'bg-orange-50 dark:bg-orange-900/20 border-orange-200 dark:border-orange-800 text-orange-800 dark:text-orange-300',
- grok: 'bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-800 text-yellow-800 dark:text-yellow-300',
-};
-
const providerWarningAlertStyle = 'bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-800 text-yellow-800 dark:text-yellow-300';
const providerErrorAlertStyle = 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800 text-red-800 dark:text-red-300';
-
-const defaultProviderAlertMessages: Record = {
- openai: 'Configure your OpenAI API key in the credentials section to use GPT models.',
- google: 'Configure your Google API key in the credentials section to use Gemini models.',
- openrouter: 'Configure your OpenRouter API key in the credentials section to use models.',
- ollama: 'Configure your Ollama instances in this panel to connect local models.',
- anthropic: 'Configure your Anthropic API key in the credentials section to use Claude models.',
- grok: 'Configure your Grok API key in the credentials section to use Grok models.',
+const providerMissingAlertStyle = providerErrorAlertStyle;
+
+const providerDisplayNames: Record = {
+ openai: 'OpenAI',
+ google: 'Google',
+ openrouter: 'OpenRouter',
+ ollama: 'Ollama',
+ anthropic: 'Anthropic',
+ grok: 'Grok',
};
const isProviderKey = (value: unknown): value is ProviderKey =>
@@ -1022,8 +1014,9 @@ const manualTestConnection = async (
providerAlertClassName = providerWarningAlertStyle;
}
} else if (activeProviderKey && selectedProviderStatus === 'missing') {
- providerAlertMessage = defaultProviderAlertMessages[activeProviderKey] ?? null;
- providerAlertClassName = providerAlertStyles[activeProviderKey] ?? '';
+ const providerName = providerDisplayNames[activeProviderKey] ?? activeProviderKey;
+ providerAlertMessage = `${providerName} API key is not configured. Add it in Settings > API Keys.`;
+ providerAlertClassName = providerMissingAlertStyle;
}
const shouldShowProviderAlert = Boolean(providerAlertMessage);
@@ -1385,46 +1378,6 @@ const manualTestConnection = async (
))}
-
- {/* API Key Validation Warnings */}
- {(() => {
- const chatStatus = getProviderStatus(chatProvider);
- const embeddingStatus = getProviderStatus(embeddingProvider);
- const missingProviders = [];
- const providerToIgnore = activeProviderKey;
-
- if (chatStatus === 'missing' && (!providerToIgnore || chatProvider !== providerToIgnore)) {
- missingProviders.push({ name: chatProvider, type: 'Chat', color: 'green' });
- }
- if (
- embeddingStatus === 'missing' &&
- embeddingProvider !== chatProvider &&
- (!providerToIgnore || embeddingProvider !== providerToIgnore)
- ) {
- missingProviders.push({ name: embeddingProvider, type: 'Embedding', color: 'purple' });
- }
-
- if (missingProviders.length > 0) {
- return (
-
-
-
-
-
-
- Missing API Key Configuration
-
-
-
- Please configure API keys for: {missingProviders.map(p => `${p.name} (${p.type})`).join(', ')}
-
-
- );
- }
- return null;
- })()}
-
-
{shouldShowProviderAlert && (
{providerAlertMessage}
From c07beeb052980ff518060b9ba87eb3a8c951a14b Mon Sep 17 00:00:00 2001
From: Josh
Date: Fri, 26 Sep 2025 06:32:36 -0500
Subject: [PATCH 26/28] Update credentialsService.ts default model
---
archon-ui-main/src/services/credentialsService.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/archon-ui-main/src/services/credentialsService.ts b/archon-ui-main/src/services/credentialsService.ts
index 8287be76f8..b2d2da52fa 100644
--- a/archon-ui-main/src/services/credentialsService.ts
+++ b/archon-ui-main/src/services/credentialsService.ts
@@ -195,7 +195,7 @@ class CredentialsService {
USE_HYBRID_SEARCH: true,
USE_AGENTIC_RAG: true,
USE_RERANKING: true,
- MODEL_CHOICE: "gpt-4o-mini",
+ MODEL_CHOICE: "gpt-4.1-nano",
LLM_PROVIDER: "openai",
LLM_BASE_URL: "",
LLM_INSTANCE_NAME: "",
From e438b7109dff10bb0a4671327d70d175297ac159 Mon Sep 17 00:00:00 2001
From: Chillbruhhh
Date: Sat, 27 Sep 2025 20:30:41 -0500
Subject: [PATCH 27/28] updated the google embedding adapter for multi
dimensional rag querying
---
.../services/embeddings/embedding_service.py | 37 +++++++++++++++++--
1 file changed, 33 insertions(+), 4 deletions(-)
diff --git a/python/src/server/services/embeddings/embedding_service.py b/python/src/server/services/embeddings/embedding_service.py
index 1a71cfdcef..095ef27872 100644
--- a/python/src/server/services/embeddings/embedding_service.py
+++ b/python/src/server/services/embeddings/embedding_service.py
@@ -12,6 +12,7 @@
from typing import Any
import httpx
+import numpy as np
import openai
from ...config.logfire_config import safe_span, search_logger
@@ -113,9 +114,6 @@ async def create_embeddings(
dimensions: int | None = None,
) -> list[list[float]]:
try:
- if dimensions is not None:
- _ = dimensions # Maintains adapter signature; Google controls dimensions server-side.
-
google_api_key = await credential_service.get_credential("GOOGLE_API_KEY")
if not google_api_key:
raise EmbeddingAPIError("Google API key not found")
@@ -123,7 +121,7 @@ async def create_embeddings(
async with httpx.AsyncClient(timeout=30.0) as http_client:
embeddings = await asyncio.gather(
*(
- self._fetch_single_embedding(http_client, google_api_key, model, text)
+ self._fetch_single_embedding(http_client, google_api_key, model, text, dimensions)
for text in texts
)
)
@@ -152,6 +150,7 @@ async def _fetch_single_embedding(
api_key: str,
model: str,
text: str,
+ dimensions: int | None = None,
) -> list[float]:
if model.startswith("models/"):
url_model = model[len("models/") :]
@@ -169,6 +168,16 @@ async def _fetch_single_embedding(
"content": {"parts": [{"text": text}]},
}
+ # Add output_dimensionality parameter if dimensions are specified
+ if dimensions is not None and dimensions > 0:
+ # Validate that the requested dimension is supported by Google
+ if dimensions not in [128, 256, 512, 768, 1024, 1536, 2048, 3072]:
+ search_logger.warning(
+ f"Requested dimension {dimensions} may not be supported by Google. "
+ f"Supported dimensions: 128, 256, 512, 768, 1024, 1536, 2048, 3072"
+ )
+ payload["outputDimensionality"] = dimensions
+
response = await http_client.post(url, headers=headers, json=payload)
response.raise_for_status()
@@ -178,8 +187,28 @@ async def _fetch_single_embedding(
if not isinstance(values, list):
raise EmbeddingAPIError(f"Invalid embedding payload from Google: {result}")
+ # Normalize embeddings for dimensions < 3072 as per Google's documentation
+ if dimensions is not None and dimensions < 3072 and len(values) > 0:
+ values = self._normalize_embedding(values)
+
return values
+ def _normalize_embedding(self, embedding: list[float]) -> list[float]:
+ """Normalize embedding vector for dimensions < 3072."""
+ try:
+ embedding_array = np.array(embedding, dtype=np.float32)
+ norm = np.linalg.norm(embedding_array)
+ if norm > 0:
+ normalized = embedding_array / norm
+ return normalized.tolist()
+ else:
+ search_logger.warning("Zero-norm embedding detected, returning unnormalized")
+ return embedding
+ except Exception as e:
+ search_logger.error(f"Failed to normalize embedding: {e}")
+ # Return original embedding if normalization fails
+ return embedding
+
def _get_embedding_adapter(provider: str, client: Any) -> EmbeddingProviderAdapter:
provider_name = (provider or "").lower()
From b5930ba52b46cc10db41cd90e104e545214a76c3 Mon Sep 17 00:00:00 2001
From: Chillbruhhh
Date: Mon, 29 Sep 2025 23:00:42 -0500
Subject: [PATCH 28/28] thought this micro fix in the google embedding pushed
with the embedding update the other day, it didnt. pushing now
---
.../services/embeddings/embedding_service.py | 27 ++++++++++++-------
1 file changed, 17 insertions(+), 10 deletions(-)
diff --git a/python/src/server/services/embeddings/embedding_service.py b/python/src/server/services/embeddings/embedding_service.py
index 095ef27872..87ce390b67 100644
--- a/python/src/server/services/embeddings/embedding_service.py
+++ b/python/src/server/services/embeddings/embedding_service.py
@@ -83,10 +83,10 @@ async def create_embeddings(
class OpenAICompatibleEmbeddingAdapter(EmbeddingProviderAdapter):
"""Adapter for providers using the OpenAI embeddings API shape."""
-
+
def __init__(self, client: Any):
self._client = client
-
+
async def create_embeddings(
self,
texts: list[str],
@@ -99,7 +99,7 @@ async def create_embeddings(
}
if dimensions is not None:
request_args["dimensions"] = dimensions
-
+
response = await self._client.embeddings.create(**request_args)
return [item.embedding for item in response.data]
@@ -168,15 +168,21 @@ async def _fetch_single_embedding(
"content": {"parts": [{"text": text}]},
}
- # Add output_dimensionality parameter if dimensions are specified
+ # Add output_dimensionality parameter if dimensions are specified and supported
if dimensions is not None and dimensions > 0:
- # Validate that the requested dimension is supported by Google
- if dimensions not in [128, 256, 512, 768, 1024, 1536, 2048, 3072]:
+ model_name = payload_model.removeprefix("models/")
+ if model_name.startswith("textembedding-gecko"):
+ supported_dimensions = {128, 256, 512, 768}
+ else:
+ supported_dimensions = {128, 256, 512, 768, 1024, 1536, 2048, 3072}
+
+ if dimensions in supported_dimensions:
+ payload["outputDimensionality"] = dimensions
+ else:
search_logger.warning(
- f"Requested dimension {dimensions} may not be supported by Google. "
- f"Supported dimensions: 128, 256, 512, 768, 1024, 1536, 2048, 3072"
+ f"Requested dimension {dimensions} is not supported by Google model '{model_name}'. "
+ "Falling back to the provider default."
)
- payload["outputDimensionality"] = dimensions
response = await http_client.post(url, headers=headers, json=payload)
response.raise_for_status()
@@ -188,7 +194,8 @@ async def _fetch_single_embedding(
raise EmbeddingAPIError(f"Invalid embedding payload from Google: {result}")
# Normalize embeddings for dimensions < 3072 as per Google's documentation
- if dimensions is not None and dimensions < 3072 and len(values) > 0:
+ actual_dimension = len(values)
+ if actual_dimension > 0 and actual_dimension < 3072:
values = self._normalize_embedding(values)
return values