Skip to content
Open
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
1 change: 1 addition & 0 deletions common/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ func positiveUserSessionEnv(name string, fallback int) int {

func initConstantEnv() {
constant.StreamingTimeout = GetEnvOrDefault("STREAMING_TIMEOUT", 300)
constant.ThinkingStreamingTimeout = GetEnvOrDefault("THINKING_STREAMING_TIMEOUT", 900)

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n 'func GetEnvOrDefault|THINKING_STREAMING_TIMEOUT|time\.Duration\(.*time\.Second|time\.NewTicker' \
  common constant relay

Repository: QuantumNous/new-api

Length of output: 1985


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== common/env.go =="
cat -n common/env.go

echo
echo "== common/init.go (near init timeout settings) =="
sed -n '90,185p' common/init.go | cat -n

echo
echo "== relay/helper/stream_scanner.go (scanner timeout usage) =="
sed -n '75,105p' relay/helper/stream_scanner.go | cat -n

echo
echo "== constant package definitions/usages =="
rg -n 'var ThinkingStreamingTimeout|ThinkingStreamingTimeout|type Duration|Duration' constant relay helper common | head -n 80

echo
echo "== Go Duration overflow probe =="
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
cat > "$tmpdir/main.go" <<'EOF'
package main

import (
	"fmt"
	"time"
)

func main() {
	values := []int{900, 1<<30, 2147483647, (1 << 31), -1 << 31}
	for _, v := range values {
		d := time.Duration(v) * time.Second
		fmt.Printf("%d -> %d ns\n", v, d)
	}
}
EOF
go run "$tmpdir/main.go"

Repository: QuantumNous/new-api

Length of output: 15491


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== package declarations for relay/helper and constant =="
sed -n '1,30p' relay/helper/stream_scanner.go | cat -n
fd -a '.*\.go$' relay/helper constant | while read -r f; do
  echo "--- $f"
  sed -n '1,60p' "$f" | cat -n
done

echo
echo "== Go Duration overflow probe =="
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
cat > "$tmpdir/main.go" <<'EOF'
package main

import (
	"fmt"
	"math"
	"time"
)

func main() {
	values := []int{900, 1 << 30, 2147483647, -1 << 31}
	for _, v := range values {
		d := time.Duration(v) * time.Second
		fmt.Printf("%d -> %d\n", v, d)
	}
	fmt.Printf("MaxDurationSeconds=%d\n", time.Duration(math.MaxInt64/time.Second/2)*time.Second/time.Second)
	panic(time.Duration(2147483647) * time.Second)
}
EOF
go run "$tmpdir/main.go" 2>&1 || true

Repository: QuantumNous/new-api

Length of output: 42741


Bound THINKING_STREAMING_TIMEOUT before publishing it.

common/init.go:178 stores the parsed value directly in constant.ThinkingStreamingTimeout, and relay/helper/stream_scanner.go:97 passes the converted time.Duration to time.NewTicker. Large configured values can overflow or normalize unexpectedly; normalize/reject values above a safe upper bound during initialization.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@common/init.go` at line 178, Update the initialization of
constant.ThinkingStreamingTimeout in common/init.go to validate or clamp the
parsed THINKING_STREAMING_TIMEOUT value before publishing it, using a safe upper
bound that prevents overflow or unexpected normalization when
relay/helper/stream_scanner.go passes it to time.NewTicker. Preserve the
existing default behavior for valid values.

constant.DifyDebug = GetEnvOrDefaultBool("DIFY_DEBUG", true)
constant.MaxFileDownloadMB = GetEnvOrDefault("MAX_FILE_DOWNLOAD_MB", 64)
constant.StreamScannerMaxBufferMB = GetEnvOrDefault("STREAM_SCANNER_MAX_BUFFER_MB", 128)
Expand Down
1 change: 1 addition & 0 deletions constant/env.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package constant

var StreamingTimeout int
var ThinkingStreamingTimeout int
var DifyDebug bool
var MaxFileDownloadMB int
var StreamScannerMaxBufferMB int
Expand Down
27 changes: 10 additions & 17 deletions controller/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ func GetAllLogs(c *gin.Context) {
group := c.Query("group")
requestId := c.Query("request_id")
upstreamRequestId := c.Query("upstream_request_id")
logs, total, err := model.GetAllLogs(logType, startTimestamp, endTimestamp, modelName, username, tokenName, pageInfo.GetStartIdx(), pageInfo.GetPageSize(), channel, group, requestId, upstreamRequestId)
ip := c.Query("ip")
logs, total, err := model.GetAllLogs(logType, startTimestamp, endTimestamp, modelName, username, tokenName, pageInfo.GetStartIdx(), pageInfo.GetPageSize(), channel, group, requestId, upstreamRequestId, ip)
if err != nil {
common.ApiError(c, err)
return
Expand All @@ -44,7 +45,8 @@ func GetUserLogs(c *gin.Context) {
group := c.Query("group")
requestId := c.Query("request_id")
upstreamRequestId := c.Query("upstream_request_id")
logs, total, err := model.GetUserLogs(userId, logType, startTimestamp, endTimestamp, modelName, tokenName, pageInfo.GetStartIdx(), pageInfo.GetPageSize(), group, requestId, upstreamRequestId)
ip := c.Query("ip")
logs, total, err := model.GetUserLogs(userId, logType, startTimestamp, endTimestamp, modelName, tokenName, pageInfo.GetStartIdx(), pageInfo.GetPageSize(), group, requestId, upstreamRequestId, ip)
if err != nil {
common.ApiError(c, err)
return
Expand Down Expand Up @@ -104,20 +106,16 @@ func GetLogsStat(c *gin.Context) {
modelName := c.Query("model_name")
channel, _ := strconv.Atoi(c.Query("channel"))
group := c.Query("group")
stat, err := model.SumUsedQuota(logType, startTimestamp, endTimestamp, modelName, username, tokenName, channel, group)
ip := c.Query("ip")
stat, err := model.SumUsedQuota(logType, startTimestamp, endTimestamp, modelName, username, tokenName, channel, group, ip)
if err != nil {
common.ApiError(c, err)
return
}
//tokenNum := model.SumUsedToken(logType, startTimestamp, endTimestamp, modelName, username, "")
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": gin.H{
"quota": stat.Quota,
"rpm": stat.Rpm,
"tpm": stat.Tpm,
},
"data": stat,
})
return
}
Expand All @@ -131,21 +129,16 @@ func GetLogsSelfStat(c *gin.Context) {
modelName := c.Query("model_name")
channel, _ := strconv.Atoi(c.Query("channel"))
group := c.Query("group")
quotaNum, err := model.SumUsedQuota(logType, startTimestamp, endTimestamp, modelName, username, tokenName, channel, group)
ip := c.Query("ip")
stat, err := model.SumUsedQuota(logType, startTimestamp, endTimestamp, modelName, username, tokenName, channel, group, ip)
if err != nil {
common.ApiError(c, err)
return
}
//tokenNum := model.SumUsedToken(logType, startTimestamp, endTimestamp, modelName, username, tokenName)
c.JSON(200, gin.H{
"success": true,
"message": "",
"data": gin.H{
"quota": quotaNum.Quota,
"rpm": quotaNum.Rpm,
"tpm": quotaNum.Tpm,
//"token": tokenNum,
},
"data": stat,
})
return
}
167 changes: 118 additions & 49 deletions model/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -350,13 +350,6 @@ func RecordConsumeLog(c *gin.Context, userId int, params RecordConsumeLogParams)
upstreamRequestId := c.GetString(common.UpstreamRequestIdKey)
createdAt := common.GetTimestamp()
otherStr := common.MapToJsonStr(params.Other)
// 判断是否需要记录 IP
needRecordIp := false
if settingMap, err := GetUserSetting(userId, false); err == nil {
if settingMap.RecordIpLog {
needRecordIp = true
}
}
log := &Log{
UserId: userId,
Username: username,
Expand All @@ -373,12 +366,7 @@ func RecordConsumeLog(c *gin.Context, userId int, params RecordConsumeLogParams)
UseTime: params.UseTimeSeconds,
IsStream: params.IsStream,
Group: params.Group,
Ip: func() string {
if needRecordIp {
return c.ClientIP()
}
return ""
}(),
Ip: c.ClientIP(),
RequestId: requestId,
UpstreamRequestId: upstreamRequestId,
Other: otherStr,
Expand Down Expand Up @@ -465,7 +453,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, upstreamRequestId 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, upstreamRequestId string, ip string) (logs []*Log, total int64, err error) {
var tx *gorm.DB
if logType == LogTypeUnknown {
tx = LOG_DB
Expand Down Expand Up @@ -500,6 +488,9 @@ func GetAllLogs(logType int, startTimestamp int64, endTimestamp int64, modelName
if group != "" {
tx = tx.Where("logs."+logGroupCol+" = ?", group)
}
if ip != "" {
tx = tx.Where("logs.ip = ?", ip)
}
err = tx.Model(&Log{}).Count(&total).Error
if err != nil {
return nil, 0, err
Expand Down Expand Up @@ -561,7 +552,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, upstreamRequestId 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, upstreamRequestId string, ip string) (logs []*Log, total int64, err error) {
var tx *gorm.DB
if logType == LogTypeUnknown {
tx = LOG_DB.Where("logs.user_id = ?", userId)
Expand Down Expand Up @@ -590,6 +581,9 @@ func GetUserLogs(userId int, logType int, startTimestamp int64, endTimestamp int
if group != "" {
tx = tx.Where("logs."+logGroupCol+" = ?", group)
}
if ip != "" {
tx = tx.Where("logs.ip = ?", ip)
}
err = tx.Model(&Log{}).Limit(logSearchCountLimit).Count(&total).Error
if err != nil {
common.SysError("failed to count user logs: " + err.Error())
Expand All @@ -610,63 +604,138 @@ func GetUserLogs(userId int, logType int, startTimestamp int64, endTimestamp int
}

type Stat struct {
Quota int `json:"quota"`
Rpm int `json:"rpm"`
Tpm int `json:"tpm"`
Quota int `json:"quota"`
Rpm int `json:"rpm"`
Tpm int `json:"tpm"`
TotalRequests int64 `json:"total_requests"`
TodayRequests int64 `json:"today_requests"`
TotalTokens int64 `json:"total_tokens"`
TodayTokens int64 `json:"today_tokens"`
AvgUseTime float64 `json:"avg_use_time"`
ErrorCount int64 `json:"error_count"`
}

func SumUsedQuota(logType int, startTimestamp int64, endTimestamp int64, modelName string, username string, tokenName string, channel int, group string) (stat Stat, err error) {
tx := LOG_DB.Table("logs").Select("COALESCE(sum(quota), 0) quota")

// 为rpm和tpm创建单独的查询
rpmTpmQuery := LOG_DB.Table("logs").Select("count(*) rpm, COALESCE(sum(prompt_tokens), 0) + COALESCE(sum(completion_tokens), 0) tpm")
func todayStartUnix() int64 {
now := time.Now()
return time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).Unix()
}

func applyStatBaseFilters(tx *gorm.DB, username, tokenName, modelName string, channel int, group string, ip string) (*gorm.DB, error) {
var err error
if tx, err = applyExplicitLogTextFilter(tx, "username", username); err != nil {
return stat, err
}
if rpmTpmQuery, err = applyExplicitLogTextFilter(rpmTpmQuery, "username", username); err != nil {
return stat, err
return nil, err
}
if tokenName != "" {
tx = tx.Where("token_name = ?", tokenName)
rpmTpmQuery = rpmTpmQuery.Where("token_name = ?", tokenName)
}
if startTimestamp != 0 {
tx = tx.Where("created_at >= ?", startTimestamp)
}
if endTimestamp != 0 {
tx = tx.Where("created_at <= ?", endTimestamp)
}
if tx, err = applyExplicitLogTextFilter(tx, "model_name", modelName); err != nil {
return stat, err
}
if rpmTpmQuery, err = applyExplicitLogTextFilter(rpmTpmQuery, "model_name", modelName); err != nil {
return stat, err
return nil, err
}
if channel != 0 {
tx = tx.Where("channel_id = ?", channel)
rpmTpmQuery = rpmTpmQuery.Where("channel_id = ?", channel)
}
if group != "" {
tx = tx.Where(logGroupCol+" = ?", group)
rpmTpmQuery = rpmTpmQuery.Where(logGroupCol+" = ?", group)
}
if ip != "" {
tx = tx.Where("ip = ?", ip)
}
return tx, nil
}

tx = tx.Where("type = ?", LogTypeConsume)
rpmTpmQuery = rpmTpmQuery.Where("type = ?", LogTypeConsume)
func SumUsedQuota(logType int, startTimestamp int64, endTimestamp int64, modelName string, username string, tokenName string, channel int, group string, ip string) (stat Stat, err error) {
// quota: sum over the requested time range (all log types when logType==0)
quotaTx := LOG_DB.Table("logs").Select("COALESCE(sum(quota), 0) quota")
if quotaTx, err = applyStatBaseFilters(quotaTx, username, tokenName, modelName, channel, group, ip); err != nil {
return stat, err
}
if startTimestamp != 0 {
quotaTx = quotaTx.Where("created_at >= ?", startTimestamp)
}
if endTimestamp != 0 {
quotaTx = quotaTx.Where("created_at <= ?", endTimestamp)
}
if logType != LogTypeUnknown {
quotaTx = quotaTx.Where("type = ?", logType)
}
if err = quotaTx.Scan(&stat).Error; err != nil {
common.SysError("failed to query log stat quota: " + err.Error())
return stat, errors.New("查询统计数据失败")
}

// 只统计最近60秒的rpm和tpm
rpmTpmQuery = rpmTpmQuery.Where("created_at >= ?", time.Now().Add(-60*time.Second).Unix())
// realtime rpm/tpm: consume logs in the last 60 seconds
realtimeTx := LOG_DB.Table("logs").
Select("count(*) rpm, COALESCE(sum(prompt_tokens), 0) + COALESCE(sum(completion_tokens), 0) tpm")
if realtimeTx, err = applyStatBaseFilters(realtimeTx, username, tokenName, modelName, channel, group, ip); err != nil {
return stat, err
}
realtimeTx = realtimeTx.
Where("type = ?", LogTypeConsume).
Where("created_at >= ?", time.Now().Add(-60*time.Second).Unix())
if err = realtimeTx.Scan(&stat).Error; err != nil {
common.SysError("failed to query rpm/tpm stat: " + err.Error())
return stat, errors.New("查询统计数据失败")
}

// 执行查询
if err := tx.Scan(&stat).Error; err != nil {
common.SysError("failed to query log stat: " + err.Error())
// range aggregate: consume logs within the requested time window
type rangeResult struct {
TotalRequests int64 `gorm:"column:total_requests"`
TotalTokens int64 `gorm:"column:total_tokens"`
AvgUseTime float64 `gorm:"column:avg_use_time"`
ErrorCount int64 `gorm:"column:error_count"`
}
tokenCastType := "INTEGER"
if common.UsingLogDatabase(common.DatabaseTypeMySQL) {
tokenCastType = "SIGNED"
}
var rr rangeResult
rangeTx := LOG_DB.Table("logs").Select(
"COALESCE(SUM(CASE WHEN type = ? THEN 1 ELSE 0 END), 0) total_requests, "+
"COALESCE(SUM(CASE WHEN type = ? THEN CAST(prompt_tokens AS "+tokenCastType+") + CAST(completion_tokens AS "+tokenCastType+") ELSE 0 END), 0) total_tokens, "+
"COALESCE(AVG(CASE WHEN type = ? AND use_time > 0 THEN use_time ELSE NULL END), 0) avg_use_time, "+
"COALESCE(SUM(CASE WHEN type = ? THEN 1 ELSE 0 END), 0) error_count",
LogTypeConsume, LogTypeConsume, LogTypeConsume, LogTypeError,
)
if rangeTx, err = applyStatBaseFilters(rangeTx, username, tokenName, modelName, channel, group, ip); err != nil {
return stat, err
}
if startTimestamp != 0 {
rangeTx = rangeTx.Where("created_at >= ?", startTimestamp)
}
if endTimestamp != 0 {
rangeTx = rangeTx.Where("created_at <= ?", endTimestamp)
}
if err = rangeTx.Scan(&rr).Error; err != nil {
common.SysError("failed to query range stat: " + err.Error())
return stat, errors.New("查询统计数据失败")
}
if err := rpmTpmQuery.Scan(&stat).Error; err != nil {
common.SysError("failed to query rpm/tpm stat: " + err.Error())
stat.TotalRequests = rr.TotalRequests
stat.TotalTokens = rr.TotalTokens
stat.AvgUseTime = rr.AvgUseTime
stat.ErrorCount = rr.ErrorCount

// today aggregate: consume logs since today 00:00:00
type todayResult struct {
TodayRequests int64 `gorm:"column:today_requests"`
TodayTokens int64 `gorm:"column:today_tokens"`
}
var tr todayResult
todayTx := LOG_DB.Table("logs").Select(
"count(*) today_requests, "+
"COALESCE(sum(CAST(prompt_tokens AS "+tokenCastType+") + CAST(completion_tokens AS "+tokenCastType+")), 0) today_tokens",
)
if todayTx, err = applyStatBaseFilters(todayTx, username, tokenName, modelName, channel, group, ip); err != nil {
return stat, err
}
todayTx = todayTx.
Where("type = ?", LogTypeConsume).
Where("created_at >= ?", todayStartUnix())
if err = todayTx.Scan(&tr).Error; err != nil {
common.SysError("failed to query today stat: " + err.Error())
return stat, errors.New("查询统计数据失败")
}
stat.TodayRequests = tr.TodayRequests
stat.TodayTokens = tr.TodayTokens

return stat, nil
}
Expand Down
4 changes: 4 additions & 0 deletions relay/claude_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ func ClaudeHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *typ
info.UpstreamModelName = request.Model
}

if request.Thinking != nil {
info.IsThinking = true
}

if info.ChannelSetting.SystemPrompt != "" {
if request.System == nil {
request.SetStringSystem(info.ChannelSetting.SystemPrompt)
Expand Down
2 changes: 2 additions & 0 deletions relay/common/relay_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,8 @@ type RelayInfo struct {

StreamStatus *StreamStatus

IsThinking bool

ThinkingContentInfo
TokenCountMeta
*ClaudeConvertInfo
Expand Down
6 changes: 5 additions & 1 deletion relay/helper/stream_scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,11 @@ func StreamScannerHandler(c *gin.Context, resp *http.Response, info *relaycommon

ctx, cancel := context.WithCancel(context.Background())

streamingTimeout := time.Duration(constant.StreamingTimeout) * time.Second
streamingTimeoutSec := constant.StreamingTimeout
if info.IsThinking && constant.ThinkingStreamingTimeout > streamingTimeoutSec {
streamingTimeoutSec = constant.ThinkingStreamingTimeout
}
streamingTimeout := time.Duration(streamingTimeoutSec) * time.Second

var (
stopChan = make(chan bool, 3) // 增加缓冲区避免阻塞
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,25 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
},
size: 160,
})

columns.push({
accessorKey: 'ip',
header: t('IP'),
cell: function IpCell({ row }) {
const { sensitiveVisible } = useUsageLogsContext()
const log = row.original
if (!isDisplayableLogType(log.type)) return null
if (!log.ip) return null
return (
<span className='font-mono text-xs tabular-nums'>
{sensitiveVisible ? log.ip : '••••'}
</span>
)
},
size: 130,
meta: { label: t('IP') },
})

columns.push(
{
accessorKey: 'model_name',
Expand Down
Loading
Loading