555e282cc4
ci / changelog_check (push) Waiting to run
ci / check_changes (push) Waiting to run
ci / build_mem0 (3.10) (push) Blocked by required conditions
ci / build_mem0 (3.11) (push) Blocked by required conditions
ci / build_mem0 (3.12) (push) Blocked by required conditions
CLI Node CI / lint (push) Waiting to run
CLI Node CI / test (20) (push) Waiting to run
CLI Node CI / test (22) (push) Waiting to run
CLI Node CI / build (push) Waiting to run
CLI Python CI / lint (push) Waiting to run
CLI Python CI / test (3.10) (push) Waiting to run
CLI Python CI / test (3.11) (push) Waiting to run
CLI Python CI / test (3.12) (push) Waiting to run
CLI Python CI / build (push) Waiting to run
openclaw checks / lint (push) Waiting to run
openclaw checks / test (20) (push) Waiting to run
openclaw checks / test (22) (push) Waiting to run
openclaw checks / build (push) Waiting to run
opencode-plugin checks / build (push) Waiting to run
pi-agent-plugin checks / lint (push) Waiting to run
pi-agent-plugin checks / test (20) (push) Waiting to run
pi-agent-plugin checks / test (22) (push) Waiting to run
pi-agent-plugin checks / build (push) Waiting to run
TypeScript SDK CI / check_changes (push) Waiting to run
TypeScript SDK CI / changelog_check (push) Blocked by required conditions
TypeScript SDK CI / build_ts_sdk (20) (push) Blocked by required conditions
TypeScript SDK CI / build_ts_sdk (22) (push) Blocked by required conditions
TypeScript SDK CI / integration_ts_sdk (20) (push) Blocked by required conditions
TypeScript SDK CI / integration_ts_sdk (22) (push) Blocked by required conditions
96 lines
3.5 KiB
Python
96 lines
3.5 KiB
Python
import logging
|
|
import sys
|
|
import uuid
|
|
from contextvars import ContextVar
|
|
|
|
from fastapi import HTTPException, Request
|
|
from fastapi.responses import JSONResponse
|
|
|
|
request_id_var: ContextVar[str] = ContextVar("request_id", default="-")
|
|
|
|
|
|
class UpstreamError(HTTPException):
|
|
def __init__(self, code: str, detail: str, request_id: str) -> None:
|
|
super().__init__(status_code=502, detail=detail)
|
|
self.code = code
|
|
self.request_id = request_id
|
|
|
|
|
|
_AUTH_NAMES = {"AuthenticationError", "PermissionDeniedError"}
|
|
_RATE_NAMES = {"RateLimitError"}
|
|
_TIMEOUT_NAMES = {"APITimeoutError"}
|
|
_CONN_NAMES = {"APIConnectionError", "ConnectionError"}
|
|
_BAD_REQUEST_NAMES = {"BadRequestError", "UnprocessableEntityError"}
|
|
_DB_NAMES = {"OperationalError", "DBAPIError", "DisconnectionError"}
|
|
_VECTOR_NAMES = {"UnexpectedResponse", "ResponseHandlingException"}
|
|
|
|
|
|
def _classify_one(exc: BaseException) -> tuple[str, str]:
|
|
name = type(exc).__name__
|
|
module = getattr(type(exc), "__module__", "") or ""
|
|
status = getattr(exc, "status_code", None)
|
|
|
|
if name in _AUTH_NAMES or status in (401, 403):
|
|
return (
|
|
"provider_auth_failed",
|
|
"Provider rejected the request (authentication). "
|
|
"Check your LLM provider API key on the Configuration page.",
|
|
)
|
|
if name in _RATE_NAMES or status == 429:
|
|
return ("provider_rate_limited", "Provider rate limit hit. Retry shortly.")
|
|
if name in _TIMEOUT_NAMES or isinstance(exc, TimeoutError):
|
|
return ("provider_timeout", "Provider timed out. Retry shortly.")
|
|
if name in _CONN_NAMES or (isinstance(status, int) and status >= 500):
|
|
return ("provider_unavailable", "Provider is unreachable or returned a server error.")
|
|
if name in _BAD_REQUEST_NAMES or status in (400, 422):
|
|
return ("provider_bad_request", "Provider rejected the request as malformed.")
|
|
if name in _DB_NAMES:
|
|
return ("datastore_unavailable", "The memory database is unreachable.")
|
|
if name in _VECTOR_NAMES or module.startswith("qdrant_client"):
|
|
return ("vector_store_unavailable", "The vector store is unreachable or returned an error.")
|
|
return ("unknown", "Upstream provider error.")
|
|
|
|
|
|
def _classify(exc: BaseException | None) -> tuple[str, str]:
|
|
# Walk the cause/context chain so wrapped provider errors still classify correctly.
|
|
seen: set[int] = set()
|
|
current = exc
|
|
while current is not None and id(current) not in seen:
|
|
seen.add(id(current))
|
|
result = _classify_one(current)
|
|
if result[0] != "unknown":
|
|
return result
|
|
current = current.__cause__ or current.__context__
|
|
return ("unknown", "Upstream provider error.")
|
|
|
|
|
|
def new_request_id() -> str:
|
|
return uuid.uuid4().hex[:8]
|
|
|
|
|
|
def upstream_error() -> UpstreamError:
|
|
exc = sys.exc_info()[1]
|
|
code, message = _classify(exc)
|
|
rid = request_id_var.get()
|
|
logging.exception("Upstream provider error (code=%s)", code)
|
|
return UpstreamError(code=code, detail=message, request_id=rid)
|
|
|
|
|
|
async def upstream_error_handler(_: Request, exc: UpstreamError) -> JSONResponse:
|
|
return JSONResponse(
|
|
status_code=exc.status_code,
|
|
content={"detail": exc.detail, "code": exc.code, "request_id": exc.request_id},
|
|
headers={"X-Request-ID": exc.request_id},
|
|
)
|
|
|
|
|
|
def install_request_id_logging() -> None:
|
|
old_factory = logging.getLogRecordFactory()
|
|
|
|
def factory(*args, **kwargs):
|
|
record = old_factory(*args, **kwargs)
|
|
record.request_id = request_id_var.get()
|
|
return record
|
|
|
|
logging.setLogRecordFactory(factory)
|