feat(model):新增缓存监控指标
This commit is contained in:
1 parent
f482cadac9
commit
6778c84ace
13 files changed
+454
-111
No files matched your search
@@ -21,42 +21,48 @@ type BilUsageDailyDao struct {
|
||||
|
||||
// BilUsageDailyColumns defines and stores column names for the table bil_usage_daily.
|
||||
type BilUsageDailyColumns struct {
|
||||
Id string //
|
||||
StatDate string // 统计日期(按 created_at 的日期分桶)
|
||||
TenantId string // 租户ID
|
||||
ProjectId string // 项目ID(0=无项目/个人Key)
|
||||
ModelName string // 模型名称
|
||||
ChannelId string // 渠道ID(0=无渠道)
|
||||
Status string // 请求状态:success/error/timeout/cancelled
|
||||
RequestCount string // 请求数(COUNT(*))
|
||||
InputTokens string // 输入Token合计
|
||||
OutputTokens string // 输出Token合计
|
||||
TotalCost string // 客户侧成本合计(USD,源自 bil_usage_logs.total_cost)
|
||||
AccountCost string // 上游账户成本合计(USD,源自 bil_usage_logs.account_cost,用于利润分析)
|
||||
SumLatencyMs string // 总延迟合计(ms,源自 bil_usage_logs.latency_ms;视图按 SUM/COUNT 求均值)
|
||||
SumFirstTokenMs string // 首Token延迟合计(ms,源自 bil_usage_logs.first_token_ms;视图按 SUM/COUNT 求均值)
|
||||
CreatedAt string //
|
||||
UpdatedAt string //
|
||||
Id string //
|
||||
StatDate string // 统计日期(按 created_at 的日期分桶)
|
||||
TenantId string // 租户ID
|
||||
ProjectId string // 项目ID(0=无项目/个人Key)
|
||||
ModelName string // 模型名称
|
||||
ChannelId string // 渠道ID(0=无渠道)
|
||||
Status string // 请求状态:success/error/timeout/cancelled
|
||||
RequestCount string // 请求数(COUNT(*))
|
||||
InputTokens string // 输入Token合计
|
||||
OutputTokens string // 输出Token合计
|
||||
TotalCost string // 客户侧成本合计(USD,源自 bil_usage_logs.total_cost)
|
||||
AccountCost string // 上游账户成本合计(USD,源自 bil_usage_logs.account_cost,用于利润分析)
|
||||
SumLatencyMs string // 总延迟合计(ms,源自 bil_usage_logs.latency_ms;视图按 SUM/COUNT 求均值)
|
||||
SumFirstTokenMs string // 首Token延迟合计(ms,源自 bil_usage_logs.first_token_ms;视图按 SUM/COUNT 求均值)
|
||||
CreatedAt string //
|
||||
UpdatedAt string //
|
||||
CacheCreationTokens string // 缓存创建 token 数(Claude cache_creation,SUM 聚合)
|
||||
CacheReadTokens string // 缓存命中读取 token 数(Claude cache_read / OpenAI cached_tokens,SUM 聚合)
|
||||
CacheHitRequestCount string // 命中缓存的请求数(明细 cache_read_tokens>0 计 1,含失败状态行)
|
||||
}
|
||||
|
||||
// bilUsageDailyColumns holds the columns for the table bil_usage_daily.
|
||||
var bilUsageDailyColumns = BilUsageDailyColumns{
|
||||
Id: "id",
|
||||
StatDate: "stat_date",
|
||||
TenantId: "tenant_id",
|
||||
ProjectId: "project_id",
|
||||
ModelName: "model_name",
|
||||
ChannelId: "channel_id",
|
||||
Status: "status",
|
||||
RequestCount: "request_count",
|
||||
InputTokens: "input_tokens",
|
||||
OutputTokens: "output_tokens",
|
||||
TotalCost: "total_cost",
|
||||
AccountCost: "account_cost",
|
||||
SumLatencyMs: "sum_latency_ms",
|
||||
SumFirstTokenMs: "sum_first_token_ms",
|
||||
CreatedAt: "created_at",
|
||||
UpdatedAt: "updated_at",
|
||||
Id: "id",
|
||||
StatDate: "stat_date",
|
||||
TenantId: "tenant_id",
|
||||
ProjectId: "project_id",
|
||||
ModelName: "model_name",
|
||||
ChannelId: "channel_id",
|
||||
Status: "status",
|
||||
RequestCount: "request_count",
|
||||
InputTokens: "input_tokens",
|
||||
OutputTokens: "output_tokens",
|
||||
TotalCost: "total_cost",
|
||||
AccountCost: "account_cost",
|
||||
SumLatencyMs: "sum_latency_ms",
|
||||
SumFirstTokenMs: "sum_first_token_ms",
|
||||
CreatedAt: "created_at",
|
||||
UpdatedAt: "updated_at",
|
||||
CacheCreationTokens: "cache_creation_tokens",
|
||||
CacheReadTokens: "cache_read_tokens",
|
||||
CacheHitRequestCount: "cache_hit_request_count",
|
||||
}
|
||||
|
||||
// NewBilUsageDailyDao creates and returns a new DAO object for table data access.
|
||||
|
||||
@@ -27,7 +27,7 @@ type MdlTenantModelsColumns struct {
|
||||
Enabled string // 是否启用(禁用后该租户无法调用此模型)
|
||||
CustomInputPrice string // 租户自定义输入价格(NULL 表示使用默认定价)
|
||||
CustomOutputPrice string // 租户自定义输出价格(NULL 表示使用默认定价)
|
||||
Multiplier string // 租户价格倍率(VIP 折扣等,最终价格 = 基础价格 × 模型倍率 × 租户倍率)
|
||||
Multiplier string // 租户价格倍率(VIP 折扣)。作为 tenant_multiplier 参与最终价格计算:最终价格 = 基础价格 × 租户倍率。倍率来源优先级:discount_ratio > multiplier > 租户等级 price_multiplier
|
||||
CreatedAt string // 创建时间
|
||||
UpdatedAt string // 更新时间
|
||||
BillingMode string // 覆盖模型计费方式(NULL表示跟随模型默认)
|
||||
|
||||
@@ -23,6 +23,8 @@ import (
|
||||
// SUM(latency_ms) 对 NULL 跳过、但 SUM/COUNT 用全部请求数作分母的语义一致)
|
||||
// - 平均首 Token = Σttft / Σttft_n(首 Token 单独计数,避免非流式/无首Token请求计入分母拉低均值)
|
||||
// - 成本以整数 micro-USD 累加(对齐 Redis 钱包 micro 规范,杜绝 HINCRBYFLOAT 漂移)
|
||||
// - 缓存指标与日聚合对齐:cache 命中请求数(chit)按「该次请求 cache_read>0」计 1,
|
||||
// 仅写入缓存(creation>0、read=0)不计命中
|
||||
//
|
||||
// 可靠性为 best-effort:Redis 不可用时计数丢弃、查询返回空,不影响请求主链路,页面退化为历史日表。
|
||||
const (
|
||||
@@ -41,6 +43,9 @@ type ModelPerfRecord struct {
|
||||
InputTokens int
|
||||
OutputTokens int
|
||||
CostUSD float64
|
||||
// 缓存 token(命中判定由本模块从 CacheReadTokens>0 推导,调用方无需单独传标志)
|
||||
CacheCreationTokens int
|
||||
CacheReadTokens int
|
||||
}
|
||||
|
||||
// ModelPerfCounter 当日某模型的累计计数(Redis 小时桶聚合结果)。
|
||||
@@ -54,6 +59,10 @@ type ModelPerfCounter struct {
|
||||
Tin int64
|
||||
Tout int64
|
||||
CostMicro int64
|
||||
// 缓存累计:creation/read token 与 命中缓存的请求数(与 bil_usage_daily 同口径)
|
||||
CacheCreation int64
|
||||
CacheRead int64
|
||||
CacheHitReq int64
|
||||
}
|
||||
|
||||
func modelPerfModelsKey(date string) string {
|
||||
@@ -84,7 +93,8 @@ func microFromUSD(usd float64) int64 {
|
||||
// modelPerfIncrLua 一次 RTT 把样本原子累加到「当日模型集合 + 小时桶」并续期。
|
||||
// KEYS[1] = perf_models:{date};KEYS[2] = perf_model:{hex}:{hour}
|
||||
// ARGV[1] = 模型名;ARGV[2] = TTL 秒;ARGV[3] = ok(1/0);ARGV[4] = lat ms;
|
||||
// ARGV[5] = ttft ms;ARGV[6] = input tokens;ARGV[7] = output tokens;ARGV[8] = cost micro-USD。
|
||||
// ARGV[5] = ttft ms;ARGV[6] = input tokens;ARGV[7] = output tokens;ARGV[8] = cost micro-USD;
|
||||
// ARGV[9] = cache creation tokens;ARGV[10] = cache read tokens(>0 时 chit 同步 +1)。
|
||||
const modelPerfIncrLua = `
|
||||
redis.call("SADD", KEYS[1], ARGV[1])
|
||||
redis.call("EXPIRE", KEYS[1], ARGV[2])
|
||||
@@ -98,14 +108,20 @@ end
|
||||
if tonumber(ARGV[6]) > 0 then redis.call("HINCRBY", KEYS[2], "tin", tonumber(ARGV[6])) end
|
||||
if tonumber(ARGV[7]) > 0 then redis.call("HINCRBY", KEYS[2], "tout", tonumber(ARGV[7])) end
|
||||
if tonumber(ARGV[8]) > 0 then redis.call("HINCRBY", KEYS[2], "cost", tonumber(ARGV[8])) end
|
||||
if tonumber(ARGV[9]) > 0 then redis.call("HINCRBY", KEYS[2], "ccr", tonumber(ARGV[9])) end
|
||||
if tonumber(ARGV[10]) > 0 then
|
||||
redis.call("HINCRBY", KEYS[2], "crd", tonumber(ARGV[10]))
|
||||
redis.call("HINCRBY", KEYS[2], "chit", 1)
|
||||
end
|
||||
redis.call("EXPIRE", KEYS[2], ARGV[2])
|
||||
return 1
|
||||
`
|
||||
|
||||
// modelPerfReadLua 累加某模型当日所有小时桶计数。KEYS = 当日各小时桶 key。
|
||||
// 返回扁平数组 [req, ok, lat, ttft, ttft_n, tin, tout, cost]。
|
||||
// 返回扁平数组 [req, ok, lat, ttft, ttft_n, tin, tout, cost, ccr, crd, chit]。
|
||||
// 旧版本桶缺失 ccr/crd/chit 字段时 tonumber(nil) or 0 兜底为 0,平滑上线。
|
||||
const modelPerfReadLua = `
|
||||
local req,ok,lat,ttft,ttftn,tin,tout,cost = 0,0,0,0,0,0,0,0
|
||||
local req,ok,lat,ttft,ttftn,tin,tout,cost,ccr,crd,chit = 0,0,0,0,0,0,0,0,0,0,0
|
||||
for i = 1, #KEYS do
|
||||
local h = redis.call("HGETALL", KEYS[i])
|
||||
for j = 1, #h, 2 do
|
||||
@@ -118,10 +134,13 @@ for i = 1, #KEYS do
|
||||
elseif f == "ttft_n" then ttftn = ttftn + v
|
||||
elseif f == "tin" then tin = tin + v
|
||||
elseif f == "tout" then tout = tout + v
|
||||
elseif f == "cost" then cost = cost + v end
|
||||
elseif f == "cost" then cost = cost + v
|
||||
elseif f == "ccr" then ccr = ccr + v
|
||||
elseif f == "crd" then crd = crd + v
|
||||
elseif f == "chit" then chit = chit + v end
|
||||
end
|
||||
end
|
||||
return {req, ok, lat, ttft, ttftn, tin, tout, cost}
|
||||
return {req, ok, lat, ttft, ttftn, tin, tout, cost, ccr, crd, chit}
|
||||
`
|
||||
|
||||
// RecordModelPerfMetrics 将一次请求的模型性能采样累加到 Redis 当日小时桶。
|
||||
@@ -153,6 +172,7 @@ func RecordModelPerfMetrics(ctx context.Context, r ModelPerfRecord) {
|
||||
boolToInt(r.Success), int64(math.Round(r.LatencyMs)),
|
||||
int64(r.FirstTokenMs), int64(r.InputTokens), int64(r.OutputTokens),
|
||||
microFromUSD(r.CostUSD),
|
||||
int64(r.CacheCreationTokens), int64(r.CacheReadTokens),
|
||||
}
|
||||
if _, err := g.Redis().Do(bgCtx, "EVAL", args...); err != nil {
|
||||
// 指标是 best-effort,Redis 抖动只记 debug,不刷告警
|
||||
@@ -203,14 +223,17 @@ func GetModelPerfMetricsToday(ctx context.Context, date string) (map[string]Mode
|
||||
}
|
||||
vals := res.Vars()
|
||||
out[model] = ModelPerfCounter{
|
||||
Req: vals[0].Int64(),
|
||||
Ok: vals[1].Int64(),
|
||||
Lat: vals[2].Int64(),
|
||||
Ttft: vals[3].Int64(),
|
||||
TtftN: vals[4].Int64(),
|
||||
Tin: vals[5].Int64(),
|
||||
Tout: vals[6].Int64(),
|
||||
CostMicro: vals[7].Int64(),
|
||||
Req: vals[0].Int64(),
|
||||
Ok: vals[1].Int64(),
|
||||
Lat: vals[2].Int64(),
|
||||
Ttft: vals[3].Int64(),
|
||||
TtftN: vals[4].Int64(),
|
||||
Tin: vals[5].Int64(),
|
||||
Tout: vals[6].Int64(),
|
||||
CostMicro: vals[7].Int64(),
|
||||
CacheCreation: vals[8].Int64(),
|
||||
CacheRead: vals[9].Int64(),
|
||||
CacheHitReq: vals[10].Int64(),
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
|
||||
@@ -121,6 +121,10 @@ type modelPerfRow struct {
|
||||
TotalCost float64 `json:"total_cost"`
|
||||
SumLatencyMs int64 `json:"sum_latency_ms"`
|
||||
SumFirstTokenMs int64 `json:"sum_first_token_ms"`
|
||||
// 缓存聚合列(000016 迁移新增,口径与 bil_usage_logs 明细一致)
|
||||
CacheCreationTokens int64 `json:"cache_creation_tokens"`
|
||||
CacheReadTokens int64 `json:"cache_read_tokens"`
|
||||
CacheHitRequestCount int64 `json:"cache_hit_request_count"`
|
||||
}
|
||||
|
||||
// gradeSuccessRate 按成功率返回分级。阈值:≥99 excellent、≥95 good、≥90 warning、<90 critical。
|
||||
@@ -146,6 +150,10 @@ type perfModelAgg struct {
|
||||
sumLatencyMs int64
|
||||
sumFirstTokenMs int64
|
||||
totalCost float64
|
||||
// 缓存累计(历史日表 + 当日热桶同口径累加)
|
||||
cacheCreationTokens int64
|
||||
cacheReadTokens int64
|
||||
cacheHitRequests int64
|
||||
}
|
||||
|
||||
// GetModelPerformance 从 bil_usage_daily 按模型聚合历史性能指标,并合并当日 Redis 实时热桶
|
||||
@@ -164,7 +172,10 @@ func GetModelPerformance(ctx context.Context, startDate, endDate string) (any, e
|
||||
COALESCE(SUM(output_tokens), 0) AS output_tokens,
|
||||
COALESCE(SUM(total_cost), 0) AS total_cost,
|
||||
COALESCE(SUM(sum_latency_ms), 0) AS sum_latency_ms,
|
||||
COALESCE(SUM(sum_first_token_ms), 0) AS sum_first_token_ms
|
||||
COALESCE(SUM(sum_first_token_ms), 0) AS sum_first_token_ms,
|
||||
COALESCE(SUM(cache_creation_tokens), 0) AS cache_creation_tokens,
|
||||
COALESCE(SUM(cache_read_tokens), 0) AS cache_read_tokens,
|
||||
COALESCE(SUM(cache_hit_request_count), 0) AS cache_hit_request_count
|
||||
FROM bil_usage_daily
|
||||
WHERE stat_date >= $1 AND stat_date <= $2 AND stat_date < $3 AND model_name <> 'unknown'
|
||||
GROUP BY model_name
|
||||
@@ -189,7 +200,7 @@ func GetModelPerformance(ctx context.Context, startDate, endDate string) (any, e
|
||||
}
|
||||
|
||||
// buildModelPerformanceList 将历史日表聚合行与当日 Redis 热桶合并为统一的模型性能列表,
|
||||
// 统一重算派生指标(成功率/均延迟/均首Token/TPS/分级)并按 request_count 降序输出。
|
||||
// 统一重算派生指标(成功率/均延迟/均首Token/TPS/分级/缓存命中率)并按模型名称升序输出。
|
||||
func buildModelPerformanceList(rows []modelPerfRow, todayCounts map[string]common.ModelPerfCounter) []map[string]any {
|
||||
agg := make(map[string]*perfModelAgg)
|
||||
ensure := func(model string) *perfModelAgg {
|
||||
@@ -212,6 +223,9 @@ func buildModelPerformanceList(rows []modelPerfRow, todayCounts map[string]commo
|
||||
a.sumLatencyMs += r.SumLatencyMs
|
||||
a.sumFirstTokenMs += r.SumFirstTokenMs
|
||||
a.totalCost += r.TotalCost
|
||||
a.cacheCreationTokens += r.CacheCreationTokens
|
||||
a.cacheReadTokens += r.CacheReadTokens
|
||||
a.cacheHitRequests += r.CacheHitRequestCount
|
||||
}
|
||||
for model, c := range todayCounts {
|
||||
if c.Req <= 0 || model == "" || model == "unknown" {
|
||||
@@ -225,6 +239,9 @@ func buildModelPerformanceList(rows []modelPerfRow, todayCounts map[string]commo
|
||||
a.sumLatencyMs += c.Lat
|
||||
a.sumFirstTokenMs += c.Ttft
|
||||
a.totalCost += billing.FromMicro(c.CostMicro).InexactFloat64()
|
||||
a.cacheCreationTokens += c.CacheCreation
|
||||
a.cacheReadTokens += c.CacheRead
|
||||
a.cacheHitRequests += c.CacheHitReq
|
||||
}
|
||||
|
||||
list := make([]map[string]any, 0, len(agg))
|
||||
@@ -238,19 +255,34 @@ func buildModelPerformanceList(rows []modelPerfRow, todayCounts map[string]commo
|
||||
if a.sumLatencyMs > 0 {
|
||||
tps = float64(a.outputTokens) / (float64(a.sumLatencyMs) / 1000)
|
||||
}
|
||||
// 缓存命中率(Token 级)= 命中读取 / 归一化总输入(input + creation + read)。
|
||||
// Claude 口径(input 不含缓存)精确;OpenAI 原生口径(input 已含 cached)分母偏大,
|
||||
// 命中率为保守值,趋势仍可比。请求级命中率分母为 request_count,不受该口径影响。
|
||||
var cacheHitRate, cacheHitReqRate float64
|
||||
if totalInput := a.inputTokens + a.cacheCreationTokens + a.cacheReadTokens; totalInput > 0 {
|
||||
cacheHitRate = float64(a.cacheReadTokens) * 100 / float64(totalInput)
|
||||
}
|
||||
if a.requestCount > 0 {
|
||||
cacheHitReqRate = float64(a.cacheHitRequests) * 100 / float64(a.requestCount)
|
||||
}
|
||||
list = append(list, map[string]any{
|
||||
"model_name": model,
|
||||
"request_count": a.requestCount,
|
||||
"success_count": a.successCount,
|
||||
"success_rate": math.Round(successRate*100) / 100,
|
||||
"grade": gradeSuccessRate(successRate),
|
||||
"input_tokens": a.inputTokens,
|
||||
"output_tokens": a.outputTokens,
|
||||
"total_tokens": a.inputTokens + a.outputTokens,
|
||||
"total_cost": a.totalCost,
|
||||
"avg_latency_ms": math.Round(avgLatency*100) / 100,
|
||||
"avg_first_token_ms": math.Round(avgTTFT*100) / 100,
|
||||
"tps": math.Round(tps*100) / 100,
|
||||
"model_name": model,
|
||||
"request_count": a.requestCount,
|
||||
"success_count": a.successCount,
|
||||
"success_rate": math.Round(successRate*100) / 100,
|
||||
"grade": gradeSuccessRate(successRate),
|
||||
"input_tokens": a.inputTokens,
|
||||
"output_tokens": a.outputTokens,
|
||||
"total_tokens": a.inputTokens + a.outputTokens,
|
||||
"total_cost": a.totalCost,
|
||||
"avg_latency_ms": math.Round(avgLatency*100) / 100,
|
||||
"avg_first_token_ms": math.Round(avgTTFT*100) / 100,
|
||||
"tps": math.Round(tps*100) / 100,
|
||||
"cache_creation_tokens": a.cacheCreationTokens,
|
||||
"cache_read_tokens": a.cacheReadTokens,
|
||||
"cache_hit_rate": math.Round(cacheHitRate*100) / 100,
|
||||
"cache_hit_request_count": a.cacheHitRequests,
|
||||
"cache_hit_request_rate": math.Round(cacheHitReqRate*100) / 100,
|
||||
})
|
||||
}
|
||||
// 默认按模型名称升序排列,便于运营按名称定位具体模型;用户可在前端表格点任意列切换排序。
|
||||
|
||||
@@ -37,8 +37,10 @@ func assertPerfField(t *testing.T, list []map[string]any, model, field string, w
|
||||
|
||||
func TestBuildModelPerformanceList_HistoryOnly(t *testing.T) {
|
||||
rows := []modelPerfRow{
|
||||
{ModelName: "gpt-4o", RequestCount: 100, SuccessCount: 90, InputTokens: 1000, OutputTokens: 2000, TotalCost: 1.5, SumLatencyMs: 50000, SumFirstTokenMs: 8000},
|
||||
{ModelName: "claude-3-5-sonnet", RequestCount: 50, SuccessCount: 1, InputTokens: 100, OutputTokens: 50, TotalCost: 0.1, SumLatencyMs: 30000, SumFirstTokenMs: 0},
|
||||
{ModelName: "gpt-4o", RequestCount: 100, SuccessCount: 90, InputTokens: 1000, OutputTokens: 2000, TotalCost: 1.5, SumLatencyMs: 50000, SumFirstTokenMs: 8000,
|
||||
CacheReadTokens: 800, CacheHitRequestCount: 80},
|
||||
{ModelName: "claude-3-5-sonnet", RequestCount: 50, SuccessCount: 1, InputTokens: 100, OutputTokens: 50, TotalCost: 0.1, SumLatencyMs: 30000, SumFirstTokenMs: 0,
|
||||
CacheCreationTokens: 200, CacheReadTokens: 700, CacheHitRequestCount: 40},
|
||||
}
|
||||
list := buildModelPerformanceList(rows, nil)
|
||||
|
||||
@@ -54,20 +56,32 @@ func TestBuildModelPerformanceList_HistoryOnly(t *testing.T) {
|
||||
assertPerfField(t, list, "gpt-4o", "total_tokens", 0, 3000)
|
||||
assertPerfField(t, list, "gpt-4o", "total_cost", 1.5, 0)
|
||||
|
||||
// 缓存:无写入,命中率 = 800/(1000+0+800) = 44.44%;请求级 = 80/100 = 80%
|
||||
assertPerfField(t, list, "gpt-4o", "cache_read_tokens", 0, 800)
|
||||
assertPerfField(t, list, "gpt-4o", "cache_creation_tokens", 0, 0)
|
||||
assertPerfField(t, list, "gpt-4o", "cache_hit_rate", 44.44, 0)
|
||||
assertPerfField(t, list, "gpt-4o", "cache_hit_request_count", 0, 80)
|
||||
assertPerfField(t, list, "gpt-4o", "cache_hit_request_rate", 80.0, 0)
|
||||
|
||||
// claude 成功率 2%(1/50)→ critical
|
||||
assertPerfField(t, list, "claude-3-5-sonnet", "success_rate", 2.0, 0)
|
||||
assertGrade(t, list, "claude-3-5-sonnet", "critical")
|
||||
assertPerfField(t, list, "claude-3-5-sonnet", "avg_latency_ms", 600.0, 0)
|
||||
assertPerfField(t, list, "claude-3-5-sonnet", "tps", 1.67, 0) // 50/(30000/1000)=1.6666
|
||||
|
||||
// 缓存:命中率 = 700/(100+200+700) = 70%;请求级 = 40/50 = 80%
|
||||
assertPerfField(t, list, "claude-3-5-sonnet", "cache_hit_rate", 70.0, 0)
|
||||
assertPerfField(t, list, "claude-3-5-sonnet", "cache_hit_request_rate", 80.0, 0)
|
||||
}
|
||||
|
||||
func TestBuildModelPerformanceList_MergeToday(t *testing.T) {
|
||||
rows := []modelPerfRow{
|
||||
{ModelName: "gpt-4o", RequestCount: 100, SuccessCount: 90, InputTokens: 1000, OutputTokens: 2000, TotalCost: 1.5, SumLatencyMs: 50000, SumFirstTokenMs: 8000},
|
||||
{ModelName: "gpt-4o", RequestCount: 100, SuccessCount: 90, InputTokens: 1000, OutputTokens: 2000, TotalCost: 1.5, SumLatencyMs: 50000, SumFirstTokenMs: 8000,
|
||||
CacheCreationTokens: 100, CacheReadTokens: 800, CacheHitRequestCount: 70},
|
||||
}
|
||||
today := map[string]common.ModelPerfCounter{
|
||||
"gpt-4o": {Req: 10, Ok: 9, Lat: 6000, Ttft: 500, TtftN: 10, Tin: 50, Tout: 150, CostMicro: 500000}, // 500000 micro = 0.5 USD
|
||||
"new-model-day": {Req: 1, Ok: 0, Lat: 8000, Ttft: 0, TtftN: 0, Tin: 100, Tout: 0, CostMicro: 12345},
|
||||
"gpt-4o": {Req: 10, Ok: 9, Lat: 6000, Ttft: 500, TtftN: 10, Tin: 50, Tout: 150, CostMicro: 500000, CacheCreation: 20, CacheRead: 180, CacheHitReq: 8}, // 500000 micro = 0.5 USD
|
||||
"new-model-day": {Req: 1, Ok: 0, Lat: 8000, Ttft: 0, TtftN: 0, Tin: 100, Tout: 0, CostMicro: 12345, CacheRead: 60, CacheHitReq: 1},
|
||||
}
|
||||
list := buildModelPerformanceList(rows, today)
|
||||
|
||||
@@ -79,6 +93,14 @@ func TestBuildModelPerformanceList_MergeToday(t *testing.T) {
|
||||
assertPerfField(t, list, "gpt-4o", "total_tokens", 0, 3200) // (1000+50)+(2000+150)
|
||||
assertPerfField(t, list, "gpt-4o", "total_cost", 2.0, 0)
|
||||
|
||||
// 缓存合并:read 800+180=980、creation 100+20=120、命中请求 70+8=78;
|
||||
// 命中率 = 980/((1000+50)+120+980) = 980/2150 = 45.58%;请求级 = 78/110 = 70.91%
|
||||
assertPerfField(t, list, "gpt-4o", "cache_read_tokens", 0, 980)
|
||||
assertPerfField(t, list, "gpt-4o", "cache_creation_tokens", 0, 120)
|
||||
assertPerfField(t, list, "gpt-4o", "cache_hit_rate", 45.58, 0)
|
||||
assertPerfField(t, list, "gpt-4o", "cache_hit_request_count", 0, 78)
|
||||
assertPerfField(t, list, "gpt-4o", "cache_hit_request_rate", 70.91, 0)
|
||||
|
||||
// 仅当日模型
|
||||
assertPerfField(t, list, "new-model-day", "request_count", 0, 1)
|
||||
assertPerfField(t, list, "new-model-day", "total_cost", 0.012345, 0)
|
||||
@@ -86,10 +108,35 @@ func TestBuildModelPerformanceList_MergeToday(t *testing.T) {
|
||||
assertPerfField(t, list, "new-model-day", "avg_latency_ms", 8000.0, 0)
|
||||
assertPerfField(t, list, "new-model-day", "tps", 0.0, 0) // 无输出 token
|
||||
|
||||
// 仅当日缓存:命中率 = 60/(100+0+60) = 37.5%;请求级 = 1/1 = 100%
|
||||
assertPerfField(t, list, "new-model-day", "cache_hit_rate", 37.5, 0)
|
||||
assertPerfField(t, list, "new-model-day", "cache_hit_request_rate", 100.0, 0)
|
||||
|
||||
// 按模型名称升序:gpt-4o('g')在 new-model-day('n')前
|
||||
_ = assertOrder(t, list, "gpt-4o", "new-model-day")
|
||||
}
|
||||
|
||||
// 缓存边界:仅写入未命中(creation>0、read=0)不算命中请求;无缓存活动时分母为 0 不 panic、比率为 0。
|
||||
func TestBuildModelPerformanceList_CacheEdgeCases(t *testing.T) {
|
||||
rows := []modelPerfRow{
|
||||
// 首次写入缓存,尚未命中:creation 500 / read 0 / 命中请求 0
|
||||
{ModelName: "write-only", RequestCount: 10, SuccessCount: 10, InputTokens: 500, OutputTokens: 100,
|
||||
CacheCreationTokens: 500},
|
||||
// 完全无缓存活动(embedding 类模型)
|
||||
{ModelName: "text-embedding-3", RequestCount: 20, SuccessCount: 20, InputTokens: 4000, OutputTokens: 0},
|
||||
}
|
||||
list := buildModelPerformanceList(rows, nil)
|
||||
|
||||
assertPerfField(t, list, "write-only", "cache_creation_tokens", 0, 500)
|
||||
assertPerfField(t, list, "write-only", "cache_read_tokens", 0, 0)
|
||||
assertPerfField(t, list, "write-only", "cache_hit_rate", 0.0, 0) // 0/(500+500+0)
|
||||
assertPerfField(t, list, "write-only", "cache_hit_request_count", 0, 0) // 仅写入不计命中
|
||||
assertPerfField(t, list, "write-only", "cache_hit_request_rate", 0.0, 0) // 0/10
|
||||
|
||||
assertPerfField(t, list, "text-embedding-3", "cache_hit_rate", 0.0, 0)
|
||||
assertPerfField(t, list, "text-embedding-3", "cache_hit_request_rate", 0.0, 0)
|
||||
}
|
||||
|
||||
func TestBuildModelPerformanceList_FilterUnknownAndEmpty(t *testing.T) {
|
||||
rows := []modelPerfRow{
|
||||
{ModelName: "unknown", RequestCount: 999, SuccessCount: 0},
|
||||
|
||||
@@ -343,6 +343,9 @@ func (p *DataProviderImpl) RecordUsage(ctx context.Context, record *common.Usage
|
||||
InputTokens: record.PromptTokens,
|
||||
OutputTokens: record.CompletionTokens,
|
||||
CostUSD: record.TotalCost,
|
||||
// 缓存 token 透传给热桶(命中请求判定由热桶模块从 read>0 推导)
|
||||
CacheCreationTokens: record.CacheCreationTokens,
|
||||
CacheReadTokens: record.CacheReadTokens,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,9 @@ const aggregateUsageSQL = `
|
||||
INSERT INTO bil_usage_daily
|
||||
(stat_date, tenant_id, project_id, model_name, channel_id, status,
|
||||
request_count, input_tokens, output_tokens, total_cost, account_cost,
|
||||
sum_latency_ms, sum_first_token_ms, updated_at)
|
||||
sum_latency_ms, sum_first_token_ms,
|
||||
cache_creation_tokens, cache_read_tokens, cache_hit_request_count,
|
||||
updated_at)
|
||||
SELECT
|
||||
DATE(created_at) AS stat_date,
|
||||
tenant_id AS tenant_id,
|
||||
@@ -28,6 +30,9 @@ SELECT
|
||||
COALESCE(SUM(account_cost), 0) AS account_cost,
|
||||
COALESCE(SUM(latency_ms), 0) AS sum_latency_ms,
|
||||
COALESCE(SUM(first_token_ms), 0) AS sum_first_token_ms,
|
||||
COALESCE(SUM(cache_creation_tokens), 0) AS cache_creation_tokens,
|
||||
COALESCE(SUM(cache_read_tokens), 0) AS cache_read_tokens,
|
||||
COALESCE(SUM(CASE WHEN cache_read_tokens > 0 THEN 1 ELSE 0 END), 0) AS cache_hit_request_count,
|
||||
now()
|
||||
FROM bil_usage_logs
|
||||
WHERE created_at >= $1 AND created_at < $2
|
||||
@@ -39,14 +44,17 @@ GROUP BY
|
||||
COALESCE(channel_id, 0),
|
||||
COALESCE(NULLIF(status, ''), 'unknown')
|
||||
ON CONFLICT (stat_date, tenant_id, project_id, model_name, channel_id, status) DO UPDATE SET
|
||||
request_count = EXCLUDED.request_count,
|
||||
input_tokens = EXCLUDED.input_tokens,
|
||||
output_tokens = EXCLUDED.output_tokens,
|
||||
total_cost = EXCLUDED.total_cost,
|
||||
account_cost = EXCLUDED.account_cost,
|
||||
sum_latency_ms = EXCLUDED.sum_latency_ms,
|
||||
request_count = EXCLUDED.request_count,
|
||||
input_tokens = EXCLUDED.input_tokens,
|
||||
output_tokens = EXCLUDED.output_tokens,
|
||||
total_cost = EXCLUDED.total_cost,
|
||||
account_cost = EXCLUDED.account_cost,
|
||||
sum_latency_ms = EXCLUDED.sum_latency_ms,
|
||||
sum_first_token_ms = EXCLUDED.sum_first_token_ms,
|
||||
updated_at = now()
|
||||
cache_creation_tokens = EXCLUDED.cache_creation_tokens,
|
||||
cache_read_tokens = EXCLUDED.cache_read_tokens,
|
||||
cache_hit_request_count = EXCLUDED.cache_hit_request_count,
|
||||
updated_at = now()
|
||||
`
|
||||
|
||||
// AggregateUsageRange 将 bil_usage_logs 在 [startDate, endDate)(半开区间)的用量
|
||||
|
||||
@@ -11,21 +11,24 @@ import (
|
||||
|
||||
// BilUsageDaily is the golang structure of table bil_usage_daily for DAO operations like Where/Data.
|
||||
type BilUsageDaily struct {
|
||||
g.Meta `orm:"table:bil_usage_daily, do:true"`
|
||||
Id any //
|
||||
StatDate *gtime.Time // 统计日期(按 created_at 的日期分桶)
|
||||
TenantId any // 租户ID
|
||||
ProjectId any // 项目ID(0=无项目/个人Key)
|
||||
ModelName any // 模型名称
|
||||
ChannelId any // 渠道ID(0=无渠道)
|
||||
Status any // 请求状态:success/error/timeout/cancelled
|
||||
RequestCount any // 请求数(COUNT(*))
|
||||
InputTokens any // 输入Token合计
|
||||
OutputTokens any // 输出Token合计
|
||||
TotalCost any // 客户侧成本合计(USD,源自 bil_usage_logs.total_cost)
|
||||
AccountCost any // 上游账户成本合计(USD,源自 bil_usage_logs.account_cost,用于利润分析)
|
||||
SumLatencyMs any // 总延迟合计(ms,源自 bil_usage_logs.latency_ms;视图按 SUM/COUNT 求均值)
|
||||
SumFirstTokenMs any // 首Token延迟合计(ms,源自 bil_usage_logs.first_token_ms;视图按 SUM/COUNT 求均值)
|
||||
CreatedAt *gtime.Time //
|
||||
UpdatedAt *gtime.Time //
|
||||
g.Meta `orm:"table:bil_usage_daily, do:true"`
|
||||
Id any //
|
||||
StatDate *gtime.Time // 统计日期(按 created_at 的日期分桶)
|
||||
TenantId any // 租户ID
|
||||
ProjectId any // 项目ID(0=无项目/个人Key)
|
||||
ModelName any // 模型名称
|
||||
ChannelId any // 渠道ID(0=无渠道)
|
||||
Status any // 请求状态:success/error/timeout/cancelled
|
||||
RequestCount any // 请求数(COUNT(*))
|
||||
InputTokens any // 输入Token合计
|
||||
OutputTokens any // 输出Token合计
|
||||
TotalCost any // 客户侧成本合计(USD,源自 bil_usage_logs.total_cost)
|
||||
AccountCost any // 上游账户成本合计(USD,源自 bil_usage_logs.account_cost,用于利润分析)
|
||||
SumLatencyMs any // 总延迟合计(ms,源自 bil_usage_logs.latency_ms;视图按 SUM/COUNT 求均值)
|
||||
SumFirstTokenMs any // 首Token延迟合计(ms,源自 bil_usage_logs.first_token_ms;视图按 SUM/COUNT 求均值)
|
||||
CreatedAt *gtime.Time //
|
||||
UpdatedAt *gtime.Time //
|
||||
CacheCreationTokens any // 缓存创建 token 数(Claude cache_creation,SUM 聚合)
|
||||
CacheReadTokens any // 缓存命中读取 token 数(Claude cache_read / OpenAI cached_tokens,SUM 聚合)
|
||||
CacheHitRequestCount any // 命中缓存的请求数(明细 cache_read_tokens>0 计 1,含失败状态行)
|
||||
}
|
||||
@@ -19,7 +19,7 @@ type MdlTenantModels struct {
|
||||
Enabled any // 是否启用(禁用后该租户无法调用此模型)
|
||||
CustomInputPrice *decimal.Decimal // 租户自定义输入价格(NULL 表示使用默认定价)
|
||||
CustomOutputPrice *decimal.Decimal // 租户自定义输出价格(NULL 表示使用默认定价)
|
||||
Multiplier any // 租户价格倍率(VIP 折扣等,最终价格 = 基础价格 × 模型倍率 × 租户倍率)
|
||||
Multiplier any // 租户价格倍率(VIP 折扣)。作为 tenant_multiplier 参与最终价格计算:最终价格 = 基础价格 × 租户倍率。倍率来源优先级:discount_ratio > multiplier > 租户等级 price_multiplier
|
||||
CreatedAt *gtime.Time // 创建时间
|
||||
UpdatedAt *gtime.Time // 更新时间
|
||||
BillingMode any // 覆盖模型计费方式(NULL表示跟随模型默认)
|
||||
|
||||
@@ -11,20 +11,23 @@ import (
|
||||
|
||||
// BilUsageDaily is the golang structure for table bil_usage_daily.
|
||||
type BilUsageDaily struct {
|
||||
Id int64 `json:"id" orm:"id" description:""` //
|
||||
StatDate *gtime.Time `json:"stat_date" orm:"stat_date" description:"统计日期(按 created_at 的日期分桶)"` // 统计日期(按 created_at 的日期分桶)
|
||||
TenantId int64 `json:"tenant_id" orm:"tenant_id" description:"租户ID"` // 租户ID
|
||||
ProjectId int64 `json:"project_id" orm:"project_id" description:"项目ID(0=无项目/个人Key)"` // 项目ID(0=无项目/个人Key)
|
||||
ModelName string `json:"model_name" orm:"model_name" description:"模型名称"` // 模型名称
|
||||
ChannelId int64 `json:"channel_id" orm:"channel_id" description:"渠道ID(0=无渠道)"` // 渠道ID(0=无渠道)
|
||||
Status string `json:"status" orm:"status" description:"请求状态:success/error/timeout/cancelled"` // 请求状态:success/error/timeout/cancelled
|
||||
RequestCount int `json:"request_count" orm:"request_count" description:"请求数(COUNT(*))"` // 请求数(COUNT(*))
|
||||
InputTokens int64 `json:"input_tokens" orm:"input_tokens" description:"输入Token合计"` // 输入Token合计
|
||||
OutputTokens int64 `json:"output_tokens" orm:"output_tokens" description:"输出Token合计"` // 输出Token合计
|
||||
TotalCost decimal.Decimal `json:"total_cost" orm:"total_cost" description:"客户侧成本合计(USD,源自 bil_usage_logs.total_cost)"` // 客户侧成本合计(USD,源自 bil_usage_logs.total_cost)
|
||||
AccountCost decimal.Decimal `json:"account_cost" orm:"account_cost" description:"上游账户成本合计(USD,源自 bil_usage_logs.account_cost,用于利润分析)"` // 上游账户成本合计(USD,源自 bil_usage_logs.account_cost,用于利润分析)
|
||||
SumLatencyMs int64 `json:"sum_latency_ms" orm:"sum_latency_ms" description:"总延迟合计(ms,源自 bil_usage_logs.latency_ms;视图按 SUM/COUNT 求均值)"` // 总延迟合计(ms,源自 bil_usage_logs.latency_ms;视图按 SUM/COUNT 求均值)
|
||||
SumFirstTokenMs int64 `json:"sum_first_token_ms" orm:"sum_first_token_ms" description:"首Token延迟合计(ms,源自 bil_usage_logs.first_token_ms;视图按 SUM/COUNT 求均值)"` // 首Token延迟合计(ms,源自 bil_usage_logs.first_token_ms;视图按 SUM/COUNT 求均值)
|
||||
CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:""` //
|
||||
UpdatedAt *gtime.Time `json:"updated_at" orm:"updated_at" description:""` //
|
||||
Id int64 `json:"id" orm:"id" description:""` //
|
||||
StatDate *gtime.Time `json:"stat_date" orm:"stat_date" description:"统计日期(按 created_at 的日期分桶)"` // 统计日期(按 created_at 的日期分桶)
|
||||
TenantId int64 `json:"tenant_id" orm:"tenant_id" description:"租户ID"` // 租户ID
|
||||
ProjectId int64 `json:"project_id" orm:"project_id" description:"项目ID(0=无项目/个人Key)"` // 项目ID(0=无项目/个人Key)
|
||||
ModelName string `json:"model_name" orm:"model_name" description:"模型名称"` // 模型名称
|
||||
ChannelId int64 `json:"channel_id" orm:"channel_id" description:"渠道ID(0=无渠道)"` // 渠道ID(0=无渠道)
|
||||
Status string `json:"status" orm:"status" description:"请求状态:success/error/timeout/cancelled"` // 请求状态:success/error/timeout/cancelled
|
||||
RequestCount int `json:"request_count" orm:"request_count" description:"请求数(COUNT(*))"` // 请求数(COUNT(*))
|
||||
InputTokens int64 `json:"input_tokens" orm:"input_tokens" description:"输入Token合计"` // 输入Token合计
|
||||
OutputTokens int64 `json:"output_tokens" orm:"output_tokens" description:"输出Token合计"` // 输出Token合计
|
||||
TotalCost decimal.Decimal `json:"total_cost" orm:"total_cost" description:"客户侧成本合计(USD,源自 bil_usage_logs.total_cost)"` // 客户侧成本合计(USD,源自 bil_usage_logs.total_cost)
|
||||
AccountCost decimal.Decimal `json:"account_cost" orm:"account_cost" description:"上游账户成本合计(USD,源自 bil_usage_logs.account_cost,用于利润分析)"` // 上游账户成本合计(USD,源自 bil_usage_logs.account_cost,用于利润分析)
|
||||
SumLatencyMs int64 `json:"sum_latency_ms" orm:"sum_latency_ms" description:"总延迟合计(ms,源自 bil_usage_logs.latency_ms;视图按 SUM/COUNT 求均值)"` // 总延迟合计(ms,源自 bil_usage_logs.latency_ms;视图按 SUM/COUNT 求均值)
|
||||
SumFirstTokenMs int64 `json:"sum_first_token_ms" orm:"sum_first_token_ms" description:"首Token延迟合计(ms,源自 bil_usage_logs.first_token_ms;视图按 SUM/COUNT 求均值)"` // 首Token延迟合计(ms,源自 bil_usage_logs.first_token_ms;视图按 SUM/COUNT 求均值)
|
||||
CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:""` //
|
||||
UpdatedAt *gtime.Time `json:"updated_at" orm:"updated_at" description:""` //
|
||||
CacheCreationTokens int64 `json:"cache_creation_tokens" orm:"cache_creation_tokens" description:"缓存创建 token 数(Claude cache_creation,SUM 聚合)"` // 缓存创建 token 数(Claude cache_creation,SUM 聚合)
|
||||
CacheReadTokens int64 `json:"cache_read_tokens" orm:"cache_read_tokens" description:"缓存命中读取 token 数(Claude cache_read / OpenAI cached_tokens,SUM 聚合)"` // 缓存命中读取 token 数(Claude cache_read / OpenAI cached_tokens,SUM 聚合)
|
||||
CacheHitRequestCount int64 `json:"cache_hit_request_count" orm:"cache_hit_request_count" description:"命中缓存的请求数(明细 cache_read_tokens>0 计 1,含失败状态行)"` // 命中缓存的请求数(明细 cache_read_tokens>0 计 1,含失败状态行)
|
||||
}
|
||||
@@ -17,7 +17,7 @@ type MdlTenantModels struct {
|
||||
Enabled bool `json:"enabled" orm:"enabled" description:"是否启用(禁用后该租户无法调用此模型)"` // 是否启用(禁用后该租户无法调用此模型)
|
||||
CustomInputPrice *decimal.Decimal `json:"custom_input_price" orm:"custom_input_price" description:"租户自定义输入价格(NULL 表示使用默认定价)"` // 租户自定义输入价格(NULL 表示使用默认定价)
|
||||
CustomOutputPrice *decimal.Decimal `json:"custom_output_price" orm:"custom_output_price" description:"租户自定义输出价格(NULL 表示使用默认定价)"` // 租户自定义输出价格(NULL 表示使用默认定价)
|
||||
Multiplier decimal.Decimal `json:"multiplier" orm:"multiplier" description:"租户价格倍率(VIP 折扣等,最终价格 = 基础价格 × 模型倍率 × 租户倍率)"` // 租户价格倍率(VIP 折扣等,最终价格 = 基础价格 × 模型倍率 × 租户倍率)
|
||||
Multiplier decimal.Decimal `json:"multiplier" orm:"multiplier" description:"租户价格倍率(VIP 折扣)。作为 tenant_multiplier 参与最终价格计算:最终价格 = 基础价格 × 租户倍率。倍率来源优先级:discount_ratio > multiplier > 租户等级 price_multiplier"` // 租户价格倍率(VIP 折扣)。作为 tenant_multiplier 参与最终价格计算:最终价格 = 基础价格 × 租户倍率。倍率来源优先级:discount_ratio > multiplier > 租户等级 price_multiplier
|
||||
CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:"创建时间"` // 创建时间
|
||||
UpdatedAt *gtime.Time `json:"updated_at" orm:"updated_at" description:"更新时间"` // 更新时间
|
||||
BillingMode string `json:"billing_mode" orm:"billing_mode" description:"覆盖模型计费方式(NULL表示跟随模型默认)"` // 覆盖模型计费方式(NULL表示跟随模型默认)
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
-- +goose Up
|
||||
-- 模型性能监控:bil_usage_daily 增加缓存聚合列(数据源自 bil_usage_logs 已有的缓存明细字段)。
|
||||
-- cache_creation_tokens 缓存创建 token 数(Claude cache_creation_input_tokens)
|
||||
-- cache_read_tokens 缓存命中读取 token 数(Claude cache_read / OpenAI cached_tokens)
|
||||
-- cache_hit_request_count 命中缓存的请求数(明细行 cache_read_tokens > 0 计 1,与 request_count 同分母)
|
||||
-- 上线后需用 aggregate-usage 命令回填历史区间(ON CONFLICT 幂等)。
|
||||
|
||||
ALTER TABLE bil_usage_daily ADD COLUMN cache_creation_tokens BIGINT DEFAULT 0 NOT NULL;
|
||||
COMMENT ON COLUMN bil_usage_daily.cache_creation_tokens IS '缓存创建 token 数(Claude cache_creation,SUM 聚合)';
|
||||
|
||||
ALTER TABLE bil_usage_daily ADD COLUMN cache_read_tokens BIGINT DEFAULT 0 NOT NULL;
|
||||
COMMENT ON COLUMN bil_usage_daily.cache_read_tokens IS '缓存命中读取 token 数(Claude cache_read / OpenAI cached_tokens,SUM 聚合)';
|
||||
|
||||
ALTER TABLE bil_usage_daily ADD COLUMN cache_hit_request_count BIGINT DEFAULT 0 NOT NULL;
|
||||
COMMENT ON COLUMN bil_usage_daily.cache_hit_request_count IS '命中缓存的请求数(明细 cache_read_tokens>0 计 1,含失败状态行)';
|
||||
|
||||
-- +goose Down
|
||||
ALTER TABLE bil_usage_daily DROP COLUMN IF EXISTS cache_creation_tokens;
|
||||
ALTER TABLE bil_usage_daily DROP COLUMN IF EXISTS cache_read_tokens;
|
||||
ALTER TABLE bil_usage_daily DROP COLUMN IF EXISTS cache_hit_request_count;
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, h } from 'vue'
|
||||
import { Tag } from '@arco-design/web-vue'
|
||||
import { IconInfoCircle } from '@arco-design/web-vue/es/icon'
|
||||
import type { TableColumnData } from '@arco-design/web-vue'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
import TableStats from '@/components/TableStats.vue'
|
||||
@@ -11,6 +12,8 @@ const loading = ref(false)
|
||||
const data = ref<any[]>([])
|
||||
// 默认只查当天(配合后端 Redis 当日实时热桶,打开即可看到最新的模型性能)
|
||||
const dateRange = ref<[string, string] | null>(defaultRange(0))
|
||||
// 指标说明弹窗
|
||||
const showMetricsGuide = ref(false)
|
||||
|
||||
function toDateStr(d: Date): string {
|
||||
const m = `${d.getMonth() + 1}`.padStart(2, '0')
|
||||
@@ -109,6 +112,29 @@ const columns: TableColumnData[] = [
|
||||
sortable: { sortDirections: ['ascend', 'descend'] },
|
||||
render: ({ record }: any) => `${fmtNum(record.tps).toFixed(1)} t/s`,
|
||||
},
|
||||
{
|
||||
title: '缓存命中',
|
||||
dataIndex: 'cache_read_tokens',
|
||||
width: 170,
|
||||
align: 'right',
|
||||
sortable: { sortDirections: ['ascend', 'descend'] },
|
||||
render: ({ record }: any) => {
|
||||
// 无缓存活动(读/写均为 0)显示 —,embedding/图像类模型不参与缓存统计
|
||||
if (fmtNum(record.cache_read_tokens) === 0 && fmtNum(record.cache_creation_tokens) === 0) return '—'
|
||||
return `${fmtNum(record.cache_read_tokens).toLocaleString()} (${fmtNum(record.cache_hit_rate).toFixed(1)}%)`
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '请求缓存命中率',
|
||||
dataIndex: 'cache_hit_request_rate',
|
||||
width: 140,
|
||||
align: 'right',
|
||||
sortable: { sortDirections: ['ascend', 'descend'] },
|
||||
render: ({ record }: any) => {
|
||||
if (fmtNum(record.cache_read_tokens) === 0 && fmtNum(record.cache_creation_tokens) === 0) return '—'
|
||||
return `${fmtNum(record.cache_hit_request_rate).toFixed(1)}%`
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '总 Token',
|
||||
dataIndex: 'total_tokens',
|
||||
@@ -148,16 +174,22 @@ const stats = computed(() => {
|
||||
let totalRequests = 0
|
||||
let totalSuccess = 0
|
||||
let totalCost = 0
|
||||
let totalCacheHitReq = 0
|
||||
let totalCacheTokens = 0
|
||||
for (const item of data.value) {
|
||||
totalRequests += fmtNum(item.request_count)
|
||||
totalSuccess += fmtNum(item.success_count)
|
||||
totalCost += fmtNum(item.total_cost)
|
||||
totalCacheHitReq += fmtNum(item.cache_hit_request_count)
|
||||
totalCacheTokens += fmtNum(item.cache_read_tokens) + fmtNum(item.cache_creation_tokens)
|
||||
}
|
||||
return {
|
||||
modelCount: data.value.length,
|
||||
totalRequests,
|
||||
overallRate: totalRequests > 0 ? (totalSuccess / totalRequests) * 100 : 0,
|
||||
totalCost,
|
||||
// 全局请求缓存命中率(无任何缓存活动时为 null,页面显示 —)
|
||||
cacheHitReqRate: totalCacheTokens > 0 && totalRequests > 0 ? (totalCacheHitReq / totalRequests) * 100 : null,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -185,7 +217,7 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<div class="page-table">
|
||||
<PageHeader title="模型性能" description="各模型跨渠道的成功率 / 延迟 / 吞吐(历史数据源自 bil_usage_daily 每日聚合,当天实时统计;延迟为总延迟/总请求数均值)">
|
||||
<PageHeader title="模型性能" description="各模型跨渠道的成功率 / 延迟 / 吞吐 / 缓存命中(历史数据源自 bil_usage_daily 每日聚合,当天实时统计;延迟为总延迟/总请求数均值。Token 命中率对 OpenAI 原生渠道为保守值)">
|
||||
<template #actions>
|
||||
<a-button
|
||||
size="small"
|
||||
@@ -201,6 +233,106 @@ onMounted(() => {
|
||||
</template>
|
||||
</PageHeader>
|
||||
|
||||
<!-- 指标说明入口(点击弹窗) -->
|
||||
<div class="metrics-hint" @click="showMetricsGuide = true">
|
||||
<IconInfoCircle /> 了解各指标的含义与计算公式 →
|
||||
</div>
|
||||
|
||||
<!-- 指标说明弹窗 -->
|
||||
<a-modal v-model:visible="showMetricsGuide" title="模型性能指标说明" :width="720" :footer="false">
|
||||
<div class="metrics-guide">
|
||||
<p class="guide-intro">
|
||||
以下指标按所选时间区间逐模型聚合:历史数据(昨天及以前)源自 <code>bil_usage_daily</code> 每日聚合,
|
||||
当天数据来自 Redis 实时热桶(小时粒度),两段口径一致、合并计算。顶部概览卡为全部模型汇总。
|
||||
</p>
|
||||
|
||||
<div class="guide-group">基础指标</div>
|
||||
<div class="metric-item">
|
||||
<div class="metric-name">请求数</div>
|
||||
<div class="metric-body">
|
||||
<p>区间内该模型的总请求数(含失败请求,重试成功按最终一次成功计)。</p>
|
||||
<p><code>请求数 = Σ request_count</code></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<div class="metric-name">成功率</div>
|
||||
<div class="metric-body">
|
||||
<p>成功请求占总请求的比例。标签按阈值分级:≥99 优秀(绿)/ ≥95 良好(蓝)/ ≥90 警告(橙)/ 其余严重(红)。</p>
|
||||
<p><code>成功率 = Σ 成功请求数 ÷ Σ 总请求数 × 100%</code></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<div class="metric-name">总 Token</div>
|
||||
<div class="metric-body">
|
||||
<p>输入与输出 token 之和(不含缓存写入部分)。</p>
|
||||
<p><code>总 Token = Σ input_tokens + Σ output_tokens</code></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<div class="metric-name">总成本</div>
|
||||
<div class="metric-body">
|
||||
<p>区间内该模型实际计费成本合计(USD),已含输入、输出与缓存各项费用。</p>
|
||||
<p><code>总成本 = Σ total_cost</code></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="guide-group">延迟与吞吐</div>
|
||||
<div class="metric-item">
|
||||
<div class="metric-name">平均延迟</div>
|
||||
<div class="metric-body">
|
||||
<p>请求从发起到响应完成的全程耗时均值(毫秒),流式请求计至最后一个分片。</p>
|
||||
<p><code>平均延迟 = Σ 请求延迟 ÷ Σ 总请求数</code></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<div class="metric-name">平均首Token</div>
|
||||
<div class="metric-body">
|
||||
<p>流式请求从发出到收到首个 token 的耗时均值(毫秒)。非流式 / 无首 token 的请求按 0 计入分母,会拉低该均值。</p>
|
||||
<p><code>平均首Token = Σ 首 token 延迟 ÷ Σ 总请求数</code></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<div class="metric-name">吞吐 TPS</div>
|
||||
<div class="metric-body">
|
||||
<p>平均每秒输出的 token 数,衡量模型生成速度。</p>
|
||||
<p><code>TPS = Σ 输出 token ÷ (Σ 请求延迟 ÷ 1000)</code></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="guide-group">缓存指标</div>
|
||||
<div class="metric-item">
|
||||
<div class="metric-name">缓存命中</div>
|
||||
<div class="metric-body">
|
||||
<p>
|
||||
从上游提示词缓存读取的 token 数,括号内为 Token 级命中率。某次请求的缓存读取 token > 0 即视为命中;
|
||||
无任何缓存读写活动的模型(如 embedding / 图像类)显示 —。
|
||||
</p>
|
||||
<p><code>缓存命中 Token = Σ cache_read_tokens</code></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<div class="metric-name">缓存命中率<br />(Token 级)</div>
|
||||
<div class="metric-body">
|
||||
<p>
|
||||
缓存命中 token 占总输入 token 的比例,分母为归一化总输入(未命中输入 + 缓存写入 + 缓存读取)。
|
||||
注意:OpenAI 原生渠道的 input_tokens 已包含缓存部分,该口径下命中率为保守值(偏低),Claude 渠道精确。
|
||||
</p>
|
||||
<p><code>缓存命中率 = Σ cache_read_tokens ÷ (Σ input_tokens + Σ cache_creation_tokens + Σ cache_read_tokens) × 100%</code></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<div class="metric-name">请求缓存命中率</div>
|
||||
<div class="metric-body">
|
||||
<p>
|
||||
命中缓存的请求数占总请求数的比例。仅写入缓存(首次建立缓存、未读取)不计命中。
|
||||
与 Token 级命中率互补:本指标高说明缓存覆盖面广,Token 级高说明单次请求复用的前缀长。
|
||||
</p>
|
||||
<p><code>请求缓存命中率 = Σ 命中缓存的请求数 ÷ Σ 总请求数 × 100%</code></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
|
||||
<div class="stats-row">
|
||||
<div class="metric-card metric-card--blue">
|
||||
<div class="metric-card__label">模型数</div>
|
||||
@@ -218,6 +350,10 @@ onMounted(() => {
|
||||
<div class="metric-card__label">总成本 (USD)</div>
|
||||
<div class="metric-card__value">${{ stats.totalCost.toFixed(6) }}</div>
|
||||
</div>
|
||||
<div class="metric-card metric-card--green">
|
||||
<div class="metric-card__label">请求缓存命中率</div>
|
||||
<div class="metric-card__value">{{ stats.cacheHitReqRate === null ? '—' : `${stats.cacheHitReqRate.toFixed(2)}%` }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-card :bordered="false">
|
||||
@@ -226,7 +362,7 @@ onMounted(() => {
|
||||
:data="data"
|
||||
:loading="loading"
|
||||
row-key="model_name"
|
||||
:scroll="{ x: 1200 }"
|
||||
:scroll="{ x: 1510 }"
|
||||
size="large"
|
||||
stripe
|
||||
:border="{ wrapper: true, headerCell: true }"
|
||||
@@ -247,10 +383,72 @@ onMounted(() => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 指标说明入口(复用渠道页 scheduling-hint 风格) */
|
||||
.metrics-hint {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 13px;
|
||||
color: var(--color-text-3);
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
user-select: none;
|
||||
}
|
||||
.metrics-hint:hover {
|
||||
color: rgb(var(--arcoblue-6));
|
||||
}
|
||||
|
||||
/* 指标说明弹窗 */
|
||||
.metrics-guide {
|
||||
color: var(--color-text-1);
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
.guide-intro {
|
||||
margin: 0 0 14px;
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
.guide-group {
|
||||
margin: 18px 0 10px;
|
||||
padding-bottom: 6px;
|
||||
font-weight: 600;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
.metric-item {
|
||||
display: grid;
|
||||
grid-template-columns: 110px 1fr;
|
||||
gap: 4px 14px;
|
||||
padding: 6px 0;
|
||||
}
|
||||
.metric-name {
|
||||
font-weight: 600;
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
.metric-body p {
|
||||
margin: 0;
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
.metric-body p + p {
|
||||
margin-top: 2px;
|
||||
}
|
||||
.metrics-guide code {
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
color: rgb(var(--arcoblue-6));
|
||||
background: var(--color-primary-light-1);
|
||||
word-break: break-all;
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.metric-item {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* 顶部概览卡片:与实时监控页 metric-card 风格保持一致(左色条 + 渐变底) */
|
||||
.stats-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user