headroom_3 / headroom /proxy /helpers.py
chopratejas's picture
Fix code review issues: batch NameError, null safety, token types
ca7cb92
Raw
History Blame
6.96 kB
"""Top-level helper functions and constants for the Headroom proxy.
Contains lazy loaders, file logging setup, request body decompression,
and safety-limit constants.
Extracted from server.py for maintainability.
"""
from __future__ import annotations
import json
import logging
from pathlib import Path
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from fastapi import Request
logger = logging.getLogger("headroom.proxy")
# Maximum request body size (100MB - increased to support image-heavy requests)
MAX_REQUEST_BODY_SIZE = 100 * 1024 * 1024
# Maximum SSE buffer size (10MB - prevents memory exhaustion from malformed streams)
MAX_SSE_BUFFER_SIZE = 10 * 1024 * 1024
# Maximum message array length (prevents DoS from deeply nested payloads)
MAX_MESSAGE_ARRAY_LENGTH = 10000
# Compression pipeline timeout in seconds
COMPRESSION_TIMEOUT_SECONDS = 30
# Maximum compression cache sessions (prevents unbounded memory growth)
MAX_COMPRESSION_CACHE_SESSIONS = 500
# Image compression (lazy-loaded to avoid heavy dependencies at startup)
_image_compressor = None
def _get_image_compressor():
"""Lazy load image compressor to avoid startup overhead."""
global _image_compressor
if _image_compressor is None:
try:
from headroom.image import ImageCompressor
_image_compressor = ImageCompressor()
logger.info("Image compression enabled (model: chopratejas/technique-router)")
except ImportError as e:
logger.warning(f"Image compression not available: {e}")
_image_compressor = False # Mark as unavailable
return _image_compressor if _image_compressor else None
# Always-on file logging to ~/.headroom/logs/ for `headroom perf` analysis
_HEADROOM_LOG_DIR = Path.home() / ".headroom" / "logs"
def _setup_file_logging() -> None:
"""Add a RotatingFileHandler to the headroom root logger.
Writes to ~/.headroom/logs/proxy.log with automatic rotation:
- Rotates at 10 MB
- Keeps 5 backups (~50 MB max)
"""
from logging.handlers import RotatingFileHandler
try:
_HEADROOM_LOG_DIR.mkdir(parents=True, exist_ok=True)
log_path = _HEADROOM_LOG_DIR / "proxy.log"
handler = RotatingFileHandler(
log_path,
maxBytes=10 * 1024 * 1024, # 10 MB
backupCount=5,
encoding="utf-8",
)
handler.setLevel(logging.INFO)
handler.setFormatter(
logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
)
# Attach to the headroom root logger so all sub-loggers are captured
logging.getLogger("headroom").addHandler(handler)
except OSError:
# Non-fatal: can't write logs (read-only fs, permissions, etc.)
pass
def _get_rtk_stats() -> dict[str, Any] | None:
"""Get rtk (Rust Token Killer) savings stats if rtk is installed.
Reads from rtk's tracking database via `rtk gain --format json`.
Returns None if rtk is not installed.
"""
import shutil
import subprocess as _sp
rtk_bin = shutil.which("rtk")
if not rtk_bin:
# Check headroom-managed install
rtk_managed = Path.home() / ".headroom" / "bin" / "rtk"
if rtk_managed.exists():
rtk_bin = str(rtk_managed)
else:
return None
try:
result = _sp.run(
[rtk_bin, "gain", "--format", "json"],
capture_output=True,
text=True,
timeout=5,
)
if result.returncode == 0 and result.stdout.strip():
data = json.loads(result.stdout)
summary = data.get("summary", {})
return {
"installed": True,
"total_commands": summary.get("total_commands", 0),
"tokens_saved": summary.get("total_saved", 0),
"avg_savings_pct": summary.get("avg_savings_pct", 0.0),
}
except Exception:
pass
return {"installed": True, "total_commands": 0, "tokens_saved": 0, "avg_savings_pct": 0.0}
async def _read_request_json(request: Request) -> dict[str, Any]:
"""Read and parse JSON from a request, handling compressed bodies.
Clients like OpenAI Codex may send zstd, gzip, or deflate-compressed
request bodies. Starlette's ``request.json()`` does not decompress
automatically, causing a UnicodeDecodeError on compressed bytes.
This helper inspects ``Content-Encoding``, decompresses if needed,
then JSON-decodes the result. It raises ``ValueError`` on any
decompression or parse failure so callers can return a clean 400.
"""
encoding = (request.headers.get("content-encoding") or "").lower().strip()
raw = await request.body()
if encoding in ("zstd", "zstandard"):
try:
import zstandard
dctx = zstandard.ZstdDecompressor()
# Use stream_reader for streaming zstd frames (no content size in header).
# Plain decompress() fails when the frame header omits the size, which
# is common with clients like OpenAI Codex.
reader = dctx.stream_reader(raw)
raw = reader.read()
reader.close()
except ImportError:
raise ValueError(
"Request body is zstd-compressed but the 'zstandard' package is not installed. "
"Install it with: pip install zstandard"
) from None
except Exception as exc:
raise ValueError(f"Failed to decompress zstd request body: {exc}") from exc
elif encoding == "gzip":
import gzip as _gzip
try:
raw = _gzip.decompress(raw)
except Exception as exc:
raise ValueError(f"Failed to decompress gzip request body: {exc}") from exc
elif encoding == "deflate":
import zlib
try:
raw = zlib.decompress(raw)
except Exception as exc:
raise ValueError(f"Failed to decompress deflate request body: {exc}") from exc
elif encoding == "br":
try:
import brotli
raw = brotli.decompress(raw)
except ImportError:
raise ValueError(
"Request body is brotli-compressed but the 'brotli' package is not installed."
) from None
except Exception as exc:
raise ValueError(f"Failed to decompress brotli request body: {exc}") from exc
elif encoding and encoding != "identity":
raise ValueError(f"Unsupported Content-Encoding: {encoding}")
# Decode and parse JSON
try:
text = raw.decode("utf-8")
except UnicodeDecodeError as exc:
raise ValueError(f"Request body is not valid UTF-8 (possibly compressed?): {exc}") from exc
result = json.loads(text)
if not isinstance(result, dict):
raise ValueError("Request body must be a JSON object, not " + type(result).__name__)
return result