189 lines
9.2 KiB
Diff
189 lines
9.2 KiB
Diff
--- 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)
|