Skip to content

Commit e104a50

Browse files
authored
Merge pull request #347 from bbbugg:Add-final-SSE-error
Fix: Gemini streaming returns a structured error instead of empty responses
2 parents 6b96478 + 1771555 commit e104a50

File tree

4 files changed

+60
-5
lines changed

4 files changed

+60
-5
lines changed

app/router/gemini_routes.py

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from fastapi import APIRouter, Depends, HTTPException
22
from fastapi.responses import StreamingResponse, JSONResponse
33
from copy import deepcopy
4+
import json
45
import asyncio
56
from app.config.config import settings
67
from app.log.logger import get_gemini_logger
@@ -159,7 +160,6 @@ async def generate_content(
159160
)
160161
return response
161162

162-
163163
@router.post("/models/{model_name}:streamGenerateContent")
164164
@router_v1beta.post("/models/{model_name}:streamGenerateContent")
165165
@RetryHandler(key_arg="api_key")
@@ -181,12 +181,42 @@ async def stream_generate_content(
181181
if not await model_service.check_model_support(model_name):
182182
raise HTTPException(status_code=400, detail=f"Model {model_name} is not supported")
183183

184-
response_stream = chat_service.stream_generate_content(
184+
raw_stream = chat_service.stream_generate_content(
185185
model=model_name,
186186
request=request,
187187
api_key=api_key
188188
)
189-
return StreamingResponse(response_stream, media_type="text/event-stream")
189+
try:
190+
# 尝试获取第一条数据,判断是正常 SSE(data: 前缀)还是错误 JSON
191+
first_chunk = await raw_stream.__anext__()
192+
except StopAsyncIteration:
193+
# 如果流直接结束,退回标准 SSE 输出
194+
return StreamingResponse(raw_stream, media_type="text/event-stream")
195+
except Exception as e:
196+
# 初始化流异常,直接返回 500 错误
197+
return JSONResponse(
198+
content={"error": {"code": 500, "message": str(e)}},
199+
status_code=500
200+
)
201+
202+
# 如果以 "data:" 开头,代表正常 SSE,将首块和后续块一起发送
203+
if isinstance(first_chunk, str) and first_chunk.startswith("data:"):
204+
async def combined():
205+
yield first_chunk
206+
async for chunk in raw_stream:
207+
yield chunk
208+
209+
return StreamingResponse(combined(), media_type="text/event-stream")
210+
211+
# 否则把首块当作错误 JSON 处理
212+
try:
213+
err = json.loads(first_chunk)
214+
code = err.get("error", {}).get("code", 500)
215+
except json.JSONDecodeError:
216+
err = {"error": {"code": 500, "message": first_chunk}}
217+
code = 500
218+
219+
return JSONResponse(content=err, status_code=code)
190220

191221

192222
@router.post("/models/{model_name}:countTokens")

app/service/chat/gemini_chat_service.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,7 @@ async def stream_generate_content(
470470
is_success = False
471471
status_code = None
472472
final_api_key = api_key
473+
last_error_msg = None
473474

474475
while retries < max_retries:
475476
request_datetime = datetime.datetime.now()
@@ -509,6 +510,7 @@ async def stream_generate_content(
509510
retries += 1
510511
is_success = False
511512
error_log_msg = str(e)
513+
last_error_msg = error_log_msg
512514
logger.warning(
513515
f"Streaming API call failed with error: {error_log_msg}. Attempt {retries} of {max_retries}"
514516
)
@@ -553,3 +555,26 @@ async def stream_generate_content(
553555
latency_ms=latency_ms,
554556
request_time=request_datetime,
555557
)
558+
559+
# Emit final error SSE event if all retries failed
560+
if not is_success:
561+
# 从错误消息中提取嵌套JSON
562+
parsed_error = None
563+
if last_error_msg:
564+
try:
565+
# 查找JSON起始位置
566+
json_start = last_error_msg.find('{')
567+
if json_start != -1:
568+
json_str = last_error_msg[json_start:]
569+
parsed_error = json.loads(json_str)
570+
except json.JSONDecodeError:
571+
pass
572+
573+
error_data = {
574+
"error": {
575+
"code": parsed_error['error']['code'] if (parsed_error and 'error' in parsed_error and 'code' in parsed_error['error']) else (status_code or 500),
576+
"message": parsed_error['error']['message'] if (parsed_error and 'error' in parsed_error and 'message' in parsed_error['error']) else (last_error_msg or "Streaming failed"),
577+
"status": parsed_error['error']['status'] if (parsed_error and 'error' in parsed_error and 'status' in parsed_error['error']) else "INTERNAL"
578+
}
579+
}
580+
yield json.dumps(error_data, ensure_ascii=False)

app/service/chat/openai_chat_service.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -742,4 +742,4 @@ async def _handle_normal_image_completion(
742742
status_code=status_code,
743743
latency_ms=latency_ms,
744744
request_time=request_datetime,
745-
)
745+
)

app/service/chat/vertex_express_chat_service.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -400,4 +400,4 @@ async def stream_generate_content(
400400
status_code=status_code,
401401
latency_ms=latency_ms,
402402
request_time=request_datetime,
403-
)
403+
)

0 commit comments

Comments
 (0)