diff --git a/model/log.go b/model/log.go index 7495d647d..f8940c150 100644 --- a/model/log.go +++ b/model/log.go @@ -56,8 +56,9 @@ func formatUserLogs(logs []*Log) { var otherMap map[string]interface{} otherMap, _ = common.StrToMap(logs[i].Other) if otherMap != nil { - // delete admin + // Remove admin-only debug fields. delete(otherMap, "admin_info") + delete(otherMap, "request_conversion") } logs[i].Other = common.MapToJsonStr(otherMap) logs[i].Id = logs[i].Id % 1024 diff --git a/relay/chat_completions_via_responses.go b/relay/chat_completions_via_responses.go index 4b369440d..38dae3c56 100644 --- a/relay/chat_completions_via_responses.go +++ b/relay/chat_completions_via_responses.go @@ -97,6 +97,7 @@ func chatCompletionsViaResponses(c *gin.Context, info *relaycommon.RelayInfo, ad if err != nil { return nil, types.NewErrorWithStatusCode(err, types.ErrorCodeInvalidRequest, http.StatusBadRequest, types.ErrOptionWithSkipRetry()) } + info.AppendRequestConversion(types.RelayFormatOpenAIResponses) savedRelayMode := info.RelayMode savedRequestURLPath := info.RequestURLPath @@ -112,6 +113,7 @@ func chatCompletionsViaResponses(c *gin.Context, info *relaycommon.RelayInfo, ad if err != nil { return nil, types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry()) } + relaycommon.AppendRequestConversionFromRequest(info, convertedRequest) jsonData, err := common.Marshal(convertedRequest) if err != nil { diff --git a/relay/claude_handler.go b/relay/claude_handler.go index 7a18c1737..7e05116da 100644 --- a/relay/claude_handler.go +++ b/relay/claude_handler.go @@ -110,6 +110,7 @@ func ClaudeHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *typ if err != nil { return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry()) } + relaycommon.AppendRequestConversionFromRequest(info, convertedRequest) jsonData, err := common.Marshal(convertedRequest) if err != nil { return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry()) diff --git a/relay/common/relay_info.go b/relay/common/relay_info.go index 4665573dd..5c24ce57a 100644 --- a/relay/common/relay_info.go +++ b/relay/common/relay_info.go @@ -121,6 +121,10 @@ type RelayInfo struct { Request dto.Request + // RequestConversionChain records request format conversions in order, e.g. + // ["openai", "openai_responses"] or ["openai", "claude"]. + RequestConversionChain []types.RelayFormat + ThinkingContentInfo TokenCountMeta *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) { + var info *RelayInfo + var err error switch relayFormat { case types.RelayFormatOpenAI: - return GenRelayInfoOpenAI(c, request), nil + info = GenRelayInfoOpenAI(c, request) case types.RelayFormatOpenAIAudio: - return GenRelayInfoOpenAIAudio(c, request), nil + info = GenRelayInfoOpenAIAudio(c, request) case types.RelayFormatOpenAIImage: - return GenRelayInfoImage(c, request), nil + info = GenRelayInfoImage(c, request) case types.RelayFormatOpenAIRealtime: - return GenRelayInfoWs(c, ws), nil + info = GenRelayInfoWs(c, ws) case types.RelayFormatClaude: - return GenRelayInfoClaude(c, request), nil + info = GenRelayInfoClaude(c, request) case types.RelayFormatRerank: 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: - return GenRelayInfoGemini(c, request), nil + info = GenRelayInfoGemini(c, request) case types.RelayFormatEmbedding: - return GenRelayInfoEmbedding(c, request), nil + info = GenRelayInfoEmbedding(c, request) case types.RelayFormatOpenAIResponses: 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: - return genBaseRelayInfo(c, nil), nil + info = genBaseRelayInfo(c, nil) case types.RelayFormatMjProxy: - return genBaseRelayInfo(c, nil), nil + info = genBaseRelayInfo(c, nil) 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) { diff --git a/relay/common/request_conversion.go b/relay/common/request_conversion.go new file mode 100644 index 000000000..96b728d21 --- /dev/null +++ b/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) +} diff --git a/relay/compatible_handler.go b/relay/compatible_handler.go index 1a534f588..eab5052d7 100644 --- a/relay/compatible_handler.go +++ b/relay/compatible_handler.go @@ -113,6 +113,7 @@ func TextHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *types if err != nil { return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry()) } + relaycommon.AppendRequestConversionFromRequest(info, convertedRequest) if info.ChannelSetting.SystemPrompt != "" { // 如果有系统提示,则将其添加到请求中 diff --git a/relay/embedding_handler.go b/relay/embedding_handler.go index 2cedf02b5..1a41756b8 100644 --- a/relay/embedding_handler.go +++ b/relay/embedding_handler.go @@ -45,6 +45,7 @@ func EmbeddingHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError * if err != nil { return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry()) } + relaycommon.AppendRequestConversionFromRequest(info, convertedRequest) jsonData, err := json.Marshal(convertedRequest) if err != nil { return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry()) diff --git a/relay/gemini_handler.go b/relay/gemini_handler.go index 79ffba515..779670b9e 100644 --- a/relay/gemini_handler.go +++ b/relay/gemini_handler.go @@ -149,6 +149,7 @@ func GeminiHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *typ if err != nil { return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry()) } + relaycommon.AppendRequestConversionFromRequest(info, convertedRequest) jsonData, err := common.Marshal(convertedRequest) if err != nil { return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry()) diff --git a/relay/image_handler.go b/relay/image_handler.go index f110f4e86..1ee790b74 100644 --- a/relay/image_handler.go +++ b/relay/image_handler.go @@ -57,6 +57,7 @@ func ImageHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *type if err != nil { return types.NewError(err, types.ErrorCodeConvertRequestFailed) } + relaycommon.AppendRequestConversionFromRequest(info, convertedRequest) switch convertedRequest.(type) { case *bytes.Buffer: diff --git a/relay/rerank_handler.go b/relay/rerank_handler.go index 9a50fd271..35c66a291 100644 --- a/relay/rerank_handler.go +++ b/relay/rerank_handler.go @@ -53,6 +53,7 @@ func RerankHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *typ if err != nil { return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry()) } + relaycommon.AppendRequestConversionFromRequest(info, convertedRequest) jsonData, err := common.Marshal(convertedRequest) if err != nil { return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry()) diff --git a/relay/responses_handler.go b/relay/responses_handler.go index 5c3d9a426..769437a1d 100644 --- a/relay/responses_handler.go +++ b/relay/responses_handler.go @@ -53,6 +53,7 @@ func ResponsesHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError * if err != nil { return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry()) } + relaycommon.AppendRequestConversionFromRequest(info, convertedRequest) jsonData, err := common.Marshal(convertedRequest) if err != nil { return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry()) diff --git a/service/log_info_generate.go b/service/log_info_generate.go index 1bd7df673..8018396d0 100644 --- a/service/log_info_generate.go +++ b/service/log_info_generate.go @@ -70,9 +70,30 @@ func GenerateTextOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, m other["admin_info"] = adminInfo appendRequestPath(ctx, relayInfo, other) + appendRequestConversionChain(relayInfo, 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{} { info := GenerateTextOtherInfo(ctx, relayInfo, modelRatio, groupRatio, completionRatio, 0, 0.0, modelPrice, userGroupRatio) info["ws"] = true diff --git a/web/src/hooks/usage-logs/useUsageLogsData.jsx b/web/src/hooks/usage-logs/useUsageLogsData.jsx index 2d0ed3249..18a8dbc7a 100644 --- a/web/src/hooks/usage-logs/useUsageLogsData.jsx +++ b/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({ - key: t('请求路径'), - value: other.request_path, + key: t('请求转换'), + value: + chain.length > 1 + ? `${chain.join(' -> ')}` + : t('原生格式'), }); } if (isAdminUser) { diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index f6d55544d..a49598bf6 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -2091,6 +2091,9 @@ "请求结束后多退少补": "Adjust after request completion", "请求超时,请刷新页面后重新发起 GitHub 登录": "Request timed out, please refresh and restart GitHub login", "请求路径": "Request path", + "请求转换": "Request conversion", + "原生格式": "Native format", + "转换": "Convert", "请求预扣费额度": "Pre-deduction quota for requests", "请点击我": "Please click me", "请确认以下设置信息,点击\"初始化系统\"开始配置": "Please confirm the following settings information, click \"Initialize system\" to start configuration", diff --git a/web/src/i18n/locales/zh.json b/web/src/i18n/locales/zh.json index e91f50a4e..e7579c591 100644 --- a/web/src/i18n/locales/zh.json +++ b/web/src/i18n/locales/zh.json @@ -2077,6 +2077,9 @@ "请求结束后多退少补": "请求结束后多退少补", "请求超时,请刷新页面后重新发起 GitHub 登录": "请求超时,请刷新页面后重新发起 GitHub 登录", "请求路径": "请求路径", + "请求转换": "请求转换", + "原生格式": "原生格式", + "转换": "转换", "请求预扣费额度": "请求预扣费额度", "请点击我": "请点击我", "请确认以下设置信息,点击\"初始化系统\"开始配置": "请确认以下设置信息,点击\"初始化系统\"开始配置",