Skip to content

Commit 1abdfc9

Browse files
refactor(error): centralize upstream taxonomy and jsonrpc translators (#299)
1 parent 5395342 commit 1abdfc9

File tree

5 files changed

+447
-323
lines changed

5 files changed

+447
-323
lines changed

src/opencode_a2a/error_taxonomy.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
from __future__ import annotations
2+
3+
from dataclasses import dataclass
4+
5+
import httpx
6+
from a2a.types import TaskState
7+
8+
9+
@dataclass(frozen=True)
10+
class UpstreamHTTPErrorProfile:
11+
error_type: str
12+
state: TaskState
13+
default_message: str
14+
15+
16+
_UPSTREAM_HTTP_ERROR_PROFILE_BY_STATUS: dict[int, UpstreamHTTPErrorProfile] = {
17+
400: UpstreamHTTPErrorProfile(
18+
"UPSTREAM_BAD_REQUEST",
19+
TaskState.failed,
20+
"OpenCode rejected the request due to invalid input",
21+
),
22+
401: UpstreamHTTPErrorProfile(
23+
"UPSTREAM_UNAUTHORIZED",
24+
TaskState.auth_required,
25+
"OpenCode rejected the request due to authentication failure",
26+
),
27+
403: UpstreamHTTPErrorProfile(
28+
"UPSTREAM_PERMISSION_DENIED",
29+
TaskState.failed,
30+
"OpenCode rejected the request due to insufficient permissions",
31+
),
32+
404: UpstreamHTTPErrorProfile(
33+
"UPSTREAM_RESOURCE_NOT_FOUND",
34+
TaskState.failed,
35+
"OpenCode rejected the request because the target resource was not found",
36+
),
37+
429: UpstreamHTTPErrorProfile(
38+
"UPSTREAM_QUOTA_EXCEEDED",
39+
TaskState.failed,
40+
"OpenCode rejected the request due to quota limits",
41+
),
42+
}
43+
44+
45+
def resolve_upstream_http_error_profile(status: int) -> UpstreamHTTPErrorProfile:
46+
if status in _UPSTREAM_HTTP_ERROR_PROFILE_BY_STATUS:
47+
return _UPSTREAM_HTTP_ERROR_PROFILE_BY_STATUS[status]
48+
if 400 <= status < 500:
49+
return UpstreamHTTPErrorProfile(
50+
"UPSTREAM_CLIENT_ERROR",
51+
TaskState.failed,
52+
f"OpenCode rejected the request with client error {status}",
53+
)
54+
if status >= 500:
55+
return UpstreamHTTPErrorProfile(
56+
"UPSTREAM_SERVER_ERROR",
57+
TaskState.failed,
58+
f"OpenCode rejected the request with server error {status}",
59+
)
60+
return UpstreamHTTPErrorProfile(
61+
"UPSTREAM_HTTP_ERROR",
62+
TaskState.failed,
63+
f"OpenCode rejected the request with HTTP status {status}",
64+
)
65+
66+
67+
def extract_upstream_error_detail(response: httpx.Response | None) -> str | None:
68+
if response is None:
69+
return None
70+
71+
payload = None
72+
try:
73+
payload = response.json()
74+
except Exception:
75+
payload = None
76+
77+
if isinstance(payload, dict):
78+
for key in ("detail", "error", "message"):
79+
value = payload.get(key)
80+
if isinstance(value, str):
81+
value = value.strip()
82+
if value:
83+
return value
84+
85+
text = response.text.strip()
86+
if text:
87+
return text[:512]
88+
return None
89+
90+
91+
__all__ = [
92+
"UpstreamHTTPErrorProfile",
93+
"extract_upstream_error_detail",
94+
"resolve_upstream_http_error_profile",
95+
]

src/opencode_a2a/execution/upstream_errors.py

Lines changed: 6 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@
77
import httpx
88
from a2a.types import TaskState
99

10+
from ..error_taxonomy import (
11+
extract_upstream_error_detail as _extract_upstream_error_detail,
12+
)
13+
from ..error_taxonomy import (
14+
resolve_upstream_http_error_profile as _resolve_upstream_error_profile,
15+
)
1016
from ..opencode_upstream_client import UpstreamContractError
1117

1218

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

2026

21-
@dataclass(frozen=True)
22-
class _UpstreamErrorProfile:
23-
error_type: str
24-
state: TaskState
25-
default_message: str
26-
27-
2827
@dataclass(frozen=True)
2928
class _UpstreamInBandError:
3029
error_type: str
@@ -33,81 +32,6 @@ class _UpstreamInBandError:
3332
upstream_status: int | None = None
3433

3534

36-
_UPSTREAM_HTTP_ERROR_PROFILE_BY_STATUS: dict[int, _UpstreamErrorProfile] = {
37-
400: _UpstreamErrorProfile(
38-
"UPSTREAM_BAD_REQUEST",
39-
TaskState.failed,
40-
"OpenCode rejected the request due to invalid input",
41-
),
42-
401: _UpstreamErrorProfile(
43-
"UPSTREAM_UNAUTHORIZED",
44-
TaskState.auth_required,
45-
"OpenCode rejected the request due to authentication failure",
46-
),
47-
403: _UpstreamErrorProfile(
48-
"UPSTREAM_PERMISSION_DENIED",
49-
TaskState.failed,
50-
"OpenCode rejected the request due to insufficient permissions",
51-
),
52-
404: _UpstreamErrorProfile(
53-
"UPSTREAM_RESOURCE_NOT_FOUND",
54-
TaskState.failed,
55-
"OpenCode rejected the request because the target resource was not found",
56-
),
57-
429: _UpstreamErrorProfile(
58-
"UPSTREAM_QUOTA_EXCEEDED",
59-
TaskState.failed,
60-
"OpenCode rejected the request due to quota limits",
61-
),
62-
}
63-
64-
65-
def _resolve_upstream_error_profile(status: int) -> _UpstreamErrorProfile:
66-
if status in _UPSTREAM_HTTP_ERROR_PROFILE_BY_STATUS:
67-
return _UPSTREAM_HTTP_ERROR_PROFILE_BY_STATUS[status]
68-
if 400 <= status < 500:
69-
return _UpstreamErrorProfile(
70-
"UPSTREAM_CLIENT_ERROR",
71-
TaskState.failed,
72-
f"OpenCode rejected the request with client error {status}",
73-
)
74-
if status >= 500:
75-
return _UpstreamErrorProfile(
76-
"UPSTREAM_SERVER_ERROR",
77-
TaskState.failed,
78-
f"OpenCode rejected the request with server error {status}",
79-
)
80-
return _UpstreamErrorProfile(
81-
"UPSTREAM_HTTP_ERROR",
82-
TaskState.failed,
83-
f"OpenCode rejected the request with HTTP status {status}",
84-
)
85-
86-
87-
def _extract_upstream_error_detail(response: httpx.Response | None) -> str | None:
88-
if response is None:
89-
return None
90-
91-
payload = None
92-
try:
93-
payload = response.json()
94-
except Exception:
95-
payload = None
96-
97-
if isinstance(payload, dict):
98-
for key in ("detail", "error", "message"):
99-
value = payload.get(key)
100-
if isinstance(value, str):
101-
value = value.strip()
102-
if value:
103-
return value
104-
105-
text = response.text.strip()
106-
if text:
107-
return text[:512]
108-
return None
109-
110-
11135
def _format_upstream_error(
11236
exc: httpx.HTTPStatusError, *, request: str
11337
) -> tuple[str, TaskState, str]:

0 commit comments

Comments
 (0)