Source code for novelai_image_mcp.nai.errors
"""NovelAI error-body parsing and official error-code explanations.
Every error raised toward NovelAI's API carries the error ``code`` and —
where documented — NovelAI's official explanation text (see
``OFFICIAL_CODE_EXPLANATIONS``). Codes sent inside MessagePack stream errors by
the API itself (``{code, message}`` events) are passed through verbatim.
"""
from __future__ import annotations
from dataclasses import dataclass
import json
from typing import Any
[docs]
@dataclass(frozen=True, slots=True)
class ErrorInfo:
"""Normalized error payload extracted from a NovelAI error body."""
code: int | str | None
message: str
raw: str
#: Official error codes documented by NovelAI's API docs
#: (https://api.novelai.net/docs) and the official FAQ. 5xx are deliberately
#: folded into the generic provider-error bucket rather than enumerated.
OFFICIAL_CODE_EXPLANATIONS: dict[int, str] = {
400: "invalid request (validation failed)",
401: "authentication failed (missing, invalid, or expired credentials)",
402: "active subscription or Anlas required",
409: "request conflicted with server state",
429: "rate limit or concurrency limit reached",
}
[docs]
def explain(code: int | str | None) -> str | None:
"""Return NovelAI's official explanation for a documented error code.
Undocumented codes (including API-supplied MessagePack stream codes)
return ``None`` — the API's own message is then the best explanation.
"""
if isinstance(code, int) and code in OFFICIAL_CODE_EXPLANATIONS:
return OFFICIAL_CODE_EXPLANATIONS[code]
return None
[docs]
def parse_error_body(content: bytes) -> ErrorInfo:
"""Extract ``(code, message)`` from a NovelAI error body.
NovelAI returns JSON in several shapes — ``{"statusCode", "message"}``,
``{"code", "message"}``, ``{"error": ...}``, or a bare ``{"message"}`` —
or, on some legacy paths, raw text. Falls back to the decoded text when
neither a code nor a message field is present.
"""
raw = content[:4_096].decode("utf-8", errors="replace")
try:
value: Any = json.loads(raw)
except (json.JSONDecodeError, UnicodeDecodeError):
return ErrorInfo(code=None, message=raw, raw=raw)
if not isinstance(value, dict):
return ErrorInfo(code=None, message=raw, raw=raw)
code: int | str | None = value.get("statusCode", value.get("code"))
if not isinstance(code, (int, str)):
code = None
message = value.get("message") or value.get("error") or value.get("detail")
if isinstance(message, str) and message.strip():
return ErrorInfo(code=code, message=message.strip(), raw=raw)
return ErrorInfo(code=code, message=raw, raw=raw)
__all__ = [
"OFFICIAL_CODE_EXPLANATIONS",
"ErrorInfo",
"explain",
"parse_error_body",
]