mirror of
https://github.com/theroyallab/tabbyAPI.git
synced 2026-06-29 02:37:13 +00:00
116 lines
3.2 KiB
Python
116 lines
3.2 KiB
Python
"""Utilities for writing reproducible request debug logs."""
|
|
|
|
import json
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import aiofiles
|
|
from fastapi import Request
|
|
|
|
from common.logger import xlogger
|
|
from common.tabby_config import config
|
|
|
|
|
|
DEBUG_LOG_DIR = Path("logs/debug")
|
|
|
|
SENSITIVE_HEADERS = {
|
|
"authorization",
|
|
"cookie",
|
|
"host",
|
|
"set-cookie",
|
|
"user-agent",
|
|
"x-admin-key",
|
|
"x-api-key",
|
|
}
|
|
|
|
SENSITIVE_KEYS = {
|
|
"admin_key",
|
|
"api_key",
|
|
"authorization",
|
|
"hf_token",
|
|
"token",
|
|
"x-admin-key",
|
|
"x-api-key",
|
|
}
|
|
|
|
|
|
def redact_sensitive_values(value: Any) -> Any:
|
|
"""Recursively redact auth-like fields from a JSON-compatible object."""
|
|
|
|
if isinstance(value, dict):
|
|
redacted = {}
|
|
for key, item in value.items():
|
|
if key.lower() in SENSITIVE_KEYS:
|
|
redacted[key] = "[REDACTED]"
|
|
else:
|
|
redacted[key] = redact_sensitive_values(item)
|
|
return redacted
|
|
|
|
if isinstance(value, list):
|
|
return [redact_sensitive_values(item) for item in value]
|
|
|
|
return value
|
|
|
|
|
|
def redact_headers(headers) -> dict:
|
|
"""Return request headers with credential-bearing values removed."""
|
|
|
|
return {
|
|
key: "[REDACTED]" if key.lower() in SENSITIVE_HEADERS else value
|
|
for key, value in headers.items()
|
|
}
|
|
|
|
|
|
async def write_chat_completion_request_log(request: Request, body: dict) -> Path | None:
|
|
"""Write a /v1/chat/completions request as a JSON debug artifact."""
|
|
|
|
timestamp = datetime.now(timezone.utc)
|
|
request_id = getattr(request.state, "id", "unknown")
|
|
filename = f"{timestamp.strftime('%Y%m%dT%H%M%S.%fZ')}_{request_id}.json"
|
|
log_path = DEBUG_LOG_DIR / filename
|
|
|
|
payload = {
|
|
"schema": "tabbyapi.chat_completion_request_log.v1",
|
|
"created_at": timestamp.isoformat(),
|
|
"request": {
|
|
"id": "[REDACTED]",
|
|
"method": request.method,
|
|
"url": f"[REDACTED]{request.url.path}",
|
|
"path": request.url.path,
|
|
"query": redact_sensitive_values(dict(request.query_params)),
|
|
"headers": redact_headers(request.headers),
|
|
"body": redact_sensitive_values(body),
|
|
},
|
|
}
|
|
|
|
try:
|
|
DEBUG_LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
async with aiofiles.open(log_path, "w", encoding="utf8") as debug_file:
|
|
await debug_file.write(json.dumps(payload, indent=2, ensure_ascii=False))
|
|
await debug_file.write("\n")
|
|
except Exception as exc:
|
|
xlogger.error(
|
|
"Failed to write chat completion request debug log",
|
|
str(exc),
|
|
details=f"\n{exc}",
|
|
)
|
|
return None
|
|
|
|
xlogger.info(f"Wrote chat completion request debug log: {log_path}")
|
|
return log_path
|
|
|
|
|
|
async def log_chat_completion_request(request: Request):
|
|
"""FastAPI dependency to log chat completion request bodies when enabled."""
|
|
|
|
if not config.logging.log_chat_completion_requests:
|
|
return
|
|
|
|
try:
|
|
body = await request.json()
|
|
except Exception:
|
|
body = {"_invalid_json_body": "[omitted: request body was not valid JSON]"}
|
|
|
|
await write_chat_completion_request_log(request, body)
|