Files
kefu/deploy/gateway-502-20260918/gateway.diff
T
2026-09-21 10:34:06 +08:00

189 lines
9.2 KiB
Diff
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
--- live-before
+++ local-after
@@ -218,10 +218,47 @@
class GatewayError(Exception):
- def __init__(self, message: str, *, retriable: bool = False, response_data=None):
+ def __init__(self, message: str, *, retriable: bool = False, response_data=None,
+ code: str = "upstream_error"):
super().__init__(message)
self.retriable = retriable
self.response_data = response_data
+ self.code = code
+
+
+def _http_failure(status: int, data: object = None) -> tuple[str, str]:
+ """Return fixed diagnostics; provider errors can echo keys, URLs or prompts."""
+ if status in (400, 422):
+ error = data.get("error") if isinstance(data, dict) else None
+ error = error if isinstance(error, dict) else {}
+ parameter = str(error.get("param") or "")
+ message = str(error.get("message") or "")
+ if "max_tokens" in parameter or "max_tokens" in message:
+ return "upstream_invalid_max_tokens", (
+ f"上游返回 {status}:输出 Token 上限(max_tokens)不符合模型要求,请在管理端调整该模型参数"
+ )
+ return "upstream_invalid_parameters", f"上游返回 {status}:模型请求参数不合法,请检查模型及参数配置"
+ if status in (401, 403):
+ return "upstream_auth_failed", f"上游返回 {status}:模型鉴权失败,请检查模型密钥与访问权限"
+ if status == 404:
+ return "upstream_not_found", "上游返回 404:模型或接口地址不存在,请检查模型配置"
+ if status == 429:
+ return "upstream_rate_limited", "上游返回 429:模型限流或额度不足,请稍后重试并检查服务商额度"
+ if status in (408, 504):
+ return "upstream_timeout", f"上游返回 {status}:模型响应超时,请稍后重试"
+ if status >= 500:
+ return "upstream_unavailable", f"上游返回 {status}:模型服务暂不可用,请稍后重试"
+ return "upstream_request_rejected", f"上游返回 {status}:模型请求被拒绝,请检查模型配置"
+
+
+def _exception_failure(exc: Exception | None) -> dict:
+ if isinstance(exc, GatewayError):
+ return {"error": str(exc), "error_code": exc.code}
+ if isinstance(exc, httpx.TimeoutException):
+ return {"error": "模型请求超时,请稍后重试", "error_code": "upstream_timeout"}
+ if isinstance(exc, httpx.HTTPError):
+ return {"error": "无法连接模型服务,请检查上游网络", "error_code": "upstream_connection_failed"}
+ return {"error": "模型响应格式不兼容,请检查模型接口协议", "error_code": "upstream_invalid_response"}
async def _post_once(
@@ -237,21 +274,19 @@
url, headers=headers, json=json_body, timeout=timeout
)
except (httpx.TimeoutException, httpx.TransportError) as exc:
- raise GatewayError(f"{type(exc).__name__}: {exc}", retriable=True) from exc
+ failure = _exception_failure(exc)
+ raise GatewayError(failure["error"], code=failure["error_code"], retriable=True) from exc
try:
data = response.json()
except ValueError:
data = None
- if response.status_code in RETRIABLE_STATUS:
+ if response.status_code >= 400:
+ code, detail = _http_failure(response.status_code, data)
raise GatewayError(
- f"上游返回 {response.status_code}", retriable=True, response_data=data
- )
- if response.status_code >= 400:
- raise GatewayError(
- f"上游返回 {response.status_code}: {response.text[:200]}", retriable=False, response_data=data
+ detail, code=code, retriable=response.status_code in RETRIABLE_STATUS, response_data=data
)
if data is None:
- raise GatewayError("上游响应不是合法 JSON", retriable=False)
+ raise GatewayError("上游响应不是合法 JSON", retriable=False, code="upstream_invalid_response")
return data
@@ -280,7 +315,8 @@
"provider": outlet.name,
"text": "",
"latency_ms": 0,
- "error": f"熔断中(连续失败 {outlet.breaker.failures} 次)",
+ "error": f"熔断中(连续失败 {outlet.breaker.failures} 次),请稍后重试并检查模型配置",
+ "error_code": "upstream_circuit_open",
}
config = outlet.config
@@ -290,7 +326,7 @@
)
if config_error:
return {"provider": outlet.name, "text": "", "latency_ms": 0,
- "error": config_error}
+ "error": config_error, "error_code": "model_configuration_invalid"}
timeout = min(float(config.get("timeout_ms") or 30000) / 1000.0, deadline)
try:
@@ -373,7 +409,7 @@
"provider": outlet.name,
"text": "",
"latency_ms": int((time.monotonic() - started) * 1000),
- "error": str(last)[:200],
+ **_exception_failure(last),
}
except TimeoutError:
outlet.breaker.record(False)
@@ -382,6 +418,7 @@
"text": "",
"latency_ms": int((time.monotonic() - started) * 1000),
"error": f"超过单路时限 {deadline:.0f}s",
+ "error_code": "upstream_timeout",
}
except (GatewayError, httpx.HTTPError, ValueError) as exc:
@@ -389,7 +426,7 @@
# concurrent model. Keep healthy candidates available to the caller.
outlet.breaker.record(False)
return {"provider": outlet.name, "text": "", "latency_ms": int((time.monotonic() - started) * 1000),
- "error": str(exc)[:200] or type(exc).__name__}
+ **_exception_failure(exc)}
async def _dify_message_files(client, outlet, messages, image_b64, timeout):
@@ -750,6 +787,36 @@
knowledge_result = {**knowledge_trace, "query": redact(query)[:1000]}
messages = inject_references(messages, [])
+ def failed_response(payload: dict) -> JSONResponse:
+ # Old desktop versions only read detail. Keep the existing shape,
+ # while making failures discoverable in the same model_calls table.
+ request_id = secrets.token_hex(12)
+ candidates = [
+ {**item, "request_id": request_id,
+ "error": item.get("error") or "模型未返回可用回复,请检查模型输出设置",
+ "error_code": item.get("error_code") or "upstream_empty_response"}
+ for item in payload.get("candidates", [])
+ ]
+ reasons = list(dict.fromkeys(item["error"] for item in candidates))
+ codes = {item["error_code"] for item in candidates}
+ payload.update({
+ "candidates": candidates, "request_id": request_id,
+ "code": next(iter(codes)) if len(codes) == 1 else "upstream_all_failed",
+ "detail": "模型调用失败:" + ";".join(reasons[:3]) + f"(请求编号:{request_id})",
+ })
+ try:
+ log_queue.put_nowait({
+ "desktop_account_id": int(account["id"]), "tenant_id": tenant,
+ "device_id": x_device_id, "task_id": usage_recorder.context["task_id"],
+ "roles_version": version, "judge_mode": mode, "chosen": "",
+ "judge": payload["judge"], "candidates": candidates, "total_ms": payload["total_ms"],
+ "customer_text": str(body.get("customer_text") or ""), "reply_text": "",
+ "purpose": purpose, "knowledge_retrieval": knowledge_result,
+ })
+ except asyncio.QueueFull:
+ pass
+ return JSONResponse(payload, status_code=502, headers={"X-Request-Id": request_id})
+
if tools:
# 工具轮:只问主出口,不并发也不评审。
#
@@ -782,7 +849,7 @@
"total_ms": int((time.monotonic() - started) * 1000),
}
if single.get("error") and not payload["reply"] and not payload["tool_calls"]:
- return JSONResponse(payload, status_code=502)
+ return failed_response(payload)
if idempotency_key:
idempotency[idempotency_key] = (now, payload)
try:
@@ -814,7 +881,7 @@
)
results = [
{"provider": outlet.name, "text": "", "latency_ms": 0,
- "error": f"模型调用异常:{type(result).__name__}"}
+ "error": f"模型调用异常:{type(result).__name__}", "error_code": "upstream_call_failed"}
if isinstance(result, BaseException) else result
for outlet, result in zip(answers, results)
]
@@ -826,7 +893,7 @@
"roles_version": version,
"total_ms": int((time.monotonic() - started) * 1000),
}
- return JSONResponse(payload, status_code=502)
+ return failed_response(payload)
if purpose == "guard":
verdict = dict(_EMPTY_VERDICT)