ai-model-studio / src /stream /processor.py
slk1st's picture
Deploy AI Model Studio
8a03d2c verified
Raw
History Blame Contribute Delete
13.5 kB
"""
流式响应处理器(简化版)
实现"假流式":收集所有上游数据后,解析并聚合为一个包含完整上下文(思考过程+最终内容)的响应,一次性发送给下游。
"""
import json
import time
from typing import Any, cast
import collections.abc
from src.core.errors import (
VcoreError,
EmptyResponseError,
InternalError,
NotFoundError,
InvalidArgumentError,
RateLimitError,
)
from src.utils.logger import get_logger
from src.utils.token_counter import calculate_usage_metadata
from .parser import parse_upstream_data
# 初始化日志
logger = get_logger(__name__)
class StreamProcessor:
"""
简化的流式响应处理器 (v3 - 假流式 + 思考过程)
职责:
1. 收集上游所有流式数据
2. 使用 parser 解析聚合后的数据
3. 构建 Gemini 格式的 SSE 响应
"""
def __init__(self):
logger.debug("初始化流处理器")
# 状态追踪
self._actual_content_sent = False
self._request_context: dict[str, Any] = {}
def has_actual_content_sent(self) -> bool:
"""检查是否已发送实际文本内容"""
return self._actual_content_sent
def set_request_context(self, downstream_payload: dict[str, Any], upstream_payload: dict[str, Any]):
"""设置请求上下文"""
logger.debug("设置流处理器请求上下文")
self._request_context = {
'downstream_payload': downstream_payload,
'upstream_payload': upstream_payload
}
def _create_gemini_chunk(
self,
parts: list[dict[str, Any]],
finish_reason: str | None,
safety_ratings: list[dict[str, Any]],
citation_metadata: dict[str, Any],
grounding_metadata: dict[str, Any],
candidate_index: int,
prompt_feedback: dict[str, Any],
usage_metadata: dict[str, Any],
finish_message: str | None = None,
token_count: int | None = None,
avg_logprobs: float | None = None,
logprobs_result: dict[str, Any] | None = None,
create_time: str | None = None,
model_version: str | None = None,
response_id: str | None = None,
model_status: dict[str, Any] | None = None,
) -> str:
"""根据聚合后的内容,创建一个包含完整上下文的Gemini格式SSE事件。"""
candidate: dict[str, Any] = {
"index": candidate_index
}
if finish_reason and isinstance(finish_reason, str):
candidate["finishReason"] = finish_reason.upper()
if finish_message:
candidate["finishMessage"] = finish_message
if parts:
candidate["content"] = {
"parts": parts,
"role": "model"
}
# 只添加实际存在的字段
if safety_ratings:
candidate["safetyRatings"] = safety_ratings
if citation_metadata:
candidate["citationMetadata"] = citation_metadata
if grounding_metadata:
candidate["groundingMetadata"] = grounding_metadata
if token_count is not None:
candidate["tokenCount"] = token_count
if avg_logprobs is not None:
candidate["avgLogprobs"] = avg_logprobs
if logprobs_result:
candidate["logprobsResult"] = logprobs_result
# 根据 doc.md,确保 candidate 其他字段正确
chunk: dict[str, Any] = {"candidates": [candidate]}
# 按照 doc.md 定义
if prompt_feedback:
chunk["promptFeedback"] = prompt_feedback
if usage_metadata:
chunk["usageMetadata"] = usage_metadata
if create_time:
chunk["createTime"] = create_time
if model_version:
chunk["modelVersion"] = model_version
if response_id:
chunk["responseId"] = response_id
if model_status:
chunk["modelStatus"] = model_status
return "data: " + json.dumps(chunk, ensure_ascii=False, separators=(',', ':')) + "\n\n"
async def process_stream(
self,
response_iterator: collections.abc.AsyncIterator[str],
model: str = "vcore-ai-proxy"
) -> collections.abc.AsyncGenerator[str, None]:
"""
处理流式响应(假流式 v3)。
"""
# 移除重复日志,只在开始时记录一次
# logger.info(f"开始处理流式响应: 模型={model}")
start_time = time.time()
raw_chunks: list[str] = []
try:
chunk_count = 0
logger.debug("开始收集上游数据块")
async for chunk in response_iterator:
chunk_count += 1
raw_chunks.append(chunk)
logger.debug(f"收集完成,共 {chunk_count} 个数据块")
raw_data = '\n'.join(raw_chunks)
# 记录完整的原始上游响应
try:
parsed_data = json.loads(raw_data)
logger.debug_json("完整原始上游响应", parsed_data)
except json.JSONDecodeError:
logger.debug_large("完整原始上游响应", raw_data)
if not raw_data:
logger.error("上游返回空数据")
# 触发错误备份
from src.utils.error_logger import save_error_snapshot
save_error_snapshot(
downstream_payload=self._request_context.get('downstream_payload', {}),
upstream_payload=self._request_context.get('upstream_payload', {}),
upstream_response="[EMPTY RESPONSE]",
error_type="empty_response"
)
raise EmptyResponseError("Upstream returned no data")
# 使用独立解析函数
result = parse_upstream_data(raw_data)
# 关键修复:在这里处理解析出的上游错误
if result["has_error"] and not result["parts"]:
error_msg = result["error_message"]
error_obj = result.get("error_obj")
# 排除特定的、不需要备份的错误
is_auth_error = "Failed to verify action" in error_msg or "The caller does not have permission" in error_msg
is_rate_limit = isinstance(error_obj, RateLimitError) or "resource has been exhausted" in error_msg.lower() or "quota" in error_msg.lower()
if not is_auth_error and not is_rate_limit:
logger.error(f"API 错误且无内容: {error_msg}")
# 确定错误类型用于备份目录名
error_type = "api_error"
if error_obj:
# 如果有解析出的错误对象,使用其类名或 code
error_type = f"upstream_{error_obj.code}_{type(error_obj).__name__}"
# 触发错误备份
from src.utils.error_logger import save_error_snapshot
save_error_snapshot(
downstream_payload=self._request_context.get('downstream_payload', {}),
upstream_payload=self._request_context.get('upstream_payload', {}),
upstream_response=raw_data,
error_type=error_type
)
# 如果 parser 已经解析出了错误对象,直接抛出
if error_obj:
raise error_obj
# 降级处理:基于上游错误消息抛出错误
error_msg_lower = error_msg.lower()
if "not found" in error_msg_lower:
raise NotFoundError(message=error_msg)
elif is_rate_limit:
raise RateLimitError(message=error_msg)
elif is_auth_error:
from src.core.errors import AuthenticationError
raise AuthenticationError(
message=f"Authentication/Recaptcha failed: {error_msg}",
details={"upstream_response": error_msg},
upstream_response=error_msg
)
else:
raise InvalidArgumentError(message=error_msg)
finish_reason = result.get("finish_reason") or "STOP"
if not result["parts"] and finish_reason == "STOP" and not result["has_error"]:
if not result.get("prompt_feedback"):
logger.error("上游返回空响应 (无 parts 且 finish_reason=STOP)")
# 触发错误备份
from src.utils.error_logger import save_error_snapshot
save_error_snapshot(
downstream_payload=self._request_context.get('downstream_payload', {}),
upstream_payload=self._request_context.get('upstream_payload', {}),
upstream_response=raw_data,
error_type="stop_no_content"
)
# 快照将由 VcoreAIClient 统一保存
raise EmptyResponseError("Upstream returned empty response (STOP with no content/metadata)")
# 计算 usage metadata
usage_metadata = result.get("usage_metadata", {})
if not usage_metadata and self._request_context:
try:
downstream_payload = self._request_context.get('downstream_payload', {})
# 从请求上下文中提取输入内容
prompt_contents: list[dict[str, Any]] = []
if 'gemini_payload' in downstream_payload:
gemini_payload = downstream_payload['gemini_payload']
if isinstance(gemini_payload, dict) and 'contents' in gemini_payload:
prompt_contents = cast(list[dict[str, Any]], gemini_payload['contents'])
# 计算 token 使用情况
usage_metadata = await calculate_usage_metadata(
prompt_contents=prompt_contents,
response_parts=result["parts"],
request_context=self._request_context
)
except Exception as e:
logger.warning(f"计算 usage metadata 失败: {e}")
usage_metadata = {}
final_chunk = self._create_gemini_chunk(
parts=result["parts"],
finish_reason=result.get("finish_reason"),
safety_ratings=result.get("safety_ratings", []),
citation_metadata=result.get("citation_metadata", {}),
grounding_metadata=result.get("grounding_metadata", {}),
candidate_index=result.get("candidate_index", 0),
prompt_feedback=result.get("prompt_feedback", {}),
usage_metadata=usage_metadata,
finish_message=result.get("finish_message"),
token_count=result.get("token_count"),
avg_logprobs=result.get("avg_logprobs"),
logprobs_result=result.get("logprobs_result"),
create_time=result.get("create_time"),
model_version=result.get("model_version"),
response_id=result.get("response_id"),
model_status=result.get("model_status")
)
process_time = time.time() - start_time
logger.success(f"流式响应处理完成: 耗时={process_time:.3f}s, 完成原因={result.get('finish_reason', 'UNKNOWN')}")
yield final_chunk
self._actual_content_sent = True
except VcoreError as e:
if "Failed to verify action" in e.message or "The caller does not have permission" in e.message:
from src.core.errors import AuthenticationError
if not isinstance(e, AuthenticationError):
raise AuthenticationError(
message=f"Stream contained Authentication error: {e.message}",
details={"upstream_response": e.upstream_response or e.message},
upstream_response=e.upstream_response or e.message
)
else:
logger.error(f"流处理 Vcore 错误: {e.message}")
raise
except Exception as e:
logger.error(f"流处理未知错误: {e}")
# 触发内部错误备份
from src.utils.error_logger import save_error_snapshot
save_error_snapshot(
downstream_payload=self._request_context.get('downstream_payload', {}),
upstream_payload=self._request_context.get('upstream_payload', {}),
upstream_response=str(e),
error_type="internal_exception"
)
raise InternalError(message=f"Unknown stream processing error: {e}")
def get_stream_processor() -> StreamProcessor:
"""创建流处理器实例"""
return StreamProcessor()