Skip to content

Commit 651c2c6

Browse files
committed
perf(ai-gateway): P2-13 流式输出加首 token/chunk 空闲超时保护
1 parent b8e9a84 commit 651c2c6

1 file changed

Lines changed: 30 additions & 2 deletions

File tree

‎server/ai-gateway/routers/streaming.py‎

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@
77
错误:data: {"error": "..."}\n\n
88
99
@ai-context: 流式输出路由(SSE):以 Server-Sent Events 流式返回全量 AI 功能结果。
10+
@ai-context: P2-13 超时保护——event_generator 以手动 __anext__ + asyncio.wait_for
11+
enforce 首 token 超时与 chunk 间空闲超时,避免 provider 挂起导致流无限等待;
12+
超时后下发 error 事件并结束,客户端据此降级非流式。
1013
"""
1114

15+
import asyncio
1216
import json
1317
import logging
1418
import time
@@ -24,6 +28,14 @@
2428
logger = logging.getLogger(__name__)
2529
router = APIRouter(prefix="/api/v1/ai", tags=["流式输出"])
2630

31+
# ============================================================
32+
# P2-13 流式超时保护(秒)
33+
# ============================================================
34+
# 首 token 超时:从开始到第一个 chunk 的最长等待(含模型冷启动/排队)
35+
_FIRST_TOKEN_TIMEOUT = 30.0
36+
# chunk 间空闲超时:相邻 chunk 的最长间隔(防中途挂起)
37+
_CHUNK_IDLE_TIMEOUT = 30.0
38+
2739
# ============================================================
2840
# Prompt 模板目录
2941
# ============================================================
@@ -284,10 +296,26 @@ def _stream_fn(provider, model_name):
284296
raise HTTPException(status_code=503, detail="所有 AI 服务暂时不可用,请稍后重试")
285297

286298
async def event_generator() -> AsyncGenerator[str, None]:
287-
"""将 provider 的 async generator 包装为 SSE 事件流"""
299+
"""将 provider 的 async generator 包装为 SSE 事件流(P2-13:带首 token/空闲超时)"""
288300
start_time = time.monotonic()
301+
agen = gen.__aiter__()
302+
is_first = True
289303
try:
290-
async for chunk_text in gen:
304+
while True:
305+
timeout = _FIRST_TOKEN_TIMEOUT if is_first else _CHUNK_IDLE_TIMEOUT
306+
try:
307+
chunk_text = await asyncio.wait_for(agen.__anext__(), timeout=timeout)
308+
except StopAsyncIteration:
309+
break
310+
except asyncio.TimeoutError:
311+
phase = "首 token" if is_first else "chunk 间隔"
312+
logger.error(
313+
"流式超时: feature=%s, provider=%s, %s 超时=%.1fs",
314+
feature, used_provider, phase, timeout,
315+
)
316+
yield _sse_error(f"AI 响应超时({phase} {timeout:.0f}s)")
317+
break
318+
is_first = False
291319
if chunk_text:
292320
yield _sse_chunk(chunk_text)
293321
yield _sse_done()

0 commit comments

Comments
 (0)