diff --git a/common/init.go b/common/init.go index 4d4c62b27cac..b425656ef314 100644 --- a/common/init.go +++ b/common/init.go @@ -175,6 +175,7 @@ func positiveUserSessionEnv(name string, fallback int) int { func initConstantEnv() { constant.StreamingTimeout = GetEnvOrDefault("STREAMING_TIMEOUT", 300) + constant.ThinkingStreamingTimeout = GetEnvOrDefault("THINKING_STREAMING_TIMEOUT", 900) constant.DifyDebug = GetEnvOrDefaultBool("DIFY_DEBUG", true) constant.MaxFileDownloadMB = GetEnvOrDefault("MAX_FILE_DOWNLOAD_MB", 64) constant.StreamScannerMaxBufferMB = GetEnvOrDefault("STREAM_SCANNER_MAX_BUFFER_MB", 128) diff --git a/constant/env.go b/constant/env.go index 512bfc31126b..a3a62d5b317b 100644 --- a/constant/env.go +++ b/constant/env.go @@ -1,6 +1,7 @@ package constant var StreamingTimeout int +var ThinkingStreamingTimeout int var DifyDebug bool var MaxFileDownloadMB int var StreamScannerMaxBufferMB int diff --git a/controller/log.go b/controller/log.go index 470c759fc1a1..7f30559fed40 100644 --- a/controller/log.go +++ b/controller/log.go @@ -22,7 +22,8 @@ func GetAllLogs(c *gin.Context) { group := c.Query("group") requestId := c.Query("request_id") upstreamRequestId := c.Query("upstream_request_id") - logs, total, err := model.GetAllLogs(logType, startTimestamp, endTimestamp, modelName, username, tokenName, pageInfo.GetStartIdx(), pageInfo.GetPageSize(), channel, group, requestId, upstreamRequestId) + ip := c.Query("ip") + logs, total, err := model.GetAllLogs(logType, startTimestamp, endTimestamp, modelName, username, tokenName, pageInfo.GetStartIdx(), pageInfo.GetPageSize(), channel, group, requestId, upstreamRequestId, ip) if err != nil { common.ApiError(c, err) return @@ -44,7 +45,8 @@ func GetUserLogs(c *gin.Context) { group := c.Query("group") requestId := c.Query("request_id") upstreamRequestId := c.Query("upstream_request_id") - logs, total, err := model.GetUserLogs(userId, logType, startTimestamp, endTimestamp, modelName, tokenName, pageInfo.GetStartIdx(), pageInfo.GetPageSize(), group, requestId, upstreamRequestId) + ip := c.Query("ip") + logs, total, err := model.GetUserLogs(userId, logType, startTimestamp, endTimestamp, modelName, tokenName, pageInfo.GetStartIdx(), pageInfo.GetPageSize(), group, requestId, upstreamRequestId, ip) if err != nil { common.ApiError(c, err) return @@ -104,20 +106,16 @@ func GetLogsStat(c *gin.Context) { modelName := c.Query("model_name") channel, _ := strconv.Atoi(c.Query("channel")) group := c.Query("group") - stat, err := model.SumUsedQuota(logType, startTimestamp, endTimestamp, modelName, username, tokenName, channel, group) + ip := c.Query("ip") + stat, err := model.SumUsedQuota(logType, startTimestamp, endTimestamp, modelName, username, tokenName, channel, group, ip) if err != nil { common.ApiError(c, err) return } - //tokenNum := model.SumUsedToken(logType, startTimestamp, endTimestamp, modelName, username, "") c.JSON(http.StatusOK, gin.H{ "success": true, "message": "", - "data": gin.H{ - "quota": stat.Quota, - "rpm": stat.Rpm, - "tpm": stat.Tpm, - }, + "data": stat, }) return } @@ -131,21 +129,16 @@ func GetLogsSelfStat(c *gin.Context) { modelName := c.Query("model_name") channel, _ := strconv.Atoi(c.Query("channel")) group := c.Query("group") - quotaNum, err := model.SumUsedQuota(logType, startTimestamp, endTimestamp, modelName, username, tokenName, channel, group) + ip := c.Query("ip") + stat, err := model.SumUsedQuota(logType, startTimestamp, endTimestamp, modelName, username, tokenName, channel, group, ip) if err != nil { common.ApiError(c, err) return } - //tokenNum := model.SumUsedToken(logType, startTimestamp, endTimestamp, modelName, username, tokenName) c.JSON(200, gin.H{ "success": true, "message": "", - "data": gin.H{ - "quota": quotaNum.Quota, - "rpm": quotaNum.Rpm, - "tpm": quotaNum.Tpm, - //"token": tokenNum, - }, + "data": stat, }) return } diff --git a/model/log.go b/model/log.go index 401d53c435a5..f3b9e54d0ae9 100644 --- a/model/log.go +++ b/model/log.go @@ -350,13 +350,6 @@ func RecordConsumeLog(c *gin.Context, userId int, params RecordConsumeLogParams) upstreamRequestId := c.GetString(common.UpstreamRequestIdKey) createdAt := common.GetTimestamp() otherStr := common.MapToJsonStr(params.Other) - // 判断是否需要记录 IP - needRecordIp := false - if settingMap, err := GetUserSetting(userId, false); err == nil { - if settingMap.RecordIpLog { - needRecordIp = true - } - } log := &Log{ UserId: userId, Username: username, @@ -373,12 +366,7 @@ func RecordConsumeLog(c *gin.Context, userId int, params RecordConsumeLogParams) UseTime: params.UseTimeSeconds, IsStream: params.IsStream, Group: params.Group, - Ip: func() string { - if needRecordIp { - return c.ClientIP() - } - return "" - }(), + Ip: c.ClientIP(), RequestId: requestId, UpstreamRequestId: upstreamRequestId, Other: otherStr, @@ -465,7 +453,7 @@ func RecordTaskBillingLog(params RecordTaskBillingLogParams) { } } -func GetAllLogs(logType int, startTimestamp int64, endTimestamp int64, modelName string, username string, tokenName string, startIdx int, num int, channel int, group string, requestId string, upstreamRequestId string) (logs []*Log, total int64, err error) { +func GetAllLogs(logType int, startTimestamp int64, endTimestamp int64, modelName string, username string, tokenName string, startIdx int, num int, channel int, group string, requestId string, upstreamRequestId string, ip string) (logs []*Log, total int64, err error) { var tx *gorm.DB if logType == LogTypeUnknown { tx = LOG_DB @@ -500,6 +488,9 @@ func GetAllLogs(logType int, startTimestamp int64, endTimestamp int64, modelName if group != "" { tx = tx.Where("logs."+logGroupCol+" = ?", group) } + if ip != "" { + tx = tx.Where("logs.ip = ?", ip) + } err = tx.Model(&Log{}).Count(&total).Error if err != nil { return nil, 0, err @@ -561,7 +552,7 @@ func GetAllLogs(logType int, startTimestamp int64, endTimestamp int64, modelName const logSearchCountLimit = 10000 -func GetUserLogs(userId int, logType int, startTimestamp int64, endTimestamp int64, modelName string, tokenName string, startIdx int, num int, group string, requestId string, upstreamRequestId string) (logs []*Log, total int64, err error) { +func GetUserLogs(userId int, logType int, startTimestamp int64, endTimestamp int64, modelName string, tokenName string, startIdx int, num int, group string, requestId string, upstreamRequestId string, ip string) (logs []*Log, total int64, err error) { var tx *gorm.DB if logType == LogTypeUnknown { tx = LOG_DB.Where("logs.user_id = ?", userId) @@ -590,6 +581,9 @@ func GetUserLogs(userId int, logType int, startTimestamp int64, endTimestamp int if group != "" { tx = tx.Where("logs."+logGroupCol+" = ?", group) } + if ip != "" { + tx = tx.Where("logs.ip = ?", ip) + } err = tx.Model(&Log{}).Limit(logSearchCountLimit).Count(&total).Error if err != nil { common.SysError("failed to count user logs: " + err.Error()) @@ -610,63 +604,138 @@ func GetUserLogs(userId int, logType int, startTimestamp int64, endTimestamp int } type Stat struct { - Quota int `json:"quota"` - Rpm int `json:"rpm"` - Tpm int `json:"tpm"` + Quota int `json:"quota"` + Rpm int `json:"rpm"` + Tpm int `json:"tpm"` + TotalRequests int64 `json:"total_requests"` + TodayRequests int64 `json:"today_requests"` + TotalTokens int64 `json:"total_tokens"` + TodayTokens int64 `json:"today_tokens"` + AvgUseTime float64 `json:"avg_use_time"` + ErrorCount int64 `json:"error_count"` } -func SumUsedQuota(logType int, startTimestamp int64, endTimestamp int64, modelName string, username string, tokenName string, channel int, group string) (stat Stat, err error) { - tx := LOG_DB.Table("logs").Select("COALESCE(sum(quota), 0) quota") - - // 为rpm和tpm创建单独的查询 - rpmTpmQuery := LOG_DB.Table("logs").Select("count(*) rpm, COALESCE(sum(prompt_tokens), 0) + COALESCE(sum(completion_tokens), 0) tpm") +func todayStartUnix() int64 { + now := time.Now() + return time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).Unix() +} +func applyStatBaseFilters(tx *gorm.DB, username, tokenName, modelName string, channel int, group string, ip string) (*gorm.DB, error) { + var err error if tx, err = applyExplicitLogTextFilter(tx, "username", username); err != nil { - return stat, err - } - if rpmTpmQuery, err = applyExplicitLogTextFilter(rpmTpmQuery, "username", username); err != nil { - return stat, err + return nil, err } if tokenName != "" { tx = tx.Where("token_name = ?", tokenName) - rpmTpmQuery = rpmTpmQuery.Where("token_name = ?", tokenName) - } - if startTimestamp != 0 { - tx = tx.Where("created_at >= ?", startTimestamp) - } - if endTimestamp != 0 { - tx = tx.Where("created_at <= ?", endTimestamp) } if tx, err = applyExplicitLogTextFilter(tx, "model_name", modelName); err != nil { - return stat, err - } - if rpmTpmQuery, err = applyExplicitLogTextFilter(rpmTpmQuery, "model_name", modelName); err != nil { - return stat, err + return nil, err } if channel != 0 { tx = tx.Where("channel_id = ?", channel) - rpmTpmQuery = rpmTpmQuery.Where("channel_id = ?", channel) } if group != "" { tx = tx.Where(logGroupCol+" = ?", group) - rpmTpmQuery = rpmTpmQuery.Where(logGroupCol+" = ?", group) } + if ip != "" { + tx = tx.Where("ip = ?", ip) + } + return tx, nil +} - tx = tx.Where("type = ?", LogTypeConsume) - rpmTpmQuery = rpmTpmQuery.Where("type = ?", LogTypeConsume) +func SumUsedQuota(logType int, startTimestamp int64, endTimestamp int64, modelName string, username string, tokenName string, channel int, group string, ip string) (stat Stat, err error) { + // quota: sum over the requested time range (all log types when logType==0) + quotaTx := LOG_DB.Table("logs").Select("COALESCE(sum(quota), 0) quota") + if quotaTx, err = applyStatBaseFilters(quotaTx, username, tokenName, modelName, channel, group, ip); err != nil { + return stat, err + } + if startTimestamp != 0 { + quotaTx = quotaTx.Where("created_at >= ?", startTimestamp) + } + if endTimestamp != 0 { + quotaTx = quotaTx.Where("created_at <= ?", endTimestamp) + } + if logType != LogTypeUnknown { + quotaTx = quotaTx.Where("type = ?", logType) + } + if err = quotaTx.Scan(&stat).Error; err != nil { + common.SysError("failed to query log stat quota: " + err.Error()) + return stat, errors.New("查询统计数据失败") + } - // 只统计最近60秒的rpm和tpm - rpmTpmQuery = rpmTpmQuery.Where("created_at >= ?", time.Now().Add(-60*time.Second).Unix()) + // realtime rpm/tpm: consume logs in the last 60 seconds + realtimeTx := LOG_DB.Table("logs"). + Select("count(*) rpm, COALESCE(sum(prompt_tokens), 0) + COALESCE(sum(completion_tokens), 0) tpm") + if realtimeTx, err = applyStatBaseFilters(realtimeTx, username, tokenName, modelName, channel, group, ip); err != nil { + return stat, err + } + realtimeTx = realtimeTx. + Where("type = ?", LogTypeConsume). + Where("created_at >= ?", time.Now().Add(-60*time.Second).Unix()) + if err = realtimeTx.Scan(&stat).Error; err != nil { + common.SysError("failed to query rpm/tpm stat: " + err.Error()) + return stat, errors.New("查询统计数据失败") + } - // 执行查询 - if err := tx.Scan(&stat).Error; err != nil { - common.SysError("failed to query log stat: " + err.Error()) + // range aggregate: consume logs within the requested time window + type rangeResult struct { + TotalRequests int64 `gorm:"column:total_requests"` + TotalTokens int64 `gorm:"column:total_tokens"` + AvgUseTime float64 `gorm:"column:avg_use_time"` + ErrorCount int64 `gorm:"column:error_count"` + } + tokenCastType := "INTEGER" + if common.UsingLogDatabase(common.DatabaseTypeMySQL) { + tokenCastType = "SIGNED" + } + var rr rangeResult + rangeTx := LOG_DB.Table("logs").Select( + "COALESCE(SUM(CASE WHEN type = ? THEN 1 ELSE 0 END), 0) total_requests, "+ + "COALESCE(SUM(CASE WHEN type = ? THEN CAST(prompt_tokens AS "+tokenCastType+") + CAST(completion_tokens AS "+tokenCastType+") ELSE 0 END), 0) total_tokens, "+ + "COALESCE(AVG(CASE WHEN type = ? AND use_time > 0 THEN use_time ELSE NULL END), 0) avg_use_time, "+ + "COALESCE(SUM(CASE WHEN type = ? THEN 1 ELSE 0 END), 0) error_count", + LogTypeConsume, LogTypeConsume, LogTypeConsume, LogTypeError, + ) + if rangeTx, err = applyStatBaseFilters(rangeTx, username, tokenName, modelName, channel, group, ip); err != nil { + return stat, err + } + if startTimestamp != 0 { + rangeTx = rangeTx.Where("created_at >= ?", startTimestamp) + } + if endTimestamp != 0 { + rangeTx = rangeTx.Where("created_at <= ?", endTimestamp) + } + if err = rangeTx.Scan(&rr).Error; err != nil { + common.SysError("failed to query range stat: " + err.Error()) return stat, errors.New("查询统计数据失败") } - if err := rpmTpmQuery.Scan(&stat).Error; err != nil { - common.SysError("failed to query rpm/tpm stat: " + err.Error()) + stat.TotalRequests = rr.TotalRequests + stat.TotalTokens = rr.TotalTokens + stat.AvgUseTime = rr.AvgUseTime + stat.ErrorCount = rr.ErrorCount + + // today aggregate: consume logs since today 00:00:00 + type todayResult struct { + TodayRequests int64 `gorm:"column:today_requests"` + TodayTokens int64 `gorm:"column:today_tokens"` + } + var tr todayResult + todayTx := LOG_DB.Table("logs").Select( + "count(*) today_requests, "+ + "COALESCE(sum(CAST(prompt_tokens AS "+tokenCastType+") + CAST(completion_tokens AS "+tokenCastType+")), 0) today_tokens", + ) + if todayTx, err = applyStatBaseFilters(todayTx, username, tokenName, modelName, channel, group, ip); err != nil { + return stat, err + } + todayTx = todayTx. + Where("type = ?", LogTypeConsume). + Where("created_at >= ?", todayStartUnix()) + if err = todayTx.Scan(&tr).Error; err != nil { + common.SysError("failed to query today stat: " + err.Error()) return stat, errors.New("查询统计数据失败") } + stat.TodayRequests = tr.TodayRequests + stat.TodayTokens = tr.TodayTokens return stat, nil } diff --git a/relay/claude_handler.go b/relay/claude_handler.go index e4a4920306f1..b589febe0ecc 100644 --- a/relay/claude_handler.go +++ b/relay/claude_handler.go @@ -107,6 +107,10 @@ func ClaudeHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *typ info.UpstreamModelName = request.Model } + if request.Thinking != nil { + info.IsThinking = true + } + if info.ChannelSetting.SystemPrompt != "" { if request.System == nil { request.SetStringSystem(info.ChannelSetting.SystemPrompt) diff --git a/relay/common/relay_info.go b/relay/common/relay_info.go index 9f460ce5c6a7..0b255f474a4c 100644 --- a/relay/common/relay_info.go +++ b/relay/common/relay_info.go @@ -184,6 +184,8 @@ type RelayInfo struct { StreamStatus *StreamStatus + IsThinking bool + ThinkingContentInfo TokenCountMeta *ClaudeConvertInfo diff --git a/relay/helper/stream_scanner.go b/relay/helper/stream_scanner.go index f88bef5206fc..ad7d283f0b49 100644 --- a/relay/helper/stream_scanner.go +++ b/relay/helper/stream_scanner.go @@ -85,7 +85,11 @@ func StreamScannerHandler(c *gin.Context, resp *http.Response, info *relaycommon ctx, cancel := context.WithCancel(context.Background()) - streamingTimeout := time.Duration(constant.StreamingTimeout) * time.Second + streamingTimeoutSec := constant.StreamingTimeout + if info.IsThinking && constant.ThinkingStreamingTimeout > streamingTimeoutSec { + streamingTimeoutSec = constant.ThinkingStreamingTimeout + } + streamingTimeout := time.Duration(streamingTimeoutSec) * time.Second var ( stopChan = make(chan bool, 3) // 增加缓冲区避免阻塞 diff --git a/web/src/features/usage-logs/components/columns/common-logs-columns.tsx b/web/src/features/usage-logs/components/columns/common-logs-columns.tsx index 07f54f747b00..aeb7a1d9225b 100644 --- a/web/src/features/usage-logs/components/columns/common-logs-columns.tsx +++ b/web/src/features/usage-logs/components/columns/common-logs-columns.tsx @@ -602,6 +602,25 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef[] { }, size: 160, }) + + columns.push({ + accessorKey: 'ip', + header: t('IP'), + cell: function IpCell({ row }) { + const { sensitiveVisible } = useUsageLogsContext() + const log = row.original + if (!isDisplayableLogType(log.type)) return null + if (!log.ip) return null + return ( + + {sensitiveVisible ? log.ip : '••••'} + + ) + }, + size: 130, + meta: { label: t('IP') }, + }) + columns.push( { accessorKey: 'model_name', diff --git a/web/src/features/usage-logs/components/common-logs-filter-bar.tsx b/web/src/features/usage-logs/components/common-logs-filter-bar.tsx index b774aa1cf8b4..08ca7188caa4 100644 --- a/web/src/features/usage-logs/components/common-logs-filter-bar.tsx +++ b/web/src/features/usage-logs/components/common-logs-filter-bar.tsx @@ -1,4 +1,4 @@ -/* +/* Copyright (C) 2023-2026 QuantumNous This program is free software: you can redistribute it and/or modify @@ -87,6 +87,7 @@ function buildSearchSourceKey(values: { username?: unknown requestId?: unknown upstreamRequestId?: unknown + ip?: unknown type?: unknown }) { return [ @@ -406,6 +407,14 @@ export function CommonLogsFilterBar( onKeyDown={handleKeyDown} /> + + handleChange('ip', e.target.value)} + onKeyDown={handleKeyDown} + /> + ) diff --git a/web/src/features/usage-logs/components/common-logs-stats.tsx b/web/src/features/usage-logs/components/common-logs-stats.tsx index dd754e227acb..88e8cf603fb4 100644 --- a/web/src/features/usage-logs/components/common-logs-stats.tsx +++ b/web/src/features/usage-logs/components/common-logs-stats.tsx @@ -16,44 +16,101 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ -import { useQuery } from '@tanstack/react-query' -import { getRouteApi } from '@tanstack/react-router' +import { useQuery, useQueryClient } from '@tanstack/react-query' +import { getRouteApi, useNavigate } from '@tanstack/react-router' +import { RefreshCw } from 'lucide-react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' +import { Button } from '@/components/ui/button' import { Skeleton } from '@/components/ui/skeleton' +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from '@/components/ui/tooltip' import { formatLogQuota } from '@/lib/format' import { cn } from '@/lib/utils' import { getLogStats, getUserLogStats } from '../api' -import { DEFAULT_LOG_STATS } from '../constants' -import { buildApiParams } from '../lib/utils' +import { DEFAULT_LOG_STATS, LOG_TYPE_ENUM } from '../constants' +import { buildApiParams, getDefaultTimeRange } from '../lib/utils' import { useLogsViewScope, useUsageLogsContext } from './usage-logs-provider' const route = getRouteApi('/_authenticated/usage-logs/$section') +const REFRESH_INTERVALS = [0, 30, 60, 300] as const +type RefreshInterval = (typeof REFRESH_INTERVALS)[number] + +function refreshLabel(ms: number): string { + if (ms === 0) return 'Off' + if (ms < 60_000) return `${ms / 1000}s` + return `${ms / 60_000}m` +} + function StatBadge(props: { label: string value: string | number accent: string + title?: string + onClick?: () => void }) { + const Tag = props.onClick ? 'button' : 'span' return ( - + {props.label} {props.value} - + ) } +function formatAvgTime(seconds: number): string { + if (seconds <= 0) return '—' + if (seconds < 1) return `${Math.round(seconds * 1000)}ms` + return `${seconds.toFixed(1)}s` +} + +function formatCount(n: number): string { + if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M` + if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K` + return String(n) +} + +function calcRpm(requests: number, minutes: number): string { + if (minutes <= 0 || requests <= 0) return '0' + const rpm = requests / minutes + return rpm < 1 ? rpm.toFixed(2) : Math.round(rpm).toLocaleString() +} + +function calcTpm(tokens: number, minutes: number): string { + if (minutes <= 0 || tokens <= 0) return '0' + const tpm = tokens / minutes + return tpm < 1 ? tpm.toFixed(1) : Math.round(tpm).toLocaleString() +} + export function CommonLogsStats() { const { t } = useTranslation() const { isAdminView: isAdmin } = useLogsViewScope() const searchParams = route.useSearch() const { sensitiveVisible } = useUsageLogsContext() + const navigate = useNavigate() + const queryClient = useQueryClient() + const [refreshIntervalIdx, setRefreshIntervalIdx] = useState(0) + const refreshMs = REFRESH_INTERVALS[refreshIntervalIdx] * 1000 - const { data: stats, isLoading } = useQuery({ + const { data: stats, isLoading, isFetching } = useQuery({ queryKey: ['usage-logs-stats', isAdmin, searchParams], queryFn: async () => { const params = buildApiParams({ @@ -63,45 +120,165 @@ export function CommonLogsStats() { columnFilters: [], isAdmin, }) - const result = isAdmin ? await getLogStats(params) : await getUserLogStats(params) - - return result.success - ? result.data || DEFAULT_LOG_STATS - : DEFAULT_LOG_STATS + return result.success ? result.data || DEFAULT_LOG_STATS : DEFAULT_LOG_STATS }, placeholderData: (previousData) => previousData, + refetchInterval: refreshMs > 0 ? refreshMs : false, }) + const rangeMinutes = (() => { + const { start, end } = getDefaultTimeRange() + const startMs = (searchParams.startTime as number | undefined) ?? start.getTime() + const endMs = (searchParams.endTime as number | undefined) ?? end.getTime() + return Math.max((endMs - startMs) / 60_000, 1) + })() + + const todayMinutes = (() => { + const now = new Date() + return Math.max((now.getHours() * 60 + now.getMinutes()) || 1, 1) + })() + + const goToToday = () => { + const now = new Date() + const start = new Date(now) + start.setHours(0, 0, 0, 0) + const end = new Date(now.getTime() + 3600 * 1000) + navigate({ + to: '/usage-logs/$section', + params: { section: 'common' }, + search: (prev: Record) => ({ + ...prev, + startTime: start.getTime(), + endTime: end.getTime(), + page: 1, + }), + }) + queryClient.invalidateQueries({ queryKey: ['logs'] }) + queryClient.invalidateQueries({ queryKey: ['usage-logs-stats'] }) + } + + const goToErrors = () => { + navigate({ + to: '/usage-logs/$section', + params: { section: 'common' }, + search: (prev: Record) => ({ + ...prev, + type: [String(LOG_TYPE_ENUM.ERROR)], + page: 1, + }), + }) + queryClient.invalidateQueries({ queryKey: ['logs'] }) + queryClient.invalidateQueries({ queryKey: ['usage-logs-stats'] }) + } + + const cycleRefresh = () => { + setRefreshIntervalIdx((i) => (i + 1) % REFRESH_INTERVALS.length) + } + if (isLoading) { return ( -
- - - +
+ {Array.from({ length: 8 }).map((_, i) => ( + + ))}
) } + const s = stats ?? DEFAULT_LOG_STATS + return (
+ + + + + + + + 0 && 'text-primary hover:text-primary', + isFetching && 'animate-spin' + )} + aria-label='Auto refresh' + /> + } + > + + {refreshMs > 0 && ( + + {refreshLabel(refreshMs)} + + )} + + + {refreshMs === 0 + ? t('Auto refresh: Off. Click to enable.') + : `${t('Auto refresh')}: ${refreshLabel(refreshMs)}`} + +
) } diff --git a/web/src/features/usage-logs/constants.ts b/web/src/features/usage-logs/constants.ts index a2dff48b3c1f..b79f5b71eebc 100644 --- a/web/src/features/usage-logs/constants.ts +++ b/web/src/features/usage-logs/constants.ts @@ -34,6 +34,12 @@ export const DEFAULT_LOG_STATS: LogStatistics = { quota: 0, rpm: 0, tpm: 0, + total_requests: 0, + today_requests: 0, + total_tokens: 0, + today_tokens: 0, + avg_use_time: 0, + error_count: 0, } /** diff --git a/web/src/features/usage-logs/lib/filter.ts b/web/src/features/usage-logs/lib/filter.ts index bd6ecb66e9fc..0d88838a070e 100644 --- a/web/src/features/usage-logs/lib/filter.ts +++ b/web/src/features/usage-logs/lib/filter.ts @@ -58,6 +58,7 @@ export function buildSearchParams( ...(commonFilters.upstreamRequestId && { upstreamRequestId: commonFilters.upstreamRequestId, }), + ...(commonFilters.ip && { ip: commonFilters.ip }), } } case 'drawing': { diff --git a/web/src/features/usage-logs/lib/utils.ts b/web/src/features/usage-logs/lib/utils.ts index 22a648f87f1b..4765ad3e0ecf 100644 --- a/web/src/features/usage-logs/lib/utils.ts +++ b/web/src/features/usage-logs/lib/utils.ts @@ -215,6 +215,7 @@ export function buildApiParams(config: { ...(searchParams.upstreamRequestId ? { upstream_request_id: String(searchParams.upstreamRequestId) } : {}), + ...(searchParams.ip ? { ip: String(searchParams.ip) } : {}), ...buildTimeRangeParams(searchParams, false), } diff --git a/web/src/features/usage-logs/types.ts b/web/src/features/usage-logs/types.ts index a03d393546a3..21f44b291ff7 100644 --- a/web/src/features/usage-logs/types.ts +++ b/web/src/features/usage-logs/types.ts @@ -53,6 +53,7 @@ export interface CommonLogFilters extends CommonFilters { username?: string requestId?: string upstreamRequestId?: string + ip?: string } /** @@ -242,6 +243,12 @@ export interface LogStatistics { quota: number rpm: number tpm: number + total_requests: number + today_requests: number + total_tokens: number + today_tokens: number + avg_use_time: number + error_count: number } // ============================================================================ @@ -313,6 +320,7 @@ export interface GetLogsParams { group?: string request_id?: string upstream_request_id?: string + ip?: string } export interface GetLogsResponse { @@ -337,6 +345,7 @@ export interface GetLogStatsParams { group?: string request_id?: string upstream_request_id?: string + ip?: string } export interface GetLogStatsResponse { diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index 609e72eb08c8..f9bddddf9bd8 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -5214,6 +5214,24 @@ "Zero retention": "Zero retention", "Zhipu": "Zhipu", "Zhipu V4": "Zhipu V4", - "Zoom": "Zoom" + "Zoom": "Zoom", + "Total Req": "Total Req", + "Today Req": "Today Req", + "Range RPM": "Range RPM", + "Today RPM": "Today RPM", + "Range TPM": "Range TPM", + "Today TPM": "Today TPM", + "Avg Latency": "Avg Latency", + "Errors": "Errors", + "Total consume requests in selected range": "Total consume requests in selected range", + "Consume requests since today 00:00": "Consume requests since today 00:00", + "Average requests per minute over selected range": "Average requests per minute over selected range", + "Average requests per minute since today 00:00": "Average requests per minute since today 00:00", + "Average tokens per minute over selected range": "Average tokens per minute over selected range", + "Average tokens per minute since today 00:00": "Average tokens per minute since today 00:00", + "Average response time for consume requests": "Average response time for consume requests", + "Error log count in selected range": "Error log count in selected range", + "Auto refresh: Off. Click to enable.": "Auto refresh: Off. Click to enable.", + "Auto refresh": "Auto refresh" } } diff --git a/web/src/i18n/locales/fr.json b/web/src/i18n/locales/fr.json index 443b3fa1b55f..4edcc18486ea 100644 --- a/web/src/i18n/locales/fr.json +++ b/web/src/i18n/locales/fr.json @@ -5214,6 +5214,24 @@ "Zero retention": "Aucune rétention", "Zhipu": "Zhipu", "Zhipu V4": "Zhipu V4", - "Zoom": "Zoom" + "Zoom": "Zoom", + "Total Req": "Req. totales", + "Today Req": "Req. aujourd'hui", + "Range RPM": "RPM plage", + "Today RPM": "RPM aujourd'hui", + "Range TPM": "TPM plage", + "Today TPM": "TPM aujourd'hui", + "Avg Latency": "Latence moy.", + "Errors": "Erreurs", + "Total consume requests in selected range": "Total des requêtes de consommation sur la plage sélectionnée", + "Consume requests since today 00:00": "Requêtes de consommation depuis 00:00 aujourd'hui", + "Average requests per minute over selected range": "Moyenne des requêtes par minute sur la plage sélectionnée", + "Average requests per minute since today 00:00": "Moyenne des requêtes par minute depuis 00:00 aujourd'hui", + "Average tokens per minute over selected range": "Moyenne des tokens par minute sur la plage sélectionnée", + "Average tokens per minute since today 00:00": "Moyenne des tokens par minute depuis 00:00 aujourd'hui", + "Average response time for consume requests": "Temps de réponse moyen pour les requêtes de consommation", + "Error log count in selected range": "Nombre d'erreurs dans la plage sélectionnée", + "Auto refresh: Off. Click to enable.": "Actualisation auto : désactivée. Cliquez pour activer.", + "Auto refresh": "Actualisation auto" } -} +} \ No newline at end of file diff --git a/web/src/i18n/locales/ja.json b/web/src/i18n/locales/ja.json index c15de5534b82..057a54ebbc49 100644 --- a/web/src/i18n/locales/ja.json +++ b/web/src/i18n/locales/ja.json @@ -5214,6 +5214,24 @@ "Zero retention": "データ保持なし", "Zhipu": "Zhipu", "Zhipu V4": "Zhipu V 4", - "Zoom": "ズーム" + "Zoom": "ズーム", + "Total Req": "総リクエスト", + "Today Req": "今日のリクエスト", + "Range RPM": "範囲 RPM", + "Today RPM": "今日の RPM", + "Range TPM": "範囲 TPM", + "Today TPM": "今日の TPM", + "Avg Latency": "平均レイテンシ", + "Errors": "エラー数", + "Total consume requests in selected range": "選択した期間内の総消費リクエスト数", + "Consume requests since today 00:00": "今日 00:00 からの消費リクエスト数", + "Average requests per minute over selected range": "選択した期間の平均毎分リクエスト数", + "Average requests per minute since today 00:00": "今日 00:00 からの平均毎分リクエスト数", + "Average tokens per minute over selected range": "選択した期間の平均毎分トークン数", + "Average tokens per minute since today 00:00": "今日 00:00 からの平均毎分トークン数", + "Average response time for consume requests": "消費リクエストの平均レスポンス時間", + "Error log count in selected range": "選択した期間内のエラーログ数", + "Auto refresh: Off. Click to enable.": "自動更新:オフ。クリックして有効化", + "Auto refresh": "自動更新" } } diff --git a/web/src/i18n/locales/ru.json b/web/src/i18n/locales/ru.json index a1c18a35657e..37e9b06ec5e5 100644 --- a/web/src/i18n/locales/ru.json +++ b/web/src/i18n/locales/ru.json @@ -5214,6 +5214,24 @@ "Zero retention": "Без хранения данных", "Zhipu": "Zhipu", "Zhipu V4": "Zhipu V4", - "Zoom": "Zoom" + "Zoom": "Zoom", + "Total Req": "Всего запросов", + "Today Req": "Запросов сегодня", + "Range RPM": "RPM за период", + "Today RPM": "RPM сегодня", + "Range TPM": "TPM за период", + "Today TPM": "TPM сегодня", + "Avg Latency": "Ср. задержка", + "Errors": "Ошибки", + "Total consume requests in selected range": "Всего запросов за выбранный период", + "Consume requests since today 00:00": "Запросы с 00:00 сегодня", + "Average requests per minute over selected range": "Среднее количество запросов в минуту за период", + "Average requests per minute since today 00:00": "Среднее количество запросов в минуту с 00:00", + "Average tokens per minute over selected range": "Среднее количество токенов в минуту за период", + "Average tokens per minute since today 00:00": "Среднее количество токенов в минуту с 00:00", + "Average response time for consume requests": "Среднее время ответа для запросов", + "Error log count in selected range": "Количество ошибок за выбранный период", + "Auto refresh: Off. Click to enable.": "Авто-обновление: выкл. Нажмите для включения.", + "Auto refresh": "Авто-обновление" } -} +} \ No newline at end of file diff --git a/web/src/i18n/locales/vi.json b/web/src/i18n/locales/vi.json index 19cdddb1621d..667abe7ddbb4 100644 --- a/web/src/i18n/locales/vi.json +++ b/web/src/i18n/locales/vi.json @@ -5214,6 +5214,24 @@ "Zero retention": "Không lưu dữ liệu", "Zhipu": "Zhipu", "Zhipu V4": "Zhipu V4", - "Zoom": "Zoom" + "Zoom": "Zoom", + "Total Req": "Tổng yêu cầu", + "Today Req": "Yêu cầu hôm nay", + "Range RPM": "RPM khoảng", + "Today RPM": "RPM hôm nay", + "Range TPM": "TPM khoảng", + "Today TPM": "TPM hôm nay", + "Avg Latency": "Độ trễ TB", + "Errors": "Lỗi", + "Total consume requests in selected range": "Tổng yêu cầu tiêu thụ trong khoảng thời gian đã chọn", + "Consume requests since today 00:00": "Yêu cầu tiêu thụ từ 00:00 hôm nay", + "Average requests per minute over selected range": "Trung bình yêu cầu mỗi phút trong khoảng đã chọn", + "Average requests per minute since today 00:00": "Trung bình yêu cầu mỗi phút từ 00:00 hôm nay", + "Average tokens per minute over selected range": "Trung bình token mỗi phút trong khoảng đã chọn", + "Average tokens per minute since today 00:00": "Trung bình token mỗi phút từ 00:00 hôm nay", + "Average response time for consume requests": "Thời gian phản hồi trung bình cho yêu cầu tiêu thụ", + "Error log count in selected range": "Số lượng lỗi trong khoảng thời gian đã chọn", + "Auto refresh: Off. Click to enable.": "Tự động làm mới: Tắt. Nhấn để bật.", + "Auto refresh": "Tự động làm mới" } -} +} \ No newline at end of file diff --git a/web/src/i18n/locales/zh-TW.json b/web/src/i18n/locales/zh-TW.json index d895a2b3d3fa..f285b939207a 100644 --- a/web/src/i18n/locales/zh-TW.json +++ b/web/src/i18n/locales/zh-TW.json @@ -5214,6 +5214,24 @@ "Zero retention": "零數據保留", "Zhipu": "智譜", "Zhipu V4": "智譜 V4", - "Zoom": "縮放" + "Zoom": "縮放", + "Total Req": "總請求", + "Today Req": "今日請求", + "Range RPM": "區間 RPM", + "Today RPM": "今日 RPM", + "Range TPM": "區間 TPM", + "Today TPM": "今日 TPM", + "Avg Latency": "平均延遲", + "Errors": "錯誤數", + "Total consume requests in selected range": "所選時間段內的總消費請求數", + "Consume requests since today 00:00": "今日 00:00 至今的消費請求數", + "Average requests per minute over selected range": "所選時間段內的平均每分鐘請求數", + "Average requests per minute since today 00:00": "今日 00:00 至今的平均每分鐘請求數", + "Average tokens per minute over selected range": "所選時間段內的平均每分鐘 Token 數", + "Average tokens per minute since today 00:00": "今日 00:00 至今的平均每分鐘 Token 數", + "Average response time for consume requests": "消費請求的平均回應時間", + "Error log count in selected range": "所選時間段內的錯誤日誌數", + "Auto refresh: Off. Click to enable.": "自動刷新:關閉,點擊開啟", + "Auto refresh": "自動刷新" } } diff --git a/web/src/i18n/locales/zh.json b/web/src/i18n/locales/zh.json index ee80b960bd2c..052ea4e77a07 100644 --- a/web/src/i18n/locales/zh.json +++ b/web/src/i18n/locales/zh.json @@ -5214,6 +5214,24 @@ "Zero retention": "零数据保留", "Zhipu": "智谱", "Zhipu V4": "智谱 V4", - "Zoom": "缩放" + "Zoom": "缩放", + "Total Req": "总请求", + "Today Req": "今日请求", + "Range RPM": "区间 RPM", + "Today RPM": "今日 RPM", + "Range TPM": "区间 TPM", + "Today TPM": "今日 TPM", + "Avg Latency": "平均延迟", + "Errors": "错误数", + "Total consume requests in selected range": "所选时间段内的总消费请求数", + "Consume requests since today 00:00": "今日 00:00 至今的消费请求数", + "Average requests per minute over selected range": "所选时间段内的平均每分钟请求数", + "Average requests per minute since today 00:00": "今日 00:00 至今的平均每分钟请求数", + "Average tokens per minute over selected range": "所选时间段内的平均每分钟 Token 数", + "Average tokens per minute since today 00:00": "今日 00:00 至今的平均每分钟 Token 数", + "Average response time for consume requests": "消费请求的平均响应时间", + "Error log count in selected range": "所选时间段内的错误日志数", + "Auto refresh: Off. Click to enable.": "自动刷新:关闭,点击开启", + "Auto refresh": "自动刷新" } } diff --git a/web/src/routes/_authenticated/usage-logs/$section.tsx b/web/src/routes/_authenticated/usage-logs/$section.tsx index 6361def35554..928b558e4f98 100644 --- a/web/src/routes/_authenticated/usage-logs/$section.tsx +++ b/web/src/routes/_authenticated/usage-logs/$section.tsx @@ -45,6 +45,7 @@ const usageLogsSearchSchema = z.object({ username: z.string().optional().catch(''), requestId: z.string().optional().catch(''), upstreamRequestId: z.string().optional().catch(''), + ip: z.string().optional().catch(''), startTime: z.number().optional(), endTime: z.number().optional(), })