diff --git a/src/chat/utils/report_generator.py b/src/chat/utils/report_generator.py
index 59467efaa..5be2fd19a 100644
--- a/src/chat/utils/report_generator.py
+++ b/src/chat/utils/report_generator.py
@@ -65,16 +65,30 @@ class HTMLReportGenerator:
f"
"
f"| {model_name} | "
f"{count} | "
- f"{stat_data[IN_TOK_BY_MODEL].get(model_name, 0)} | "
- f"{stat_data[OUT_TOK_BY_MODEL].get(model_name, 0)} | "
+ f"{stat_data[AVG_TOK_BY_MODEL].get(model_name, 0)} | "
f"{stat_data[TOTAL_TOK_BY_MODEL].get(model_name, 0)} | "
+ f"{stat_data[TPS_BY_MODEL].get(model_name, 0):.2f} | "
+ f"{stat_data[COST_PER_KTOK_BY_MODEL].get(model_name, 0):.4f} ¥ | "
f"{stat_data[COST_BY_MODEL].get(model_name, 0):.4f} ¥ | "
f"{stat_data[AVG_TIME_COST_BY_MODEL].get(model_name, 0):.3f} 秒 | "
- f"{stat_data[STD_TIME_COST_BY_MODEL].get(model_name, 0):.3f} 秒 | "
f"
"
for model_name, count in sorted(stat_data[REQ_CNT_BY_MODEL].items())
]
)
+ # 按供应商分类统计
+ provider_rows = "\n".join(
+ [
+ f""
+ f"| {provider_name} | "
+ f"{count} | "
+ f"{stat_data[TOTAL_TOK_BY_PROVIDER].get(provider_name, 0)} | "
+ f"{stat_data[TPS_BY_PROVIDER].get(provider_name, 0):.2f} | "
+ f"{stat_data[COST_PER_KTOK_BY_PROVIDER].get(provider_name, 0):.4f} ¥ | "
+ f"{stat_data[COST_BY_PROVIDER].get(provider_name, 0):.4f} ¥ | "
+ f"
"
+ 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"""
+
+
+
总花费
+
{stat_data.get(TOTAL_COST, 0):.4f} ¥
+
+
+
总请求数
+
{stat_data.get(TOTAL_REQ_CNT, 0)}
+
+
+
总Token数
+
{sum(stat_data.get(TOTAL_TOK_BY_MODEL, {}).values())}
+
+
+
总消息数
+
{stat_data.get(TOTAL_MSG_CNT, 0)}
+
+
+
总在线时间
+
{format_online_time(int(stat_data.get(ONLINE_TIME, 0)))}
+
+
+ """
+
+ # 增加饼图和条形图
+ # static_charts = self._generate_static_charts_div(stat_data, div_id) # 该功能尚未实现
+ static_charts = ""
return f"""
统计时段:
{start_time.strftime("%Y-%m-%d %H:%M:%S")} ~ {now.strftime("%Y-%m-%d %H:%M:%S")}
-
总在线时间: {format_online_time(int(stat_data.get(ONLINE_TIME, 0)))}
-
总消息数: {stat_data.get(TOTAL_MSG_CNT, 0)}
-
总请求数: {stat_data.get(TOTAL_REQ_CNT, 0)}
-
总花费: {stat_data.get(TOTAL_COST, 0):.4f} ¥
+ {summary_cards}
+ {static_charts}
按模型分类统计
- | 模型名称 | 调用次数 | 输入Token | 输出Token | Token总量 | 累计花费 | 平均耗时(秒) | 标准差(秒) |
+ | 模型名称 | 调用次数 | 平均Token数 | Token总量 | TPS | 每K Token成本 | 累计花费 | 平均耗时(秒) |
{model_rows}
+
按供应商分类统计
+
+ | 供应商名称 | 调用次数 | Token总量 | TPS | 每K Token成本 | 累计花费 |
+ {provider_rows}
+
+
按模块分类统计
@@ -156,7 +202,6 @@ class HTMLReportGenerator:
"""
-
def _generate_chart_tab(self, chart_data: dict) -> str:
"""生成图表选项卡的HTML内容。"""
return f"""
diff --git a/src/chat/utils/statistic.py b/src/chat/utils/statistic.py
index 8043450d9..08d2ed8be 100644
--- a/src/chat/utils/statistic.py
+++ b/src/chat/utils/statistic.py
@@ -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 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 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)
- for item_name in stats[period_key][category]:
- time_costs = stats[period_key][time_cost_key].get(item_name, [])
+ # 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 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 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
diff --git a/src/chat/utils/statistic_keys.py b/src/chat/utils/statistic_keys.py
index 41e96b1be..f7c91780c 100644
--- a/src/chat/utils/statistic_keys.py
+++ b/src/chat/utils/statistic_keys.py
@@ -40,4 +40,23 @@ AVG_TIME_COST_BY_MODULE = "avg_time_costs_by_module"
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"
\ No newline at end of file
+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"
\ No newline at end of file