mirror of https://github.com/Mai-with-u/MaiBot.git
fix: 捕获OpenAI流式错误并触发重试
parent
4d5456ed4b
commit
cb40ff6a0e
|
|
@ -7,13 +7,7 @@ from collections.abc import Iterable
|
||||||
from typing import Callable, Any, Coroutine, Optional
|
from typing import Callable, Any, Coroutine, Optional
|
||||||
from json_repair import repair_json
|
from json_repair import repair_json
|
||||||
|
|
||||||
from openai import (
|
from openai import AsyncOpenAI, APIConnectionError, APIError, APIStatusError, NOT_GIVEN, AsyncStream
|
||||||
AsyncOpenAI,
|
|
||||||
APIConnectionError,
|
|
||||||
APIStatusError,
|
|
||||||
NOT_GIVEN,
|
|
||||||
AsyncStream,
|
|
||||||
)
|
|
||||||
from openai.types.chat import (
|
from openai.types.chat import (
|
||||||
ChatCompletion,
|
ChatCompletion,
|
||||||
ChatCompletionChunk,
|
ChatCompletionChunk,
|
||||||
|
|
@ -39,6 +33,23 @@ from ..payload_content.tool_option import ToolOption, ToolParam, ToolCall
|
||||||
logger = get_logger("OpenAI客户端")
|
logger = get_logger("OpenAI客户端")
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_status_code_from_api_error(error: APIError) -> int:
|
||||||
|
"""
|
||||||
|
尝试从APIError对象中提取HTTP状态码,无法确定时回退为500。
|
||||||
|
"""
|
||||||
|
status_code = getattr(error, "status_code", None)
|
||||||
|
if status_code is None:
|
||||||
|
status_code = getattr(error, "status", None)
|
||||||
|
if status_code is None:
|
||||||
|
response = getattr(error, "response", None)
|
||||||
|
if response is not None:
|
||||||
|
status_code = getattr(response, "status_code", None) or getattr(response, "status", None)
|
||||||
|
try:
|
||||||
|
return int(status_code)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return 500
|
||||||
|
|
||||||
|
|
||||||
def _convert_messages(messages: list[Message]) -> list[ChatCompletionMessageParam]:
|
def _convert_messages(messages: list[Message]) -> list[ChatCompletionMessageParam]:
|
||||||
"""
|
"""
|
||||||
转换消息格式 - 将消息转换为OpenAI API所需的格式
|
转换消息格式 - 将消息转换为OpenAI API所需的格式
|
||||||
|
|
@ -281,45 +292,52 @@ async def _default_stream_response_handler(
|
||||||
if buffer and not buffer.closed:
|
if buffer and not buffer.closed:
|
||||||
buffer.close()
|
buffer.close()
|
||||||
|
|
||||||
async for event in resp_stream:
|
try:
|
||||||
if interrupt_flag and interrupt_flag.is_set():
|
async for event in resp_stream:
|
||||||
# 如果中断量被设置,则抛出ReqAbortException
|
if interrupt_flag and interrupt_flag.is_set():
|
||||||
_insure_buffer_closed()
|
# 如果中断量被设置,则抛出ReqAbortException
|
||||||
raise ReqAbortException("请求被外部信号中断")
|
_insure_buffer_closed()
|
||||||
# 空 choices / usage-only 帧的防御
|
raise ReqAbortException("请求被外部信号中断")
|
||||||
if not hasattr(event, "choices") or not event.choices:
|
# 空 choices / usage-only 帧的防御
|
||||||
if hasattr(event, "usage") and event.usage:
|
if not hasattr(event, "choices") or not event.choices:
|
||||||
|
if hasattr(event, "usage") and event.usage:
|
||||||
|
_usage_record = (
|
||||||
|
event.usage.prompt_tokens or 0,
|
||||||
|
event.usage.completion_tokens or 0,
|
||||||
|
event.usage.total_tokens or 0,
|
||||||
|
)
|
||||||
|
continue # 跳过本帧,避免访问 choices[0]
|
||||||
|
delta = event.choices[0].delta # 获取当前块的delta内容
|
||||||
|
|
||||||
|
if hasattr(event.choices[0], "finish_reason") and event.choices[0].finish_reason:
|
||||||
|
finish_reason = event.choices[0].finish_reason
|
||||||
|
|
||||||
|
if hasattr(delta, "reasoning_content") and delta.reasoning_content: # type: ignore
|
||||||
|
# 标记:有独立的推理内容块
|
||||||
|
_has_rc_attr_flag = True
|
||||||
|
|
||||||
|
_in_rc_flag = _process_delta(
|
||||||
|
delta,
|
||||||
|
_has_rc_attr_flag,
|
||||||
|
_in_rc_flag,
|
||||||
|
_rc_delta_buffer,
|
||||||
|
_fc_delta_buffer,
|
||||||
|
_tool_calls_buffer,
|
||||||
|
)
|
||||||
|
|
||||||
|
if event.usage:
|
||||||
|
# 如果有使用情况,则将其存储在APIResponse对象中
|
||||||
_usage_record = (
|
_usage_record = (
|
||||||
event.usage.prompt_tokens or 0,
|
event.usage.prompt_tokens or 0,
|
||||||
event.usage.completion_tokens or 0,
|
event.usage.completion_tokens or 0,
|
||||||
event.usage.total_tokens or 0,
|
event.usage.total_tokens or 0,
|
||||||
)
|
)
|
||||||
continue # 跳过本帧,避免访问 choices[0]
|
except APIError as e:
|
||||||
delta = event.choices[0].delta # 获取当前块的delta内容
|
_insure_buffer_closed()
|
||||||
|
status_code = _extract_status_code_from_api_error(e)
|
||||||
if hasattr(event.choices[0], "finish_reason") and event.choices[0].finish_reason:
|
message = getattr(e, "message", None) or str(e)
|
||||||
finish_reason = event.choices[0].finish_reason
|
logger.warning(f"OpenAI流式响应异常: {message}")
|
||||||
|
raise RespNotOkException(status_code, message) from e
|
||||||
if hasattr(delta, "reasoning_content") and delta.reasoning_content: # type: ignore
|
|
||||||
# 标记:有独立的推理内容块
|
|
||||||
_has_rc_attr_flag = True
|
|
||||||
|
|
||||||
_in_rc_flag = _process_delta(
|
|
||||||
delta,
|
|
||||||
_has_rc_attr_flag,
|
|
||||||
_in_rc_flag,
|
|
||||||
_rc_delta_buffer,
|
|
||||||
_fc_delta_buffer,
|
|
||||||
_tool_calls_buffer,
|
|
||||||
)
|
|
||||||
|
|
||||||
if event.usage:
|
|
||||||
# 如果有使用情况,则将其存储在APIResponse对象中
|
|
||||||
_usage_record = (
|
|
||||||
event.usage.prompt_tokens or 0,
|
|
||||||
event.usage.completion_tokens or 0,
|
|
||||||
event.usage.total_tokens or 0,
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return _build_stream_api_resp(
|
return _build_stream_api_resp(
|
||||||
|
|
@ -533,6 +551,10 @@ class OpenaiClient(BaseClient):
|
||||||
except APIStatusError as e:
|
except APIStatusError as e:
|
||||||
# 重封装APIError为RespNotOkException
|
# 重封装APIError为RespNotOkException
|
||||||
raise RespNotOkException(e.status_code, e.message) from e
|
raise RespNotOkException(e.status_code, e.message) from e
|
||||||
|
except APIError as e:
|
||||||
|
status_code = _extract_status_code_from_api_error(e)
|
||||||
|
message = getattr(e, "message", None) or str(e)
|
||||||
|
raise RespNotOkException(status_code, message) from e
|
||||||
|
|
||||||
if usage_record:
|
if usage_record:
|
||||||
resp.usage = UsageRecord(
|
resp.usage = UsageRecord(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue