log_info_generate.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. package service
  2. import (
  3. "encoding/base64"
  4. "strings"
  5. "github.com/QuantumNous/new-api/common"
  6. "github.com/QuantumNous/new-api/constant"
  7. "github.com/QuantumNous/new-api/dto"
  8. "github.com/QuantumNous/new-api/pkg/billingexpr"
  9. relaycommon "github.com/QuantumNous/new-api/relay/common"
  10. "github.com/QuantumNous/new-api/types"
  11. "github.com/gin-gonic/gin"
  12. )
  13. func appendRequestPath(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, other map[string]interface{}) {
  14. if other == nil {
  15. return
  16. }
  17. if ctx != nil && ctx.Request != nil && ctx.Request.URL != nil {
  18. if path := ctx.Request.URL.Path; path != "" {
  19. other["request_path"] = path
  20. return
  21. }
  22. }
  23. if relayInfo != nil && relayInfo.RequestURLPath != "" {
  24. path := relayInfo.RequestURLPath
  25. if idx := strings.Index(path, "?"); idx != -1 {
  26. path = path[:idx]
  27. }
  28. other["request_path"] = path
  29. }
  30. }
  31. func GenerateTextOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, modelRatio, groupRatio, completionRatio float64,
  32. cacheTokens int, cacheRatio float64, modelPrice float64, userGroupRatio float64) map[string]interface{} {
  33. other := make(map[string]interface{})
  34. other["model_ratio"] = modelRatio
  35. other["group_ratio"] = groupRatio
  36. other["completion_ratio"] = completionRatio
  37. other["cache_tokens"] = cacheTokens
  38. other["cache_ratio"] = cacheRatio
  39. other["model_price"] = modelPrice
  40. other["user_group_ratio"] = userGroupRatio
  41. other["frt"] = float64(relayInfo.FirstResponseTime.UnixMilli() - relayInfo.StartTime.UnixMilli())
  42. if relayInfo.ReasoningEffort != "" {
  43. other["reasoning_effort"] = relayInfo.ReasoningEffort
  44. }
  45. if relayInfo.IsModelMapped {
  46. other["is_model_mapped"] = true
  47. other["upstream_model_name"] = relayInfo.UpstreamModelName
  48. }
  49. isSystemPromptOverwritten := common.GetContextKeyBool(ctx, constant.ContextKeySystemPromptOverride)
  50. if isSystemPromptOverwritten {
  51. other["is_system_prompt_overwritten"] = true
  52. }
  53. adminInfo := make(map[string]interface{})
  54. adminInfo["use_channel"] = ctx.GetStringSlice("use_channel")
  55. isMultiKey := common.GetContextKeyBool(ctx, constant.ContextKeyChannelIsMultiKey)
  56. if isMultiKey {
  57. adminInfo["is_multi_key"] = true
  58. adminInfo["multi_key_index"] = common.GetContextKeyInt(ctx, constant.ContextKeyChannelMultiKeyIndex)
  59. }
  60. isLocalCountTokens := common.GetContextKeyBool(ctx, constant.ContextKeyLocalCountTokens)
  61. if isLocalCountTokens {
  62. adminInfo["local_count_tokens"] = isLocalCountTokens
  63. }
  64. AppendChannelAffinityAdminInfo(ctx, adminInfo)
  65. other["admin_info"] = adminInfo
  66. appendRequestPath(ctx, relayInfo, other)
  67. appendRequestConversionChain(relayInfo, other)
  68. appendFinalRequestFormat(relayInfo, other)
  69. appendBillingInfo(relayInfo, other)
  70. appendParamOverrideInfo(relayInfo, other)
  71. appendStreamStatus(relayInfo, other)
  72. return other
  73. }
  74. func appendParamOverrideInfo(relayInfo *relaycommon.RelayInfo, other map[string]interface{}) {
  75. if relayInfo == nil || other == nil || len(relayInfo.ParamOverrideAudit) == 0 {
  76. return
  77. }
  78. other["po"] = relayInfo.ParamOverrideAudit
  79. }
  80. func appendStreamStatus(relayInfo *relaycommon.RelayInfo, other map[string]interface{}) {
  81. if relayInfo == nil || other == nil || !relayInfo.IsStream || relayInfo.StreamStatus == nil {
  82. return
  83. }
  84. ss := relayInfo.StreamStatus
  85. status := "ok"
  86. if !ss.IsNormalEnd() || ss.HasErrors() {
  87. status = "error"
  88. }
  89. streamInfo := map[string]interface{}{
  90. "status": status,
  91. "end_reason": string(ss.EndReason),
  92. }
  93. if ss.EndError != nil {
  94. streamInfo["end_error"] = ss.EndError.Error()
  95. }
  96. if ss.ErrorCount > 0 {
  97. streamInfo["error_count"] = ss.ErrorCount
  98. messages := make([]string, 0, len(ss.Errors))
  99. for _, e := range ss.Errors {
  100. messages = append(messages, e.Message)
  101. }
  102. streamInfo["errors"] = messages
  103. }
  104. other["stream_status"] = streamInfo
  105. }
  106. func appendBillingInfo(relayInfo *relaycommon.RelayInfo, other map[string]interface{}) {
  107. if relayInfo == nil || other == nil {
  108. return
  109. }
  110. // billing_source: "wallet" or "subscription"
  111. if relayInfo.BillingSource != "" {
  112. other["billing_source"] = relayInfo.BillingSource
  113. }
  114. if relayInfo.UserSetting.BillingPreference != "" {
  115. other["billing_preference"] = relayInfo.UserSetting.BillingPreference
  116. }
  117. if relayInfo.BillingSource == "subscription" {
  118. if relayInfo.SubscriptionId != 0 {
  119. other["subscription_id"] = relayInfo.SubscriptionId
  120. }
  121. if relayInfo.SubscriptionPreConsumed > 0 {
  122. other["subscription_pre_consumed"] = relayInfo.SubscriptionPreConsumed
  123. }
  124. // post_delta: settlement delta applied after actual usage is known (can be negative for refund)
  125. if relayInfo.SubscriptionPostDelta != 0 {
  126. other["subscription_post_delta"] = relayInfo.SubscriptionPostDelta
  127. }
  128. if relayInfo.SubscriptionPlanId != 0 {
  129. other["subscription_plan_id"] = relayInfo.SubscriptionPlanId
  130. }
  131. if relayInfo.SubscriptionPlanTitle != "" {
  132. other["subscription_plan_title"] = relayInfo.SubscriptionPlanTitle
  133. }
  134. // Compute "this request" subscription consumed + remaining
  135. consumed := relayInfo.SubscriptionPreConsumed + relayInfo.SubscriptionPostDelta
  136. usedFinal := relayInfo.SubscriptionAmountUsedAfterPreConsume + relayInfo.SubscriptionPostDelta
  137. if consumed < 0 {
  138. consumed = 0
  139. }
  140. if usedFinal < 0 {
  141. usedFinal = 0
  142. }
  143. if relayInfo.SubscriptionAmountTotal > 0 {
  144. remain := relayInfo.SubscriptionAmountTotal - usedFinal
  145. if remain < 0 {
  146. remain = 0
  147. }
  148. other["subscription_total"] = relayInfo.SubscriptionAmountTotal
  149. other["subscription_used"] = usedFinal
  150. other["subscription_remain"] = remain
  151. }
  152. if consumed > 0 {
  153. other["subscription_consumed"] = consumed
  154. }
  155. // Wallet quota is not deducted when billed from subscription.
  156. other["wallet_quota_deducted"] = 0
  157. }
  158. }
  159. func appendRequestConversionChain(relayInfo *relaycommon.RelayInfo, other map[string]interface{}) {
  160. if relayInfo == nil || other == nil {
  161. return
  162. }
  163. if len(relayInfo.RequestConversionChain) == 0 {
  164. return
  165. }
  166. chain := make([]string, 0, len(relayInfo.RequestConversionChain))
  167. for _, f := range relayInfo.RequestConversionChain {
  168. switch f {
  169. case types.RelayFormatOpenAI:
  170. chain = append(chain, "OpenAI Compatible")
  171. case types.RelayFormatClaude:
  172. chain = append(chain, "Claude Messages")
  173. case types.RelayFormatGemini:
  174. chain = append(chain, "Google Gemini")
  175. case types.RelayFormatOpenAIResponses:
  176. chain = append(chain, "OpenAI Responses")
  177. default:
  178. chain = append(chain, string(f))
  179. }
  180. }
  181. if len(chain) == 0 {
  182. return
  183. }
  184. other["request_conversion"] = chain
  185. }
  186. func appendFinalRequestFormat(relayInfo *relaycommon.RelayInfo, other map[string]interface{}) {
  187. if relayInfo == nil || other == nil {
  188. return
  189. }
  190. if relayInfo.GetFinalRequestRelayFormat() == types.RelayFormatClaude {
  191. // claude indicates the final upstream request format is Claude Messages.
  192. // Frontend log rendering uses this to keep the original Claude input display.
  193. other["claude"] = true
  194. }
  195. }
  196. func GenerateWssOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usage *dto.RealtimeUsage, modelRatio, groupRatio, completionRatio, audioRatio, audioCompletionRatio, modelPrice, userGroupRatio float64) map[string]interface{} {
  197. info := GenerateTextOtherInfo(ctx, relayInfo, modelRatio, groupRatio, completionRatio, 0, 0.0, modelPrice, userGroupRatio)
  198. info["ws"] = true
  199. info["audio_input"] = usage.InputTokenDetails.AudioTokens
  200. info["audio_output"] = usage.OutputTokenDetails.AudioTokens
  201. info["text_input"] = usage.InputTokenDetails.TextTokens
  202. info["text_output"] = usage.OutputTokenDetails.TextTokens
  203. info["audio_ratio"] = audioRatio
  204. info["audio_completion_ratio"] = audioCompletionRatio
  205. return info
  206. }
  207. func GenerateAudioOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usage *dto.Usage, modelRatio, groupRatio, completionRatio, audioRatio, audioCompletionRatio, modelPrice, userGroupRatio float64) map[string]interface{} {
  208. info := GenerateTextOtherInfo(ctx, relayInfo, modelRatio, groupRatio, completionRatio, 0, 0.0, modelPrice, userGroupRatio)
  209. info["audio"] = true
  210. info["audio_input"] = usage.PromptTokensDetails.AudioTokens
  211. info["audio_output"] = usage.CompletionTokenDetails.AudioTokens
  212. info["text_input"] = usage.PromptTokensDetails.TextTokens
  213. info["text_output"] = usage.CompletionTokenDetails.TextTokens
  214. info["audio_ratio"] = audioRatio
  215. info["audio_completion_ratio"] = audioCompletionRatio
  216. return info
  217. }
  218. func GenerateClaudeOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, modelRatio, groupRatio, completionRatio float64,
  219. cacheTokens int, cacheRatio float64,
  220. cacheCreationTokens int, cacheCreationRatio float64,
  221. cacheCreationTokens5m int, cacheCreationRatio5m float64,
  222. cacheCreationTokens1h int, cacheCreationRatio1h float64,
  223. modelPrice float64, userGroupRatio float64) map[string]interface{} {
  224. info := GenerateTextOtherInfo(ctx, relayInfo, modelRatio, groupRatio, completionRatio, cacheTokens, cacheRatio, modelPrice, userGroupRatio)
  225. info["claude"] = true
  226. info["cache_creation_tokens"] = cacheCreationTokens
  227. info["cache_creation_ratio"] = cacheCreationRatio
  228. if cacheCreationTokens5m != 0 {
  229. info["cache_creation_tokens_5m"] = cacheCreationTokens5m
  230. info["cache_creation_ratio_5m"] = cacheCreationRatio5m
  231. }
  232. if cacheCreationTokens1h != 0 {
  233. info["cache_creation_tokens_1h"] = cacheCreationTokens1h
  234. info["cache_creation_ratio_1h"] = cacheCreationRatio1h
  235. }
  236. return info
  237. }
  238. func GenerateMjOtherInfo(relayInfo *relaycommon.RelayInfo, priceData types.PriceData) map[string]interface{} {
  239. other := make(map[string]interface{})
  240. other["model_price"] = priceData.ModelPrice
  241. other["group_ratio"] = priceData.GroupRatioInfo.GroupRatio
  242. if priceData.GroupRatioInfo.HasSpecialRatio {
  243. other["user_group_ratio"] = priceData.GroupRatioInfo.GroupSpecialRatio
  244. }
  245. appendRequestPath(nil, relayInfo, other)
  246. return other
  247. }
  248. // InjectTieredBillingInfo overlays tiered billing fields onto an existing
  249. // module-specific other map. Call this after GenerateTextOtherInfo /
  250. // GenerateClaudeOtherInfo / etc. when the request used tiered_expr billing.
  251. func InjectTieredBillingInfo(other map[string]interface{}, relayInfo *relaycommon.RelayInfo, result *billingexpr.TieredResult) {
  252. snap := relayInfo.TieredBillingSnapshot
  253. if snap == nil {
  254. return
  255. }
  256. other["billing_mode"] = "tiered_expr"
  257. other["expr_b64"] = base64.StdEncoding.EncodeToString([]byte(snap.ExprString))
  258. if result != nil {
  259. other["matched_tier"] = result.MatchedTier
  260. }
  261. }