package service import ( "fmt" "strings" "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/model" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) // resolveConsumeLogChannelDiscountPercent 返回与实扣额度一致的渠道价格折扣百分数(100=无折扣)。 func resolveConsumeLogChannelDiscountPercent(relayInfo *relaycommon.RelayInfo) float64 { if relayInfo == nil { return 100 } if relayInfo.PriceData.ChannelPriceDiscount != nil { return *relayInfo.PriceData.ChannelPriceDiscount } chID := 0 if relayInfo.ChannelMeta != nil { chID = relayInfo.ChannelId } return model.ResolveChannelPriceDiscountPercent(chID) } // appendChannelPriceDiscountToConsumeOther 写入 channel_price_discount_percent、markup_discount_rate 及全局倍率/固定价,供前端展示与实扣对齐。 func appendChannelPriceDiscountToConsumeOther(relayInfo *relaycommon.RelayInfo, other map[string]interface{}) { if other == nil { return } other["channel_price_discount_percent"] = resolveConsumeLogChannelDiscountPercent(relayInfo) if relayInfo != nil { other["markup_discount_rate"] = relayInfo.PriceData.MarkupDiscountPercent other["global_model_ratio"] = relayInfo.PriceData.GlobalModelRatio other["global_model_price"] = relayInfo.PriceData.GlobalModelPrice other["global_completion_ratio"] = relayInfo.PriceData.GlobalCompletionRatio other["global_cache_ratio"] = relayInfo.PriceData.GlobalCacheRatio other["global_create_cache_ratio"] = relayInfo.PriceData.GlobalCreateCacheRatio } } func appendRequestPath(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, other map[string]interface{}) { if other == nil { return } if ctx != nil && ctx.Request != nil && ctx.Request.URL != nil { if path := ctx.Request.URL.Path; path != "" { other["request_path"] = path return } } if relayInfo != nil && relayInfo.RequestURLPath != "" { path := relayInfo.RequestURLPath if idx := strings.Index(path, "?"); idx != -1 { path = path[:idx] } other["request_path"] = path } } 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{}) other["model_ratio"] = modelRatio other["group_ratio"] = groupRatio other["completion_ratio"] = completionRatio other["cache_tokens"] = cacheTokens other["cache_ratio"] = cacheRatio other["model_price"] = modelPrice other["user_group_ratio"] = userGroupRatio other["frt"] = float64(relayInfo.FirstResponseTime.UnixMilli() - relayInfo.StartTime.UnixMilli()) if relayInfo.ReasoningEffort != "" { other["reasoning_effort"] = relayInfo.ReasoningEffort } if relayInfo.IsModelMapped { other["is_model_mapped"] = true other["upstream_model_name"] = relayInfo.UpstreamModelName } isSystemPromptOverwritten := common.GetContextKeyBool(ctx, constant.ContextKeySystemPromptOverride) if isSystemPromptOverwritten { other["is_system_prompt_overwritten"] = true } adminInfo := make(map[string]interface{}) adminInfo["use_channel"] = ctx.GetStringSlice("use_channel") isMultiKey := common.GetContextKeyBool(ctx, constant.ContextKeyChannelIsMultiKey) if isMultiKey { adminInfo["is_multi_key"] = true adminInfo["multi_key_index"] = common.GetContextKeyInt(ctx, constant.ContextKeyChannelMultiKeyIndex) } isLocalCountTokens := common.GetContextKeyBool(ctx, constant.ContextKeyLocalCountTokens) if isLocalCountTokens { adminInfo["local_count_tokens"] = isLocalCountTokens } AppendChannelAffinityAdminInfo(ctx, adminInfo) other["admin_info"] = adminInfo appendRequestPath(ctx, relayInfo, other) appendRequestConversionChain(relayInfo, other) appendFinalRequestFormat(relayInfo, other) appendBillingInfo(relayInfo, other) appendImagePerImageBillingInfo(relayInfo, other) appendParamOverrideInfo(relayInfo, other) appendStreamStatus(relayInfo, other) appendChannelPriceDiscountToConsumeOther(relayInfo, other) return other } func appendParamOverrideInfo(relayInfo *relaycommon.RelayInfo, other map[string]interface{}) { if relayInfo == nil || other == nil || len(relayInfo.ParamOverrideAudit) == 0 { return } other["po"] = relayInfo.ParamOverrideAudit } func appendStreamStatus(relayInfo *relaycommon.RelayInfo, other map[string]interface{}) { if relayInfo == nil || other == nil || !relayInfo.IsStream || relayInfo.StreamStatus == nil { return } ss := relayInfo.StreamStatus status := "ok" if !ss.IsNormalEnd() || ss.HasErrors() { status = "error" } streamInfo := map[string]interface{}{ "status": status, "end_reason": string(ss.EndReason), } if ss.EndError != nil { streamInfo["end_error"] = ss.EndError.Error() } if ss.ErrorCount > 0 { streamInfo["error_count"] = ss.ErrorCount messages := make([]string, 0, len(ss.Errors)) for _, e := range ss.Errors { messages = append(messages, e.Message) } streamInfo["errors"] = messages } other["stream_status"] = streamInfo } func appendBillingInfo(relayInfo *relaycommon.RelayInfo, other map[string]interface{}) { if relayInfo == nil || other == nil { return } // billing_source: "wallet" or "subscription" if relayInfo.BillingSource != "" { other["billing_source"] = relayInfo.BillingSource } if relayInfo.UserSetting.BillingPreference != "" { other["billing_preference"] = relayInfo.UserSetting.BillingPreference } if relayInfo.BillingSource == "subscription" { if relayInfo.SubscriptionId != 0 { other["subscription_id"] = relayInfo.SubscriptionId } if relayInfo.SubscriptionPreConsumed > 0 { other["subscription_pre_consumed"] = relayInfo.SubscriptionPreConsumed } // post_delta: settlement delta applied after actual usage is known (can be negative for refund) if relayInfo.SubscriptionPostDelta != 0 { other["subscription_post_delta"] = relayInfo.SubscriptionPostDelta } if relayInfo.SubscriptionPlanId != 0 { other["subscription_plan_id"] = relayInfo.SubscriptionPlanId } if relayInfo.SubscriptionPlanTitle != "" { other["subscription_plan_title"] = relayInfo.SubscriptionPlanTitle } // Compute "this request" subscription consumed + remaining consumed := relayInfo.SubscriptionPreConsumed + relayInfo.SubscriptionPostDelta usedFinal := relayInfo.SubscriptionAmountUsedAfterPreConsume + relayInfo.SubscriptionPostDelta if consumed < 0 { consumed = 0 } if usedFinal < 0 { usedFinal = 0 } if relayInfo.SubscriptionAmountTotal > 0 { remain := relayInfo.SubscriptionAmountTotal - usedFinal if remain < 0 { remain = 0 } other["subscription_total"] = relayInfo.SubscriptionAmountTotal other["subscription_used"] = usedFinal other["subscription_remain"] = remain } if consumed > 0 { other["subscription_consumed"] = consumed } // Wallet quota is not deducted when billed from subscription. other["wallet_quota_deducted"] = 0 } } func appendImagePerImageBillingInfo(relayInfo *relaycommon.RelayInfo, other map[string]interface{}) { if relayInfo == nil || other == nil || relayInfo.ImageBilling == nil || !relayInfo.PriceData.UsePrice { return } b := relayInfo.ImageBilling other["billing_mode"] = "image_per_image" other["image_usd_per_image"] = b.UsdPerImage if relayInfo.PriceData.ModelPrice > 0 { other["image_channel_rule_usd"] = relayInfo.PriceData.ModelPrice } if relayInfo.PriceData.GlobalModelPrice > 0 { other["image_global_rule_usd"] = relayInfo.PriceData.GlobalModelPrice } if b.Count > 0 { other["image_count"] = b.Count } else if n, ok := relayInfo.PriceData.OtherRatios["n"]; ok && n > 0 { other["image_count"] = int(n) } if b.Width > 0 && b.Height > 0 { other["image_resolution"] = fmt.Sprintf("%dx%d", b.Width, b.Height) } if b.Mode != "" { other["image_billing_mode"] = b.Mode } } func appendRequestConversionChain(relayInfo *relaycommon.RelayInfo, other map[string]interface{}) { if relayInfo == nil || other == nil { return } if len(relayInfo.RequestConversionChain) == 0 { return } chain := make([]string, 0, len(relayInfo.RequestConversionChain)) for _, f := range relayInfo.RequestConversionChain { switch f { case types.RelayFormatOpenAI: chain = append(chain, "OpenAI Compatible") case types.RelayFormatClaude: chain = append(chain, "Claude Messages") case types.RelayFormatGemini: chain = append(chain, "Google Gemini") case types.RelayFormatOpenAIResponses: chain = append(chain, "OpenAI Responses") default: chain = append(chain, string(f)) } } if len(chain) == 0 { return } other["request_conversion"] = chain } func appendFinalRequestFormat(relayInfo *relaycommon.RelayInfo, other map[string]interface{}) { if relayInfo == nil || other == nil { return } if relayInfo.GetFinalRequestRelayFormat() == types.RelayFormatClaude { // claude indicates the final upstream request format is Claude Messages. // Frontend log rendering uses this to keep the original Claude input display. other["claude"] = true } } func GenerateWssOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usage *dto.RealtimeUsage, modelRatio, groupRatio, completionRatio, audioRatio, audioCompletionRatio, modelPrice, userGroupRatio float64) map[string]interface{} { info := GenerateTextOtherInfo(ctx, relayInfo, modelRatio, groupRatio, completionRatio, 0, 0.0, modelPrice, userGroupRatio) info["ws"] = true info["audio_input"] = usage.InputTokenDetails.AudioTokens info["audio_output"] = usage.OutputTokenDetails.AudioTokens info["text_input"] = usage.InputTokenDetails.TextTokens info["text_output"] = usage.OutputTokenDetails.TextTokens info["audio_ratio"] = audioRatio info["audio_completion_ratio"] = audioCompletionRatio return info } func GenerateAudioOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usage *dto.Usage, modelRatio, groupRatio, completionRatio, audioRatio, audioCompletionRatio, modelPrice, userGroupRatio float64) map[string]interface{} { info := GenerateTextOtherInfo(ctx, relayInfo, modelRatio, groupRatio, completionRatio, 0, 0.0, modelPrice, userGroupRatio) info["audio"] = true info["audio_input"] = usage.PromptTokensDetails.AudioTokens info["audio_output"] = usage.CompletionTokenDetails.AudioTokens info["text_input"] = usage.PromptTokensDetails.TextTokens info["text_output"] = usage.CompletionTokenDetails.TextTokens info["audio_ratio"] = audioRatio info["audio_completion_ratio"] = audioCompletionRatio return info } func GenerateClaudeOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, modelRatio, groupRatio, completionRatio float64, cacheTokens int, cacheRatio float64, cacheCreationTokens int, cacheCreationRatio float64, cacheCreationTokens5m int, cacheCreationRatio5m float64, cacheCreationTokens1h int, cacheCreationRatio1h float64, modelPrice float64, userGroupRatio float64) map[string]interface{} { info := GenerateTextOtherInfo(ctx, relayInfo, modelRatio, groupRatio, completionRatio, cacheTokens, cacheRatio, modelPrice, userGroupRatio) info["claude"] = true info["cache_creation_tokens"] = cacheCreationTokens info["cache_creation_ratio"] = cacheCreationRatio if cacheCreationTokens5m != 0 { info["cache_creation_tokens_5m"] = cacheCreationTokens5m info["cache_creation_ratio_5m"] = cacheCreationRatio5m } if cacheCreationTokens1h != 0 { info["cache_creation_tokens_1h"] = cacheCreationTokens1h info["cache_creation_ratio_1h"] = cacheCreationRatio1h } return info } func GenerateMjOtherInfo(relayInfo *relaycommon.RelayInfo, priceData types.PriceData) map[string]interface{} { other := make(map[string]interface{}) other["model_price"] = priceData.ModelPrice other["group_ratio"] = priceData.GroupRatioInfo.GroupRatio if priceData.GroupRatioInfo.HasSpecialRatio { other["user_group_ratio"] = priceData.GroupRatioInfo.GroupSpecialRatio } pct := resolveConsumeLogChannelDiscountPercent(relayInfo) if priceData.ChannelPriceDiscount != nil { pct = *priceData.ChannelPriceDiscount } other["channel_price_discount_percent"] = pct other["markup_discount_rate"] = priceData.MarkupDiscountPercent other["global_model_ratio"] = priceData.GlobalModelRatio other["global_model_price"] = priceData.GlobalModelPrice other["global_completion_ratio"] = priceData.GlobalCompletionRatio other["global_cache_ratio"] = priceData.GlobalCacheRatio other["global_create_cache_ratio"] = priceData.GlobalCreateCacheRatio appendRequestPath(nil, relayInfo, other) return other }