Skip to content
Closed
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
2 changes: 2 additions & 0 deletions common/api_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ func ChannelType2APIType(channelType int) (int, bool) {
apiType = constant.APITypeReplicate
case constant.ChannelTypeCodex:
apiType = constant.APITypeCodex
case constant.ChannelTypeChatGPTWeb:
apiType = constant.APITypeChatGPTWeb
}
if apiType == -1 {
return constant.APITypeOpenAI, false
Expand Down
1 change: 1 addition & 0 deletions common/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ var DebugEnabled bool
var MemoryCacheEnabled bool

var LogConsumeEnabled = true
var LogRequestBodyEnabled = false

var TLSInsecureSkipVerify bool
var InsecureTLSConfig = &tls.Config{InsecureSkipVerify: true}
Expand Down
1 change: 1 addition & 0 deletions constant/api_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,6 @@ const (
APITypeMiniMax
APITypeReplicate
APITypeCodex
APITypeChatGPTWeb
APITypeDummy // this one is only for count, do not add any channel after this
)
3 changes: 3 additions & 0 deletions constant/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ const (
ChannelTypeSora = 55
ChannelTypeReplicate = 56
ChannelTypeCodex = 57
ChannelTypeChatGPTWeb = 58 // ChatGPT 网页逆向(/backend-api/conversation,用订阅账号 OAuth token)
ChannelTypeDummy // this one is only for count, do not add any channel after this

)
Expand Down Expand Up @@ -118,6 +119,7 @@ var ChannelBaseURLs = []string{
"https://api.openai.com", //55
"https://api.replicate.com", //56
"https://chatgpt.com", //57
"https://chatgpt.com", //58 ChatGPTWeb
}

var ChannelTypeNames = map[int]string{
Expand Down Expand Up @@ -175,6 +177,7 @@ var ChannelTypeNames = map[int]string{
ChannelTypeSora: "Sora",
ChannelTypeReplicate: "Replicate",
ChannelTypeCodex: "Codex",
ChannelTypeChatGPTWeb: "ChatGPTWeb",
}

func GetChannelTypeName(channelType int) string {
Expand Down
21 changes: 21 additions & 0 deletions controller/relay.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,21 @@ func geminiRelayHandler(c *gin.Context, info *relaycommon.RelayInfo) *types.NewA
func Relay(c *gin.Context, relayFormat types.RelayFormat) {

requestId := c.GetString(common.RequestIdKey)
relayStartTime := time.Now()
//group := common.GetContextKeyString(c, constant.ContextKeyUsingGroup)
//originalModel := common.GetContextKeyString(c, constant.ContextKeyOriginalModel)

// Capture request body for request logging
var capturedRequestBody string
if common.LogRequestBodyEnabled {
if bodyStorage, bodyErr := common.GetBodyStorage(c); bodyErr == nil {
if bodyBytes, readErr := bodyStorage.Bytes(); readErr == nil {
capturedRequestBody = string(bodyBytes)
}
bodyStorage.Seek(0, 0)
}
}

var (
newAPIError *types.NewAPIError
ws *websocket.Conn
Expand Down Expand Up @@ -103,6 +115,15 @@ func Relay(c *gin.Context, relayFormat types.RelayFormat) {
})
}
}
// Save request log after relay completes
if common.LogRequestBodyEnabled && capturedRequestBody != "" {
responseBody := ""
if rb := middleware.GetCapturedResponseBody(c); len(rb) > 0 {
responseBody = string(rb)
}
statusCode := c.Writer.Status()
service.SaveRequestLog(c, capturedRequestBody, responseBody, statusCode, relayStartTime)
}
}()

request, err := helper.GetAndValidateRequest(c, relayFormat)
Expand Down
60 changes: 60 additions & 0 deletions controller/request_log.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package controller

import (
"net/http"

"github.com/QuantumNous/new-api/model"

"github.com/gin-gonic/gin"
)

// GetRequestLogDetail returns the full request/response body for a given request_id (admin)
func GetRequestLogDetail(c *gin.Context) {
requestId := c.Query("request_id")
if requestId == "" {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "request_id is required",
})
return
}
log, err := model.GetRequestLogByRequestId(requestId)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "request log not found",
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": log,
})
}

// GetUserRequestLogDetail returns the full request/response body for a given request_id (user's own)
func GetUserRequestLogDetail(c *gin.Context) {
requestId := c.Query("request_id")
if requestId == "" {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "request_id is required",
})
return
}
userId := c.GetInt("id")
log, err := model.GetRequestLogByRequestIdAndUserId(requestId, userId)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "request log not found",
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": log,
})
}
62 changes: 62 additions & 0 deletions middleware/request_log.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package middleware

import (
"bytes"

"github.com/QuantumNous/new-api/common"
"github.com/gin-gonic/gin"
)

const KeyResponseBodyWriter = "key_response_body_writer"
const maxResponseCaptureSize = 4 * 1024 * 1024 // 4MB

// responseBodyWriter wraps gin.ResponseWriter to capture the response body
type responseBodyWriter struct {
gin.ResponseWriter
body *bytes.Buffer
capped bool
}

func (w *responseBodyWriter) Write(b []byte) (int, error) {
if !w.capped {
remaining := maxResponseCaptureSize - w.body.Len()
if remaining > 0 {
if len(b) <= remaining {
w.body.Write(b)
} else {
w.body.Write(b[:remaining])
w.body.WriteString("\n...[truncated]")
w.capped = true
}
}
}
return w.ResponseWriter.Write(b)
}

// GetCapturedResponseBody returns the captured bytes from the writer stored in context.
func GetCapturedResponseBody(c *gin.Context) []byte {
if w, exists := c.Get(KeyResponseBodyWriter); exists {
if rbw, ok := w.(*responseBodyWriter); ok {
return rbw.body.Bytes()
}
}
return nil
}

// RequestLogCapture captures the response body for request logging.
// Only active when LogRequestBodyEnabled is true.
func RequestLogCapture() gin.HandlerFunc {
return func(c *gin.Context) {
if !common.LogRequestBodyEnabled {
c.Next()
return
}
writer := &responseBodyWriter{
ResponseWriter: c.Writer,
body: &bytes.Buffer{},
}
c.Writer = writer
c.Set(KeyResponseBodyWriter, writer)
c.Next()
}
}
11 changes: 11 additions & 0 deletions model/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,9 @@ func InitDB() (err error) {
func InitLogDB() (err error) {
if os.Getenv("LOG_SQL_DSN") == "" {
LOG_DB = DB
if common.IsMasterNode {
err = migrateLOGDB()
}
return
}
db, err := chooseDB("LOG_SQL_DSN", true)
Expand Down Expand Up @@ -370,6 +373,14 @@ func migrateLOGDB() error {
if err = LOG_DB.AutoMigrate(&Log{}); err != nil {
return err
}
if err = LOG_DB.AutoMigrate(&RequestLog{}); err != nil {
return err
}
// Migrate TEXT -> MEDIUMTEXT for MySQL (TEXT is 64KB, too small for large requests)
if common.UsingMySQL || common.LogSqlType == common.DatabaseTypeMySQL {
LOG_DB.Exec("ALTER TABLE `request_logs` MODIFY `request_body` MEDIUMTEXT")
LOG_DB.Exec("ALTER TABLE `request_logs` MODIFY `response_body` MEDIUMTEXT")
}
return nil
}

Expand Down
3 changes: 3 additions & 0 deletions model/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ func InitOptionMap() {
common.OptionMap["AutomaticDisableChannelEnabled"] = strconv.FormatBool(common.AutomaticDisableChannelEnabled)
common.OptionMap["AutomaticEnableChannelEnabled"] = strconv.FormatBool(common.AutomaticEnableChannelEnabled)
common.OptionMap["LogConsumeEnabled"] = strconv.FormatBool(common.LogConsumeEnabled)
common.OptionMap["LogRequestBodyEnabled"] = strconv.FormatBool(common.LogRequestBodyEnabled)
common.OptionMap["DisplayInCurrencyEnabled"] = strconv.FormatBool(common.DisplayInCurrencyEnabled)
common.OptionMap["DisplayTokenStatEnabled"] = strconv.FormatBool(common.DisplayTokenStatEnabled)
common.OptionMap["DrawingEnabled"] = strconv.FormatBool(common.DrawingEnabled)
Expand Down Expand Up @@ -264,6 +265,8 @@ func updateOptionMap(key string, value string) (err error) {
common.AutomaticEnableChannelEnabled = boolValue
case "LogConsumeEnabled":
common.LogConsumeEnabled = boolValue
case "LogRequestBodyEnabled":
common.LogRequestBodyEnabled = boolValue
case "DisplayInCurrencyEnabled":
// 兼容旧字段:同步到新配置 general_setting.quota_display_type(运行时生效)
// true -> USD, false -> TOKENS
Expand Down
66 changes: 66 additions & 0 deletions model/request_log.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package model

// RequestLog stores the full request and response body for API relay calls.
// Linked to the Log table via RequestId for detail lookups.
type RequestLog struct {
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
UserId int `json:"user_id" gorm:"index"`
CreatedAt int64 `json:"created_at" gorm:"bigint;index"`
RequestId string `json:"request_id" gorm:"type:varchar(64);index:idx_request_log_request_id;default:''"`
RequestBody string `json:"request_body" gorm:"type:mediumtext"`
ResponseBody string `json:"response_body" gorm:"type:mediumtext"`
ModelName string `json:"model_name" gorm:"type:varchar(255);index;default:''"`
TokenName string `json:"token_name" gorm:"type:varchar(255);default:''"`
ChannelId int `json:"channel_id" gorm:"default:0"`
Endpoint string `json:"endpoint" gorm:"type:varchar(512);default:''"`
StatusCode int `json:"status_code" gorm:"default:0"`
UseTime int `json:"use_time" gorm:"default:0"`
IsStream bool `json:"is_stream"`
}

func CreateRequestLog(log *RequestLog) error {
return LOG_DB.Create(log).Error
}

func GetRequestLogByRequestId(requestId string) (*RequestLog, error) {
var log RequestLog
err := LOG_DB.Where("request_id = ?", requestId).First(&log).Error
if err != nil {
return nil, err
}
return &log, nil
}

func GetRequestLogByRequestIdAndUserId(requestId string, userId int) (*RequestLog, error) {
var log RequestLog
err := LOG_DB.Where("request_id = ? AND user_id = ?", requestId, userId).First(&log).Error
if err != nil {
return nil, err
}
return &log, nil
}

func GetAllRequestLogs(startTimestamp int64, endTimestamp int64, modelName string, username string, startIdx int, num int) (logs []*RequestLog, total int64, err error) {
tx := LOG_DB.Model(&RequestLog{})

if modelName != "" {
tx = tx.Where("model_name like ?", modelName)
}
if startTimestamp != 0 {
tx = tx.Where("created_at >= ?", startTimestamp)
}
if endTimestamp != 0 {
tx = tx.Where("created_at <= ?", endTimestamp)
}
err = tx.Count(&total).Error
if err != nil {
return nil, 0, err
}
err = tx.Order("id desc").Limit(num).Offset(startIdx).Find(&logs).Error
return logs, total, err
}

func DeleteOldRequestLog(targetTimestamp int64, limit int) (int64, error) {
result := LOG_DB.Where("created_at < ?", targetTimestamp).Limit(limit).Delete(&RequestLog{})
return result.RowsAffected, result.Error
}
Loading
Loading