mirror of
https://github.com/rookiestar28/ComfyUI-OpenClaw.git
synced 2026-08-14 08:52:45 +00:00
129 lines
4.1 KiB
Python
129 lines
4.1 KiB
Python
"""
|
|
Provider Error Types (R14/R37).
|
|
|
|
Structured exceptions for provider HTTP failures with retry-after propagation.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any, Dict, Optional
|
|
|
|
|
|
class ProviderHTTPError(RuntimeError):
|
|
"""
|
|
Exception for provider HTTP failures.
|
|
|
|
Carries structured error information including retry-after hints
|
|
from upstream providers.
|
|
|
|
Attributes:
|
|
status_code: HTTP status code (e.g., 429, 503)
|
|
retry_after: Recommended retry delay in seconds (parsed from headers/body)
|
|
provider: Provider name (for logging/metrics)
|
|
model: Model name (optional, for specific model failures)
|
|
message: Human-readable error message
|
|
headers: Redacted response headers (optional, for debugging)
|
|
body: Redacted response body (optional, for debugging)
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
status_code: int,
|
|
message: str,
|
|
provider: str,
|
|
retry_after: Optional[int] = None,
|
|
model: Optional[str] = None,
|
|
headers: Optional[Dict[str, str]] = None,
|
|
body: Optional[Any] = None,
|
|
):
|
|
"""
|
|
Initialize ProviderHTTPError.
|
|
|
|
Args:
|
|
status_code: HTTP status code
|
|
message: Error message
|
|
provider: Provider name
|
|
retry_after: Retry delay in seconds (clamped 1-3600)
|
|
model: Model name (optional)
|
|
headers: Response headers (will be redacted)
|
|
body: Response body (will be redacted/truncated)
|
|
"""
|
|
self.status_code = status_code
|
|
self.retry_after = retry_after
|
|
self.provider = provider
|
|
self.model = model
|
|
self.message = message
|
|
self.headers = self._redact_headers(headers) if headers else None
|
|
self.body = self._redact_body(body) if body else None
|
|
|
|
# Format error message
|
|
parts = [f"Provider {provider} returned {status_code}: {message}"]
|
|
if model:
|
|
parts.append(f"(model={model})")
|
|
if retry_after:
|
|
parts.append(f"(retry_after={retry_after}s)")
|
|
|
|
super().__init__(" ".join(parts))
|
|
|
|
def _redact_headers(self, headers: Dict[str, str]) -> Dict[str, str]:
|
|
"""Redact sensitive headers (API keys, auth tokens)."""
|
|
redacted = {}
|
|
sensitive_keys = {
|
|
"authorization",
|
|
"x-api-key",
|
|
"api-key",
|
|
"apikey",
|
|
"cookie",
|
|
"set-cookie",
|
|
"x-auth-token",
|
|
"bearer",
|
|
}
|
|
|
|
for key, value in headers.items():
|
|
key_lower = key.lower()
|
|
if (
|
|
key_lower in sensitive_keys
|
|
or "token" in key_lower
|
|
or "key" in key_lower
|
|
):
|
|
redacted[key] = "[REDACTED]"
|
|
else:
|
|
# Keep useful headers (retry, rate-limit, content-type)
|
|
if key_lower.startswith(("retry-", "x-ratelimit-", "content-")):
|
|
redacted[key] = value
|
|
|
|
return redacted
|
|
|
|
def _redact_body(self, body: Any) -> Any:
|
|
"""Redact/truncate response body."""
|
|
# If dict, redact known secret fields
|
|
if isinstance(body, dict):
|
|
redacted = {}
|
|
sensitive_fields = {"api_key", "apiKey", "token", "secret", "password"}
|
|
|
|
for key, value in body.items():
|
|
if any(s in key.lower() for s in sensitive_fields):
|
|
redacted[key] = "[REDACTED]"
|
|
else:
|
|
redacted[key] = value
|
|
|
|
return redacted
|
|
|
|
# If string, truncate to 500 chars
|
|
if isinstance(body, str):
|
|
return body[:500] + ("..." if len(body) > 500 else "")
|
|
|
|
return body
|
|
|
|
def is_rate_limit(self) -> bool:
|
|
"""Check if this is a rate-limit error (429)."""
|
|
return self.status_code == 429
|
|
|
|
def is_capacity_error(self) -> bool:
|
|
"""Check if this is a capacity/overload error (503, 529)."""
|
|
return self.status_code in (503, 529)
|
|
|
|
def is_retriable(self) -> bool:
|
|
"""Check if this error is retriable (rate-limit or capacity)."""
|
|
return self.is_rate_limit() or self.is_capacity_error()
|