Преглед изворни кода

feat: log shows request conversion

Seefs пре 3 недеља
родитељ
комит
d4582ede98

+ 2 - 1
model/log.go

@@ -56,8 +56,9 @@ func formatUserLogs(logs []*Log) {
 		var otherMap map[string]interface{}
 		var otherMap map[string]interface{}
 		otherMap, _ = common.StrToMap(logs[i].Other)
 		otherMap, _ = common.StrToMap(logs[i].Other)
 		if otherMap != nil {
 		if otherMap != nil {
-			// delete admin
+			// Remove admin-only debug fields.
 			delete(otherMap, "admin_info")
 			delete(otherMap, "admin_info")
+			delete(otherMap, "request_conversion")
 		}
 		}
 		logs[i].Other = common.MapToJsonStr(otherMap)
 		logs[i].Other = common.MapToJsonStr(otherMap)
 		logs[i].Id = logs[i].Id % 1024
 		logs[i].Id = logs[i].Id % 1024

+ 2 - 0
relay/chat_completions_via_responses.go

@@ -97,6 +97,7 @@ func chatCompletionsViaResponses(c *gin.Context, info *relaycommon.RelayInfo, ad
 	if err != nil {
 	if err != nil {
 		return nil, types.NewErrorWithStatusCode(err, types.ErrorCodeInvalidRequest, http.StatusBadRequest, types.ErrOptionWithSkipRetry())
 		return nil, types.NewErrorWithStatusCode(err, types.ErrorCodeInvalidRequest, http.StatusBadRequest, types.ErrOptionWithSkipRetry())
 	}
 	}
+	info.AppendRequestConversion(types.RelayFormatOpenAIResponses)
 
 
 	savedRelayMode := info.RelayMode
 	savedRelayMode := info.RelayMode
 	savedRequestURLPath := info.RequestURLPath
 	savedRequestURLPath := info.RequestURLPath
@@ -112,6 +113,7 @@ func chatCompletionsViaResponses(c *gin.Context, info *relaycommon.RelayInfo, ad
 	if err != nil {
 	if err != nil {
 		return nil, types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry())
 		return nil, types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry())
 	}
 	}
+	relaycommon.AppendRequestConversionFromRequest(info, convertedRequest)
 
 
 	jsonData, err := common.Marshal(convertedRequest)
 	jsonData, err := common.Marshal(convertedRequest)
 	if err != nil {
 	if err != nil {

+ 1 - 0
relay/claude_handler.go

@@ -110,6 +110,7 @@ func ClaudeHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *typ
 		if err != nil {
 		if err != nil {
 			return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry())
 			return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry())
 		}
 		}
+		relaycommon.AppendRequestConversionFromRequest(info, convertedRequest)
 		jsonData, err := common.Marshal(convertedRequest)
 		jsonData, err := common.Marshal(convertedRequest)
 		if err != nil {
 		if err != nil {
 			return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry())
 			return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry())

+ 63 - 14
relay/common/relay_info.go

@@ -121,6 +121,10 @@ type RelayInfo struct {
 
 
 	Request dto.Request
 	Request dto.Request
 
 
+	// RequestConversionChain records request format conversions in order, e.g.
+	// ["openai", "openai_responses"] or ["openai", "claude"].
+	RequestConversionChain []types.RelayFormat
+
 	ThinkingContentInfo
 	ThinkingContentInfo
 	TokenCountMeta
 	TokenCountMeta
 	*ClaudeConvertInfo
 	*ClaudeConvertInfo
@@ -448,38 +452,83 @@ func genBaseRelayInfo(c *gin.Context, request dto.Request) *RelayInfo {
 }
 }
 
 
 func GenRelayInfo(c *gin.Context, relayFormat types.RelayFormat, request dto.Request, ws *websocket.Conn) (*RelayInfo, error) {
 func GenRelayInfo(c *gin.Context, relayFormat types.RelayFormat, request dto.Request, ws *websocket.Conn) (*RelayInfo, error) {
+	var info *RelayInfo
+	var err error
 	switch relayFormat {
 	switch relayFormat {
 	case types.RelayFormatOpenAI:
 	case types.RelayFormatOpenAI:
-		return GenRelayInfoOpenAI(c, request), nil
+		info = GenRelayInfoOpenAI(c, request)
 	case types.RelayFormatOpenAIAudio:
 	case types.RelayFormatOpenAIAudio:
-		return GenRelayInfoOpenAIAudio(c, request), nil
+		info = GenRelayInfoOpenAIAudio(c, request)
 	case types.RelayFormatOpenAIImage:
 	case types.RelayFormatOpenAIImage:
-		return GenRelayInfoImage(c, request), nil
+		info = GenRelayInfoImage(c, request)
 	case types.RelayFormatOpenAIRealtime:
 	case types.RelayFormatOpenAIRealtime:
-		return GenRelayInfoWs(c, ws), nil
+		info = GenRelayInfoWs(c, ws)
 	case types.RelayFormatClaude:
 	case types.RelayFormatClaude:
-		return GenRelayInfoClaude(c, request), nil
+		info = GenRelayInfoClaude(c, request)
 	case types.RelayFormatRerank:
 	case types.RelayFormatRerank:
 		if request, ok := request.(*dto.RerankRequest); ok {
 		if request, ok := request.(*dto.RerankRequest); ok {
-			return GenRelayInfoRerank(c, request), nil
+			info = GenRelayInfoRerank(c, request)
+			break
 		}
 		}
-		return nil, errors.New("request is not a RerankRequest")
+		err = errors.New("request is not a RerankRequest")
 	case types.RelayFormatGemini:
 	case types.RelayFormatGemini:
-		return GenRelayInfoGemini(c, request), nil
+		info = GenRelayInfoGemini(c, request)
 	case types.RelayFormatEmbedding:
 	case types.RelayFormatEmbedding:
-		return GenRelayInfoEmbedding(c, request), nil
+		info = GenRelayInfoEmbedding(c, request)
 	case types.RelayFormatOpenAIResponses:
 	case types.RelayFormatOpenAIResponses:
 		if request, ok := request.(*dto.OpenAIResponsesRequest); ok {
 		if request, ok := request.(*dto.OpenAIResponsesRequest); ok {
-			return GenRelayInfoResponses(c, request), nil
+			info = GenRelayInfoResponses(c, request)
+			break
 		}
 		}
-		return nil, errors.New("request is not a OpenAIResponsesRequest")
+		err = errors.New("request is not a OpenAIResponsesRequest")
 	case types.RelayFormatTask:
 	case types.RelayFormatTask:
-		return genBaseRelayInfo(c, nil), nil
+		info = genBaseRelayInfo(c, nil)
 	case types.RelayFormatMjProxy:
 	case types.RelayFormatMjProxy:
-		return genBaseRelayInfo(c, nil), nil
+		info = genBaseRelayInfo(c, nil)
 	default:
 	default:
-		return nil, errors.New("invalid relay format")
+		err = errors.New("invalid relay format")
+	}
+
+	if err != nil {
+		return nil, err
+	}
+	if info == nil {
+		return nil, errors.New("failed to build relay info")
+	}
+
+	info.InitRequestConversionChain()
+	return info, nil
+}
+
+func (info *RelayInfo) InitRequestConversionChain() {
+	if info == nil {
+		return
+	}
+	if len(info.RequestConversionChain) > 0 {
+		return
+	}
+	if info.RelayFormat == "" {
+		return
+	}
+	info.RequestConversionChain = []types.RelayFormat{info.RelayFormat}
+}
+
+func (info *RelayInfo) AppendRequestConversion(format types.RelayFormat) {
+	if info == nil {
+		return
+	}
+	if format == "" {
+		return
+	}
+	if len(info.RequestConversionChain) == 0 {
+		info.RequestConversionChain = []types.RelayFormat{format}
+		return
+	}
+	last := info.RequestConversionChain[len(info.RequestConversionChain)-1]
+	if last == format {
+		return
 	}
 	}
+	info.RequestConversionChain = append(info.RequestConversionChain, format)
 }
 }
 
 
 //func (info *RelayInfo) SetPromptTokens(promptTokens int) {
 //func (info *RelayInfo) SetPromptTokens(promptTokens int) {

+ 40 - 0
relay/common/request_conversion.go

@@ -0,0 +1,40 @@
+package common
+
+import (
+	"github.com/QuantumNous/new-api/dto"
+	"github.com/QuantumNous/new-api/types"
+)
+
+func GuessRelayFormatFromRequest(req any) (types.RelayFormat, bool) {
+	switch req.(type) {
+	case *dto.GeneralOpenAIRequest, dto.GeneralOpenAIRequest:
+		return types.RelayFormatOpenAI, true
+	case *dto.OpenAIResponsesRequest, dto.OpenAIResponsesRequest:
+		return types.RelayFormatOpenAIResponses, true
+	case *dto.ClaudeRequest, dto.ClaudeRequest:
+		return types.RelayFormatClaude, true
+	case *dto.GeminiChatRequest, dto.GeminiChatRequest:
+		return types.RelayFormatGemini, true
+	case *dto.EmbeddingRequest, dto.EmbeddingRequest:
+		return types.RelayFormatEmbedding, true
+	case *dto.RerankRequest, dto.RerankRequest:
+		return types.RelayFormatRerank, true
+	case *dto.ImageRequest, dto.ImageRequest:
+		return types.RelayFormatOpenAIImage, true
+	case *dto.AudioRequest, dto.AudioRequest:
+		return types.RelayFormatOpenAIAudio, true
+	default:
+		return "", false
+	}
+}
+
+func AppendRequestConversionFromRequest(info *RelayInfo, req any) {
+	if info == nil {
+		return
+	}
+	format, ok := GuessRelayFormatFromRequest(req)
+	if !ok {
+		return
+	}
+	info.AppendRequestConversion(format)
+}

+ 1 - 0
relay/compatible_handler.go

@@ -113,6 +113,7 @@ func TextHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *types
 		if err != nil {
 		if err != nil {
 			return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry())
 			return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry())
 		}
 		}
+		relaycommon.AppendRequestConversionFromRequest(info, convertedRequest)
 
 
 		if info.ChannelSetting.SystemPrompt != "" {
 		if info.ChannelSetting.SystemPrompt != "" {
 			// 如果有系统提示,则将其添加到请求中
 			// 如果有系统提示,则将其添加到请求中

+ 1 - 0
relay/embedding_handler.go

@@ -45,6 +45,7 @@ func EmbeddingHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *
 	if err != nil {
 	if err != nil {
 		return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry())
 		return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry())
 	}
 	}
+	relaycommon.AppendRequestConversionFromRequest(info, convertedRequest)
 	jsonData, err := json.Marshal(convertedRequest)
 	jsonData, err := json.Marshal(convertedRequest)
 	if err != nil {
 	if err != nil {
 		return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry())
 		return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry())

+ 1 - 0
relay/gemini_handler.go

@@ -149,6 +149,7 @@ func GeminiHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *typ
 		if err != nil {
 		if err != nil {
 			return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry())
 			return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry())
 		}
 		}
+		relaycommon.AppendRequestConversionFromRequest(info, convertedRequest)
 		jsonData, err := common.Marshal(convertedRequest)
 		jsonData, err := common.Marshal(convertedRequest)
 		if err != nil {
 		if err != nil {
 			return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry())
 			return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry())

+ 1 - 0
relay/image_handler.go

@@ -57,6 +57,7 @@ func ImageHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *type
 		if err != nil {
 		if err != nil {
 			return types.NewError(err, types.ErrorCodeConvertRequestFailed)
 			return types.NewError(err, types.ErrorCodeConvertRequestFailed)
 		}
 		}
+		relaycommon.AppendRequestConversionFromRequest(info, convertedRequest)
 
 
 		switch convertedRequest.(type) {
 		switch convertedRequest.(type) {
 		case *bytes.Buffer:
 		case *bytes.Buffer:

+ 1 - 0
relay/rerank_handler.go

@@ -53,6 +53,7 @@ func RerankHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *typ
 		if err != nil {
 		if err != nil {
 			return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry())
 			return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry())
 		}
 		}
+		relaycommon.AppendRequestConversionFromRequest(info, convertedRequest)
 		jsonData, err := common.Marshal(convertedRequest)
 		jsonData, err := common.Marshal(convertedRequest)
 		if err != nil {
 		if err != nil {
 			return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry())
 			return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry())

+ 1 - 0
relay/responses_handler.go

@@ -53,6 +53,7 @@ func ResponsesHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *
 		if err != nil {
 		if err != nil {
 			return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry())
 			return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry())
 		}
 		}
+		relaycommon.AppendRequestConversionFromRequest(info, convertedRequest)
 		jsonData, err := common.Marshal(convertedRequest)
 		jsonData, err := common.Marshal(convertedRequest)
 		if err != nil {
 		if err != nil {
 			return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry())
 			return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry())

+ 21 - 0
service/log_info_generate.go

@@ -70,9 +70,30 @@ func GenerateTextOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, m
 
 
 	other["admin_info"] = adminInfo
 	other["admin_info"] = adminInfo
 	appendRequestPath(ctx, relayInfo, other)
 	appendRequestPath(ctx, relayInfo, other)
+	appendRequestConversionChain(relayInfo, other)
 	return other
 	return other
 }
 }
 
 
+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 {
+		if f == "" {
+			continue
+		}
+		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{} {
 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 := GenerateTextOtherInfo(ctx, relayInfo, modelRatio, groupRatio, completionRatio, 0, 0.0, modelPrice, userGroupRatio)
 	info["ws"] = true
 	info["ws"] = true

+ 10 - 3
web/src/hooks/usage-logs/useUsageLogsData.jsx

@@ -476,10 +476,17 @@ export const useLogsData = () => {
           });
           });
         }
         }
       }
       }
-      if (other?.request_path) {
+      if (isAdminUser) {
+        const requestConversionChain = other?.request_conversion;
+        const chain = Array.isArray(requestConversionChain)
+          ? requestConversionChain.filter(Boolean)
+          : [];
         expandDataLocal.push({
         expandDataLocal.push({
-          key: t('请求路径'),
-          value: other.request_path,
+          key: t('请求转换'),
+          value:
+            chain.length > 1
+              ? `${chain.join(' -> ')}`
+              : t('原生格式'),
         });
         });
       }
       }
       if (isAdminUser) {
       if (isAdminUser) {

+ 3 - 0
web/src/i18n/locales/en.json

@@ -2091,6 +2091,9 @@
     "请求结束后多退少补": "Adjust after request completion",
     "请求结束后多退少补": "Adjust after request completion",
     "请求超时,请刷新页面后重新发起 GitHub 登录": "Request timed out, please refresh and restart GitHub login",
     "请求超时,请刷新页面后重新发起 GitHub 登录": "Request timed out, please refresh and restart GitHub login",
     "请求路径": "Request path",
     "请求路径": "Request path",
+    "请求转换": "Request conversion",
+    "原生格式": "Native format",
+    "转换": "Convert",
     "请求预扣费额度": "Pre-deduction quota for requests",
     "请求预扣费额度": "Pre-deduction quota for requests",
     "请点击我": "Please click me",
     "请点击我": "Please click me",
     "请确认以下设置信息,点击\"初始化系统\"开始配置": "Please confirm the following settings information, click \"Initialize system\" to start configuration",
     "请确认以下设置信息,点击\"初始化系统\"开始配置": "Please confirm the following settings information, click \"Initialize system\" to start configuration",

+ 3 - 0
web/src/i18n/locales/zh.json

@@ -2077,6 +2077,9 @@
     "请求结束后多退少补": "请求结束后多退少补",
     "请求结束后多退少补": "请求结束后多退少补",
     "请求超时,请刷新页面后重新发起 GitHub 登录": "请求超时,请刷新页面后重新发起 GitHub 登录",
     "请求超时,请刷新页面后重新发起 GitHub 登录": "请求超时,请刷新页面后重新发起 GitHub 登录",
     "请求路径": "请求路径",
     "请求路径": "请求路径",
+    "请求转换": "请求转换",
+    "原生格式": "原生格式",
+    "转换": "转换",
     "请求预扣费额度": "请求预扣费额度",
     "请求预扣费额度": "请求预扣费额度",
     "请点击我": "请点击我",
     "请点击我": "请点击我",
     "请确认以下设置信息,点击\"初始化系统\"开始配置": "请确认以下设置信息,点击\"初始化系统\"开始配置",
     "请确认以下设置信息,点击\"初始化系统\"开始配置": "请确认以下设置信息,点击\"初始化系统\"开始配置",