Files
Aether/_deprecated_py_src/services/request/candidate.py

404 lines
15 KiB
Python
Raw Normal View History

2025-12-10 20:52:44 +08:00
"""
请求候选记录服务 - 管理候选队列
"""
import uuid
from datetime import datetime, timezone
from sqlalchemy.orm import Session
from src.core.batch_committer import get_batch_committer
from src.core.logger import logger
2025-12-10 20:52:44 +08:00
from src.models.database import RequestCandidate
class RequestCandidateService:
"""请求候选记录服务"""
perf: 优化请求鉴权链路并批量化统计/调度查询 - 为 Pipeline/Context 增加按需读取请求体能力,支持 async 懒加载 JSON body - 为 chat/cli/video/claude/openai-cli 适配器关闭默认预读,减少无效 body 读取与超时风险 - 将本地登录、JWT 用户加载、API Key 鉴权迁移到线程池隔离会话执行,避免阻塞事件循环 - 为 API Key 鉴权返回结构化余额结果,并在主请求会话中重新绑定 user/api_key 后再校验状态、过期和锁定信息 - 为 management/user token 前缀认证引入独立会话与结果回绑,避免跨会话对象写入失效 - 为 Usage 余额检查补充结构化返回,统一透出 remaining 与欠费/不可用文案映射 - 为用户与管理端活跃请求查询增加 maintain_status 开关,避免轮询指定 id 时误触发状态修复 - 重写 user_me usage 汇总逻辑,支持 group_by=None 的粗粒度聚合 - 修正 provider 维度成功率与平均响应时间统计,基于 success_count 和成功响应耗时汇总计算 - 前端 Usage 轮询由 setInterval 改为串行 setTimeout,避免并发轮询叠加 - 为 StatsAggregator 增加按本地日期批量计算百分位能力,替代逐天 fan-out 查询 - 为混合统计查询合并连续实时日期区间,并批量读取 StatsDaily,减少逐日查询次数 - 为用户日统计增加批量聚合入口,替代逐用户循环聚合 - 为系统配置导出改用 selectinload 预加载 provider 关联数据,减少 N+1 查询 - 为管理员用户列表增加钱包批量查询,避免逐用户回表 - 为调度器增加 provider 轻量引用预过滤,先按 allowed_providers 缩小范围再加载完整 provider 图 - 为 CandidateBuilder 增加 provider refs/provider_ids 查询能力,保留分页顺序 - 为模型缓存增加 provider_model_mappings 索引缓存与 model_mappings 规则缓存,减少重复全量扫描 - 为请求候选中间态改为 flush/batch commit,降低 pending/streaming 状态切换的事务往返 - 为钱包访问结果补充 balance_snapshot,并抽取余额快照复用逻辑 - 补充 pipeline、auth、admin users、user_me usage、stats aggregator、model cache、 scheduler、wallet、request candidate 等回归与契约测试
2026-03-09 22:57:23 +08:00
@staticmethod
def _persist_candidate_update(db: Session, *, immediate: bool) -> None:
if immediate:
db.commit()
return
db.flush()
get_batch_committer().mark_dirty(db)
2025-12-10 20:52:44 +08:00
@staticmethod
def create_candidate(
db: Session,
request_id: str,
candidate_index: int,
candidate_id: str | None = None,
2025-12-10 20:52:44 +08:00
retry_index: int = 0, # 新增:重试序号
user_id: str | None = None,
api_key_id: str | None = None,
username: str | None = None,
api_key_name: str | None = None,
provider_id: str | None = None,
endpoint_id: str | None = None,
key_id: str | None = None,
2025-12-10 20:52:44 +08:00
status: str = "available",
skip_reason: str | None = None,
2025-12-10 20:52:44 +08:00
is_cached: bool = False,
extra_data: dict | None = None,
required_capabilities: dict | None = None,
2025-12-10 20:52:44 +08:00
) -> RequestCandidate:
"""
创建候选记录
Args:
db: 数据库会话
request_id: 请求ID
candidate_index: 候选序号
retry_index: 重试序号从0开始
user_id: 用户ID
api_key_id: API Key ID
username: 用户名快照
api_key_name: API Key 名称快照
2025-12-10 20:52:44 +08:00
provider_id: Provider ID
endpoint_id: Endpoint ID
key_id: API Key ID
status: 候选状态 ('available', 'used', 'skipped', 'success', 'failed')
skip_reason: 跳过原因
is_cached: 是否为缓存亲和性候选
extra_data: 额外数据
required_capabilities: 请求需要的能力标签
"""
candidate = RequestCandidate(
id=str(candidate_id or uuid.uuid4()),
2025-12-10 20:52:44 +08:00
request_id=request_id,
candidate_index=candidate_index,
retry_index=retry_index, # 新增
user_id=user_id,
api_key_id=api_key_id,
username=username,
api_key_name=api_key_name,
2025-12-10 20:52:44 +08:00
provider_id=provider_id,
endpoint_id=endpoint_id,
key_id=key_id,
status=status,
skip_reason=skip_reason,
is_cached=is_cached,
extra_data=extra_data or {},
required_capabilities=required_capabilities,
created_at=datetime.now(timezone.utc),
)
db.add(candidate)
db.flush() # 只flush不立即 commit
# 标记为批量提交(非关键数据,可延迟)
get_batch_committer().mark_dirty(db)
return candidate
@staticmethod
def mark_candidate_started(db: Session, candidate_id: str) -> None:
"""
标记候选开始执行
Args:
db: 数据库会话
candidate_id: 候选ID
"""
candidate = db.query(RequestCandidate).filter(RequestCandidate.id == candidate_id).first()
if candidate:
candidate.status = "pending"
candidate.started_at = datetime.now(timezone.utc)
perf: 优化请求鉴权链路并批量化统计/调度查询 - 为 Pipeline/Context 增加按需读取请求体能力,支持 async 懒加载 JSON body - 为 chat/cli/video/claude/openai-cli 适配器关闭默认预读,减少无效 body 读取与超时风险 - 将本地登录、JWT 用户加载、API Key 鉴权迁移到线程池隔离会话执行,避免阻塞事件循环 - 为 API Key 鉴权返回结构化余额结果,并在主请求会话中重新绑定 user/api_key 后再校验状态、过期和锁定信息 - 为 management/user token 前缀认证引入独立会话与结果回绑,避免跨会话对象写入失效 - 为 Usage 余额检查补充结构化返回,统一透出 remaining 与欠费/不可用文案映射 - 为用户与管理端活跃请求查询增加 maintain_status 开关,避免轮询指定 id 时误触发状态修复 - 重写 user_me usage 汇总逻辑,支持 group_by=None 的粗粒度聚合 - 修正 provider 维度成功率与平均响应时间统计,基于 success_count 和成功响应耗时汇总计算 - 前端 Usage 轮询由 setInterval 改为串行 setTimeout,避免并发轮询叠加 - 为 StatsAggregator 增加按本地日期批量计算百分位能力,替代逐天 fan-out 查询 - 为混合统计查询合并连续实时日期区间,并批量读取 StatsDaily,减少逐日查询次数 - 为用户日统计增加批量聚合入口,替代逐用户循环聚合 - 为系统配置导出改用 selectinload 预加载 provider 关联数据,减少 N+1 查询 - 为管理员用户列表增加钱包批量查询,避免逐用户回表 - 为调度器增加 provider 轻量引用预过滤,先按 allowed_providers 缩小范围再加载完整 provider 图 - 为 CandidateBuilder 增加 provider refs/provider_ids 查询能力,保留分页顺序 - 为模型缓存增加 provider_model_mappings 索引缓存与 model_mappings 规则缓存,减少重复全量扫描 - 为请求候选中间态改为 flush/batch commit,降低 pending/streaming 状态切换的事务往返 - 为钱包访问结果补充 balance_snapshot,并抽取余额快照复用逻辑 - 补充 pipeline、auth、admin users、user_me usage、stats aggregator、model cache、 scheduler、wallet、request candidate 等回归与契约测试
2026-03-09 22:57:23 +08:00
# 中间态改为 flush最终 success/failed 仍会立即提交,
# 但开始执行这一跳不再单独制造一次事务往返。
RequestCandidateService._persist_candidate_update(db, immediate=False)
2025-12-10 20:52:44 +08:00
@staticmethod
def update_candidate_status(db: Session, candidate_id: str, status: str) -> None:
"""
更新候选状态通用方法
Args:
db: 数据库会话
candidate_id: 候选ID
status: 新状态pending, available, success, failed, skipped
"""
candidate = db.query(RequestCandidate).filter(RequestCandidate.id == candidate_id).first()
if candidate:
candidate.status = status
# 如果状态变更为 pending记录开始时间
if status == "pending" and not candidate.started_at:
candidate.started_at = datetime.now(timezone.utc)
perf: 优化请求鉴权链路并批量化统计/调度查询 - 为 Pipeline/Context 增加按需读取请求体能力,支持 async 懒加载 JSON body - 为 chat/cli/video/claude/openai-cli 适配器关闭默认预读,减少无效 body 读取与超时风险 - 将本地登录、JWT 用户加载、API Key 鉴权迁移到线程池隔离会话执行,避免阻塞事件循环 - 为 API Key 鉴权返回结构化余额结果,并在主请求会话中重新绑定 user/api_key 后再校验状态、过期和锁定信息 - 为 management/user token 前缀认证引入独立会话与结果回绑,避免跨会话对象写入失效 - 为 Usage 余额检查补充结构化返回,统一透出 remaining 与欠费/不可用文案映射 - 为用户与管理端活跃请求查询增加 maintain_status 开关,避免轮询指定 id 时误触发状态修复 - 重写 user_me usage 汇总逻辑,支持 group_by=None 的粗粒度聚合 - 修正 provider 维度成功率与平均响应时间统计,基于 success_count 和成功响应耗时汇总计算 - 前端 Usage 轮询由 setInterval 改为串行 setTimeout,避免并发轮询叠加 - 为 StatsAggregator 增加按本地日期批量计算百分位能力,替代逐天 fan-out 查询 - 为混合统计查询合并连续实时日期区间,并批量读取 StatsDaily,减少逐日查询次数 - 为用户日统计增加批量聚合入口,替代逐用户循环聚合 - 为系统配置导出改用 selectinload 预加载 provider 关联数据,减少 N+1 查询 - 为管理员用户列表增加钱包批量查询,避免逐用户回表 - 为调度器增加 provider 轻量引用预过滤,先按 allowed_providers 缩小范围再加载完整 provider 图 - 为 CandidateBuilder 增加 provider refs/provider_ids 查询能力,保留分页顺序 - 为模型缓存增加 provider_model_mappings 索引缓存与 model_mappings 规则缓存,减少重复全量扫描 - 为请求候选中间态改为 flush/batch commit,降低 pending/streaming 状态切换的事务往返 - 为钱包访问结果补充 balance_snapshot,并抽取余额快照复用逻辑 - 补充 pipeline、auth、admin users、user_me usage、stats aggregator、model cache、 scheduler、wallet、request candidate 等回归与契约测试
2026-03-09 22:57:23 +08:00
RequestCandidateService._persist_candidate_update(
db, immediate=status not in {"pending", "streaming"}
)
2025-12-10 20:52:44 +08:00
@staticmethod
def mark_candidate_streaming(
db: Session,
candidate_id: str,
concurrent_requests: int | None = None,
2025-12-10 20:52:44 +08:00
) -> None:
"""
标记候选为流式传输中
用于流式请求连接建立成功后流开始传输时调用
此时请求尚未完成需要等流传输完毕后再调用 mark_candidate_success
注意streaming 阶段不设置 status_code最终状态码由
mark_candidate_success / mark_candidate_failed 在流结束时写入
2025-12-10 20:52:44 +08:00
Args:
db: 数据库会话
candidate_id: 候选ID
concurrent_requests: 并发请求数
"""
candidate = db.query(RequestCandidate).filter(RequestCandidate.id == candidate_id).first()
if candidate:
candidate.status = "streaming"
candidate.concurrent_requests = concurrent_requests
# streaming 状态不设置 finished_at 和 status_code因为请求还在进行中
perf: 优化请求鉴权链路并批量化统计/调度查询 - 为 Pipeline/Context 增加按需读取请求体能力,支持 async 懒加载 JSON body - 为 chat/cli/video/claude/openai-cli 适配器关闭默认预读,减少无效 body 读取与超时风险 - 将本地登录、JWT 用户加载、API Key 鉴权迁移到线程池隔离会话执行,避免阻塞事件循环 - 为 API Key 鉴权返回结构化余额结果,并在主请求会话中重新绑定 user/api_key 后再校验状态、过期和锁定信息 - 为 management/user token 前缀认证引入独立会话与结果回绑,避免跨会话对象写入失效 - 为 Usage 余额检查补充结构化返回,统一透出 remaining 与欠费/不可用文案映射 - 为用户与管理端活跃请求查询增加 maintain_status 开关,避免轮询指定 id 时误触发状态修复 - 重写 user_me usage 汇总逻辑,支持 group_by=None 的粗粒度聚合 - 修正 provider 维度成功率与平均响应时间统计,基于 success_count 和成功响应耗时汇总计算 - 前端 Usage 轮询由 setInterval 改为串行 setTimeout,避免并发轮询叠加 - 为 StatsAggregator 增加按本地日期批量计算百分位能力,替代逐天 fan-out 查询 - 为混合统计查询合并连续实时日期区间,并批量读取 StatsDaily,减少逐日查询次数 - 为用户日统计增加批量聚合入口,替代逐用户循环聚合 - 为系统配置导出改用 selectinload 预加载 provider 关联数据,减少 N+1 查询 - 为管理员用户列表增加钱包批量查询,避免逐用户回表 - 为调度器增加 provider 轻量引用预过滤,先按 allowed_providers 缩小范围再加载完整 provider 图 - 为 CandidateBuilder 增加 provider refs/provider_ids 查询能力,保留分页顺序 - 为模型缓存增加 provider_model_mappings 索引缓存与 model_mappings 规则缓存,减少重复全量扫描 - 为请求候选中间态改为 flush/batch commit,降低 pending/streaming 状态切换的事务往返 - 为钱包访问结果补充 balance_snapshot,并抽取余额快照复用逻辑 - 补充 pipeline、auth、admin users、user_me usage、stats aggregator、model cache、 scheduler、wallet、request candidate 等回归与契约测试
2026-03-09 22:57:23 +08:00
RequestCandidateService._persist_candidate_update(db, immediate=False)
2025-12-10 20:52:44 +08:00
@staticmethod
def mark_candidate_success(
db: Session,
candidate_id: str,
status_code: int,
latency_ms: int,
concurrent_requests: int | None = None,
extra_data: dict | None = None,
2025-12-10 20:52:44 +08:00
) -> None:
"""
标记候选执行成功
Args:
db: 数据库会话
candidate_id: 候选ID
status_code: HTTP 状态码
latency_ms: 延迟毫秒
concurrent_requests: 并发请求数
extra_data: 额外数据
"""
candidate = db.query(RequestCandidate).filter(RequestCandidate.id == candidate_id).first()
if candidate:
candidate.status = "success"
candidate.status_code = status_code
candidate.latency_ms = latency_ms
candidate.concurrent_requests = concurrent_requests
candidate.finished_at = datetime.now(timezone.utc)
# 成功时清空错误字段(可能是整流重试后成功,之前记录过错误)
candidate.error_type = None
candidate.error_message = None
2025-12-10 20:52:44 +08:00
if extra_data:
candidate.extra_data = {**(candidate.extra_data or {}), **extra_data}
# 关键状态更新:立即提交,不使用批量提交
# 原因:前端需要实时看到请求成功/失败状态
db.commit()
@staticmethod
def mark_candidate_failed(
db: Session,
candidate_id: str,
error_type: str,
error_message: str,
status_code: int | None = None,
latency_ms: int | None = None,
concurrent_requests: int | None = None,
extra_data: dict | None = None,
2025-12-10 20:52:44 +08:00
) -> None:
"""
标记候选执行失败
Args:
db: 数据库会话
candidate_id: 候选ID
error_type: 错误类型
error_message: 错误消息
status_code: HTTP 状态码如果有
latency_ms: 延迟毫秒
concurrent_requests: 并发请求数
extra_data: 额外数据
"""
candidate = db.query(RequestCandidate).filter(RequestCandidate.id == candidate_id).first()
if candidate:
candidate.status = "failed"
candidate.error_type = error_type
candidate.error_message = error_message
candidate.status_code = status_code
candidate.latency_ms = latency_ms
candidate.concurrent_requests = concurrent_requests
candidate.finished_at = datetime.now(timezone.utc)
if extra_data:
candidate.extra_data = {**(candidate.extra_data or {}), **extra_data}
# 关键状态更新:立即提交,不使用批量提交
# 原因:前端需要实时看到请求成功/失败状态
db.commit()
@staticmethod
def mark_candidate_cancelled(
db: Session,
candidate_id: str,
status_code: int = 499,
latency_ms: int | None = None,
concurrent_requests: int | None = None,
extra_data: dict | None = None,
) -> None:
"""
标记候选被客户端取消
客户端主动断开连接不算系统失败使用 cancelled 状态
Args:
db: 数据库会话
candidate_id: 候选ID
status_code: HTTP 状态码通常是 499
latency_ms: 延迟毫秒
concurrent_requests: 并发请求数
extra_data: 额外数据
"""
candidate = db.query(RequestCandidate).filter(RequestCandidate.id == candidate_id).first()
if candidate:
candidate.status = "cancelled"
candidate.status_code = status_code
candidate.latency_ms = latency_ms
candidate.concurrent_requests = concurrent_requests
candidate.finished_at = datetime.now(timezone.utc)
if extra_data:
candidate.extra_data = {**(candidate.extra_data or {}), **extra_data}
db.commit()
2025-12-10 20:52:44 +08:00
@staticmethod
def mark_candidate_skipped(
db: Session,
candidate_id: str,
skip_reason: str | None = None,
*,
status_code: int | None = None,
concurrent_requests: int | None = None,
extra_data: dict | None = None,
2025-12-10 20:52:44 +08:00
) -> None:
"""
标记候选为已跳过
Args:
db: 数据库会话
candidate_id: 候选ID
skip_reason: 跳过原因
status_code: HTTP 状态码可选
concurrent_requests: 并发请求数这里实际记录 RPM 计数
extra_data: 额外数据合并写入
2025-12-10 20:52:44 +08:00
"""
candidate = db.query(RequestCandidate).filter(RequestCandidate.id == candidate_id).first()
if candidate:
candidate.status = "skipped"
candidate.skip_reason = skip_reason
candidate.finished_at = datetime.now(timezone.utc)
if status_code is not None:
candidate.status_code = int(status_code)
if concurrent_requests is not None:
candidate.concurrent_requests = int(concurrent_requests)
if extra_data:
base = candidate.extra_data if isinstance(candidate.extra_data, dict) else {}
candidate.extra_data = {**base, **extra_data}
2025-12-10 20:52:44 +08:00
db.flush() # 只 flush不立即 commit
get_batch_committer().mark_dirty(db)
@staticmethod
def get_candidates_by_request_id(db: Session, request_id: str) -> list[RequestCandidate]:
2025-12-10 20:52:44 +08:00
"""
获取请求的所有候选记录
Args:
db: 数据库会话
request_id: 请求ID
Returns:
候选记录列表 candidate_index 排序
"""
return (
db.query(RequestCandidate)
.filter(RequestCandidate.request_id == request_id)
.order_by(RequestCandidate.candidate_index)
.all()
)
@staticmethod
def get_candidate_stats_by_provider(db: Session, provider_id: str, limit: int = 100) -> dict:
"""
获取 Provider 的候选统计
Args:
db: 数据库会话
provider_id: Provider ID
limit: 最近记录数量限制
Returns:
统计信息字典
"""
candidates = (
db.query(RequestCandidate)
.filter(RequestCandidate.provider_id == provider_id)
.order_by(RequestCandidate.created_at.desc())
.limit(limit)
.all()
)
total_candidates = len(candidates)
success_count = sum(1 for c in candidates if c.status == "success")
failed_count = sum(1 for c in candidates if c.status == "failed")
cancelled_count = sum(1 for c in candidates if c.status == "cancelled")
2025-12-10 20:52:44 +08:00
skipped_count = sum(1 for c in candidates if c.status == "skipped")
pending_count = sum(1 for c in candidates if c.status == "pending")
available_count = sum(1 for c in candidates if c.status == "available")
# 计算失败率只统计已完成的候选即成功或失败的cancelled 不算失败)
2025-12-10 20:52:44 +08:00
completed_count = success_count + failed_count
failure_rate = (failed_count / completed_count * 100) if completed_count > 0 else 0
return {
"total_attempts": total_candidates, # 前端使用 total_attempts 字段
"success_count": success_count,
"failed_count": failed_count,
"cancelled_count": cancelled_count, # 客户端取消数
2025-12-10 20:52:44 +08:00
"skipped_count": skipped_count,
"pending_count": pending_count,
"available_count": available_count, # 尚未被调度的候选数
2025-12-10 20:52:44 +08:00
"failure_rate": round(failure_rate, 2),
}
@staticmethod
def calculate_candidate_ttfb(
db: Session,
candidate_id: str,
request_start_time: float,
global_first_byte_time_ms: int,
) -> int:
"""
计算候选自身的首字节时间 (TTFB)
请求链路追踪中的 TTFB 应该是"该候选自身"的首字时间
而不是整个请求从开始到收到首字节的时间
Args:
db: 数据库会话
candidate_id: 候选 ID
request_start_time: 请求开始时间Unix timestamp
global_first_byte_time_ms: 全局首字节时间相对于 request_start_time 的毫秒数
Returns:
候选自身的 TTFB毫秒如果计算失败则返回 global_first_byte_time_ms
"""
try:
candidate = (
db.query(RequestCandidate).filter(RequestCandidate.id == candidate_id).first()
)
if candidate and candidate.started_at:
started_at = candidate.started_at
if started_at.tzinfo is None:
started_at = started_at.replace(tzinfo=timezone.utc)
# 使用整数毫秒计算,避免浮点精度问题
request_start_epoch_ms = round(request_start_time * 1000)
started_at_epoch_ms = round(started_at.timestamp() * 1000)
first_byte_epoch_ms = request_start_epoch_ms + global_first_byte_time_ms
return max(0, int(first_byte_epoch_ms - started_at_epoch_ms))
except Exception as e:
logger.debug("计算候选 TTFB 失败: {}", e)
return global_first_byte_time_ms