Skip to content

Commit e666ee7

Browse files
committed
Simplify model rate-limit run errors
Constraint: Aone 83566999 only needs model 429 output to terminate as AG-UI RUN_ERROR without hardcoded user-facing copy.\nRejected: fixed localized message or broad error framework | original provider error text is required and a small regex/status-code guard is enough.\nConfidence: high\nScope-risk: narrow\nDirective: Keep RUN_ERROR.message sourced from the original error text; do not introduce generic friendly copy.\nTested: uv run --extra server pytest targeted files -q; uv run ruff check targeted source/server-test files; git diff --check; uvicorn local harness for text 429, structured 429, and normal text.\nNot-tested: GitHub CI final status and DCO fix; earlier pushed commits still require sign-off remediation. Signed-off-by: congxiao.wxx <congxiao.wxx@alibaba-inc.com> Change-Id: I248d3f29cfd95560edc48f22d2bd014cc5762cef Co-developed-by: Codex <noreply@openai.com> Signed-off-by: congxiao.wxx <congxiao.wxx@alibaba-inc.com>
1 parent 48ccc11 commit e666ee7

9 files changed

Lines changed: 134 additions & 503 deletions

File tree

agentrun/integration/langgraph/agent_converter.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,11 @@
2929
import json
3030
from typing import Any, Dict, Iterator, List, Optional, Union
3131

32+
from agentrun.server.model import AgentResult, EventType
3233
from agentrun.utils.error_utils import (
3334
build_error_event_data,
34-
format_error_message,
35+
is_rate_limited_error,
3536
)
36-
from agentrun.server.model import AgentResult, EventType
3737
from agentrun.utils.log import logger
3838

3939
# 需要从工具输入中过滤掉的内部字段(LangGraph/MCP 注入的运行时对象)
@@ -945,22 +945,40 @@ def _convert_astream_events_event(
945945
# 7. LLM 错误
946946
elif event_type == "on_llm_error":
947947
error = data.get("error")
948-
error_message = format_error_message(error)
948+
error_message = ""
949+
if error is not None:
950+
if isinstance(error, Exception):
951+
error_message = f"{type(error).__name__}: {str(error)}"
952+
elif isinstance(error, str):
953+
error_message = error
954+
else:
955+
error_message = str(error)
949956

950957
yield AgentResult(
951958
event=EventType.ERROR,
952959
data=build_error_event_data(
953960
error,
954961
fallback_code="LLM_ERROR",
955-
fallback_message=f"LLM error: {error_message}",
962+
fallback_message=(
963+
error_message
964+
if is_rate_limited_error(error)
965+
else f"LLM error: {error_message}"
966+
),
956967
),
957968
)
958969

959970
# 8. Chain 错误
960971
elif event_type == "on_chain_error":
961972
error = data.get("error")
962973
chain_name = event_dict.get("name", "")
963-
error_message = format_error_message(error)
974+
error_message = ""
975+
if error is not None:
976+
if isinstance(error, Exception):
977+
error_message = f"{type(error).__name__}: {str(error)}"
978+
elif isinstance(error, str):
979+
error_message = error
980+
else:
981+
error_message = str(error)
964982

965983
yield AgentResult(
966984
event=EventType.ERROR,
@@ -978,7 +996,14 @@ def _convert_astream_events_event(
978996
elif event_type == "on_retriever_error":
979997
error = data.get("error")
980998
retriever_name = event_dict.get("name", "")
981-
error_message = format_error_message(error)
999+
error_message = ""
1000+
if error is not None:
1001+
if isinstance(error, Exception):
1002+
error_message = f"{type(error).__name__}: {str(error)}"
1003+
elif isinstance(error, str):
1004+
error_message = error
1005+
else:
1006+
error_message = str(error)
9821007

9831008
yield AgentResult(
9841009
event=EventType.ERROR,

agentrun/server/agui_protocol.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -748,15 +748,11 @@ def _process_event_with_boundaries(
748748
message=event.data.get("message", ""),
749749
code=event.data.get("code"),
750750
)
751-
extra_fields = {}
752-
for key in RUN_ERROR_EXTRA_FIELDS:
753-
value = event.data.get(key)
754-
if value is not None:
755-
extra_fields[key] = value
756-
elif event.addition:
757-
value = event.addition.get(key)
758-
if value is not None:
759-
extra_fields[key] = value
751+
extra_fields = {
752+
key: event.data[key]
753+
for key in RUN_ERROR_EXTRA_FIELDS
754+
if key in event.data
755+
}
760756
if extra_fields:
761757
agui_event = agui_event.model_copy(update=extra_fields)
762758
yield self._encoder.encode(agui_event)

agentrun/server/invoker.py

Lines changed: 24 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@
2424
)
2525
import uuid
2626

27-
from agentrun.utils.error_utils import build_error_event_data
27+
from agentrun.utils.error_utils import (
28+
build_error_event_data,
29+
is_rate_limited_error,
30+
)
2831
from .model import AgentEvent, AgentRequest, EventType
2932
from .protocol import (
3033
AsyncInvokeAgentHandler,
@@ -118,10 +121,7 @@ async def invoke_stream(
118121
if isinstance(item, str):
119122
if not item: # 跳过空字符串
120123
continue
121-
yield AgentEvent(
122-
event=EventType.TEXT,
123-
data={"delta": item},
124-
)
124+
yield self._wrap_text(item)
125125

126126
elif isinstance(item, AgentEvent):
127127
# 处理用户返回的事件
@@ -232,12 +232,7 @@ def _wrap_non_stream(self, result: Any) -> List[AgentEvent]:
232232
return results
233233

234234
if isinstance(result, str):
235-
results.append(
236-
AgentEvent(
237-
event=EventType.TEXT,
238-
data={"delta": result},
239-
)
240-
)
235+
results.append(self._wrap_text(result))
241236

242237
elif isinstance(result, AgentEvent):
243238
# 处理可能的 TOOL_CALL 展开
@@ -248,12 +243,7 @@ def _wrap_non_stream(self, result: Any) -> List[AgentEvent]:
248243
if isinstance(item, AgentEvent):
249244
results.extend(self._process_user_event(item))
250245
elif isinstance(item, str) and item:
251-
results.append(
252-
AgentEvent(
253-
event=EventType.TEXT,
254-
data={"delta": item},
255-
)
256-
)
246+
results.append(self._wrap_text(item))
257247
else:
258248
results.extend(self._wrap_model_chunk(item))
259249

@@ -280,10 +270,7 @@ async def _wrap_stream(
280270
if isinstance(item, str):
281271
if not item:
282272
continue
283-
yield AgentEvent(
284-
event=EventType.TEXT,
285-
data={"delta": item},
286-
)
273+
yield self._wrap_text(item)
287274

288275
elif isinstance(item, AgentEvent):
289276
for processed_event in self._process_user_event(item):
@@ -351,15 +338,25 @@ def _wrap_model_chunk(self, item: Any) -> List[AgentEvent]:
351338

352339
content = self._read_attr_or_key(item, "content")
353340
if isinstance(content, str) and content:
354-
events.append(
355-
AgentEvent(
356-
event=EventType.TEXT,
357-
data={"delta": content},
358-
)
359-
)
341+
events.append(self._wrap_text(content))
360342

361343
return events
362344

345+
def _wrap_text(self, text: str) -> AgentEvent:
346+
if is_rate_limited_error(text):
347+
return AgentEvent(
348+
event=EventType.ERROR,
349+
data=build_error_event_data(
350+
text,
351+
fallback_code=type(text).__name__,
352+
fallback_message=text,
353+
),
354+
)
355+
return AgentEvent(
356+
event=EventType.TEXT,
357+
data={"delta": text},
358+
)
359+
363360
def _read_attr_or_key(self, obj: Any, key: str) -> Any:
364361
if isinstance(obj, dict):
365362
return obj.get(key)

agentrun/utils/error_utils.py

Lines changed: 30 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,29 @@
1-
"""Error helpers for AgentRun event streams."""
1+
"""Small helpers for model rate-limit errors."""
22

33
import re
44
from typing import Any, Dict, Optional
55

66
RATE_LIMITED_CODE = "RATE_LIMITED"
77
RATE_LIMITED_RETRY_AFTER_MS = 2000
8-
8+
_RATE_LIMIT_TEXT_RE = re.compile(
9+
r"429|too[-_\s]*many[-_\s]*requests|rate[-_\s]*limit|throttl",
10+
re.IGNORECASE,
11+
)
912
_RATE_LIMIT_CODES = {
1013
"ratelimitexceeded",
1114
"ratelimited",
12-
"resourcethrottled",
1315
"throttling",
14-
"throttlingquota",
15-
"throttlingratequota",
16-
"throttlingexception",
1716
"toomanyrequests",
1817
}
1918

20-
_RATE_LIMIT_TEXT_PATTERNS = [
21-
re.compile(r"\btoo[-_\s]*many[-_\s]*requests\b", re.IGNORECASE),
22-
re.compile(
23-
r"\brate[-_\s]*limit(?:ed|[-_\s]*exceeded)\b",
24-
re.IGNORECASE,
25-
),
26-
re.compile(r"\bresource[-_\s]*throttled\b", re.IGNORECASE),
27-
re.compile(
28-
r"\b(?:throttling|throttlingexception|throttled)\b",
29-
re.IGNORECASE,
30-
),
31-
]
32-
33-
34-
def format_error_message(error: Any) -> str:
35-
"""Format errors consistently with existing LangGraph conversion."""
36-
if error is None:
37-
return ""
38-
if isinstance(error, Exception):
39-
return f"{type(error).__name__}: {str(error)}"
40-
return str(error)
41-
4219

4320
def build_error_event_data(
4421
error: Any,
4522
*,
4623
fallback_code: str,
4724
fallback_message: str,
4825
) -> Dict[str, Any]:
49-
"""Build AgentEvent ERROR data, normalizing model rate limits."""
26+
"""Keep the original message; add rate-limit metadata only when matched."""
5027
if not is_rate_limited_error(error):
5128
return {"message": fallback_message, "code": fallback_code}
5229

@@ -56,70 +33,42 @@ def build_error_event_data(
5633
"retryable": True,
5734
"retryAfterMs": RATE_LIMITED_RETRY_AFTER_MS,
5835
}
59-
trace_id = _extract_trace_id(error)
36+
trace_id = _get_value(error, "trace_id") or _get_value(error, "traceId")
6037
if trace_id:
6138
data["traceId"] = str(trace_id)
6239
return data
6340

6441

6542
def is_rate_limited_error(error: Any) -> bool:
66-
"""Return whether an error carries an explicit rate-limit signal."""
67-
if _extract_status_code(error) == 429:
43+
if error is None:
44+
return False
45+
if _status_code(error) == 429 or _status_code(_get_value(error, "response")) == 429:
6846
return True
69-
70-
if _has_rate_limit_code(error):
47+
if _rate_limit_code(error) or _rate_limit_code(_get_value(error, "response")):
7148
return True
72-
73-
message = str(error or "")
74-
return any(pattern.search(message) for pattern in _RATE_LIMIT_TEXT_PATTERNS)
49+
return bool(_RATE_LIMIT_TEXT_RE.search(str(error)))
7550

7651

77-
def _extract_status_code(error: Any) -> Optional[int]:
78-
fallback = None
79-
for obj in (error, _get_value(error, "response")):
80-
if obj is None:
52+
def _status_code(obj: Any) -> Optional[int]:
53+
for name in ("status_code", "status", "http_status", "statusCode"):
54+
value = _get_value(obj, name)
55+
if value is None:
8156
continue
82-
for name in ("status_code", "status", "http_status", "statusCode"):
83-
status_code = _to_int(_get_value(obj, name))
84-
if status_code == 429:
85-
return status_code
86-
if fallback is None and status_code is not None:
87-
fallback = status_code
88-
return fallback
57+
try:
58+
return int(value)
59+
except (TypeError, ValueError):
60+
return None
61+
return None
8962

9063

91-
def _has_rate_limit_code(error: Any) -> bool:
92-
for obj in (error, _get_value(error, "response")):
93-
if obj is None:
94-
continue
95-
for name in ("code", "error_code", "errorCode"):
96-
error_code = _get_value(obj, name)
97-
if (
98-
error_code is not None
99-
and _normalize_code(error_code) in _RATE_LIMIT_CODES
100-
):
101-
return True
64+
def _rate_limit_code(obj: Any) -> bool:
65+
for name in ("code", "error_code", "errorCode"):
66+
code = _get_value(obj, name)
67+
if code and _normalize_code(code) in _RATE_LIMIT_CODES:
68+
return True
10269
return False
10370

10471

105-
def _extract_trace_id(error: Any) -> Optional[Any]:
106-
for name in ("trace_id", "traceId", "request_id", "requestId"):
107-
trace_id = _get_value(error, name)
108-
if trace_id:
109-
return trace_id
110-
111-
response = _get_value(error, "response")
112-
headers = _get_value(response, "headers")
113-
if not headers:
114-
return None
115-
116-
for name in ("x-acs-request-id", "x-request-id", "x-trace-id", "trace-id"):
117-
trace_id = _get_header(headers, name)
118-
if trace_id:
119-
return trace_id
120-
return None
121-
122-
12372
def _get_value(obj: Any, name: str) -> Optional[Any]:
12473
if obj is None:
12574
return None
@@ -128,31 +77,9 @@ def _get_value(obj: Any, name: str) -> Optional[Any]:
12877
return getattr(obj, name, None)
12978

13079

131-
def _get_header(headers: Any, name: str) -> Optional[Any]:
132-
target = str(name).lower()
133-
if isinstance(headers, dict):
134-
for key, value in headers.items():
135-
if str(key).lower() == target:
136-
return value
137-
return None
138-
get = getattr(headers, "get", None)
139-
if callable(get):
140-
return get(name)
141-
return None
142-
143-
14480
def _normalize_code(code: Any) -> str:
145-
normalized = re.sub(r"[^a-z0-9]", "", str(code).lower())
81+
value = "".join(ch for ch in str(code).lower() if ch.isalnum())
14682
for suffix in ("exception", "error"):
147-
if normalized.endswith(suffix):
148-
return normalized[: -len(suffix)]
149-
return normalized
150-
151-
152-
def _to_int(value: Any) -> Optional[int]:
153-
if value is None:
154-
return None
155-
try:
156-
return int(value)
157-
except (TypeError, ValueError):
158-
return None
83+
if value.endswith(suffix):
84+
return value[: -len(suffix)]
85+
return value

0 commit comments

Comments
 (0)