Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion model/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions relay/chat_completions_via_responses.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions relay/claude_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
77 changes: 63 additions & 14 deletions relay/common/relay_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Comment on lines 454 to +500

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Set RelayFormat for task/mj_proxy to initialize the conversion chain.
InitRequestConversionChain seeds from RelayInfo.RelayFormat, but the Task/MjProxy cases only call genBaseRelayInfo, leaving the chain empty. Assign the format in those cases so logs include the starting format.

🔧 Suggested fix
	case types.RelayFormatTask:
		info = genBaseRelayInfo(c, nil)
+		info.RelayFormat = types.RelayFormatTask
	case types.RelayFormatMjProxy:
		info = genBaseRelayInfo(c, nil)
+		info.RelayFormat = types.RelayFormatMjProxy
🤖 Prompt for AI Agents
In `@relay/common/relay_info.go` around lines 454 - 500, The Task and MjProxy
branches in GenRelayInfo call genBaseRelayInfo(c, nil) but do not set
RelayInfo.RelayFormat, so InitRequestConversionChain() has no starting format;
update the types.RelayFormatTask and types.RelayFormatMjProxy cases in
GenRelayInfo to create the base RelayInfo via genBaseRelayInfo and then set
info.RelayFormat = types.RelayFormatTask (or types.RelayFormatMjProxy)
respectively before returning so InitRequestConversionChain can seed the
conversion chain correctly.

}

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) {
Expand Down
40 changes: 40 additions & 0 deletions relay/common/request_conversion.go
Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions relay/compatible_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 != "" {
// 如果有系统提示,则将其添加到请求中
Expand Down
1 change: 1 addition & 0 deletions relay/embedding_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
1 change: 1 addition & 0 deletions relay/gemini_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
1 change: 1 addition & 0 deletions relay/image_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions relay/rerank_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
1 change: 1 addition & 0 deletions relay/responses_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
29 changes: 29 additions & 0 deletions service/log_info_generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,38 @@ 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 {
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
}
Comment on lines +73 to +103

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Consider emitting locale-agnostic format identifiers.
appendRequestConversionChain hard-codes English labels (e.g., “OpenAI Compatible”), which will show untranslated in non-English UIs. Prefer storing canonical relay-format values and localizing in the frontend.

🤖 Prompt for AI Agents
In `@service/log_info_generate.go` around lines 73 - 103,
appendRequestConversionChain currently maps relayInfo.RequestConversionChain
entries to hard-coded English labels ("OpenAI Compatible", "Claude Messages",
etc.); change it to emit locale-agnostic canonical identifiers instead (e.g.,
the enum/string values from types.RelayFormat*). In function
appendRequestConversionChain, replace the switch that appends human-readable
strings with logic that appends the canonical identifier (for example string(f)
or f.String() if available) into the chain slice, and keep setting
other["request_conversion"] = chain so the frontend can handle localization.


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
Expand Down
16 changes: 16 additions & 0 deletions web/src/hooks/usage-logs/useUsageLogsData.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,16 @@ export const useLogsData = () => {

// Format logs data
const setLogsFormat = (logs) => {
const requestConversionDisplayValue = (conversionChain) => {
const chain = Array.isArray(conversionChain)
? conversionChain.filter(Boolean)
: [];
if (chain.length <= 1) {
return t('原生格式');
}
return `${chain.join(' -> ')}`;
};
Comment on lines +309 to +317

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, verify the file exists and check the specific lines
fd "useUsageLogsData.jsx" --type f

Repository: QuantumNous/new-api

Length of output: 108


🏁 Script executed:

# Examine the file around lines 309-317
if [ -f "web/src/hooks/usage-logs/useUsageLogsData.jsx" ]; then
  echo "=== Lines 300-330 ===" && sed -n '300,330p' "web/src/hooks/usage-logs/useUsageLogsData.jsx"
  echo ""
  echo "=== Searching for request_conversion usage ===" && grep -n "request_conversion\|conversionChain\|requestConversionDisplayValue" "web/src/hooks/usage-logs/useUsageLogsData.jsx" | head -20
else
  echo "File not found"
fi

Repository: QuantumNous/new-api

Length of output: 1308


🏁 Script executed:

# Search for where other.request_conversion comes from (likely API response)
rg "request_conversion|requestConversionDisplayValue" web/src/hooks/usage-logs/ -A 2 -B 2

Repository: QuantumNous/new-api

Length of output: 893


🏁 Script executed:

# Look for type definitions or API contracts related to request_conversion
fd -e "ts" -e "tsx" -e "d.ts" | xargs rg "request_conversion" 2>/dev/null | head -20

Repository: QuantumNous/new-api

Length of output: 304


🏁 Script executed:

# Find and examine service/log_info_generate.go
fd "log_info_generate.go" --type f

Repository: QuantumNous/new-api

Length of output: 91


🏁 Script executed:

# Look at the context around request_conversion assignment in the backend
if [ -f "service/log_info_generate.go" ]; then
  # Find lines with request_conversion and show context
  rg "request_conversion|chain" "service/log_info_generate.go" -B 5 -A 5 | head -60
else
  echo "File not found"
fi

Repository: QuantumNous/new-api

Length of output: 1094


🏁 Script executed:

# Also check model/log.go to understand when request_conversion is deleted
if [ -f "model/log.go" ]; then
  rg "request_conversion" "model/log.go" -B 3 -A 3
else
  echo "File not found"
fi

Repository: QuantumNous/new-api

Length of output: 284


🏁 Script executed:

# Search for type definitions or comments about the conversion chain structure
rg "type.*[Cc]onversion|[Cc]onversion.*struct" --type go | head -20

Repository: QuantumNous/new-api

Length of output: 45


🏁 Script executed:

# Verify if there are any tests or comments that clarify the expected behavior
rg "RequestConversionChain|request_conversion" --type go -B 2 -A 2 | grep -E "comment|//|len\(chain\)|len\(|if.*==" | head -30

Repository: QuantumNous/new-api

Length of output: 880


🏁 Script executed:

# Check if there are any tests related to request_conversion formatting
fd -e "test.go" -e "_test.go" | xargs rg "request_conversion\|requestConversion" 2>/dev/null

Repository: QuantumNous/new-api

Length of output: 47


🏁 Script executed:

# Double-check the exact behavior: when is request_conversion NOT set vs when it IS set
sed -n '1,100p' "service/log_info_generate.go" | grep -A 50 "RequestConversionChain"

Repository: QuantumNous/new-api

Length of output: 872


🏁 Script executed:

# Confirm the actual behavior when request_conversion is undefined/missing
sed -n '490,510p' "web/src/hooks/usage-logs/useUsageLogsData.jsx"

Repository: QuantumNous/new-api

Length of output: 661


Display single-step conversions instead of hiding them as native format.

The condition chain.length <= 1 incorrectly treats single-element conversion arrays as native format. The backend only sets request_conversion when there are one or more conversions (e.g., ["OpenAI Compatible"]), so admins will see "原生格式" even when a conversion occurred. Change the condition to chain.length === 0 to properly distinguish native requests from converted ones.

Suggested fix
-    const requestConversionDisplayValue = (conversionChain) => {
-      const chain = Array.isArray(conversionChain)
-        ? conversionChain.filter(Boolean)
-        : [];
-      if (chain.length <= 1) {
-        return t('原生格式');
-      }
-      return `${chain.join(' -> ')}`;
-    };
+    const requestConversionDisplayValue = (conversionChain) => {
+      const chain = Array.isArray(conversionChain)
+        ? conversionChain.filter(Boolean)
+        : conversionChain
+          ? [conversionChain]
+          : [];
+      if (chain.length === 0) {
+        return t('原生格式');
+      }
+      return chain.join(' -> ');
+    };
🤖 Prompt for AI Agents
In `@web/src/hooks/usage-logs/useUsageLogsData.jsx` around lines 309 - 317, The
function requestConversionDisplayValue currently hides single-step conversions
by returning "原生格式" when chain.length <= 1; update its logic so that only empty
chains are treated as native by changing the condition to check chain.length ===
0 (preserve the Array.isArray filtering behavior and the join for non-empty
chains) so arrays like ["OpenAI Compatible"] are displayed rather than labeled
native.


let expandDatesLocal = {};
for (let i = 0; i < logs.length; i++) {
logs[i].timestamp2string = timestamp2string(logs[i].created_at);
Expand Down Expand Up @@ -482,6 +492,12 @@ export const useLogsData = () => {
value: other.request_path,
});
}
if (isAdminUser) {
expandDataLocal.push({
key: t('请求转换'),
value: requestConversionDisplayValue(other?.request_conversion),
});
}
if (isAdminUser) {
let localCountMode = '';
if (other?.admin_info?.local_count_tokens) {
Expand Down
3 changes: 3 additions & 0 deletions web/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions web/src/i18n/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -2077,6 +2077,9 @@
"请求结束后多退少补": "请求结束后多退少补",
"请求超时,请刷新页面后重新发起 GitHub 登录": "请求超时,请刷新页面后重新发起 GitHub 登录",
"请求路径": "请求路径",
"请求转换": "请求转换",
"原生格式": "原生格式",
"转换": "转换",
"请求预扣费额度": "请求预扣费额度",
"请点击我": "请点击我",
"请确认以下设置信息,点击\"初始化系统\"开始配置": "请确认以下设置信息,点击\"初始化系统\"开始配置",
Expand Down