feat: Sidecar V2 — multi-pool provider proxy with 429 cooldown

- proxy.py: Fix route path duplication (v1/v1 → v1) when upstream
  base URL already includes /v1 prefix
- proxy.py: Fix _emergency_count global variable for metrics tracking
- server.py: Add logging.basicConfig(level=logging.INFO) for structlog
  INFO-level log visibility
- Full multi-pool routing: primary → fallback → emergency passthrough
- Per-backend rate limiting with RPM-based token bucket
- 429 cooldown mechanism with automatic recovery
- Dashboard with SSE real-time monitoring
- Admin API for backend/pool/config management
- SQLite-backed persistence with encrypted API key storage
- Docker compose deployment

Deployed by opengineer 严维序 as BIZ-50 Step 4
This commit is contained in:
2026-06-25 21:20:32 +08:00
commit 2d95ae50a5
26 changed files with 3625 additions and 0 deletions
+108
View File
@@ -0,0 +1,108 @@
"""AES-256-GCM encryption for API Key storage."""
import os
import secrets
import structlog
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
logger = structlog.get_logger()
_ENCRYPTION_KEY: bytes | None = None
_cipher: AESGCM | None = None
def init_crypto(hex_key: str) -> None:
"""Initialize the encryption module.
Validates the key and prepares the cipher.
Raises ValueError if key is invalid.
"""
global _ENCRYPTION_KEY, _cipher
if not hex_key:
raise ValueError("FATAL: SIDECAR_ENCRYPTION_KEY not set")
if len(hex_key) != 64:
raise ValueError(
f"FATAL: SIDECAR_ENCRYPTION_KEY must be 64 hex chars (32 bytes), "
f"got {len(hex_key)} chars"
)
try:
key_bytes = bytes.fromhex(hex_key)
except ValueError:
raise ValueError(
"FATAL: SIDECAR_ENCRYPTION_KEY must be valid hexadecimal"
)
global _ENCRYPTION_KEY, _cipher
_ENCRYPTION_KEY = key_bytes
_cipher = AESGCM(key_bytes)
logger.info("crypto_initialized")
def encrypt(plaintext: str) -> str:
"""Encrypt plaintext using AES-256-GCM.
Returns: hex-encoded nonce (12 bytes) + ciphertext + tag.
Format: <nonce_hex>:<ciphertext_hex>
"""
if _cipher is None:
raise RuntimeError("Crypto not initialized. Call init_crypto() first.")
nonce = secrets.token_bytes(12)
ciphertext = _cipher.encrypt(nonce, plaintext.encode("utf-8"), None)
return nonce.hex() + ":" + ciphertext.hex()
def decrypt(encrypted: str) -> str:
"""Decrypt AES-256-GCM ciphertext.
Args:
encrypted: Format "<nonce_hex>:<ciphertext_hex>"
Returns: Decrypted plaintext string.
"""
if _cipher is None:
raise RuntimeError("Crypto not initialized. Call init_crypto() first.")
parts = encrypted.split(":", 1)
if len(parts) != 2:
raise ValueError("Invalid encrypted format: expected nonce:ciphertext")
nonce = bytes.fromhex(parts[0])
ciphertext = bytes.fromhex(parts[1])
try:
plaintext = _cipher.decrypt(nonce, ciphertext, None)
return plaintext.decode("utf-8")
except Exception as e:
raise ValueError(f"Decryption failed: {e}")
def is_initialized() -> bool:
"""Check if crypto has been initialized."""
return _cipher is not None
def mask_api_key(api_key_plain: str) -> str:
"""Mask API key for display: show first 6 + last 4 chars."""
if len(api_key_plain) <= 10:
return api_key_plain[:2] + "****"
return api_key_plain[:6] + "****" + api_key_plain[-4:]
def try_decrypt_existing(encrypted_value: str) -> str | None:
"""Try to decrypt an existing encrypted value.
Returns the plaintext if successful, None if decryption fails
(e.g., encryption key was changed).
"""
try:
return decrypt(encrypted_value)
except Exception:
logger.warning(
"decrypt_existing_failed",
hint="Encryption key may have been changed, existing keys unrecoverable"
)
return None