feat: sync deployed v1.0.0-rc.38-custom.42

Sync production source including routing, billing, Astra detection and public uptime. Update custom build instructions and license packaging.
This commit is contained in:
cneicy committed 2026-09-26 04:49:11 +08:00
1 parent aa6c3f50f4
commit ceddfd3d6b
828 files changed
+122253 -13702

No files matched your search

+83 -22
View File
@@ -11,7 +11,10 @@ import (
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/model"
relaycommon "github.com/QuantumNous/new-api/relay/common"
"github.com/QuantumNous/new-api/relaykit/types"
"github.com/QuantumNous/new-api/setting/perf_metrics_setting"
"github.com/bytedance/gopkg/util/gopool"
)
var hotBuckets sync.Map
@@ -24,11 +27,20 @@ func Init() {
go flushLoop()
}
func RecordRelaySample(info *relaycommon.RelayInfo, success bool, outputTokens int64) {
// RecordRelayResult samples one finished relay exactly once, at the request
// boundary, regardless of how many channel attempts it took.
func RecordRelayResult(ctx context.Context, info *relaycommon.RelayInfo, apiErr *types.NewAPIError) {
if info == nil {
return
}
outcome := ClassifyRelayOutcome(ctx, info, apiErr)
if outcome == OutcomeIgnored {
return
}
now := time.Now()
if outcome == OutcomeSuccess && !info.IsChannelTest && info.TokenId > 0 {
recordUserSuccess(info.UsingGroup, now.Unix())
}
hasTtft := info.IsStream && info.HasSendResponse()
ttftMs := int64(0)
if hasTtft {
@@ -48,8 +60,8 @@ func RecordRelaySample(info *relaycommon.RelayInfo, success bool, outputTokens i
LatencyMs: latencyMs,
TtftMs: ttftMs,
HasTtft: hasTtft,
Success: success,
OutputTokens: outputTokens,
Success: outcome == OutcomeSuccess,
OutputTokens: info.PerformanceOutputTokens,
GenerationMs: generationMs,
})
}
@@ -73,18 +85,25 @@ func Record(sample Sample) {
}
actual, _ := hotBuckets.LoadOrStore(key, &atomicBucket{})
actual.(*atomicBucket).add(sample)
recordRedis(key, sample)
gopool.Go(func() {
recordRedis(key, sample)
})
}
// queryWindow covers the current partial hour plus the preceding complete
// hours, so every reader shares the same hourly buckets.
func queryWindow(now time.Time, hours int) (int64, int64) {
if hours <= 0 {
hours = 24
}
hours = min(hours, 24*30)
endTs := now.Unix()
return endTs - endTs%3600 - int64(hours-1)*3600, endTs
}
func Query(params QueryParams) (QueryResult, error) {
if params.Hours <= 0 {
params.Hours = 24
}
if params.Hours > 24*30 {
params.Hours = 24 * 30
}
endTs := time.Now().Unix()
startTs := endTs - int64(params.Hours)*3600
startTs, endTs := queryWindow(time.Now(), params.Hours)
allowedGroups := allowedGroupSet(params.AllowedGroups)
merged := map[bucketKey]counters{}
rows, err := model.GetPerfMetrics(params.Model, params.Group, startTs, endTs)
@@ -92,6 +111,11 @@ func Query(params QueryParams) (QueryResult, error) {
return QueryResult{}, err
}
for _, row := range rows {
if allowedGroups != nil {
if _, ok := allowedGroups[row.Group]; !ok {
continue
}
}
mergeCounters(merged, bucketKey{
model: row.ModelName,
group: row.Group,
@@ -115,22 +139,22 @@ func Query(params QueryParams) (QueryResult, error) {
if params.Group != "" && k.group != params.Group {
return true
}
if allowedGroups != nil {
if _, ok := allowedGroups[k.group]; !ok {
return true
}
}
mergeCounters(merged, k, value.(*atomicBucket).snapshot())
return true
})
return buildQueryResult(params.Model, merged), nil
result := buildQueryResult(params.Model, merged)
result.WindowStart, result.WindowEnd = startTs, endTs
return result, nil
}
func QuerySummaryAll(hours int, groups []string) (SummaryAllResult, error) {
if hours <= 0 {
hours = 24
}
if hours > 24*30 {
hours = 24 * 30
}
endTs := time.Now().Unix()
startTs := endTs - int64(hours)*3600
startTs, endTs := queryWindow(time.Now(), hours)
allowedGroups := allowedGroupSet(groups)
rows, err := model.GetPerfMetricsSummaryBucketsAll(startTs, endTs, groups)
@@ -171,11 +195,17 @@ func QuerySummaryAll(hours int, groups []string) (SummaryAllResult, error) {
return true
})
all := counters{}
models := make([]ModelSummary, 0, len(totals))
for name, total := range totals {
if total.requestCount == 0 {
continue
}
all.requestCount += total.requestCount
all.successCount += total.successCount
all.totalLatencyMs += total.totalLatencyMs
all.outputTokens += total.outputTokens
all.generationMs += total.generationMs
avgLatency := total.totalLatencyMs / total.requestCount
successRate := float64(total.successCount) / float64(total.requestCount) * 100
avgTps := 0.0
@@ -195,7 +225,7 @@ func QuerySummaryAll(hours int, groups []string) (SummaryAllResult, error) {
return models[i].RequestCount > models[j].RequestCount
})
return SummaryAllResult{Models: models}, nil
return SummaryAllResult{Summary: summarize(all), WindowStart: startTs, WindowEnd: endTs, Models: models}, nil
}
// QueryGroups combines raw bucket counters so busy models carry their actual weight.
@@ -324,6 +354,7 @@ func mergeCounters(merged map[bucketKey]counters, key bucketKey, value counters)
func buildQueryResult(modelName string, merged map[bucketKey]counters) QueryResult {
groupBuckets := map[string]map[int64]counters{}
modelBuckets := map[string]map[int64]counters{}
for key, value := range merged {
if value.requestCount == 0 {
continue
@@ -332,6 +363,7 @@ func buildQueryResult(modelName string, merged map[bucketKey]counters) QueryResu
groupBuckets[key.group] = map[int64]counters{}
}
groupBuckets[key.group][key.bucketTs] = value
mergeModelBucket(modelBuckets, modelName, key.bucketTs, value)
}
groups := make([]string, 0, len(groupBuckets))
@@ -340,6 +372,7 @@ func buildQueryResult(modelName string, merged map[bucketKey]counters) QueryResu
}
sort.Strings(groups)
all := counters{}
results := make([]GroupResult, 0, len(groups))
for _, group := range groups {
buckets := groupBuckets[group]
@@ -364,6 +397,11 @@ func buildQueryResult(modelName string, merged map[bucketKey]counters) QueryResu
total.generationMs += value.generationMs
series = append(series, bucketPoint(ts, value))
}
all.requestCount += total.requestCount
all.successCount += total.successCount
all.totalLatencyMs += total.totalLatencyMs
all.outputTokens += total.outputTokens
all.generationMs += total.generationMs
results = append(results, GroupResult{
Group: group,
@@ -375,13 +413,36 @@ func buildQueryResult(modelName string, merged map[bucketKey]counters) QueryResu
})
}
modelSeries := make([]BucketPoint, 0, len(modelBuckets[modelName]))
for ts, value := range modelBuckets[modelName] {
modelSeries = append(modelSeries, bucketPoint(ts, value))
}
sort.Slice(modelSeries, func(i, j int) bool {
return modelSeries[i].Ts < modelSeries[j].Ts
})
return QueryResult{
ModelName: modelName,
SeriesSchema: seriesSchema,
Summary: summarize(all),
Series: modelSeries,
Groups: results,
}
}
// summarize returns nil when nothing was sampled, so clients can show
// "no data" instead of a fabricated 0% success rate.
func summarize(total counters) *Summary {
if total.requestCount <= 0 {
return nil
}
return &Summary{
AvgLatencyMs: avg(total.totalLatencyMs, total.requestCount),
SuccessRate: math.Round(successRate(total)*100) / 100,
AvgTps: math.Round(avgTps(total)*100) / 100,
}
}
func bucketPoint(ts int64, value counters) BucketPoint {
return BucketPoint{
totals: value,