fix:修复回复器格式问题,记录完整空回复请求

This commit is contained in:
SengokuCola
2026-04-11 21:23:31 +08:00
parent d9b3440169
commit 6db380b10d
8 changed files with 91 additions and 96 deletions

View File

@@ -54,27 +54,30 @@ class RespNotOkException(Exception):
return f"未知的异常响应代码:{self.status_code}"
class RespParseException(Exception):
"""响应解析错误,常见于响应格式不正确或解析方法不匹配"""
class ResponseContextException(Exception):
"""携带原始响应上下文的异常基类。"""
def __init__(self, ext_info: Any, message: str | None = None):
default_message: str = "请求失败"
def __init__(self, ext_info: Any = None, message: str | None = None):
super().__init__(message)
self.ext_info = ext_info
self.message = message
def __str__(self):
return self.message or "解析响应内容时发生未知错误,请检查是否配置了正确的解析方法"
return self.message or self.default_message
class EmptyResponseException(Exception):
class RespParseException(ResponseContextException):
"""响应解析错误,常见于响应格式不正确或解析方法不匹配"""
default_message = "解析响应内容时发生未知错误,请检查是否配置了正确的解析方法"
class EmptyResponseException(ResponseContextException):
"""响应内容为空"""
def __init__(self, message: str = "响应内容为空,这可能是一个临时性问题"):
super().__init__(message)
self.message = message
def __str__(self):
return self.message
default_message = "响应内容为空,这可能是一个临时性问题"
class ModelAttemptFailed(Exception):

View File

@@ -552,7 +552,7 @@ def _build_stream_api_response(
_warn_if_max_tokens_truncated(last_response, response.content, response.tool_calls)
if not response.content and not response.tool_calls and not response.reasoning_content:
raise EmptyResponseException()
raise EmptyResponseException(last_response)
return response
@@ -627,7 +627,7 @@ def _default_normal_response_parser(
usage_record = _extract_usage_record(response)
_warn_if_max_tokens_truncated(response, api_response.content, api_response.tool_calls)
if not api_response.content and not api_response.tool_calls and not api_response.reasoning_content:
raise EmptyResponseException("响应中既无文本内容也无工具调用")
raise EmptyResponseException(response, "响应中既无文本内容也无工具调用")
return api_response, usage_record

View File

@@ -587,7 +587,7 @@ def _build_api_status_message(error: APIStatusError) -> str:
message_parts.append(str(error.message))
response_text = getattr(getattr(error, "response", None), "text", None)
if response_text:
message_parts.append(str(response_text)[:300])
message_parts.append(str(response_text))
if message_parts:
return " | ".join(message_parts)
return f"上游接口返回状态码 {error.status_code}"
@@ -750,7 +750,7 @@ class _OpenAIStreamAccumulator:
response.raw_data = {"model": self.model_name} if self.model_name else None
if not response.content and not response.tool_calls:
raise EmptyResponseException()
raise EmptyResponseException(response.raw_data)
return response
@@ -834,7 +834,7 @@ def _default_normal_response_parser(
"""
choices = getattr(resp, "choices", None)
if not choices:
raise EmptyResponseException("响应解析失败choices 为空或缺失")
raise EmptyResponseException(resp, "响应解析失败choices 为空或缺失")
api_response = APIResponse()
message_part = choices[0].message
@@ -875,7 +875,7 @@ def _default_normal_response_parser(
_log_length_truncation(finish_reason, getattr(resp, "model", None))
if not api_response.content and not api_response.tool_calls:
raise EmptyResponseException()
raise EmptyResponseException(resp)
return api_response, usage_record

View File

@@ -58,6 +58,42 @@ def _json_friendly(value: Any) -> Any:
return str(value)
def extract_error_response_body(error: Exception) -> Any | None:
"""尽量从异常对象中提取上游返回体,便于排查模型请求失败。"""
candidate_errors = [error, getattr(error, "__cause__", None)]
for candidate in candidate_errors:
if candidate is None:
continue
response = getattr(candidate, "response", None)
if response is not None:
response_json = getattr(response, "json", None)
if callable(response_json):
try:
return _json_friendly(response_json())
except Exception:
pass
response_text = getattr(response, "text", None)
if response_text not in (None, ""):
return str(response_text)
response_content = getattr(response, "content", None)
if response_content not in (None, b"", ""):
return _json_friendly(response_content)
response_body = getattr(candidate, "body", None)
if response_body not in (None, "", b""):
return _json_friendly(response_body)
ext_info = getattr(candidate, "ext_info", None)
if ext_info is not None:
return _json_friendly(ext_info)
return None
def _sanitize_filename_component(value: str) -> str:
"""将任意字符串转换为适合文件名使用的片段。"""
normalized_value = FILENAME_SAFE_PATTERN.sub("-", value.strip())
@@ -388,6 +424,10 @@ def save_failed_request_snapshot(
"snapshot_version": SNAPSHOT_VERSION,
}
response_body = extract_error_response_body(error)
if response_body is not None:
snapshot_payload["error"]["response_body"] = response_body
snapshot_payload["replay"] = {
"command": build_replay_command(snapshot_path),
"file_uri": snapshot_path.as_uri(),