Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -67,16 +67,21 @@ with client.sessions.events.stream(session.id) as stream:
| Error code/exception | HTTP status | Meaning | Handling |
| --- | --- | --- | --- |
| `InputRequired` / `ModelRequired` / `InvalidInput` | raised locally | missing required params such as app_id/prompt/model | add params per the exception message; Application failures do not raise — check status_code |
| `invalid_request_error` (InvalidRequestError) | 400 | invalid agentstudio request parameters | check parameter names/types, e.g. empty events raises a local ValueError |
| `invalid_request_error` / `request_too_large` (InvalidRequestError) | 400/413 | invalid agentstudio request parameters, or payload too large | check parameter names/types, e.g. empty events raises a local ValueError |
| `authentication_error` (AuthenticationError) | 401 | api_key missing or invalid | check api_key or DASHSCOPE_API_KEY |
| `permission_denied_error` (PermissionDeniedError) | 403 | no access to the resource | check workspace/uid and resource ownership |
| `permission_denied_error` / `permission_error` (PermissionDeniedError) | 403 | no access to the resource | check workspace/uid and resource ownership |
| `not_found_error` (NotFoundError) | 404 | agent/session and other resources do not exist | verify the id and region/workspace are correct |
| `conflict_error` (ConflictError) | 409 | version in agents.update does not match the server | retrieve the latest version first, then update |
| `rate_limit_error` (RateLimitError) | 429 | rate limited | retry with backoff (client default max_retries=2) |
| `api_error` (InternalServerError) / `overloaded_error` (OverloadedError) | 500/502/504 / 503 | server error/overloaded | troubleshoot with `request_id` or retry |
| `rate_limit_error` / `billing_error` (RateLimitError) | 429 | rate limited or out of quota | retry with backoff (client default max_retries=2) |
| `api_error` / `timeout_error` (InternalServerError), `overloaded_error` (OverloadedError) | 500/502/504 / 503 | server error/overloaded | troubleshoot with `request_id` or retry |
| `APITimeoutError` / `APIConnectionError` | no response | connection/read/write timeout | increase timeout, check network, then retry |
| `StreamError` / `StreamClosedError` | mid-stream | SSE protocol error/stream closed | call `events.stream` again to reopen the stream |

Match on the exception class, not on `.code`: `.code` echoes the server's raw
string, so a 403 may arrive as either `permission_denied_error` or
`permission_error`, and it is normalised to `api_error` when the server omits
it. The class is always the one listed above.

## Java SDK

Java SDK v2.22.23 entry points:
Expand Down
119 changes: 84 additions & 35 deletions dashscope/agentstudio/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,46 @@
"request_id": "req_..."
}

The pre-release backend currently emits ``error_code``/``error_message``
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 come from the server response and are preserved as-is on ``.code``.
When no code is present, :func:`from_response` reports the generic
``api_error`` rather than inventing a public code from the status number.
The raw payload stays on ``.raw``.

The exception *type* is resolved separately: a recognized server code wins,
otherwise the HTTP status picks the class. Callers can therefore keep using
``except NotFoundError`` while ``.code`` still carries whatever the server
actually said (including service-specific codes such as
``bma_invalid_event``).

The pre-release backend emits ``error_code``/``error_message`` instead of
nested ``error.{code,message}``. Both shapes are accepted; the compatibility
branches are marked ``# TODO(bma-fix)`` for removal once the backend aligns.
"""

from __future__ import annotations

from typing import Any, Dict, Mapping, Optional

from dashscope.common.error import DashScopeException
from dashscope.common.error_registry import (
SDK_AGENTSTUDIO_API_CONNECTION_ERROR,
SDK_AGENTSTUDIO_API_TIMEOUT_ERROR,
SDK_AGENTSTUDIO_STREAM_CLOSED_ERROR,
SDK_AGENTSTUDIO_STREAM_ERROR,
INTERNAL_ERROR,
)


def describe_exception(err: BaseException) -> str:
"""Render an exception into a message worth showing the caller.

``httpx`` timeout exceptions usually carry no message at all, so a bare
``str(err)`` would surface an empty string. Fall back to the type name.
"""
detail = str(err).strip()
if detail:
return f"{type(err).__name__}: {detail}"
return type(err).__name__


class AgentStudioError(DashScopeException):
Expand Down Expand Up @@ -75,13 +104,13 @@ def __repr__(self) -> str: # pragma: no cover - debug helper
class APIConnectionError(AgentStudioError):
"""Raised when the HTTP request fails before a response is read."""

code = "api_connection_error"
code = SDK_AGENTSTUDIO_API_CONNECTION_ERROR.name


class APITimeoutError(APIConnectionError):
"""Raised on connect / read timeouts."""

code = "api_timeout_error"
code = SDK_AGENTSTUDIO_API_TIMEOUT_ERROR.name


# ---------------------------------------------------------------------------
Expand All @@ -90,7 +119,12 @@ class APITimeoutError(APIConnectionError):


class APIStatusError(AgentStudioError):
"""Raised when the server returns a non-2xx status."""
"""Raised when the server returns a non-2xx status.

Subclasses below give callers a stable type to catch. ``.code`` is always
the server's own value, so the class carries no information that the code
attribute does not -- it exists for ``except`` clauses.
"""

code = "api_status_error"

Expand Down Expand Up @@ -124,7 +158,7 @@ class OverloadedError(APIStatusError):


class InternalServerError(APIStatusError):
code = "api_error"
code = INTERNAL_ERROR.anthropic_error_code

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个check下



# ---------------------------------------------------------------------------
Expand All @@ -135,42 +169,54 @@ class InternalServerError(APIStatusError):
class StreamError(AgentStudioError):
"""Raised when an SSE stream encounters a fatal protocol error."""

code = "stream_error"
code = SDK_AGENTSTUDIO_STREAM_ERROR.name


class StreamClosedError(StreamError):
"""Raised when consumers attempt I/O on an already-closed stream."""

code = "stream_closed"
code = SDK_AGENTSTUDIO_STREAM_CLOSED_ERROR.name


# ---------------------------------------------------------------------------
# Dispatcher
# ---------------------------------------------------------------------------


_STATUS_TO_DEFAULT: Dict[int, type] = {
400: InvalidRequestError,
401: AuthenticationError,
403: PermissionDeniedError,
404: NotFoundError,
# 408 pairs with 504: both are timeouts, which the registry classes
# as InternalServerError (timeout_error).
408: InternalServerError,
409: ConflictError,
413: InvalidRequestError,
429: RateLimitError,
500: InternalServerError,
502: InternalServerError,
503: OverloadedError,
504: InternalServerError,
}

# Covers every anthropic_error_code the error registry can emit, plus the
# registry's request_timeout key spelling and the legacy
# permission_denied_error / conflict_error spellings. Each entry agrees
# with _STATUS_TO_DEFAULT for the status the registry pairs it with.
_CODE_TO_CLASS: Dict[str, type] = {
"invalid_request_error": InvalidRequestError,
"authentication_error": AuthenticationError,
"permission_error": PermissionDeniedError,
"permission_denied_error": PermissionDeniedError,
"not_found_error": NotFoundError,
"conflict_error": ConflictError,
"request_too_large": InvalidRequestError,
"rate_limit_error": RateLimitError,
"billing_error": RateLimitError,
"overloaded_error": OverloadedError,
"api_error": InternalServerError,
"timeout_error": InternalServerError,
"request_timeout": InternalServerError,
}


Expand All @@ -179,16 +225,21 @@ def from_response(
status_code: int,
body: Any,
headers: Optional[Mapping[str, str]] = None,
) -> AgentStudioError:
"""Build an :class:`AgentStudioError` instance from a HTTP response.
) -> APIStatusError:
"""Build an :class:`APIStatusError` instance from a HTTP response.

Accepts both the documented ``{type, error:{code,message}, request_id}``
shape and the pre-release ``{type, error:{error_code, error_message}}``
shape. Falls back to a Spring default ``{timestamp,status,error,path}``
when the body is not JSON-serializable.
Accepts the documented ``{type, error:{code,message}, request_id}`` shape,
the pre-release ``error:{error_code,error_message}`` shape, the classic
flat DashScope ``{code, message, request_id}`` envelope, and falls back to
a Spring default ``{timestamp,status,error,path}`` page.

The ``x-request-id`` response header is preferred over the body
``request_id`` field (server-generated IDs are more reliable for tracing).

The server's code is preserved as-is on ``.code``; only when no code is
present does it fall back to the generic ``api_error``. The exception
class is resolved separately -- a recognized code wins, otherwise the
HTTP status decides.
"""

code: Optional[str] = None
Expand All @@ -211,30 +262,28 @@ def from_response(
message = err.get("message") or err.get(
"error_message",
) # TODO(bma-fix)
# Spring default fallback.
if (
message is None
and "error" in body
and isinstance(body["error"], str)
):
# Spring default fallback. Its ``error`` field is a human phrase
# ("Not Found"), so it informs the message but never the code.
if message is None and isinstance(body.get("error"), str):
message = body["error"]
code = body.get("error") or "api_error"
# Flat DashScope envelope: code/message at the top level.
if code is None:
code = body.get("code")
if message is None:
message = body.get("message")

# Classify before normalizing: the synthesized generic code below must not
# drive classification, or a bodiless 404 would report InternalServerError.
cls = _CODE_TO_CLASS.get(code) if code else None
if cls is None:
cls = _STATUS_TO_DEFAULT.get(status_code, APIStatusError)

if not code:
code = INTERNAL_ERROR.anthropic_error_code

if message is None:
message = f"HTTP {status_code}"
if code is None:
code = _STATUS_TO_DEFAULT.get( # type: ignore[attr-defined]
status_code,
APIStatusError,
).code

cls = (
_CODE_TO_CLASS.get(code)
or _STATUS_TO_DEFAULT.get(status_code)
or APIStatusError
)

return cls(
message,
code=code,
Expand Down
12 changes: 10 additions & 2 deletions dashscope/agentstudio/streaming.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,11 @@ def _produce() -> None:
if item is None:
return
if isinstance(item, Exception):
raise item
if isinstance(item, exceptions.AgentStudioError):
raise item
raise exceptions.StreamError(
exceptions.describe_exception(item),
) from item
yield item
finally:
self.close()
Expand Down Expand Up @@ -205,7 +209,11 @@ async def _produce() -> None:
if item is None:
return
if isinstance(item, Exception):
raise item
if isinstance(item, exceptions.AgentStudioError):
raise item
raise exceptions.StreamError(
exceptions.describe_exception(item),
) from item
yield item
finally:
await self.aclose()
Expand Down
19 changes: 14 additions & 5 deletions dashscope/agentstudio/transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
AGENTSTUDIO_DEFAULT_TIMEOUT,
AGENTSTUDIO_MAX_RETRIES,
)
from dashscope.common.error_registry import AUTH_FAILED
from dashscope.common.utils import get_sdk_headers

logger = logging.getLogger("dashscope.agentstudio")
Expand Down Expand Up @@ -223,7 +224,7 @@ def build_headers(
raise exceptions.AuthenticationError(
"api_key is required. Pass it via Client(api_key=...) or "
"the DASHSCOPE_API_KEY environment variable.",
code="authentication_error",
status_code=AUTH_FAILED.status_code,
)

headers: Dict[str, str] = {
Expand Down Expand Up @@ -363,14 +364,18 @@ def request( # pylint: disable=too-many-branches
except httpx.TimeoutException as exc:
last_exc = exc
if attempt >= self.max_retries:
raise exceptions.APITimeoutError(str(exc)) from exc
raise exceptions.APITimeoutError(
exceptions.describe_exception(exc),
) from exc
except Exception as exc:
if isinstance(exc, exceptions.AgentStudioError):
raise
should_retry, _ = _should_retry_exception(exc)
last_exc = exc
if attempt >= self.max_retries or not should_retry:
raise exceptions.APIConnectionError(str(exc)) from exc
raise exceptions.APIConnectionError(
exceptions.describe_exception(exc),
) from exc
else:
if stream:
if resp.status_code >= 400:
Expand Down Expand Up @@ -587,14 +592,18 @@ async def request( # pylint: disable=too-many-branches
except httpx.TimeoutException as exc:
last_exc = exc
if attempt >= self.max_retries:
raise exceptions.APITimeoutError(str(exc)) from exc
raise exceptions.APITimeoutError(
exceptions.describe_exception(exc),
) from exc
except Exception as exc:
if isinstance(exc, exceptions.AgentStudioError):
raise
should_retry, _ = _should_retry_exception(exc)
last_exc = exc
if attempt >= self.max_retries or not should_retry:
raise exceptions.APIConnectionError(str(exc)) from exc
raise exceptions.APIConnectionError(
exceptions.describe_exception(exc),
) from exc
else:
if stream:
if resp.status_code >= 400:
Expand Down
Loading
Loading