Spaces:
Running
Running
File size: 2,347 Bytes
6303ae6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | """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
|