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:
Eric Wang
2026-10-22 21:50:56 +08:00
parent 2bbbaf07af
commit ca73b1e8c5
3 changed files with 185 additions and 11 deletions
+27 -10
View File
@@ -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]:
"""流式:产生中立流事件。