Skip to content
Merged
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
95 changes: 95 additions & 0 deletions src/opencode_a2a/error_taxonomy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
from __future__ import annotations

from dataclasses import dataclass

import httpx
from a2a.types import TaskState


@dataclass(frozen=True)
class UpstreamHTTPErrorProfile:
error_type: str
state: TaskState
default_message: str


_UPSTREAM_HTTP_ERROR_PROFILE_BY_STATUS: dict[int, UpstreamHTTPErrorProfile] = {
400: UpstreamHTTPErrorProfile(
"UPSTREAM_BAD_REQUEST",
TaskState.failed,
"OpenCode rejected the request due to invalid input",
),
401: UpstreamHTTPErrorProfile(
"UPSTREAM_UNAUTHORIZED",
TaskState.auth_required,
"OpenCode rejected the request due to authentication failure",
),
403: UpstreamHTTPErrorProfile(
"UPSTREAM_PERMISSION_DENIED",
TaskState.failed,
"OpenCode rejected the request due to insufficient permissions",
),
404: UpstreamHTTPErrorProfile(
"UPSTREAM_RESOURCE_NOT_FOUND",
TaskState.failed,
"OpenCode rejected the request because the target resource was not found",
),
429: UpstreamHTTPErrorProfile(
"UPSTREAM_QUOTA_EXCEEDED",
TaskState.failed,
"OpenCode rejected the request due to quota limits",
),
}


def resolve_upstream_http_error_profile(status: int) -> UpstreamHTTPErrorProfile:
if status in _UPSTREAM_HTTP_ERROR_PROFILE_BY_STATUS:
return _UPSTREAM_HTTP_ERROR_PROFILE_BY_STATUS[status]
if 400 <= status < 500:
return UpstreamHTTPErrorProfile(
"UPSTREAM_CLIENT_ERROR",
TaskState.failed,
f"OpenCode rejected the request with client error {status}",
)
if status >= 500:
return UpstreamHTTPErrorProfile(
"UPSTREAM_SERVER_ERROR",
TaskState.failed,
f"OpenCode rejected the request with server error {status}",
)
return UpstreamHTTPErrorProfile(
"UPSTREAM_HTTP_ERROR",
TaskState.failed,
f"OpenCode rejected the request with HTTP status {status}",
)


def extract_upstream_error_detail(response: httpx.Response | None) -> str | None:
if response is None:
return None

payload = None
try:
payload = response.json()
except Exception:
payload = None

if isinstance(payload, dict):
for key in ("detail", "error", "message"):
value = payload.get(key)
if isinstance(value, str):
value = value.strip()
if value:
return value

text = response.text.strip()
if text:
return text[:512]
return None


__all__ = [
"UpstreamHTTPErrorProfile",
"extract_upstream_error_detail",
"resolve_upstream_http_error_profile",
]
88 changes: 6 additions & 82 deletions src/opencode_a2a/execution/upstream_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
import httpx
from a2a.types import TaskState

from ..error_taxonomy import (
extract_upstream_error_detail as _extract_upstream_error_detail,
)
from ..error_taxonomy import (
resolve_upstream_http_error_profile as _resolve_upstream_error_profile,
)
from ..opencode_upstream_client import UpstreamContractError


Expand All @@ -18,13 +24,6 @@ class _StreamTerminalSignal:
upstream_status: int | None = None


@dataclass(frozen=True)
class _UpstreamErrorProfile:
error_type: str
state: TaskState
default_message: str


@dataclass(frozen=True)
class _UpstreamInBandError:
error_type: str
Expand All @@ -33,81 +32,6 @@ class _UpstreamInBandError:
upstream_status: int | None = None


_UPSTREAM_HTTP_ERROR_PROFILE_BY_STATUS: dict[int, _UpstreamErrorProfile] = {
400: _UpstreamErrorProfile(
"UPSTREAM_BAD_REQUEST",
TaskState.failed,
"OpenCode rejected the request due to invalid input",
),
401: _UpstreamErrorProfile(
"UPSTREAM_UNAUTHORIZED",
TaskState.auth_required,
"OpenCode rejected the request due to authentication failure",
),
403: _UpstreamErrorProfile(
"UPSTREAM_PERMISSION_DENIED",
TaskState.failed,
"OpenCode rejected the request due to insufficient permissions",
),
404: _UpstreamErrorProfile(
"UPSTREAM_RESOURCE_NOT_FOUND",
TaskState.failed,
"OpenCode rejected the request because the target resource was not found",
),
429: _UpstreamErrorProfile(
"UPSTREAM_QUOTA_EXCEEDED",
TaskState.failed,
"OpenCode rejected the request due to quota limits",
),
}


def _resolve_upstream_error_profile(status: int) -> _UpstreamErrorProfile:
if status in _UPSTREAM_HTTP_ERROR_PROFILE_BY_STATUS:
return _UPSTREAM_HTTP_ERROR_PROFILE_BY_STATUS[status]
if 400 <= status < 500:
return _UpstreamErrorProfile(
"UPSTREAM_CLIENT_ERROR",
TaskState.failed,
f"OpenCode rejected the request with client error {status}",
)
if status >= 500:
return _UpstreamErrorProfile(
"UPSTREAM_SERVER_ERROR",
TaskState.failed,
f"OpenCode rejected the request with server error {status}",
)
return _UpstreamErrorProfile(
"UPSTREAM_HTTP_ERROR",
TaskState.failed,
f"OpenCode rejected the request with HTTP status {status}",
)


def _extract_upstream_error_detail(response: httpx.Response | None) -> str | None:
if response is None:
return None

payload = None
try:
payload = response.json()
except Exception:
payload = None

if isinstance(payload, dict):
for key in ("detail", "error", "message"):
value = payload.get(key)
if isinstance(value, str):
value = value.strip()
if value:
return value

text = response.text.strip()
if text:
return text[:512]
return None


def _format_upstream_error(
exc: httpx.HTTPStatusError, *, request: str
) -> tuple[str, TaskState, str]:
Expand Down
Loading
Loading