fix: proxy exception repair
This commit is contained in:
1 parent
050c6be5f6
commit
90a11df85d
7 files changed
+95
-103
No files matched your search
+12
-10
@@ -252,7 +252,7 @@ async def admin_login(request: LoginRequest) -> LoginResponse:
|
||||
|
||||
|
||||
@router.post("/api/logout")
|
||||
async def admin_logout(authenticated: bool = Depends(verify_admin_session),
|
||||
async def admin_logout(_: bool = Depends(verify_admin_session),
|
||||
authorization: Optional[str] = Header(None)) -> Dict[str, Any]:
|
||||
"""
|
||||
管理员登出
|
||||
@@ -279,7 +279,7 @@ async def admin_logout(authenticated: bool = Depends(verify_admin_session),
|
||||
|
||||
|
||||
@router.get("/api/tokens", response_model=TokenListResponse)
|
||||
async def list_tokens(authenticated: bool = Depends(verify_admin_session)) -> TokenListResponse:
|
||||
async def list_tokens(_: bool = Depends(verify_admin_session)) -> TokenListResponse:
|
||||
"""
|
||||
获取所有Token列表
|
||||
|
||||
@@ -337,7 +337,7 @@ async def list_tokens(authenticated: bool = Depends(verify_admin_session)) -> To
|
||||
|
||||
@router.post("/api/tokens/add")
|
||||
async def add_tokens(request: AddTokensRequest,
|
||||
authenticated: bool = Depends(verify_admin_session)) -> Dict[str, Any]:
|
||||
_: bool = Depends(verify_admin_session)) -> Dict[str, Any]:
|
||||
"""
|
||||
批量添加Token
|
||||
|
||||
@@ -372,7 +372,7 @@ async def add_tokens(request: AddTokensRequest,
|
||||
|
||||
@router.post("/api/tokens/delete")
|
||||
async def delete_tokens(request: DeleteTokensRequest,
|
||||
authenticated: bool = Depends(verify_admin_session)) -> Dict[str, Any]:
|
||||
_: bool = Depends(verify_admin_session)) -> Dict[str, Any]:
|
||||
"""
|
||||
批量删除Token
|
||||
|
||||
@@ -406,7 +406,7 @@ async def delete_tokens(request: DeleteTokensRequest,
|
||||
|
||||
|
||||
@router.get("/api/settings")
|
||||
async def get_settings(authenticated: bool = Depends(verify_admin_session)) -> Dict[str, Any]:
|
||||
async def get_settings(_: bool = Depends(verify_admin_session)) -> Dict[str, Any]:
|
||||
"""获取全局配置"""
|
||||
try:
|
||||
logger.debug("[Admin] 获取全局配置")
|
||||
@@ -429,7 +429,7 @@ class UpdateSettingsRequest(BaseModel):
|
||||
|
||||
|
||||
@router.post("/api/settings")
|
||||
async def update_settings(request: UpdateSettingsRequest, authenticated: bool = Depends(verify_admin_session)) -> Dict[str, Any]:
|
||||
async def update_settings(request: UpdateSettingsRequest, _: bool = Depends(verify_admin_session)) -> Dict[str, Any]:
|
||||
"""更新全局配置"""
|
||||
try:
|
||||
import toml
|
||||
@@ -484,7 +484,7 @@ def _format_size(size_bytes: int) -> str:
|
||||
|
||||
|
||||
@router.get("/api/cache/size")
|
||||
async def get_cache_size(authenticated: bool = Depends(verify_admin_session)) -> Dict[str, Any]:
|
||||
async def get_cache_size(_: bool = Depends(verify_admin_session)) -> Dict[str, Any]:
|
||||
"""获取缓存大小"""
|
||||
try:
|
||||
logger.debug("[Admin] 开始获取缓存大小")
|
||||
@@ -525,8 +525,10 @@ async def get_cache_size(authenticated: bool = Depends(verify_admin_session)) ->
|
||||
|
||||
|
||||
@router.post("/api/cache/clear")
|
||||
async def clear_cache(authenticated: bool = Depends(verify_admin_session)) -> Dict[str, Any]:
|
||||
"""清理缓存 - 删除所有临时文件"""
|
||||
async def clear_cache(_: bool = Depends(verify_admin_session)) -> Dict[str, Any]:
|
||||
"""清理缓存
|
||||
|
||||
删除所有临时文件"""
|
||||
try:
|
||||
logger.debug("[Admin] 开始清理缓存")
|
||||
|
||||
@@ -578,7 +580,7 @@ async def clear_cache(authenticated: bool = Depends(verify_admin_session)) -> Di
|
||||
|
||||
|
||||
@router.get("/api/stats")
|
||||
async def get_stats(authenticated: bool = Depends(verify_admin_session)) -> Dict[str, Any]:
|
||||
async def get_stats(_: bool = Depends(verify_admin_session)) -> Dict[str, Any]:
|
||||
"""
|
||||
获取统计信息
|
||||
|
||||
|
||||
+7
-7
@@ -22,21 +22,21 @@ router = APIRouter(prefix="/chat", tags=["聊天"])
|
||||
@router.post("/completions", response_model=None)
|
||||
async def chat_completions(
|
||||
request: OpenAIChatRequest,
|
||||
authenticated: Optional[str] = Depends(auth_manager.verify)
|
||||
_: Optional[str] = Depends(auth_manager.verify)
|
||||
):
|
||||
"""
|
||||
创建聊天补全
|
||||
|
||||
|
||||
兼容OpenAI聊天API的端点,支持流式和非流式响应。
|
||||
|
||||
|
||||
Args:
|
||||
request: OpenAI格式的聊天请求
|
||||
authenticated: 认证状态(由依赖注入)
|
||||
|
||||
_: 认证依赖(自动验证)
|
||||
|
||||
Returns:
|
||||
OpenAIChatCompletionResponse: 非流式响应
|
||||
StreamingResponse: 流式响应
|
||||
|
||||
|
||||
Raises:
|
||||
HTTPException: 当请求处理失败时
|
||||
"""
|
||||
@@ -74,7 +74,7 @@ async def chat_completions(
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"[Chat] 聊天请求处理失败: {str(e)}", exc_info=True)
|
||||
logger.error(f"[Chat] 聊天请求处理失败: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail={
|
||||
|
||||
@@ -19,15 +19,15 @@ router = APIRouter(tags=["模型"])
|
||||
|
||||
|
||||
@router.get("/models")
|
||||
async def list_models(authenticated: Optional[str] = Depends(auth_manager.verify)) -> Dict[str, Any]:
|
||||
async def list_models(_: Optional[str] = Depends(auth_manager.verify)) -> Dict[str, Any]:
|
||||
"""
|
||||
获取可用模型列表
|
||||
|
||||
|
||||
返回 OpenAI 兼容的模型列表格式,包含系统支持的所有 Grok 模型的详细信息。
|
||||
|
||||
|
||||
Args:
|
||||
authenticated: 认证状态(由依赖注入)
|
||||
|
||||
_: 认证依赖(自动验证)
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 包含模型列表的响应数据
|
||||
"""
|
||||
@@ -85,14 +85,14 @@ async def list_models(authenticated: Optional[str] = Depends(auth_manager.verify
|
||||
|
||||
|
||||
@router.get("/models/{model_id}")
|
||||
async def get_model(model_id: str, authenticated: Optional[str] = Depends(auth_manager.verify)) -> Dict[str, Any]:
|
||||
async def get_model(model_id: str, _: Optional[str] = Depends(auth_manager.verify)) -> Dict[str, Any]:
|
||||
"""
|
||||
获取特定模型信息
|
||||
|
||||
|
||||
Args:
|
||||
model_id (str): 模型ID
|
||||
authenticated: 认证状态(由依赖注入)
|
||||
|
||||
_: 认证依赖(自动验证)
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 模型详细信息
|
||||
"""
|
||||
|
||||
@@ -15,8 +15,8 @@ class OpenAIChatRequest(BaseModel):
|
||||
max_tokens: Optional[int] = Field(None, ge=1, le=100000, description="最大Token数")
|
||||
top_p: Optional[float] = Field(1.0, ge=0, le=1, description="采样参数")
|
||||
|
||||
@field_validator('messages')
|
||||
@classmethod
|
||||
@field_validator('messages')
|
||||
def validate_messages(cls, v):
|
||||
"""验证消息格式"""
|
||||
if not v:
|
||||
@@ -49,8 +49,8 @@ class OpenAIChatRequest(BaseModel):
|
||||
|
||||
return v
|
||||
|
||||
@field_validator('model')
|
||||
@classmethod
|
||||
@field_validator('model')
|
||||
def validate_model(cls, v):
|
||||
"""验证模型名称"""
|
||||
allowed_models = [
|
||||
@@ -68,16 +68,16 @@ class OpenAIChatCompletionMessage(BaseModel):
|
||||
"""聊天完成消息"""
|
||||
role: str = Field(..., description="角色")
|
||||
content: str = Field(..., description="消息内容")
|
||||
reference_id: Optional[str] = Field(None, description="参考ID")
|
||||
annotations: Optional[List[str]] = Field(None, description="注释")
|
||||
reference_id: Optional[str] = Field(default=None, description="参考ID")
|
||||
annotations: Optional[List[str]] = Field(default=None, description="注释")
|
||||
|
||||
|
||||
class OpenAIChatCompletionChoice(BaseModel):
|
||||
"""聊天完成选项"""
|
||||
index: int = Field(..., description="选项索引")
|
||||
message: OpenAIChatCompletionMessage = Field(..., description="响应消息")
|
||||
logprobs: Optional[float] = Field(None, description="对数概率")
|
||||
finish_reason: str = Field("stop", description="完成原因")
|
||||
logprobs: Optional[float] = Field(default=None, description="对数概率")
|
||||
finish_reason: str = Field(default="stop", description="完成原因")
|
||||
|
||||
|
||||
class OpenAIChatCompletionResponse(BaseModel):
|
||||
@@ -108,8 +108,8 @@ class OpenAIChatCompletionChunkChoice(BaseModel):
|
||||
class OpenAIChatCompletionChunkResponse(BaseModel):
|
||||
"""流式聊天完成响应"""
|
||||
id: str = Field(..., description="响应ID")
|
||||
object: str = Field("chat.completion.chunk", description="对象类型")
|
||||
object: str = Field(default="chat.completion.chunk", description="对象类型")
|
||||
created: int = Field(..., description="创建时间戳")
|
||||
model: str = Field(..., description="使用的模型")
|
||||
system_fingerprint: Optional[str] = Field(None, description="系统指纹")
|
||||
system_fingerprint: Optional[str] = Field(default=None, description="系统指纹")
|
||||
choices: List[OpenAIChatCompletionChunkChoice] = Field(..., description="响应选项")
|
||||
@@ -14,12 +14,7 @@ class CacheService:
|
||||
"""缓存服务基类"""
|
||||
|
||||
def __init__(self, cache_type: str):
|
||||
"""
|
||||
初始化缓存服务
|
||||
|
||||
Args:
|
||||
cache_type: 缓存类型,'image' 或 'video'
|
||||
"""
|
||||
"""初始化缓存服务"""
|
||||
self.cache_type = cache_type
|
||||
self.cache_dir = Path(f"data/temp/{cache_type}")
|
||||
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
||||
@@ -27,7 +22,6 @@ class CacheService:
|
||||
@staticmethod
|
||||
def _get_cache_filename(file_path: str) -> str:
|
||||
"""将文件路径转换为缓存文件名"""
|
||||
# 移除开头的斜杠并替换所有斜杠为短横线
|
||||
filename = file_path.lstrip('/').replace('/', '-')
|
||||
return filename
|
||||
|
||||
@@ -75,7 +69,7 @@ class CacheService:
|
||||
|
||||
# 代理配置
|
||||
proxy_url = setting.grok_config.get("proxy_url")
|
||||
proxies = {"http": proxy_url, "https": proxy_url} if proxy_url else None
|
||||
proxies = {"http": proxy_url, "https": proxy_url} if proxy_url else {}
|
||||
|
||||
async with AsyncSession() as session:
|
||||
logger.debug(f"[{self.cache_type.upper()}Cache] 开始下载: {file_url}")
|
||||
@@ -115,12 +109,12 @@ class CacheService:
|
||||
async def cleanup_cache(self):
|
||||
"""清理缓存目录,确保不超过配置的大小限制"""
|
||||
try:
|
||||
# 获取配置的最大缓存大小(MB)
|
||||
# 获取配置的最大缓存大小
|
||||
config_key = f"{self.cache_type}_cache_max_size_mb"
|
||||
max_size_mb = setting.global_config.get(config_key, 500)
|
||||
max_size_bytes = max_size_mb * 1024 * 1024
|
||||
|
||||
# 获取所有缓存文件及其大小和修改时间
|
||||
# 获取缓存大小和修改时间
|
||||
files = []
|
||||
total_size = 0
|
||||
|
||||
@@ -138,10 +132,10 @@ class CacheService:
|
||||
|
||||
logger.info(f"[{self.cache_type.upper()}Cache] 缓存大小 {total_size / 1024 / 1024:.2f}MB 超过限制 {max_size_mb}MB,开始清理")
|
||||
|
||||
# 按修改时间排序(最旧的在前)
|
||||
# 按修改时间排序
|
||||
files.sort(key=lambda x: x[2])
|
||||
|
||||
# 删除最旧的文件直到总大小低于限制
|
||||
# 删除文件直到总大小低于限制
|
||||
for file_path, size, _ in files:
|
||||
if total_size <= max_size_bytes:
|
||||
break
|
||||
@@ -170,7 +164,7 @@ class ImageCacheService(CacheService):
|
||||
auth_token: 认证令牌
|
||||
|
||||
Returns:
|
||||
缓存文件路径,下载失败返回 None
|
||||
缓存文件路径,下载失败返回 None`
|
||||
"""
|
||||
return await self.download_file(image_path, auth_token, timeout=30.0)
|
||||
|
||||
|
||||
@@ -148,18 +148,23 @@ class GrokClient:
|
||||
try:
|
||||
# 构建请求头和代理
|
||||
headers = GrokClient._build_headers(auth_token)
|
||||
proxies = GrokClient._get_proxy()
|
||||
proxy_config = GrokClient._get_proxy()
|
||||
|
||||
# 构建请求参数
|
||||
request_kwargs = {
|
||||
"headers": headers,
|
||||
"data": json.dumps(payload),
|
||||
"impersonate": IMPERSONATE_BROWSER,
|
||||
"timeout": REQUEST_TIMEOUT,
|
||||
"stream": True,
|
||||
"proxies": proxy_config if proxy_config else None
|
||||
}
|
||||
|
||||
# 在线程池中执行同步HTTP请求,避免阻塞事件循环
|
||||
response = await asyncio.to_thread(
|
||||
curl_requests.post,
|
||||
GROK_API_ENDPOINT,
|
||||
headers=headers,
|
||||
data=json.dumps(payload),
|
||||
impersonate=IMPERSONATE_BROWSER,
|
||||
timeout=REQUEST_TIMEOUT,
|
||||
stream=True,
|
||||
**proxies
|
||||
**request_kwargs
|
||||
)
|
||||
|
||||
logger.debug(f"[Client] API响应状态码: {response.status_code}")
|
||||
@@ -204,9 +209,9 @@ class GrokClient:
|
||||
try:
|
||||
error_data = response.json()
|
||||
error_message = str(error_data)
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
error_data = response.text
|
||||
error_message = error_data[:200] if error_data else "未知错误"
|
||||
error_message = error_data[:200] if error_data else e
|
||||
|
||||
# 记录Token失败
|
||||
asyncio.create_task(token_manager.record_failure(auth_token, response.status_code, error_message))
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import json
|
||||
import uuid
|
||||
import time
|
||||
from typing import Iterator
|
||||
from typing import AsyncGenerator
|
||||
|
||||
from app.core.config import setting
|
||||
from app.core.exception import GrokApiException
|
||||
@@ -43,7 +43,7 @@ class GrokResponseProcessor:
|
||||
# 提取响应数据
|
||||
grok_resp = data.get("result", {}).get("response", {})
|
||||
|
||||
# 检查视频生成响应
|
||||
# 提取视频数据
|
||||
if video_resp := grok_resp.get("streamingVideoGenerationResponse"):
|
||||
if video_url := video_resp.get("videoUrl"):
|
||||
logger.debug(f"[Processor] 检测到视频生成: {video_url}")
|
||||
@@ -98,7 +98,7 @@ class GrokResponseProcessor:
|
||||
model_name = model_response.get("model")
|
||||
content = model_response.get("message", "")
|
||||
|
||||
# 添加生成的图片
|
||||
# 提取图片数据
|
||||
if images := model_response.get("generatedImageUrls"):
|
||||
for img in images:
|
||||
try:
|
||||
@@ -114,7 +114,7 @@ class GrokResponseProcessor:
|
||||
logger.warning(f"[Processor] 缓存图片失败: {e}")
|
||||
content += f"\n"
|
||||
|
||||
# 返回OpenAI格式响应
|
||||
# 返回 OpenAI 响应格式
|
||||
result = OpenAIChatCompletionResponse(
|
||||
id=f"chatcmpl-{uuid.uuid4()}",
|
||||
object="chat.completion",
|
||||
@@ -143,22 +143,19 @@ class GrokResponseProcessor:
|
||||
response.close()
|
||||
|
||||
@staticmethod
|
||||
async def process_stream(response, auth_token: str) -> Iterator[str]:
|
||||
async def process_stream(response, auth_token: str) -> AsyncGenerator[str, None]:
|
||||
"""处理流式响应"""
|
||||
# 流式生成状态
|
||||
is_image = False
|
||||
is_thinking = False
|
||||
thinking_finished = False
|
||||
chunk_index = 0
|
||||
model = None
|
||||
filtered_tags = setting.grok_config.get("filtered_tags", "").split(",")
|
||||
|
||||
# 视频生成相关状态
|
||||
is_video = False
|
||||
video_url = None
|
||||
filtered_tags = setting.grok_config.get("filtered_tags", "").split(",")
|
||||
video_progress_started = False
|
||||
last_video_progress = -1
|
||||
|
||||
def make_chunk(content: str, finish: str = None):
|
||||
def make_chunk(chunk_content: str, finish: str = None):
|
||||
"""生成OpenAI格式的响应块"""
|
||||
chunk_data = OpenAIChatCompletionChunkResponse(
|
||||
id=f"chatcmpl-{uuid.uuid4()}",
|
||||
@@ -168,8 +165,8 @@ class GrokResponseProcessor:
|
||||
index=chunk_index,
|
||||
delta=OpenAIChatCompletionChunkMessage(
|
||||
role="assistant",
|
||||
content=content
|
||||
) if content else {},
|
||||
content=chunk_content
|
||||
) if chunk_content else {},
|
||||
finish_reason=finish
|
||||
)]
|
||||
).model_dump()
|
||||
@@ -204,27 +201,40 @@ class GrokResponseProcessor:
|
||||
if m := user_resp.get("model"):
|
||||
model = m
|
||||
|
||||
# 检查视频生成响应
|
||||
# 提取视频数据
|
||||
if video_resp := grok_resp.get("streamingVideoGenerationResponse"):
|
||||
is_video = True
|
||||
progress = video_resp.get("progress", 0)
|
||||
|
||||
if progress > last_video_progress:
|
||||
last_video_progress = progress
|
||||
|
||||
# 首次显示进度时添加 <think>
|
||||
# 添加 <think> 标签
|
||||
if not video_progress_started:
|
||||
content = f"<think>视频已生成{progress}%\n"
|
||||
video_progress_started = True
|
||||
elif progress < 100:
|
||||
content = f"视频已生成{progress}%\n"
|
||||
else:
|
||||
# 进度100%时关闭think标签
|
||||
# 进度100%时关闭 <think> 标签并立即处理视频
|
||||
content = f"视频已生成{progress}%</think>\n"
|
||||
# 保存视频URL
|
||||
|
||||
# 立即下载并缓存视频
|
||||
if v_url := video_resp.get("videoUrl"):
|
||||
video_url = v_url
|
||||
logger.debug(f"[Processor] 视频生成完成: {video_url}")
|
||||
logger.debug(f"[Processor] 视频生成完成: {v_url}")
|
||||
full_video_url = f"https://assets.grok.com/{v_url}"
|
||||
|
||||
try:
|
||||
cache_path = await video_cache_service.download_video(f"/{v_url}", auth_token)
|
||||
if cache_path:
|
||||
video_path = v_url.replace('/', '-')
|
||||
base_url = setting.global_config.get("base_url", "")
|
||||
local_video_url = f"{base_url}/images/{video_path}" if base_url else f"/images/{video_path}"
|
||||
content += f'<video src="{local_video_url}" controls="controls"></video>'
|
||||
else:
|
||||
content += f'<video src="{full_video_url}" controls="controls"></video>'
|
||||
except Exception as e:
|
||||
logger.warning(f"[Processor] 缓存视频失败: {e}")
|
||||
content += f'<video src="{full_video_url}" controls="controls"></video>'
|
||||
|
||||
yield make_chunk(content)
|
||||
chunk_index += 1
|
||||
@@ -238,16 +248,16 @@ class GrokResponseProcessor:
|
||||
# 获取token
|
||||
token = grok_resp.get("token", "")
|
||||
|
||||
# 图片模式
|
||||
# 提取图片数据
|
||||
if is_image:
|
||||
if model_resp := grok_resp.get("modelResponse"):
|
||||
# 生成图片链接并缓存
|
||||
content = ""
|
||||
for img in model_resp.get("generatedImageUrls", []):
|
||||
try:
|
||||
# 异步下载并缓存图片
|
||||
# 缓存图片
|
||||
await image_cache_service.download_image(f"/{img}", auth_token)
|
||||
# 使用本地缓存路径
|
||||
# 本地图片路径
|
||||
img_path = img.replace('/', '-')
|
||||
base_url = setting.global_config.get("base_url", "")
|
||||
img_url = f"{base_url}/images/{img_path}" if base_url else f"/images/{img_path}"
|
||||
@@ -261,7 +271,7 @@ class GrokResponseProcessor:
|
||||
yield make_chunk(token)
|
||||
chunk_index += 1
|
||||
|
||||
# 对话模式
|
||||
# 提取对话数据
|
||||
else:
|
||||
# 过滤 list 格式的 token
|
||||
if isinstance(token, list):
|
||||
@@ -275,7 +285,7 @@ class GrokResponseProcessor:
|
||||
current_is_thinking = grok_resp.get("isThinking", False)
|
||||
message_tag = grok_resp.get("messageTag")
|
||||
|
||||
# 跳过后续的 thinking
|
||||
# 跳过后续的 <think> 标签
|
||||
if thinking_finished and current_is_thinking:
|
||||
continue
|
||||
|
||||
@@ -283,7 +293,7 @@ class GrokResponseProcessor:
|
||||
if grok_resp.get("toolUsageCardId"):
|
||||
if web_search := grok_resp.get("webSearchResults"):
|
||||
if current_is_thinking:
|
||||
# 添加搜索结果到 token
|
||||
# 封装搜索结果
|
||||
for result in web_search.get("results", []):
|
||||
title = result.get("title", "")
|
||||
url = result.get("url", "")
|
||||
@@ -323,27 +333,8 @@ class GrokResponseProcessor:
|
||||
logger.warning(f"[Processor] 处理chunk出错: {e}")
|
||||
continue
|
||||
|
||||
# 处理视频生成完成后的输出
|
||||
if is_video and video_url:
|
||||
full_video_url = f"https://assets.grok.com/{video_url}"
|
||||
try:
|
||||
# 下载并缓存视频
|
||||
cache_path = await video_cache_service.download_video(f"/{video_url}", auth_token)
|
||||
if cache_path:
|
||||
video_path = video_url.replace('/', '-')
|
||||
base_url = setting.global_config.get("base_url", "")
|
||||
local_video_url = f"{base_url}/images/{video_path}" if base_url else f"/images/{video_path}"
|
||||
content = f'<video src="{local_video_url}" controls="controls" width="500" height="300"></video>'
|
||||
else:
|
||||
content = f'<video src="{full_video_url}" controls="controls" width="500" height="300"></video>'
|
||||
except Exception as e:
|
||||
logger.warning(f"[Processor] 缓存视频失败: {e}")
|
||||
content = f'<video src="{full_video_url}" controls="controls" width="500" height="300"></video>'
|
||||
|
||||
yield make_chunk(content, "stop")
|
||||
else:
|
||||
# 发送结束块
|
||||
yield make_chunk("", "stop")
|
||||
# 发送结束块
|
||||
yield make_chunk("", "stop")
|
||||
|
||||
# 发送流结束标记
|
||||
yield "data: [DONE]\n\n"
|
||||
|
||||
Reference in new issue
Block a user