diff --git a/constant/context_key.go b/constant/context_key.go index 833aabae1..b494f3685 100644 --- a/constant/context_key.go +++ b/constant/context_key.go @@ -55,4 +55,8 @@ const ( ContextKeyLocalCountTokens ContextKey = "local_count_tokens" ContextKeySystemPromptOverride ContextKey = "system_prompt_override" + + // ContextKeyAdminRejectReason stores an admin-only reject/block reason extracted from upstream responses. + // It is not returned to end users, but can be persisted into consume/error logs for debugging. + ContextKeyAdminRejectReason ContextKey = "admin_reject_reason" ) diff --git a/model/log.go b/model/log.go index f8940c150..872d73d4f 100644 --- a/model/log.go +++ b/model/log.go @@ -59,6 +59,7 @@ func formatUserLogs(logs []*Log) { // Remove admin-only debug fields. delete(otherMap, "admin_info") delete(otherMap, "request_conversion") + delete(otherMap, "reject_reason") } logs[i].Other = common.MapToJsonStr(otherMap) logs[i].Id = logs[i].Id % 1024 diff --git a/relay/channel/gemini/relay-gemini-native.go b/relay/channel/gemini/relay-gemini-native.go index 5f9ff7cdf..cd9d06db2 100644 --- a/relay/channel/gemini/relay-gemini-native.go +++ b/relay/channel/gemini/relay-gemini-native.go @@ -1,10 +1,12 @@ package gemini import ( + "fmt" "io" "net/http" "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/constant" "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/logger" relaycommon "github.com/QuantumNous/new-api/relay/common" @@ -35,6 +37,10 @@ func GeminiTextGenerationHandler(c *gin.Context, info *relaycommon.RelayInfo, re return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError) } + if len(geminiResponse.Candidates) == 0 && geminiResponse.PromptFeedback != nil && geminiResponse.PromptFeedback.BlockReason != nil { + common.SetContextKey(c, constant.ContextKeyAdminRejectReason, fmt.Sprintf("gemini_block_reason=%s", *geminiResponse.PromptFeedback.BlockReason)) + } + // 计算使用量(基于 UsageMetadata) usage := dto.Usage{ PromptTokens: geminiResponse.UsageMetadata.PromptTokenCount, diff --git a/relay/channel/gemini/relay-gemini.go b/relay/channel/gemini/relay-gemini.go index 22ddfaeaa..77e335c5b 100644 --- a/relay/channel/gemini/relay-gemini.go +++ b/relay/channel/gemini/relay-gemini.go @@ -1197,6 +1197,10 @@ func geminiStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http return false } + if len(geminiResponse.Candidates) == 0 && geminiResponse.PromptFeedback != nil && geminiResponse.PromptFeedback.BlockReason != nil { + common.SetContextKey(c, constant.ContextKeyAdminRejectReason, fmt.Sprintf("gemini_block_reason=%s", *geminiResponse.PromptFeedback.BlockReason)) + } + // 统计图片数量 for _, candidate := range geminiResponse.Candidates { for _, part := range candidate.Content.Parts { @@ -1372,12 +1376,14 @@ func GeminiChatHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.R var newAPIError *types.NewAPIError if geminiResponse.PromptFeedback != nil && geminiResponse.PromptFeedback.BlockReason != nil { + common.SetContextKey(c, constant.ContextKeyAdminRejectReason, fmt.Sprintf("gemini_block_reason=%s", *geminiResponse.PromptFeedback.BlockReason)) newAPIError = types.NewOpenAIError( errors.New("request blocked by Gemini API: "+*geminiResponse.PromptFeedback.BlockReason), types.ErrorCodePromptBlocked, http.StatusBadRequest, ) } else { + common.SetContextKey(c, constant.ContextKeyAdminRejectReason, "gemini_empty_candidates") newAPIError = types.NewOpenAIError( errors.New("empty response from Gemini API"), types.ErrorCodeEmptyResponse, diff --git a/relay/channel/openai/relay-openai.go b/relay/channel/openai/relay-openai.go index a4c6ef605..a4de16112 100644 --- a/relay/channel/openai/relay-openai.go +++ b/relay/channel/openai/relay-openai.go @@ -229,6 +229,13 @@ func OpenaiHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Respo return nil, types.WithOpenAIError(*oaiError, resp.StatusCode) } + for _, choice := range simpleResponse.Choices { + if choice.FinishReason == constant.FinishReasonContentFilter { + common.SetContextKey(c, constant.ContextKeyAdminRejectReason, "openai_finish_reason=content_filter") + break + } + } + forceFormat := false if info.ChannelSetting.ForceFormat { forceFormat = true diff --git a/relay/compatible_handler.go b/relay/compatible_handler.go index eab5052d7..5792715b2 100644 --- a/relay/compatible_handler.go +++ b/relay/compatible_handler.go @@ -237,6 +237,9 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usage } extraContent = append(extraContent, "上游无计费信息") } + + adminRejectReason := common.GetContextKeyString(ctx, constant.ContextKeyAdminRejectReason) + useTimeSeconds := time.Now().Unix() - relayInfo.StartTime.Unix() promptTokens := usage.PromptTokens cacheTokens := usage.PromptTokensDetails.CachedTokens @@ -461,6 +464,9 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usage } logContent := strings.Join(extraContent, ", ") other := service.GenerateTextOtherInfo(ctx, relayInfo, modelRatio, groupRatio, completionRatio, cacheTokens, cacheRatio, modelPrice, relayInfo.PriceData.GroupRatioInfo.GroupSpecialRatio) + if adminRejectReason != "" { + other["reject_reason"] = adminRejectReason + } // For chat-based calls to the Claude model, tagging is required. Using Claude's rendering logs, the two approaches handle input rendering differently. if isClaudeUsageSemantic { other["claude"] = true diff --git a/web/src/components/table/usage-logs/UsageLogsColumnDefs.jsx b/web/src/components/table/usage-logs/UsageLogsColumnDefs.jsx index b3096c286..84e57ea6f 100644 --- a/web/src/components/table/usage-logs/UsageLogsColumnDefs.jsx +++ b/web/src/components/table/usage-logs/UsageLogsColumnDefs.jsx @@ -578,6 +578,9 @@ export const getLogsColumns = ({ other?.is_system_prompt_overwritten, 'openai', ); + if (isAdminUser && other?.reject_reason) { + content += `\nBlock reason: ${other.reject_reason}`; + } return (