diff --git a/dashscope/agentstudio/exceptions.py b/dashscope/agentstudio/exceptions.py index 124ab5e..b3c1a88 100644 --- a/dashscope/agentstudio/exceptions.py +++ b/dashscope/agentstudio/exceptions.py @@ -14,13 +14,31 @@ instead of nested ``error.{code,message}``. We accept both shapes and normalize to the documented form. The compatibility branch is marked with ``# TODO(bma-fix)`` so we can remove it once the backend aligns. + +Codes and default messages come from :mod:`dashscope.common.error_registry` +(the single source of truth). :func:`from_response` normalizes to that +taxonomy — rewriting legacy aliases and falling back to the per-status code +— so ``.code`` is always unified; the server's raw payload stays on ``.raw``. """ from __future__ import annotations +import re from typing import Any, Dict, Mapping, Optional +from dashscope.common import error_registry as _error_registry from dashscope.common.error import DashScopeException +from dashscope.common.error_registry import ( + AUTH_FAILED, + INTERNAL_ERROR, + INVALID_REQUEST, + PERMISSION_DENIED, + PublicError, + RATE_LIMIT_EXCEEDED, + REQUEST_TIMEOUT, + RESOURCE_NOT_FOUND, + SERVICE_UNAVAILABLE, +) class AgentStudioError(DashScopeException): @@ -104,7 +122,9 @@ class AuthenticationError(APIStatusError): class PermissionDeniedError(APIStatusError): - code = "permission_denied_error" + # Unified external standard (registry anthropic_error_code / the + # canonical Anthropic API error type). + code = "permission_error" class NotFoundError(APIStatusError): @@ -162,10 +182,26 @@ class StreamClosedError(StreamError): 504: InternalServerError, } +# Registry row per status, for the default code/message when the server +# omits one. 409 has no row and falls back to a bare HTTP status. +_STATUS_TO_PUBLIC: Dict[int, PublicError] = { + 400: INVALID_REQUEST, + 401: AUTH_FAILED, + 403: PERMISSION_DENIED, + 404: RESOURCE_NOT_FOUND, + 429: RATE_LIMIT_EXCEEDED, + 500: INTERNAL_ERROR, + 502: INTERNAL_ERROR, + 503: SERVICE_UNAVAILABLE, + 504: REQUEST_TIMEOUT, +} + +# Class routing: normalized code -> exception type. Legacy aliases are +# rewritten to their canonical form by ``_normalize_code`` before routing. _CODE_TO_CLASS: Dict[str, type] = { "invalid_request_error": InvalidRequestError, "authentication_error": AuthenticationError, - "permission_denied_error": PermissionDeniedError, + "permission_error": PermissionDeniedError, "not_found_error": NotFoundError, "conflict_error": ConflictError, "rate_limit_error": RateLimitError, @@ -173,6 +209,54 @@ class StreamClosedError(StreamError): "api_error": InternalServerError, } +# Codes the registry defines; a server code already in this set is kept +# as-is (e.g. ``billing_error`` stays distinct from ``rate_limit_error``). +_REGISTRY_CODES = frozenset( + pe.anthropic_error_code + for pe in vars(_error_registry).values() + if isinstance(pe, PublicError) +) + +# Pre-standard codes the backend has emitted historically, mapped to their +# normalized form so old exceptions still resolve to a unified code. +_LEGACY_CODE_ALIASES: Dict[str, str] = { + "permission_denied_error": "permission_error", # TODO(bma-fix) +} + +_PLACEHOLDER_RE = re.compile(r"\s*:?\s*\{[^}]+\}") + + +def _normalize_code(code: Optional[str]) -> Optional[str]: + """Map a server-supplied code to the unified taxonomy. + + Returns the canonical code for a known alias, the code itself when it is + already a registry code, or ``None`` when the SDK does not recognize it + (the caller then falls back to the registry's per-status code). + """ + + if code is None: + return None + if code in _LEGACY_CODE_ALIASES: + return _LEGACY_CODE_ALIASES[code] + if code in _REGISTRY_CODES: + return code + return None + + +def _default_message(public: PublicError) -> str: + """Placeholder-free default message from a registry entry. + + Registry ``error_msg`` values may embed ``{var}`` templates the server + fills in (e.g. ``"...not found: {resource}."``). When falling back to a + default we have no values, so strip the unresolved placeholders for a + clean sentence. + """ + + msg = _PLACEHOLDER_RE.sub("", public.error_msg).strip() + if msg and not msg.endswith("."): + msg += "." + return msg + def from_response( *, @@ -222,14 +306,24 @@ def from_response( if message is None: message = body.get("message") - if message is None: - message = f"HTTP {status_code}" - if code is None: + public = _STATUS_TO_PUBLIC.get(status_code) + + # Normalized SDK code wins; unknown/omitted falls back to the status row. + normalized = _normalize_code(code) + if normalized is not None: + code = normalized + elif public is not None: + code = public.anthropic_error_code + else: code = _STATUS_TO_DEFAULT.get( # type: ignore[attr-defined] status_code, APIStatusError, ).code + if message is None: + # Registry default (placeholder-free); else a bare HTTP status. + message = _default_message(public) if public else f"HTTP {status_code}" + cls = ( _CODE_TO_CLASS.get(code) or _STATUS_TO_DEFAULT.get(status_code) diff --git a/dashscope/api_entities/aiohttp_request.py b/dashscope/api_entities/aiohttp_request.py index d220ffb..97d8d32 100644 --- a/dashscope/api_entities/aiohttp_request.py +++ b/dashscope/api_entities/aiohttp_request.py @@ -3,7 +3,7 @@ import json from http import HTTPStatus -from typing import Optional +from typing import Optional, Dict import aiohttp @@ -17,7 +17,11 @@ ) from dashscope.common.error import UnsupportedHTTPMethod from dashscope.common.logging import logger -from dashscope.common.utils import async_to_sync +from dashscope.common.error_registry import INTERNAL_ERROR +from dashscope.common.utils import ( + async_to_sync, + _handle_aiohttp_failed_response, +) class AioHttpRequest(AioBaseRequest): @@ -85,10 +89,10 @@ def __init__( else: self.timeout = timeout # type: ignore[has-type] - def add_header(self, key, value): + def add_header(self, key: str, value: str) -> None: self.headers[key] = value - def add_headers(self, headers): + def add_headers(self, headers: Dict[str, str]) -> None: self.headers = {**self.headers, **headers} def call(self): @@ -97,9 +101,8 @@ def call(self): return (item for item in response) else: output = next(response) - try: - next(response) - except StopIteration: + # Consume remaining items to ensure generator completes + for _ in response: pass return output @@ -109,9 +112,8 @@ async def aio_call(self): return (item async for item in response) else: result = await response.__anext__() - try: - await response.__anext__() - except StopAsyncIteration: + # Consume remaining items to ensure generator completes + async for _ in response: pass return result @@ -142,6 +144,7 @@ async def _handle_response( # pylint: disable=too-many-branches response: aiohttp.ClientResponse, ): request_id = "" + headers = dict(response.headers) if ( response.status == HTTPStatus.OK and self.stream @@ -162,19 +165,29 @@ async def _handle_response( # pylint: disable=too-many-branches if "request_id" in msg: request_id = msg["request_id"] except json.JSONDecodeError: + logger.error( + "Failed to parse SSE stream data: %s", + data[:200] if len(data) > 200 else data, + ) yield DashScopeAPIResponse( request_id=request_id, - status_code=HTTPStatus.INTERNAL_SERVER_ERROR, - code="Unknown", - message=data, + status_code=INTERNAL_ERROR.status_code, + code=INTERNAL_ERROR.error_code, + message=INTERNAL_ERROR.error_msg, + headers=headers, ) continue - if is_error: + if is_error and msg is not None: yield DashScopeAPIResponse( request_id=request_id, status_code=status_code, - code=msg["code"], - message=msg["message"], + code=msg.get("code") + or msg.get("error_code") + or f"http_{status_code}", + message=msg.get("message") + or msg.get("error_message") + or f"HTTP {status_code} error", + headers=headers, ) else: yield DashScopeAPIResponse( @@ -182,6 +195,7 @@ async def _handle_response( # pylint: disable=too-many-branches status_code=HTTPStatus.OK, output=output, usage=usage, + headers=headers, ) elif ( response.status == HTTPStatus.OK @@ -201,6 +215,7 @@ async def _handle_response( # pylint: disable=too-many-branches request_id=request_id, status_code=HTTPStatus.OK, output=output, + headers=headers, ) elif response.status == HTTPStatus.OK: json_content = await response.json() @@ -217,51 +232,22 @@ async def _handle_response( # pylint: disable=too-many-branches status_code=HTTPStatus.OK, output=output, usage=usage, + headers=headers, ) else: - if "application/json" in response.content_type: - error = await response.json() - if "request_id" in error: - request_id = error["request_id"] - if "message" not in error: - message = "" - logger.error( - "Request: %s failed, status: %s", - self.url, - response.status, - ) - else: - message = error["message"] - logger.error( - "Request: %s failed, status: %s, message: %s", - self.url, - response.status, - error["message"], - ) - yield DashScopeAPIResponse( - request_id=request_id, - status_code=response.status, - code=error["code"], - message=message, - ) - else: - msg = await response.read() - yield DashScopeAPIResponse( - request_id=request_id, - status_code=response.status, - code="Unknown", - message=msg.decode("utf-8"), - ) + yield _handle_aiohttp_failed_response(response) # pylint: disable=too-many-branches async def _handle_request(self): try: + # Session management: + # - External session: managed by caller, we never close it + # - Shared session: managed by get_shared_aio_session(), + # uses connection pooling and is closed when no longer needed if self._external_aio_session is not None: session = self._external_aio_session - should_close = False else: session = await get_shared_aio_session() - should_close = False if self.stream: request_timeout = aiohttp.ClientTimeout( @@ -315,8 +301,21 @@ async def _handle_request(self): async for rsp in self._handle_response(response): yield rsp finally: - if should_close: - await session.close() + # Note: We don't close the session here because: + # - External sessions are managed by the caller + # - Shared sessions use connection pooling and are + # managed centrally by get_shared_aio_session() + pass except Exception as e: - logger.debug(e) - raise e + logger.error( + "Request failed: url=%s, method=%s, error=%s", + self.url, + self.method, + str(e), + exc_info=True, + ) + from dashscope.common.error import DashScopeException + + raise DashScopeException( + f"Request failed: {str(e)}", + ) from e diff --git a/dashscope/api_entities/api_request_factory.py b/dashscope/api_entities/api_request_factory.py index c231287..a9da3b9 100644 --- a/dashscope/api_entities/api_request_factory.py +++ b/dashscope/api_entities/api_request_factory.py @@ -69,6 +69,68 @@ def _get_protocol_params(kwargs): ) +def _build_http_url( + base_address: str, + is_service: bool, + task_group: str, + task: str, + function: str, + extra_url_parameters: dict, +) -> str: + """Build HTTP/HTTPS URL from components with validation.""" + if base_address is None: + base_address = dashscope.base_http_api_url + + # Validate base_address has proper scheme (http:// or https://) + if base_address and not base_address.startswith( + ("http://", "https://"), + ): + from dashscope.common.error import InvalidBaseURL + + raise InvalidBaseURL( + f"Invalid URL '{base_address}': No scheme supplied. " + f"Perhaps you meant https://{base_address}?", + ) + + if not base_address.endswith("/"): + http_url = base_address + "/" + else: + http_url = base_address + + if is_service: + http_url = http_url + SERVICE_API_PATH + "/" + + if task_group: + http_url += f"{task_group}/" + if task: + http_url += f"{task}/" + if function: + http_url += function + if extra_url_parameters is not None and extra_url_parameters: + http_url += "?" + urlencode(extra_url_parameters) + + return http_url + + +def _build_websocket_url(base_address: str) -> str: + """Build and validate WebSocket URL.""" + if base_address is not None: + websocket_url = base_address + else: + websocket_url = dashscope.base_websocket_api_url + + # Validate websocket_url has proper scheme (ws:// or wss://) + if websocket_url and not websocket_url.startswith(("ws://", "wss://")): + from dashscope.common.error import InvalidBaseURL + + raise InvalidBaseURL( + f"Invalid URL '{websocket_url}': No scheme supplied. " + f"Perhaps you meant wss://{websocket_url}?", + ) + + return websocket_url + + def _build_api_request( # pylint: disable=too-many-branches model: str, input: object, # pylint: disable=redefined-builtin @@ -102,24 +164,14 @@ def _build_api_request( # pylint: disable=too-many-branches encryption = None if api_protocol in [ApiProtocol.HTTP, ApiProtocol.HTTPS]: - if base_address is None: - base_address = dashscope.base_http_api_url - if not base_address.endswith("/"): - http_url = base_address + "/" - else: - http_url = base_address - - if is_service: - http_url = http_url + SERVICE_API_PATH + "/" - - if task_group: - http_url += f"{task_group}/" - if task: - http_url += f"{task}/" - if function: - http_url += function - if extra_url_parameters is not None and extra_url_parameters: - http_url += "?" + urlencode(extra_url_parameters) + http_url = _build_http_url( + base_address, + is_service, + task_group, + task, + function, + extra_url_parameters, + ) if enable_encryption is True: encryption = Encryption() @@ -142,10 +194,7 @@ def _build_api_request( # pylint: disable=too-many-branches session=session, ) elif api_protocol == ApiProtocol.WEBSOCKET: - if base_address is not None: - websocket_url = base_address - else: - websocket_url = dashscope.base_websocket_api_url + websocket_url = _build_websocket_url(base_address) pre_task_id = kwargs.pop("pre_task_id", None) request = WebSocketRequest( url=websocket_url, diff --git a/dashscope/api_entities/http_request.py b/dashscope/api_entities/http_request.py index 285c6ab..72366fd 100644 --- a/dashscope/api_entities/http_request.py +++ b/dashscope/api_entities/http_request.py @@ -17,6 +17,7 @@ HTTPMethod, ) from dashscope.common.error import UnsupportedHTTPMethod +from dashscope.common.error_registry import MISSING_PARAMETER, INVALID_REQUEST from dashscope.common.logging import logger from dashscope.common.utils import ( _handle_aio_stream, @@ -130,10 +131,10 @@ def __init__( else: self.timeout = timeout # type: ignore[has-type] - def add_header(self, key, value): + def add_header(self, key: str, value: str) -> None: self.headers[key] = value - def add_headers(self, headers): + def add_headers(self, headers: Dict[str, str]) -> None: self.headers = {**self.headers, **headers} def call(self): @@ -142,9 +143,8 @@ def call(self): return (item for item in response) else: output = next(response) - try: - next(response) - except StopIteration: + # Consume remaining items to ensure generator completes + for _ in response: pass return output @@ -154,9 +154,8 @@ async def aio_call(self): return (item async for item in response) else: result = await response.__anext__() - try: - await response.__anext__() - except StopAsyncIteration: + # Consume remaining items to ensure generator completes + async for _ in response: pass return result @@ -228,8 +227,10 @@ async def _handle_aio_request(self): # pylint: disable=too-many-branches if should_close: await session.close() except Exception as e: - logger.debug(e) - raise e + logger.error(f"Async request failed: {e}", exc_info=True) + from dashscope.common.error import DashScopeException + + raise DashScopeException(str(e)) from e @staticmethod def __handle_parameters(params: dict) -> dict: @@ -290,9 +291,11 @@ async def _handle_aio_response( # pylint: disable=too-many-branches, too-many-s except json.JSONDecodeError: yield DashScopeAPIResponse( request_id=request_id, - status_code=HTTPStatus.INTERNAL_SERVER_ERROR, - code="Unknown", - message=data, + status_code=MISSING_PARAMETER.status_code, + code=MISSING_PARAMETER.error_code, + message=MISSING_PARAMETER.format_msg( + {"parameter": "response data"}, + ), headers=headers, ) continue @@ -300,8 +303,12 @@ async def _handle_aio_response( # pylint: disable=too-many-branches, too-many-s yield DashScopeAPIResponse( request_id=request_id, status_code=status_code, - code=msg["code"], - message=msg["message"], + code=msg.get("code") + or msg.get("error_code") + or f"http_{status_code}", + message=msg.get("message") + or msg.get("error_message") + or f"HTTP {status_code} error", headers=headers, ) else: @@ -399,10 +406,10 @@ def _handle_response( # pylint: disable=too-many-branches except json.JSONDecodeError: yield DashScopeAPIResponse( request_id=request_id, - status_code=HTTPStatus.BAD_REQUEST, + status_code=INVALID_REQUEST.status_code, output=None, - code="Unknown", - message=data, + code=INVALID_REQUEST.error_code, + message=INVALID_REQUEST.error_msg, headers=headers, ) continue @@ -411,10 +418,12 @@ def _handle_response( # pylint: disable=too-many-branches request_id=request_id, status_code=status_code, output=None, - code=msg["code"] - if "code" in msg - else None, # noqa E501 - message=msg["message"] if "message" in msg else None, + code=msg.get("code") + or msg.get("error_code") + or f"http_{status_code}", + message=msg.get("message") + or msg.get("error_message") + or f"HTTP {status_code} error", headers=headers, ) # noqa E501 else: @@ -517,5 +526,7 @@ def _handle_request(self): # pylint: disable=too-many-branches if should_close: session.close() except Exception as e: - logger.debug(e) - raise e + logger.error(f"Sync request failed: {e}", exc_info=True) + from dashscope.common.error import DashScopeException + + raise DashScopeException(str(e)) from e diff --git a/dashscope/api_entities/websocket_request.py b/dashscope/api_entities/websocket_request.py index a3ceb97..0f0a704 100644 --- a/dashscope/api_entities/websocket_request.py +++ b/dashscope/api_entities/websocket_request.py @@ -5,7 +5,7 @@ import json import uuid from http import HTTPStatus -from typing import Tuple, Union +from typing import Tuple, Union, Dict import aiohttp @@ -13,7 +13,6 @@ from dashscope.api_entities.dashscope_response import DashScopeAPIResponse from dashscope.common.constants import ( DEFAULT_REQUEST_TIMEOUT_SECONDS, - SERVICE_503_MESSAGE, WEBSOCKET_ERROR_CODE, ) from dashscope.common.error import ( @@ -21,6 +20,11 @@ UnexpectedMessageReceived, UnknownMessageReceived, ) +from dashscope.common.error_registry import ( + SERVICE_UNAVAILABLE, + AUTH_FAILED, + INTERNAL_ERROR, +) from dashscope.common.logging import logger from dashscope.common.utils import async_to_sync from dashscope.protocol.websocket import ( @@ -81,7 +85,7 @@ def __init__( } self.pre_task_id = pre_task_id - def add_headers(self, headers): + def add_headers(self, headers: Dict[str, str]) -> None: self.headers = {**self.headers, **headers} def call(self): @@ -112,7 +116,9 @@ async def aio_call(self): pass return result - async def connection_handler(self): # pylint: disable=too-many-branches + async def connection_handler( + self, + ): # pylint: disable=too-many-branches,too-many-statements try: task_id = None async with aiohttp.ClientSession( @@ -195,34 +201,67 @@ async def connection_handler(self): # pylint: disable=too-many-branches ) except aiohttp.ClientConnectorError as e: logger.exception(e) + # Check if the error message indicates service unavailability + error_str = str(e).lower() + if "service unavailable" in error_str or "503" in error_str: + yield DashScopeAPIResponse( + request_id=task_id if task_id else "", + status_code=SERVICE_UNAVAILABLE.status_code, + code=SERVICE_UNAVAILABLE.error_code, + message=SERVICE_UNAVAILABLE.error_msg, + ) + return + yield DashScopeAPIResponse( - request_id="", - status_code=-1, - code="ClientConnectorError", - message=str(e), + request_id=task_id if task_id else "", + status_code=INTERNAL_ERROR.status_code, + code=INTERNAL_ERROR.error_code, + message=INTERNAL_ERROR.error_msg, ) except aiohttp.WSServerHandshakeError as e: - code = e.status - msg = e.message + original_msg = e.message or "" + if e.status in [HTTPStatus.FORBIDDEN, HTTPStatus.UNAUTHORIZED]: - msg = "Unauthorized, your api-key is invalid!" + yield DashScopeAPIResponse( + request_id=task_id if task_id else "", + status_code=AUTH_FAILED.status_code, + code=AUTH_FAILED.error_code, + message=AUTH_FAILED.error_msg, + ) elif e.status == HTTPStatus.SERVICE_UNAVAILABLE: - msg = SERVICE_503_MESSAGE + yield DashScopeAPIResponse( + request_id=task_id if task_id else "", + status_code=SERVICE_UNAVAILABLE.status_code, + code=SERVICE_UNAVAILABLE.error_code, + message=SERVICE_UNAVAILABLE.error_msg, + ) else: - pass - yield DashScopeAPIResponse( - request_id=task_id, - status_code=code, - code=code, - message=msg, - ) - except BaseException as e: + if e.status == HTTPStatus.INTERNAL_SERVER_ERROR: + yield DashScopeAPIResponse( + request_id=task_id if task_id else "", + status_code=INTERNAL_ERROR.status_code, + code=INTERNAL_ERROR.error_code, + message=INTERNAL_ERROR.error_msg, + ) + else: + # Log unexpected status codes for debugging + logger.warning( + "WebSocket handshake failed with unexpected " + "status %s: %s", + e.status, + original_msg, + ) + raise e + except Exception as e: logger.exception(e) yield DashScopeAPIResponse( - request_id="", - status_code=-1, - code="Unknown", - message=f"Error type: {type(e)}, message: {e}", + request_id=task_id if task_id else "", + status_code=INTERNAL_ERROR.status_code, + code=INTERNAL_ERROR.error_code, + message=( + f"{INTERNAL_ERROR.error_msg} " + f"(SDK Internal Error: {type(e).__name__}: {e})" + ), ) def _to_DashScopeAPIResponse(self, task_id, is_binary, result): diff --git a/dashscope/audio/qwen_tts_realtime/qwen_tts_realtime.py b/dashscope/audio/qwen_tts_realtime/qwen_tts_realtime.py index 3d5c374..2a7e077 100644 --- a/dashscope/audio/qwen_tts_realtime/qwen_tts_realtime.py +++ b/dashscope/audio/qwen_tts_realtime/qwen_tts_realtime.py @@ -10,7 +10,7 @@ import websocket # pylint: disable=wrong-import-order import dashscope -from dashscope.common.error import ModelRequired +from dashscope.common.error import AuthenticationError, ModelRequired from dashscope.common.logging import logger from dashscope.common.utils import get_user_agent @@ -105,6 +105,8 @@ def __init__( self.last_first_text_time = None self.last_first_audio_delay = None self.metrics = [] + self._auth_failed = False # Flag to track authentication failures + self._connection_error = None # Store connection error details def _generate_event_id(self): """ @@ -131,6 +133,10 @@ def connect(self) -> None: """ connect to server, create session and return default session configuration # noqa: E501 """ + # Reset error flags before connecting + self._auth_failed = False + self._connection_error = None + self.ws = websocket.WebSocketApp( self.url, header=self._get_websocket_header(), @@ -147,7 +153,16 @@ def connect(self) -> None: not (self.ws.sock and self.ws.sock.connected) and (time.time() - start_time) < timeout ): + # Check for authentication errors first + if self._auth_failed and self._connection_error is not None: + raise self._connection_error + time.sleep(0.1) # Brief sleep to avoid busy polling + + # Final check after timeout + if self._auth_failed and self._connection_error is not None: + raise self._connection_error + if not (self.ws.sock and self.ws.sock.connected): raise TimeoutError( "websocket connection could not established within 5s. " @@ -415,6 +430,20 @@ def on_close( # pylint: disable=unused-argument # Callback for WebSocket error def on_error(self, ws, error): # pylint: disable=unused-argument logger.error(f"websocket error: {error}") + + # Detect authentication errors from error message + error_str = str(error) + if ( + "401" in error_str + or "Unauthorized" in error_str + or "InvalidApiKey" in error_str + ): + self._auth_failed = True + self._connection_error = AuthenticationError( + "API Key is invalid. Please check your API key configuration.", + ) + logger.error("Authentication failed: Invalid API Key") + # Do not raise exception here, let the connection close naturally # Raising exception in callback can cause unexpected thread termination diff --git a/dashscope/cli/agentic_rl.py b/dashscope/cli/agentic_rl.py index cd8386c..65376c8 100644 --- a/dashscope/cli/agentic_rl.py +++ b/dashscope/cli/agentic_rl.py @@ -24,7 +24,11 @@ ) from dashscope.finetune.reinforcement.common.errors import OutputError from dashscope.finetune.customize_types import FineTune -from dashscope.cli.common import error, normalize_local_path_or_url +from dashscope.cli.common import ( + error, + normalize_local_path_or_url, + _handle_exception, +) app = typer.Typer( @@ -215,9 +219,8 @@ async def _register_fc_async( } except Exception as e: - root = _root_cause(e) - err_console.print(f"[red]❌ FC registration failed: {root}[/red]") - raise typer.Exit(1) + _handle_exception(e, "FC registration failed", err_console) + raise # pragma: no cover @app.command("register-functions", hidden=True) @@ -292,9 +295,8 @@ async def _test_fc_async( return result except Exception as e: - root = _root_cause(e) - err_console.print(f"[red]❌ Function test failed: {root}[/red]") - raise typer.Exit(1) + _handle_exception(e, "Function test failed", err_console) + raise # pragma: no cover @app.command("test-functions", hidden=True) @@ -370,9 +372,7 @@ async def _upload_data_async( } except Exception as e: - root = _root_cause(e) - err_console.print(f"[red]❌ Dataset upload failed: {root}[/red]") - raise typer.Exit(1) + _handle_exception(e, "Dataset upload failed", err_console) @app.command("upload-data", hidden=True) @@ -536,8 +536,16 @@ def run( # Handle API response errors if result.status_code != 200: raise OutputError( - f"API returned status {result.status_code}:" - f" {result.message}", + ( + f"API error [status={result.status_code}, " + f"code={result.code}]: {result.message}" + ), + response={ + "status_code": result.status_code, + "code": result.code, + "message": result.message, + "request_id": result.request_id, + }, ) progress.update( @@ -559,24 +567,22 @@ def run( ) except Exception as e: - root = _root_cause(e) label = ( "Validation error" if isinstance(e, ValueError) else "Workflow execution failed" ) - err_console.print(f"[red]❌ {label}: {root}[/red]") + _handle_exception(e, label, err_console) if _cli_verbose: err_console.print( "".join( tb_module.format_exception( - type(root), - root, - root.__traceback__, + type(e), + e, + e.__traceback__, ), ), ) - raise typer.Exit(1) @app.command() @@ -598,7 +604,16 @@ def get( # Handle API response errors if result.status_code != 200: raise OutputError( - f"API returned status {result.status_code}: {result.message}", + ( + f"API error [status={result.status_code}, " + f"code={result.code}]: {result.message}" + ), + response={ + "status_code": result.status_code, + "code": result.code, + "message": result.message, + "request_id": result.request_id, + }, ) # Validate output is not None before accessing attributes @@ -614,9 +629,7 @@ def get( fmt=output_format, ) except Exception as e: - root = _root_cause(e) - err_console.print(f"[red]❌ Query failed: {root}[/red]") - raise typer.Exit(1) + _handle_exception(e, "Query failed", err_console) @app.command() @@ -636,16 +649,23 @@ def cancel( # Handle API response errors if result.status_code != 200: raise OutputError( - f"API returned status {result.status_code}: {result.message}", + ( + f"API error [status={result.status_code}, " + f"code={result.code}]: {result.message}" + ), + response={ + "status_code": result.status_code, + "code": result.code, + "message": result.message, + "request_id": result.request_id, + }, ) err_console.print( f"[green]✅ Job {job_id} canceled successfully[/green]", ) except Exception as e: - root = _root_cause(e) - err_console.print(f"[red]❌ Cancellation failed: {root}[/red]") - raise typer.Exit(1) + _handle_exception(e, "Cancellation failed", err_console) @app.command() @@ -674,22 +694,32 @@ def logs( # Handle API response errors if result.status_code != 200: raise OutputError( - f"API returned status {result.status_code}: {result.message}", + ( + f"API error [status={result.status_code}, " + f"code={result.code}]: {result.message}" + ), + response={ + "status_code": result.status_code, + "code": result.code, + "message": result.message, + "request_id": result.request_id, + }, ) # Validate output is not None before accessing attributes logs_data = "" - if result.output is not None and isinstance(result.output, dict): - logs_data = result.output.get("logs", "") + if result.output is not None: + if isinstance(result.output, dict): + logs_data = result.output.get("logs", "") + else: + logs_data = getattr(result.output, "logs", "") format_output( {"job_id": job_id, "logs": logs_data}, fmt=output_format, ) except Exception as e: - root = _root_cause(e) - err_console.print(f"[red]❌ Log retrieval failed: {root}[/red]") - raise typer.Exit(1) + _handle_exception(e, "Log retrieval failed", err_console) @app.command("list") @@ -716,7 +746,16 @@ def list_jobs( # Handle API response errors if result.status_code != 200: raise OutputError( - f"API returned status {result.status_code}: {result.message}", + ( + f"API error [status={result.status_code}, " + f"code={result.code}]: {result.message}" + ), + response={ + "status_code": result.status_code, + "code": result.code, + "message": result.message, + "request_id": result.request_id, + }, ) output_data = serialize_for_output( @@ -724,9 +763,7 @@ def list_jobs( ) format_output(output_data, fmt=output_format) except Exception as e: - root = _root_cause(e) - err_console.print(f"[red]❌ List query failed: {root}[/red]") - raise typer.Exit(1) + _handle_exception(e, "List query failed", err_console) # if __name__ == "__main__": diff --git a/dashscope/cli/common.py b/dashscope/cli/common.py index ba7fb3f..ae9719c 100644 --- a/dashscope/cli/common.py +++ b/dashscope/cli/common.py @@ -4,12 +4,14 @@ import os from functools import wraps from http import HTTPStatus -from typing import Callable, NoReturn, TypeVar +from typing import Callable, Dict, NoReturn, TypeVar from urllib.parse import urlparse import typer from rich.console import Console +from dashscope.common.error import DashScopeException + logger = logging.getLogger("dashscope.cli") CommandFunction = TypeVar("CommandFunction", bound=Callable) @@ -21,6 +23,123 @@ DEFAULT_PAGE_SIZE = 10 DEFAULT_START_PAGE = 1 +# --------------------------------------------------------------------------- +# Error code mapping: SDK error codes -> CLI-friendly error codes +# --------------------------------------------------------------------------- +ERROR_CODE_MAPPING: Dict[str, str] = { + # Authentication errors - keep original camelCase format + "AuthenticationError": "AuthenticationError", + "AuthFailed": "AuthFailed", + "InvalidToken": "InvalidToken", + "TokenExpired": "TokenExpired", + "Unauthorized": "Unauthorized", + # Parameter errors - keep original camelCase format + "InvalidParameter": "InvalidParameter", + "InvalidParam": "InvalidParam", + "BadRequest": "BadRequest", + "ModelRequired": "ModelRequired", + "InvalidModel": "InvalidModel", + "InvalidInput": "InvalidInput", + "InvalidFileFormat": "InvalidFileFormat", + "InputDataRequired": "InputDataRequired", + "InputRequired": "InputRequired", + "UnsupportedDataType": "UnsupportedDataType", + # Task errors - keep original camelCase format + "InvalidTask": "InvalidTask", + "UnsupportedTask": "UnsupportedTask", + "UnsupportedModel": "UnsupportedModel", + "UnsupportedApiProtocol": "UnsupportedApiProtocol", + "NotImplemented": "NotImplemented", + "MultiInputsWithBinaryNotSupported": "MultiInputsWithBinaryNotSupported", + "UnexpectedMessageReceived": "UnexpectedMessageReceived", + "UnsupportedData": "UnsupportedData", + "UnknownMessageReceived": "UnknownMessageReceived", + # Service errors - keep original camelCase format + "ServiceUnavailableError": "ServiceUnavailableError", + "UnsupportedHTTPMethod": "UnsupportedHTTPMethod", + "AsyncTaskCreateFailed": "AsyncTaskCreateFailed", + "UploadFileException": "UploadFileException", + "TimeoutException": "TimeoutException", + # Assistant errors - keep original camelCase format + "AssistantError": "AssistantError", +} + +# Error message templates with CLI context +ERROR_MESSAGE_TEMPLATES: Dict[str, str] = { + "AuthFailed": ( + "Authentication failed. " "Please check your API key and try again." + ), + "InvalidToken": ( + "Authentication failed. " "Please check your API key and try again." + ), + "TokenExpired": ( + "Authentication failed. " "Please check your API key and try again." + ), + "Unauthorized": ( + "Authentication failed. " "Please check your API key and try again." + ), + "InvalidParameter": ( + "Invalid parameter provided. " "Please check your input parameters." + ), + "InvalidParam": ( + "Invalid parameter provided. " "Please check your input parameters." + ), + "ModelRequired": ( + "Model parameter is required. " "Please specify a valid model." + ), + "InvalidModel": ( + "Invalid model specified. " "Please check the model name." + ), + "InvalidInput": ("Invalid input data. " "Please check your input format."), + "InvalidFileFormat": ( + "Invalid file format. " "Please check the file type." + ), + "InputDataRequired": ( + "Input data is required. " "Please provide the necessary input." + ), + "InputRequired": ( + "Input is required. " "Please provide the necessary input." + ), + "UnsupportedDataType": ( + "Unsupported data type. " "Please check the data format." + ), + "InvalidTask": ("Invalid task specified. " "Please check the task type."), + "UnsupportedTask": ( + "Unsupported task type. " "Please check the available tasks." + ), + "UnsupportedModel": ( + "Unsupported model. " "Please check the available models." + ), + "UnsupportedApiProtocol": ( + "Unsupported API protocol. " "Please check the protocol version." + ), + "NotImplemented": "This feature is not yet implemented.", + "MultiInputsWithBinaryNotSupported": ( + "Binary input is not supported with multiple inputs." + ), + "UnexpectedMessageReceived": ( + "Unexpected message received from the server." + ), + "UnsupportedData": "Unsupported data format.", + "UnknownMessageReceived": ("Unknown message received from the server."), + "ServiceUnavailableError": ( + "Service is temporarily unavailable. " "Please try again later." + ), + "UnsupportedHTTPMethod": ( + "Unsupported HTTP method. " "Please check the request method." + ), + "AsyncTaskCreateFailed": ( + "Failed to create async task. " "Please check your request." + ), + "UploadFileException": ( + "File upload failed. " "Please check the file and try again." + ), + "TimeoutException": "Request timed out. Please try again.", + "AssistantError": ( + "Assistant encountered an error. " "Please check the error details." + ), +} + # --------------------------------------------------------------------------- # Rich consoles # --------------------------------------------------------------------------- @@ -28,20 +147,158 @@ err_console = Console(stderr=True) # --------------------------------------------------------------------------- -# Response helpers +# Error handling utilities # --------------------------------------------------------------------------- -def print_failed_message(rsp): - """Print a standardised error message for a failed API response.""" - err_console.print( - f"[red]Failed[/red] request_id: {rsp.request_id}, " - f"status_code: {rsp.status_code}, " - f"code: {rsp.code}, message: {rsp.message}", +def _get_cli_error_code(sdk_error_code: str) -> str: + """Map SDK error code to CLI-friendly error code. + + Args: + sdk_error_code: The error code from the SDK response + + Returns: + CLI-friendly error code, or the original code if no mapping exists + """ + return ERROR_CODE_MAPPING.get(sdk_error_code, sdk_error_code) + + +def _get_cli_error_message(sdk_error_code: str, sdk_error_message: str) -> str: + """Get CLI-friendly error message with context. + + Args: + sdk_error_code: The error code from the SDK response + sdk_error_message: The error message from the SDK response + + Returns: + CLI-friendly error message + """ + cli_error_code = _get_cli_error_code(sdk_error_code) + + # Use template message if available, otherwise use SDK message + template_message = ERROR_MESSAGE_TEMPLATES.get(cli_error_code) + if template_message: + # Append SDK-specific message if available + if sdk_error_message: + return f"{sdk_error_message}" + return template_message + + # No template available, use SDK message directly + return ( + sdk_error_message + if sdk_error_message + else f"Error code: {cli_error_code}" ) -def ensure_ok(rsp, check_business_error: bool = True): +def _format_error_parts( + request_id: str, + status_code: str, + cli_error_code: str, + cli_error_message: str, + command_name: str = None, +) -> str: + """Build formatted error output parts. + + Args: + request_id: The request ID from the response + status_code: The HTTP status code + cli_error_code: The CLI-friendly error code + cli_error_message: The CLI-friendly error message + command_name: The CLI command name (optional) + + Returns: + Formatted error message string + """ + parts = [] + if command_name: + parts.append(f"[red]{command_name} failed[/red]") + else: + parts.append("[red]Request failed[/red]") + + if request_id and request_id != "N/A": + parts.append(f"request_id: {request_id}") + if status_code and status_code != "N/A": + parts.append(f"status_code: {status_code}") + parts.append(f"code: {cli_error_code}") + parts.append(f"message: {cli_error_message}") + + return ", ".join(parts) + + +# --------------------------------------------------------------------------- +# Response helpers +# --------------------------------------------------------------------------- + + +def print_failed_message(rsp, command_name: str = None): + """Print a standardised error message for a failed API response. + + Maps SDK error codes to CLI-friendly codes and enhances error messages + with CLI context. Safely handles responses with missing or None attributes. + + Args: + rsp: The API response object + command_name: The CLI command name (optional, for better context) + """ + # Use try-except to handle missing attributes gracefully (works with Mock + # objects) + try: + request_id = rsp.request_id + except AttributeError: + request_id = None + + try: + status_code = rsp.status_code + except AttributeError: + status_code = None + + try: + code = rsp.code + except AttributeError: + code = None + + try: + message = rsp.message + except AttributeError: + message = None + + # Normalize None and empty strings + request_id = request_id if request_id else "N/A" + status_code = status_code if status_code is not None else "N/A" + code = code if code else "" + message = message if message else "" + + # Use the new error formatting with CLI context + if code: + cli_error_code = _get_cli_error_code(code) + cli_error_message = _get_cli_error_message(code, message) + + formatted_error = _format_error_parts( + request_id=request_id, + status_code=status_code, + cli_error_code=cli_error_code, + cli_error_message=cli_error_message, + command_name=command_name, + ) + err_console.print(formatted_error) + else: + # Fallback for responses without error code + parts = ["[red]Failed[/red]"] + if request_id != "N/A": + parts.append(f"request_id: {request_id}") + if status_code != "N/A": + parts.append(f"status_code: {status_code}") + if message: + parts.append(f"message: {message}") + err_console.print(", ".join(parts)) + + +def ensure_ok( + rsp, + check_business_error: bool = True, + command_name: str = None, +): """Return *rsp.output* when the response is OK; otherwise print the error and exit with code 1. @@ -50,7 +307,7 @@ def ensure_ok(rsp, check_business_error: bool = True): Enhanced to check both HTTP status and business-level error codes: - HTTP 200 but InvalidParameter → still treated as failure - - HTTP 4xx/5xx → clear error message + - HTTP 4xx/5xx → clear error message with CLI context Args: rsp: The API response object @@ -58,15 +315,22 @@ def ensure_ok(rsp, check_business_error: bool = True): error codes in the output. Set to False for async task creation where we only care about HTTP success, not task execution. + command_name: The CLI command name (optional, for better context) """ + # Check HTTP status first if rsp.status_code != HTTPStatus.OK: - print_failed_message(rsp) + print_failed_message(rsp, command_name=command_name) raise typer.Exit(1) - # Check for business-level errors even when HTTP status is 200 + # Check if output exists output = rsp.output if output is None: - print_failed_message(rsp) + # HTTP 200 but no output - this is unusual, treat as error + err_console.print( + f"[red]Error[/red] " + f"request_id: {getattr(rsp, 'request_id', 'N/A')}, " + f"HTTP 200 but response has no output data", + ) raise typer.Exit(1) # Only check business-level errors if explicitly requested @@ -74,18 +338,29 @@ def ensure_ok(rsp, check_business_error: bool = True): # Some APIs return error info in output even with HTTP 200 if isinstance(output, dict): error_code = output.get("code") - message = output.get("message", "Unknown error") + message = output.get("message") else: error_code = getattr(output, "code", None) - message = getattr(output, "message", "Unknown error") - - if error_code and error_code != "": - err_console.print( - f"[red]Business Error[/red] request_id: {rsp.request_id}, " - f"status_code: {rsp.status_code}, " - f"code: {error_code}, " - f"message: {message}", + message = getattr(output, "message", None) + + # Only report if there's an actual error code + if error_code: + # Use the new error formatting with CLI context + request_id = getattr(rsp, "request_id", "N/A") + cli_error_code = _get_cli_error_code(error_code) + cli_error_message = _get_cli_error_message( + error_code, + message or "API returned error code without message", ) + + formatted_error = _format_error_parts( + request_id=request_id, + status_code=str(rsp.status_code), + cli_error_code=cli_error_code, + cli_error_message=cli_error_message, + command_name=command_name, + ) + err_console.print(formatted_error) raise typer.Exit(1) return output @@ -107,8 +382,64 @@ def error(message: str, exit_code: int = 1) -> NoReturn: raise typer.Exit(exit_code) +def _handle_exception( + exception: Exception, + action: str, + output_console: Console, +) -> NoReturn: + """Handle an exception and print a friendly error message. + + Maps SDK exception types to CLI-friendly error codes and enhances + error messages with CLI context. Preserves full exception context + including stack trace for debugging. + + Args: + exception: The exception to handle. + action: The action that failed (e.g., "FC registration failed"). + output_console: The Rich console to print to. + """ + if isinstance(exception, DashScopeException): + # Handle known DashScope exceptions with structured error info + request_id = getattr(exception, "request_id", "N/A") or "N/A" + message = getattr(exception, "message", str(exception)) + + # Map exception type to CLI-friendly error code + exception_type_name = type(exception).__name__ + cli_error_code = _get_cli_error_code(exception_type_name) + cli_error_message = _get_cli_error_message( + exception_type_name, + message, + ) + + output_console.print( + f"[red]{action}[/red] " + f"(request_id: {request_id}, code: {cli_error_code})\n" + f" {cli_error_message}", + no_wrap=True, + ) + # Log full traceback for debugging + logger.debug( + f"{action} failed with DashScopeException", + exc_info=True, + ) + else: + # Handle unexpected exceptions with full context + output_console.print(f"[red]{action}:[/red] {exception}") + # Log full traceback for debugging unexpected errors + logger.debug( + f"{action} failed with unexpected exception", + exc_info=True, + ) + raise typer.Exit(1) from exception + + def handle_sdk_error(action: str): - """Convert unexpected SDK exceptions into friendly CLI errors.""" + """Convert unexpected SDK exceptions into friendly CLI errors. + + Maps SDK exception types to CLI-friendly error codes and enhances + error messages with CLI context. Preserves full exception context + including stack trace for debugging. + """ def decorator(command_function: CommandFunction) -> CommandFunction: @wraps(command_function) @@ -116,9 +447,42 @@ def wrapper(*args, **kwargs): try: return command_function(*args, **kwargs) except typer.Exit: + # Re-raise intentional exits without modification raise + except DashScopeException as exception: + # Handle known DashScope exceptions with structured error info + request_id = getattr(exception, "request_id", "N/A") or "N/A" + message = getattr(exception, "message", str(exception)) + + # Map exception type to CLI-friendly error code + exception_type_name = type(exception).__name__ + cli_error_code = _get_cli_error_code(exception_type_name) + cli_error_message = _get_cli_error_message( + exception_type_name, + message, + ) + + err_console.print( + f"[red]{action}[/red] " + f"(request_id: {request_id}, code: {cli_error_code})\n" + f" {cli_error_message}", + no_wrap=True, + ) + # Log full traceback for debugging + logger.debug( + f"{action} failed with DashScopeException", + exc_info=True, + ) + raise typer.Exit(1) from exception except Exception as exception: - error(f"{action}: {exception}") + # Handle unexpected exceptions with full context + err_console.print(f"[red]{action}:[/red] {exception}") + # Log full traceback for debugging unexpected errors + logger.debug( + f"{action} failed with unexpected exception", + exc_info=True, + ) + raise typer.Exit(1) from exception return wrapper # type: ignore[return-value] diff --git a/dashscope/cli/deployments.py b/dashscope/cli/deployments.py index 44f2a58..a026a70 100644 --- a/dashscope/cli/deployments.py +++ b/dashscope/cli/deployments.py @@ -100,7 +100,24 @@ def _print_deployments(output): ): console.print("There is no deployed model!") return - for dep in output.deployments: + + # Support both dictionary and object formats + if isinstance(output, dict): + raw_deps = output.get("deployments", []) + from types import SimpleNamespace + + deployments = [ + SimpleNamespace(**d) if isinstance(d, dict) else d + for d in raw_deps + ] + else: + deployments = getattr(output, "deployments", []) + + if not deployments: + console.print("There is no deployed model!") + return + + for dep in deployments: console.print( f"Deployed_model: {dep.deployed_model}, " f"model: {dep.model_name}, " diff --git a/dashscope/cli/fine_tunes.py b/dashscope/cli/fine_tunes.py index d531752..9e28e73 100644 --- a/dashscope/cli/fine_tunes.py +++ b/dashscope/cli/fine_tunes.py @@ -115,11 +115,11 @@ def _stream_events(job_id: str): ) return - status = ( - rsp.output.get("status") - if isinstance(rsp.output, dict) - else getattr(rsp.output, "status", None) - ) + # Support both dictionary and object formats + if isinstance(rsp.output, dict): + status = rsp.output.get("status") + else: + status = getattr(rsp.output, "status", None) if status in ( TaskStatus.FAILED, TaskStatus.CANCELED, @@ -252,7 +252,11 @@ def create( if output is None: error("Fine-tune creation returned empty response") - job_id = getattr(output, "job_id", None) + job_id = ( + output.get("job_id") + if isinstance(output, dict) + else getattr(output, "job_id", None) + ) if not job_id: error( "Fine-tune creation succeeded but missing job_id in response. " diff --git a/dashscope/cli/generation.py b/dashscope/cli/generation.py index b624acc..73c3617 100644 --- a/dashscope/cli/generation.py +++ b/dashscope/cli/generation.py @@ -7,7 +7,7 @@ from dashscope.aigc import Generation from dashscope.cli.common import ( - error, + err_console, handle_sdk_error, print_failed_message, ) @@ -43,7 +43,9 @@ def _build_generation_kwargs( try: kwargs["messages"] = json.loads(messages) except json.JSONDecodeError as exc: - error("--messages must be a valid JSON string") + err_console.print( + "[red]Error:[/red] --messages must be a valid JSON string", + ) raise typer.Exit(1) from exc # Group simple parameters to reduce branches @@ -178,6 +180,7 @@ def create( typer.echo(usage) else: print_failed_message(rsp) + break else: if response.status_code == HTTPStatus.OK: typer.echo(response.output) diff --git a/dashscope/client/base_api.py b/dashscope/client/base_api.py index fc067fc..1786500 100644 --- a/dashscope/client/base_api.py +++ b/dashscope/client/base_api.py @@ -1556,12 +1556,23 @@ def _handle_response(cls, response: requests.Response): ): for is_error, status_code, data in cls._handle_stream(response): if is_error: + try: + error_data = json.loads(data) + code = error_data.get("code") or error_data.get( + "error_code", + ) + message = error_data.get("message") or error_data.get( + "error_message", + ) + except json.JSONDecodeError: + code = None + message = data yield DashScopeAPIResponse( request_id=request_id, status_code=status_code, output=None, - code="", - message="", + code=code, + message=message, ) # noqa E501 else: yield DashScopeAPIResponse( diff --git a/dashscope/common/error.py b/dashscope/common/error.py index 2b49cc7..cb1cabb 100644 --- a/dashscope/common/error.py +++ b/dashscope/common/error.py @@ -144,3 +144,15 @@ class UploadFileException(DashScopeException): class TimeoutException(DashScopeException): pass + + +class InvalidBaseURL(DashScopeException): + """Raised when the base_address is invalid. + + This can happen when the scheme is missing or the URL is malformed. + """ + + class BaseUrlError: + """Error message pattern for invalid base URL.""" + + pattern = r"No scheme supplied" diff --git a/dashscope/common/error_registry.py b/dashscope/common/error_registry.py new file mode 100644 index 0000000..ce189c6 --- /dev/null +++ b/dashscope/common/error_registry.py @@ -0,0 +1,3216 @@ +# -*- coding: utf-8 -*- +"""Auto-generated by tools/generate.py -- DO NOT EDIT.""" +from __future__ import annotations +from dataclasses import dataclass, field +from typing import Dict, List + +__version__ = "1.0.0" +__commit__ = "aa17fe8" +__snapshot__ = False + + +@dataclass(frozen=True) +class PublicError: + key: str + status_code: int + error_code: str + error_msg: str + anthropic_error_code: str + + def format_msg(self, variables: Dict[str, str] = None) -> str: + msg = self.error_msg + if variables: + for k, v in variables.items(): + msg = msg.replace("{" + k + "}", v) + return msg + + +@dataclass(frozen=True) +class InternalErrorDef: + name: str + external: PublicError + message: str + allow_retry: bool = False + vars: List[str] = field(default_factory=list) + cause: str = "" + solution: str = "" + + def format_message(self, variables: Dict[str, str] = None) -> str: + msg = self.message + if variables: + for k, v in variables.items(): + msg = msg.replace("{" + k + "}", v) + return msg + + +# -- Public Errors -------------------------------------------------- + +INVALID_REQUEST = PublicError( + key="invalid_request", + status_code=400, + error_code="BadRequestError", + error_msg=( + "The request is invalid. Please check the request and try " "again." + ), + anthropic_error_code="invalid_request_error", +) + +MISSING_PARAMETER = PublicError( + key="missing_parameter", + status_code=400, + error_code="BadRequestError", + error_msg="Missing required parameter: {parameter}.", + anthropic_error_code="invalid_request_error", +) + +CONTENT_POLICY_VIOLATION = PublicError( + key="content_policy_violation", + status_code=400, + error_code="BadRequestError", + error_msg="The request was rejected by content policy.", + anthropic_error_code="invalid_request_error", +) + +INVALID_URL = PublicError( + key="invalid_url", + status_code=400, + error_code="BadRequestError", + error_msg="The provided URL is invalid or cannot be accessed.", + anthropic_error_code="invalid_request_error", +) + +INVALID_FILE = PublicError( + key="invalid_file", + status_code=400, + error_code="BadRequestError", + error_msg="The provided file is invalid.", + anthropic_error_code="invalid_request_error", +) + +AUTH_FAILED = PublicError( + key="auth_failed", + status_code=401, + error_code="AuthenticationError", + error_msg=( + "Authentication failed. Please provide valid " + "authentication credentials." + ), + anthropic_error_code="authentication_error", +) + +INVALID_API_KEY = PublicError( + key="invalid_api_key", + status_code=401, + error_code="AuthenticationError", + error_msg="Incorrect API key provided.", + anthropic_error_code="authentication_error", +) + +PERMISSION_DENIED = PublicError( + key="permission_denied", + status_code=403, + error_code="PermissionDeniedError", + error_msg="You do not have permission to access this resource.", + anthropic_error_code="permission_error", +) + +RESOURCE_NOT_FOUND = PublicError( + key="resource_not_found", + status_code=404, + error_code="NotFoundError", + error_msg="The requested resource was not found: {resource}.", + anthropic_error_code="not_found_error", +) + +REQUEST_TOO_LARGE = PublicError( + key="request_too_large", + status_code=413, + error_code="RequestTooLargeError", + error_msg="The request exceeds the maximum allowed size of {limit}.", + anthropic_error_code="request_too_large", +) + +RATE_LIMIT_EXCEEDED = PublicError( + key="rate_limit_exceeded", + status_code=429, + error_code="RateLimitError", + error_msg="Rate limit exceeded. Please slow down your requests.", + anthropic_error_code="rate_limit_error", +) + +CONCURRENCY_LIMIT_EXCEEDED = PublicError( + key="concurrency_limit_exceeded", + status_code=429, + error_code="RateLimitError", + error_msg=( + "Too many concurrent requests. Please reduce concurrency " + "and try again." + ), + anthropic_error_code="rate_limit_error", +) + +INSUFFICIENT_QUOTA = PublicError( + key="insufficient_quota", + status_code=429, + error_code="RateLimitError", + error_msg=( + "You exceeded your current quota. Please check your plan " + "and billing details." + ), + anthropic_error_code="billing_error", +) + +INTERNAL_ERROR = PublicError( + key="internal_error", + status_code=500, + error_code="InternalServerError", + error_msg=( + "The server encountered an internal error. Please try again " "later." + ), + anthropic_error_code="api_error", +) + +SERVICE_UNAVAILABLE = PublicError( + key="service_unavailable", + status_code=503, + error_code="ServiceUnavailableError", + error_msg=( + "The service is temporarily unavailable. Please try again " "later." + ), + anthropic_error_code="overloaded_error", +) + +REQUEST_TIMEOUT = PublicError( + key="request_timeout", + status_code=504, + error_code="GatewayTimeoutError", + error_msg="The request timed out. Please try again later.", + anthropic_error_code="timeout_error", +) + + +# -- Internal Errors ------------------------------------------------- + +GATEWAY_INVALID_REQUEST = InternalErrorDef( + name="gateway.InvalidRequest", + external=INVALID_REQUEST, + message="The request body is malformed or not valid JSON.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +GATEWAY_MISSING_INPUT = InternalErrorDef( + name="gateway.MissingInput", + external=MISSING_PARAMETER, + message="Required input field is missing.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +GATEWAY_INVALID_PARAMETER = InternalErrorDef( + name="gateway.InvalidParameter", + external=INVALID_REQUEST, + message="Parameter '{parameter}' has an invalid value.", + allow_retry=False, + vars=["parameter"], + cause="", + solution="", +) + +GATEWAY_INVALID_REQUEST_BODY = InternalErrorDef( + name="gateway.InvalidRequestBody", + external=INVALID_REQUEST, + message="The request body structure is invalid or unexpected.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +GATEWAY_REQUEST_TOO_LARGE = InternalErrorDef( + name="gateway.RequestTooLarge", + external=REQUEST_TOO_LARGE, + message="Request payload exceeds the allowed size of {limit}.", + allow_retry=False, + vars=["limit"], + cause="", + solution="", +) + +GATEWAY_RATE_LIMIT_EXCEEDED = InternalErrorDef( + name="gateway.RateLimitExceeded", + external=RATE_LIMIT_EXCEEDED, + message="Request rate exceeds the configured RPS limit.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +GATEWAY_BURST_RATE_EXCEEDED = InternalErrorDef( + name="gateway.BurstRateExceeded", + external=RATE_LIMIT_EXCEEDED, + message="Burst request rate exceeded; service is shedding load.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +GATEWAY_CONCURRENCY_LIMIT_EXCEEDED = InternalErrorDef( + name="gateway.ConcurrencyLimitExceeded", + external=CONCURRENCY_LIMIT_EXCEEDED, + message="Concurrent request count exceeds the limit.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +GATEWAY_QUOTA_EXCEEDED = InternalErrorDef( + name="gateway.QuotaExceeded", + external=INSUFFICIENT_QUOTA, + message="Account usage quota has been exhausted.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +GATEWAY_ACCOUNT_IN_ARREARS = InternalErrorDef( + name="gateway.AccountInArrears", + external=PERMISSION_DENIED, + message=( + "Account is in arrears; requests are suspended until " + "balance is restored." + ), + allow_retry=False, + vars=[], + cause="", + solution="", +) + +GATEWAY_PERMISSION_DENIED = InternalErrorDef( + name="gateway.PermissionDenied", + external=PERMISSION_DENIED, + message="Caller does not have permission to perform this operation.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +GATEWAY_MODEL_ACCESS_DENIED = InternalErrorDef( + name="gateway.ModelAccessDenied", + external=PERMISSION_DENIED, + message="Caller does not have access to the requested model.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +GATEWAY_RESOURCE_NOT_FOUND = InternalErrorDef( + name="gateway.ResourceNotFound", + external=RESOURCE_NOT_FOUND, + message="The requested {resource} does not exist.", + allow_retry=False, + vars=["resource"], + cause="", + solution="", +) + +GATEWAY_MODEL_NOT_FOUND = InternalErrorDef( + name="gateway.ModelNotFound", + external=RESOURCE_NOT_FOUND, + message="The requested model does not exist.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +GATEWAY_INVALID_API_KEY = InternalErrorDef( + name="gateway.InvalidApiKey", + external=AUTH_FAILED, + message="Invalid or missing API-key provided.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +GATEWAY_INVALID_API_TOKEN = InternalErrorDef( + name="gateway.InvalidApiToken", + external=AUTH_FAILED, + message="Invalid or expired API Token provided.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +GATEWAY_WORKSPACE_ACCESS_DENIED = InternalErrorDef( + name="gateway.WorkspaceAccessDenied", + external=PERMISSION_DENIED, + message="Workspace access denied.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +GATEWAY_ENDPOINT_ACCESS_DENIED = InternalErrorDef( + name="gateway.EndpointAccessDenied", + external=PERMISSION_DENIED, + message="Workspace endpoint access denied.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +GATEWAY_I_P_ACCESS_DENIED = InternalErrorDef( + name="gateway.IPAccessDenied", + external=PERMISSION_DENIED, + message="IP access denied by API-Key restrictions.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +GATEWAY_USER_BLACKLISTED = InternalErrorDef( + name="gateway.UserBlacklisted", + external=PERMISSION_DENIED, + message="User is blacklisted; access denied.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +GATEWAY_ENTITY_UNPURCHASED = InternalErrorDef( + name="gateway.EntityUnpurchased", + external=PERMISSION_DENIED, + message="Access to model denied. The entity has not been purchased.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +GATEWAY_TOKEN_PLAN_EXPIRED = InternalErrorDef( + name="gateway.TokenPlanExpired", + external=PERMISSION_DENIED, + message="Token-plan subscription has expired.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +GATEWAY_TOKEN_PLAN_QUOTA_EXHAUSTED = InternalErrorDef( + name="gateway.TokenPlanQuotaExhausted", + external=INSUFFICIENT_QUOTA, + message="Token-plan quota has been exhausted.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +GATEWAY_FREE_QUOTA_EXHAUSTED = InternalErrorDef( + name="gateway.FreeQuotaExhausted", + external=PERMISSION_DENIED, + message="Free allocated quota has been exhausted.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +GATEWAY_ILLEGAL_ENDPOINT = InternalErrorDef( + name="gateway.IllegalEndpoint", + external=INVALID_REQUEST, + message="The endpoint does not support the requested operation.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +GATEWAY_DATA_INSPECTION_FAILED = InternalErrorDef( + name="gateway.DataInspectionFailed", + external=CONTENT_POLICY_VIOLATION, + message="Data inspection failed; content rejected.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +GATEWAY_GATEWAY_TIMEOUT = InternalErrorDef( + name="gateway.GatewayTimeout", + external=REQUEST_TIMEOUT, + message="Gateway operation timed out.", + allow_retry=True, + vars=[], + cause="", + solution="", +) + +GATEWAY_O_S_S_UPLOAD_FAILED = InternalErrorDef( + name="gateway.OSSUploadFailed", + external=INTERNAL_ERROR, + message="OSS upload failed.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +GATEWAY_RESOURCE_ACCESS_DENIED = InternalErrorDef( + name="gateway.ResourceAccessDenied", + external=PERMISSION_DENIED, + message="OSS resource access denied (ownership check failed).", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +GATEWAY_ASYNC_SYNC_DENIED = InternalErrorDef( + name="gateway.AsyncSyncDenied", + external=PERMISSION_DENIED, + message="Current user API does not support the requested call mode.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +GATEWAY_INTERNAL_ERROR = InternalErrorDef( + name="gateway.InternalError", + external=INTERNAL_ERROR, + message="A system error has occurred, please try again later.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +GATEWAY_SERVICE_SITE_UNSUPPORTED = InternalErrorDef( + name="gateway.ServiceSiteUnsupported", + external=INVALID_REQUEST, + message="Model is not supported in current workspace service site.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +GATEWAY_APP_ACCESS_DENIED = InternalErrorDef( + name="gateway.AppAccessDenied", + external=PERMISSION_DENIED, + message="App access denied.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +GATEWAY_HEADER_ACCESS_DENIED = InternalErrorDef( + name="gateway.HeaderAccessDenied", + external=PERMISSION_DENIED, + message="Header authentication failed.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +GATEWAY_RESOURCE_NOT_EXIST = InternalErrorDef( + name="gateway.ResourceNotExist", + external=INVALID_REQUEST, + message="OSS resource not found or inaccessible.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +GATEWAY_REQUEST_TIMEOUT = InternalErrorDef( + name="gateway.RequestTimeout", + external=REQUEST_TIMEOUT, + message="Request timed out, please try again later.", + allow_retry=True, + vars=[], + cause="", + solution="", +) + +APISERVER_UNSUPPORTED_MODEL = InternalErrorDef( + name="apiserver.UnsupportedModel", + external=RESOURCE_NOT_FOUND, + message="The requested model is not supported by this operation.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +APISERVER_UNSUPPORTED_OPERATION = InternalErrorDef( + name="apiserver.UnsupportedOperation", + external=INVALID_REQUEST, + message="This operation is not supported for the current request.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +APISERVER_UNSUPPORTED_PARAMETER = InternalErrorDef( + name="apiserver.UnsupportedParameter", + external=INVALID_REQUEST, + message="The provided parameter is not supported in this context.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +APISERVER_INVALID_PARAMETER_COMBINATION = InternalErrorDef( + name="apiserver.InvalidParameterCombination", + external=INVALID_REQUEST, + message="The combination of provided parameters is invalid.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +APISERVER_CONTENT_POLICY_VIOLATION = InternalErrorDef( + name="apiserver.ContentPolicyViolation", + external=CONTENT_POLICY_VIOLATION, + message="Request content violates content safety policy.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +APISERVER_INVALID_U_R_L = InternalErrorDef( + name="apiserver.InvalidURL", + external=INVALID_URL, + message="The provided URL is invalid or unreachable.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +APISERVER_FILE_DOWNLOAD_FAILED = InternalErrorDef( + name="apiserver.FileDownloadFailed", + external=INVALID_FILE, + message="Failed to download file from the provided URL.", + allow_retry=True, + vars=[], + cause="", + solution="", +) + +APISERVER_INVALID_FILE = InternalErrorDef( + name="apiserver.InvalidFile", + external=INVALID_FILE, + message="The uploaded or referenced file is invalid.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +APISERVER_UNSUPPORTED_FILE_FORMAT = InternalErrorDef( + name="apiserver.UnsupportedFileFormat", + external=INVALID_FILE, + message="The file format is not supported.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +APISERVER_FILE_TOO_LARGE = InternalErrorDef( + name="apiserver.FileTooLarge", + external=REQUEST_TOO_LARGE, + message="The uploaded file exceeds the maximum allowed size of {limit}.", + allow_retry=False, + vars=["limit"], + cause="", + solution="", +) + +APISERVER_INVALID_AUDIO = InternalErrorDef( + name="apiserver.InvalidAudio", + external=INVALID_FILE, + message="The provided audio data is invalid or unreadable.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +APISERVER_TIMEOUT = InternalErrorDef( + name="apiserver.Timeout", + external=REQUEST_TIMEOUT, + message="Upstream processing timed out.", + allow_retry=True, + vars=[], + cause="", + solution="", +) + +MINT_INTERNAL_PROCESS_ERROR = InternalErrorDef( + name="mint.InternalProcessError", + external=INTERNAL_ERROR, + message="Internal Error during model process.", + allow_retry=True, + vars=[], + cause="服务端内部处理异常", + solution="稍后重试或联系技术支持", +) + +MINT_JSON_SERIALIZATION_ERROR = InternalErrorDef( + name="mint.JsonSerializationError", + external=INTERNAL_ERROR, + message="The service output is not serializable.", + allow_retry=True, + vars=[], + cause="服务端输出 JSON 序列化失败", + solution="联系技术支持", +) + +MINT_OUTPUT_PAYLOAD_SCHEMA_ERROR = InternalErrorDef( + name="mint.OutputPayloadSchemaError", + external=INTERNAL_ERROR, + message="The output payload schema is not valid.", + allow_retry=True, + vars=[], + cause="服务端输出数据结构不合法", + solution="联系技术支持", +) + +MINT_MODEL_SERVING_ERROR = InternalErrorDef( + name="mint.ModelServingError", + external=INTERNAL_ERROR, + message="Internal Error calling model processing.", + allow_retry=True, + vars=[], + cause="调用模型服务时发生内部错误", + solution="稍后重试或联系技术支持", +) + +MINT_MODEL_SERVING_WITH_DETAIL_ERROR = InternalErrorDef( + name="mint.ModelServingWithDetailError", + external=INTERNAL_ERROR, + message=( + "An error occurred in model serving with code: {}, " + "and the error message is {}" + ), + allow_retry=True, + vars=[], + cause="模型服务返回异常", + solution="根据错误详情排查,或联系技术支持", +) + +MINT_MODEL_CALL_ERROR = InternalErrorDef( + name="mint.ModelCallError", + external=INTERNAL_ERROR, + message="Failed to invoke the model service", + allow_retry=True, + vars=[], + cause="模型服务调用失败", + solution="稍后重试或联系技术支持", +) + +MINT_INTERNAL_PREPROCESS_ERROR = InternalErrorDef( + name="mint.InternalPreprocessError", + external=INTERNAL_ERROR, + message="Internal Error during model pre-process.", + allow_retry=True, + vars=[], + cause="请求预处理过程中发生内部错误", + solution="稍后重试或联系技术支持", +) + +MINT_INTERNAL_POSTPROCESS_ERROR = InternalErrorDef( + name="mint.InternalPostprocessError", + external=INTERNAL_ERROR, + message=( + "Internal Error during model post-process. " + "The error message is [{}]" + ), + allow_retry=True, + vars=[], + cause="响应后处理过程中发生内部错误", + solution="稍后重试或联系技术支持", +) + +MINT_WANX_CALLING_ERROR = InternalErrorDef( + name="mint.WanxCallingError", + external=INTERNAL_ERROR, + message="Internal Error during calling wanx", + allow_retry=True, + vars=[], + cause="调用 Wanx 服务时发生内部错误", + solution="稍后重试或联系技术支持", +) + +MINT_INTERNAL_PLUG_ERROR = InternalErrorDef( + name="mint.InternalPlugError", + external=INTERNAL_ERROR, + message="Internal Error during Plug calling.", + allow_retry=True, + vars=[], + cause="插件调用过程中发生内部错误", + solution="稍后重试或联系技术支持", +) + +MINT_PLUGIN_RESULT_IS_NONE = InternalErrorDef( + name="mint.PluginResultIsNone", + external=INTERNAL_ERROR, + message="Plugin result is null", + allow_retry=True, + vars=[], + cause="插件返回结果为空", + solution="检查插件配置后重试", +) + +MINT_PLUGIN_RESULT_PARSE_ERROR = InternalErrorDef( + name="mint.PluginResultParseError", + external=INTERNAL_ERROR, + message="Plugin result parse error", + allow_retry=True, + vars=[], + cause="插件返回结果解析失败", + solution="检查插件返回数据格式", +) + +MINT_PLUGIN_EXECUTION_RESULT_FORMAT_ERROR = InternalErrorDef( + name="mint.PluginExecutionResultFormatError", + external=INTERNAL_ERROR, + message="Plugin execution result is not a legal JSON. Result is [{}]", + allow_retry=True, + vars=[], + cause="插件执行结果不是合法 JSON", + solution="检查插件实现是否返回正确的 JSON 格式", +) + +MINT_SEARCH_RESULT_FORMAT_ERROR = InternalErrorDef( + name="mint.SearchResultFormatError", + external=INTERNAL_ERROR, + message="Search result error", + allow_retry=True, + vars=[], + cause="搜索结果格式异常", + solution="稍后重试或联系技术支持", +) + +MINT_INTERNAL_SEARCH_ERROR = InternalErrorDef( + name="mint.InternalSearchError", + external=INTERNAL_ERROR, + message="Internal Error during search phase.", + allow_retry=True, + vars=[], + cause="搜索过程中发生内部错误", + solution="稍后重试或联系技术支持", +) + +MINT_INTERNAL_AGENT_ERROR = InternalErrorDef( + name="mint.InternalAgentError", + external=INTERNAL_ERROR, + message="An error occurred in agent system", + allow_retry=True, + vars=[], + cause="Agent 系统内部发生错误", + solution="稍后重试或联系技术支持", +) + +MINT_AGENT_SERVICE_GET_RESULT_ERROR = InternalErrorDef( + name="mint.AgentServiceGetResultError", + external=INTERNAL_ERROR, + message="An error occurred in agent system when getting result", + allow_retry=True, + vars=[], + cause="Agent 系统获取结果时发生错误", + solution="稍后重试或联系技术支持", +) + +MINT_GENERATOR_RESULT_J_S_O_N_DECODE_ERROR = InternalErrorDef( + name="mint.GeneratorResultJSONDecodeError", + external=INTERNAL_ERROR, + message="JSON decode error in generator:{}", + allow_retry=True, + vars=[], + cause="生成器结果 JSON 解码失败", + solution="稍后重试或联系技术支持", +) + +MINT_SERVICE_UNAVAILABLE_ERROR = InternalErrorDef( + name="mint.ServiceUnavailableError", + external=SERVICE_UNAVAILABLE, + message=( + "Too many requests. Your requests are being throttled due to " + "system capacity limits. Please try again later." + ), + allow_retry=False, + vars=[], + cause="请求过多,系统容量不足触发限流", + solution="稍后重试", +) + +MINT_RESPONSE_TIMEOUT_ERROR = InternalErrorDef( + name="mint.ResponseTimeoutError", + external=REQUEST_TIMEOUT, + message="Response timeout.", + allow_retry=True, + vars=[], + cause="服务端响应超时", + solution="稍后重试或切换为流式调用", +) + +MINT_V_L_O_C_R_DOCUMENT_PARSING_TIMEOUT_ERROR = InternalErrorDef( + name="mint.VLOCRDocumentParsingTimeoutError", + external=REQUEST_TIMEOUT, + message="VLOCR document_parsing task Response timeout.", + allow_retry=True, + vars=[], + cause="VLOCR 文档解析任务响应超时", + solution="减少文档页数或文件大小后重试", +) + +MINT_INVALID_PARAMETER = InternalErrorDef( + name="mint.InvalidParameter", + external=INVALID_REQUEST, + message="InvalidParameter from input", + allow_retry=False, + vars=[], + cause="请求参数不合法", + solution="检查请求参数是否符合 API 文档要求", +) + +MINT_MISSING_INPUT = InternalErrorDef( + name="mint.MissingInput", + external=INVALID_REQUEST, + message="Missing input", + allow_retry=False, + vars=[], + cause="请求缺少输入内容", + solution="确保请求中包含必要的 input 字段", +) + +MINT_INPUT_FORMAT_ERROR = InternalErrorDef( + name="mint.InputFormatError", + external=INVALID_REQUEST, + message="The input format error", + allow_retry=False, + vars=[], + cause="输入格式错误", + solution="检查输入格式是否符合 API 文档要求", +) + +MINT_MODEL_INPUT_FORMAT_ERROR = InternalErrorDef( + name="mint.ModelInputFormatError", + external=INVALID_REQUEST, + message="The model input format error", + allow_retry=False, + vars=[], + cause="模型输入格式错误", + solution="检查输入格式是否匹配模型要求", +) + +MINT_INPUT_UNKNOWN_ERROR = InternalErrorDef( + name="mint.InputUnknownError", + external=INVALID_REQUEST, + message="An unknown error occurred due to an unsupported input format.", + allow_retry=False, + vars=[], + cause="不支持的输入格式导致未知错误", + solution="检查输入格式是否符合 API 文档要求", +) + +MINT_INPUT_PREPROCESS_ERROR = InternalErrorDef( + name="mint.InputPreprocessError", + external=INVALID_REQUEST, + message="An error during model pre-process.", + allow_retry=False, + vars=[], + cause="输入预处理过程中发生错误", + solution="检查输入数据格式", +) + +MINT_ECC_ERROR = InternalErrorDef( + name="mint.EccError", + external=INVALID_REQUEST, + message="The service is ecc check failed", + allow_retry=False, + vars=[], + cause="ECC 校验失败", + solution="检查请求内容后重试", +) + +MINT_MESSAGE_ERROR = InternalErrorDef( + name="mint.MessageError", + external=INVALID_REQUEST, + message=( + 'Role must be in [\\"user\\", \\"assistant\\", \\"system\\", ' + '\\"function\\", \\"plugin\\", \\"tool\\"] and the role in last ' + 'message must be in [\\"user\\", \\"function\\", \\"tool\\"]' + ), + allow_retry=False, + vars=[], + cause="messages 中的 role 不合法", + solution="确保 role 在合法范围内,且最后一条消息的 role 正确", +) + +MINT_MESSAGE_CONTENT_FIELD_REQUIRED_ERROR = InternalErrorDef( + name="mint.MessageContentFieldRequiredError", + external=INVALID_REQUEST, + message="The content field is a required field.", + allow_retry=False, + vars=[], + cause="消息中缺少必填的 content 字段", + solution="为每条消息添加 content 字段", +) + +MINT_MESSAGE_CONTENT_FIELD_ERROR = InternalErrorDef( + name="mint.MessageContentFieldError", + external=INVALID_REQUEST, + message="Invalid content field provided.", + allow_retry=False, + vars=[], + cause="消息的 content 字段内容不合法", + solution="检查 content 字段格式和类型", +) + +MINT_MESSAGE_CONTENT_ITEM_TYPE_ERROR = InternalErrorDef( + name="mint.MessageContentItemTypeError", + external=INVALID_REQUEST, + message=( + "When content is a list, each item must be an object " + '(e.g. {\\"text\\": \\"...\\"}, {\\"image\\": \\"...\\"})' + ), + allow_retry=False, + vars=[], + cause="content 列表中的元素格式不正确", + solution="确保每个元素是对象格式而非裸字符串", +) + +MINT_MESSAGE_CONTENT_MULTIMODAL_UNSUPPORTED_ERROR = InternalErrorDef( + name="mint.MessageContentMultimodalUnsupportedError", + external=INVALID_REQUEST, + message=( + "The current model only supports text modality and does not support " + "image input. Please remove the image content from your messages " + "and retry." + ), + allow_retry=False, + vars=[], + cause="当前模型不支持多模态输入", + solution="移除消息中的多模态内容", +) + +MINT_MESSAGE_FUNCTION_FIELD_ERROR = InternalErrorDef( + name="mint.MessageFunctionFieldError", + external=INVALID_REQUEST, + message="Invalid function field provided.", + allow_retry=False, + vars=[], + cause="消息的 function 字段内容不合法", + solution="检查 function 字段的格式和类型", +) + +MINT_LAST_MESSAGE_MUST_BE_USER = InternalErrorDef( + name="mint.LastMessageMustBeUser", + external=INVALID_REQUEST, + message="The last message must be a user message", + allow_retry=False, + vars=[], + cause="最后一条消息必须是 user 角色", + solution="确保 messages 最后一条消息 role 为 user", +) + +MINT_SINGLE_SYSTEM_ROLE_ERROR = InternalErrorDef( + name="mint.SingleSystemRoleError", + external=INVALID_REQUEST, + message=( + "The messages list should not contain only one message with role " + '\\"system\\". Please include at least one other role in the ' + "messages." + ), + allow_retry=False, + vars=[], + cause="messages 列表不能只有一条 system 消息", + solution="在 messages 中添加其他角色的消息", +) + +MINT_MESSAGE_CONTAINS_SURROGATE_ERROR = InternalErrorDef( + name="mint.MessageContainsSurrogateError", + external=INVALID_REQUEST, + message=( + "Message contains invalid Unicode surrogate characters " + "(U+D800-U+DFFF)" + ), + allow_retry=False, + vars=[], + cause="消息包含无效 Unicode 代理字符", + solution="移除消息中的 U+D800-U+DFFF 范围字符后重试", +) + +MINT_INPUT_MESSAGES_TOO_LONG = InternalErrorDef( + name="mint.InputMessagesTooLong", + external=INVALID_REQUEST, + message=( + "The length of the input.messages field has exceeded the limit. " + "only one" + ), + allow_retry=False, + vars=[], + cause="input.messages 内容长度超出限制", + solution="缩短 messages 内容后重试", +) + +MINT_ROLE_ERROR = InternalErrorDef( + name="mint.RoleError", + external=INVALID_REQUEST, + message="Role must be in [user, assistant].", + allow_retry=False, + vars=[], + cause="role 值不合法", + solution="确保 role 为合法值", +) + +MINT_INPUT_MESSAGE_FORMAT_ERROR = InternalErrorDef( + name="mint.InputMessageFormatError", + external=INVALID_REQUEST, + message=( + "The input format supports only the messages format, " + "not the prompt format." + ), + allow_retry=False, + vars=[], + cause="输入格式仅支持 messages 格式", + solution="使用 messages 格式替代 prompt 格式", +) + +MINT_TEMPERATURE_TYPE_ERROR = InternalErrorDef( + name="mint.TemperatureTypeError", + external=INVALID_REQUEST, + message="Type of temperature should be float", + allow_retry=False, + vars=[], + cause="temperature 参数类型错误", + solution="确保 temperature 为 float 类型", +) + +MINT_TOP_K_RANGE_ERROR = InternalErrorDef( + name="mint.TopKRangeError", + external=INVALID_REQUEST, + message=( + "Parameter top_k be greater than or equal to " + "{base_configure.PARAM_TOP_K_MIN}" + ), + allow_retry=False, + vars=[], + cause="top_k 参数超出合法范围", + solution="将 top_k 调整到合法范围内", +) + +MINT_TOP_P_RANGE_ERROR = InternalErrorDef( + name="mint.TopPRangeError", + external=INVALID_REQUEST, + message="Range of top_p should be", + allow_retry=False, + vars=[], + cause="top_p 参数超出合法范围", + solution="将 top_p 调整到合法范围内", +) + +MINT_INPUT_CONTENT_TOO_LONG = InternalErrorDef( + name="mint.InputContentTooLong", + external=INVALID_REQUEST, + message="The length of the input.content field has exceeded the limit.", + allow_retry=False, + vars=[], + cause="input.content 字段长度超出限制", + solution="缩短 content 内容后重试", +) + +MINT_REPETITION_PENALTY_TYPE_ERROR = InternalErrorDef( + name="mint.RepetitionPenaltyTypeError", + external=INVALID_REQUEST, + message="Type of repetition_penalty should be float", + allow_retry=False, + vars=[], + cause="repetition_penalty 参数类型错误", + solution="确保 repetition_penalty 为 float 类型", +) + +MINT_HEAD_RESPONSE_PER_TOKENS_TYPE_ERROR = InternalErrorDef( + name="mint.HeadResponsePerTokensTypeError", + external=INVALID_REQUEST, + message="Type of head_response_per_tokens should be int", + allow_retry=False, + vars=[], + cause="head_response_per_tokens 参数类型错误", + solution="确保为 int 类型", +) + +MINT_HEAD_RESPONSE_PER_TOKENS_RANGE_ERROR = InternalErrorDef( + name="mint.HeadResponsePerTokensRangeError", + external=INVALID_REQUEST, + message=( + "Parameter head_response_per_tokens should be in " + "{base_configure.PARAM_HEAD_RESPONSE_PER_TOKENS_MIN}, " + "{base_configure.PARAM_HEAD_RESPONSE_PER_TOKENS_MAX}" + ), + allow_retry=False, + vars=[], + cause="head_response_per_tokens 参数超出合法范围", + solution="将参数调整到合法范围内", +) + +MINT_ENABLE_THINKING_ERROR = InternalErrorDef( + name="mint.EnableThinkingError", + external=INVALID_REQUEST, + message="The value of the enable_thinking parameter is restricted to {}.", + allow_retry=False, + vars=[], + cause="enable_thinking 参数值不合法", + solution="将 enable_thinking 设置为合法值", +) + +MINT_THINKING_MODE_N_ERROR = InternalErrorDef( + name="mint.ThinkingModeNError", + external=INVALID_REQUEST, + message="The n parameter must be 1 when enable_thinking is true", + allow_retry=False, + vars=[], + cause="开启 thinking 时 n 必须为 1", + solution="将 n 参数设置为 1,或关闭 thinking", +) + +MINT_SEED_TYPE_ERROR = InternalErrorDef( + name="mint.SeedTypeError", + external=INVALID_REQUEST, + message="Type of seed should be int", + allow_retry=False, + vars=[], + cause="seed 参数类型错误", + solution="确保 seed 为 int 类型", +) + +MINT_PRESENCE_PENALTY_RANGE_ERROR = InternalErrorDef( + name="mint.PresencePenaltyRangeError", + external=INVALID_REQUEST, + message=( + "Presence_penalty should be in " + "[{base_configure.PARAM_PRESENCE_PENALTY_MIN}, " + "{base_configure.PARAM_PRESENCE_PENALTY_MAX}]" + ), + allow_retry=False, + vars=[], + cause="presence_penalty 参数超出合法范围", + solution="将 presence_penalty 调整到合法范围内", +) + +MINT_FREQUENCY_PENALTY_RANGE_ERROR = InternalErrorDef( + name="mint.FrequencyPenaltyRangeError", + external=INVALID_REQUEST, + message=( + "Frequency_penalty should be in " + "[{base_configure.PARAM_FREQUENCY_PENALTY_MIN}, " + "{base_configure.PARAM_FREQUENCY_PENALTY_MAX}]" + ), + allow_retry=False, + vars=[], + cause="frequency_penalty 参数超出合法范围", + solution="将 frequency_penalty 调整到合法范围内", +) + +MINT_N_RANGE_ERROR = InternalErrorDef( + name="mint.NRangeError", + external=INVALID_REQUEST, + message="Range of n should be [1, {base_configure.PARAM_N_MAX}]", + allow_retry=False, + vars=[], + cause="n 参数超出合法范围", + solution="将 n 调整到合法范围内", +) + +MINT_INCREMENTAL_OUTPUT_ERROR = InternalErrorDef( + name="mint.IncrementalOutputError", + external=INVALID_REQUEST, + message="The incremental_output parameter only supports streaming mode.", + allow_retry=False, + vars=[], + cause="incremental_output 仅支持流式模式", + solution="使用流式调用或关闭 incremental_output", +) + +MINT_INCREMENTAL_OUTPUT_VALUE_ERROR = InternalErrorDef( + name="mint.IncrementalOutputValueError", + external=INVALID_REQUEST, + message="The model does not support incremental output.", + allow_retry=False, + vars=[], + cause="当前模型不支持 incremental_output", + solution="移除 incremental_output 参数", +) + +MINT_ONLY_SUPPORT_INCREMENTAL = InternalErrorDef( + name="mint.OnlySupportIncremental", + external=INVALID_REQUEST, + message="This model only supports incremental_output set to True.", + allow_retry=False, + vars=[], + cause="当前模型仅支持 incremental_output 为 True", + solution="将 incremental_output 设置为 true", +) + +MINT_FORCE_INCREMENTAL_OUTPUT_ERROR = InternalErrorDef( + name="mint.ForceIncrementalOutputError", + external=INVALID_REQUEST, + message=( + "The incremental_output parameter of this model cannot be set " + "to False." + ), + allow_retry=False, + vars=[], + cause="当前模型的 incremental_output 不可设为 False", + solution="将 incremental_output 设置为 true", +) + +MINT_OMNI_INCREMENTAL_OUTPUT_ERROR = InternalErrorDef( + name="mint.OmniIncrementalOutputError", + external=INVALID_REQUEST, + message=( + "The parameter enable_omni_output_audio_url must be set to true " + "when using the Omni model in non-streaming mode." + ), + allow_retry=False, + vars=[], + cause="Omni 模型非流式模式下需开启 enable_omni_output_audio_url", + solution="将 enable_omni_output_audio_url 设置为 true", +) + +MINT_INPUT_TOO_MANY_ARGUMENTS_ERROR = InternalErrorDef( + name="mint.InputTooManyArgumentsError", + external=INVALID_REQUEST, + message="Only one of the parameters", + allow_retry=False, + vars=[], + cause="输入参数过多,存在互斥参数", + solution="检查并移除多余的请求参数", +) + +MINT_INPUT_NOTHING_ERROR = InternalErrorDef( + name="mint.InputNothingError", + external=INVALID_REQUEST, + message="Either", + allow_retry=False, + vars=[], + cause="缺少必要的输入参数", + solution="确保请求中包含必要的输入参数", +) + +MINT_PROMPT_ERROR = InternalErrorDef( + name="mint.PromptError", + external=INVALID_REQUEST, + message="Missing prompt in input or type of prompt is not string", + allow_retry=False, + vars=[], + cause="缺少 prompt 输入或 prompt 类型错误", + solution="确保 prompt 字段存在且为字符串类型", +) + +MINT_USE_RAW_PROMPT_ERROR = InternalErrorDef( + name="mint.UseRawPromptError", + external=INVALID_REQUEST, + message=( + "When using prompt input, the use_raw_prompt parameter must be " + "set to true." + ), + allow_retry=False, + vars=[], + cause="使用 prompt 输入时需开启 use_raw_prompt", + solution="将 use_raw_prompt 参数设置为 true", +) + +MINT_NON_STREAM_TIMEOUT_ERROR = InternalErrorDef( + name="mint.NonStreamTimeoutError", + external=INVALID_REQUEST, + message=( + "Model generation timed out. Please switch to streaming mode " + "for your request." + ), + allow_retry=False, + vars=[], + cause="非流式模式下模型生成超时", + solution="切换为流式调用后重试", +) + +MINT_SEARCH_PARAMETER_ERROR = InternalErrorDef( + name="mint.SearchParameterError", + external=INVALID_REQUEST, + message="InvalidParameter from input", + allow_retry=False, + vars=[], + cause="搜索参数配置错误", + solution="检查搜索相关参数是否符合文档要求", +) + +MINT_ENABLE_SEARCH_TYPE_ERROR = InternalErrorDef( + name="mint.EnableSearchTypeError", + external=INVALID_REQUEST, + message="Type of enable_search should be bool", + allow_retry=False, + vars=[], + cause="enable_search 参数类型错误", + solution="确保 enable_search 为 bool 类型", +) + +MINT_MODEL_NOT_SUPPORT_SEARCH = InternalErrorDef( + name="mint.ModelNotSupportSearch", + external=INVALID_REQUEST, + message="This model does not support enable_search.", + allow_retry=False, + vars=[], + cause="当前模型不支持搜索功能", + solution="使用支持搜索的模型或关闭搜索功能", +) + +MINT_UNSUPPORTED_LANGUAGE_TYPE = InternalErrorDef( + name="mint.UnsupportedLanguageType", + external=INVALID_REQUEST, + message="Language code", + allow_retry=False, + vars=[], + cause="不支持当前设置的语种", + solution="检查 language 参数是否在支持列表中", +) + +MINT_NOT_ALLOW_SYSTEM_INPUT_ERROR = InternalErrorDef( + name="mint.NotAllowSystemInputError", + external=INVALID_REQUEST, + message=( + "Input with the role system is temporarily not allowed for " + "this model." + ), + allow_retry=False, + vars=[], + cause="当前模型不允许 system 角色输入", + solution="移除 messages 中的 system 消息", +) + +MINT_PARAM_NOT_SUPPORT_ERROR = InternalErrorDef( + name="mint.ParamNotSupportError", + external=INVALID_REQUEST, + message="The parameters {} is not supported.", + allow_retry=False, + vars=[], + cause="当前模型不支持该参数", + solution="检查模型文档,移除不支持的参数", +) + +MINT_MAX_TOKENS_NOT_SUPPORT_ERROR = InternalErrorDef( + name="mint.MaxTokensNotSupportError", + external=INVALID_REQUEST, + message=( + "Parameter max_tokens is not supported for this model. " + "Please use max_completion_tokens instead." + ), + allow_retry=False, + vars=[], + cause="当前模型不支持 max_tokens 参数", + solution="移除 max_tokens 参数或使用支持的模型", +) + +MINT_INVALID_VOICE_ERROR = InternalErrorDef( + name="mint.InvalidVoiceError", + external=INVALID_REQUEST, + message="Invalid voice parameter", + allow_retry=False, + vars=[], + cause="voice 参数值不合法", + solution="检查 voice 参数是否在支持列表中", +) + +MINT_INVALID_INSTRUCTION_LENGTH_ERROR = InternalErrorDef( + name="mint.InvalidInstructionLengthError", + external=INVALID_REQUEST, + message="TTS instruction exceeds maximum allowed length.", + allow_retry=False, + vars=[], + cause="instruction 参数长度超出限制", + solution="缩短 instruction 内容", +) + +MINT_FRESHNESS_TYPE_ERROR = InternalErrorDef( + name="mint.FreshnessTypeError", + external=INVALID_REQUEST, + message="Parameter freshness must be of type", + allow_retry=False, + vars=[], + cause="freshness 参数类型错误", + solution="确保 freshness 参数为合法类型", +) + +MINT_TOOL_CHOICE_SCHEMA_ERROR = InternalErrorDef( + name="mint.ToolChoiceSchemaError", + external=INVALID_REQUEST, + message="Invalid value for tool_choice. Only named tools,", + allow_retry=False, + vars=[], + cause="tool_choice 参数格式错误", + solution="检查 tool_choice 格式是否符合文档要求", +) + +MINT_INPUT_TOOL_CALL_REPETITION_ERROR = InternalErrorDef( + name="mint.InputToolCallRepetitionError", + external=INVALID_REQUEST, + message=( + "Repetitive tool calls detected in the conversation history. " + "The same tool call with identical name and arguments has been " + "repeated across multiple consecutive rounds. Please modify your " + "request or adjust the tool call arguments to avoid infinite loops." + ), + allow_retry=False, + vars=[], + cause="tool_calls 中存在重复调用", + solution="检查并移除重复的 tool_call", +) + +MINT_JSON_RESPONSE_OUTPUT_ABNORMAL_ERROR = InternalErrorDef( + name="mint.JsonResponseOutputAbnormalError", + external=INVALID_REQUEST, + message=( + "Model output became abnormal while generating a JSON response " + "for response_format. The generation was aborted because the " + "partial output may be incomplete or invalid JSON. Please retry " + "the request or adjust your prompt or JSON schema." + ), + allow_retry=False, + vars=[], + cause="JSON 响应输出异常", + solution="检查 response_format 配置后重试", +) + +MINT_USER_DEFINED_FUNCTION_ERROR = InternalErrorDef( + name="mint.UserDefinedFunctionError", + external=INVALID_REQUEST, + message=( + "Please provide the function result for the user-defined function." + ), + allow_retry=False, + vars=[], + cause="用户自定义函数执行异常", + solution="检查函数实现逻辑后重试", +) + +MINT_AGENT_TOOL_EXECUTION_TIMEOUT_ERROR = InternalErrorDef( + name="mint.AgentToolExecutionTimeoutError", + external=INVALID_REQUEST, + message="Agent tool execution timeout.", + allow_retry=False, + vars=[], + cause="Agent 工具执行超时", + solution="简化任务或稍后重试", +) + +MINT_GET_TOOL_CHOICE_NAME_ERROR = InternalErrorDef( + name="mint.GetToolChoiceNameError", + external=INTERNAL_ERROR, + message="Fail to get tool choice name, the error is {}", + allow_retry=True, + vars=[], + cause="获取 tool_choice 名称时发生内部错误", + solution="检查 tools 配置后重试", +) + +MINT_M_M_G_P_T3_ITEM_ERROR = InternalErrorDef( + name="mint.MMGPT3ItemError", + external=INVALID_REQUEST, + message="The item of content should be a message of a certain modal", + allow_retry=False, + vars=[], + cause="多模态 GPT3 消息元素错误", + solution="检查消息中各元素的格式", +) + +MINT_INVALID_TEXT_ERROR = InternalErrorDef( + name="mint.InvalidTextError", + external=INVALID_REQUEST, + message="The special words {} is not allowed in input messages", + allow_retry=False, + vars=[], + cause="文本内容不合法", + solution="检查输入文本内容", +) + +MINT_MIN_PIXELS_RANGE_ERROR = InternalErrorDef( + name="mint.MinPixelsRangeError", + external=INVALID_REQUEST, + message="Parameter min_pixels must be greater than or equal to {}", + allow_retry=False, + vars=[], + cause="min_pixels 参数超出合法范围", + solution="将 min_pixels 调整到合法范围内", +) + +MINT_MAX_PIXELS_RANGE_ERROR = InternalErrorDef( + name="mint.MaxPixelsRangeError", + external=INVALID_REQUEST, + message="Parameter max_pixels must be smaller than or equal to {}", + allow_retry=False, + vars=[], + cause="max_pixels 参数超出合法范围", + solution="将 max_pixels 调整到合法范围内", +) + +MINT_TOTAL_PIXELS_RANGE_ERROR = InternalErrorDef( + name="mint.TotalPixelsRangeError", + external=INVALID_REQUEST, + message="Parameter total_pixels must be smaller than or equal to {}", + allow_retry=False, + vars=[], + cause="total_pixels 参数超出合法范围", + solution="将 total_pixels 调整到合法范围内", +) + +MINT_MAX_FRAME_RANGE_ERROR = InternalErrorDef( + name="mint.MaxFrameRangeError", + external=INVALID_REQUEST, + message="Parameter max_frames must be smaller than or equal to {}", + allow_retry=False, + vars=[], + cause="max_frames 参数超出合法范围", + solution="将 max_frames 调整到合法范围内", +) + +MINT_MIN_MAX_PIXELS_RANGE_ERROR = InternalErrorDef( + name="mint.MinMaxPixelsRangeError", + external=INVALID_REQUEST, + message="Parameter max_pixels be greater than or equal to min_pixels", + allow_retry=False, + vars=[], + cause="max_pixels 应大于等于 min_pixels", + solution="确保 max_pixels >= min_pixels", +) + +MINT_FPS_RANGE_ERROR = InternalErrorDef( + name="mint.FpsRangeError", + external=INVALID_REQUEST, + message="Range of fps should be [0.1, 10]", + allow_retry=False, + vars=[], + cause="fps 参数超出合法范围", + solution="将 fps 调整到合法范围内", +) + +MINT_NON_VISION_PIXELS_ERROR = InternalErrorDef( + name="mint.NonVisionPixelsError", + external=INVALID_REQUEST, + message=( + "For non-vision objects, the min_pixels or max_pixels parameter " + "was input." + ), + allow_retry=False, + vars=[], + cause="非视觉类模型不支持 pixels 参数", + solution="移除 min_pixels/max_pixels 参数", +) + +MINT_NON_VIDEO_FPS_ERROR = InternalErrorDef( + name="mint.NonVideoFpsError", + external=INVALID_REQUEST, + message="For non-vision objects, the fps parameter was input.", + allow_retry=False, + vars=[], + cause="非视频类模型不支持 fps 参数", + solution="移除 fps 参数", +) + +MINT_MODAL_INPUT_ERROR = InternalErrorDef( + name="mint.ModalInputError", + external=INVALID_REQUEST, + message=( + "An incorrect modal {} was entered, which may not be supported " + "by the model or was placed in the wrong position (e.g., in " + "system/assistant)." + ), + allow_retry=False, + vars=[], + cause="输入的 modal 参数不正确", + solution="检查 modal 参数是否匹配模型支持的能力", +) + +MINT_OMNI_MODALITY_ERROR = InternalErrorDef( + name="mint.OmniModalityError", + external=INVALID_REQUEST, + message=( + "Multiple inputs of the same modality or mixed modality inputs " + "are currently not applicable to the omni model." + ), + allow_retry=False, + vars=[], + cause="Omni 模型不支持同模态多输入或混合模态输入", + solution="调整输入模态配置", +) + +MINT_MODALITIES_EMPTY_ERROR = InternalErrorDef( + name="mint.ModalitiesEmptyError", + external=INVALID_REQUEST, + message="The modalities parameter cannot be empty.", + allow_retry=False, + vars=[], + cause="modalities 参数不能为空", + solution="为 modalities 指定至少一种模态", +) + +MINT_MODALITIES_NON_TEXT_ERROR = InternalErrorDef( + name="mint.ModalitiesNonTextError", + external=INVALID_REQUEST, + message="The model does not support the modalities without text.", + allow_retry=False, + vars=[], + cause="当前模型不支持非文本模态", + solution="仅使用 text 模态", +) + +MINT_MODALITIES_DISABLE_AUDIO_ERROR = InternalErrorDef( + name="mint.ModalitiesDisableAudioError", + external=INVALID_REQUEST, + message=( + "The current model does not support the modalities parameter " + "containing audio." + ), + allow_retry=False, + vars=[], + cause="当前模型不支持音频模态", + solution="从 modalities 中移除 audio", +) + +MINT_INVALID_FUNCTION_NAME = InternalErrorDef( + name="mint.InvalidFunctionName", + external=INVALID_REQUEST, + message="Tool names are not allowed to be [{}]", + allow_retry=False, + vars=[], + cause="工具名称不合法", + solution="检查 tool name 是否在允许范围内", +) + +MINT_FUNCTION_ARGUMENTS_INVALID_JSON = InternalErrorDef( + name="mint.FunctionArgumentsInvalidJson", + external=INVALID_REQUEST, + message="Input tool arguments are invalid json.", + allow_retry=False, + vars=[], + cause="tool arguments 不是合法 JSON", + solution="确保 tool arguments 为合法的 JSON 字符串", +) + +MINT_INVALID_FUNCTION_NAME_PREFIX = InternalErrorDef( + name="mint.InvalidFunctionNamePrefix", + external=INVALID_REQUEST, + message="Tool names are not to have the prefix [tongyi]", + allow_retry=False, + vars=[], + cause="工具名称不允许使用 tongyi 前缀", + solution="移除 tool name 中的 tongyi 前缀", +) + +MINT_INVALID_FUNCTION_NAME_LENGTH = InternalErrorDef( + name="mint.InvalidFunctionNameLength", + external=INVALID_REQUEST, + message="The length of the tool name cannot exceed 64.", + allow_retry=False, + vars=[], + cause="工具名称长度不能超过 64 个字符", + solution="缩短 tool name 长度", +) + +MINT_PLUGIN_NOT_SUPPORT_SEARCH = InternalErrorDef( + name="mint.PluginNotSupportSearch", + external=INVALID_REQUEST, + message="Plugins don't support enable_search", + allow_retry=False, + vars=[], + cause="插件不支持 enable_search 功能", + solution="移除 enable_search 参数或不使用插件", +) + +MINT_PLUGIN_NOT_SUPPORT_COMPLETION = InternalErrorDef( + name="mint.PluginNotSupportCompletion", + external=INVALID_REQUEST, + message="Plugins don't support use_raw_prompt", + allow_retry=False, + vars=[], + cause="插件不支持 use_raw_prompt 功能", + solution="移除 use_raw_prompt 参数或不使用插件", +) + +MINT_PLUGIN_NOT_SUPPORT_INCREMENT = InternalErrorDef( + name="mint.PluginNotSupportIncrement", + external=INVALID_REQUEST, + message="Plugins=[{}] don't support incremental_output", + allow_retry=False, + vars=[], + cause="指定插件不支持 incremental_output", + solution="移除 incremental_output 参数或更换插件", +) + +MINT_QWEN_AGENT_NOT_SUPPORT_TOOLS_ERROR = InternalErrorDef( + name="mint.QwenAgentNotSupportToolsError", + external=INVALID_REQUEST, + message=( + "Agent mode does not support tools. You need to either avoid " + "using enable_code_interpreter or avoid using the agent mode " + "with enable_search." + ), + allow_retry=False, + vars=[], + cause="Qwen Agent 不支持当前 tools 配置", + solution="检查并调整 tools 配置", +) + +MINT_QWEN_AGENT_NOT_SUPPORT_C_I_ON_INSTRUCT = InternalErrorDef( + name="mint.QwenAgentNotSupportCIOnInstruct", + external=INVALID_REQUEST, + message=( + "Normal mode does not support Code interpreter. Please set " + "enable_thinking to true." + ), + allow_retry=False, + vars=[], + cause="普通模式不支持 Code Interpreter", + solution="将 enable_thinking 设置为 true", +) + +MINT_QWEN_AGENT_NOT_SUPPORT_WEB_EXTRACTOR_ON_INSTRUCT = InternalErrorDef( + name="mint.QwenAgentNotSupportWebExtractorOnInstruct", + external=INVALID_REQUEST, + message=( + "Normal mode does not support web_extractor. " + "Please set enable_thinking to true." + ), + allow_retry=False, + vars=[], + cause="普通模式不支持 web_extractor", + solution="将 enable_thinking 设置为 true", +) + +MINT_QWEN_AGENT_NOT_SUPPORT_SEARCH_AGENT_ON_THINKING = InternalErrorDef( + name="mint.QwenAgentNotSupportSearchAgentOnThinking", + external=INVALID_REQUEST, + message="Thinking mode does not support the", + allow_retry=False, + vars=[], + cause="thinking 模式下不支持搜索 Agent", + solution="关闭 thinking 或移除搜索 Agent", +) + +MINT_NON_STREAM_MODE_NOT_SUPPORT_C_I = InternalErrorDef( + name="mint.NonStreamModeNotSupportCI", + external=INVALID_REQUEST, + message="Non-streaming mode does not support Code interpreter.", + allow_retry=False, + vars=[], + cause="非流式模式不支持 Code Interpreter", + solution="切换为流式调用", +) + +MINT_NON_STREAM_MODE_NOT_SUPPORT_WEB_EXTRACTOR = InternalErrorDef( + name="mint.NonStreamModeNotSupportWebExtractor", + external=INVALID_REQUEST, + message="Non-streaming mode does not support Web Extractor.", + allow_retry=False, + vars=[], + cause="非流式模式不支持 Web Extractor", + solution="切换为流式调用", +) + +MINT_NON_STREAM_MODE_NOT_SUPPORT_WEB_SEARCH = InternalErrorDef( + name="mint.NonStreamModeNotSupportWebSearch", + external=INVALID_REQUEST, + message="Non-streaming mode does not support Web Search.", + allow_retry=False, + vars=[], + cause="非流式模式不支持 Web Search", + solution="切换为流式调用", +) + +MINT_SEARCH_AGENT_NOT_SUPPORTED_IN_NORMAL_MODE_ERROR = InternalErrorDef( + name="mint.SearchAgentNotSupportedInNormalModeError", + external=INVALID_REQUEST, + message="The current model does not support the", + allow_retry=False, + vars=[], + cause="普通模式下不支持搜索 Agent", + solution="切换到支持搜索 Agent 的模式", +) + +MINT_SEARCH_AGENT_MAX_NOT_SUPPORTED_IN_NORMAL_MODE_ERROR = InternalErrorDef( + name="mint.SearchAgentMaxNotSupportedInNormalModeError", + external=INVALID_REQUEST, + message="The current model mode does not support the", + allow_retry=False, + vars=[], + cause="普通模式下不支持搜索 Agent Max", + solution="切换到支持的模式", +) + +MINT_AGENT_NOT_SUPPORTED_WEB_SEARCH_ERROR = InternalErrorDef( + name="mint.AgentNotSupportedWebSearchError", + external=INVALID_REQUEST, + message="The current model does not support the web_search tool.", + allow_retry=False, + vars=[], + cause="当前模型不支持 web_search 工具", + solution="使用支持 web_search 的模型", +) + +MINT_AGENT_NOT_SUPPORTED_CODE_INTERPRETER_ERROR = InternalErrorDef( + name="mint.AgentNotSupportedCodeInterpreterError", + external=INVALID_REQUEST, + message="The current model does not support the code_interpreter tool.", + allow_retry=False, + vars=[], + cause="当前模型不支持 code_interpreter 工具", + solution="使用支持 code_interpreter 的模型", +) + +MINT_AGENT_NOT_SUPPORTED_WEB_EXTRACTOR_ERROR = InternalErrorDef( + name="mint.AgentNotSupportedWebExtractorError", + external=INVALID_REQUEST, + message="The current model does not support the web_extractor tool.", + allow_retry=False, + vars=[], + cause="当前模型不支持 web_extractor 工具", + solution="使用支持 web_extractor 的模型", +) + +MINT_AGENT_WEB_EXTRACTOR_WITHOUT_SEARCH = InternalErrorDef( + name="mint.AgentWebExtractorWithoutSearch", + external=INVALID_REQUEST, + message="The web_extractor tool must be executed with web_search tool.", + allow_retry=False, + vars=[], + cause="web_extractor 必须配合 web_search 使用", + solution="同时添加 web_search 和 web_extractor 工具", +) + +MINT_AGENT_NOT_SUPPORTED_IMAGE_SEARCH_ERROR = InternalErrorDef( + name="mint.AgentNotSupportedImageSearchError", + external=INVALID_REQUEST, + message="The current model does not support the image_search tool.", + allow_retry=False, + vars=[], + cause="当前模型不支持 image_search 工具", + solution="使用支持 image_search 的模型", +) + +MINT_AGENT_NOT_SUPPORTED_WEB_SEARCH_IMAGE_ERROR = InternalErrorDef( + name="mint.AgentNotSupportedWebSearchImageError", + external=INVALID_REQUEST, + message="The current model does not support the web_search_image tool.", + allow_retry=False, + vars=[], + cause="当前模型不支持 web_search_image 工具", + solution="使用支持 web_search_image 的模型", +) + +MINT_AGENT_NOT_SUPPORTED_FILE_SEARCH_ERROR = InternalErrorDef( + name="mint.AgentNotSupportedFileSearchError", + external=INVALID_REQUEST, + message="The current model does not support the file_search tool.", + allow_retry=False, + vars=[], + cause="当前模型不支持 file_search 工具", + solution="使用支持 file_search 的模型", +) + +MINT_O_M_N_I_MODEL_ONLY_SUPPORT_SEARCH_AGENT = InternalErrorDef( + name="mint.OMNIModelOnlySupportSearchAgent", + external=INVALID_REQUEST, + message="This model only supports search_strategy=", + allow_retry=False, + vars=[], + cause="OMNI 模型仅支持特定 search_strategy", + solution="检查并调整 search_strategy 参数", +) + +MINT_AGENT_API_NOT_ENABLED_ERROR = InternalErrorDef( + name="mint.AgentApiNotEnabledError", + external=INVALID_REQUEST, + message="Agent capabilities are not enabled for the current model.", + allow_retry=False, + vars=[], + cause="当前模型未启用 Agent 能力", + solution="启用 Agent 能力或使用支持 Agent 的模型", +) + +MINT_INVALID_U_R_L = InternalErrorDef( + name="mint.InvalidURL", + external=INVALID_URL, + message="Invalid URL provided in your request.", + allow_retry=False, + vars=[], + cause="请求中的 URL 无效", + solution="检查 URL 格式是否正确", +) + +MINT_V_L_O_C_R_DOCUMENT_PARSING_INVALID_PARAMETER = InternalErrorDef( + name="mint.VLOCRDocumentParsingInvalidParameter", + external=INVALID_FILE, + message="VLOCR document_parsing task invalid parameter: {}.", + allow_retry=False, + vars=[], + cause="VLOCR 文档解析参数无效", + solution="检查文档解析相关参数", +) + +MINT_V_L_O_C_R_DOCUMENT_PARSING_INPUT_TYPE_ERROR = InternalErrorDef( + name="mint.VLOCRDocumentParsingInputTypeError", + external=INVALID_FILE, + message="VLOCR document_parsing task input type error", + allow_retry=False, + vars=[], + cause="VLOCR 文档解析输入类型错误", + solution="检查输入文件类型是否支持", +) + +MINT_V_L_O_C_R_DOCUMENT_PARSING_P_D_F_INPUT_PAGE_COUNT_ERROR = ( + InternalErrorDef( + name="mint.VLOCRDocumentParsingPDFInputPageCountError", + external=INVALID_FILE, + message=( + "VLOCR document_parsing task input PDF is beyond the page " + "count limitation" + ), + allow_retry=False, + vars=[], + cause="VLOCR 文档解析 PDF 页数超限", + solution="减少 PDF 页数后重试", + ) +) + +MINT_V_L_O_C_R_DOCUMENT_PARSING_INPUT_SIZE_ERROR = InternalErrorDef( + name="mint.VLOCRDocumentParsingInputSizeError", + external=INVALID_FILE, + message=( + "VLOCR document_parsing task file size is beyond " + "the size limitation" + ), + allow_retry=False, + vars=[], + cause="VLOCR 文档解析文件大小超限", + solution="减小文件大小后重试", +) + +MINT_V_L_O_C_R_DOCUMENT_PARSING_IMAGE_INPUT_SIZE_ERROR = InternalErrorDef( + name="mint.VLOCRDocumentParsingImageInputSizeError", + external=INVALID_FILE, + message=( + "VLOCR document_parsing task input image file size " + "({}MB) exceeds limit ({}MB)" + ), + allow_retry=False, + vars=[], + cause="VLOCR 文档解析图片文件过大", + solution="压缩图片后重试", +) + +MINT_V_L_O_C_R_P_D_F_PAGE_COUNT_ERROR = InternalErrorDef( + name="mint.VLOCRPDFPageCountError", + external=INVALID_FILE, + message="VLOCR input PDF page count ({}) exceeds limit ({})", + allow_retry=False, + vars=[], + cause="VLOCR 输入 PDF 页数超限", + solution="减少 PDF 页数后重试", +) + +MINT_V_L_O_C_R_P_D_F_INPUT_SIZE_ERROR = InternalErrorDef( + name="mint.VLOCRPDFInputSizeError", + external=INVALID_FILE, + message="VLOCR input PDF file size ({}MB) exceeds limit ({}MB)", + allow_retry=False, + vars=[], + cause="VLOCR 输入 PDF 文件大小超限", + solution="压缩 PDF 文件后重试", +) + +MINT_NOT_SUPPORT_TASK = InternalErrorDef( + name="mint.NotSupportTask", + external=INVALID_REQUEST, + message="The current task is an unsupported task.", + allow_retry=False, + vars=[], + cause="当前任务类型不受支持", + solution="检查 task 参数是否在支持列表中", +) + +MINT_URL_ERROR = InternalErrorDef( + name="mint.UrlError", + external=INVALID_URL, + message=( + "The provided URL does not appear to be valid. " + "Ensure it is correctly formatted." + ), + allow_retry=False, + vars=[], + cause="提供的 URL 格式不正确", + solution="确保 URL 格式正确且可访问", +) + +MINT_DOWNLOAD_ERROR = InternalErrorDef( + name="mint.DownloadError", + external=INVALID_FILE, + message="Failed to download multimodal content", + allow_retry=False, + vars=[], + cause="下载多模态内容失败", + solution="检查资源链接是否有效且可访问", +) + +MINT_FILE_OPEN_ERROR = InternalErrorDef( + name="mint.FileOpenError", + external=INVALID_FILE, + message="The file format is illegal and cannot be opened", + allow_retry=False, + vars=[], + cause="文件格式非法,无法打开", + solution="检查文件格式是否正确", +) + +MINT_FIM_USE_ERROR = InternalErrorDef( + name="mint.FimUseError", + external=INVALID_REQUEST, + message=( + "Please ensure that the enable_fim parameter is set " + "when using the fim function." + ), + allow_retry=False, + vars=[], + cause="使用 FIM 功能时未设置 enable_fim", + solution="将 enable_fim 参数设置为 true", +) + +MINT_IMAGE_OPEN_ERROR = InternalErrorDef( + name="mint.ImageOpenError", + external=INVALID_FILE, + message="The image format is illegal and cannot be opened", + allow_retry=False, + vars=[], + cause="图片格式非法,无法打开", + solution="检查图片格式是否正确(如 JPEG/PNG)", +) + +MINT_ALGO_UNKNOWN_ERROR = InternalErrorDef( + name="mint.AlgoUnknownError", + external=INTERNAL_ERROR, + message="An unknown error occurred during the algorithm execution.", + allow_retry=True, + vars=[], + cause="算法执行过程中发生未知错误", + solution="稍后重试或联系技术支持", +) + +MINT_INPUT_PARAMETER_LIMIT_ERROR = InternalErrorDef( + name="mint.InputParameterLimitError", + external=INVALID_REQUEST, + message="Parameter limit exceeded", + allow_retry=False, + vars=[], + cause="参数超出限制", + solution="检查并调整参数值", +) + +MINT_LANGUAGE_ERROR = InternalErrorDef( + name="mint.LanguageError", + external=INVALID_REQUEST, + message="Language is not supported.", + allow_retry=False, + vars=[], + cause="不支持当前设置的语种", + solution="检查 language 参数是否在支持列表中", +) + +MINT_HEADER_VERIFY_ERROR = InternalErrorDef( + name="mint.HeaderVerifyError", + external=INVALID_REQUEST, + message="HeaderVerifyError", + allow_retry=False, + vars=[], + cause="请求头校验失败", + solution="检查请求头参数是否正确", +) + +MINT_BASE_DATA_INSPECTION_ERROR = InternalErrorDef( + name="mint.BaseDataInspectionError", + external=CONTENT_POLICY_VIOLATION, + message="BaseDataInspectionError", + allow_retry=False, + vars=[], + cause="内容安全审核基类错误", + solution="检查输入/输出内容是否符合安全规范", +) + +MINT_UNSAFE_INPUT_CONTEXT_ERROR = InternalErrorDef( + name="mint.UnsafeInputContextError", + external=CONTENT_POLICY_VIOLATION, + message="Input {} data may contain inappropriate content.", + allow_retry=False, + vars=[], + cause="输入内容可能包含不当内容", + solution="检查并修改输入内容后重试", +) + +MINT_UNSAFE_OUTPUT_CONTEXT_ERROR = InternalErrorDef( + name="mint.UnsafeOutputContextError", + external=CONTENT_POLICY_VIOLATION, + message="Output data may contain inappropriate content.", + allow_retry=False, + vars=[], + cause="输出内容可能包含不当内容", + solution="调整输入内容或稍后重试", +) + +MINT_DATA_INSPECTION_FAILED = InternalErrorDef( + name="mint.DataInspectionFailed", + external=CONTENT_POLICY_VIOLATION, + message="Output data may contain inappropriate content.", + allow_retry=False, + vars=[], + cause="输出内容可能包含不当内容", + solution="调整输入内容或稍后重试", +) + +MINT_INPUT_HEADER_INVALID = InternalErrorDef( + name="mint.InputHeaderInvalid", + external=INVALID_REQUEST, + message="Input header required by data inspection is invalid", + allow_retry=False, + vars=[], + cause="内容安全审核所需的请求头无效", + solution="检查数据审核相关的请求头参数", +) + +MINT_UNSAFE_INVALID_DATA_ERROR = InternalErrorDef( + name="mint.UnsafeInvalidDataError", + external=CONTENT_POLICY_VIOLATION, + message="The input multimodal data has issues.", + allow_retry=False, + vars=[], + cause="输入的多模态数据存在问题", + solution="检查输入的多模态数据格式和内容", +) + +MINT_UNSUPPORTED_FORMAT_DATA_ERROR = InternalErrorDef( + name="mint.UnsupportedFormatDataError", + external=CONTENT_POLICY_VIOLATION, + message=( + "The media format is not supported or incorrect for " + "the data inspection." + ), + allow_retry=False, + vars=[], + cause="媒体格式不被支持或格式错误", + solution="转换为支持的媒体格式后重试", +) + +MINT_DOWNLOAD_FAILED_ERROR = InternalErrorDef( + name="mint.DownloadFailedError", + external=INVALID_FILE, + message=( + "Failed to find the requested media resource during " + "the data inspection process." + ), + allow_retry=False, + vars=[], + cause="内容审核过程中无法找到请求的媒体资源", + solution="检查资源链接是否有效", +) + +MINT_DATA_INSPECTION_INVALID_PARAMETER_ERROR = InternalErrorDef( + name="mint.DataInspectionInvalidParameterError", + external=CONTENT_POLICY_VIOLATION, + message="The input multimodal data has issues.", + allow_retry=False, + vars=[], + cause="输入的多模态数据存在问题", + solution="检查输入的多模态数据", +) + +MINT_DATA_INSPECTION_INVALID_PARAMETER_MSG_ERROR = InternalErrorDef( + name="mint.DataInspectionInvalidParameterMsgError", + external=CONTENT_POLICY_VIOLATION, + message="{}", + allow_retry=False, + vars=[], + cause="数据审核参数错误", + solution="检查数据审核相关参数", +) + +MINT_QWEN_GUARD_PARAMETER_ERROR = InternalErrorDef( + name="mint.QwenGuardParameterError", + external=CONTENT_POLICY_VIOLATION, + message="The data_inspection parameter qwen_guard only supports n=1.", + allow_retry=False, + vars=[], + cause="data_inspection 参数 qwen_guard 仅支持 n=1", + solution="将 n 参数设置为 1", +) + +MINT_RATE_LIMIT_EXCEEDED_ERROR = InternalErrorDef( + name="mint.RateLimitExceededError", + external=RATE_LIMIT_EXCEEDED, + message="Allocated quota exceeded, please increase your quota limit.", + allow_retry=False, + vars=[], + cause="配额超限", + solution="增加配额或等待后重试", +) + +API_CLIENT_ERROR = InternalErrorDef( + name="api.ClientError", + external=INVALID_REQUEST, + message="Client error!", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +API_CLIENT_DISCONNECT = InternalErrorDef( + name="api.ClientDisconnect", + external=INVALID_REQUEST, + message="Client disconnected!", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +API_INVALID_PARAMETER = InternalErrorDef( + name="api.InvalidParameter", + external=MISSING_PARAMETER, + message="Invalid parameter!", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +API_ARREARAGE_PTU_OVER_LIMIT = InternalErrorDef( + name="api.Arrearage.PtuOverLimit", + external=INVALID_REQUEST, + message=( + "Access denied, please make sure your account is in good standing " + "while ptu over limit." + ), + allow_retry=False, + vars=[], + cause="", + solution="", +) + +API_THROTTLING_BURST_RATE = InternalErrorDef( + name="api.Throttling.BurstRate", + external=RATE_LIMIT_EXCEEDED, + message=( + "Request rate increased too quickly. To ensure system stability, " + "please adjust your client logic to scale requests more smoothly " + "over time." + ), + allow_retry=False, + vars=[], + cause="", + solution="", +) + +API_THROTTLING_CONCURRENCY = InternalErrorDef( + name="api.Throttling.Concurrency", + external=CONCURRENCY_LIMIT_EXCEEDED, + message="Too many concurrent requests.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +API_THROTTLING_USER_INPUT_TOKEN = InternalErrorDef( + name="api.Throttling.UserInputToken", + external=RATE_LIMIT_EXCEEDED, + message="User input token tpm over limit.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +API_THROTTLING_USER_THINKING_OUTPUT_TOKEN = InternalErrorDef( + name="api.Throttling.UserThinkingOutputToken", + external=RATE_LIMIT_EXCEEDED, + message="User thinking output token tpm over limit.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +API_THROTTLING_USER_NOTHINKING_OUTPUT_TOKEN = InternalErrorDef( + name="api.Throttling.UserNothinkingOutputToken", + external=RATE_LIMIT_EXCEEDED, + message="User nothinking output token tpm over limit.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +API_THROTTLING_USER_OUTPUT_TOKEN = InternalErrorDef( + name="api.Throttling.UserOutputToken", + external=RATE_LIMIT_EXCEEDED, + message="User output token tpm over limit.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +API_INTERNAL_ERROR = InternalErrorDef( + name="api.InternalError", + external=INTERNAL_ERROR, + message="Internal server error.", + allow_retry=True, + vars=[], + cause="", + solution="", +) + +API_ROUTE_ERROR = InternalErrorDef( + name="api.RouteError", + external=INTERNAL_ERROR, + message="Route error!", + allow_retry=True, + vars=[], + cause="", + solution="", +) + +API_BALANCE_ERROR = InternalErrorDef( + name="api.BalanceError", + external=INTERNAL_ERROR, + message="Balance error.", + allow_retry=True, + vars=[], + cause="", + solution="", +) + +API_INTERNAL_ERROR_CONNECT_TIMEOUT = InternalErrorDef( + name="api.InternalError.ConnectTimeout", + external=INTERNAL_ERROR, + message="Connect timeout.", + allow_retry=True, + vars=[], + cause="", + solution="", +) + +API_INTERNAL_ERROR_SOCKET_TIMEOUT = InternalErrorDef( + name="api.InternalError.SocketTimeout", + external=INTERNAL_ERROR, + message="Socket timeout.", + allow_retry=True, + vars=[], + cause="", + solution="", +) + +API_INTERNAL_ERROR_ENGINE_ERROR = InternalErrorDef( + name="api.InternalError.EngineError", + external=INTERNAL_ERROR, + message="Inference engine error.", + allow_retry=True, + vars=[], + cause="", + solution="", +) + +API_INTERNAL_ERROR_ENGINE_ABORT = InternalErrorDef( + name="api.InternalError.EngineAbort", + external=INTERNAL_ERROR, + message="Inference engine abort.", + allow_retry=True, + vars=[], + cause="", + solution="", +) + +API_SERVICE_UNAVAILABLE = InternalErrorDef( + name="api.ServiceUnavailable", + external=SERVICE_UNAVAILABLE, + message=( + "Too many requests. Your requests are being throttled due to " + "system capacity limits. Please try again later." + ), + allow_retry=False, + vars=[], + cause="", + solution="", +) + +API_ROUTER_INSTANCE_OVERLOADED = InternalErrorDef( + name="api.RouterInstanceOverloaded", + external=SERVICE_UNAVAILABLE, + message="Router backend instance overloaded.", + allow_retry=True, + vars=[], + cause="", + solution="", +) + +API_RESPONSE_TIMEOUT = InternalErrorDef( + name="api.ResponseTimeout", + external=REQUEST_TIMEOUT, + message="Response timeout.", + allow_retry=True, + vars=[], + cause="", + solution="", +) + +API_INTERNAL_ERROR_JSON_PARSE_ERROR = InternalErrorDef( + name="api.InternalError.JsonParseError", + external=INTERNAL_ERROR, + message="Failed to parse json.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +TURBO_INTERNAL_ERROR = InternalErrorDef( + name="turbo.InternalError", + external=INTERNAL_ERROR, + message="Turbo service encountered an internal error.", + allow_retry=True, + vars=[], + cause="", + solution="", +) + +TURBO_UPLOAD_FAILED = InternalErrorDef( + name="turbo.UploadFailed", + external=SERVICE_UNAVAILABLE, + message="File upload to Turbo service failed.", + allow_retry=True, + vars=[], + cause="", + solution="", +) + +TURBO_FILE_UPLOAD_FAILED = InternalErrorDef( + name="turbo.FileUploadFailed", + external=SERVICE_UNAVAILABLE, + message="File upload operation failed.", + allow_retry=True, + vars=[], + cause="", + solution="", +) + +TURBO_RESPONSE_TIMEOUT = InternalErrorDef( + name="turbo.ResponseTimeout", + external=REQUEST_TIMEOUT, + message="Turbo service response timed out.", + allow_retry=True, + vars=[], + cause="", + solution="", +) + +MODEL_TASK_LIST_FULL = InternalErrorDef( + name="model.TaskListFull", + external=INTERNAL_ERROR, + message="Internal error: task list full.", + allow_retry=True, + vars=[], + cause="", + solution="", +) + +MODEL_TIME_OUT_FOR_FIRST_TOKEN = InternalErrorDef( + name="model.TimeOutForFirstToken", + external=INTERNAL_ERROR, + message="Internal error: time out for first token.", + allow_retry=True, + vars=[], + cause="", + solution="", +) + +MODEL_STOP_FROM_ENGINE = InternalErrorDef( + name="model.StopFromEngine", + external=INTERNAL_ERROR, + message="Internal error: stop from engine.", + allow_retry=True, + vars=[], + cause="", + solution="", +) + +MODEL_STOP_ENGINE_PARAM = InternalErrorDef( + name="model.StopEngineParam", + external=INVALID_REQUEST, + message="Request error: stop engine param.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +MODEL_STOP_ENGINE_ERROR = InternalErrorDef( + name="model.StopEngineError", + external=INTERNAL_ERROR, + message="Internal error: stop engine.", + allow_retry=True, + vars=[], + cause="", + solution="", +) + +MODEL_ABORT = InternalErrorDef( + name="model.Abort", + external=INTERNAL_ERROR, + message="Internal error: abort.", + allow_retry=True, + vars=[], + cause="", + solution="", +) + +MODEL_PREFILL_PREEMPTED = InternalErrorDef( + name="model.PrefillPreempted", + external=INTERNAL_ERROR, + message="Internal error: prefill preempted.", + allow_retry=True, + vars=[], + cause="", + solution="", +) + +MODEL_DECODE_PREEMPTED = InternalErrorDef( + name="model.DecodePreempted", + external=INTERNAL_ERROR, + message="Internal error: decode preempted.", + allow_retry=True, + vars=[], + cause="", + solution="", +) + +MODEL_STOP_TIMEOUT = InternalErrorDef( + name="model.StopTimeout", + external=INTERNAL_ERROR, + message="Internal error: stop timeout.", + allow_retry=True, + vars=[], + cause="", + solution="", +) + +MODEL_INNER_ENGINE_STUCK = InternalErrorDef( + name="model.InnerEngineStuck", + external=INTERNAL_ERROR, + message="Internal error: inner engine stuck.", + allow_retry=True, + vars=[], + cause="", + solution="", +) + +MODEL_INNER_ENGINE_ERROR = InternalErrorDef( + name="model.InnerEngineError", + external=INTERNAL_ERROR, + message="Internal error: inner engine.", + allow_retry=True, + vars=[], + cause="", + solution="", +) + +MODEL_INNER_GENERATE_EXIT = InternalErrorDef( + name="model.InnerGenerateExit", + external=INTERNAL_ERROR, + message="Internal error: inner generate exit.", + allow_retry=True, + vars=[], + cause="", + solution="", +) + +MODEL_STOP_ENGINE_INVALID_OUTPUT = InternalErrorDef( + name="model.StopEngineInvalidOutput", + external=INTERNAL_ERROR, + message="Internal error: stop engine invalid output.", + allow_retry=True, + vars=[], + cause="", + solution="", +) + +# -- Client Internal Errors ---------------------------------------- + +AGENTIC_RL_INVALID_API_KEY = InternalErrorDef( + name="agentic_rl.InvalidApiKey", + external=AUTH_FAILED, + message="Invalid API key configuration for AgenticRL.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +AGENTIC_RL_FUNCTION_REGISTRATION_FAILED = InternalErrorDef( + name="agentic_rl.FunctionRegistrationFailed", + external=INTERNAL_ERROR, + message="Function component registration failed.", + allow_retry=True, + vars=[], + cause="", + solution="", +) + +AGENTIC_RL_DATASETS_UPLOAD_FAILED = InternalErrorDef( + name="agentic_rl.DatasetsUploadFailed", + external=INTERNAL_ERROR, + message="Datasets upload failed.", + allow_retry=True, + vars=[], + cause="", + solution="", +) + +AGENTIC_RL_DUPLICATE_FUNCTION_NAMES = InternalErrorDef( + name="agentic_rl.DuplicateFunctionNames", + external=INVALID_REQUEST, + message="Duplicate function names detected.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +AGENTIC_RL_JOB_SUBMISSION_FAILED = InternalErrorDef( + name="agentic_rl.JobSubmissionFailed", + external=INTERNAL_ERROR, + message="Job submission failed.", + allow_retry=True, + vars=[], + cause="", + solution="", +) + +AGENTIC_RL_WORKFLOW_FAILED = InternalErrorDef( + name="agentic_rl.WorkflowFailed", + external=INTERNAL_ERROR, + message="RL tuning workflow failed.", + allow_retry=True, + vars=[], + cause="", + solution="", +) + +AGENTIC_RL_UNSUPPORTED_FUNCTION_TYPE = InternalErrorDef( + name="agentic_rl.UnsupportedFunctionType", + external=INVALID_REQUEST, + message="Unsupported function type.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +AGENTIC_RL_FUNCTION_TEST_FAILED = InternalErrorDef( + name="agentic_rl.FunctionTestFailed", + external=INTERNAL_ERROR, + message="Function test failed.", + allow_retry=True, + vars=[], + cause="", + solution="", +) + +AGENTIC_RL_FUNCTION_TEST_TIMEOUT = InternalErrorDef( + name="agentic_rl.FunctionTestTimeout", + external=REQUEST_TIMEOUT, + message="Function test timed out.", + allow_retry=True, + vars=[], + cause="", + solution="", +) + +# -- AgenticRL Exception Class Errors --------------------------------- + +AGENTIC_RL_INPUT_ERROR = InternalErrorDef( + name="agentic_rl.InputError", + external=INVALID_REQUEST, + message="Invalid input data detected during validation.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +AGENTIC_RL_OUTPUT_ERROR = InternalErrorDef( + name="agentic_rl.OutputError", + external=INTERNAL_ERROR, + message="Service response failed output validation checks.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +AGENTIC_RL_BASE_CONNECTION_ERROR = InternalErrorDef( + name="agentic_rl.BaseConnectionError", + external=INTERNAL_ERROR, + message="Connection-related error occurred.", + allow_retry=True, + vars=[], + cause="", + solution="", +) + +AGENTIC_RL_OSS_CONNECTION_ERROR = InternalErrorDef( + name="agentic_rl.OSSConnectionError", + external=INTERNAL_ERROR, + message="Connecting to OSS storage service failed.", + allow_retry=True, + vars=[], + cause="", + solution="", +) + +AGENTIC_RL_OSS_UPLOAD_ERROR = InternalErrorDef( + name="agentic_rl.OSSUploadError", + external=INTERNAL_ERROR, + message="File upload operation to OSS failed.", + allow_retry=True, + vars=[], + cause="", + solution="", +) + +AGENTIC_RL_DEPLOYMENT_ERROR = InternalErrorDef( + name="agentic_rl.DeploymentError", + external=INTERNAL_ERROR, + message="Deployment-related error occurred.", + allow_retry=True, + vars=[], + cause="", + solution="", +) + +AGENTIC_RL_REGISTRATION_ERROR = InternalErrorDef( + name="agentic_rl.RegistrationError", + external=INTERNAL_ERROR, + message="Function registration failed in the deployment system.", + allow_retry=True, + vars=[], + cause="", + solution="", +) + +AGENTIC_RL_FUNCTION_LOAD_ERROR = InternalErrorDef( + name="agentic_rl.FunctionLoadError", + external=INTERNAL_ERROR, + message="Loading a registered function into runtime failed.", + allow_retry=True, + vars=[], + cause="", + solution="", +) + +AGENTIC_RL_INSTANCE_WARMUP_ERROR = InternalErrorDef( + name="agentic_rl.InstanceWarmupError", + external=INTERNAL_ERROR, + message="Function instance health check failed after deployment.", + allow_retry=True, + vars=[], + cause="", + solution="", +) + +AGENTIC_RL_INSTANCE_QUERY_ERROR = InternalErrorDef( + name="agentic_rl.InstanceQueryError", + external=INTERNAL_ERROR, + message="Querying function instance status failed.", + allow_retry=True, + vars=[], + cause="", + solution="", +) + +AGENTIC_RL_FUNCTION_LAYER_ERROR = InternalErrorDef( + name="agentic_rl.FunctionLayerError", + external=INTERNAL_ERROR, + message="Creating a layer of function failed.", + allow_retry=True, + vars=[], + cause="", + solution="", +) + +AGENTIC_RL_DATASETS_ERROR = InternalErrorDef( + name="agentic_rl.DatasetsError", + external=INTERNAL_ERROR, + message="Update datasets failed in the deployment system.", + allow_retry=True, + vars=[], + cause="", + solution="", +) + +AGENTIC_RL_VALIDATION_ERROR = InternalErrorDef( + name="agentic_rl.ValidationError", + external=INVALID_REQUEST, + message="Data validation failed.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +AGENTIC_RL_CONFIGURATION_ERROR = InternalErrorDef( + name="agentic_rl.ConfigurationError", + external=INVALID_REQUEST, + message="Invalid system configuration detected.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +AGENTIC_RL_BASE_PERMISSION_ERROR = InternalErrorDef( + name="agentic_rl.BasePermissionError", + external=PERMISSION_DENIED, + message="Operation lacks required permissions.", + allow_retry=False, + vars=[], + cause="", + solution="", +) + +AGENTIC_RL_IO_ERROR_WITH_CODE = InternalErrorDef( + name="agentic_rl.IOErrorWithCode", + external=INTERNAL_ERROR, + message="General I/O operation failure.", + allow_retry=True, + vars=[], + cause="", + solution="", +) + + +# -- Catalogs ------------------------------------------------------ +PUBLIC_ERRORS = [ + INVALID_REQUEST, + MISSING_PARAMETER, + CONTENT_POLICY_VIOLATION, + INVALID_URL, + INVALID_FILE, + AUTH_FAILED, + INVALID_API_KEY, + PERMISSION_DENIED, + RESOURCE_NOT_FOUND, + REQUEST_TOO_LARGE, + RATE_LIMIT_EXCEEDED, + CONCURRENCY_LIMIT_EXCEEDED, + INSUFFICIENT_QUOTA, + INTERNAL_ERROR, + SERVICE_UNAVAILABLE, + REQUEST_TIMEOUT, +] +INTERNAL_ERRORS = [ + GATEWAY_INVALID_REQUEST, + GATEWAY_MISSING_INPUT, + GATEWAY_INVALID_PARAMETER, + GATEWAY_INVALID_REQUEST_BODY, + GATEWAY_REQUEST_TOO_LARGE, + GATEWAY_RATE_LIMIT_EXCEEDED, + GATEWAY_BURST_RATE_EXCEEDED, + GATEWAY_CONCURRENCY_LIMIT_EXCEEDED, + GATEWAY_QUOTA_EXCEEDED, + GATEWAY_ACCOUNT_IN_ARREARS, + GATEWAY_PERMISSION_DENIED, + GATEWAY_MODEL_ACCESS_DENIED, + GATEWAY_RESOURCE_NOT_FOUND, + GATEWAY_MODEL_NOT_FOUND, + GATEWAY_INVALID_API_KEY, + GATEWAY_INVALID_API_TOKEN, + GATEWAY_WORKSPACE_ACCESS_DENIED, + GATEWAY_ENDPOINT_ACCESS_DENIED, + GATEWAY_I_P_ACCESS_DENIED, + GATEWAY_USER_BLACKLISTED, + GATEWAY_ENTITY_UNPURCHASED, + GATEWAY_TOKEN_PLAN_EXPIRED, + GATEWAY_TOKEN_PLAN_QUOTA_EXHAUSTED, + GATEWAY_FREE_QUOTA_EXHAUSTED, + GATEWAY_ILLEGAL_ENDPOINT, + GATEWAY_DATA_INSPECTION_FAILED, + GATEWAY_GATEWAY_TIMEOUT, + GATEWAY_O_S_S_UPLOAD_FAILED, + GATEWAY_RESOURCE_ACCESS_DENIED, + GATEWAY_ASYNC_SYNC_DENIED, + GATEWAY_INTERNAL_ERROR, + GATEWAY_SERVICE_SITE_UNSUPPORTED, + GATEWAY_APP_ACCESS_DENIED, + GATEWAY_HEADER_ACCESS_DENIED, + GATEWAY_RESOURCE_NOT_EXIST, + GATEWAY_REQUEST_TIMEOUT, + APISERVER_UNSUPPORTED_MODEL, + APISERVER_UNSUPPORTED_OPERATION, + APISERVER_UNSUPPORTED_PARAMETER, + APISERVER_INVALID_PARAMETER_COMBINATION, + APISERVER_CONTENT_POLICY_VIOLATION, + APISERVER_INVALID_U_R_L, + APISERVER_FILE_DOWNLOAD_FAILED, + APISERVER_INVALID_FILE, + APISERVER_UNSUPPORTED_FILE_FORMAT, + APISERVER_FILE_TOO_LARGE, + APISERVER_INVALID_AUDIO, + APISERVER_TIMEOUT, + MINT_INTERNAL_PROCESS_ERROR, + MINT_JSON_SERIALIZATION_ERROR, + MINT_OUTPUT_PAYLOAD_SCHEMA_ERROR, + MINT_MODEL_SERVING_ERROR, + MINT_MODEL_SERVING_WITH_DETAIL_ERROR, + MINT_MODEL_CALL_ERROR, + MINT_INTERNAL_PREPROCESS_ERROR, + MINT_INTERNAL_POSTPROCESS_ERROR, + MINT_WANX_CALLING_ERROR, + MINT_INTERNAL_PLUG_ERROR, + MINT_PLUGIN_RESULT_IS_NONE, + MINT_PLUGIN_RESULT_PARSE_ERROR, + MINT_PLUGIN_EXECUTION_RESULT_FORMAT_ERROR, + MINT_SEARCH_RESULT_FORMAT_ERROR, + MINT_INTERNAL_SEARCH_ERROR, + MINT_INTERNAL_AGENT_ERROR, + MINT_AGENT_SERVICE_GET_RESULT_ERROR, + MINT_GENERATOR_RESULT_J_S_O_N_DECODE_ERROR, + MINT_SERVICE_UNAVAILABLE_ERROR, + MINT_RESPONSE_TIMEOUT_ERROR, + MINT_V_L_O_C_R_DOCUMENT_PARSING_TIMEOUT_ERROR, + MINT_INVALID_PARAMETER, + MINT_MISSING_INPUT, + MINT_INPUT_FORMAT_ERROR, + MINT_MODEL_INPUT_FORMAT_ERROR, + MINT_INPUT_UNKNOWN_ERROR, + MINT_INPUT_PREPROCESS_ERROR, + MINT_ECC_ERROR, + MINT_MESSAGE_ERROR, + MINT_MESSAGE_CONTENT_FIELD_REQUIRED_ERROR, + MINT_MESSAGE_CONTENT_FIELD_ERROR, + MINT_MESSAGE_CONTENT_ITEM_TYPE_ERROR, + MINT_MESSAGE_CONTENT_MULTIMODAL_UNSUPPORTED_ERROR, + MINT_MESSAGE_FUNCTION_FIELD_ERROR, + MINT_LAST_MESSAGE_MUST_BE_USER, + MINT_SINGLE_SYSTEM_ROLE_ERROR, + MINT_MESSAGE_CONTAINS_SURROGATE_ERROR, + MINT_INPUT_MESSAGES_TOO_LONG, + MINT_ROLE_ERROR, + MINT_INPUT_MESSAGE_FORMAT_ERROR, + MINT_TEMPERATURE_TYPE_ERROR, + MINT_TOP_K_RANGE_ERROR, + MINT_TOP_P_RANGE_ERROR, + MINT_INPUT_CONTENT_TOO_LONG, + MINT_REPETITION_PENALTY_TYPE_ERROR, + MINT_HEAD_RESPONSE_PER_TOKENS_TYPE_ERROR, + MINT_HEAD_RESPONSE_PER_TOKENS_RANGE_ERROR, + MINT_ENABLE_THINKING_ERROR, + MINT_THINKING_MODE_N_ERROR, + MINT_SEED_TYPE_ERROR, + MINT_PRESENCE_PENALTY_RANGE_ERROR, + MINT_FREQUENCY_PENALTY_RANGE_ERROR, + MINT_N_RANGE_ERROR, + MINT_INCREMENTAL_OUTPUT_ERROR, + MINT_INCREMENTAL_OUTPUT_VALUE_ERROR, + MINT_ONLY_SUPPORT_INCREMENTAL, + MINT_FORCE_INCREMENTAL_OUTPUT_ERROR, + MINT_OMNI_INCREMENTAL_OUTPUT_ERROR, + MINT_INPUT_TOO_MANY_ARGUMENTS_ERROR, + MINT_INPUT_NOTHING_ERROR, + MINT_PROMPT_ERROR, + MINT_USE_RAW_PROMPT_ERROR, + MINT_NON_STREAM_TIMEOUT_ERROR, + MINT_SEARCH_PARAMETER_ERROR, + MINT_ENABLE_SEARCH_TYPE_ERROR, + MINT_MODEL_NOT_SUPPORT_SEARCH, + MINT_UNSUPPORTED_LANGUAGE_TYPE, + MINT_NOT_ALLOW_SYSTEM_INPUT_ERROR, + MINT_PARAM_NOT_SUPPORT_ERROR, + MINT_MAX_TOKENS_NOT_SUPPORT_ERROR, + MINT_INVALID_VOICE_ERROR, + MINT_INVALID_INSTRUCTION_LENGTH_ERROR, + MINT_FRESHNESS_TYPE_ERROR, + MINT_TOOL_CHOICE_SCHEMA_ERROR, + MINT_INPUT_TOOL_CALL_REPETITION_ERROR, + MINT_JSON_RESPONSE_OUTPUT_ABNORMAL_ERROR, + MINT_USER_DEFINED_FUNCTION_ERROR, + MINT_AGENT_TOOL_EXECUTION_TIMEOUT_ERROR, + MINT_GET_TOOL_CHOICE_NAME_ERROR, + MINT_M_M_G_P_T3_ITEM_ERROR, + MINT_INVALID_TEXT_ERROR, + MINT_MIN_PIXELS_RANGE_ERROR, + MINT_MAX_PIXELS_RANGE_ERROR, + MINT_TOTAL_PIXELS_RANGE_ERROR, + MINT_MAX_FRAME_RANGE_ERROR, + MINT_MIN_MAX_PIXELS_RANGE_ERROR, + MINT_FPS_RANGE_ERROR, + MINT_NON_VISION_PIXELS_ERROR, + MINT_NON_VIDEO_FPS_ERROR, + MINT_MODAL_INPUT_ERROR, + MINT_OMNI_MODALITY_ERROR, + MINT_MODALITIES_EMPTY_ERROR, + MINT_MODALITIES_NON_TEXT_ERROR, + MINT_MODALITIES_DISABLE_AUDIO_ERROR, + MINT_INVALID_FUNCTION_NAME, + MINT_FUNCTION_ARGUMENTS_INVALID_JSON, + MINT_INVALID_FUNCTION_NAME_PREFIX, + MINT_INVALID_FUNCTION_NAME_LENGTH, + MINT_PLUGIN_NOT_SUPPORT_SEARCH, + MINT_PLUGIN_NOT_SUPPORT_COMPLETION, + MINT_PLUGIN_NOT_SUPPORT_INCREMENT, + MINT_QWEN_AGENT_NOT_SUPPORT_TOOLS_ERROR, + MINT_QWEN_AGENT_NOT_SUPPORT_C_I_ON_INSTRUCT, + MINT_QWEN_AGENT_NOT_SUPPORT_WEB_EXTRACTOR_ON_INSTRUCT, + MINT_QWEN_AGENT_NOT_SUPPORT_SEARCH_AGENT_ON_THINKING, + MINT_NON_STREAM_MODE_NOT_SUPPORT_C_I, + MINT_NON_STREAM_MODE_NOT_SUPPORT_WEB_EXTRACTOR, + MINT_NON_STREAM_MODE_NOT_SUPPORT_WEB_SEARCH, + MINT_SEARCH_AGENT_NOT_SUPPORTED_IN_NORMAL_MODE_ERROR, + MINT_SEARCH_AGENT_MAX_NOT_SUPPORTED_IN_NORMAL_MODE_ERROR, + MINT_AGENT_NOT_SUPPORTED_WEB_SEARCH_ERROR, + MINT_AGENT_NOT_SUPPORTED_CODE_INTERPRETER_ERROR, + MINT_AGENT_NOT_SUPPORTED_WEB_EXTRACTOR_ERROR, + MINT_AGENT_WEB_EXTRACTOR_WITHOUT_SEARCH, + MINT_AGENT_NOT_SUPPORTED_IMAGE_SEARCH_ERROR, + MINT_AGENT_NOT_SUPPORTED_WEB_SEARCH_IMAGE_ERROR, + MINT_AGENT_NOT_SUPPORTED_FILE_SEARCH_ERROR, + MINT_O_M_N_I_MODEL_ONLY_SUPPORT_SEARCH_AGENT, + MINT_AGENT_API_NOT_ENABLED_ERROR, + MINT_INVALID_U_R_L, + MINT_V_L_O_C_R_DOCUMENT_PARSING_INVALID_PARAMETER, + MINT_V_L_O_C_R_DOCUMENT_PARSING_INPUT_TYPE_ERROR, + MINT_V_L_O_C_R_DOCUMENT_PARSING_P_D_F_INPUT_PAGE_COUNT_ERROR, + MINT_V_L_O_C_R_DOCUMENT_PARSING_INPUT_SIZE_ERROR, + MINT_V_L_O_C_R_DOCUMENT_PARSING_IMAGE_INPUT_SIZE_ERROR, + MINT_V_L_O_C_R_P_D_F_PAGE_COUNT_ERROR, + MINT_V_L_O_C_R_P_D_F_INPUT_SIZE_ERROR, + MINT_NOT_SUPPORT_TASK, + MINT_URL_ERROR, + MINT_DOWNLOAD_ERROR, + MINT_FILE_OPEN_ERROR, + MINT_FIM_USE_ERROR, + MINT_IMAGE_OPEN_ERROR, + MINT_ALGO_UNKNOWN_ERROR, + MINT_INPUT_PARAMETER_LIMIT_ERROR, + MINT_LANGUAGE_ERROR, + MINT_HEADER_VERIFY_ERROR, + MINT_BASE_DATA_INSPECTION_ERROR, + MINT_UNSAFE_INPUT_CONTEXT_ERROR, + MINT_UNSAFE_OUTPUT_CONTEXT_ERROR, + MINT_DATA_INSPECTION_FAILED, + MINT_INPUT_HEADER_INVALID, + MINT_UNSAFE_INVALID_DATA_ERROR, + MINT_UNSUPPORTED_FORMAT_DATA_ERROR, + MINT_DOWNLOAD_FAILED_ERROR, + MINT_DATA_INSPECTION_INVALID_PARAMETER_ERROR, + MINT_DATA_INSPECTION_INVALID_PARAMETER_MSG_ERROR, + MINT_QWEN_GUARD_PARAMETER_ERROR, + MINT_RATE_LIMIT_EXCEEDED_ERROR, + API_CLIENT_ERROR, + API_CLIENT_DISCONNECT, + API_INVALID_PARAMETER, + API_ARREARAGE_PTU_OVER_LIMIT, + API_THROTTLING_BURST_RATE, + API_THROTTLING_CONCURRENCY, + API_THROTTLING_USER_INPUT_TOKEN, + API_THROTTLING_USER_THINKING_OUTPUT_TOKEN, + API_THROTTLING_USER_NOTHINKING_OUTPUT_TOKEN, + API_THROTTLING_USER_OUTPUT_TOKEN, + API_INTERNAL_ERROR, + API_ROUTE_ERROR, + API_BALANCE_ERROR, + API_INTERNAL_ERROR_CONNECT_TIMEOUT, + API_INTERNAL_ERROR_SOCKET_TIMEOUT, + API_INTERNAL_ERROR_ENGINE_ERROR, + API_INTERNAL_ERROR_ENGINE_ABORT, + API_SERVICE_UNAVAILABLE, + API_ROUTER_INSTANCE_OVERLOADED, + API_RESPONSE_TIMEOUT, + API_INTERNAL_ERROR_JSON_PARSE_ERROR, + TURBO_INTERNAL_ERROR, + TURBO_UPLOAD_FAILED, + TURBO_FILE_UPLOAD_FAILED, + TURBO_RESPONSE_TIMEOUT, + MODEL_TASK_LIST_FULL, + MODEL_TIME_OUT_FOR_FIRST_TOKEN, + MODEL_STOP_FROM_ENGINE, + MODEL_STOP_ENGINE_PARAM, + MODEL_STOP_ENGINE_ERROR, + MODEL_ABORT, + MODEL_PREFILL_PREEMPTED, + MODEL_DECODE_PREEMPTED, + MODEL_STOP_TIMEOUT, + MODEL_INNER_ENGINE_STUCK, + MODEL_INNER_ENGINE_ERROR, + MODEL_INNER_GENERATE_EXIT, + MODEL_STOP_ENGINE_INVALID_OUTPUT, + AGENTIC_RL_INVALID_API_KEY, + AGENTIC_RL_FUNCTION_REGISTRATION_FAILED, + AGENTIC_RL_DATASETS_UPLOAD_FAILED, + AGENTIC_RL_DUPLICATE_FUNCTION_NAMES, + AGENTIC_RL_JOB_SUBMISSION_FAILED, + AGENTIC_RL_WORKFLOW_FAILED, + AGENTIC_RL_UNSUPPORTED_FUNCTION_TYPE, + AGENTIC_RL_FUNCTION_TEST_FAILED, + AGENTIC_RL_FUNCTION_TEST_TIMEOUT, + AGENTIC_RL_INPUT_ERROR, + AGENTIC_RL_OUTPUT_ERROR, + AGENTIC_RL_BASE_CONNECTION_ERROR, + AGENTIC_RL_OSS_CONNECTION_ERROR, + AGENTIC_RL_OSS_UPLOAD_ERROR, + AGENTIC_RL_DEPLOYMENT_ERROR, + AGENTIC_RL_REGISTRATION_ERROR, + AGENTIC_RL_FUNCTION_LOAD_ERROR, + AGENTIC_RL_INSTANCE_WARMUP_ERROR, + AGENTIC_RL_INSTANCE_QUERY_ERROR, + AGENTIC_RL_FUNCTION_LAYER_ERROR, + AGENTIC_RL_DATASETS_ERROR, + AGENTIC_RL_VALIDATION_ERROR, + AGENTIC_RL_CONFIGURATION_ERROR, + AGENTIC_RL_BASE_PERMISSION_ERROR, + AGENTIC_RL_IO_ERROR_WITH_CODE, +] diff --git a/dashscope/common/utils.py b/dashscope/common/utils.py index 7a87125..bf19bba 100644 --- a/dashscope/common/utils.py +++ b/dashscope/common/utils.py @@ -9,7 +9,7 @@ import threading from dataclasses import dataclass from http import HTTPStatus -from typing import Dict +from typing import Dict, Tuple from urllib.parse import urlparse import aiohttp @@ -18,11 +18,28 @@ from dashscope.api_entities.dashscope_response import DashScopeAPIResponse from dashscope.common.api_key import get_default_api_key from dashscope.common.constants import SSE_CONTENT_TYPE +from dashscope.common.error_registry import INTERNAL_ERROR from dashscope.common.logging import logger from dashscope.version import __version__ -def is_validate_fine_tune_file(file_path): +def truncate_error_message(message: str, max_length: int = 200) -> str: + """Truncate error message for logging to avoid excessive log output. + + Args: + message: The error message to truncate. + max_length: Maximum length of the message. Defaults to 200. + + Returns: + Truncated message with '...' suffix if longer than max_length, + otherwise original message. + """ + if len(message) > max_length: + return message[:max_length] + "..." + return message + + +def is_validate_fine_tune_file(file_path: str) -> bool: with open(file_path, encoding="utf-8") as f: for line in f: try: @@ -32,7 +49,7 @@ def is_validate_fine_tune_file(file_path): return True -def _get_task_group_and_task(module_name): +def _get_task_group_and_task(module_name: str) -> Tuple[str, str]: """Get task_group and task name. get task_group and task name based on api file __name__ @@ -48,41 +65,36 @@ def _get_task_group_and_task(module_name): return task_group, task -def is_path(path: str): - """Check the input path is valid local path. +def is_path(path: str) -> bool: + """Check if the input is a valid local path. Args: - path_or_url (str): The path. + path: The path to check. Returns: - bool: If path return True, otherwise False. + True if it's a valid local path, False otherwise. """ url_parsed = urlparse(path) - if url_parsed.scheme in ("file", ""): - return os.path.exists(url_parsed.path) - else: - return False + return url_parsed.scheme in ("file", "") and os.path.exists( + url_parsed.path, + ) -def is_url(url: str): - """Check the input url is valid url. +def is_url(url: str) -> bool: + """Check if the input is a valid URL. Args: - url (str): The url + url: The URL to check. Returns: - bool: If is url return True, otherwise False. + True if it's a valid URL, False otherwise. """ url_parsed = urlparse(url) - # pylint: disable=simplifiable-if-statement - if url_parsed.scheme in ("http", "https", "oss"): - return True - else: - return False + return url_parsed.scheme in ("http", "https", "oss") def iter_over_async(ait): - loop = asyncio.get_event_loop_policy().new_event_loop() + loop = asyncio.new_event_loop() ait = ait.__aiter__() async def get_next(): @@ -106,16 +118,14 @@ def iter_thread(loop, message_queue): message_queue.put((True, e, None)) break finally: - try: - loop.close() - except Exception: - pass + loop.close() message_queue = queue.Queue() x = threading.Thread( target=iter_thread, args=(loop, message_queue), name="iter_async_thread", + daemon=True, ) x.start() while True: @@ -123,10 +133,10 @@ def iter_thread(loop, message_queue): if finished: if error is not None: yield DashScopeAPIResponse( - -1, - "", - "Unknown", - message=f"Error type: {type(error)}, message: {error}", + status_code=INTERNAL_ERROR.status_code, + request_id="", + code=INTERNAL_ERROR.error_code, + message=INTERNAL_ERROR.format_msg(), ) break yield obj # pylint: disable=no-else-break @@ -170,7 +180,7 @@ def default_headers(api_key: str = None) -> Dict[str, str]: return headers -def join_url(base_url, *args): +def join_url(base_url: str, *args: str) -> str: if not base_url.endswith("/"): base_url = base_url + "/" url = base_url @@ -180,58 +190,19 @@ def join_url(base_url, *args): return url[:-1] -async def _handle_aiohttp_response(response: aiohttp.ClientResponse): - request_id = "" - if response.status == HTTPStatus.OK: - json_content = await response.json() - if "request_id" in json_content: - request_id = json_content["request_id"] - return DashScopeAPIResponse( - request_id=request_id, - status_code=HTTPStatus.OK, - output=json_content, - ) - else: - if "application/json" in response.content_type: - error = await response.json() - msg = "" - if "message" in error: - msg = error["message"] - if "request_id" in error: - request_id = error["request_id"] - return DashScopeAPIResponse( - request_id=request_id, - status_code=response.status, - output=None, - code=error["code"], - message=msg, - ) - else: - msg = await response.read() - return DashScopeAPIResponse( - request_id=request_id, - status_code=response.status, - output=None, - code="Unknown", - message=msg, - ) - - @dataclass class SSEEvent: - id: str - eventType: str - data: str - - def __init__( # pylint: disable=redefined-builtin - self, - id: str, - type: str, - data: str, - ): - self.id = id - self.eventType = type - self.data = data + """Server-Sent Events event representation. + + Attributes: + id: Event ID from the 'id:' field. + eventType: Event type from the 'event:' field. + data: Event data from the 'data:' field. + """ + + id: str = "" + eventType: str = "" + data: str = "" def _handle_stream(response: requests.Response): @@ -268,20 +239,48 @@ def _handle_stream(response: requests.Response): def _handle_error_message(error, status_code, flattened_output, headers): - code = None msg = "" request_id = "" + + # Log original error information + original_code = error.get("code", error.get("error_code", "")) + original_message = error.get( + "message", + error.get("error_message", error.get("msg", "")), + ) + logger.error( + "Request failed: status=%s, code=%s, message=%s", + status_code, + original_code or "unknown", + original_message or "unknown", + ) + if flattened_output: error["status_code"] = status_code return error - if "message" in error: + + # Extract message, fallback to INTERNAL_ERROR.format_msg() if empty + if "message" in error and error["message"]: msg = error["message"] - if "msg" in error: + elif "msg" in error and error["msg"]: msg = error["msg"] - if "code" in error: + elif "error_message" in error and error["error_message"]: + msg = error["error_message"] + else: + msg = INTERNAL_ERROR.format_msg() + + # Extract code, fallback to INTERNAL_ERROR.error_code if empty + if "code" in error and error["code"]: code = error["code"] + elif "error_code" in error and error["error_code"]: + code = error["error_code"] + else: + code = INTERNAL_ERROR.error_code + + # Extract request_id if "request_id" in error: request_id = error["request_id"] + return DashScopeAPIResponse( request_id=request_id, status_code=status_code, @@ -316,22 +315,43 @@ def _handle_http_failed_response( flattened_output, headers, ) + # No valid error data in SSE response + error_message = "\n".join(msgs).strip() or INTERNAL_ERROR.format_msg() + error_code = INTERNAL_ERROR.error_code + logger.error( + "Request failed: status=%s, code=%s, message=%s", + response.status_code, + error_code, + truncate_error_message(error_message), + ) return DashScopeAPIResponse( request_id=request_id, status_code=response.status_code, - code="Unknown", - message=msgs, + code=error_code, + message=error_message, headers=headers, ) else: msg = response.content.decode("utf-8") + error_message = msg or INTERNAL_ERROR.format_msg() + error_code = INTERNAL_ERROR.error_code + logger.error( + "Request failed: status=%s, code=%s, message=%s", + response.status_code, + error_code, + truncate_error_message(error_message), + ) if flattened_output: - return {"status_code": response.status_code, "message": msg} # type: ignore[return-value] # pylint: disable=line-too-long # noqa: E501 + return { # type: ignore[return-value] + "status_code": response.status_code, + "code": error_code, + "message": error_message, + } return DashScopeAPIResponse( request_id=request_id, status_code=response.status_code, - code="Unknown", - message=msg, + code=error_code, + message=error_message, headers=headers, ) @@ -359,13 +379,33 @@ async def _handle_aio_stream(response): async def _handle_aiohttp_failed_response( - response: requests.Response, + response: aiohttp.ClientResponse, flattened_output: bool = False, ) -> DashScopeAPIResponse: request_id = "" headers = dict(response.headers) if "application/json" in response.content_type: error = await response.json() + # Pass through code, fallback to + # INTERNAL_ERROR.error_code if not available + error_code = ( + error.get("code") + or error.get("error_code") + or INTERNAL_ERROR.error_code + ) + # Pass through message, fallback to + # INTERNAL_ERROR.error_msg if not available + error_message = ( + error.get("message") + or error.get("error_message") + or INTERNAL_ERROR.format_msg() + ) + logger.error( + "Request failed: status=%s, code=%s, message=%s", + response.status, + error_code, + truncate_error_message(error_message), + ) return _handle_error_message( error, response.status, @@ -374,19 +414,34 @@ async def _handle_aiohttp_failed_response( ) elif SSE_CONTENT_TYPE in response.content_type: error = None + raw_data = [] async for _, _, data in _handle_aio_stream(response): - error = json.loads(data) + raw_data.append(data) + try: + error = json.loads(data) + except json.JSONDecodeError: + continue if error is None: + raw_content = "\n".join(raw_data).strip() if raw_data else "" + error_code = INTERNAL_ERROR.error_code + error_message = raw_content or INTERNAL_ERROR.format_msg() + logger.error( + "Request failed: status=%s, code=%s, message=%s", + response.status, + error_code, + truncate_error_message(error_message), + ) if flattened_output: return { # type: ignore[return-value] "status_code": response.status, - "message": "Empty SSE error response", + "code": error_code, + "message": error_message, } return DashScopeAPIResponse( request_id=request_id, status_code=response.status, - code="Unknown", - message="Empty SSE error response", + code=error_code, + message=error_message, headers=headers, ) return _handle_error_message( @@ -397,13 +452,25 @@ async def _handle_aiohttp_failed_response( ) else: msg = await response.text() + error_code = INTERNAL_ERROR.error_code + error_message = msg or INTERNAL_ERROR.format_msg() + logger.error( + "Request failed: status=%s, code=%s, message=%s", + response.status, + error_code, + truncate_error_message(error_message), + ) if flattened_output: - return {"status_code": response.status, "message": msg} # type: ignore[return-value] # pylint: disable=line-too-long # noqa: E501 + return { # type: ignore[return-value] + "status_code": response.status, + "code": error_code, + "message": error_message, + } return DashScopeAPIResponse( request_id=request_id, status_code=response.status, - code="Unknown", - message=msg, + code=error_code, + message=error_message, headers=headers, ) @@ -412,11 +479,10 @@ def _handle_http_response( response: requests.Response, flattened_output: bool = False, ): - response = _handle_http_stream_response(response, flattened_output) - _, output = next(response) - try: - next(response) - except StopIteration: + response_gen = _handle_http_stream_response(response, flattened_output) + _, output = next(response_gen) + # Consume remaining items to ensure generator completes + for _ in response_gen: pass return output @@ -457,19 +523,28 @@ def _handle_http_stream_response( usage=usage, headers=headers, ) - except json.JSONDecodeError as e: + except json.JSONDecodeError: + error_code = INTERNAL_ERROR.error_code + error_message = event.data or INTERNAL_ERROR.format_msg() + logger.error( + "Request failed: status=%s, code=%s, message=%s", + response.status_code, + error_code, + truncate_error_message(error_message), + ) if flattened_output: yield event.eventType, { "status_code": response.status_code, - "message": e.message, + "code": error_code, + "message": error_message, } else: yield event.eventType, DashScopeAPIResponse( request_id=request_id, - status_code=HTTPStatus.BAD_REQUEST, + status_code=HTTPStatus.INTERNAL_SERVER_ERROR, output=None, - code="Unknown", - message=event.data, + code=error_code, + message=error_message, headers=headers, ) continue @@ -480,17 +555,39 @@ def _handle_http_stream_response( "message": event.data, } else: - msg = json.loads(event.data) - yield event.eventType, DashScopeAPIResponse( - request_id=request_id, - status_code=status_code, - output=None, - code=msg["code"] - if "code" in msg - else None, # noqa E501 - message=msg["message"] if "message" in msg else None, - headers=headers, - ) # noqa E501 + try: + msg = json.loads(event.data) + yield event.eventType, DashScopeAPIResponse( + request_id=request_id, + status_code=status_code, + output=None, + code=msg.get("code") + or msg.get("error_code") + or f"http_{status_code}", + message=msg.get("message") + or msg.get("error_message") + or f"HTTP {status_code} error", + headers=headers, + ) # noqa E501 + except json.JSONDecodeError: + error_code = INTERNAL_ERROR.error_code + error_message = ( + event.data or INTERNAL_ERROR.format_msg() + ) + logger.error( + "Request failed: status=%s, code=%s, message=%s", + status_code, + error_code, + truncate_error_message(error_message), + ) + yield event.eventType, DashScopeAPIResponse( + request_id=request_id, + status_code=status_code, + output=None, + code=error_code, + message=error_message, + headers=headers, + ) # pylint: disable=consider-using-in elif ( response.status_code == HTTPStatus.OK diff --git a/dashscope/finetune/agentic_rl.py b/dashscope/finetune/agentic_rl.py index 61b9c18..a86812c 100644 --- a/dashscope/finetune/agentic_rl.py +++ b/dashscope/finetune/agentic_rl.py @@ -3,7 +3,7 @@ # Copyright (c) Alibaba, Inc. and its affiliates. -from typing import Union, List, Optional, ClassVar, Dict, Any +from typing import Union, List, Optional, ClassVar, Dict, Any, Tuple from typing_extensions import Self from dashscope.client.base_api import CreateMixin @@ -40,15 +40,53 @@ get_func_type_id, deep_remove_none, ) +from dashscope.common.error import ( + AuthenticationError, + InvalidParameter, + DashScopeException, +) +from dashscope.common.error_registry import ( + INVALID_API_KEY, + INVALID_REQUEST, + INTERNAL_ERROR, + REQUEST_TIMEOUT, + AGENTIC_RL_INVALID_API_KEY, + AGENTIC_RL_FUNCTION_REGISTRATION_FAILED, + AGENTIC_RL_DATASETS_UPLOAD_FAILED, + AGENTIC_RL_DUPLICATE_FUNCTION_NAMES, + AGENTIC_RL_JOB_SUBMISSION_FAILED, + AGENTIC_RL_WORKFLOW_FAILED, + AGENTIC_RL_UNSUPPORTED_FUNCTION_TYPE, + AGENTIC_RL_FUNCTION_TEST_FAILED, + AGENTIC_RL_FUNCTION_TEST_TIMEOUT, +) from dashscope.finetune.reinforcement.common.errors import ( - RegistrationError, + BasePermissionError, + ConfigurationError, + InputError, + ValidationError, +) + +# 4xx exception types — client-side errors +_CLIENT_ERROR_TYPES = ( + BasePermissionError, + ConfigurationError, ValidationError, - RuntimeErrorWithCode, - ValueErrorWithCode, - DatasetsError, + InputError, ) +def _is_client_error(exc: Exception) -> bool: + """Check if an exception is a client-side error (should map to 4xx). + + Per dashscope public-errors spec: + - BasePermissionError / ConfigurationError / ValidationError / InputError + -> 4xx (client-side) + - All other exceptions -> 5xx (server-side) + """ + return isinstance(exc, _CLIENT_ERROR_TYPES) + + class AgenticRL(AgenticRLTuning, CreateMixin): SUB_PATH: ClassVar[str] = "fine-tunes" @@ -58,10 +96,17 @@ def __init__(self, api_key: str = None): try: set_api_key(api_key) except Exception as e: - raise ValueErrorWithCode( - "Invalid API key configuration", - error_code=3001, - ) from e + logger.error( + "[%s] %s | %s", + AGENTIC_RL_INVALID_API_KEY.name, + AGENTIC_RL_INVALID_API_KEY.format_message(), + e, + exc_info=True, + ) + exc = AuthenticationError(INVALID_API_KEY.format_msg()) + exc.status_code = INVALID_API_KEY.status_code + exc.error_code = INVALID_API_KEY.error_code + raise exc from e def init(self, config_path: Optional[str] = None, **kwargs) -> Self: """ @@ -81,7 +126,7 @@ async def register_functions( ] ] = None, lazy_load: Optional[bool] = True, - ) -> tuple[ + ) -> Tuple[ List[str], List[str], List[str], @@ -106,12 +151,31 @@ async def register_functions( ) logger.info("Function components registered") except Exception as e: - if hasattr(e, "error_code"): + if isinstance(e, DashScopeException): raise - raise RegistrationError( - "Function registration failed", - error_code=3002, - ) from e + logger.error( + "[%s] %s | %s", + AGENTIC_RL_FUNCTION_REGISTRATION_FAILED.name, + AGENTIC_RL_FUNCTION_REGISTRATION_FAILED.format_message(), + e, + exc_info=True, + ) + original_summary = f"{type(e).__name__}: {str(e)[:150]}" + if _is_client_error(e): + public_error = INVALID_REQUEST + exc = InvalidParameter( + f"{public_error.format_msg()} | " + f"Caused by: {original_summary}", + ) + else: + public_error = INTERNAL_ERROR + exc = DashScopeException( + f"{public_error.format_msg()} | " + f"Caused by: {original_summary}", + ) + exc.status_code = public_error.status_code + exc.error_code = public_error.error_code + raise exc from e return ( rollout_entity_ids, @@ -127,7 +191,7 @@ async def upload_datasets( datasets: Optional[List[Dataset]] = None, training_files: Optional[Union[List[str], str]] = None, validation_files: Optional[Union[List[str], str]] = None, - ) -> tuple[List[str], List[str]]: + ) -> Tuple[List[str], List[str]]: if datasets: self.tuning.datasets = datasets @@ -141,10 +205,31 @@ async def upload_datasets( ) logger.info("Datasets uploaded") except Exception as e: - raise DatasetsError( - "Datasets upload failed", - error_code=3003, - ) from e + if isinstance(e, DashScopeException): + raise + logger.error( + "[%s] %s | %s", + AGENTIC_RL_DATASETS_UPLOAD_FAILED.name, + AGENTIC_RL_DATASETS_UPLOAD_FAILED.format_message(), + e, + exc_info=True, + ) + original_summary = f"{type(e).__name__}: {str(e)[:150]}" + if _is_client_error(e): + public_error = INVALID_REQUEST + exc = InvalidParameter( + f"{public_error.format_msg()} | " + f"Caused by: {original_summary}", + ) + else: + public_error = INTERNAL_ERROR + exc = DashScopeException( + f"{public_error.format_msg()} | " + f"Caused by: {original_summary}", + ) + exc.status_code = public_error.status_code + exc.error_code = public_error.error_code + raise exc from e return uploaded_training_ids, uploaded_validation_ids @@ -200,11 +285,15 @@ def submit_job( ) # names of functions if not self.tuning.check_function_names(): - raise ValueErrorWithCode( - "Duplicate function names detected. All function names must " - "be unique.", - error_code=3004, + logger.error( + "[%s] %s", + AGENTIC_RL_DUPLICATE_FUNCTION_NAMES.name, + AGENTIC_RL_DUPLICATE_FUNCTION_NAMES.format_message(), ) + exc = InvalidParameter(INVALID_REQUEST.format_msg()) + exc.status_code = INVALID_REQUEST.status_code + exc.error_code = INVALID_REQUEST.error_code + raise exc # datasets if datasets: @@ -251,12 +340,31 @@ def submit_job( **kwargs, ) except Exception as e: - if hasattr(e, "error_code"): + if isinstance(e, DashScopeException): raise - raise RuntimeErrorWithCode( - "Job submission failed", - error_code=3005, - ) from e + logger.error( + "[%s] %s | %s", + AGENTIC_RL_JOB_SUBMISSION_FAILED.name, + AGENTIC_RL_JOB_SUBMISSION_FAILED.format_message(), + e, + exc_info=True, + ) + original_summary = f"{type(e).__name__}: {str(e)[:150]}" + if _is_client_error(e): + public_error = INVALID_REQUEST + exc = InvalidParameter( + f"{public_error.format_msg()} | " + f"Caused by: {original_summary}", + ) + else: + public_error = INTERNAL_ERROR + exc = DashScopeException( + f"{public_error.format_msg()} | " + f"Caused by: {original_summary}", + ) + exc.status_code = public_error.status_code + exc.error_code = public_error.error_code + raise exc from e return FineTune(**resp) @@ -316,12 +424,31 @@ async def run( **kwargs, ) except Exception as e: - if hasattr(e, "error_code"): + if isinstance(e, DashScopeException): raise - raise RuntimeErrorWithCode( - "RL tuning workflow failed", - error_code=3006, - ) from e + logger.error( + "[%s] %s | %s", + AGENTIC_RL_WORKFLOW_FAILED.name, + AGENTIC_RL_WORKFLOW_FAILED.format_message(), + e, + exc_info=True, + ) + original_summary = f"{type(e).__name__}: {str(e)[:150]}" + if _is_client_error(e): + public_error = INVALID_REQUEST + exc = InvalidParameter( + f"{public_error.format_msg()} | " + f"Caused by: {original_summary}", + ) + else: + public_error = INTERNAL_ERROR + exc = DashScopeException( + f"{public_error.format_msg()} | " + f"Caused by: {original_summary}", + ) + exc.status_code = public_error.status_code + exc.error_code = public_error.error_code + raise exc from e @classmethod def cancel( @@ -437,10 +564,16 @@ async def test_functions( elif functype == FunctionType.GROUP_REWARD: value = GroupRewardInput.model_validate(input_data) else: - raise ValueErrorWithCode( - f"Unsupported function type: {functype}", - error_code=3007, + logger.error( + "[%s] %s | functype=%s", + AGENTIC_RL_UNSUPPORTED_FUNCTION_TYPE.name, + AGENTIC_RL_UNSUPPORTED_FUNCTION_TYPE.format_message(), + functype, ) + exc = InvalidParameter(INVALID_REQUEST.format_msg()) + exc.status_code = INVALID_REQUEST.status_code + exc.error_code = INVALID_REQUEST.error_code + raise exc logger.info( f"Starting {str(functype)} verification", @@ -456,7 +589,44 @@ async def test_functions( ) except Exception as e: - raise ValidationError( - "Function test failed", - error_code=3008, - ) from e + if isinstance(e, (DashScopeException, InvalidParameter)): + raise + + original_summary = f"{type(e).__name__}: {str(e)[:150]}" + + if _is_client_error(e): + # 4xx: client-side error (InputError, ValidationError, etc.) + public_error = INVALID_REQUEST + exc_class = InvalidParameter + internal_error_def = AGENTIC_RL_FUNCTION_TEST_FAILED + elif isinstance(e, (ConnectionError, OSError)): + # 5xx: server/infra error + public_error = INTERNAL_ERROR + exc_class = DashScopeException + internal_error_def = AGENTIC_RL_FUNCTION_TEST_FAILED + elif isinstance(e, TimeoutError): + # 5xx: timeout + public_error = REQUEST_TIMEOUT + exc_class = DashScopeException + internal_error_def = AGENTIC_RL_FUNCTION_TEST_TIMEOUT + else: + # 5xx: validation / other → treat as client input issue + public_error = INVALID_REQUEST + exc_class = InvalidParameter + internal_error_def = AGENTIC_RL_FUNCTION_TEST_FAILED + + logger.error( + "[%s] %s | %s", + internal_error_def.name, + internal_error_def.format_message(), + e, + exc_info=True, + ) + + exc = exc_class( + f"{public_error.format_msg()} | " + f"Caused by: {original_summary}", + ) + exc.status_code = public_error.status_code + exc.error_code = public_error.error_code + raise exc from e diff --git a/dashscope/finetune/reinforcement/common/errors.py b/dashscope/finetune/reinforcement/common/errors.py index 0d9f31d..773c7d0 100644 --- a/dashscope/finetune/reinforcement/common/errors.py +++ b/dashscope/finetune/reinforcement/common/errors.py @@ -3,6 +3,8 @@ Custom exception hierarchy for the AgenticRL system """ +from __future__ import annotations + from datetime import datetime from typing import Optional, Dict @@ -11,13 +13,21 @@ class _RootCauseMixin: """Mixin that provides root-cause traversal and formatting for exceptions that carry an error code.""" + MAX_CAUSE_DEPTH = 50 + @property def root_cause(self) -> "Exception": root: "Exception" = self # type: ignore[assignment] seen = {id(root)} - while root.__cause__ and id(root.__cause__) not in seen: + depth = 0 + while ( + root.__cause__ + and id(root.__cause__) not in seen + and depth < self.MAX_CAUSE_DEPTH + ): root = root.__cause__ seen.add(id(root)) + depth += 1 return root def _format_cause(self) -> str: @@ -31,18 +41,42 @@ def _format_cause(self) -> str: return f" (caused by: {type(root).__name__})" return "" + def _get_registry_message(self, error_code: str) -> str: + """Get the message from error_registry for the given error_code name. + + Uses lazy import to avoid circular dependency with error_registry. + Returns empty string if not found. + """ + try: + from dashscope.common.error_registry import INTERNAL_ERRORS + + for err_def in INTERNAL_ERRORS: + if err_def.name == error_code: + return err_def.format_message() + except Exception: + pass + return "" + class AgenticRLError(_RootCauseMixin, Exception): """Base class for all Agentic RL exceptions.""" - def __init__(self, message: str, error_code: int = 1000): + def __init__( + self, + message: str, + error_code: str = "agentic_rl.AgenticRLError", + ): super().__init__(message) self.error_code = error_code self.timestamp = datetime.now().isoformat() self.message = message def __str__(self): - base = f"[{self.error_code}] {self.message} (at {self.timestamp})" + registry_msg = self._get_registry_message(self.error_code) + if registry_msg: + base = f"[{self.error_code}] {registry_msg} | {self.message}" + else: + base = f"[{self.error_code}] {self.message}" return f"{base}{self._format_cause()}" @@ -52,7 +86,7 @@ class IOErrorWithCode(AgenticRLError): def __init__( self, message: str, - error_code: int = 1800, + error_code: str = "agentic_rl.IOErrorWithCode", path: Optional[str] = None, operation: Optional[str] = None, ): @@ -65,12 +99,22 @@ class RuntimeErrorWithCode(_RootCauseMixin, RuntimeError): """Enhanced RuntimeError that supports error codes for better error categorization.""" - def __init__(self, message: str, error_code: int = 0): + def __init__( + self, + message: str, + error_code: str = "agentic_rl.RuntimeErrorWithCode", + ): super().__init__(message) self.error_code = error_code self.message = message def __str__(self): + registry_msg = self._get_registry_message(self.error_code) + if registry_msg: + return ( + f"[{self.error_code}] {registry_msg} | " + f"{self.message}{self._format_cause()}" + ) return f"[{self.error_code}] {self.message}{self._format_cause()}" @@ -78,12 +122,22 @@ class ValueErrorWithCode(_RootCauseMixin, ValueError): """Enhanced ValueError that supports error codes for better error categorization.""" - def __init__(self, message: str, error_code: int = 0): + def __init__( + self, + message: str, + error_code: str = "agentic_rl.ValueErrorWithCode", + ): super().__init__(message) self.error_code = error_code self.message = message def __str__(self): + registry_msg = self._get_registry_message(self.error_code) + if registry_msg: + return ( + f"[{self.error_code}] {registry_msg} | " + f"{self.message}{self._format_cause()}" + ) return f"[{self.error_code}] {self.message}{self._format_cause()}" @@ -93,7 +147,7 @@ class InputError(AgenticRLError): def __init__( self, message: str, - error_code: int = 1100, + error_code: str = "agentic_rl.InputError", field: Optional[str] = None, ): super().__init__(message, error_code) @@ -106,7 +160,7 @@ class OutputError(AgenticRLError): def __init__( self, message: str, - error_code: int = 1200, + error_code: str = "agentic_rl.OutputError", response: Optional[Dict] = None, ): super().__init__(message, error_code) @@ -119,7 +173,7 @@ class BaseConnectionError(AgenticRLError): def __init__( self, message: str, - error_code: int = 1300, + error_code: str = "agentic_rl.BaseConnectionError", endpoint: Optional[str] = None, ): super().__init__(message, error_code) @@ -132,7 +186,7 @@ class OSSConnectionError(BaseConnectionError): def __init__( self, message: str, - error_code: int = 1310, + error_code: str = "agentic_rl.OSSConnectionError", endpoint: str = None, ): super().__init__( @@ -148,13 +202,17 @@ class OSSUploadError(BaseConnectionError): def __init__( self, message: str, - error_code: int = 1320, + error_code: str = "agentic_rl.OSSUploadError", endpoint: str = None, bucket: Optional[str] = None, object_key: Optional[str] = None, file_size: Optional[int] = None, ): - super().__init__(f"OSS upload failed: {message}", error_code, endpoint) + super().__init__( + f"OSS upload failed: {message}", + error_code, + endpoint, + ) self.bucket = bucket self.object_key = object_key self.file_size = file_size @@ -166,7 +224,7 @@ class DeploymentError(AgenticRLError): def __init__( self, message: str, - error_code: int = 1400, + error_code: str = "agentic_rl.DeploymentError", resource_id: Optional[str] = None, ): super().__init__(message, error_code) @@ -179,7 +237,7 @@ class RegistrationError(DeploymentError): def __init__( self, message: str, - error_code: int = 1410, + error_code: str = "agentic_rl.RegistrationError", resource_id: Optional[str] = None, ): super().__init__( @@ -192,8 +250,15 @@ def __init__( class DatasetsError(DeploymentError): """Raised when update datasets fails in the deployment system""" - def __init__(self, message: str, error_code: int = 1460): - super().__init__(f"Datasets failed: {message}", error_code) + def __init__( + self, + message: str, + error_code: str = "agentic_rl.DatasetsError", + ): + super().__init__( + f"Datasets failed: {message}", + error_code, + ) class FunctionLoadError(DeploymentError): @@ -202,7 +267,7 @@ class FunctionLoadError(DeploymentError): def __init__( self, message: str, - error_code: int = 1420, + error_code: str = "agentic_rl.FunctionLoadError", entity_id: str = None, error_log: Optional[str] = None, ): @@ -221,7 +286,7 @@ class FunctionLayerError(DeploymentError): def __init__( self, message: str, - error_code: int = 1450, + error_code: str = "agentic_rl.FunctionLayerError", layer_name: str = None, error_log: Optional[str] = None, ): @@ -240,12 +305,15 @@ class InstanceWarmupError(DeploymentError): def __init__( self, message: str, - error_code: int = 1430, + error_code: str = "agentic_rl.InstanceWarmupError", instance_url: str = None, timeout: float = 0.0, retry_after: Optional[float] = None, ): - super().__init__(f"Instance warmup failed: {message}", error_code) + super().__init__( + f"Instance warmup failed: {message}", + error_code, + ) self.instance_url = instance_url self.timeout = timeout self.retry_after = retry_after @@ -257,14 +325,13 @@ class InstanceQueryError(DeploymentError): def __init__( self, message: str, - error_code: int = 1440, + error_code: str = "agentic_rl.InstanceQueryError", instance_id: str = None, query_attempts: int = 1, ): super().__init__( f"Instance query failed: {message}", error_code, - instance_id, ) self.instance_id = instance_id self.query_attempts = query_attempts @@ -276,11 +343,14 @@ class ValidationError(AgenticRLError): def __init__( self, message: str, - error_code: int = 1500, + error_code: str = "agentic_rl.ValidationError", invalid_data: Optional[Dict] = None, validation_rules: Optional[Dict] = None, ): - super().__init__(f"Validation failed: {message}", error_code) + super().__init__( + f"Validation failed: {message}", + error_code, + ) self.invalid_data = invalid_data self.validation_rules = validation_rules @@ -291,7 +361,7 @@ class ConfigurationError(ValidationError): def __init__( self, message: str, - error_code: int = 1510, + error_code: str = "agentic_rl.ConfigurationError", config_path: Optional[str] = None, ): super().__init__(message, error_code=error_code) @@ -304,7 +374,7 @@ class BasePermissionError(AgenticRLError): def __init__( self, message: str, - error_code: int = 1700, + error_code: str = "agentic_rl.BasePermissionError", operation: str = None, resource: str = None, ): diff --git a/dashscope/finetune/reinforcement/common/model.py b/dashscope/finetune/reinforcement/common/model.py index 8f4123b..1a62fa9 100644 --- a/dashscope/finetune/reinforcement/common/model.py +++ b/dashscope/finetune/reinforcement/common/model.py @@ -107,7 +107,7 @@ class Dataset(BaseModel): download_url: Optional[str] = None mount_storage: Optional[MountStorage] = None - async def upload_dataset(self) -> Optional[str]: + async def upload_dataset(self) -> str: if ( self.data_source_type == DataSourceType.FILE_ID and self.file_name is not None @@ -118,6 +118,11 @@ async def upload_dataset(self) -> Optional[str]: ) if file_id and isinstance(file_id, List) and len(file_id) > 0: self.file_id = file_id[0] + else: + raise OSSUploadError( + f"Empty upload result for {self.file_name}", + error_code=2061, + ) except Exception as e: raise OSSUploadError( @@ -125,6 +130,12 @@ async def upload_dataset(self) -> Optional[str]: error_code=2061, ) from e + if not self.file_id: + raise InputError( + f"Missing file_id after upload attempt: {self.file_name}", + error_code=1101, + field="file_id", + ) return self.file_id @@ -495,17 +506,24 @@ async def get_layer(self, layer_code: str) -> str: return "SUCCESS" - def clean_temp_files(self, tmp_path: str) -> None: + def clean_temp_files(self, *tmp_paths: str) -> None: """Cleanup temporary deployment files.""" - try: - for f in [tmp_path]: - if os.path.exists(f): - if os.path.isfile(f): - os.remove(f) + failed_cleanups = [] + for tmp_path in tmp_paths: + if not tmp_path: + continue + try: + if os.path.exists(tmp_path): + if os.path.isfile(tmp_path): + os.remove(tmp_path) else: - shutil.rmtree(f) - except Exception as e: - logger.warning(f"Temp file cleanup failed: {str(e)}") + shutil.rmtree(tmp_path) + except Exception as e: + failed_cleanups.append((tmp_path, str(e))) + + if failed_cleanups: + msg = "; ".join(f"{p}: {err}" for p, err in failed_cleanups) + logger.error(f"Temp file cleanup failed: {msg}") def split_classpath(self): self.filepath, self.classname = get_filepath_classname(self.classpath) @@ -1171,7 +1189,7 @@ class TuningModel(Models, BaseModel): async def register_functions( self, lazy_load: Optional[bool] = True, - ) -> tuple[ + ) -> Tuple[ List[str], List[str], List[str], @@ -1265,7 +1283,7 @@ async def upload_datasets( self, training_files: Union[List[str], str] = None, validation_files: Union[List[str], str] = None, - ) -> tuple[List[str], List[str]]: + ) -> Tuple[List[str], List[str]]: """Register and validate training/validation datasets.""" uploaded_training_ids = [] uploaded_validation_ids = [] diff --git a/tests/unit/mock_request_base.py b/tests/unit/mock_request_base.py index c5c016b..ef3ee37 100644 --- a/tests/unit/mock_request_base.py +++ b/tests/unit/mock_request_base.py @@ -20,7 +20,7 @@ def setup_class(cls): super().setup_class() dashscope.base_http_api_url = "http://localhost:8089/api/v1/" dashscope.base_websocket_api_url = ( - "http://localhost:8089/api-ws/v1/inference" + "ws://localhost:8089/api-ws/v1/inference" ) dashscope.api_key = "default" dashscope.api_protocol = "http" diff --git a/tests/unit/test_agentic_rl_error_codes.py b/tests/unit/test_agentic_rl_error_codes.py new file mode 100644 index 0000000..b2c74f7 --- /dev/null +++ b/tests/unit/test_agentic_rl_error_codes.py @@ -0,0 +1,804 @@ +# -*- coding: utf-8 -*- +"""Tests for AgenticRL error handling with standard SDK exceptions. + +Validates that AgenticRL methods raise correct standard SDK exceptions +(AuthenticationError, InvalidParameter, DashScopeException) with proper +status_code and error_code attributes in error scenarios. +""" +from unittest.mock import patch, MagicMock, AsyncMock +import pytest + +from dashscope.common.error import ( + AuthenticationError, + InvalidParameter, + DashScopeException, +) +from dashscope.finetune.agentic_rl import AgenticRL +from dashscope.finetune.reinforcement import FunctionType + + +class TestInitAuthenticationError: + """Test __init__ raises AuthenticationError for invalid API key.""" + + @patch("dashscope.finetune.agentic_rl.set_api_key") + def test_init_invalid_api_key_raises_authentication_error( + self, + mock_set_api_key, + ): + """__init__ should raise AuthenticationError when set_api_key fails.""" + mock_set_api_key.side_effect = ValueError("Invalid API key") + + with pytest.raises(AuthenticationError) as exc_info: + AgenticRL(api_key="invalid-key") + + assert exc_info.value.status_code == 401 + assert exc_info.value.error_code == "AuthenticationError" + assert "Incorrect API key" in str(exc_info.value) + + +class TestSubmitJobInvalidParameter: + """Test submit_job raises InvalidParameter for duplicate function names.""" + + @patch( + "dashscope.finetune.agentic_rl.AgenticRLTuning.__init__", + return_value=None, + ) + @patch( + "dashscope.finetune.agentic_rl.generate_random_id", + return_value="abcd1234", + ) + def test_submit_job_duplicate_function_names_raises_invalid_parameter( + self, + _mock_random_id, + _mock_parent_init, + ): + """submit_job should raise InvalidParameter when + check_function_names returns False.""" + agent = AgenticRL.__new__(AgenticRL) + object.__setattr__(agent, "tuning", MagicMock()) + agent.tuning.name = "test-job" + agent.tuning.model.name = "test-model" + agent.tuning.check_function_names.return_value = False + agent.tuning.datasets = [] + agent.tuning.combine_ids_runtimes.return_value = [] + agent.tuning.training.hyper_parameters = None + agent.tuning.training.resources = None + agent.tuning.training.type = "rl" + + with pytest.raises(InvalidParameter) as exc_info: + agent.submit_job() + + assert exc_info.value.status_code == 400 + assert exc_info.value.error_code == "BadRequestError" + assert "request is invalid" in str(exc_info.value).lower() + + +class TestRegisterFunctionsDashScopeException: + """Test register_functions raises DashScopeException on failure.""" + + @pytest.mark.asyncio + @patch( + "dashscope.finetune.agentic_rl.AgenticRLTuning.__init__", + return_value=None, + ) + async def test_register_functions_failure_raises_dashscope_exception( + self, + _mock_parent_init, + ): + """register_functions should raise DashScopeException + when internal call fails.""" + agent = AgenticRL.__new__(AgenticRL) + object.__setattr__(agent, "tuning", MagicMock()) + agent.tuning.register_functions = AsyncMock( + side_effect=RuntimeError("Registration failed"), + ) + + with pytest.raises(DashScopeException) as exc_info: + await agent.register_functions() + + assert exc_info.value.status_code == 500 + assert exc_info.value.error_code == "InternalServerError" + assert "internal error" in str(exc_info.value).lower() + + +class TestUploadDatasetsDashScopeException: + """Test upload_datasets raises DashScopeException on failure.""" + + @pytest.mark.asyncio + @patch( + "dashscope.finetune.agentic_rl.AgenticRLTuning.__init__", + return_value=None, + ) + async def test_upload_datasets_failure_raises_dashscope_exception( + self, + _mock_parent_init, + ): + """upload_datasets should raise DashScopeException + when upload fails.""" + agent = AgenticRL.__new__(AgenticRL) + object.__setattr__(agent, "tuning", MagicMock()) + agent.tuning.upload_datasets = AsyncMock( + side_effect=IOError("Upload failed"), + ) + + with pytest.raises(DashScopeException) as exc_info: + await agent.upload_datasets() + + assert exc_info.value.status_code == 500 + assert exc_info.value.error_code == "InternalServerError" + + +class TestSubmitJobCallFailure: + """Test submit_job raises DashScopeException when API call fails.""" + + @patch( + "dashscope.finetune.agentic_rl.AgenticRLTuning.__init__", + return_value=None, + ) + @patch( + "dashscope.finetune.agentic_rl.generate_random_id", + return_value="abcd1234", + ) + @patch("dashscope.finetune.agentic_rl.CreateMixin.call") + def test_submit_job_call_failure_raises_dashscope_exception( + self, + mock_call, + _mock_random_id, + _mock_parent_init, + ): + """submit_job should raise DashScopeException + when super().call() fails.""" + mock_call.side_effect = RuntimeError("API call failed") + + agent = AgenticRL.__new__(AgenticRL) + object.__setattr__(agent, "tuning", MagicMock()) + agent.tuning.name = "test-job" + agent.tuning.model.name = "test-model" + agent.tuning.check_function_names.return_value = True + agent.tuning.datasets = [] + agent.tuning.combine_ids_runtimes.return_value = [] + agent.tuning.training.hyper_parameters = None + agent.tuning.training.resources = None + agent.tuning.training.type = "rl" + + with pytest.raises(DashScopeException) as exc_info: + agent.submit_job() + + assert exc_info.value.status_code == 500 + assert exc_info.value.error_code == "InternalServerError" + + +class TestRunFailure: + """Test run raises DashScopeException on failure.""" + + @pytest.mark.asyncio + @patch( + "dashscope.finetune.agentic_rl.AgenticRLTuning.__init__", + return_value=None, + ) + async def test_run_failure_raises_dashscope_exception( + self, + _mock_parent_init, + ): + """run should raise DashScopeException when any step fails.""" + agent = AgenticRL.__new__(AgenticRL) + object.__setattr__(agent, "tuning", MagicMock()) + object.__setattr__( + agent, + "register_functions", + AsyncMock( + side_effect=RuntimeError("Step failed"), + ), + ) + + with pytest.raises(DashScopeException) as exc_info: + await agent.run() + + assert exc_info.value.status_code == 500 + assert exc_info.value.error_code == "InternalServerError" + + +class TestTestFunctionsInvalidParameter: + """Test test_functions raises InvalidParameter for + unsupported types and validation failures.""" + + @pytest.mark.asyncio + @patch("dashscope.finetune.agentic_rl.set_api_key") + async def test_test_functions_unsupported_type_raises_invalid_parameter( + self, + _mock_set_api_key, + ): + """test_functions should raise InvalidParameter + for unsupported FunctionType.""" + with pytest.raises(InvalidParameter) as exc_info: + await AgenticRL.test_functions( + instance_id="inst-123", + functype=MagicMock(spec=FunctionType), # Unsupported type + input_data={}, + ) + + assert exc_info.value.status_code == 400 + assert exc_info.value.error_code == "BadRequestError" + + @pytest.mark.asyncio + @patch("dashscope.finetune.agentic_rl.set_api_key") + @patch("dashscope.finetune.agentic_rl.RolloutInput.model_validate") + async def test_test_functions_validation_failure_raises_invalid_parameter( + self, + mock_validate, + _mock_set_api_key, + ): + """test_functions should raise InvalidParameter + when validation fails.""" + mock_validate.side_effect = ValueError("Validation failed") + + with pytest.raises(InvalidParameter) as exc_info: + await AgenticRL.test_functions( + instance_id="inst-123", + functype=FunctionType.ROLLOUT, + input_data={"invalid": "data"}, + ) + + assert exc_info.value.status_code == 400 + assert exc_info.value.error_code == "BadRequestError" + + +class TestDashScopeExceptionPassthrough: + """Test that DashScopeException from API is passed through unchanged.""" + + @patch( + "dashscope.finetune.agentic_rl.AgenticRLTuning.__init__", + return_value=None, + ) + @patch( + "dashscope.finetune.agentic_rl.generate_random_id", + return_value="abcd1234", + ) + @patch("dashscope.finetune.agentic_rl.CreateMixin.call") + def test_submit_job_passthrough_dashscope_exception( + self, + mock_call, + _mock_random_id, + _mock_parent_init, + ): + """submit_job should pass through DashScopeException + from API unchanged.""" + original_exc = DashScopeException("API error") + original_exc.status_code = 429 + original_exc.error_code = "RateLimitError" + original_exc.request_id = "req-123" + mock_call.side_effect = original_exc + + agent = AgenticRL.__new__(AgenticRL) + object.__setattr__(agent, "tuning", MagicMock()) + agent.tuning.name = "test-job" + agent.tuning.model.name = "test-model" + agent.tuning.check_function_names.return_value = True + agent.tuning.datasets = [] + agent.tuning.combine_ids_runtimes.return_value = [] + agent.tuning.training.hyper_parameters = None + agent.tuning.training.resources = None + agent.tuning.training.type = "rl" + + with pytest.raises(DashScopeException) as exc_info: + agent.submit_job() + + # Should be the exact same exception object + assert exc_info.value is original_exc + assert exc_info.value.status_code == 429 + assert exc_info.value.error_code == "RateLimitError" + assert exc_info.value.request_id == "req-123" + + @pytest.mark.asyncio + @patch( + "dashscope.finetune.agentic_rl.AgenticRLTuning.__init__", + return_value=None, + ) + async def test_register_functions_passthrough_dashscope_exception( + self, + _mock_parent_init, + ): + """register_functions should pass through + DashScopeException unchanged.""" + original_exc = DashScopeException("Service error") + original_exc.status_code = 503 + original_exc.error_code = "ServiceUnavailableError" + + agent = AgenticRL.__new__(AgenticRL) + object.__setattr__(agent, "tuning", MagicMock()) + agent.tuning.register_functions = AsyncMock(side_effect=original_exc) + + with pytest.raises(DashScopeException) as exc_info: + await agent.register_functions() + + assert exc_info.value is original_exc + assert exc_info.value.status_code == 503 + assert exc_info.value.error_code == "ServiceUnavailableError" + + @pytest.mark.asyncio + @patch( + "dashscope.finetune.agentic_rl.AgenticRLTuning.__init__", + return_value=None, + ) + async def test_run_passthrough_dashscope_exception( + self, + _mock_parent_init, + ): + """run should pass through DashScopeException + from inner calls unchanged.""" + original_exc = DashScopeException("Inner error") + original_exc.status_code = 400 + original_exc.error_code = "BadRequestError" + + agent = AgenticRL.__new__(AgenticRL) + object.__setattr__(agent, "tuning", MagicMock()) + object.__setattr__( + agent, + "register_functions", + AsyncMock(side_effect=original_exc), + ) + + with pytest.raises(DashScopeException) as exc_info: + await agent.run() + + assert exc_info.value is original_exc + assert exc_info.value.status_code == 400 + assert exc_info.value.error_code == "BadRequestError" + + +class TestExceptionChainPreserved: + """Verify that __cause__ is correctly preserved in all error scenarios.""" + + @patch("dashscope.finetune.agentic_rl.set_api_key") + def test_init_preserves_cause(self, mock_set_api_key): + """__init__ should preserve the original exception as __cause__.""" + original = ValueError("bad key") + mock_set_api_key.side_effect = original + + with pytest.raises(AuthenticationError) as exc_info: + AgenticRL(api_key="invalid") + + assert exc_info.value.__cause__ is original + + @pytest.mark.asyncio + @patch( + "dashscope.finetune.agentic_rl.AgenticRLTuning.__init__", + return_value=None, + ) + async def test_register_functions_preserves_cause(self, _mock_parent_init): + """register_functions should preserve __cause__ + for non-DashScopeException.""" + original = IOError("network down") + agent = AgenticRL.__new__(AgenticRL) + object.__setattr__(agent, "tuning", MagicMock()) + agent.tuning.register_functions = AsyncMock(side_effect=original) + + with pytest.raises(DashScopeException) as exc_info: + await agent.register_functions() + + assert exc_info.value.__cause__ is original + + @pytest.mark.asyncio + @patch( + "dashscope.finetune.agentic_rl.AgenticRLTuning.__init__", + return_value=None, + ) + async def test_upload_datasets_preserves_cause(self, _mock_parent_init): + """upload_datasets should preserve __cause__.""" + original = OSError("disk full") + agent = AgenticRL.__new__(AgenticRL) + object.__setattr__(agent, "tuning", MagicMock()) + agent.tuning.upload_datasets = AsyncMock(side_effect=original) + + with pytest.raises(DashScopeException) as exc_info: + await agent.upload_datasets() + + assert exc_info.value.__cause__ is original + + @patch( + "dashscope.finetune.agentic_rl.AgenticRLTuning.__init__", + return_value=None, + ) + @patch( + "dashscope.finetune.agentic_rl.generate_random_id", + return_value="abcd1234", + ) + @patch("dashscope.finetune.agentic_rl.CreateMixin.call") + def test_submit_job_call_failure_preserves_cause( + self, + mock_call, + _mock_random_id, + _mock_parent_init, + ): + """submit_job should preserve __cause__ when API call fails.""" + original = TimeoutError("connection timeout") + mock_call.side_effect = original + + agent = AgenticRL.__new__(AgenticRL) + object.__setattr__(agent, "tuning", MagicMock()) + agent.tuning.name = "test-job" + agent.tuning.model.name = "test-model" + agent.tuning.check_function_names.return_value = True + agent.tuning.datasets = [] + agent.tuning.combine_ids_runtimes.return_value = [] + agent.tuning.training.hyper_parameters = None + agent.tuning.training.resources = None + agent.tuning.training.type = "rl" + + with pytest.raises(DashScopeException) as exc_info: + agent.submit_job() + + assert exc_info.value.__cause__ is original + + @pytest.mark.asyncio + @patch("dashscope.finetune.agentic_rl.set_api_key") + async def test_test_functions_validation_preserves_cause( + self, + _mock_set_api_key, + ): + """test_functions should preserve __cause__ on validation failure.""" + original = TypeError("wrong type") + + with patch( + "dashscope.finetune.agentic_rl.RolloutInput.model_validate", + side_effect=original, + ): + with pytest.raises(InvalidParameter) as exc_info: + await AgenticRL.test_functions( + instance_id="inst-1", + functype=FunctionType.ROLLOUT, + input_data={"bad": "data"}, + ) + + assert exc_info.value.__cause__ is original + + +class TestInternalLogCodes: + """Verify that internal integer error codes (3001-3008) are logged.""" + + @patch("dashscope.finetune.agentic_rl.set_api_key") + @patch("dashscope.finetune.agentic_rl.logger") + def test_init_logs_code_3001(self, mock_logger, mock_set_api_key): + """__init__ should log agentic_rl.InvalidApiKey error.""" + mock_set_api_key.side_effect = ValueError("bad") + + with pytest.raises(AuthenticationError): + AgenticRL(api_key="x") + + mock_logger.error.assert_called_once() + # Logger uses placeholder format: "[%s] %s | %s", + # name, message, exception + assert mock_logger.error.call_args[0][1] == "agentic_rl.InvalidApiKey" + + @pytest.mark.asyncio + @patch( + "dashscope.finetune.agentic_rl.AgenticRLTuning.__init__", + return_value=None, + ) + @patch("dashscope.finetune.agentic_rl.logger") + async def test_register_functions_logs_code_3002( + self, + mock_logger, + _mock_parent_init, + ): + """register_functions should log + agentic_rl.FunctionRegistrationFailed error.""" + agent = AgenticRL.__new__(AgenticRL) + object.__setattr__(agent, "tuning", MagicMock()) + agent.tuning.register_functions = AsyncMock( + side_effect=RuntimeError("fail"), + ) + + with pytest.raises(DashScopeException): + await agent.register_functions() + + mock_logger.error.assert_called_once() + # Logger uses placeholder format: "[%s] %s | %s", + # name, message, exception + assert ( + mock_logger.error.call_args[0][1] + == "agentic_rl.FunctionRegistrationFailed" + ) + + @pytest.mark.asyncio + @patch( + "dashscope.finetune.agentic_rl.AgenticRLTuning.__init__", + return_value=None, + ) + @patch("dashscope.finetune.agentic_rl.logger") + async def test_upload_datasets_logs_code_3003( + self, + mock_logger, + _mock_parent_init, + ): + """upload_datasets should log + agentic_rl.DatasetsUploadFailed error.""" + agent = AgenticRL.__new__(AgenticRL) + object.__setattr__(agent, "tuning", MagicMock()) + agent.tuning.upload_datasets = AsyncMock(side_effect=IOError("fail")) + + with pytest.raises(DashScopeException): + await agent.upload_datasets() + + mock_logger.error.assert_called_once() + # Logger uses placeholder format: "[%s] %s | %s", + # name, message, exception + assert ( + mock_logger.error.call_args[0][1] + == "agentic_rl.DatasetsUploadFailed" + ) + + @patch( + "dashscope.finetune.agentic_rl.AgenticRLTuning.__init__", + return_value=None, + ) + @patch( + "dashscope.finetune.agentic_rl.generate_random_id", + return_value="abcd1234", + ) + @patch("dashscope.finetune.agentic_rl.logger") + def test_submit_job_duplicate_names_logs_code_3004( + self, + mock_logger, + _mock_random_id, + _mock_parent_init, + ): + """submit_job duplicate names should log + agentic_rl.DuplicateFunctionNames error.""" + agent = AgenticRL.__new__(AgenticRL) + object.__setattr__(agent, "tuning", MagicMock()) + agent.tuning.name = "test-job" + agent.tuning.model.name = "test-model" + agent.tuning.check_function_names.return_value = False + agent.tuning.datasets = [] + agent.tuning.combine_ids_runtimes.return_value = [] + agent.tuning.training.hyper_parameters = None + agent.tuning.training.resources = None + agent.tuning.training.type = "rl" + + with pytest.raises(InvalidParameter): + agent.submit_job() + + mock_logger.error.assert_called_once() + # Logger uses placeholder format: "[%s] %s", name, message + assert ( + mock_logger.error.call_args[0][1] + == "agentic_rl.DuplicateFunctionNames" + ) + + @patch( + "dashscope.finetune.agentic_rl.AgenticRLTuning.__init__", + return_value=None, + ) + @patch( + "dashscope.finetune.agentic_rl.generate_random_id", + return_value="abcd1234", + ) + @patch("dashscope.finetune.agentic_rl.CreateMixin.call") + @patch("dashscope.finetune.agentic_rl.logger") + def test_submit_job_call_failure_logs_code_3005( + self, + mock_logger, + mock_call, + _mock_random_id, + _mock_parent_init, + ): + """submit_job call failure should log + agentic_rl.JobSubmissionFailed error.""" + mock_call.side_effect = RuntimeError("API down") + + agent = AgenticRL.__new__(AgenticRL) + object.__setattr__(agent, "tuning", MagicMock()) + agent.tuning.name = "test-job" + agent.tuning.model.name = "test-model" + agent.tuning.check_function_names.return_value = True + agent.tuning.datasets = [] + agent.tuning.combine_ids_runtimes.return_value = [] + agent.tuning.training.hyper_parameters = None + agent.tuning.training.resources = None + agent.tuning.training.type = "rl" + + with pytest.raises(DashScopeException): + agent.submit_job() + + mock_logger.error.assert_called_once() + # Logger uses placeholder format: "[%s] %s | %s", + # name, message, exception + assert ( + mock_logger.error.call_args[0][1] + == "agentic_rl.JobSubmissionFailed" + ) + + @pytest.mark.asyncio + @patch( + "dashscope.finetune.agentic_rl.AgenticRLTuning.__init__", + return_value=None, + ) + @patch("dashscope.finetune.agentic_rl.logger") + async def test_run_logs_code_3006( + self, + mock_logger, + _mock_parent_init, + ): + """run should log agentic_rl.WorkflowFailed error on failure.""" + agent = AgenticRL.__new__(AgenticRL) + object.__setattr__(agent, "tuning", MagicMock()) + object.__setattr__( + agent, + "register_functions", + AsyncMock(side_effect=RuntimeError("step failed")), + ) + + with pytest.raises(DashScopeException): + await agent.run() + + mock_logger.error.assert_called_once() + # Logger uses placeholder format: "[%s] %s | %s", + # name, message, exception + assert mock_logger.error.call_args[0][1] == "agentic_rl.WorkflowFailed" + + @pytest.mark.asyncio + @patch("dashscope.finetune.agentic_rl.set_api_key") + @patch("dashscope.finetune.agentic_rl.logger") + async def test_test_functions_unsupported_type_logs_code_3007( + self, + mock_logger, + _mock_set_api_key, + ): + """test_functions unsupported type should log + agentic_rl.UnsupportedFunctionType error.""" + with pytest.raises(InvalidParameter): + await AgenticRL.test_functions( + instance_id="inst-1", + functype=MagicMock(spec=FunctionType), + input_data={}, + ) + + # Check all logged messages for the error name + error_names = [call[0][1] for call in mock_logger.error.call_args_list] + assert "agentic_rl.UnsupportedFunctionType" in error_names + assert "agentic_rl.FunctionTestFailed" not in error_names + + @pytest.mark.asyncio + @patch("dashscope.finetune.agentic_rl.set_api_key") + @patch("dashscope.finetune.agentic_rl.logger") + async def test_test_functions_validation_failure_logs_code_3008( + self, + mock_logger, + _mock_set_api_key, + ): + """test_functions validation failure should log + agentic_rl.FunctionTestFailed error.""" + with patch( + "dashscope.finetune.agentic_rl.RolloutInput.model_validate", + side_effect=ValueError("bad input"), + ): + with pytest.raises(InvalidParameter): + await AgenticRL.test_functions( + instance_id="inst-1", + functype=FunctionType.ROLLOUT, + input_data={"bad": "data"}, + ) + + mock_logger.error.assert_called_once() + # Logger uses placeholder format: "[%s] %s | %s", + # name, message, exception + assert ( + mock_logger.error.call_args[0][1] + == "agentic_rl.FunctionTestFailed" + ) + + +class TestPassthroughAttributeCompleteness: + """Verify that passthrough DashScopeException preserves ALL attributes.""" + + @patch( + "dashscope.finetune.agentic_rl.AgenticRLTuning.__init__", + return_value=None, + ) + @patch( + "dashscope.finetune.agentic_rl.generate_random_id", + return_value="abcd1234", + ) + @patch("dashscope.finetune.agentic_rl.CreateMixin.call") + def test_submit_job_passthrough_preserves_all_attributes( + self, + mock_call, + _mock_random_id, + _mock_parent_init, + ): + """Passthrough should preserve status_code, + error_code, request_id, and message.""" + original = DashScopeException("Rate limited by server") + original.status_code = 429 + original.error_code = "RateLimitError" + original.request_id = "req-abc-123" + mock_call.side_effect = original + + agent = AgenticRL.__new__(AgenticRL) + object.__setattr__(agent, "tuning", MagicMock()) + agent.tuning.name = "test-job" + agent.tuning.model.name = "test-model" + agent.tuning.check_function_names.return_value = True + agent.tuning.datasets = [] + agent.tuning.combine_ids_runtimes.return_value = [] + agent.tuning.training.hyper_parameters = None + agent.tuning.training.resources = None + agent.tuning.training.type = "rl" + + with pytest.raises(DashScopeException) as exc_info: + agent.submit_job() + + assert exc_info.value is original + assert exc_info.value.status_code == 429 + assert exc_info.value.error_code == "RateLimitError" + assert exc_info.value.request_id == "req-abc-123" + assert "Rate limited" in str(exc_info.value) + + +class TestMultipleUnderlyingExceptionTypes: + """Verify correct behavior with various underlying exception types.""" + + @pytest.mark.asyncio + @patch( + "dashscope.finetune.agentic_rl.AgenticRLTuning.__init__", + return_value=None, + ) + @pytest.mark.parametrize( + "underlying_exc", + [ + IOError("disk error"), + TimeoutError("timed out"), + ConnectionError("connection refused"), + PermissionError("access denied"), + MemoryError("out of memory"), + ], + ) + async def test_register_functions_handles_various_exceptions( + self, + _mock_parent_init, + underlying_exc, + ): + """register_functions should convert various + exception types to DashScopeException.""" + agent = AgenticRL.__new__(AgenticRL) + object.__setattr__(agent, "tuning", MagicMock()) + agent.tuning.register_functions = AsyncMock(side_effect=underlying_exc) + + with pytest.raises(DashScopeException) as exc_info: + await agent.register_functions() + + assert exc_info.value.status_code == 500 + assert exc_info.value.error_code == "InternalServerError" + assert exc_info.value.__cause__ is underlying_exc + + @pytest.mark.asyncio + @patch( + "dashscope.finetune.agentic_rl.AgenticRLTuning.__init__", + return_value=None, + ) + @pytest.mark.parametrize( + "underlying_exc", + [ + OSError("os error"), + RuntimeError("runtime error"), + SystemError("system error"), + ], + ) + async def test_upload_datasets_handles_various_exceptions( + self, + _mock_parent_init, + underlying_exc, + ): + """upload_datasets should convert various + exception types to DashScopeException.""" + agent = AgenticRL.__new__(AgenticRL) + object.__setattr__(agent, "tuning", MagicMock()) + agent.tuning.upload_datasets = AsyncMock(side_effect=underlying_exc) + + with pytest.raises(DashScopeException) as exc_info: + await agent.upload_datasets() + + assert exc_info.value.status_code == 500 + assert exc_info.value.error_code == "InternalServerError" + assert exc_info.value.__cause__ is underlying_exc diff --git a/tests/unit/test_agentic_rl_submit_job.py b/tests/unit/test_agentic_rl_submit_job.py index 9fc79ff..7d1b6bd 100644 --- a/tests/unit/test_agentic_rl_submit_job.py +++ b/tests/unit/test_agentic_rl_submit_job.py @@ -507,15 +507,13 @@ def test_init_with_valid_api_key(self): def test_init_with_invalid_api_key(self): from dashscope.finetune.agentic_rl import AgenticRL - from dashscope.finetune.reinforcement.common.errors import ( - ValueErrorWithCode, - ) + from dashscope.common.error import DashScopeException with patch( "dashscope.finetune.agentic_rl.set_api_key", side_effect=Exception("Invalid key"), ): - with pytest.raises(ValueErrorWithCode): + with pytest.raises(DashScopeException): AgenticRL(api_key="invalid") def test_init_without_api_key(self): @@ -603,9 +601,7 @@ async def test_register_functions_success(self): @pytest.mark.asyncio async def test_register_functions_failure(self): from dashscope.finetune.agentic_rl import AgenticRL - from dashscope.finetune.reinforcement.common.errors import ( - RegistrationError, - ) + from dashscope.common.error import DashScopeException client = AgenticRL() client.tuning = MagicMock() @@ -614,7 +610,7 @@ async def test_register_functions_failure(self): ) client.tuning.functions = [] - with pytest.raises(RegistrationError): + with pytest.raises(DashScopeException): await client.register_functions() @pytest.mark.asyncio @@ -634,9 +630,7 @@ async def test_upload_datasets_success(self): @pytest.mark.asyncio async def test_upload_datasets_failure(self): from dashscope.finetune.agentic_rl import AgenticRL - from dashscope.finetune.reinforcement.common.errors import ( - DatasetsError, - ) + from dashscope.common.error import DashScopeException client = AgenticRL() client.tuning = MagicMock() @@ -645,7 +639,7 @@ async def test_upload_datasets_failure(self): ) client.tuning.datasets = [] - with pytest.raises(DatasetsError): + with pytest.raises(DashScopeException): await client.upload_datasets() @@ -692,9 +686,7 @@ def test_submit_job_builds_correct_request(self): def test_submit_job_duplicate_function_names(self): from dashscope.finetune.agentic_rl import AgenticRL - from dashscope.finetune.reinforcement.common.errors import ( - ValueErrorWithCode, - ) + from dashscope.common.error import DashScopeException client = AgenticRL() client.tuning = MagicMock() @@ -711,7 +703,7 @@ def test_submit_job_duplicate_function_names(self): return_value=False, ) - with pytest.raises(ValueErrorWithCode, match="Duplicate"): + with pytest.raises(DashScopeException, match="request is invalid"): client.submit_job() @@ -846,9 +838,7 @@ async def test_run_full_workflow( @pytest.mark.asyncio async def test_run_register_failure_raises(self): from dashscope.finetune.agentic_rl import AgenticRL - from dashscope.finetune.reinforcement.common.errors import ( - RuntimeErrorWithCode, - ) + from dashscope.common.error import DashScopeException with patch.object( AgenticRL, @@ -859,15 +849,13 @@ async def test_run_register_failure_raises(self): client = AgenticRL() client.tuning = MagicMock() - with pytest.raises(RuntimeErrorWithCode): + with pytest.raises(DashScopeException): await client.run(model="qwen3.5-9b") @pytest.mark.asyncio async def test_run_upload_failure_raises(self): from dashscope.finetune.agentic_rl import AgenticRL - from dashscope.finetune.reinforcement.common.errors import ( - RuntimeErrorWithCode, - ) + from dashscope.common.error import DashScopeException with patch.object( AgenticRL, @@ -882,15 +870,13 @@ async def test_run_upload_failure_raises(self): client = AgenticRL() client.tuning = MagicMock() - with pytest.raises(RuntimeErrorWithCode): + with pytest.raises(DashScopeException): await client.run(model="qwen3.5-9b") @pytest.mark.asyncio async def test_run_submit_failure_raises(self): from dashscope.finetune.agentic_rl import AgenticRL - from dashscope.finetune.reinforcement.common.errors import ( - RuntimeErrorWithCode, - ) + from dashscope.common.error import DashScopeException with patch.object( AgenticRL, @@ -908,7 +894,7 @@ async def test_run_submit_failure_raises(self): client = AgenticRL() client.tuning = MagicMock() - with pytest.raises(RuntimeErrorWithCode): + with pytest.raises(DashScopeException): await client.run(model="qwen3.5-9b") @pytest.mark.asyncio diff --git a/tests/unit/test_agentstudio_protocol.py b/tests/unit/test_agentstudio_protocol.py index a028afc..ea0da73 100644 --- a/tests/unit/test_agentstudio_protocol.py +++ b/tests/unit/test_agentstudio_protocol.py @@ -56,6 +56,40 @@ def test_error_legacy_underscored_fields_still_parsed(): assert err.code == "rate_limit_error" +def test_legacy_permission_code_is_normalized(): + """Pre-standard ``permission_denied_error`` normalizes to the unified + ``permission_error`` (SDK normalization wins over the raw server code).""" + body = {"type": "error", "error": {"code": "permission_denied_error"}} + err = exceptions.from_response(status_code=403, body=body) + assert isinstance(err, exceptions.PermissionDeniedError) + assert err.code == "permission_error" + + +def test_404_default_message_has_no_unresolved_placeholder(): + """The registry 404 message is templated; the default must not leak the + raw ``{resource}`` placeholder when the server omits a message.""" + err = exceptions.from_response(status_code=404, body=None) + assert isinstance(err, exceptions.NotFoundError) + assert "{" not in err.message + assert err.message == "The requested resource was not found." + + +def test_504_code_and_message_are_consistent(): + """504 code and default message come from the same registry row, so + they no longer disagree (previously ``api_error`` + a timeout message).""" + err = exceptions.from_response(status_code=504, body=None) + assert err.code == "timeout_error" + assert "timed out" in err.message + + +def test_default_messages_sourced_from_registry(): + """Statuses the registry models carry a placeholder-free default.""" + for status in (400, 401, 403, 404, 429, 500, 503, 504): + err = exceptions.from_response(status_code=status, body=None) + assert "{" not in err.message + assert not err.message.startswith("HTTP ") + + def test_is_error_payload_detects_both_shapes(): assert is_error_payload( {"type": "error", "error": {"code": "x", "message": "y"}}, diff --git a/tests/unit/test_cli_common.py b/tests/unit/test_cli_common.py new file mode 100644 index 0000000..37e5a97 --- /dev/null +++ b/tests/unit/test_cli_common.py @@ -0,0 +1,263 @@ +# -*- coding: utf-8 -*- +"""Test cases for dashscope/cli/common.py error handling improvements.""" +from http import HTTPStatus +from unittest.mock import Mock + +import pytest +import typer + +from dashscope.cli.common import ( + print_failed_message, + ensure_ok, + handle_sdk_error, +) +from dashscope.api_entities.dashscope_response import DashScopeAPIResponse +from dashscope.common.error import AuthenticationError + + +class TestPrintFailedMessage: + """Test print_failed_message with various response scenarios.""" + + def test_complete_response(self, capsys): + """Test with all fields present.""" + rsp = DashScopeAPIResponse( + status_code=500, + request_id="req_123", + code="ServerError", + message="Internal server error", + ) + print_failed_message(rsp) + captured = capsys.readouterr() + assert "req_123" in captured.err + assert "500" in captured.err + assert "ServerError" in captured.err + assert "Internal server error" in captured.err + + def test_missing_request_id(self, capsys): + """Test when request_id is missing.""" + rsp = Mock() + rsp.status_code = 400 + rsp.code = "BadRequest" + rsp.message = "Invalid parameter" + # Simulate missing request_id attribute + del rsp.request_id + + print_failed_message(rsp) + captured = capsys.readouterr() + # Missing request_id should not be displayed (empty fields are omitted) + assert "request_id:" not in captured.err + # Error code is kept in original camelCase format + assert "BadRequest" in captured.err + assert "400" in captured.err + + def test_empty_code_and_message(self, capsys): + """Test when code and message are empty strings.""" + rsp = DashScopeAPIResponse( + status_code=503, + request_id="req_456", + code="", + message="", + ) + print_failed_message(rsp) + captured = capsys.readouterr() + # Should not show empty code/message fields + # Use word boundary check: "code: " with space after colon + assert ", code: " not in captured.err + assert ", message: " not in captured.err + assert "req_456" in captured.err + + def test_none_attributes(self, capsys): + """Test when attributes are None.""" + rsp = Mock() + rsp.status_code = 502 + rsp.request_id = None + rsp.code = None + rsp.message = None + + print_failed_message(rsp) + captured = capsys.readouterr() + # None values should not be displayed + assert ", request_id:" not in captured.err + assert ", code: " not in captured.err + assert ", message: " not in captured.err + assert "502" in captured.err + + +class TestEnsureOk: + """Test ensure_ok with various response scenarios.""" + + def test_successful_response(self): + """Test with successful HTTP 200 and no business error.""" + rsp = DashScopeAPIResponse( + status_code=HTTPStatus.OK, + request_id="req_ok", + code="", + message="", + output={"result": "success"}, + ) + result = ensure_ok(rsp) + assert result == {"result": "success"} + + def test_http_error(self, capsys): + """Test with HTTP error status.""" + rsp = DashScopeAPIResponse( + status_code=404, + request_id="req_404", + code="NotFound", + message="Resource not found", + ) + + with pytest.raises(typer.Exit): + ensure_ok(rsp) + + captured = capsys.readouterr() + # Should only print once (not duplicated) + # Error message now uses "Request failed" instead of "Failed" + assert captured.err.count("Request failed") == 1 + + def test_business_error_in_dict_output(self, capsys): + """Test with HTTP 200 but business error in dict output.""" + rsp = DashScopeAPIResponse( + status_code=HTTPStatus.OK, + request_id="req_biz_err", + code="", + message="", + output={"code": "InvalidParameter", "message": "Model not found"}, + ) + + with pytest.raises(typer.Exit): + ensure_ok(rsp) + + captured = capsys.readouterr() + # Rich may wrap long lines and add extra spaces, normalize whitespace + normalized_err = " ".join(captured.err.split()) + # Error code is kept in original camelCase format + assert "InvalidParameter" in normalized_err + assert "Model not found" in normalized_err + + def test_business_error_without_message(self, capsys): + """Test business error without message field.""" + rsp = DashScopeAPIResponse( + status_code=HTTPStatus.OK, + request_id="req_no_msg", + output={"code": "SomeError"}, # No message field + ) + + with pytest.raises(typer.Exit): + ensure_ok(rsp) + + captured = capsys.readouterr() + # Should show improved fallback message + # (normalize all whitespace for Rich formatting) + normalized_err = " ".join(captured.err.split()) + assert "API returned error code without message" in normalized_err + + def test_none_output(self, capsys): + """Test when output is None despite HTTP 200.""" + rsp = DashScopeAPIResponse( + status_code=HTTPStatus.OK, + request_id="req_null", + output=None, + ) + + with pytest.raises(typer.Exit): + ensure_ok(rsp) + + captured = capsys.readouterr() + assert "no output data" in captured.err + + def test_skip_business_error_check(self): + """Test with check_business_error=False.""" + rsp = DashScopeAPIResponse( + status_code=HTTPStatus.OK, + output={ + "code": "AsyncTaskPending", + "message": "Task is processing", + }, + ) + + # Should not raise even though there's a code in output + result = ensure_ok(rsp, check_business_error=False) + assert result == { + "code": "AsyncTaskPending", + "message": "Task is processing", + } + + def test_object_output_with_error(self, capsys): + """Test with object output containing error fields.""" + mock_output = Mock() + mock_output.code = "ObjectError" + mock_output.message = "Object-level error" + + rsp = DashScopeAPIResponse( + status_code=HTTPStatus.OK, + request_id="req_obj", + output=mock_output, + ) + + with pytest.raises(typer.Exit): + ensure_ok(rsp) + + captured = capsys.readouterr() + assert "ObjectError" in captured.err + + +class TestHandleSdkError: + """Test handle_sdk_error decorator.""" + + def test_dashscope_exception_handling(self, capsys): + """Test handling of DashScopeException.""" + + @handle_sdk_error("Test action") + def failing_function(): + # Create exception properly using __init__ with positional args + exc = AuthenticationError() + exc.request_id = "req_auth" + exc.code = "AuthFailed" + exc.message = "Invalid API key" + raise exc + + with pytest.raises(typer.Exit): + failing_function() + + captured = capsys.readouterr() + assert "Test action" in captured.err + assert "req_auth" in captured.err + # Error code is kept in original camelCase format (exception type name) + assert "AuthenticationError" in captured.err + + def test_generic_exception_handling(self, capsys): + """Test handling of generic exceptions.""" + + @handle_sdk_error("Generic test") + def generic_failing_function(): + raise ValueError("Something went wrong") + + with pytest.raises(typer.Exit): + generic_failing_function() + + captured = capsys.readouterr() + assert "Generic test" in captured.err + assert "Something went wrong" in captured.err + + def test_typer_exit_passthrough(self): + """Test that typer.Exit is re-raised without modification.""" + + @handle_sdk_error("Should not catch this") + def intentional_exit(): + raise typer.Exit(code=2) + + with pytest.raises(typer.Exit) as exc_info: + intentional_exit() + + assert exc_info.value.exit_code == 2 + + def test_successful_function_passthrough(self): + """Test that successful functions work normally.""" + + @handle_sdk_error("Success test") + def success_function(): + return "success" + + result = success_function() + assert result == "success" diff --git a/tests/unit/test_cli_main.py b/tests/unit/test_cli_main.py index d4c839c..4b178ef 100644 --- a/tests/unit/test_cli_main.py +++ b/tests/unit/test_cli_main.py @@ -46,7 +46,7 @@ def mock_list(**kwargs): combined_output = captured_output.out + captured_output.err assert exception_info.value.code == 1 - assert "No api key provided." in combined_output + assert "No api key provided" in combined_output assert "Traceback" not in combined_output def test_top_level_help_shows_global_api_key(self): diff --git a/tests/unit/test_utf8_encoding.py b/tests/unit/test_utf8_encoding.py index ebee93c..965f719 100644 --- a/tests/unit/test_utf8_encoding.py +++ b/tests/unit/test_utf8_encoding.py @@ -59,6 +59,7 @@ def _make_mock_sync_session(): class _MockAioResponse: status = 200 content_type = "application/json" + headers = {"Content-Type": "application/json; charset=utf-8"} async def json(self): return {"output": {"text": "ok"}, "request_id": "test-123"}