diff --git a/controller/log.go b/controller/log.go index cf3825f..9d9eb36 100644 --- a/controller/log.go +++ b/controller/log.go @@ -21,7 +21,9 @@ func GetAllLogs(c *gin.Context) { channel, _ := strconv.Atoi(c.Query("channel")) group := c.Query("group") requestId := c.Query("request_id") - logs, total, err := model.GetAllLogs(logType, startTimestamp, endTimestamp, modelName, username, tokenName, pageInfo.GetStartIdx(), pageInfo.GetPageSize(), channel, group, requestId) + chatId := c.Query("chat_id") + upstreamId := c.Query("upstream_id") + logs, total, err := model.GetAllLogs(logType, startTimestamp, endTimestamp, modelName, username, tokenName, pageInfo.GetStartIdx(), pageInfo.GetPageSize(), channel, group, requestId, chatId, upstreamId) if err != nil { common.ApiError(c, err) return @@ -42,7 +44,9 @@ func GetUserLogs(c *gin.Context) { modelName := c.Query("model_name") group := c.Query("group") requestId := c.Query("request_id") - logs, total, err := model.GetUserLogs(userId, logType, startTimestamp, endTimestamp, modelName, tokenName, pageInfo.GetStartIdx(), pageInfo.GetPageSize(), group, requestId) + chatId := c.Query("chat_id") + upstreamId := c.Query("upstream_id") + logs, total, err := model.GetUserLogs(userId, logType, startTimestamp, endTimestamp, modelName, tokenName, pageInfo.GetStartIdx(), pageInfo.GetPageSize(), group, requestId, chatId, upstreamId) if err != nil { common.ApiError(c, err) return diff --git a/controller/log_identity_test.go b/controller/log_identity_test.go new file mode 100644 index 0000000..79b2562 --- /dev/null +++ b/controller/log_identity_test.go @@ -0,0 +1,94 @@ +package controller + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/model" + "github.com/glebarez/sqlite" + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/require" + "gorm.io/gorm" +) + +func setupControllerLogIdentityDB(t *testing.T) *gorm.DB { + t.Helper() + + db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) + require.NoError(t, err) + + origLogDB := model.LOG_DB + origSQLite := common.UsingSQLite + origMySQL := common.UsingMySQL + origPostgreSQL := common.UsingPostgreSQL + + model.LOG_DB = db + common.UsingSQLite = true + common.UsingMySQL = false + common.UsingPostgreSQL = false + + require.NoError(t, db.AutoMigrate(&model.Log{})) + + t.Cleanup(func() { + model.LOG_DB = origLogDB + common.UsingSQLite = origSQLite + common.UsingMySQL = origMySQL + common.UsingPostgreSQL = origPostgreSQL + }) + + return db +} + +func TestGetAllLogsPassesChatIDAndUpstreamIDFilters(t *testing.T) { + db := setupControllerLogIdentityDB(t) + + require.NoError(t, db.Create(&model.Log{ + UserId: 1, + Username: "alice", + CreatedAt: 1714465001, + Type: model.LogTypeConsume, + Content: "usage-a", + ModelName: "gpt-4o-mini", + TokenName: "demo", + RequestId: "req_a", + ChatId: "chat_a", + UpstreamId: "up_a", + }).Error) + require.NoError(t, db.Create(&model.Log{ + UserId: 1, + Username: "alice", + CreatedAt: 1714465002, + Type: model.LogTypeConsume, + Content: "usage-b", + ModelName: "gpt-4o-mini", + TokenName: "demo", + RequestId: "req_b", + ChatId: "chat_b", + UpstreamId: "up_b", + }).Error) + + gin.SetMode(gin.TestMode) + router := gin.New() + router.GET("/api/log", GetAllLogs) + + req := httptest.NewRequest(http.MethodGet, "/api/log?type=2&chat_id=chat_a&upstream_id=up_a", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + require.Equal(t, http.StatusOK, w.Code) + + var payload struct { + Success bool `json:"success"` + Data struct { + Items []model.Log `json:"items"` + } `json:"data"` + } + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &payload)) + require.True(t, payload.Success) + require.Len(t, payload.Data.Items, 1) + require.Equal(t, "chat_a", payload.Data.Items[0].ChatId) + require.Equal(t, "up_a", payload.Data.Items[0].UpstreamId) +} diff --git a/model/log.go b/model/log.go index 2d4782f..c90cb91 100644 --- a/model/log.go +++ b/model/log.go @@ -8,6 +8,7 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/logger" + relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" @@ -36,6 +37,8 @@ type Log struct { Group string `json:"group" gorm:"index"` Ip string `json:"ip" gorm:"index;default:''"` RequestId string `json:"request_id,omitempty" gorm:"type:varchar(64);index:idx_logs_request_id;default:''"` + ChatId string `json:"chat_id,omitempty" gorm:"type:varchar(128);index:idx_logs_chat_id;default:''"` + UpstreamId string `json:"upstream_id,omitempty" gorm:"type:varchar(128);index:idx_logs_upstream_id;default:''"` Other string `json:"other"` } @@ -94,6 +97,8 @@ func RecordErrorLog(c *gin.Context, userId int, channelId int, modelName string, logger.LogInfo(c, fmt.Sprintf("record error log: userId=%d, channelId=%d, modelName=%s, tokenName=%s, content=%s", userId, channelId, modelName, tokenName, content)) username := c.GetString("username") requestId := c.GetString(common.RequestIdKey) + chatId := relaycommon.GetRelayChatID(c) + upstreamId := relaycommon.GetRelayUpstreamID(c) otherStr := common.MapToJsonStr(other) // 判断是否需要记录 IP needRecordIp := false @@ -124,8 +129,10 @@ func RecordErrorLog(c *gin.Context, userId int, channelId int, modelName string, } return "" }(), - RequestId: requestId, - Other: otherStr, + RequestId: requestId, + ChatId: chatId, + UpstreamId: upstreamId, + Other: otherStr, } err := LOG_DB.Create(log).Error if err != nil { @@ -155,6 +162,8 @@ func RecordConsumeLog(c *gin.Context, userId int, params RecordConsumeLogParams) logger.LogInfo(c, fmt.Sprintf("record consume log: userId=%d, params=%s", userId, common.GetJsonString(params))) username := c.GetString("username") requestId := c.GetString(common.RequestIdKey) + chatId := relaycommon.GetRelayChatID(c) + upstreamId := relaycommon.GetRelayUpstreamID(c) otherStr := common.MapToJsonStr(params.Other) // 判断是否需要记录 IP needRecordIp := false @@ -185,8 +194,10 @@ func RecordConsumeLog(c *gin.Context, userId int, params RecordConsumeLogParams) } return "" }(), - RequestId: requestId, - Other: otherStr, + RequestId: requestId, + ChatId: chatId, + UpstreamId: upstreamId, + Other: otherStr, } err := LOG_DB.Create(log).Error if err != nil { @@ -242,7 +253,7 @@ func RecordTaskBillingLog(params RecordTaskBillingLogParams) { } } -func GetAllLogs(logType int, startTimestamp int64, endTimestamp int64, modelName string, username string, tokenName string, startIdx int, num int, channel int, group string, requestId string) (logs []*Log, total int64, err error) { +func GetAllLogs(logType int, startTimestamp int64, endTimestamp int64, modelName string, username string, tokenName string, startIdx int, num int, channel int, group string, requestId string, chatId string, upstreamId string) (logs []*Log, total int64, err error) { var tx *gorm.DB if logType == LogTypeUnknown { tx = LOG_DB @@ -262,6 +273,12 @@ func GetAllLogs(logType int, startTimestamp int64, endTimestamp int64, modelName if requestId != "" { tx = tx.Where("logs.request_id = ?", requestId) } + if chatId != "" { + tx = tx.Where("logs.chat_id = ?", chatId) + } + if upstreamId != "" { + tx = tx.Where("logs.upstream_id = ?", upstreamId) + } if startTimestamp != 0 { tx = tx.Where("logs.created_at >= ?", startTimestamp) } @@ -328,7 +345,7 @@ func GetAllLogs(logType int, startTimestamp int64, endTimestamp int64, modelName const logSearchCountLimit = 10000 -func GetUserLogs(userId int, logType int, startTimestamp int64, endTimestamp int64, modelName string, tokenName string, startIdx int, num int, group string, requestId string) (logs []*Log, total int64, err error) { +func GetUserLogs(userId int, logType int, startTimestamp int64, endTimestamp int64, modelName string, tokenName string, startIdx int, num int, group string, requestId string, chatId string, upstreamId string) (logs []*Log, total int64, err error) { var tx *gorm.DB if logType == LogTypeUnknown { tx = LOG_DB.Where("logs.user_id = ?", userId) @@ -349,6 +366,12 @@ func GetUserLogs(userId int, logType int, startTimestamp int64, endTimestamp int if requestId != "" { tx = tx.Where("logs.request_id = ?", requestId) } + if chatId != "" { + tx = tx.Where("logs.chat_id = ?", chatId) + } + if upstreamId != "" { + tx = tx.Where("logs.upstream_id = ?", upstreamId) + } if startTimestamp != 0 { tx = tx.Where("logs.created_at >= ?", startTimestamp) } diff --git a/model/log_identity_test.go b/model/log_identity_test.go new file mode 100644 index 0000000..afccdef --- /dev/null +++ b/model/log_identity_test.go @@ -0,0 +1,197 @@ +package model + +import ( + "net/http/httptest" + "testing" + + "github.com/QuantumNous/new-api/common" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/gin-gonic/gin" + "github.com/glebarez/sqlite" + "github.com/stretchr/testify/require" + "gorm.io/gorm" +) + +func setupLogIdentityDB(t *testing.T) *gorm.DB { + t.Helper() + + db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) + require.NoError(t, err) + + origDB := DB + origLogDB := LOG_DB + origSQLite := common.UsingSQLite + origMySQL := common.UsingMySQL + origPostgres := common.UsingPostgreSQL + + DB = db + LOG_DB = db + common.UsingSQLite = true + common.UsingMySQL = false + common.UsingPostgreSQL = false + initCol() + + require.NoError(t, db.AutoMigrate(&Log{})) + + t.Cleanup(func() { + DB = origDB + LOG_DB = origLogDB + common.UsingSQLite = origSQLite + common.UsingMySQL = origMySQL + common.UsingPostgreSQL = origPostgres + initCol() + }) + + return db +} + +func TestLogPersistsChatIDAndUpstreamID(t *testing.T) { + db := setupLogIdentityDB(t) + + entry := &Log{ + UserId: 1, + Username: "alice", + CreatedAt: 1714464000, + Type: LogTypeConsume, + Content: "usage", + ModelName: "gpt-4o-mini", + TokenName: "demo", + ChatId: "chat_123", + UpstreamId: "req_upstream_123", + RequestId: "req_internal_123", + } + + require.NoError(t, db.Create(entry).Error) + + var saved Log + require.NoError(t, db.First(&saved).Error) + require.Equal(t, "chat_123", saved.ChatId) + require.Equal(t, "req_upstream_123", saved.UpstreamId) + require.Equal(t, "req_internal_123", saved.RequestId) +} + +func TestGetAllLogsFiltersByChatIDAndUpstreamID(t *testing.T) { + db := setupLogIdentityDB(t) + + require.NoError(t, db.Create(&Log{ + UserId: 1, + Username: "alice", + CreatedAt: 1714464001, + Type: LogTypeConsume, + Content: "usage-a", + ModelName: "gpt-4o-mini", + TokenName: "demo", + ChatId: "chat_a", + UpstreamId: "up_a", + RequestId: "req_a", + }).Error) + require.NoError(t, db.Create(&Log{ + UserId: 2, + Username: "bob", + CreatedAt: 1714464002, + Type: LogTypeConsume, + Content: "usage-b", + ModelName: "gpt-4o-mini", + TokenName: "demo", + ChatId: "chat_b", + UpstreamId: "up_b", + RequestId: "req_b", + }).Error) + + logs, total, err := GetAllLogs(LogTypeConsume, 0, 0, "", "", "", 0, 20, 0, "", "", "chat_a", "") + require.NoError(t, err) + require.Equal(t, int64(1), total) + require.Len(t, logs, 1) + require.Equal(t, "chat_a", logs[0].ChatId) + + logs, total, err = GetAllLogs(LogTypeConsume, 0, 0, "", "", "", 0, 20, 0, "", "", "", "up_b") + require.NoError(t, err) + require.Equal(t, int64(1), total) + require.Len(t, logs, 1) + require.Equal(t, "up_b", logs[0].UpstreamId) +} + +func TestGetUserLogsFiltersByChatIDAndUpstreamID(t *testing.T) { + db := setupLogIdentityDB(t) + + require.NoError(t, db.Create(&Log{ + UserId: 9, + Username: "owner", + CreatedAt: 1714464010, + Type: LogTypeConsume, + Content: "usage-owner-a", + ModelName: "gpt-4o-mini", + TokenName: "demo", + ChatId: "chat-owner-a", + UpstreamId: "up-owner-a", + RequestId: "req-owner-a", + }).Error) + require.NoError(t, db.Create(&Log{ + UserId: 9, + Username: "owner", + CreatedAt: 1714464011, + Type: LogTypeConsume, + Content: "usage-owner-b", + ModelName: "gpt-4o-mini", + TokenName: "demo", + ChatId: "chat-owner-b", + UpstreamId: "up-owner-b", + RequestId: "req-owner-b", + }).Error) + + logs, total, err := GetUserLogs(9, LogTypeConsume, 0, 0, "", "", 0, 20, "", "", "chat-owner-b", "") + require.NoError(t, err) + require.Equal(t, int64(1), total) + require.Len(t, logs, 1) + require.Equal(t, "chat-owner-b", logs[0].ChatId) + + logs, total, err = GetUserLogs(9, LogTypeConsume, 0, 0, "", "", 0, 20, "", "", "", "up-owner-a") + require.NoError(t, err) + require.Equal(t, int64(1), total) + require.Len(t, logs, 1) + require.Equal(t, "up-owner-a", logs[0].UpstreamId) +} + +func TestRecordConsumeLogPersistsContextChatIDAndUpstreamID(t *testing.T) { + db := setupLogIdentityDB(t) + + gin.SetMode(gin.TestMode) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Set("username", "alice") + c.Set(common.RequestIdKey, "req_internal_ctx") + relaycommon.SetRelayChatID(c, "chat_ctx") + relaycommon.SetRelayUpstreamID(c, "up_ctx") + + RecordConsumeLog(c, 1, RecordConsumeLogParams{ + ChannelId: 1, + ModelName: "gpt-4o-mini", + TokenName: "demo", + Content: "usage", + Quota: 10, + }) + + var saved Log + require.NoError(t, db.First(&saved).Error) + require.Equal(t, "chat_ctx", saved.ChatId) + require.Equal(t, "up_ctx", saved.UpstreamId) +} + +func TestRecordErrorLogPersistsContextChatIDAndUpstreamID(t *testing.T) { + db := setupLogIdentityDB(t) + + gin.SetMode(gin.TestMode) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Set("username", "alice") + c.Set(common.RequestIdKey, "req_internal_err") + relaycommon.SetRelayChatID(c, "chat_err") + relaycommon.SetRelayUpstreamID(c, "up_err") + + RecordErrorLog(c, 1, 2, "gpt-4o-mini", "demo", "upstream failed", 3, 4, false, "default", map[string]interface{}{}) + + var saved Log + require.NoError(t, db.First(&saved).Error) + require.Equal(t, "chat_err", saved.ChatId) + require.Equal(t, "up_err", saved.UpstreamId) +} diff --git a/relay/channel/api_request.go b/relay/channel/api_request.go index 407ca2d..43def3e 100644 --- a/relay/channel/api_request.go +++ b/relay/channel/api_request.go @@ -360,10 +360,14 @@ func DoWssRequest(a Adaptor, c *gin.Context, info *common.RelayInfo, requestBody targetHeader.Set(key, value) } targetHeader.Set("Content-Type", c.Request.Header.Get("Content-Type")) - targetConn, _, err := websocket.DefaultDialer.Dial(fullRequestURL, targetHeader) + targetConn, resp, err := websocket.DefaultDialer.Dial(fullRequestURL, targetHeader) if err != nil { return nil, fmt.Errorf("dial failed to %s: %w", fullRequestURL, err) } + if resp != nil { + common.CaptureUpstreamIDFromHTTPResponse(info, resp) + common.SetRelayUpstreamID(c, info.UpstreamID) + } // send request body //all, err := io.ReadAll(requestBody) //err = service.WssString(c, targetConn, string(all)) @@ -512,6 +516,8 @@ func doRequest(c *gin.Context, req *http.Request, info *common.RelayInfo) (*http if resp == nil { return nil, errors.New("resp is nil") } + common.CaptureUpstreamIDFromHTTPResponse(info, resp) + common.SetRelayUpstreamID(c, info.UpstreamID) _ = req.Body.Close() _ = c.Request.Body.Close() diff --git a/relay/common/relay_info.go b/relay/common/relay_info.go index 66f95c9..3877ad4 100644 --- a/relay/common/relay_info.go +++ b/relay/common/relay_info.go @@ -87,6 +87,8 @@ type RelayInfo struct { TokenKey string TokenGroup string UserId int + ChatID string + UpstreamID string UsingGroup string // 使用的分组,当auto跨分组重试时,会变动 UserGroup string // 用户所在分组 TokenUnlimited bool @@ -452,6 +454,7 @@ func genBaseRelayInfo(c *gin.Context, request dto.Request) *RelayInfo { RequestId: reqId, UserId: common.GetContextKeyInt(c, constant.ContextKeyUserId), + ChatID: GetRelayChatID(c), UsingGroup: common.GetContextKeyString(c, constant.ContextKeyUsingGroup), UserGroup: common.GetContextKeyString(c, constant.ContextKeyUserGroup), UserQuota: common.GetContextKeyInt(c, constant.ContextKeyUserQuota), diff --git a/relay/common/request_identity.go b/relay/common/request_identity.go new file mode 100644 index 0000000..513199f --- /dev/null +++ b/relay/common/request_identity.go @@ -0,0 +1,82 @@ +package common + +import ( + "net/http" + "strings" + + commonpkg "github.com/QuantumNous/new-api/common" + "github.com/gin-gonic/gin" +) + +const ( + ContextKeyRelayChatID = "relay_chat_id" + ContextKeyRelayUpstreamID = "relay_upstream_id" +) + +func SetRelayChatID(c *gin.Context, chatID string) { + if c == nil { + return + } + chatID = strings.TrimSpace(chatID) + if chatID == "" { + return + } + c.Set(ContextKeyRelayChatID, chatID) +} + +func GetRelayChatID(c *gin.Context) string { + if c == nil { + return "" + } + return strings.TrimSpace(c.GetString(ContextKeyRelayChatID)) +} + +func SetRelayUpstreamID(c *gin.Context, upstreamID string) { + if c == nil { + return + } + upstreamID = strings.TrimSpace(upstreamID) + if upstreamID == "" { + return + } + c.Set(ContextKeyRelayUpstreamID, upstreamID) +} + +func GetRelayUpstreamID(c *gin.Context) string { + if c == nil { + return "" + } + return strings.TrimSpace(c.GetString(ContextKeyRelayUpstreamID)) +} + +func ExtractTopLevelChatID(c *gin.Context) (string, error) { + if c == nil { + return "", nil + } + bodyStorage, err := commonpkg.GetBodyStorage(c) + if err != nil { + return "", err + } + bodyBytes, err := bodyStorage.Bytes() + if err != nil || len(bodyBytes) == 0 { + return "", err + } + var payload map[string]interface{} + if err := commonpkg.Unmarshal(bodyBytes, &payload); err != nil { + return "", nil + } + return strings.TrimSpace(commonpkg.Interface2String(payload["chat_id"])), nil +} + +func CaptureUpstreamIDFromHTTPResponse(info *RelayInfo, resp *http.Response) { + if info == nil || resp == nil { + return + } + for _, key := range []string{"x-request-id", "request-id"} { + value := strings.TrimSpace(resp.Header.Get(key)) + if value != "" { + info.UpstreamID = value + return + } + } +} diff --git a/relay/common/request_identity_test.go b/relay/common/request_identity_test.go new file mode 100644 index 0000000..47040c1 --- /dev/null +++ b/relay/common/request_identity_test.go @@ -0,0 +1,89 @@ +package common + +import ( + "bytes" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/require" +) + +func TestExtractChatIDFromReusableBody(t *testing.T) { + gin.SetMode(gin.TestMode) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = httptest.NewRequest(http.MethodPost, "/v1/chat/completions", bytes.NewBufferString(`{"model":"gpt-4o-mini","chat_id":"chat_abc","messages":[{"role":"user","content":"hi"}]}`)) + c.Request.Header.Set("Content-Type", "application/json") + + chatID, err := ExtractTopLevelChatID(c) + require.NoError(t, err) + require.Equal(t, "chat_abc", chatID) +} + +func TestExtractChatIDReturnsEmptyWhenMissing(t *testing.T) { + gin.SetMode(gin.TestMode) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = httptest.NewRequest(http.MethodPost, "/v1/chat/completions", bytes.NewBufferString(`{"model":"gpt-4o-mini","messages":[{"role":"user","content":"hi"}]}`)) + c.Request.Header.Set("Content-Type", "application/json") + + chatID, err := ExtractTopLevelChatID(c) + require.NoError(t, err) + require.Empty(t, chatID) +} + +func TestCaptureUpstreamIDFromHeaderPrefersXRequestID(t *testing.T) { + info := &RelayInfo{} + resp := &http.Response{ + Header: http.Header{ + "X-Request-Id": []string{"xreq_123"}, + "Request-Id": []string{"req_456"}, + }, + } + + CaptureUpstreamIDFromHTTPResponse(info, resp) + require.Equal(t, "xreq_123", info.UpstreamID) +} + +func TestCaptureUpstreamIDFallsBackToRequestID(t *testing.T) { + info := &RelayInfo{} + resp := &http.Response{ + Header: http.Header{ + "Request-Id": []string{"req_456"}, + }, + } + + CaptureUpstreamIDFromHTTPResponse(info, resp) + require.Equal(t, "req_456", info.UpstreamID) +} + +func TestSetAndGetChatIDFromGinContext(t *testing.T) { + gin.SetMode(gin.TestMode) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + + SetRelayChatID(c, "chat_ctx_1") + require.Equal(t, "chat_ctx_1", GetRelayChatID(c)) +} + +func TestCaptureUpstreamIDDoesNotOverwriteExistingValueWithEmptyHeader(t *testing.T) { + info := &RelayInfo{UpstreamID: "existing_upstream"} + resp := &http.Response{Header: http.Header{}} + + CaptureUpstreamIDFromHTTPResponse(info, resp) + require.Equal(t, "existing_upstream", info.UpstreamID) +} + +func TestCaptureUpstreamIDFromDialResponseHeaders(t *testing.T) { + info := &RelayInfo{} + resp := &http.Response{ + Header: http.Header{ + "X-Request-Id": []string{"ws_upstream_1"}, + }, + } + + CaptureUpstreamIDFromHTTPResponse(info, resp) + require.Equal(t, "ws_upstream_1", info.UpstreamID) +} diff --git a/relay/helper/valid_request.go b/relay/helper/valid_request.go index 750f749..a7ac6c7 100644 --- a/relay/helper/valid_request.go +++ b/relay/helper/valid_request.go @@ -10,6 +10,7 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/logger" + relaycommon "github.com/QuantumNous/new-api/relay/common" relayconstant "github.com/QuantumNous/new-api/relay/constant" "github.com/QuantumNous/new-api/types" @@ -17,6 +18,10 @@ import ( ) func GetAndValidateRequest(c *gin.Context, format types.RelayFormat) (request dto.Request, err error) { + if chatID, extractErr := relaycommon.ExtractTopLevelChatID(c); extractErr == nil && chatID != "" { + relaycommon.SetRelayChatID(c, chatID) + } + relayMode := relayconstant.Path2RelayMode(c.Request.URL.Path) switch format { diff --git a/web/src/components/table/usage-logs/UsageLogsFilters.jsx b/web/src/components/table/usage-logs/UsageLogsFilters.jsx index 8d0d837..f34858e 100644 --- a/web/src/components/table/usage-logs/UsageLogsFilters.jsx +++ b/web/src/components/table/usage-logs/UsageLogsFilters.jsx @@ -102,6 +102,24 @@ const LogsFilters = ({ size='small' /> +