package service import ( "strings" "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" "github.com/QuantumNous/new-api/dto" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) 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) appendBillingInfo(relayInfo, other) return other } 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 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 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 } appendRequestPath(nil, relayInfo, other) return other }