From 79c604b2c97814cd5e88fbd5d0a074ad478fa3ad Mon Sep 17 00:00:00 2001 From: CSCITech Date: Sat, 27 Jun 2026 20:31:22 +0800 Subject: [PATCH 1/4] fix some bugs --- .../channels/components/channels-columns.tsx | 36 ++----------------- .../drawers/channel-mutate-drawer.tsx | 1 - 2 files changed, 2 insertions(+), 35 deletions(-) diff --git a/web/default/src/features/channels/components/channels-columns.tsx b/web/default/src/features/channels/components/channels-columns.tsx index 903207e..c7fc84a 100644 --- a/web/default/src/features/channels/components/channels-columns.tsx +++ b/web/default/src/features/channels/components/channels-columns.tsx @@ -48,7 +48,7 @@ import { import { ConfirmDialog } from '@/components/confirm-dialog' import { DataTableColumnHeader } from '@/components/data-table/column-header' import { GroupBadge } from '@/components/group-badge' -import { StatusBadge, StatusBadgeList } from '@/components/status-badge' +import { StatusBadge } from '@/components/status-badge' import { TableId } from '@/components/table-id' import { getCodexUsage } from '../api' import { CHANNEL_STATUS_CONFIG, MODEL_FETCHABLE_TYPES } from '../constants' @@ -97,22 +97,6 @@ function parseIonetMeta(otherInfo: string | null | undefined): null | { return null } -/** - * Render limited items with "and X more" indicator - */ -function renderLimitedItems( - items: React.ReactNode[], - maxDisplay: number = 2 -): React.ReactNode { - return ( - item} - /> - ) -} - /** * Upstream update tags (+N / -N) shown on channel name for model-fetchable channels */ @@ -863,23 +847,7 @@ export function useChannelsColumns(): ColumnDef[] { /> )) - return ( - - - }> - {renderLimitedItems(modelBadges, 2)} - - {modelArray.length > 2 && ( - -
{modelBadges}
-
- )} -
-
- ) + return
{modelBadges}
}, size: 200, enableSorting: false, diff --git a/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx b/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx index 66d8b7f..72f276b 100644 --- a/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx +++ b/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx @@ -2250,7 +2250,6 @@ export function ChannelMutateDrawer({ )} allowCreate createLabel='Add custom model "{{value}}"' - maxVisibleChips={8} /> {modelMappingGuardrail.exposedTargetModels From df9ffdc512b2112e85c22a4e30a16a89bab52315 Mon Sep 17 00:00:00 2001 From: CSCITech Date: Sun, 28 Jun 2026 00:10:13 +0800 Subject: [PATCH 2/4] fix some bugs --- controller/log.go | 12 ++-- controller/relay.go | 6 +- model/log.go | 49 ++++++++++----- model/log_test.go | 61 +++++++++++++++++++ service/log_info_generate.go | 6 +- service/log_info_generate_test.go | 50 +++++++++++++++ .../columns/common-logs-columns.tsx | 3 +- .../components/common-logs-filter-bar.tsx | 18 +++++- .../src/features/usage-logs/constants.ts | 2 + .../src/features/usage-logs/lib/utils.ts | 18 +++++- web/default/src/features/usage-logs/types.ts | 2 + .../_authenticated/usage-logs/$section.tsx | 2 +- 12 files changed, 202 insertions(+), 27 deletions(-) create mode 100644 service/log_info_generate_test.go diff --git a/controller/log.go b/controller/log.go index 93f71e2..9839641 100644 --- a/controller/log.go +++ b/controller/log.go @@ -15,6 +15,7 @@ import ( func GetAllLogs(c *gin.Context) { pageInfo := common.GetPageQuery(c) logType, _ := strconv.Atoi(c.Query("type")) + logFilter := c.Query("log_filter") startTimestamp, _ := strconv.ParseInt(c.Query("start_timestamp"), 10, 64) endTimestamp, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64) username := c.Query("username") @@ -24,7 +25,7 @@ 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) + logs, total, err := model.GetAllLogs(logType, logFilter, startTimestamp, endTimestamp, modelName, username, tokenName, pageInfo.GetStartIdx(), pageInfo.GetPageSize(), channel, group, requestId, upstreamRequestId) if err != nil { common.ApiError(c, err) return @@ -68,6 +69,7 @@ func GetUserLogs(c *gin.Context) { pageInfo := common.GetPageQuery(c) userId := c.GetInt("id") logType, _ := strconv.Atoi(c.Query("type")) + logFilter := c.Query("log_filter") startTimestamp, _ := strconv.ParseInt(c.Query("start_timestamp"), 10, 64) endTimestamp, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64) tokenName := c.Query("token_name") @@ -75,7 +77,7 @@ 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) + logs, total, err := model.GetUserLogs(userId, logType, logFilter, startTimestamp, endTimestamp, modelName, tokenName, pageInfo.GetStartIdx(), pageInfo.GetPageSize(), group, requestId, upstreamRequestId) if err != nil { common.ApiError(c, err) return @@ -155,6 +157,7 @@ func GetLogByKey(c *gin.Context) { func GetLogsStat(c *gin.Context) { logType, _ := strconv.Atoi(c.Query("type")) + logFilter := c.Query("log_filter") startTimestamp, _ := strconv.ParseInt(c.Query("start_timestamp"), 10, 64) endTimestamp, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64) tokenName := c.Query("token_name") @@ -162,7 +165,7 @@ 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) + stat, err := model.SumUsedQuota(logType, logFilter, startTimestamp, endTimestamp, modelName, username, tokenName, channel, group) if err != nil { common.ApiError(c, err) return @@ -183,13 +186,14 @@ func GetLogsStat(c *gin.Context) { func GetLogsSelfStat(c *gin.Context) { username := c.GetString("username") logType, _ := strconv.Atoi(c.Query("type")) + logFilter := c.Query("log_filter") startTimestamp, _ := strconv.ParseInt(c.Query("start_timestamp"), 10, 64) endTimestamp, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64) tokenName := c.Query("token_name") 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) + quotaNum, err := model.SumUsedQuota(logType, logFilter, startTimestamp, endTimestamp, modelName, username, tokenName, channel, group) if err != nil { common.ApiError(c, err) return diff --git a/controller/relay.go b/controller/relay.go index f701e03..8d8db3e 100644 --- a/controller/relay.go +++ b/controller/relay.go @@ -387,7 +387,11 @@ func processChannelError(c *gin.Context, channelError types.ChannelError, err *t other["channel_name"] = c.GetString("channel_name") other["channel_type"] = c.GetInt("channel_type") adminInfo := make(map[string]interface{}) - adminInfo["use_channel"] = c.GetStringSlice("use_channel") + useChannel := c.GetStringSlice("use_channel") + adminInfo["use_channel"] = useChannel + if len(useChannel) > 1 { + other["retry_log"] = true + } isMultiKey := common.GetContextKeyBool(c, constant.ContextKeyChannelIsMultiKey) if isMultiKey { adminInfo["is_multi_key"] = true diff --git a/model/log.go b/model/log.go index e17a5eb..2425848 100644 --- a/model/log.go +++ b/model/log.go @@ -68,6 +68,32 @@ const ( LogTypeLogin = 7 ) +const LogFilterRetry = "retry" + +func applyLogTypeFilter(tx *gorm.DB, logType int) *gorm.DB { + if logType == LogTypeUnknown { + return tx + } + return tx.Where("logs.type = ?", logType) +} + +func applyRetryLogFilter(tx *gorm.DB) *gorm.DB { + retryPatterns := []string{ + `%"retry_log":true%`, + `%"empty_retry":true%`, + } + return tx.Where("(logs.other LIKE ? OR logs.other LIKE ?)", retryPatterns[0], retryPatterns[1]) +} + +func applyLogFilter(tx *gorm.DB, filter string) *gorm.DB { + switch filter { + case LogFilterRetry: + return applyRetryLogFilter(tx) + default: + return tx + } +} + func userVisibleAuditInfo(adminInfoValue interface{}) map[string]interface{} { if !common.LogRequestContentEnabled && !common.LogResponseContentEnabled { return nil @@ -464,13 +490,8 @@ 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) { - var tx *gorm.DB - if logType == LogTypeUnknown { - tx = LOG_DB - } else { - tx = LOG_DB.Where("logs.type = ?", logType) - } +func GetAllLogs(logType int, logFilter string, 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) { + tx := applyLogFilter(applyLogTypeFilter(LOG_DB, logType), logFilter) if tx, err = applyExplicitLogTextFilter(tx, "logs.model_name", modelName); err != nil { return nil, 0, err @@ -554,13 +575,8 @@ 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) { - var tx *gorm.DB - if logType == LogTypeUnknown { - tx = LOG_DB.Where("logs.user_id = ?", userId) - } else { - tx = LOG_DB.Where("logs.user_id = ? and logs.type = ?", userId, logType) - } +func GetUserLogs(userId int, logType int, logFilter string, startTimestamp int64, endTimestamp int64, modelName string, tokenName string, startIdx int, num int, group string, requestId string, upstreamRequestId string) (logs []*Log, total int64, err error) { + tx := applyLogFilter(applyLogTypeFilter(LOG_DB.Where("logs.user_id = ?", userId), logType), logFilter) if tx, err = applyExplicitLogTextFilter(tx, "logs.model_name", modelName); err != nil { return nil, 0, err @@ -635,12 +651,15 @@ type Stat struct { Tpm int `json:"tpm"` } -func SumUsedQuota(logType int, startTimestamp int64, endTimestamp int64, modelName string, username string, tokenName string, channel int, group string) (stat Stat, err error) { +func SumUsedQuota(logType int, logFilter string, startTimestamp int64, endTimestamp int64, modelName string, username string, tokenName string, channel int, group string) (stat Stat, err error) { tx := LOG_DB.Table("logs").Select("sum(quota) quota") // 为rpm和tpm创建单独的查询 rpmTpmQuery := LOG_DB.Table("logs").Select("count(*) rpm, sum(prompt_tokens) + sum(completion_tokens) tpm") + tx = applyLogFilter(tx, logFilter) + rpmTpmQuery = applyLogFilter(rpmTpmQuery, logFilter) + if tx, err = applyExplicitLogTextFilter(tx, "username", username); err != nil { return stat, err } diff --git a/model/log_test.go b/model/log_test.go index 335b7ef..c7fa614 100644 --- a/model/log_test.go +++ b/model/log_test.go @@ -19,6 +19,67 @@ func withLogAuditSettings(t *testing.T, requestEnabled bool, responseEnabled boo }) } +func TestGetAllLogsRetryFilter(t *testing.T) { + require.NoError(t, LOG_DB.Where("1 = 1").Delete(&Log{}).Error) + t.Cleanup(func() { + require.NoError(t, LOG_DB.Where("1 = 1").Delete(&Log{}).Error) + }) + + logs := []Log{ + { + UserId: 1, + CreatedAt: 10, + Type: LogTypeConsume, + Other: common.MapToJsonStr(map[string]interface{}{ + "retry_log": true, + "admin_info": map[string]interface{}{ + "use_channel": []string{"1", "2"}, + }, + }), + }, + { + UserId: 1, + CreatedAt: 20, + Type: LogTypeConsume, + Other: common.MapToJsonStr(map[string]interface{}{ + "empty_retry": true, + "admin_info": map[string]interface{}{ + "use_channel": []string{"3"}, + }, + }), + }, + { + UserId: 1, + CreatedAt: 30, + Type: LogTypeConsume, + Other: common.MapToJsonStr(map[string]interface{}{ + "admin_info": map[string]interface{}{ + "use_channel": []string{"4"}, + }, + "empty_output_types": []string{"message", "function_call"}, + }), + }, + { + UserId: 1, + CreatedAt: 40, + Type: LogTypeConsume, + Other: common.MapToJsonStr(map[string]interface{}{ + "admin_info": map[string]interface{}{ + "use_channel": []string{"5"}, + }, + }), + }, + } + require.NoError(t, LOG_DB.Create(&logs).Error) + + got, total, err := GetAllLogs(LogTypeUnknown, LogFilterRetry, 0, 0, "", "", "", 0, 10, 0, "", "", "") + require.NoError(t, err) + require.EqualValues(t, 2, total) + require.Len(t, got, 2) + require.Equal(t, logs[1].Id, got[0].Id) + require.Equal(t, logs[0].Id, got[1].Id) +} + func TestFormatUserLogDetailExposesOnlyUserAuditContent(t *testing.T) { withLogAuditSettings(t, true, true) diff --git a/service/log_info_generate.go b/service/log_info_generate.go index 4f67560..147e9fa 100644 --- a/service/log_info_generate.go +++ b/service/log_info_generate.go @@ -58,7 +58,11 @@ func GenerateTextOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, m } adminInfo := make(map[string]interface{}) - adminInfo["use_channel"] = ctx.GetStringSlice("use_channel") + useChannel := ctx.GetStringSlice("use_channel") + adminInfo["use_channel"] = useChannel + if len(useChannel) > 1 { + other["retry_log"] = true + } isMultiKey := common.GetContextKeyBool(ctx, constant.ContextKeyChannelIsMultiKey) if isMultiKey { adminInfo["is_multi_key"] = true diff --git a/service/log_info_generate_test.go b/service/log_info_generate_test.go new file mode 100644 index 0000000..1641c8e --- /dev/null +++ b/service/log_info_generate_test.go @@ -0,0 +1,50 @@ +package service + +import ( + "net/http/httptest" + "testing" + "time" + + relaycommon "github.com/MAX-API-Next/MAX-API/relay/common" + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/require" +) + +func TestGenerateTextOtherInfoMarksRetryLog(t *testing.T) { + gin.SetMode(gin.TestMode) + rec := httptest.NewRecorder() + ctx, _ := gin.CreateTestContext(rec) + ctx.Set("use_channel", []string{"1", "2"}) + + now := time.Now() + info := &relaycommon.RelayInfo{ + StartTime: now, + FirstResponseTime: now, + ChannelMeta: &relaycommon.ChannelMeta{}, + } + + other := GenerateTextOtherInfo(ctx, info, 1, 1, 1, 0, 0, 0, 1) + + require.Equal(t, true, other["retry_log"]) + adminInfo, ok := other["admin_info"].(map[string]interface{}) + require.True(t, ok) + require.Equal(t, []string{"1", "2"}, adminInfo["use_channel"]) +} + +func TestGenerateTextOtherInfoDoesNotMarkSingleChannelAsRetry(t *testing.T) { + gin.SetMode(gin.TestMode) + rec := httptest.NewRecorder() + ctx, _ := gin.CreateTestContext(rec) + ctx.Set("use_channel", []string{"1"}) + + now := time.Now() + info := &relaycommon.RelayInfo{ + StartTime: now, + FirstResponseTime: now, + ChannelMeta: &relaycommon.ChannelMeta{}, + } + + other := GenerateTextOtherInfo(ctx, info, 1, 1, 1, 0, 0, 0, 1) + + require.NotContains(t, other, "retry_log") +} diff --git a/web/default/src/features/usage-logs/components/columns/common-logs-columns.tsx b/web/default/src/features/usage-logs/components/columns/common-logs-columns.tsx index 589746d..7a5018c 100644 --- a/web/default/src/features/usage-logs/components/columns/common-logs-columns.tsx +++ b/web/default/src/features/usage-logs/components/columns/common-logs-columns.tsx @@ -37,7 +37,7 @@ import { } from '@/components/ui/tooltip' import { DataTableColumnHeader } from '@/components/data-table' import { StatusBadge, type StatusBadgeProps } from '@/components/status-badge' -import { LOG_TYPE_ALL_VALUE } from '../../constants' +import { LOG_TYPE_ALL_VALUE, LOG_TYPE_RETRY_VALUE } from '../../constants' import { formatModelName, getFirstResponseTimeColor, @@ -296,6 +296,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef[] { filterFn: (row, _id, value) => { if (!Array.isArray(value) || value.length === 0) return true if (value.includes(LOG_TYPE_ALL_VALUE)) return true + if (value.includes(LOG_TYPE_RETRY_VALUE)) return true return value.includes(String(row.original.type)) }, enableHiding: false, diff --git a/web/default/src/features/usage-logs/components/common-logs-filter-bar.tsx b/web/default/src/features/usage-logs/components/common-logs-filter-bar.tsx index 84e1cc1..237dd13 100644 --- a/web/default/src/features/usage-logs/components/common-logs-filter-bar.tsx +++ b/web/default/src/features/usage-logs/components/common-logs-filter-bar.tsx @@ -39,7 +39,11 @@ import { TooltipTrigger, } from '@/components/ui/tooltip' import { StatusBadge } from '@/components/status-badge' -import { LOG_TYPE_ALL_VALUE, LOG_TYPE_FILTERS } from '../constants' +import { + LOG_TYPE_ALL_VALUE, + LOG_TYPE_FILTERS, + LOG_TYPE_RETRY_VALUE, +} from '../constants' import { buildSearchParams } from '../lib/filter' import { getDefaultTimeRange } from '../lib/utils' import type { CommonLogFilters } from '../types' @@ -53,7 +57,17 @@ import { import { useUsageLogsContext } from './usage-logs-provider' const route = getRouteApi('/_authenticated/usage-logs/$section') -const logTypeValues = ['0', '1', '2', '3', '4', '5', '6', '7'] as const +const logTypeValues = [ + '0', + LOG_TYPE_RETRY_VALUE, + '1', + '2', + '3', + '4', + '5', + '6', + '7', +] as const type LogTypeValue = (typeof logTypeValues)[number] diff --git a/web/default/src/features/usage-logs/constants.ts b/web/default/src/features/usage-logs/constants.ts index b564add..281c0da 100644 --- a/web/default/src/features/usage-logs/constants.ts +++ b/web/default/src/features/usage-logs/constants.ts @@ -66,6 +66,7 @@ export const LOG_TYPE_ENUM = { * Row rendering still displays records with type=0 as "Unknown". */ export const LOG_TYPE_ALL_VALUE = '0' as const +export const LOG_TYPE_RETRY_VALUE = 'retry' as const // ============================================================================ // Time Range Presets @@ -106,6 +107,7 @@ export const LOG_TYPES = [ */ export const LOG_TYPE_FILTERS = [ { label: 'All Types', value: LOG_TYPE_ALL_VALUE }, + { label: 'Retry', value: LOG_TYPE_RETRY_VALUE }, ...LOG_TYPES.filter((type) => type.value !== LOG_TYPE_ENUM.UNKNOWN).map( (type) => ({ label: type.label, diff --git a/web/default/src/features/usage-logs/lib/utils.ts b/web/default/src/features/usage-logs/lib/utils.ts index c5b705f..95e9bd6 100644 --- a/web/default/src/features/usage-logs/lib/utils.ts +++ b/web/default/src/features/usage-logs/lib/utils.ts @@ -31,6 +31,7 @@ import { LOG_TYPES, DISPLAYABLE_LOG_TYPES, TIMING_LOG_TYPES, + LOG_TYPE_RETRY_VALUE, } from '../constants' import type { GetLogsParams, @@ -180,6 +181,17 @@ export function buildApiParams(config: { }): GetLogsParams { const { page, pageSize, searchParams, columnFilters = [], isAdmin } = config + const applyTypeFilter = (params: GetLogsParams, value: unknown) => { + const typeValue = Array.isArray(value) && value.length === 1 ? value[0] : value + if (typeValue === LOG_TYPE_RETRY_VALUE) { + params.log_filter = LOG_TYPE_RETRY_VALUE + params.type = undefined + return + } + params.type = processType(value) + params.log_filter = undefined + } + // Helper to process type parameter (single value from array) const processType = (value: unknown): number | undefined => { const parseType = (raw: unknown): number | undefined => { @@ -200,7 +212,6 @@ export function buildApiParams(config: { const params: GetLogsParams = { p: page, page_size: pageSize, - ...(searchParams.type ? { type: processType(searchParams.type) } : {}), ...(searchParams.model ? { model_name: String(searchParams.model) } : {}), ...(searchParams.token ? { token_name: String(searchParams.token) } : {}), ...(searchParams.group ? { group: String(searchParams.group) } : {}), @@ -218,6 +229,9 @@ export function buildApiParams(config: { : {}), ...buildTimeRangeParams(searchParams, false), } + if (searchParams.type) { + applyTypeFilter(params, searchParams.type) + } // Override with column filters if present if (columnFilters.length > 0) { @@ -226,7 +240,7 @@ export function buildApiParams(config: { switch (id) { case 'type': - params.type = processType(value) + applyTypeFilter(params, value) break case 'model_name': params.model_name = String(value) diff --git a/web/default/src/features/usage-logs/types.ts b/web/default/src/features/usage-logs/types.ts index cac1452..23a0ae0 100644 --- a/web/default/src/features/usage-logs/types.ts +++ b/web/default/src/features/usage-logs/types.ts @@ -300,6 +300,7 @@ export interface GetLogsParams { p?: number page_size?: number type?: number + log_filter?: string username?: string token_name?: string model_name?: string @@ -330,6 +331,7 @@ export interface GetLogDetailResponse { export interface GetLogStatsParams { type?: number + log_filter?: string username?: string token_name?: string model_name?: string diff --git a/web/default/src/routes/_authenticated/usage-logs/$section.tsx b/web/default/src/routes/_authenticated/usage-logs/$section.tsx index b378934..3ff2536 100644 --- a/web/default/src/routes/_authenticated/usage-logs/$section.tsx +++ b/web/default/src/routes/_authenticated/usage-logs/$section.tsx @@ -24,7 +24,7 @@ import { USAGE_LOGS_DEFAULT_SECTION, } from '@/features/usage-logs/section-registry' -const logTypeValues = ['0', '1', '2', '3', '4', '5', '6', '7'] as const +const logTypeValues = ['0', 'retry', '1', '2', '3', '4', '5', '6', '7'] as const const logTypeSearchSchema = z .preprocess( (value) => { From 50d291721a86c8341089ab7756f45f9199b65aba Mon Sep 17 00:00:00 2001 From: CSCITech Date: Sun, 28 Jun 2026 02:03:06 +0800 Subject: [PATCH 3/4] fix some bugs --- .../columns/common-logs-columns.tsx | 7 +- .../src/features/usage-logs/lib/utils.test.ts | 125 ++++++++++++++++++ .../src/features/usage-logs/lib/utils.ts | 42 +++++- web/default/src/features/usage-logs/types.ts | 2 + 4 files changed, 168 insertions(+), 8 deletions(-) create mode 100644 web/default/src/features/usage-logs/lib/utils.test.ts diff --git a/web/default/src/features/usage-logs/components/columns/common-logs-columns.tsx b/web/default/src/features/usage-logs/components/columns/common-logs-columns.tsx index 7a5018c..e5ed50e 100644 --- a/web/default/src/features/usage-logs/components/columns/common-logs-columns.tsx +++ b/web/default/src/features/usage-logs/components/columns/common-logs-columns.tsx @@ -37,7 +37,6 @@ import { } from '@/components/ui/tooltip' import { DataTableColumnHeader } from '@/components/data-table' import { StatusBadge, type StatusBadgeProps } from '@/components/status-badge' -import { LOG_TYPE_ALL_VALUE, LOG_TYPE_RETRY_VALUE } from '../../constants' import { formatModelName, getFirstResponseTimeColor, @@ -53,6 +52,7 @@ import { isTimingLogType, getLogTypeConfig, isPerCallBilling, + matchesCommonLogTypeFilter, } from '../../lib/utils' import type { LogOtherData, UsageLog } from '../../types' import { DetailsDialog } from '../dialogs/details-dialog' @@ -294,10 +294,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef[] { ) }, filterFn: (row, _id, value) => { - if (!Array.isArray(value) || value.length === 0) return true - if (value.includes(LOG_TYPE_ALL_VALUE)) return true - if (value.includes(LOG_TYPE_RETRY_VALUE)) return true - return value.includes(String(row.original.type)) + return matchesCommonLogTypeFilter(row.original, value) }, enableHiding: false, meta: { label: t('Time') }, diff --git a/web/default/src/features/usage-logs/lib/utils.test.ts b/web/default/src/features/usage-logs/lib/utils.test.ts new file mode 100644 index 0000000..09a5d83 --- /dev/null +++ b/web/default/src/features/usage-logs/lib/utils.test.ts @@ -0,0 +1,125 @@ +/* +Copyright (C) 2023-2026 MAX-API-Next + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact https://github.com/MAX-API-Next/MAX-API/issues +*/ +import assert from 'node:assert/strict' +import { describe, test } from 'node:test' +import { buildApiParams, matchesCommonLogTypeFilter } from './utils' + +describe('buildApiParams', () => { + test('treats mixed retry and numeric type filters as retry filters', () => { + const params = buildApiParams({ + page: 1, + pageSize: 100, + searchParams: { type: ['retry', '1'] }, + isAdmin: true, + }) + + assert.equal(params.log_filter, 'retry') + assert.equal(params.type, undefined) + }) + + test('treats mixed retry and numeric column filters as retry filters', () => { + const params = buildApiParams({ + page: 1, + pageSize: 100, + searchParams: {}, + columnFilters: [{ id: 'type', value: ['retry', '1'] }], + isAdmin: true, + }) + + assert.equal(params.log_filter, 'retry') + assert.equal(params.type, undefined) + }) + + test('ignores blank type filter array entries', () => { + const blankParams = buildApiParams({ + page: 1, + pageSize: 100, + searchParams: { type: [''] }, + isAdmin: true, + }) + assert.equal(blankParams.type, undefined) + assert.equal(blankParams.log_filter, undefined) + + const numericParams = buildApiParams({ + page: 1, + pageSize: 100, + searchParams: { type: ['', '2'] }, + isAdmin: true, + }) + assert.equal(numericParams.type, 2) + assert.equal(numericParams.log_filter, undefined) + }) + + test('treats all as match-all before retry filters', () => { + for (const type of [ + ['0', 'retry'], + ['all', 'retry'], + ]) { + const params = buildApiParams({ + page: 1, + pageSize: 100, + searchParams: { type }, + isAdmin: true, + }) + + assert.equal(params.type, undefined) + assert.equal(params.log_filter, undefined) + } + }) +}) + +describe('matchesCommonLogTypeFilter', () => { + test('matches retry filters against row retry markers', () => { + assert.equal( + matchesCommonLogTypeFilter( + { type: 2, other: JSON.stringify({ retry_log: true }) }, + ['retry'] + ), + true + ) + assert.equal( + matchesCommonLogTypeFilter( + { type: 2, other: JSON.stringify({ empty_retry: true }) }, + ['retry'] + ), + true + ) + assert.equal( + matchesCommonLogTypeFilter( + { type: 2, other: JSON.stringify({ admin_info: { use_channel: ['1'] } }) }, + ['retry'] + ), + false + ) + }) + + test('gives retry precedence for mixed retry and numeric filters', () => { + assert.equal( + matchesCommonLogTypeFilter({ type: 1, other: '{}' }, ['retry', '1']), + false + ) + assert.equal( + matchesCommonLogTypeFilter( + { type: 1, other: JSON.stringify({ retry_log: true }) }, + ['retry', '1'] + ), + true + ) + }) +}) diff --git a/web/default/src/features/usage-logs/lib/utils.ts b/web/default/src/features/usage-logs/lib/utils.ts index 95e9bd6..ca9fc07 100644 --- a/web/default/src/features/usage-logs/lib/utils.ts +++ b/web/default/src/features/usage-logs/lib/utils.ts @@ -28,6 +28,7 @@ import { getUserTaskLogs, } from '../api' import { + LOG_TYPE_ALL_VALUE, LOG_TYPES, DISPLAYABLE_LOG_TYPES, TIMING_LOG_TYPES, @@ -40,6 +41,7 @@ import type { GetMidjourneyLogsParams, GetTaskLogsParams, } from '../types' +import { parseLogOther } from './format' // ============================================================================ // Type Checkers & Utilities @@ -93,6 +95,25 @@ function timestampToSeconds(ms: number): number { return Math.floor(ms / 1000) } +export function matchesCommonLogTypeFilter( + log: { type: number; other: string }, + value: unknown +): boolean { + if (!Array.isArray(value) || value.length === 0) return true + + const filterValues = value + .map((item) => String(item).trim()) + .filter((item) => item !== '') + + if (filterValues.length === 0) return true + if (filterValues.includes(LOG_TYPE_ALL_VALUE)) return true + if (filterValues.includes(LOG_TYPE_RETRY_VALUE)) { + const other = parseLogOther(log.other) + return other?.retry_log === true || other?.empty_retry === true + } + return filterValues.includes(String(log.type)) +} + /** * Build query parameters from filters */ @@ -182,13 +203,28 @@ export function buildApiParams(config: { const { page, pageSize, searchParams, columnFilters = [], isAdmin } = config const applyTypeFilter = (params: GetLogsParams, value: unknown) => { - const typeValue = Array.isArray(value) && value.length === 1 ? value[0] : value - if (typeValue === LOG_TYPE_RETRY_VALUE) { + const typeValues = (Array.isArray(value) ? value : [value]) + .map((item) => String(item).trim()) + .filter((item) => item !== '') + + if (typeValues.length === 0) { + params.type = undefined + params.log_filter = undefined + return + } + + if (typeValues.includes(LOG_TYPE_ALL_VALUE) || typeValues.includes('all')) { + params.type = undefined + params.log_filter = undefined + return + } + + if (typeValues.includes(LOG_TYPE_RETRY_VALUE)) { params.log_filter = LOG_TYPE_RETRY_VALUE params.type = undefined return } - params.type = processType(value) + params.type = processType(typeValues) params.log_filter = undefined } diff --git a/web/default/src/features/usage-logs/types.ts b/web/default/src/features/usage-logs/types.ts index 23a0ae0..0ccc93e 100644 --- a/web/default/src/features/usage-logs/types.ts +++ b/web/default/src/features/usage-logs/types.ts @@ -128,6 +128,8 @@ export interface LogAuditInfo { } export interface LogOtherData { + retry_log?: boolean + empty_retry?: boolean op?: { action?: string params?: Record From 2db0d1e83a0a71749031780b71afbca4a083638a Mon Sep 17 00:00:00 2001 From: CSCITech Date: Sun, 28 Jun 2026 02:51:45 +0800 Subject: [PATCH 4/4] fix some bugs --- controller/relay.go | 6 +- model/log_test.go | 86 +++++++++++++++---- service/log_info_generate.go | 17 ++-- .../components/common-logs-filter-bar.tsx | 17 +--- .../src/features/usage-logs/constants.ts | 4 + .../_authenticated/usage-logs/$section.tsx | 4 +- 6 files changed, 90 insertions(+), 44 deletions(-) diff --git a/controller/relay.go b/controller/relay.go index 8d8db3e..409e5b5 100644 --- a/controller/relay.go +++ b/controller/relay.go @@ -387,11 +387,7 @@ func processChannelError(c *gin.Context, channelError types.ChannelError, err *t other["channel_name"] = c.GetString("channel_name") other["channel_type"] = c.GetInt("channel_type") adminInfo := make(map[string]interface{}) - useChannel := c.GetStringSlice("use_channel") - adminInfo["use_channel"] = useChannel - if len(useChannel) > 1 { - other["retry_log"] = true - } + service.AppendRetryLogInfo(c, adminInfo, other) isMultiKey := common.GetContextKeyBool(c, constant.ContextKeyChannelIsMultiKey) if isMultiKey { adminInfo["is_multi_key"] = true diff --git a/model/log_test.go b/model/log_test.go index c7fa614..eab8127 100644 --- a/model/log_test.go +++ b/model/log_test.go @@ -2,6 +2,7 @@ package model import ( "testing" + "time" "github.com/MAX-API-Next/MAX-API/common" "github.com/stretchr/testify/require" @@ -25,11 +26,56 @@ func TestGetAllLogsRetryFilter(t *testing.T) { require.NoError(t, LOG_DB.Where("1 = 1").Delete(&Log{}).Error) }) + logs := createRetryFilterLogs(t) + + got, total, err := GetAllLogs(LogTypeUnknown, LogFilterRetry, 0, 0, "", "", "", 0, 10, 0, "", "", "") + require.NoError(t, err) + require.EqualValues(t, 2, total) + require.Len(t, got, 2) + require.ElementsMatch(t, []int{logs[0].Id, logs[1].Id}, []int{got[0].Id, got[1].Id}) +} + +func TestGetUserLogsRetryFilter(t *testing.T) { + require.NoError(t, LOG_DB.Where("1 = 1").Delete(&Log{}).Error) + t.Cleanup(func() { + require.NoError(t, LOG_DB.Where("1 = 1").Delete(&Log{}).Error) + }) + + logs := createRetryFilterLogs(t) + + got, total, err := GetUserLogs(1, LogTypeUnknown, LogFilterRetry, 0, 0, "", "", 0, 10, "", "", "") + require.NoError(t, err) + require.EqualValues(t, 2, total) + require.Len(t, got, 2) + require.ElementsMatch(t, []int{logs[0].Id, logs[1].Id}, []int{got[0].LogId, got[1].LogId}) +} + +func TestSumUsedQuotaRetryFilter(t *testing.T) { + require.NoError(t, LOG_DB.Where("1 = 1").Delete(&Log{}).Error) + t.Cleanup(func() { + require.NoError(t, LOG_DB.Where("1 = 1").Delete(&Log{}).Error) + }) + + createRetryFilterLogs(t) + + stat, err := SumUsedQuota(LogTypeUnknown, LogFilterRetry, 0, 0, "", "", "", 0, "") + require.NoError(t, err) + require.Equal(t, 300, stat.Quota) + require.Equal(t, 2, stat.Rpm) + require.Equal(t, 30, stat.Tpm) +} + +func createRetryFilterLogs(t *testing.T) []Log { + t.Helper() + logs := []Log{ { - UserId: 1, - CreatedAt: 10, - Type: LogTypeConsume, + UserId: 1, + CreatedAt: time.Now().Unix() - 10, + Type: LogTypeConsume, + Quota: 100, + PromptTokens: 5, + CompletionTokens: 10, Other: common.MapToJsonStr(map[string]interface{}{ "retry_log": true, "admin_info": map[string]interface{}{ @@ -38,9 +84,12 @@ func TestGetAllLogsRetryFilter(t *testing.T) { }), }, { - UserId: 1, - CreatedAt: 20, - Type: LogTypeConsume, + UserId: 1, + CreatedAt: time.Now().Unix() - 20, + Type: LogTypeConsume, + Quota: 200, + PromptTokens: 7, + CompletionTokens: 8, Other: common.MapToJsonStr(map[string]interface{}{ "empty_retry": true, "admin_info": map[string]interface{}{ @@ -49,9 +98,12 @@ func TestGetAllLogsRetryFilter(t *testing.T) { }), }, { - UserId: 1, - CreatedAt: 30, - Type: LogTypeConsume, + UserId: 1, + CreatedAt: time.Now().Unix() - 30, + Type: LogTypeConsume, + Quota: 400, + PromptTokens: 11, + CompletionTokens: 13, Other: common.MapToJsonStr(map[string]interface{}{ "admin_info": map[string]interface{}{ "use_channel": []string{"4"}, @@ -60,9 +112,12 @@ func TestGetAllLogsRetryFilter(t *testing.T) { }), }, { - UserId: 1, - CreatedAt: 40, - Type: LogTypeConsume, + UserId: 2, + CreatedAt: time.Now().Unix() - 40, + Type: LogTypeConsume, + Quota: 800, + PromptTokens: 17, + CompletionTokens: 19, Other: common.MapToJsonStr(map[string]interface{}{ "admin_info": map[string]interface{}{ "use_channel": []string{"5"}, @@ -72,12 +127,7 @@ func TestGetAllLogsRetryFilter(t *testing.T) { } require.NoError(t, LOG_DB.Create(&logs).Error) - got, total, err := GetAllLogs(LogTypeUnknown, LogFilterRetry, 0, 0, "", "", "", 0, 10, 0, "", "", "") - require.NoError(t, err) - require.EqualValues(t, 2, total) - require.Len(t, got, 2) - require.Equal(t, logs[1].Id, got[0].Id) - require.Equal(t, logs[0].Id, got[1].Id) + return logs } func TestFormatUserLogDetailExposesOnlyUserAuditContent(t *testing.T) { diff --git a/service/log_info_generate.go b/service/log_info_generate.go index 147e9fa..cebecc6 100644 --- a/service/log_info_generate.go +++ b/service/log_info_generate.go @@ -33,6 +33,17 @@ func appendRequestPath(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, other } } +func AppendRetryLogInfo(ctx *gin.Context, adminInfo map[string]interface{}, other map[string]interface{}) { + if ctx == nil || adminInfo == nil || other == nil { + return + } + useChannel := ctx.GetStringSlice("use_channel") + adminInfo["use_channel"] = useChannel + if len(useChannel) > 1 { + other["retry_log"] = true + } +} + func GenerateTextOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, modelRatio, groupRatio, completionRatio float64, cacheTokens int, cacheRatio float64, modelPrice float64, userGroupRatio float64) map[string]interface{} { other := make(map[string]interface{}) @@ -58,11 +69,7 @@ func GenerateTextOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, m } adminInfo := make(map[string]interface{}) - useChannel := ctx.GetStringSlice("use_channel") - adminInfo["use_channel"] = useChannel - if len(useChannel) > 1 { - other["retry_log"] = true - } + AppendRetryLogInfo(ctx, adminInfo, other) isMultiKey := common.GetContextKeyBool(ctx, constant.ContextKeyChannelIsMultiKey) if isMultiKey { adminInfo["is_multi_key"] = true diff --git a/web/default/src/features/usage-logs/components/common-logs-filter-bar.tsx b/web/default/src/features/usage-logs/components/common-logs-filter-bar.tsx index 237dd13..7521138 100644 --- a/web/default/src/features/usage-logs/components/common-logs-filter-bar.tsx +++ b/web/default/src/features/usage-logs/components/common-logs-filter-bar.tsx @@ -41,8 +41,8 @@ import { import { StatusBadge } from '@/components/status-badge' import { LOG_TYPE_ALL_VALUE, + LOG_TYPE_FILTER_VALUES, LOG_TYPE_FILTERS, - LOG_TYPE_RETRY_VALUE, } from '../constants' import { buildSearchParams } from '../lib/filter' import { getDefaultTimeRange } from '../lib/utils' @@ -57,22 +57,11 @@ import { import { useUsageLogsContext } from './usage-logs-provider' const route = getRouteApi('/_authenticated/usage-logs/$section') -const logTypeValues = [ - '0', - LOG_TYPE_RETRY_VALUE, - '1', - '2', - '3', - '4', - '5', - '6', - '7', -] as const -type LogTypeValue = (typeof logTypeValues)[number] +type LogTypeValue = (typeof LOG_TYPE_FILTER_VALUES)[number] function isLogTypeValue(value: string): value is LogTypeValue { - return (logTypeValues as readonly string[]).includes(value) + return LOG_TYPE_FILTER_VALUES.includes(value) } interface CommonLogsFilterBarProps { diff --git a/web/default/src/features/usage-logs/constants.ts b/web/default/src/features/usage-logs/constants.ts index 281c0da..8b93121 100644 --- a/web/default/src/features/usage-logs/constants.ts +++ b/web/default/src/features/usage-logs/constants.ts @@ -116,6 +116,10 @@ export const LOG_TYPE_FILTERS = [ ), ] as const +export const LOG_TYPE_FILTER_VALUES = LOG_TYPE_FILTERS.map( + (type) => type.value +) as [string, ...string[]] + // ============================================================================ // Drawing Logs (Midjourney) Constants // ============================================================================ diff --git a/web/default/src/routes/_authenticated/usage-logs/$section.tsx b/web/default/src/routes/_authenticated/usage-logs/$section.tsx index 3ff2536..1542565 100644 --- a/web/default/src/routes/_authenticated/usage-logs/$section.tsx +++ b/web/default/src/routes/_authenticated/usage-logs/$section.tsx @@ -19,19 +19,19 @@ For commercial licensing, please contact https://github.com/MAX-API-Next/MAX-API import z from 'zod' import { createFileRoute, redirect } from '@tanstack/react-router' import { UsageLogs } from '@/features/usage-logs' +import { LOG_TYPE_FILTER_VALUES } from '@/features/usage-logs/constants' import { isUsageLogsSectionId, USAGE_LOGS_DEFAULT_SECTION, } from '@/features/usage-logs/section-registry' -const logTypeValues = ['0', 'retry', '1', '2', '3', '4', '5', '6', '7'] as const const logTypeSearchSchema = z .preprocess( (value) => { if (value == null || value === '') return undefined return Array.isArray(value) ? value : [value] }, - z.array(z.enum(logTypeValues)).optional() + z.array(z.enum(LOG_TYPE_FILTER_VALUES)).optional() ) .catch([])