Spaces:
Sleeping
Sleeping
| """Secret/payload redaction patterns shared across the app. | |
| This module has no internal dependencies so it can be imported by both | |
| :mod:`app.utils.logging_utils` (sink-level redaction Filter) and | |
| :mod:`app.services.llm_client` (error-message sanitizer) without creating | |
| an import cycle. It is the single source of truth for what an API key or | |
| provider organization ID looks like. | |
| """ | |
| from __future__ import annotations | |
| import re | |
| from typing import Iterable, Optional | |
| # Patterns that match API-key-shaped tokens for every supported provider. | |
| # Provider SDKs (and OpenAI-compatible endpoints such as Groq's, plus | |
| # google-api-core for Gemini) routinely echo the raw key in auth errors. | |
| API_KEY_PATTERNS: tuple[re.Pattern[str], ...] = ( | |
| re.compile(r"sk-ant-[A-Za-z0-9_\-]{6,}"), # Anthropic | |
| re.compile(r"sk-proj-[A-Za-z0-9_\-]{6,}"), # OpenAI project keys | |
| re.compile(r"sk-[A-Za-z0-9_\-]{16,}"), # OpenAI / generic sk- keys | |
| re.compile(r"gsk_[A-Za-z0-9_\-]{8,}"), # Groq | |
| re.compile(r"AIza[0-9A-Za-z_\-]{20,}"), # Google / Gemini | |
| ) | |
| # Provider organization IDs / request slugs we never want to surface. | |
| ORG_ID_PATTERNS: tuple[re.Pattern[str], ...] = ( | |
| re.compile(r"\borg[-_][A-Za-z0-9]{4,}\b", re.IGNORECASE), | |
| re.compile(r"\borganization[-_]?id[\"'=:\s]+[A-Za-z0-9_-]{4,}", re.IGNORECASE), | |
| re.compile(r"\bopenai[- _]?organization[\"'=:\s]+[A-Za-z0-9_-]{4,}", re.IGNORECASE), | |
| re.compile(r"\banthropic[- _]?organization[\"'=:\s]+[A-Za-z0-9_-]{4,}", re.IGNORECASE), | |
| ) | |
| def redact(text: Optional[str], *, secrets: Iterable[Optional[str]] = ()) -> Optional[str]: | |
| """Return ``text`` with API keys and organization IDs scrubbed. | |
| Redacts, in order: any exact ``secrets`` value the caller supplies | |
| (catches even unanticipated key formats), then key-shaped tokens for | |
| every provider family, then provider organization IDs. Idempotent and | |
| never returns a raw key. | |
| """ | |
| if not text: | |
| return text | |
| cleaned = str(text) | |
| for secret in secrets: | |
| if secret: | |
| cleaned = cleaned.replace(str(secret), "[api key redacted]") | |
| for pattern in API_KEY_PATTERNS: | |
| cleaned = pattern.sub("[api key redacted]", cleaned) | |
| for pattern in ORG_ID_PATTERNS: | |
| cleaned = pattern.sub("[organization id redacted]", cleaned) | |
| return cleaned | |