feat(backends): map LLM auth/rate-limit errors to backend-neutral errors
Closes the error-code gap between LangGraph and FastGPT backends: the endpoint's 401/429 branches previously could never trigger under the LangGraph backend because all provider failures collapsed into LLMAPIError (500). - agent/llm: LLMAuthError / LLMRateLimitError as LLMAPIError subclasses; OpenAILLMService raises them from openai AuthenticationError (401, no retry) and RateLimitError (429, after bounded retries exhausted) - backends/langgraph: _complete_turn translates LLMAuthError -> ChatBackendAuthenticationError and LLMRateLimitError -> ChatBackendRateLimitError; stream reuses complete() so the mapping applies to both paths; other errors (timeout etc.) propagate unchanged - endpoint untouched: existing 401/429 response branches now fire for both backends Verified: 212 unit tests (7 new error-mapping tests)
This commit is contained in:
+24
-1
@@ -27,6 +27,16 @@ class LLMAPIError(LLMError):
|
||||
"""provider 返回错误或无效响应。"""
|
||||
|
||||
|
||||
class LLMAuthError(LLMAPIError):
|
||||
"""provider 拒绝凭证(401)。不可重试,需区分上报(对照
|
||||
FastGPT 后端的 ChatBackendAuthenticationError 语义)。"""
|
||||
|
||||
|
||||
class LLMRateLimitError(LLMAPIError):
|
||||
"""provider 限流(429)。重试耗尽后抛出,需区分上报(对照
|
||||
FastGPT 后端的 ChatBackendRateLimitError 语义)。"""
|
||||
|
||||
|
||||
class LLMService(Protocol):
|
||||
"""节点依赖的最小 LLM 接口。"""
|
||||
|
||||
@@ -128,7 +138,12 @@ class OpenAILLMService:
|
||||
) -> str:
|
||||
import asyncio
|
||||
|
||||
from openai import APIConnectionError, APITimeoutError, RateLimitError
|
||||
from openai import (
|
||||
APIConnectionError,
|
||||
APITimeoutError,
|
||||
AuthenticationError,
|
||||
RateLimitError,
|
||||
)
|
||||
|
||||
client = self._get_client()
|
||||
messages = [
|
||||
@@ -163,7 +178,15 @@ class OpenAILLMService:
|
||||
retries += 1
|
||||
await asyncio.sleep(min(2 ** retries, 10))
|
||||
continue
|
||||
if isinstance(exc, RateLimitError):
|
||||
# 限流耗尽:区分上报,API 层映射为 429(两后端错误码等价)
|
||||
raise LLMRateLimitError(
|
||||
f"LLM provider 限流(重试 {self._max_retries} 次后仍失败)"
|
||||
) from exc
|
||||
raise LLMAPIError(f"LLM provider 不可用:{type(exc).__name__}") from exc
|
||||
except AuthenticationError as exc:
|
||||
# 401 鉴权失败:不可重试,区分上报,API 层映射为 401
|
||||
raise LLMAuthError("LLM provider 认证失败") from exc
|
||||
except Exception as exc:
|
||||
# 其他错误(如 4xx 参数错)不可重试
|
||||
raise LLMAPIError(f"LLM 调用失败:{type(exc).__name__}") from exc
|
||||
|
||||
+27
-10
@@ -15,8 +15,11 @@
|
||||
from collections.abc import AsyncIterator
|
||||
|
||||
from ..agent.graph import GraphDeps, build_graph, workflow_fake_llm
|
||||
from ..agent.llm import LLMAuthError, LLMRateLimitError
|
||||
from .chat import (
|
||||
ChatBackend,
|
||||
ChatBackendAuthenticationError,
|
||||
ChatBackendRateLimitError,
|
||||
ChatInput,
|
||||
ChatResult,
|
||||
ChatStreamEvent,
|
||||
@@ -49,6 +52,20 @@ class LangGraphBackend(ChatBackend):
|
||||
- 本阶段不处理 ``need_form_update`` 的真实表单差量
|
||||
(作业 6 起实现)。
|
||||
"""
|
||||
result = await self._complete_turn(chat_input)
|
||||
return ChatResult(
|
||||
content = result["response_text"],
|
||||
status_code = None,
|
||||
form_update = result.get("turn_patch") or {}
|
||||
)
|
||||
|
||||
async def _complete_turn(self, chat_input: ChatInput) -> dict:
|
||||
"""执行一轮图,把 LLM 层错误翻译为后端中立错误。
|
||||
|
||||
错误码等价(对照 FastGPT 后端):
|
||||
LLMAuthError -> ChatBackendAuthenticationError(API 层 401);
|
||||
LLMRateLimitError -> ChatBackendRateLimitError(API 层 429)。
|
||||
"""
|
||||
graph_input: dict = {
|
||||
"session_id": chat_input.session_id,
|
||||
"input_text": chat_input.text,
|
||||
@@ -58,16 +75,16 @@ class LangGraphBackend(ChatBackend):
|
||||
# 使 set_info 写入后本轮 chat 立即可见(输入覆盖 checkpoint 同键)。
|
||||
if chat_input.injected_fields:
|
||||
graph_input.update(chat_input.injected_fields)
|
||||
result = await self._graph.ainvoke(
|
||||
graph_input,
|
||||
config={"configurable": {"thread_id": chat_input.session_id}},
|
||||
context=GraphDeps(llm=self._llm),
|
||||
)
|
||||
return ChatResult(
|
||||
content = result["response_text"],
|
||||
status_code = None,
|
||||
form_update = result.get("turn_patch") or {}
|
||||
)
|
||||
try:
|
||||
return await self._graph.ainvoke(
|
||||
graph_input,
|
||||
config={"configurable": {"thread_id": chat_input.session_id}},
|
||||
context=GraphDeps(llm=self._llm),
|
||||
)
|
||||
except LLMAuthError as exc:
|
||||
raise ChatBackendAuthenticationError(str(exc)) from exc
|
||||
except LLMRateLimitError as exc:
|
||||
raise ChatBackendRateLimitError(str(exc)) from exc
|
||||
|
||||
async def stream(self, chat_input: ChatInput) -> AsyncIterator[ChatStreamEvent]:
|
||||
"""流式:产生中立流事件。
|
||||
|
||||
Reference in New Issue
Block a user