This commit is contained in:
minecraft1024a
2025-11-12 21:26:21 +08:00
parent 6ef1829072
commit 89ca1651d9
3 changed files with 164 additions and 29 deletions

View File

@@ -65,16 +65,30 @@ class HTMLReportGenerator:
f"<tr>"
f"<td>{model_name}</td>"
f"<td>{count}</td>"
f"<td>{stat_data[IN_TOK_BY_MODEL].get(model_name, 0)}</td>"
f"<td>{stat_data[OUT_TOK_BY_MODEL].get(model_name, 0)}</td>"
f"<td>{stat_data[AVG_TOK_BY_MODEL].get(model_name, 0)}</td>"
f"<td>{stat_data[TOTAL_TOK_BY_MODEL].get(model_name, 0)}</td>"
f"<td>{stat_data[TPS_BY_MODEL].get(model_name, 0):.2f}</td>"
f"<td>{stat_data[COST_PER_KTOK_BY_MODEL].get(model_name, 0):.4f} ¥</td>"
f"<td>{stat_data[COST_BY_MODEL].get(model_name, 0):.4f} ¥</td>"
f"<td>{stat_data[AVG_TIME_COST_BY_MODEL].get(model_name, 0):.3f} 秒</td>"
f"<td>{stat_data[STD_TIME_COST_BY_MODEL].get(model_name, 0):.3f} 秒</td>"
f"</tr>"
for model_name, count in sorted(stat_data[REQ_CNT_BY_MODEL].items())
]
)
# 按供应商分类统计
provider_rows = "\n".join(
[
f"<tr>"
f"<td>{provider_name}</td>"
f"<td>{count}</td>"
f"<td>{stat_data[TOTAL_TOK_BY_PROVIDER].get(provider_name, 0)}</td>"
f"<td>{stat_data[TPS_BY_PROVIDER].get(provider_name, 0):.2f}</td>"
f"<td>{stat_data[COST_PER_KTOK_BY_PROVIDER].get(provider_name, 0):.4f} ¥</td>"
f"<td>{stat_data[COST_BY_PROVIDER].get(provider_name, 0):.4f} ¥</td>"
f"</tr>"
for provider_name, count in sorted(stat_data[REQ_CNT_BY_PROVIDER].items())
]
)
# 按请求类型分类统计
type_rows = "\n".join(
[
@@ -114,23 +128,55 @@ class HTMLReportGenerator:
for chat_id, count in sorted(stat_data[MSG_CNT_BY_CHAT].items())
]
)
summary_cards = f"""
<div class="summary-cards">
<div class="card">
<h3>总花费</h3>
<p>{stat_data.get(TOTAL_COST, 0):.4f} ¥</p>
</div>
<div class="card">
<h3>总请求数</h3>
<p>{stat_data.get(TOTAL_REQ_CNT, 0)}</p>
</div>
<div class="card">
<h3>总Token数</h3>
<p>{sum(stat_data.get(TOTAL_TOK_BY_MODEL, {}).values())}</p>
</div>
<div class="card">
<h3>总消息数</h3>
<p>{stat_data.get(TOTAL_MSG_CNT, 0)}</p>
</div>
<div class="card">
<h3>总在线时间</h3>
<p>{format_online_time(int(stat_data.get(ONLINE_TIME, 0)))}</p>
</div>
</div>
"""
# 增加饼图和条形图
# static_charts = self._generate_static_charts_div(stat_data, div_id) # 该功能尚未实现
static_charts = ""
return f"""
<div id="{div_id}" class="tab-content">
<p class="info-item">
<strong>统计时段: </strong>
{start_time.strftime("%Y-%m-%d %H:%M:%S")} ~ {now.strftime("%Y-%m-%d %H:%M:%S")}
</p>
<p class="info-item"><strong>总在线时间: </strong>{format_online_time(int(stat_data.get(ONLINE_TIME, 0)))}</p>
<p class="info-item"><strong>总消息数: </strong>{stat_data.get(TOTAL_MSG_CNT, 0)}</p>
<p class="info-item"><strong>总请求数: </strong>{stat_data.get(TOTAL_REQ_CNT, 0)}</p>
<p class="info-item"><strong>总花费: </strong>{stat_data.get(TOTAL_COST, 0):.4f} ¥</p>
{summary_cards}
{static_charts}
<h2>按模型分类统计</h2>
<table>
<tr><th>模型名称</th><th>调用次数</th><th>输入Token</th><th>输出Token</th><th>Token总量</th><th>累计花费</th><th>平均耗时(秒)</th><th>标准差(秒)</th></tr>
<tr><th>模型名称</th><th>调用次数</th><th>平均Token</th><th>Token总量</th><th>TPS</th><th>每K Token成本</th><th>累计花费</th><th>平均耗时(秒)</th></tr>
<tbody>{model_rows}</tbody>
</table>
<h2>按供应商分类统计</h2>
<table>
<tr><th>供应商名称</th><th>调用次数</th><th>Token总量</th><th>TPS</th><th>每K Token成本</th><th>累计花费</th></tr>
<tbody>{provider_rows}</tbody>
</table>
<h2>按模块分类统计</h2>
<table>
<thead>
@@ -156,7 +202,6 @@ class HTMLReportGenerator:
</table>
</div>
"""
def _generate_chart_tab(self, chart_data: dict) -> str:
"""生成图表选项卡的HTML内容。"""
return f"""

View File

@@ -246,6 +246,7 @@ class StatisticOutputTask(AsyncTask):
REQ_CNT_BY_USER: defaultdict(int),
REQ_CNT_BY_MODEL: defaultdict(int),
REQ_CNT_BY_MODULE: defaultdict(int),
REQ_CNT_BY_PROVIDER: defaultdict(int), # New
IN_TOK_BY_TYPE: defaultdict(int),
IN_TOK_BY_USER: defaultdict(int),
IN_TOK_BY_MODEL: defaultdict(int),
@@ -258,15 +259,18 @@ class StatisticOutputTask(AsyncTask):
TOTAL_TOK_BY_USER: defaultdict(int),
TOTAL_TOK_BY_MODEL: defaultdict(int),
TOTAL_TOK_BY_MODULE: defaultdict(int),
TOTAL_TOK_BY_PROVIDER: defaultdict(int), # New
TOTAL_COST: 0.0,
COST_BY_TYPE: defaultdict(float),
COST_BY_USER: defaultdict(float),
COST_BY_MODEL: defaultdict(float),
COST_BY_MODULE: defaultdict(float),
COST_BY_PROVIDER: defaultdict(float), # New
TIME_COST_BY_TYPE: defaultdict(list),
TIME_COST_BY_USER: defaultdict(list),
TIME_COST_BY_MODEL: defaultdict(list),
TIME_COST_BY_MODULE: defaultdict(list),
TIME_COST_BY_PROVIDER: defaultdict(list), # New
AVG_TIME_COST_BY_TYPE: defaultdict(float),
AVG_TIME_COST_BY_USER: defaultdict(float),
AVG_TIME_COST_BY_MODEL: defaultdict(float),
@@ -275,6 +279,17 @@ class StatisticOutputTask(AsyncTask):
STD_TIME_COST_BY_USER: defaultdict(float),
STD_TIME_COST_BY_MODEL: defaultdict(float),
STD_TIME_COST_BY_MODULE: defaultdict(float),
# New calculated fields
TPS_BY_MODEL: defaultdict(float),
COST_PER_KTOK_BY_MODEL: defaultdict(float),
AVG_TOK_BY_MODEL: defaultdict(float),
TPS_BY_PROVIDER: defaultdict(float),
COST_PER_KTOK_BY_PROVIDER: defaultdict(float),
# Chart data
PIE_CHART_COST_BY_PROVIDER: {},
PIE_CHART_REQ_BY_PROVIDER: {},
BAR_CHART_COST_BY_MODEL: {},
BAR_CHART_REQ_BY_MODEL: {},
}
for period_key, _ in collect_period
}
@@ -309,6 +324,7 @@ class StatisticOutputTask(AsyncTask):
request_type = record.get("request_type") or "unknown"
user_id = record.get("user_id") or "unknown"
model_name = record.get("model_name") or "unknown"
provider_name = record.get("model_api_provider") or "unknown"
# 提取模块名:如果请求类型包含".",取第一个"."之前的部分
module_name = request_type.split(".")[0] if "." in request_type else request_type
@@ -317,6 +333,7 @@ class StatisticOutputTask(AsyncTask):
stats[period_key][REQ_CNT_BY_USER][user_id] += 1
stats[period_key][REQ_CNT_BY_MODEL][model_name] += 1
stats[period_key][REQ_CNT_BY_MODULE][module_name] += 1
stats[period_key][REQ_CNT_BY_PROVIDER][provider_name] += 1
prompt_tokens = record.get("prompt_tokens") or 0
completion_tokens = record.get("completion_tokens") or 0
@@ -336,6 +353,7 @@ class StatisticOutputTask(AsyncTask):
stats[period_key][TOTAL_TOK_BY_USER][user_id] += total_tokens
stats[period_key][TOTAL_TOK_BY_MODEL][model_name] += total_tokens
stats[period_key][TOTAL_TOK_BY_MODULE][module_name] += total_tokens
stats[period_key][TOTAL_TOK_BY_PROVIDER][provider_name] += total_tokens
cost = record.get("cost") or 0.0
stats[period_key][TOTAL_COST] += cost
@@ -343,6 +361,7 @@ class StatisticOutputTask(AsyncTask):
stats[period_key][COST_BY_USER][user_id] += cost
stats[period_key][COST_BY_MODEL][model_name] += cost
stats[period_key][COST_BY_MODULE][module_name] += cost
stats[period_key][COST_BY_PROVIDER][provider_name] += cost
# 收集time_cost数据
time_cost = record.get("time_cost") or 0.0
@@ -351,32 +370,84 @@ class StatisticOutputTask(AsyncTask):
stats[period_key][TIME_COST_BY_USER][user_id].append(time_cost)
stats[period_key][TIME_COST_BY_MODEL][model_name].append(time_cost)
stats[period_key][TIME_COST_BY_MODULE][module_name].append(time_cost)
stats[period_key][TIME_COST_BY_PROVIDER][provider_name].append(time_cost)
break
# -- 计算派生指标 --
for period_key, period_stats in stats.items():
# 计算模型相关指标
for model_name, req_count in period_stats[REQ_CNT_BY_MODEL].items():
total_tok = period_stats[TOTAL_TOK_BY_MODEL].get(model_name, 0)
total_cost = period_stats[COST_BY_MODEL].get(model_name, 0.0)
time_costs = period_stats[TIME_COST_BY_MODEL].get(model_name, [])
total_time_cost = sum(time_costs)
# TPS
if total_time_cost > 0:
period_stats[TPS_BY_MODEL][model_name] = round(total_tok / total_time_cost, 2)
# Cost per 1K Tokens
if total_tok > 0:
period_stats[COST_PER_KTOK_BY_MODEL][model_name] = round((total_cost / total_tok) * 1000, 4)
# Avg Tokens per Request
period_stats[AVG_TOK_BY_MODEL][model_name] = round(total_tok / req_count) if req_count > 0 else 0
# 计算供应商相关指标
for provider_name, req_count in period_stats[REQ_CNT_BY_PROVIDER].items():
total_tok = period_stats[TOTAL_TOK_BY_PROVIDER].get(provider_name, 0)
total_cost = period_stats[COST_BY_PROVIDER].get(provider_name, 0.0)
time_costs = period_stats[TIME_COST_BY_PROVIDER].get(provider_name, [])
total_time_cost = sum(time_costs)
# TPS
if total_time_cost > 0:
period_stats[TPS_BY_PROVIDER][provider_name] = round(total_tok / total_time_cost, 2)
# Cost per 1K Tokens
if total_tok > 0:
period_stats[COST_PER_KTOK_BY_PROVIDER][provider_name] = round((total_cost / total_tok) * 1000, 4)
# 计算平均耗时和标准差
for period_key in stats:
for category in [REQ_CNT_BY_TYPE, REQ_CNT_BY_USER, REQ_CNT_BY_MODEL, REQ_CNT_BY_MODULE]:
time_cost_key = f"time_costs_by_{category.split('_')[-1]}"
avg_key = f"avg_time_costs_by_{category.split('_')[-1]}"
std_key = f"std_time_costs_by_{category.split('_')[-1]}"
for category_key, items in [
(REQ_CNT_BY_TYPE, "type"),
(REQ_CNT_BY_USER, "user"),
(REQ_CNT_BY_MODEL, "model"),
(REQ_CNT_BY_MODULE, "module"),
]:
time_cost_key = f"time_costs_by_{items}"
avg_key = f"avg_time_costs_by_{items}"
std_key = f"std_time_costs_by_{items}"
for item_name in stats[period_key][category]:
time_costs = stats[period_key][time_cost_key].get(item_name, [])
for item_name in period_stats[category_key]:
time_costs = period_stats[time_cost_key].get(item_name, [])
if time_costs:
# 计算平均耗时
avg_time_cost = sum(time_costs) / len(time_costs)
stats[period_key][avg_key][item_name] = round(avg_time_cost, 3)
# 计算标准差
avg_time = sum(time_costs) / len(time_costs)
period_stats[avg_key][item_name] = round(avg_time, 3)
if len(time_costs) > 1:
variance = sum((x - avg_time_cost) ** 2 for x in time_costs) / len(time_costs)
std_time_cost = variance**0.5
stats[period_key][std_key][item_name] = round(std_time_cost, 3)
variance = sum((x - avg_time) ** 2 for x in time_costs) / len(time_costs)
period_stats[std_key][item_name] = round(variance**0.5, 3)
else:
stats[period_key][std_key][item_name] = 0.0
period_stats[std_key][item_name] = 0.0
else:
stats[period_key][avg_key][item_name] = 0.0
stats[period_key][std_key][item_name] = 0.0
period_stats[avg_key][item_name] = 0.0
period_stats[std_key][item_name] = 0.0
# 准备图表数据
# 按供应商花费饼图
provider_costs = period_stats[COST_BY_PROVIDER]
if provider_costs:
sorted_providers = sorted(provider_costs.items(), key=lambda item: item[1], reverse=True)
period_stats[PIE_CHART_COST_BY_PROVIDER] = {
"labels": [item[0] for item in sorted_providers],
"data": [round(item[1], 4) for item in sorted_providers],
}
# 按模型花费条形图
model_costs = period_stats[COST_BY_MODEL]
if model_costs:
sorted_models = sorted(model_costs.items(), key=lambda item: item[1], reverse=True)
period_stats[BAR_CHART_COST_BY_MODEL] = {
"labels": [item[0] for item in sorted_models],
"data": [round(item[1], 4) for item in sorted_models],
}
return stats
@staticmethod

View File

@@ -41,3 +41,22 @@ STD_TIME_COST_BY_TYPE = "std_time_costs_by_type"
STD_TIME_COST_BY_USER = "std_time_costs_by_user"
STD_TIME_COST_BY_MODEL = "std_time_costs_by_model"
STD_TIME_COST_BY_MODULE = "std_time_costs_by_module"
# 新增模型性能指标
TPS_BY_MODEL = "tps_by_model" # Tokens Per Second
COST_PER_KTOK_BY_MODEL = "cost_per_ktok_by_model"
AVG_TOK_BY_MODEL = "avg_tok_by_model"
# 新增按供应商统计
REQ_CNT_BY_PROVIDER = "requests_by_provider"
COST_BY_PROVIDER = "costs_by_provider"
TOTAL_TOK_BY_PROVIDER = "tokens_by_provider"
TPS_BY_PROVIDER = "tps_by_provider"
COST_PER_KTOK_BY_PROVIDER = "cost_per_ktok_by_provider"
TIME_COST_BY_PROVIDER = "time_cost_by_provider"
# 新增饼图和条形图数据
PIE_CHART_COST_BY_PROVIDER = "pie_chart_cost_by_provider"
PIE_CHART_REQ_BY_PROVIDER = "pie_chart_req_by_provider"
BAR_CHART_COST_BY_MODEL = "bar_chart_cost_by_model"
BAR_CHART_REQ_BY_MODEL = "bar_chart_req_by_model"