"""任务级成本/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"]