Spaces:
Running
Running
| """SSE 流式工具。""" | |
| from __future__ import annotations | |
| import codecs | |
| import json | |
| from typing import Any, AsyncIterator, Iterable, Optional | |
| def sse_data(obj: Any) -> str: | |
| return f"data: {json.dumps(obj, ensure_ascii=False)}\n\n" | |
| def sse_done() -> str: | |
| return "data: [DONE]\n\n" | |
| def sse_event(event: str, obj: Any) -> str: | |
| return f"event: {event}\ndata: {json.dumps(obj, ensure_ascii=False)}\n\n" | |
| async def parse_sse_stream(byte_stream: AsyncIterator[bytes]) -> AsyncIterator[dict]: | |
| """从字节流解析 SSE,yield 每个 data JSON。 | |
| 两个关键点(否则中文/长回复会丢字、换行被破坏): | |
| 1. 用增量 UTF-8 解码器(codecs.getincrementaldecoder)跨 chunk 组装多字节 | |
| 字符。若每个 chunk 单独 decode(errors="replace"),被 TCP 分段切断的中文 | |
| 字符的前半字节会被替换成 ``?``,造成乱码/丢字。 | |
| 2. 按完整 SSE 帧(``\\n\\n`` 分隔)缓冲后再解析。httpx 的 aiter_bytes() | |
| 字节块边界与 SSE 帧边界无关,若按每个 chunk 独立 split('\\n') 解析, | |
| 跨块切断的 ``data: {...}`` 行会 json.loads 失败被静默丢弃。 | |
| """ | |
| decoder = codecs.getincrementaldecoder("utf-8")(errors="replace") | |
| buffer = "" | |
| async for chunk in byte_stream: | |
| if isinstance(chunk, (bytes, bytearray)): | |
| text = decoder.decode(bytes(chunk)) | |
| else: | |
| text = str(chunk) | |
| buffer += text | |
| # SSE 以双换行分隔 | |
| while "\n\n" in buffer: | |
| block, buffer = buffer.split("\n\n", 1) | |
| data_lines = [] | |
| for line in block.split("\n"): | |
| if line.startswith("data:"): | |
| data_lines.append(line[5:].strip()) | |
| if not data_lines: | |
| continue | |
| data_str = "\n".join(data_lines) | |
| if data_str == "[DONE]": | |
| yield {"__done__": True} | |
| return | |
| try: | |
| yield json.loads(data_str) | |
| except json.JSONDecodeError: | |
| yield {"__raw__": data_str} | |
| # 收尾:flush 增量解码器残余字节,并尝试解析缓冲区里最后一帧 | |
| tail = decoder.decode(b"", final=True) | |
| if tail: | |
| buffer += tail | |
| if buffer.strip(): | |
| data_lines = [] | |
| for line in buffer.split("\n"): | |
| if line.startswith("data:"): | |
| data_lines.append(line[5:].strip()) | |
| if data_lines: | |
| data_str = "\n".join(data_lines) | |
| if data_str == "[DONE]": | |
| yield {"__done__": True} | |
| return | |
| try: | |
| yield json.loads(data_str) | |
| except json.JSONDecodeError: | |
| yield {"__raw__": data_str} | |
| def _usage_from_sse_block(block: str) -> Optional[dict]: | |
| """从一个 SSE 帧文本里提取 usage 字段,没有则返回 None。""" | |
| data_lines = [] | |
| for line in block.split("\n"): | |
| if line.startswith("data:"): | |
| data_lines.append(line[5:].strip()) | |
| if not data_lines: | |
| return None | |
| data_str = "\n".join(data_lines) | |
| if data_str == "[DONE]": | |
| return None | |
| try: | |
| obj = json.loads(data_str) | |
| except json.JSONDecodeError: | |
| return None | |
| if not isinstance(obj, dict): | |
| return None | |
| u = obj.get("usage") | |
| return u if isinstance(u, dict) else None | |
| async def passthrough_with_usage( | |
| byte_stream: AsyncIterator[bytes], state: dict | |
| ) -> AsyncIterator[bytes]: | |
| """透传 SSE 字节流,同时把最后一个 chunk 的 usage 写入 ``state['usage']``。 | |
| 用于 OpenAI 透传流式:把字节原样转发给客户端的同时,旁路解析 SSE 帧, | |
| 提取上游在最后一个 chunk 里返回的 usage。需要请求时带 | |
| ``stream_options.include_usage=true`` 上游才会返回 usage。 | |
| """ | |
| decoder = codecs.getincrementaldecoder("utf-8")(errors="replace") | |
| buffer = "" | |
| async for chunk in byte_stream: | |
| yield chunk | |
| if isinstance(chunk, (bytes, bytearray)): | |
| text = decoder.decode(bytes(chunk)) | |
| else: | |
| text = str(chunk) | |
| buffer += text | |
| while "\n\n" in buffer: | |
| block, buffer = buffer.split("\n\n", 1) | |
| u = _usage_from_sse_block(block) | |
| if u is not None: | |
| state["usage"] = u | |
| tail = decoder.decode(b"", final=True) | |
| if tail: | |
| buffer += tail | |
| if buffer.strip(): | |
| u = _usage_from_sse_block(buffer) | |
| if u is not None: | |
| state["usage"] = u | |