Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions src/extension_shield/llm/clients/fallback.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""LLM fallback client module for multi-provider support with automatic failover."""

import os
import re
import logging
import threading
from typing import Dict, Optional, Any, List
Expand All @@ -12,6 +13,20 @@

logger = logging.getLogger(__name__)

# Patterns that may indicate leaked secrets in error messages
_SECRET_PATTERNS = [
re.compile(r"sk-[A-Za-z0-9\-_]{10,}", re.IGNORECASE), # OpenAI/Groq keys
re.compile(r"Bearer\s+[A-Za-z0-9\-_\.]{10,}", re.IGNORECASE), # Bearer tokens
re.compile(r"key[=:\s]+['\"]?[A-Za-z0-9\-_]{20,}['\"]?", re.IGNORECASE), # Generic key=value
]


def _sanitize_error_message(msg: str) -> str:
"""Remove potential secrets (API keys, tokens) from error messages."""
for pattern in _SECRET_PATTERNS:
msg = pattern.sub("[REDACTED]", msg)
return msg


class LLMFallbackError(Exception):
"""Exception raised when all LLM providers fail."""
Expand Down Expand Up @@ -284,8 +299,8 @@ def invoke_with_fallback(
# Don't retry non-retryable errors
break

# Store error for final exception
errors[f"{provider_name} (attempt {attempt + 1})"] = f"{error_type}: {error_msg[:200]}"
# Store sanitized error for final exception (strip potential secrets)
errors[f"{provider_name} (attempt {attempt + 1})"] = f"{error_type}: {_sanitize_error_message(error_msg[:200])}"

# If this was the last retry for this provider, try next provider
if attempt < max_retries:
Expand Down