Files
2026-07-13 13:39:38 +08:00

157 lines
4.4 KiB
Python

from __future__ import annotations
class UnexpectedModelBehavior(RuntimeError):
"""Raised when the model behaves in a way the run cannot recover from,
e.g. a run with an output_type ends without the expected output after
exhausting its retries."""
class AssignmentTimeoutError(Exception):
"""Raised when accepting a job but not receiving an assignment within the specified timeout.
The server may have chosen another worker to handle this job."""
pass
# errors used by our plugins
class APIError(Exception):
"""Raised when an API request failed.
This is used on our TTS/STT/LLM plugins."""
message: str
"""
The error message returned by the API.
"""
body: object | None
"""The API response body, if available.
If the API returned a valid json, the body will contains
the decodede result.
"""
retryable: bool = False
"""Whether the error can be retried."""
def __init__(self, message: str, *, body: object | None = None, retryable: bool = True) -> None:
super().__init__(message)
self.message = message
self.body = body
self.retryable = retryable
def __str__(self) -> str:
return self.message
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.message!r}, body={self.body!r}, retryable={self.retryable!r})"
class APIStatusError(APIError):
"""Raised when an API response has a status code of 4xx or 5xx."""
status_code: int
"""The status code of the API response."""
request_id: str | None
"""The request ID of the API response, if available."""
def __init__(
self,
message: str,
*,
status_code: int = -1,
request_id: str | None = None,
body: object | None = None,
retryable: bool | None = None,
) -> None:
if retryable is None:
retryable = True
# 4xx client errors are not retryable, except for transient codes:
# 408 (Request Timeout), 429 (Too Many Requests), 499 (Client Closed Request / gRPC CANCELLED).
# This overrides any caller-provided retryable=True, since a client
# error (bad URL, auth, bad request) will keep failing on retry.
if 400 <= status_code < 500 and status_code not in (408, 429, 499):
retryable = False
super().__init__(message, body=body, retryable=retryable)
self.status_code = status_code
self.request_id = request_id
def __str__(self) -> str:
parts = [
f"message={self.message!r}",
f"status_code={self.status_code}",
f"retryable={self.retryable}",
]
if self.request_id:
parts.append(f"request_id={self.request_id}")
if self.body:
parts.append(f"body={self.body}")
return ", ".join(parts)
def __repr__(self) -> str:
return (
f"{self.__class__.__name__}({self.message!r}, "
f"status_code={self.status_code!r}, "
f"request_id={self.request_id!r}, "
f"body={self.body!r}, "
f"retryable={self.retryable!r})"
)
class APIConnectionError(APIError):
"""Raised when an API request failed due to a connection error."""
def __init__(self, message: str = "Connection error.", *, retryable: bool = True) -> None:
super().__init__(message, body=None, retryable=retryable)
class APITimeoutError(APIConnectionError):
"""Raised when an API request timed out."""
def __init__(self, message: str = "Request timed out.", *, retryable: bool = True) -> None:
super().__init__(message, retryable=retryable)
class CLIError(Exception):
pass
def create_api_error_from_http(
message: str = "",
*,
status: int,
request_id: str | None = None,
body: object | None = None,
) -> APIStatusError:
"""Create an APIStatusError from an HTTP status code.
When the message carries extra detail beyond the standard reason phrase,
both the message and the reason are shown. Otherwise just the reason.
"""
from http import HTTPStatus
try:
reason = HTTPStatus(status).phrase
except ValueError:
reason = f"HTTP {status}"
if message and message != reason:
display = f"{message} ({status} {reason})"
else:
display = f"{reason} ({status})"
return APIStatusError(
message=display,
status_code=status,
request_id=request_id,
body=body,
)