diff --git a/model/log.go b/model/log.go index 3307fde17..bcf471edc 100644 --- a/model/log.go +++ b/model/log.go @@ -386,17 +386,26 @@ func RecordConsumeLog(c *gin.Context, userId int, params RecordConsumeLogParams) logger.LogError(c, common.LogText("failed to record log: %s", err.Error())) } if common.DataExportEnabled { + var cacheReadTokens, cacheCreationTokens int + if other != nil { + cacheReadTokens, _ = other.public["cache_tokens"].(int) + // The settlement path already normalizes the cache creation total. + // Do not add the 5m/1h breakdown again or infer tokens from prices. + cacheCreationTokens, _ = other.public["cache_write_tokens"].(int) + } LogQuotaData(QuotaDataLogParams{ - UserID: userId, - Username: username, - ModelName: params.ModelName, - Quota: params.Quota, - CreatedAt: createdAt, - TokenUsed: params.PromptTokens + params.CompletionTokens, - UseGroup: params.Group, - TokenID: params.TokenId, - ChannelID: params.ChannelId, - NodeName: common.NodeName, + UserID: userId, + Username: username, + ModelName: params.ModelName, + Quota: params.Quota, + CreatedAt: createdAt, + TokenUsed: params.PromptTokens + params.CompletionTokens, + CacheReadTokens: int64(cacheReadTokens), + CacheCreationTokens: int64(cacheCreationTokens), + UseGroup: params.Group, + TokenID: params.TokenId, + ChannelID: params.ChannelId, + NodeName: common.NodeName, }) } } diff --git a/model/usedata.go b/model/usedata.go index f8e04de43..20618945f 100644 --- a/model/usedata.go +++ b/model/usedata.go @@ -11,31 +11,35 @@ import ( // QuotaData 柱状图数据 type QuotaData struct { - Id int `json:"id"` - UserID int `json:"user_id" gorm:"index"` - Username string `json:"username" gorm:"index:idx_qdt_model_user_name,priority:2;size:64;default:''"` - ModelName string `json:"model_name" gorm:"index:idx_qdt_model_user_name,priority:1;size:64;default:''"` - CreatedAt int64 `json:"created_at" gorm:"bigint;index:idx_qdt_created_at,priority:2"` - UseGroup string `json:"use_group" gorm:"index;size:64;default:''"` - TokenID int `json:"token_id" gorm:"index;default:0"` - ChannelID int `json:"channel_id" gorm:"index;default:0"` - NodeName string `json:"node_name" gorm:"index;size:64;default:''"` - TokenUsed int `json:"token_used" gorm:"default:0"` - Count int `json:"count" gorm:"default:0"` - Quota int `json:"quota" gorm:"default:0"` + Id int `json:"id"` + UserID int `json:"user_id" gorm:"index"` + Username string `json:"username" gorm:"index:idx_qdt_model_user_name,priority:2;size:64;default:''"` + ModelName string `json:"model_name" gorm:"index:idx_qdt_model_user_name,priority:1;size:64;default:''"` + CreatedAt int64 `json:"created_at" gorm:"bigint;index:idx_qdt_created_at,priority:2"` + UseGroup string `json:"use_group" gorm:"index;size:64;default:''"` + TokenID int `json:"token_id" gorm:"index;default:0"` + ChannelID int `json:"channel_id" gorm:"index;default:0"` + NodeName string `json:"node_name" gorm:"index;size:64;default:''"` + TokenUsed int `json:"token_used" gorm:"default:0"` + Count int `json:"count" gorm:"default:0"` + Quota int `json:"quota" gorm:"default:0"` + CacheReadTokens int64 `json:"cache_read_tokens" gorm:"default:0"` + CacheCreationTokens int64 `json:"cache_creation_tokens" gorm:"default:0"` } type QuotaDataLogParams struct { - UserID int - Username string - ModelName string - Quota int - CreatedAt int64 - TokenUsed int - UseGroup string - TokenID int - ChannelID int - NodeName string + UserID int + Username string + ModelName string + Quota int + CreatedAt int64 + TokenUsed int + CacheReadTokens int64 + CacheCreationTokens int64 + UseGroup string + TokenID int + ChannelID int + NodeName string } func UpdateQuotaData() { @@ -70,6 +74,8 @@ func logQuotaDataCache(quotaData *QuotaData) { cachedQuotaData.Count += count cachedQuotaData.Quota += quota cachedQuotaData.TokenUsed += tokenUsed + cachedQuotaData.CacheReadTokens += quotaData.CacheReadTokens + cachedQuotaData.CacheCreationTokens += quotaData.CacheCreationTokens quotaData = cachedQuotaData } CacheQuotaData[key] = quotaData @@ -79,17 +85,19 @@ func LogQuotaData(params QuotaDataLogParams) { // 只精确到小时 createdAt := params.CreatedAt - (params.CreatedAt % 3600) quotaData := &QuotaData{ - UserID: params.UserID, - Username: params.Username, - ModelName: params.ModelName, - CreatedAt: createdAt, - UseGroup: params.UseGroup, - TokenID: params.TokenID, - ChannelID: params.ChannelID, - NodeName: params.NodeName, - Count: 1, - Quota: params.Quota, - TokenUsed: params.TokenUsed, + UserID: params.UserID, + Username: params.Username, + ModelName: params.ModelName, + CreatedAt: createdAt, + UseGroup: params.UseGroup, + TokenID: params.TokenID, + ChannelID: params.ChannelID, + NodeName: params.NodeName, + Count: 1, + Quota: params.Quota, + TokenUsed: params.TokenUsed, + CacheReadTokens: max(0, params.CacheReadTokens), + CacheCreationTokens: max(0, params.CacheCreationTokens), } CacheQuotaDataLock.Lock() @@ -129,9 +137,11 @@ func increaseQuotaData(quotaData *QuotaData) { Where("user_id = ? and username = ? and model_name = ? and created_at = ? and use_group = ? and token_id = ? and channel_id = ? and node_name = ?", quotaData.UserID, quotaData.Username, quotaData.ModelName, quotaData.CreatedAt, quotaData.UseGroup, quotaData.TokenID, quotaData.ChannelID, quotaData.NodeName). Updates(map[string]any{ - "count": gorm.Expr("count + ?", quotaData.Count), - "quota": gorm.Expr("quota + ?", quotaData.Quota), - "token_used": gorm.Expr("token_used + ?", quotaData.TokenUsed), + "count": gorm.Expr("count + ?", quotaData.Count), + "quota": gorm.Expr("quota + ?", quotaData.Quota), + "token_used": gorm.Expr("token_used + ?", quotaData.TokenUsed), + "cache_read_tokens": gorm.Expr("cache_read_tokens + ?", quotaData.CacheReadTokens), + "cache_creation_tokens": gorm.Expr("cache_creation_tokens + ?", quotaData.CacheCreationTokens), }).Error if err != nil { common.SysLog(common.LogText("increaseQuotaData error: %s", err)) @@ -142,7 +152,7 @@ func GetQuotaDataByUsername(username string, startTime int64, endTime int64) (qu var quotaDatas []*QuotaData // 从quota_data表中查询数据 err = DB.Table("quota_data"). - Select("user_id, username, model_name, created_at, sum(count) as count, sum(quota) as quota, sum(token_used) as token_used"). + Select("user_id, username, model_name, created_at, sum(count) as count, sum(quota) as quota, sum(token_used) as token_used, sum(cache_read_tokens) as cache_read_tokens, sum(cache_creation_tokens) as cache_creation_tokens"). Where("username = ? and created_at >= ? and created_at <= ?", username, startTime, endTime). Group("user_id, username, model_name, created_at"). Find("aDatas).Error @@ -153,7 +163,7 @@ func GetQuotaDataByUserId(userId int, startTime int64, endTime int64) (quotaData var quotaDatas []*QuotaData // 从quota_data表中查询数据 err = DB.Table("quota_data"). - Select("user_id, username, model_name, created_at, sum(count) as count, sum(quota) as quota, sum(token_used) as token_used"). + Select("user_id, username, model_name, created_at, sum(count) as count, sum(quota) as quota, sum(token_used) as token_used, sum(cache_read_tokens) as cache_read_tokens, sum(cache_creation_tokens) as cache_creation_tokens"). Where("user_id = ? and created_at >= ? and created_at <= ?", userId, startTime, endTime). Group("user_id, username, model_name, created_at"). Find("aDatas).Error @@ -163,7 +173,7 @@ func GetQuotaDataByUserId(userId int, startTime int64, endTime int64) (quotaData func GetQuotaDataGroupByUser(startTime int64, endTime int64) (quotaData []*QuotaData, err error) { var quotaDatas []*QuotaData err = DB.Table("quota_data"). - Select("username, created_at, sum(count) as count, sum(quota) as quota, sum(token_used) as token_used"). + Select("username, created_at, sum(count) as count, sum(quota) as quota, sum(token_used) as token_used, sum(cache_read_tokens) as cache_read_tokens, sum(cache_creation_tokens) as cache_creation_tokens"). Where("created_at >= ? and created_at <= ?", startTime, endTime). Group("username, created_at"). Find("aDatas).Error @@ -178,6 +188,6 @@ func GetAllQuotaDates(startTime int64, endTime int64, username string) (quotaDat // 从quota_data表中查询数据 // only select model_name, sum(count) as count, sum(quota) as quota, model_name, created_at from quota_data group by model_name, created_at; //err = DB.Table("quota_data").Where("created_at >= ? and created_at <= ?", startTime, endTime).Find("aDatas).Error - err = DB.Table("quota_data").Select("model_name, sum(count) as count, sum(quota) as quota, sum(token_used) as token_used, created_at").Where("created_at >= ? and created_at <= ?", startTime, endTime).Group("model_name, created_at").Find("aDatas).Error + err = DB.Table("quota_data").Select("model_name, sum(count) as count, sum(quota) as quota, sum(token_used) as token_used, sum(cache_read_tokens) as cache_read_tokens, sum(cache_creation_tokens) as cache_creation_tokens, created_at").Where("created_at >= ? and created_at <= ?", startTime, endTime).Group("model_name, created_at").Find("aDatas).Error return quotaDatas, err } diff --git a/web/src/features/dashboard/components/models/log-stat-cards.tsx b/web/src/features/dashboard/components/models/log-stat-cards.tsx index 5dbac2f75..eb76b9f82 100644 --- a/web/src/features/dashboard/components/models/log-stat-cards.tsx +++ b/web/src/features/dashboard/components/models/log-stat-cards.tsx @@ -43,7 +43,7 @@ interface LogStatCardsProps { onDataUpdate?: (data: QuotaDataItem[], loading: boolean) => void } -const MAX_INLINE_STAT_CHARS = 9 +const MAX_INLINE_STAT_CHARS = 7 function formatStatNumber(value: number, locale: Intl.LocalesArgument) { const fullValue = formatNumber(value, locale) @@ -63,11 +63,9 @@ export function LogStatCards(props: LogStatCardsProps) { const statCardsConfig = useModelStatCardsConfig() const user = useAuthStore((state) => state.auth.user) const isAdmin = !!(user?.role && user.role >= 10) - const [stats, setStats] = useState<{ - totalQuota: number - totalCount: number - totalTokens: number - } | null>(null) + const [stats, setStats] = useState | null>(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(false) @@ -119,6 +117,8 @@ export function LogStatCards(props: LogStatCardsProps) { rpm: stats?.totalCount ?? 0, quota: stats?.totalQuota ?? 0, tpm: stats?.totalTokens ?? 0, + cacheRead: stats?.totalCacheReadTokens ?? 0, + cacheCreation: stats?.totalCacheCreationTokens ?? 0, } const items = statCardsConfig.map((config) => { @@ -133,6 +133,7 @@ export function LogStatCards(props: LogStatCardsProps) { : formatStatNumber(rawValue, locale) return { + key: config.key, title: config.title, value: formatted.displayValue, fullValue: formatted.fullValue, @@ -144,7 +145,7 @@ export function LogStatCards(props: LogStatCardsProps) { return (
-
+
{items.map((it, idx) => { const Icon = it.icon let valueContent @@ -184,9 +185,12 @@ export function LogStatCards(props: LogStatCardsProps) { return (
-
+
{it.title}
@@ -210,6 +217,11 @@ export function LogStatCards(props: LogStatCardsProps) { ) })}
+ {/*

*/} + {/* {t(*/} + {/* 'Cache usage is recorded only after the upgrade; historical data is not backfilled.'*/} + {/* )}*/} + {/*

*/}
) } diff --git a/web/src/features/dashboard/hooks/use-dashboard-config.tsx b/web/src/features/dashboard/hooks/use-dashboard-config.tsx index 71bf070f1..94f409f95 100644 --- a/web/src/features/dashboard/hooks/use-dashboard-config.tsx +++ b/web/src/features/dashboard/hooks/use-dashboard-config.tsx @@ -20,6 +20,8 @@ import { Hash, Coins, Layers, + Download, + Upload, Gauge, Zap, Flame, @@ -69,6 +71,22 @@ export function useModelStatCardsConfig(): StatCardConfig[] { iconTone: 'chart-4', getValue: (stat) => stat?.tpm ?? 0, }, + { + key: 'cacheRead', + title: t('Cache Read'), + description: t('Statistical tokens'), + icon: Download, + iconTone: 'chart-3', + getValue: (stat) => stat?.cacheRead ?? 0, + }, + { + key: 'cacheCreation', + title: t('Cache Write'), + description: t('Statistical tokens'), + icon: Upload, + iconTone: 'chart-5', + getValue: (stat) => stat?.cacheCreation ?? 0, + }, { key: 'avgRpm', title: t('Average RPM'), diff --git a/web/src/features/dashboard/index.tsx b/web/src/features/dashboard/index.tsx index ac5856478..0120d4826 100644 --- a/web/src/features/dashboard/index.tsx +++ b/web/src/features/dashboard/index.tsx @@ -64,6 +64,8 @@ const LOG_STAT_CARD_FALLBACK_KEYS = [ 'count', 'quota', 'tokens', + 'cache-read', + 'cache-creation', 'average-rpm', 'average-tpm', ] as const @@ -116,12 +118,12 @@ const LazyFlowCharts = lazy(() => function LogStatCardsFallback() { return (
-
+
{LOG_STAT_CARD_FALLBACK_KEYS.map((key, index) => (
))}
+
+ +
) } diff --git a/web/src/features/dashboard/lib/stats.ts b/web/src/features/dashboard/lib/stats.ts index 89e4232cb..0460a3226 100644 --- a/web/src/features/dashboard/lib/stats.ts +++ b/web/src/features/dashboard/lib/stats.ts @@ -37,11 +37,27 @@ export function safeDivide( */ export function calculateDashboardStats(data: QuotaDataItem[]) { return data.reduce( - (acc, item) => ({ - totalQuota: acc.totalQuota + (Number(item.quota) || 0), - totalCount: acc.totalCount + (Number(item.count) || 0), - totalTokens: acc.totalTokens + (Number(item.token_used) || 0), - }), - { totalQuota: 0, totalCount: 0, totalTokens: 0 } + (acc, item) => { + const cacheRead = item.cache_read_tokens ?? 0 + const cacheCreation = item.cache_creation_tokens ?? 0 + return { + totalQuota: acc.totalQuota + (Number(item.quota) || 0), + totalCount: acc.totalCount + (Number(item.count) || 0), + totalTokens: acc.totalTokens + (Number(item.token_used) || 0), + totalCacheReadTokens: + acc.totalCacheReadTokens + + (Number.isFinite(cacheRead) ? Math.max(0, cacheRead) : 0), + totalCacheCreationTokens: + acc.totalCacheCreationTokens + + (Number.isFinite(cacheCreation) ? Math.max(0, cacheCreation) : 0), + } + }, + { + totalQuota: 0, + totalCount: 0, + totalTokens: 0, + totalCacheReadTokens: 0, + totalCacheCreationTokens: 0, + } ) } diff --git a/web/src/features/dashboard/types.ts b/web/src/features/dashboard/types.ts index b8771df25..4d11c2171 100644 --- a/web/src/features/dashboard/types.ts +++ b/web/src/features/dashboard/types.ts @@ -28,6 +28,8 @@ export interface QuotaDataItem { username?: string model_name?: string created_at: number + cache_read_tokens?: number + cache_creation_tokens?: number token_used?: number count?: number quota?: number diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index 6f65eeab0..b82a0fa2c 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -3719,6 +3719,7 @@ "Model assignments": "Model assignments", "model billing support": "model billing support", "Model Call Analytics": "Model Call Analytics", + "Cache usage is recorded only after the upgrade; historical data is not backfilled.": "Cache usage is recorded only after the upgrade; historical data is not backfilled.", "Model context usage": "Model context usage", "Model deleted": "Model deleted", "Model deleted successfully": "Model deleted successfully", diff --git a/web/src/i18n/locales/fr.json b/web/src/i18n/locales/fr.json index 819bbffac..b26513fc4 100644 --- a/web/src/i18n/locales/fr.json +++ b/web/src/i18n/locales/fr.json @@ -3719,6 +3719,7 @@ "Model assignments": "Attribution des modèles", "model billing support": "prise en charge de la facturation des modèles", "Model Call Analytics": "Analyse des appels de modèles", + "Cache usage is recorded only after the upgrade; historical data is not backfilled.": "L’utilisation du cache est enregistrée uniquement après la mise à niveau ; les données historiques ne sont pas complétées.", "Model context usage": "Utilisation du contexte du modèle", "Model deleted": "Modèle supprimé", "Model deleted successfully": "Modèle supprimé avec succès", diff --git a/web/src/i18n/locales/ja.json b/web/src/i18n/locales/ja.json index 627b6f659..3fa3848b8 100644 --- a/web/src/i18n/locales/ja.json +++ b/web/src/i18n/locales/ja.json @@ -3719,6 +3719,7 @@ "Model assignments": "モデルの割り当て", "model billing support": "モデル課金対応", "Model Call Analytics": "モデル呼び出し分析", + "Cache usage is recorded only after the upgrade; historical data is not backfilled.": "キャッシュ使用量はアップグレード後から記録され、過去のデータは補完されません。", "Model context usage": "モデルのコンテキスト使用量", "Model deleted": "モデルが削除されました", "Model deleted successfully": "モデルが正常に削除されました", diff --git a/web/src/i18n/locales/ru.json b/web/src/i18n/locales/ru.json index f8106cbaa..64fa78c9e 100644 --- a/web/src/i18n/locales/ru.json +++ b/web/src/i18n/locales/ru.json @@ -3719,6 +3719,7 @@ "Model assignments": "Привязка моделей", "model billing support": "поддержка биллинга моделей", "Model Call Analytics": "Аналитика вызовов моделей", + "Cache usage is recorded only after the upgrade; historical data is not backfilled.": "Использование кэша учитывается только после обновления; исторические данные не заполняются.", "Model context usage": "Использование контекста модели", "Model deleted": "Модель удалена", "Model deleted successfully": "Модель успешно удалена", diff --git a/web/src/i18n/locales/vi.json b/web/src/i18n/locales/vi.json index 78d5802e2..25902e7c8 100644 --- a/web/src/i18n/locales/vi.json +++ b/web/src/i18n/locales/vi.json @@ -3719,6 +3719,7 @@ "Model assignments": "Gán mô hình", "model billing support": "hỗ trợ tính phí mô hình", "Model Call Analytics": "Phân tích lượt gọi mô hình", + "Cache usage is recorded only after the upgrade; historical data is not backfilled.": "Mức sử dụng bộ nhớ đệm chỉ được ghi nhận sau khi nâng cấp; dữ liệu lịch sử không được bổ sung.", "Model context usage": "Sử dụng ngữ cảnh mô hình", "Model deleted": "Đã xóa mô hình", "Model deleted successfully": "Model đã được xóa thành công", diff --git a/web/src/i18n/locales/zh-TW.json b/web/src/i18n/locales/zh-TW.json index 30f221e0b..250aa107d 100644 --- a/web/src/i18n/locales/zh-TW.json +++ b/web/src/i18n/locales/zh-TW.json @@ -3719,6 +3719,7 @@ "Model assignments": "模型歸屬", "model billing support": "模型收費支援", "Model Call Analytics": "模型呼叫分析", + "Cache usage is recorded only after the upgrade; historical data is not backfilled.": "快取用量僅從升級後開始記錄,歷史資料不回填。", "Model context usage": "模型上下文用量", "Model deleted": "模型已刪除", "Model deleted successfully": "模型刪除成功", diff --git a/web/src/i18n/locales/zh.json b/web/src/i18n/locales/zh.json index 3bc1acbce..d8dd6e12b 100644 --- a/web/src/i18n/locales/zh.json +++ b/web/src/i18n/locales/zh.json @@ -3719,6 +3719,7 @@ "Model assignments": "模型归属", "model billing support": "模型计费支持", "Model Call Analytics": "模型调用分析", + "Cache usage is recorded only after the upgrade; historical data is not backfilled.": "缓存用量仅从升级后开始记录,历史数据不回填。", "Model context usage": "模型上下文用量", "Model deleted": "模型已删除", "Model deleted successfully": "模型删除成功",