Files

90 lines
3.6 KiB
Python
Raw Permalink Normal View History

"""任务级成本/Token/耗时预估(交付报告 P2-1)。
思路:用本组织近期已结算的真实用量外推(历史均值 × 本次段数),
没有历史数据时用保守常量估计。不依赖任何外部服务。
"""
from __future__ import annotations
from sqlmodel import Session, select
from .models import Campaign, LLMCall, Segment, UsageEvent
from .billing import POINTS_PER_TOKEN
# 无历史数据时的保守单段估计(deepseek 级别的小上下文生产段)
FALLBACK_INPUT_TOKENS = 6000
FALLBACK_OUTPUT_TOKENS = 2500
FALLBACK_SECONDS = 90
def estimate_campaign_cost(session: Session, campaign: Campaign, single_segment_id: int | None = None) -> dict:
"""single_segment_id:逐段生成模式——只按指定段估算。"""
if single_segment_id:
segments = [s for s in session.exec(select(Segment).where(
Segment.campaign_id == campaign.id)).all() if s.id == int(single_segment_id)]
else:
segments = session.exec(select(Segment).where(
Segment.campaign_id == campaign.id)).all()
seg_count = len(segments)
if not seg_count:
return {"ready": False, "reason": "任务还没有内容段,先完成导图拆解",
"segments": 0}
org_id = int(campaign.organization_id or 1)
# 历史均值:本组织最近 200 条已测量 LLM 用量事件
events = session.exec(select(UsageEvent).where(
UsageEvent.organization_id == org_id,
UsageEvent.service_type == "llm",
).order_by(UsageEvent.id.desc()).limit(200)).all()
measured = [e for e in events
if e.status == "measured" and (e.input_quantity or 0) + (e.output_quantity or 0) > 0]
if measured:
avg_in = sum(e.input_quantity or 0 for e in measured) / len(measured)
avg_out = sum(e.output_quantity or 0 for e in measured) / len(measured)
source = "history"
else:
avg_in, avg_out = float(FALLBACK_INPUT_TOKENS), float(FALLBACK_OUTPUT_TOKENS)
source = "fallback"
# 耗时:近 200 次 LLM 调用的平均时长 × 每段约 3 次调用(生产+质检)+ 组合余量
durations = [float(row.duration_ms or 0) for row in session.exec(
select(LLMCall).order_by(LLMCall.id.desc()).limit(200)).all()
if (row.duration_ms or 0) > 0]
avg_seconds = FALLBACK_SECONDS
if durations:
avg_seconds = (sum(durations) / len(durations)) / 1000 * 3 + 15
total_in = avg_in * seg_count
total_out = avg_out * seg_count
total_tokens = int((total_in + total_out) * 1.6) # 1.6 倍:每段附带质检与组合分摊
est_points = total_tokens * POINTS_PER_TOKEN
est_seconds = int(avg_seconds * seg_count + 20)
# 费用:历史事件费率均值
if measured:
avg_in_rate = sum(e.input_rate or 0 for e in measured) / len(measured)
avg_out_rate = sum(e.output_rate or 0 for e in measured) / len(measured)
else:
avg_in_rate = avg_out_rate = 0.0
amount_minor = int(round((total_in / 1e6 * avg_in_rate + total_out / 1e6 * avg_out_rate) * 100))
return {
"ready": True,
"source": source,
"segments": seg_count,
"estimated_input_tokens": int(total_in),
"estimated_output_tokens": int(total_out),
"estimated_tokens": total_tokens,
"estimated_points": est_points,
"estimated_amount_minor": amount_minor,
"estimated_seconds": est_seconds,
"estimated_duration_text": (f"{est_seconds // 60} 分 {est_seconds % 60} 秒"
if est_seconds >= 60 else f"{est_seconds} 秒"),
}
__all__ = ["estimate_campaign_cost"]