diff --git a/controller/perf_metrics.go b/controller/perf_metrics.go index 66d0787f2a92..cbf29d25e364 100644 --- a/controller/perf_metrics.go +++ b/controller/perf_metrics.go @@ -3,6 +3,7 @@ package controller import ( "net/http" "strconv" + "time" perfmetrics "github.com/QuantumNous/new-api/pkg/perf_metrics" "github.com/QuantumNous/new-api/setting/ratio_setting" @@ -73,6 +74,38 @@ func GetPerfMetrics(c *gin.Context) { }) } +func GetAdminModelPerfMetrics(c *gin.Context) { + const maxRangeSeconds = int64(30 * 24 * 60 * 60) + rawStart := c.Query("start_timestamp") + rawEnd := c.Query("end_timestamp") + if rawStart == "" || rawEnd == "" { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "start_timestamp and end_timestamp are required"}) + return + } + + startTs, startErr := strconv.ParseInt(rawStart, 10, 64) + endTs, endErr := strconv.ParseInt(rawEnd, 10, 64) + if startErr != nil || endErr != nil || startTs <= 0 || endTs <= startTs { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid performance metric time range"}) + return + } + if endTs > time.Now().Unix() { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "end_timestamp cannot be in the future"}) + return + } + if endTs-startTs > maxRangeSeconds { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "performance metric time range cannot exceed 30 days"}) + return + } + + result, err := perfmetrics.QueryAdmin(startTs, endTs) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": result}) +} + func filterActiveGroups(groups []perfmetrics.GroupResult) []perfmetrics.GroupResult { activeRatios := ratio_setting.GetGroupRatioCopy() return lo.Filter(groups, func(g perfmetrics.GroupResult, _ int) bool { diff --git a/controller/perf_metrics_admin_test.go b/controller/perf_metrics_admin_test.go new file mode 100644 index 000000000000..f6f920ce062f --- /dev/null +++ b/controller/perf_metrics_admin_test.go @@ -0,0 +1,42 @@ +package controller + +import ( + "net/http" + "net/http/httptest" + "strconv" + "testing" + "time" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetAdminModelPerfMetricsRejectsInvalidRanges(t *testing.T) { + gin.SetMode(gin.TestMode) + now := time.Now().Unix() + tests := []struct { + name string + query string + }{ + {name: "missing end", query: "?start_timestamp=100"}, + {name: "non numeric", query: "?start_timestamp=bad&end_timestamp=200"}, + {name: "reversed", query: "?start_timestamp=200&end_timestamp=100"}, + {name: "future end", query: "?start_timestamp=100&end_timestamp=" + strconv.FormatInt(now+60, 10)}, + {name: "over thirty days", query: "?start_timestamp=1&end_timestamp=2592002"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + recorder := httptest.NewRecorder() + ctx, _ := gin.CreateTestContext(recorder) + request, err := http.NewRequest(http.MethodGet, "/api/perf-metrics/admin/models"+tt.query, nil) + require.NoError(t, err) + ctx.Request = request + + GetAdminModelPerfMetrics(ctx) + + assert.Equal(t, http.StatusBadRequest, recorder.Code) + }) + } +} diff --git a/model/ability.go b/model/ability.go index d950a6adbfc4..f31104f8380b 100644 --- a/model/ability.go +++ b/model/ability.go @@ -54,9 +54,14 @@ func GetEnabledModels() []string { return models } -func GetAllEnableAbilities() []Ability { +func ListEnabledAbilities() ([]Ability, error) { var abilities []Ability - DB.Find(&abilities, "enabled = ?", true) + err := DB.Where("enabled = ?", true).Find(&abilities).Error + return abilities, err +} + +func GetAllEnableAbilities() []Ability { + abilities, _ := ListEnabledAbilities() return abilities } diff --git a/model/perf_metric.go b/model/perf_metric.go index f9c33c851989..54e5aa36bdb2 100644 --- a/model/perf_metric.go +++ b/model/perf_metric.go @@ -1,6 +1,7 @@ package model import ( + "database/sql" "time" "gorm.io/gorm" @@ -115,6 +116,54 @@ func GetPerfMetricsSummaryBucketsAll(startTs int64, endTs int64, groups []string return summaries, err } +type PerfMetricGroupSummary struct { + ModelName string `json:"model_name"` + Group string `json:"group"` + RequestCount int64 `json:"request_count"` + SuccessCount int64 `json:"success_count"` + TotalLatencyMs int64 `json:"total_latency_ms"` + TtftSumMs int64 `json:"ttft_sum_ms"` + TtftCount int64 `json:"ttft_count"` + OutputTokens int64 `json:"output_tokens"` + GenerationMs int64 `json:"generation_ms"` +} + +func GetPerfMetricGroupSummaries(startTs int64, endTs int64) ([]PerfMetricGroupSummary, error) { + summaries := make([]PerfMetricGroupSummary, 0) + if endTs <= startTs { + return summaries, nil + } + err := DB.Model(&PerfMetric{}). + Select("model_name, "+commonGroupCol+", SUM(request_count) as request_count, SUM(success_count) as success_count, SUM(total_latency_ms) as total_latency_ms, SUM(ttft_sum_ms) as ttft_sum_ms, SUM(ttft_count) as ttft_count, SUM(output_tokens) as output_tokens, SUM(generation_ms) as generation_ms"). + Where("bucket_ts >= ? AND bucket_ts < ?", startTs, endTs). + Group("model_name, " + commonGroupCol). + Having("SUM(request_count) > 0"). + Find(&summaries).Error + return summaries, err +} + +func GetPerfMetricAvailableRange() (*int64, *int64, error) { + var bounds struct { + Oldest sql.NullInt64 + Newest sql.NullInt64 + } + err := DB.Model(&PerfMetric{}). + Select("MIN(bucket_ts) as oldest, MAX(bucket_ts) as newest"). + Scan(&bounds).Error + if err != nil { + return nil, nil, err + } + var oldest *int64 + var newest *int64 + if bounds.Oldest.Valid { + oldest = &bounds.Oldest.Int64 + } + if bounds.Newest.Valid { + newest = &bounds.Newest.Int64 + } + return oldest, newest, nil +} + func DeletePerfMetricsBefore(cutoffTs int64) error { if cutoffTs <= 0 { return nil diff --git a/model/perf_metric_admin_test.go b/model/perf_metric_admin_test.go new file mode 100644 index 000000000000..1a5dabbd1a22 --- /dev/null +++ b/model/perf_metric_admin_test.go @@ -0,0 +1,34 @@ +package model + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetPerfMetricGroupSummariesUsesHalfOpenRange(t *testing.T) { + modelName := "admin-summary-half-open" + t.Cleanup(func() { + require.NoError(t, DB.Where("model_name = ?", modelName).Delete(&PerfMetric{}).Error) + }) + + rows := []PerfMetric{ + {ModelName: modelName, Group: "default", BucketTs: 100, RequestCount: 2, SuccessCount: 1, TotalLatencyMs: 400, TtftSumMs: 100, TtftCount: 1, OutputTokens: 20, GenerationMs: 1000}, + {ModelName: modelName, Group: "default", BucketTs: 200, RequestCount: 3, SuccessCount: 3, TotalLatencyMs: 600, TtftSumMs: 300, TtftCount: 2, OutputTokens: 30, GenerationMs: 1500}, + {ModelName: modelName, Group: "default", BucketTs: 300, RequestCount: 7, SuccessCount: 7, TotalLatencyMs: 700, TtftSumMs: 700, TtftCount: 7, OutputTokens: 70, GenerationMs: 3500}, + } + require.NoError(t, DB.Create(&rows).Error) + + summaries, err := GetPerfMetricGroupSummaries(100, 300) + + require.NoError(t, err) + require.Len(t, summaries, 1) + assert.Equal(t, int64(5), summaries[0].RequestCount) + assert.Equal(t, int64(4), summaries[0].SuccessCount) + assert.Equal(t, int64(1000), summaries[0].TotalLatencyMs) + assert.Equal(t, int64(400), summaries[0].TtftSumMs) + assert.Equal(t, int64(3), summaries[0].TtftCount) + assert.Equal(t, int64(50), summaries[0].OutputTokens) + assert.Equal(t, int64(2500), summaries[0].GenerationMs) +} diff --git a/pkg/perf_metrics/admin.go b/pkg/perf_metrics/admin.go new file mode 100644 index 000000000000..b90840b783ae --- /dev/null +++ b/pkg/perf_metrics/admin.go @@ -0,0 +1,467 @@ +package perfmetrics + +import ( + "math" + "sort" + "time" + + "github.com/QuantumNous/new-api/model" + "github.com/QuantumNous/new-api/setting/perf_metrics_setting" +) + +const ( + adminMinRequestSamples = int64(20) + adminMinTtftSamples = int64(10) + adminMinOutputTokens = int64(100) +) + +type AdminHealth string + +const ( + AdminHealthCritical AdminHealth = "critical" + AdminHealthDegraded AdminHealth = "degraded" + AdminHealthHealthy AdminHealth = "healthy" + AdminHealthInsufficientSamples AdminHealth = "insufficient_samples" + AdminHealthNoSamples AdminHealth = "no_samples" +) + +type AdminTimeRange struct { + Start int64 `json:"start"` + End int64 `json:"end"` +} + +type AdminAvailableRange struct { + OldestBucketTs *int64 `json:"oldest_bucket_ts"` + NewestBucketTs *int64 `json:"newest_bucket_ts"` +} + +type AdminMetricValues struct { + RequestCount int64 `json:"request_count"` + SuccessCount int64 `json:"success_count"` + FailureCount int64 `json:"failure_count"` + SuccessRate *float64 `json:"success_rate"` + AvgLatencyMs *int64 `json:"avg_latency_ms"` + AvgTtftMs *int64 `json:"avg_ttft_ms"` + TtftSampleCount int64 `json:"ttft_sample_count"` + OutputTokens int64 `json:"output_tokens"` + AvgTps *float64 `json:"avg_tps"` + ActiveGroupCount int `json:"active_group_count"` +} + +type AdminMetricChanges struct { + RequestCountPct *float64 `json:"request_count_pct"` + SuccessRatePp *float64 `json:"success_rate_pp"` + AvgLatencyPct *float64 `json:"avg_latency_pct"` + AvgTtftPct *float64 `json:"avg_ttft_pct"` + AvgTpsPct *float64 `json:"avg_tps_pct"` +} + +type AdminGroupResult struct { + Group string `json:"group"` + Enabled bool `json:"enabled"` + Health AdminHealth `json:"health"` + HealthReasons []string `json:"health_reasons"` + Metrics AdminMetricValues `json:"metrics"` + PreviousMetrics AdminMetricValues `json:"previous_metrics"` + Changes AdminMetricChanges `json:"changes"` +} + +type AdminModelResult struct { + ModelName string `json:"model_name"` + Enabled bool `json:"enabled"` + Health AdminHealth `json:"health"` + HealthReasons []string `json:"health_reasons"` + Metrics AdminMetricValues `json:"metrics"` + PreviousMetrics AdminMetricValues `json:"previous_metrics"` + Changes AdminMetricChanges `json:"changes"` + Groups []AdminGroupResult `json:"groups"` +} + +type AdminQueryResult struct { + MetricsEnabled bool `json:"metrics_enabled"` + GeneratedAt int64 `json:"generated_at"` + BucketSeconds int64 `json:"bucket_seconds"` + ExpectedMaxLag int64 `json:"expected_max_lag_seconds"` + RequestedPeriod AdminTimeRange `json:"requested_period"` + ActualPeriod AdminTimeRange `json:"actual_period"` + PreviousPeriod AdminTimeRange `json:"previous_period"` + AvailableRange AdminAvailableRange `json:"available_range"` + HasCompleteBuckets bool `json:"has_complete_buckets"` + Models []AdminModelResult `json:"models"` +} + +type modelGroupKey struct { + model string + group string +} + +func QueryAdmin(startTs int64, endTs int64) (AdminQueryResult, error) { + setting := perf_metrics_setting.GetSetting() + bucketSeconds := perf_metrics_setting.GetBucketSeconds() + actualPeriod, previousPeriod := resolveAdminPeriods(startTs, endTs, bucketSeconds) + result := AdminQueryResult{ + MetricsEnabled: setting.Enabled, + GeneratedAt: time.Now().Unix(), + BucketSeconds: bucketSeconds, + ExpectedMaxLag: int64(perf_metrics_setting.GetFlushIntervalMinutes() * 60), + RequestedPeriod: AdminTimeRange{Start: startTs, End: endTs}, + ActualPeriod: actualPeriod, + PreviousPeriod: previousPeriod, + HasCompleteBuckets: actualPeriod.End > actualPeriod.Start, + Models: make([]AdminModelResult, 0), + } + + abilities, err := model.ListEnabledAbilities() + if err != nil { + return AdminQueryResult{}, err + } + if !setting.Enabled || !result.HasCompleteBuckets { + result.Models = buildAdminModels(nil, nil, abilities) + return result, nil + } + + current, previous, availableRange, err := readAdminCounters(actualPeriod, previousPeriod) + if err != nil { + return AdminQueryResult{}, err + } + result.AvailableRange = availableRange + result.Models = buildAdminModels(current, previous, abilities) + return result, nil +} + +func resolveAdminPeriods(startTs int64, endTs int64, bucketSeconds int64) (AdminTimeRange, AdminTimeRange) { + if bucketSeconds <= 0 { + bucketSeconds = 3600 + } + actualStart := alignBucketUp(startTs, bucketSeconds) + actualEnd := endTs - endTs%bucketSeconds + if actualEnd <= actualStart { + return AdminTimeRange{Start: actualStart, End: actualStart}, AdminTimeRange{Start: actualStart, End: actualStart} + } + duration := actualEnd - actualStart + return AdminTimeRange{Start: actualStart, End: actualEnd}, AdminTimeRange{Start: actualStart - duration, End: actualStart} +} + +func alignBucketUp(ts int64, bucketSeconds int64) int64 { + remainder := ts % bucketSeconds + if remainder == 0 { + return ts + } + return ts + bucketSeconds - remainder +} + +func readAdminCounters(currentPeriod AdminTimeRange, previousPeriod AdminTimeRange) (map[modelGroupKey]counters, map[modelGroupKey]counters, AdminAvailableRange, error) { + hotBucketsMu.RLock() + defer hotBucketsMu.RUnlock() + + currentRows, err := model.GetPerfMetricGroupSummaries(currentPeriod.Start, currentPeriod.End) + if err != nil { + return nil, nil, AdminAvailableRange{}, err + } + previousRows, err := model.GetPerfMetricGroupSummaries(previousPeriod.Start, previousPeriod.End) + if err != nil { + return nil, nil, AdminAvailableRange{}, err + } + oldest, newest, err := model.GetPerfMetricAvailableRange() + if err != nil { + return nil, nil, AdminAvailableRange{}, err + } + + current := adminCountersFromRows(currentRows) + previous := adminCountersFromRows(previousRows) + hotBuckets.Range(func(key, value any) bool { + bucket := key.(bucketKey) + snapshot := value.(*atomicBucket).snapshot() + if snapshot.requestCount == 0 { + return true + } + if oldest == nil || bucket.bucketTs < *oldest { + oldest = int64Pointer(bucket.bucketTs) + } + if newest == nil || bucket.bucketTs > *newest { + newest = int64Pointer(bucket.bucketTs) + } + if bucket.bucketTs >= currentPeriod.Start && bucket.bucketTs < currentPeriod.End { + mergeAdminCounters(current, modelGroupKey{model: bucket.model, group: bucket.group}, snapshot) + } + if bucket.bucketTs >= previousPeriod.Start && bucket.bucketTs < previousPeriod.End { + mergeAdminCounters(previous, modelGroupKey{model: bucket.model, group: bucket.group}, snapshot) + } + return true + }) + + return current, previous, AdminAvailableRange{OldestBucketTs: oldest, NewestBucketTs: newest}, nil +} + +func adminCountersFromRows(rows []model.PerfMetricGroupSummary) map[modelGroupKey]counters { + result := make(map[modelGroupKey]counters, len(rows)) + for _, row := range rows { + result[modelGroupKey{model: row.ModelName, group: row.Group}] = counters{ + requestCount: row.RequestCount, + successCount: row.SuccessCount, + totalLatencyMs: row.TotalLatencyMs, + ttftSumMs: row.TtftSumMs, + ttftCount: row.TtftCount, + outputTokens: row.OutputTokens, + generationMs: row.GenerationMs, + } + } + return result +} + +func mergeAdminCounters(values map[modelGroupKey]counters, key modelGroupKey, addition counters) { + current := values[key] + current.requestCount += addition.requestCount + current.successCount += addition.successCount + current.totalLatencyMs += addition.totalLatencyMs + current.ttftSumMs += addition.ttftSumMs + current.ttftCount += addition.ttftCount + current.outputTokens += addition.outputTokens + current.generationMs += addition.generationMs + values[key] = current +} + +func buildAdminModels(current map[modelGroupKey]counters, previous map[modelGroupKey]counters, abilities []model.Ability) []AdminModelResult { + enabledModels := map[string]bool{} + enabledGroups := map[modelGroupKey]bool{} + modelNames := map[string]struct{}{} + for _, ability := range abilities { + enabledModels[ability.Model] = true + enabledGroups[modelGroupKey{model: ability.Model, group: ability.Group}] = true + modelNames[ability.Model] = struct{}{} + } + for key, value := range current { + if value.requestCount > 0 { + modelNames[key.model] = struct{}{} + } + } + + currentTotals := aggregateAdminModelCounters(current) + previousTotals := aggregateAdminModelCounters(previous) + models := make([]AdminModelResult, 0, len(modelNames)) + for modelName := range modelNames { + currentValue := currentTotals[modelName] + previousValue := previousTotals[modelName] + metrics := adminMetricValues(currentValue) + metrics.ActiveGroupCount = countActiveAdminGroups(modelName, current) + previousMetrics := adminMetricValues(previousValue) + previousMetrics.ActiveGroupCount = countActiveAdminGroups(modelName, previous) + health, reasons := classifyAdminHealth(currentValue, previousValue) + models = append(models, AdminModelResult{ + ModelName: modelName, + Enabled: enabledModels[modelName], + Health: health, + HealthReasons: reasons, + Metrics: metrics, + PreviousMetrics: previousMetrics, + Changes: buildAdminChanges(currentValue, previousValue), + Groups: buildAdminGroups(modelName, current, previous, enabledGroups), + }) + } + + sort.Slice(models, func(i, j int) bool { + leftRank := adminHealthRank(models[i].Health) + rightRank := adminHealthRank(models[j].Health) + if leftRank != rightRank { + return leftRank < rightRank + } + if models[i].Metrics.RequestCount != models[j].Metrics.RequestCount { + return models[i].Metrics.RequestCount > models[j].Metrics.RequestCount + } + return models[i].ModelName < models[j].ModelName + }) + return models +} + +func aggregateAdminModelCounters(values map[modelGroupKey]counters) map[string]counters { + result := map[string]counters{} + for key, value := range values { + current := result[key.model] + current.requestCount += value.requestCount + current.successCount += value.successCount + current.totalLatencyMs += value.totalLatencyMs + current.ttftSumMs += value.ttftSumMs + current.ttftCount += value.ttftCount + current.outputTokens += value.outputTokens + current.generationMs += value.generationMs + result[key.model] = current + } + return result +} + +func countActiveAdminGroups(modelName string, values map[modelGroupKey]counters) int { + count := 0 + for key, value := range values { + if key.model == modelName && value.requestCount > 0 { + count++ + } + } + return count +} + +func buildAdminGroups(modelName string, current map[modelGroupKey]counters, previous map[modelGroupKey]counters, enabled map[modelGroupKey]bool) []AdminGroupResult { + groupNames := map[string]struct{}{} + for key := range enabled { + if key.model == modelName { + groupNames[key.group] = struct{}{} + } + } + for key, value := range current { + if key.model == modelName && value.requestCount > 0 { + groupNames[key.group] = struct{}{} + } + } + + groups := make([]AdminGroupResult, 0, len(groupNames)) + for group := range groupNames { + key := modelGroupKey{model: modelName, group: group} + currentValue := current[key] + previousValue := previous[key] + health, reasons := classifyAdminHealth(currentValue, previousValue) + groups = append(groups, AdminGroupResult{ + Group: group, + Enabled: enabled[key], + Health: health, + HealthReasons: reasons, + Metrics: adminMetricValues(currentValue), + PreviousMetrics: adminMetricValues(previousValue), + Changes: buildAdminChanges(currentValue, previousValue), + }) + } + sort.Slice(groups, func(i, j int) bool { + if groups[i].Metrics.RequestCount != groups[j].Metrics.RequestCount { + return groups[i].Metrics.RequestCount > groups[j].Metrics.RequestCount + } + return groups[i].Group < groups[j].Group + }) + return groups +} + +func adminMetricValues(value counters) AdminMetricValues { + metrics := AdminMetricValues{ + RequestCount: value.requestCount, + SuccessCount: value.successCount, + FailureCount: max(value.requestCount-value.successCount, 0), + TtftSampleCount: value.ttftCount, + OutputTokens: value.outputTokens, + } + if value.requestCount > 0 { + rate := roundAdminMetric(successRate(value)) + latency := avg(value.totalLatencyMs, value.requestCount) + metrics.SuccessRate = &rate + metrics.AvgLatencyMs = &latency + } + if value.ttftCount > 0 { + ttft := avg(value.ttftSumMs, value.ttftCount) + metrics.AvgTtftMs = &ttft + } + if value.outputTokens > 0 && value.generationMs > 0 { + tps := roundAdminMetric(avgTps(value)) + metrics.AvgTps = &tps + } + return metrics +} + +func buildAdminChanges(current counters, previous counters) AdminMetricChanges { + changes := AdminMetricChanges{} + if current.requestCount >= adminMinRequestSamples && previous.requestCount >= adminMinRequestSamples { + changes.RequestCountPct = adminPercentChange(float64(current.requestCount), float64(previous.requestCount)) + currentRate := successRate(current) + previousRate := successRate(previous) + difference := roundAdminMetric(currentRate - previousRate) + changes.SuccessRatePp = &difference + changes.AvgLatencyPct = adminPercentChange(float64(avg(current.totalLatencyMs, current.requestCount)), float64(avg(previous.totalLatencyMs, previous.requestCount))) + } + if current.ttftCount >= adminMinTtftSamples && previous.ttftCount >= adminMinTtftSamples { + changes.AvgTtftPct = adminPercentChange(float64(avg(current.ttftSumMs, current.ttftCount)), float64(avg(previous.ttftSumMs, previous.ttftCount))) + } + if current.outputTokens >= adminMinOutputTokens && previous.outputTokens >= adminMinOutputTokens && current.generationMs > 0 && previous.generationMs > 0 { + changes.AvgTpsPct = adminPercentChange(avgTps(current), avgTps(previous)) + } + return changes +} + +func adminPercentChange(current float64, previous float64) *float64 { + if previous <= 0 || math.IsNaN(current) || math.IsInf(current, 0) { + return nil + } + change := roundAdminMetric((current - previous) / previous * 100) + return &change +} + +func classifyAdminHealth(current counters, previous counters) (AdminHealth, []string) { + if current.requestCount == 0 { + return AdminHealthNoSamples, []string{"no_samples"} + } + if current.requestCount < adminMinRequestSamples { + return AdminHealthInsufficientSamples, []string{"insufficient_samples"} + } + + currentRate := successRate(current) + criticalReasons := make([]string, 0, 2) + if currentRate < 90 { + criticalReasons = append(criticalReasons, "success_rate_critical") + } + if previous.requestCount >= adminMinRequestSamples && currentRate-successRate(previous) <= -10 { + criticalReasons = append(criticalReasons, "success_rate_regression_critical") + } + if len(criticalReasons) > 0 { + return AdminHealthCritical, criticalReasons + } + + degradedReasons := make([]string, 0, 4) + if currentRate < 98 { + degradedReasons = append(degradedReasons, "success_rate_degraded") + } + if previous.requestCount >= adminMinRequestSamples { + previousRate := successRate(previous) + if currentRate-previousRate <= -3 { + degradedReasons = append(degradedReasons, "success_rate_regression") + } + currentLatency := avg(current.totalLatencyMs, current.requestCount) + previousLatency := avg(previous.totalLatencyMs, previous.requestCount) + if previousLatency > 0 && currentLatency-previousLatency >= 500 && float64(currentLatency)/float64(previousLatency) >= 1.5 { + degradedReasons = append(degradedReasons, "latency_regression") + } + } + if current.ttftCount >= adminMinTtftSamples && previous.ttftCount >= adminMinTtftSamples { + currentTtft := avg(current.ttftSumMs, current.ttftCount) + previousTtft := avg(previous.ttftSumMs, previous.ttftCount) + if previousTtft > 0 && currentTtft-previousTtft >= 300 && float64(currentTtft)/float64(previousTtft) >= 1.5 { + degradedReasons = append(degradedReasons, "ttft_regression") + } + } + if current.outputTokens >= adminMinOutputTokens && previous.outputTokens >= adminMinOutputTokens && current.generationMs > 0 && previous.generationMs > 0 { + previousTps := avgTps(previous) + if previousTps > 0 && (avgTps(current)-previousTps)/previousTps <= -0.3 { + degradedReasons = append(degradedReasons, "tps_regression") + } + } + if len(degradedReasons) > 0 { + return AdminHealthDegraded, degradedReasons + } + return AdminHealthHealthy, []string{} +} + +func adminHealthRank(health AdminHealth) int { + switch health { + case AdminHealthCritical: + return 0 + case AdminHealthDegraded: + return 1 + case AdminHealthHealthy: + return 2 + case AdminHealthInsufficientSamples: + return 3 + default: + return 4 + } +} + +func roundAdminMetric(value float64) float64 { + return math.Round(value*100) / 100 +} + +func int64Pointer(value int64) *int64 { + return &value +} diff --git a/pkg/perf_metrics/admin_test.go b/pkg/perf_metrics/admin_test.go new file mode 100644 index 000000000000..358bb1ed95a2 --- /dev/null +++ b/pkg/perf_metrics/admin_test.go @@ -0,0 +1,115 @@ +package perfmetrics + +import ( + "testing" + + "github.com/QuantumNous/new-api/model" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestResolveAdminPeriodsUsesCompleteBuckets(t *testing.T) { + actual, previous := resolveAdminPeriods(3660, 14430, 3600) + + assert.Equal(t, AdminTimeRange{Start: 7200, End: 14400}, actual) + assert.Equal(t, AdminTimeRange{Start: 0, End: 7200}, previous) +} + +func TestResolveAdminPeriodsReportsRangeWithoutCompleteBucket(t *testing.T) { + actual, previous := resolveAdminPeriods(3660, 7199, 3600) + + assert.Equal(t, AdminTimeRange{Start: 7200, End: 7200}, actual) + assert.Equal(t, actual, previous) +} + +func TestClassifyAdminHealth(t *testing.T) { + tests := []struct { + name string + current counters + previous counters + expectedHealth AdminHealth + expectedReason string + }{ + {name: "no samples", expectedHealth: AdminHealthNoSamples, expectedReason: "no_samples"}, + {name: "insufficient samples", current: counters{requestCount: 19, successCount: 19}, expectedHealth: AdminHealthInsufficientSamples, expectedReason: "insufficient_samples"}, + {name: "critical success rate", current: counters{requestCount: 20, successCount: 17}, expectedHealth: AdminHealthCritical, expectedReason: "success_rate_critical"}, + {name: "degraded success rate", current: counters{requestCount: 20, successCount: 19}, expectedHealth: AdminHealthDegraded, expectedReason: "success_rate_degraded"}, + {name: "latency regression", current: counters{requestCount: 20, successCount: 20, totalLatencyMs: 30000}, previous: counters{requestCount: 20, successCount: 20, totalLatencyMs: 20000}, expectedHealth: AdminHealthDegraded, expectedReason: "latency_regression"}, + {name: "ttft regression", current: counters{requestCount: 20, successCount: 20, ttftCount: 10, ttftSumMs: 9000}, previous: counters{requestCount: 20, successCount: 20, ttftCount: 10, ttftSumMs: 6000}, expectedHealth: AdminHealthDegraded, expectedReason: "ttft_regression"}, + {name: "tps regression", current: counters{requestCount: 20, successCount: 20, outputTokens: 100, generationMs: 2000}, previous: counters{requestCount: 20, successCount: 20, outputTokens: 100, generationMs: 1000}, expectedHealth: AdminHealthDegraded, expectedReason: "tps_regression"}, + {name: "healthy", current: counters{requestCount: 20, successCount: 20, totalLatencyMs: 20000}, previous: counters{requestCount: 20, successCount: 20, totalLatencyMs: 20000}, expectedHealth: AdminHealthHealthy}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + health, reasons := classifyAdminHealth(tt.current, tt.previous) + assert.Equal(t, tt.expectedHealth, health) + if tt.expectedReason != "" { + assert.Contains(t, reasons, tt.expectedReason) + } else { + assert.Empty(t, reasons) + } + }) + } +} + +func TestBuildAdminChangesRequiresMetricSpecificSamples(t *testing.T) { + current := counters{ + requestCount: 20, + successCount: 20, + totalLatencyMs: 20000, + ttftCount: 10, + ttftSumMs: 5000, + outputTokens: 100, + generationMs: 2000, + } + previous := counters{ + requestCount: 19, + successCount: 19, + totalLatencyMs: 19000, + ttftCount: 9, + ttftSumMs: 4500, + outputTokens: 99, + generationMs: 2000, + } + + changes := buildAdminChanges(current, previous) + + assert.Nil(t, changes.RequestCountPct) + assert.Nil(t, changes.SuccessRatePp) + assert.Nil(t, changes.AvgLatencyPct) + assert.Nil(t, changes.AvgTtftPct) + assert.Nil(t, changes.AvgTpsPct) +} + +func TestBuildAdminModelsIncludesEnabledAndCurrentlySampledModels(t *testing.T) { + current := map[modelGroupKey]counters{ + {model: "sampled-disabled", group: "legacy"}: { + requestCount: 20, + successCount: 20, + }, + } + previous := map[modelGroupKey]counters{ + {model: "previous-only", group: "legacy"}: { + requestCount: 20, + successCount: 20, + }, + } + abilities := []model.Ability{ + {Model: "enabled-no-samples", Group: "default", Enabled: true, ChannelId: 1}, + {Model: "enabled-no-samples", Group: "default", Enabled: true, ChannelId: 2}, + } + + models := buildAdminModels(current, previous, abilities) + + require.Len(t, models, 2) + assert.Equal(t, "sampled-disabled", models[0].ModelName) + assert.False(t, models[0].Enabled) + require.Len(t, models[0].Groups, 1) + assert.Equal(t, "legacy", models[0].Groups[0].Group) + assert.False(t, models[0].Groups[0].Enabled) + assert.Equal(t, "enabled-no-samples", models[1].ModelName) + assert.True(t, models[1].Enabled) + assert.Equal(t, AdminHealthNoSamples, models[1].Health) + assert.NotContains(t, []string{models[0].ModelName, models[1].ModelName}, "previous-only") +} diff --git a/pkg/perf_metrics/flush.go b/pkg/perf_metrics/flush.go index dddc24725852..7a7058648a6b 100644 --- a/pkg/perf_metrics/flush.go +++ b/pkg/perf_metrics/flush.go @@ -15,22 +15,17 @@ func flushLoop() { interval := perf_metrics_setting.GetFlushIntervalMinutes() time.Sleep(time.Duration(interval) * time.Minute) setting := perf_metrics_setting.GetSetting() - if !setting.Enabled { - continue - } - flushCompletedBuckets() + flushPerfBuckets() cleanupExpiredMetrics(setting.RetentionDays) } } -func flushCompletedBuckets() { - currentBucket := bucketStart(time.Now().Unix()) +func flushPerfBuckets() { + hotBucketsMu.Lock() + defer hotBucketsMu.Unlock() + hotBuckets.Range(func(key, value any) bool { k := key.(bucketKey) - if k.bucketTs >= currentBucket { - return true - } - bucket := value.(*atomicBucket) drained := bucket.drain() if drained.requestCount == 0 { diff --git a/pkg/perf_metrics/metrics.go b/pkg/perf_metrics/metrics.go index 33b79ee478e9..901369620873 100644 --- a/pkg/perf_metrics/metrics.go +++ b/pkg/perf_metrics/metrics.go @@ -15,6 +15,7 @@ import ( ) var hotBuckets sync.Map +var hotBucketsMu sync.RWMutex // seriesSchema is a stable client cache/schema marker. Do not change it when // hiding fields or making response-only privacy hardening changes. @@ -86,6 +87,9 @@ func Query(params QueryParams) (QueryResult, error) { endTs := time.Now().Unix() startTs := endTs - int64(params.Hours)*3600 + hotBucketsMu.RLock() + defer hotBucketsMu.RUnlock() + merged := map[bucketKey]counters{} rows, err := model.GetPerfMetrics(params.Model, params.Group, startTs, endTs) if err != nil { @@ -133,6 +137,9 @@ func QuerySummaryAll(hours int, groups []string) (SummaryAllResult, error) { startTs := endTs - int64(hours)*3600 allowedGroups := allowedGroupSet(groups) + hotBucketsMu.RLock() + defer hotBucketsMu.RUnlock() + rows, err := model.GetPerfMetricsSummaryBucketsAll(startTs, endTs, groups) if err != nil { return SummaryAllResult{}, err diff --git a/router/api-router.go b/router/api-router.go index 907cf1ed2885..fa2d08a4dda7 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -32,6 +32,7 @@ func SetApiRouter(router *gin.Engine) { //apiRouter.GET("/midjourney", controller.GetMidjourney) apiRouter.GET("/home_page_content", controller.GetHomePageContent) apiRouter.GET("/pricing", middleware.HeaderNavModuleAuth("pricing"), controller.GetPricing) + apiRouter.GET("/perf-metrics/admin/models", middleware.AdminAuth(), controller.GetAdminModelPerfMetrics) perfMetricsRoute := apiRouter.Group("/perf-metrics") perfMetricsRoute.Use(middleware.HeaderNavModulePublicOrUserAuth("pricing")) { diff --git a/web/src/components/data-table/hooks/use-data-table.ts b/web/src/components/data-table/hooks/use-data-table.ts index 3dd66af7e926..4ac8ac24d6d3 100644 --- a/web/src/components/data-table/hooks/use-data-table.ts +++ b/web/src/components/data-table/hooks/use-data-table.ts @@ -360,6 +360,16 @@ export function useDataTable(options: UseDataTableOptions) { initialExpanded, options.onExpandedChange ) + const [columnFilters, onColumnFiltersChange] = useControllableTableState( + options.columnFilters, + [], + options.onColumnFiltersChange + ) + const [globalFilter, onGlobalFilterChange] = useControllableTableState( + options.globalFilter, + '', + options.onGlobalFilterChange + ) const [pagination, onPaginationChange] = useControllableTableState( options.pagination, initialPagination, @@ -388,8 +398,8 @@ export function useDataTable(options: UseDataTableOptions) { columnSizing, rowSelection, expanded, - columnFilters: options.columnFilters, - globalFilter: options.globalFilter, + columnFilters, + globalFilter, pagination, }, enableRowSelection: options.enableRowSelection, @@ -408,8 +418,8 @@ export function useDataTable(options: UseDataTableOptions) { onColumnSizingChange, onRowSelectionChange, onExpandedChange, - onColumnFiltersChange: options.onColumnFiltersChange, - onGlobalFilterChange: options.onGlobalFilterChange, + onColumnFiltersChange, + onGlobalFilterChange, onPaginationChange, getCoreRowModel: getCoreRowModel(), getFilteredRowModel: withFilteredRowModel diff --git a/web/src/features/dashboard/components/models/model-performance-columns.tsx b/web/src/features/dashboard/components/models/model-performance-columns.tsx new file mode 100644 index 000000000000..3b357284a7d7 --- /dev/null +++ b/web/src/features/dashboard/components/models/model-performance-columns.tsx @@ -0,0 +1,379 @@ +/* +Copyright (C) 2023-2026 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ +/* eslint-disable react-refresh/only-export-components */ +import type { ColumnDef } from '@tanstack/react-table' +import { ArrowDown, ArrowUp, ChevronDown, ChevronRight } from 'lucide-react' +import { useTranslation } from 'react-i18next' + +import { GroupBadge } from '@/components/group-badge' +import { StatusBadge, type StatusVariant } from '@/components/status-badge' +import { Button } from '@/components/ui/button' +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from '@/components/ui/tooltip' +import { + adminPerformanceHealthRank, + type AdminPerformanceTableRow, +} from '@/features/performance-metrics/lib/admin' +import { + formatLatency, + formatThroughput, + formatUptimePct, +} from '@/features/performance-metrics/lib/format' +import type { AdminPerformanceHealth } from '@/features/performance-metrics/types' +import { toIntlLocale } from '@/i18n/languages' +import { formatNumber } from '@/lib/format' +import { cn } from '@/lib/utils' + +const HEALTH_CONFIG: Record< + AdminPerformanceHealth, + { label: string; variant: StatusVariant } +> = { + critical: { label: 'Critical', variant: 'danger' }, + degraded: { label: 'Degraded', variant: 'warning' }, + healthy: { label: 'Healthy', variant: 'success' }, + insufficient_samples: { + label: 'Insufficient samples', + variant: 'neutral', + }, + no_samples: { label: 'No performance samples', variant: 'neutral' }, +} + +const HEALTH_REASON_KEYS: Record = { + success_rate_critical: 'Success rate is below 90%', + success_rate_regression_critical: + 'Success rate dropped by at least 10 percentage points', + success_rate_degraded: 'Success rate is below 98%', + success_rate_regression: + 'Success rate dropped by at least 3 percentage points', + latency_regression: 'Average latency increased significantly', + ttft_regression: 'TTFT increased significantly', + tps_regression: 'Output TPS decreased significantly', + insufficient_samples: 'There are not enough requests to assess health', + no_samples: 'No relay performance samples were recorded', +} + +type ChangeDirection = 'higher' | 'lower' | 'neutral' + +function MetricValue(props: { + value: string + change?: number | null + changeSuffix?: '%' | ' pp' + direction?: ChangeDirection + title?: string +}) { + const change = props.change + const hasChange = typeof change === 'number' && Number.isFinite(change) + let changeClassName = 'text-muted-foreground' + if (hasChange && change !== 0 && props.direction !== 'neutral') { + const improved = props.direction === 'higher' ? change > 0 : change < 0 + changeClassName = improved ? 'text-success' : 'text-destructive' + } + + let ChangeIcon: typeof ArrowUp | null = null + if (hasChange && change > 0) ChangeIcon = ArrowUp + if (hasChange && change < 0) ChangeIcon = ArrowDown + + return ( +
+ + {props.value} + + {hasChange && ( + + {ChangeIcon && + )} +
+ ) +} + +function unavailable(row: AdminPerformanceTableRow): boolean { + return !row.metrics_enabled +} + +export function useModelPerformanceColumns(): ColumnDef[] { + const { t, i18n } = useTranslation() + const locale = toIntlLocale(i18n.resolvedLanguage || i18n.language) + + return [ + { + accessorKey: 'model_name', + header: t('Model'), + meta: { label: t('Model'), pinned: 'left' }, + cell: ({ row }) => { + if (row.original.kind === 'group') { + return ( +
+ +
+ ) + } + const canExpand = row.getCanExpand() + return ( +
+ {canExpand ? ( + + ) : ( +
+ ) + }, + filterFn: (row, _id, value) => { + const query = String(value ?? '') + .trim() + .toLowerCase() + return !query || row.original.model_name.toLowerCase().includes(query) + }, + size: 260, + minSize: 220, + }, + { + accessorKey: 'health', + header: t('Health'), + meta: { label: t('Health'), pinned: 'left' }, + cell: ({ row }) => { + if (!row.original.metrics_enabled) { + return + } + const config = HEALTH_CONFIG[row.original.health] + const reasons = row.original.health_reasons + const badge = ( + + ) + if (reasons.length === 0) return badge + return ( + + }> + {badge} + + +
    + {reasons.map((reason) => ( +
  • + {t(HEALTH_REASON_KEYS[reason] ?? reason)} +
  • + ))} +
+
+
+ ) + }, + sortingFn: (left, right) => + adminPerformanceHealthRank(left.original.health) - + adminPerformanceHealthRank(right.original.health), + filterFn: (row, id, value) => + !value?.length || value.includes(String(row.getValue(id))), + size: 160, + minSize: 150, + }, + { + accessorKey: 'enabled', + header: t('Status'), + meta: { label: t('Status') }, + cell: ({ row }) => ( + + ), + filterFn: (row, id, value) => + !value?.length || value.includes(String(row.getValue(id))), + size: 100, + }, + { + id: 'request_count', + accessorFn: (row) => row.metrics.request_count, + header: t('Requests'), + meta: { label: t('Requests') }, + cell: ({ row }) => ( + + ), + size: 115, + }, + { + id: 'failure_count', + accessorFn: (row) => row.metrics.failure_count, + header: t('Failures'), + meta: { label: t('Failures') }, + cell: ({ row }) => ( + + ), + size: 100, + }, + { + id: 'success_rate', + accessorFn: (row) => row.metrics.success_rate ?? -1, + header: t('Success rate'), + meta: { label: t('Success rate') }, + cell: ({ row }) => ( + + ), + size: 125, + }, + { + id: 'avg_ttft_ms', + accessorFn: (row) => row.metrics.avg_ttft_ms ?? -1, + header: t('TTFT'), + meta: { label: t('TTFT') }, + cell: ({ row }) => ( + + ), + size: 115, + }, + { + id: 'avg_latency_ms', + accessorFn: (row) => row.metrics.avg_latency_ms ?? -1, + header: t('Average latency'), + meta: { label: t('Average latency') }, + cell: ({ row }) => ( + + ), + size: 140, + }, + { + id: 'avg_tps', + accessorFn: (row) => row.metrics.avg_tps ?? -1, + header: t('Output TPS'), + meta: { label: t('Output TPS') }, + cell: ({ row }) => ( + + ), + size: 120, + }, + { + id: 'output_tokens', + accessorFn: (row) => row.metrics.output_tokens, + header: t('Output tokens'), + meta: { label: t('Output tokens') }, + cell: ({ row }) => ( + + ), + size: 130, + }, + { + id: 'groups', + accessorFn: (row) => row.group_names, + header: t('Active groups'), + meta: { label: t('Active groups') }, + cell: ({ row }) => ( + + ), + filterFn: (row, _id, value) => + !value?.length || + value.some((group: string) => row.original.group_names.includes(group)), + size: 115, + }, + ] +} diff --git a/web/src/features/dashboard/components/models/model-performance-table.tsx b/web/src/features/dashboard/components/models/model-performance-table.tsx new file mode 100644 index 000000000000..06e4c7a867a2 --- /dev/null +++ b/web/src/features/dashboard/components/models/model-performance-table.tsx @@ -0,0 +1,369 @@ +/* +Copyright (C) 2023-2026 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ +import { useQuery } from '@tanstack/react-query' +import { Link } from '@tanstack/react-router' +import { + Activity, + AlertTriangle, + DatabaseZap, + RefreshCw, + Settings2, +} from 'lucide-react' +import { useMemo } from 'react' +import { useTranslation } from 'react-i18next' + +import { DataTablePage, useDataTable } from '@/components/data-table' +import { + Alert, + AlertAction, + AlertDescription, + AlertTitle, +} from '@/components/ui/alert' +import { Button } from '@/components/ui/button' +import { + Empty, + EmptyContent, + EmptyDescription, + EmptyHeader, + EmptyMedia, + EmptyTitle, +} from '@/components/ui/empty' +import { IconBadge } from '@/components/ui/icon-badge' +import { getDefaultDays } from '@/features/dashboard/lib' +import type { DashboardFilters } from '@/features/dashboard/types' +import { getAdminModelPerformance } from '@/features/performance-metrics/api' +import { + buildAdminPerformanceRows, + getAdminPerformanceDisplayState, +} from '@/features/performance-metrics/lib/admin' +import { useMediaQuery } from '@/hooks' +import { formatTimestampToDate } from '@/lib/format' +import { cn } from '@/lib/utils' + +import { useModelPerformanceColumns } from './model-performance-columns' + +const LIVE_RANGE_TOLERANCE_SECONDS = 5 * 60 +const REFRESH_INTERVAL_MS = 60 * 1000 +const COLUMN_VISIBILITY_STORAGE_KEY = 'dashboard-model-performance-columns:v1' + +interface ModelPerformanceTableProps { + filters: DashboardFilters +} + +function resolveRequestRange(filters: DashboardFilters): { + start: number + end: number + followsNow: boolean + duration: number +} { + const now = Math.floor(Date.now() / 1000) + const defaultDuration = getDefaultDays(filters.time_granularity) * 24 * 3600 + const rawEnd = filters.end_timestamp + ? Math.floor(filters.end_timestamp.getTime() / 1000) + : now + const rawStart = filters.start_timestamp + ? Math.floor(filters.start_timestamp.getTime() / 1000) + : rawEnd - defaultDuration + const duration = Math.max(1, rawEnd - rawStart) + return { + start: rawStart, + end: rawEnd, + followsNow: Math.abs(now - rawEnd) <= LIVE_RANGE_TOLERANCE_SECONDS, + duration, + } +} + +export function ModelPerformanceTable(props: ModelPerformanceTableProps) { + const { t } = useTranslation() + const isMobile = useMediaQuery('(max-width: 640px)') + const requestRange = resolveRequestRange(props.filters) + const metricsQuery = useQuery({ + queryKey: [ + 'admin-model-performance', + requestRange.start, + requestRange.end, + requestRange.followsNow, + ], + queryFn: () => { + if (!requestRange.followsNow) { + return getAdminModelPerformance(requestRange.start, requestRange.end) + } + const end = Math.floor(Date.now() / 1000) + return getAdminModelPerformance(end - requestRange.duration, end) + }, + placeholderData: (previousData) => previousData, + refetchInterval: REFRESH_INTERVAL_MS, + retry: false, + }) + const data = metricsQuery.data?.data + const rows = useMemo(() => buildAdminPerformanceRows(data), [data]) + const columns = useModelPerformanceColumns() + const initialColumnVisibility = useMemo( + () => ({ + output_tokens: !isMobile, + groups: !isMobile, + }), + [isMobile] + ) + const { table } = useDataTable({ + data: rows, + columns, + getRowId: (row) => row.id, + getSubRows: (row) => row.children, + withExpandedRowModel: true, + initialSorting: [ + { id: 'health', desc: false }, + { id: 'request_count', desc: true }, + ], + initialColumnVisibility, + initialPagination: { pageIndex: 0, pageSize: 20 }, + columnVisibilityStorageKey: COLUMN_VISIBILITY_STORAGE_KEY, + }) + const groupOptions = useMemo(() => { + const groups = new Set() + for (const model of data?.models ?? []) { + for (const group of model.groups) groups.add(group.group) + } + return [...groups] + .sort((left, right) => left.localeCompare(right)) + .map((group) => ({ label: group, value: group })) + }, [data?.models]) + const displayState = getAdminPerformanceDisplayState({ + loading: metricsQuery.isLoading, + error: metricsQuery.isError, + hasData: data != null, + metricsEnabled: data?.metrics_enabled, + hasCompleteBuckets: data?.has_complete_buckets, + rowCount: rows.length, + }) + + if (displayState === 'error') { + return ( + + + + + + {t('Failed to load model performance')} + + {t( + 'The performance query failed. Existing call analytics are unaffected.' + )} + + + + + + + ) + } + + const actualEnd = data?.actual_period.end + const updatedThrough = actualEnd + ? formatTimestampToDate(actualEnd) + : t('Waiting for complete data') + + return ( +
+
+
+ + + +
+

+ {t('Model performance metrics')} +

+

+ {t('Aggregated relay performance for all models.')} +

+
+
+
+
{t('Updated through {{time}}', { time: updatedThrough })}
+ {data && ( +
+ {t('Expected aggregation delay: up to {{seconds}} seconds', { + seconds: data.expected_max_lag_seconds, + })} +
+ )} +
+
+ +
+ {props.filters.username && ( + + + {t('Global model performance')} + + {t( + 'Call analytics are filtered by user; model performance remains global.' + )} + + + )} + + {metricsQuery.isError && data && ( + + + {t('Refresh failed')} + + {t('Showing the most recent successful performance data.')} + + + + + + )} + + {displayState === 'disabled' && ( + + + {t('Metrics disabled')} + + {t('Model performance metrics are disabled.')} + + + + + + )} + + {displayState === 'no_complete_buckets' && ( + + + {t('No complete performance buckets')} + + {t( + 'The selected range does not contain a complete performance bucket.' + )} + + + )} + + } + skeletonKeyPrefix='model-performance-skeleton' + applyHeaderSize + fixedHeight={false} + hideMobile + paginationInFooter={false} + pinnedColumns={[ + { columnId: 'model_name', side: 'left' }, + { columnId: 'health', side: 'left' }, + ]} + getRowClassName={(row) => + row.original.kind === 'group' ? 'bg-muted/25' : undefined + } + getColumnClassName={(columnId) => + columnId === 'model_name' || columnId === 'health' + ? undefined + : 'text-right' + } + toolbarProps={{ + searchKey: 'model_name', + searchPlaceholder: t('Filter by model name...'), + searchDebounceMs: 200, + filters: [ + { + columnId: 'health', + title: t('Health'), + options: [ + { label: 'Critical', value: 'critical' }, + { label: 'Degraded', value: 'degraded' }, + { label: 'Healthy', value: 'healthy' }, + { + label: 'Insufficient samples', + value: 'insufficient_samples', + }, + { + label: 'No performance samples', + value: 'no_samples', + }, + ], + }, + { + columnId: 'enabled', + title: t('Status'), + options: [ + { label: 'Enabled', value: 'true' }, + { label: 'Disabled', value: 'false' }, + ], + singleSelect: true, + }, + { + columnId: 'groups', + title: t('Groups'), + options: groupOptions, + }, + ], + preActions: ( + + ), + }} + /> +
+
+ ) +} diff --git a/web/src/features/dashboard/index.tsx b/web/src/features/dashboard/index.tsx index 9d814f886a6e..e4b0563bd257 100644 --- a/web/src/features/dashboard/index.tsx +++ b/web/src/features/dashboard/index.tsx @@ -101,6 +101,12 @@ const LazyPerformanceOverview = lazy(() => })) ) +const LazyModelPerformanceTable = lazy(() => + import('./components/models/model-performance-table').then((m) => ({ + default: m.ModelPerformanceTable, + })) +) + const LazyUserCharts = lazy(() => import('./components/users/user-charts').then((m) => ({ default: m.UserCharts, @@ -356,11 +362,18 @@ export function Dashboard() { {isAdmin && ( - - }> - - - + <> + + }> + + + + + }> + + + + )} }> diff --git a/web/src/features/performance-metrics/api.ts b/web/src/features/performance-metrics/api.ts index e27ba8499e56..510376ec3205 100644 --- a/web/src/features/performance-metrics/api.ts +++ b/web/src/features/performance-metrics/api.ts @@ -18,7 +18,11 @@ For commercial licensing, please contact support@quantumnous.com */ import { api } from '@/lib/api' -import type { PerformanceMetricsData, PerfSummaryAllData } from './types' +import type { + AdminPerformanceResponse, + PerformanceMetricsData, + PerfSummaryAllData, +} from './types' export async function getPerfMetricsSummary( hours = 24 @@ -41,3 +45,19 @@ export async function getPerfMetrics( }) return res.data } + +export async function getAdminModelPerformance( + startTimestamp: number, + endTimestamp: number +): Promise { + const res = await api.get( + '/api/perf-metrics/admin/models', + { + params: { + start_timestamp: startTimestamp, + end_timestamp: endTimestamp, + }, + } + ) + return res.data +} diff --git a/web/src/features/performance-metrics/lib/__tests__/admin.test.ts b/web/src/features/performance-metrics/lib/__tests__/admin.test.ts new file mode 100644 index 000000000000..3e18a600f5ee --- /dev/null +++ b/web/src/features/performance-metrics/lib/__tests__/admin.test.ts @@ -0,0 +1,118 @@ +/* +Copyright (C) 2023-2026 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ +import assert from 'node:assert/strict' +import { describe, test } from 'node:test' + +import type { AdminPerformanceData } from '../../types' +import { + buildAdminPerformanceRows, + getAdminPerformanceDisplayState, +} from '../admin' + +const emptyMetrics = { + request_count: 0, + success_count: 0, + failure_count: 0, + success_rate: null, + avg_latency_ms: null, + avg_ttft_ms: null, + ttft_sample_count: 0, + output_tokens: 0, + avg_tps: null, + active_group_count: 0, +} + +const emptyChanges = { + request_count_pct: null, + success_rate_pp: null, + avg_latency_pct: null, + avg_ttft_pct: null, + avg_tps_pct: null, +} + +function performanceData(): AdminPerformanceData { + return { + metrics_enabled: true, + generated_at: 200, + bucket_seconds: 3600, + expected_max_lag_seconds: 300, + requested_period: { start: 100, end: 200 }, + actual_period: { start: 100, end: 200 }, + previous_period: { start: 0, end: 100 }, + available_range: { oldest_bucket_ts: 0, newest_bucket_ts: 100 }, + has_complete_buckets: true, + models: [ + { + model_name: 'gpt-test', + enabled: true, + health: 'healthy', + health_reasons: [], + metrics: { ...emptyMetrics, request_count: 20 }, + previous_metrics: emptyMetrics, + changes: emptyChanges, + groups: [ + { + group: 'default', + enabled: false, + health: 'no_samples', + health_reasons: ['no_samples'], + metrics: emptyMetrics, + previous_metrics: emptyMetrics, + changes: emptyChanges, + }, + ], + }, + ], + } +} + +describe('admin model performance helpers', () => { + test('builds expandable group rows without losing disabled group state', () => { + const rows = buildAdminPerformanceRows(performanceData()) + + assert.equal(rows.length, 1) + assert.deepEqual(rows[0].group_names, ['default']) + assert.equal(rows[0].children?.length, 1) + assert.equal(rows[0].children?.[0].kind, 'group') + assert.equal(rows[0].children?.[0].enabled, false) + }) + + test('prioritizes errors before empty data and distinguishes disabled metrics', () => { + assert.equal( + getAdminPerformanceDisplayState({ + loading: false, + error: true, + hasData: false, + rowCount: 0, + }), + 'error' + ) + assert.equal( + getAdminPerformanceDisplayState({ + loading: false, + error: false, + hasData: true, + metricsEnabled: false, + hasCompleteBuckets: true, + rowCount: 1, + }), + 'disabled' + ) + }) +}) diff --git a/web/src/features/performance-metrics/lib/admin.ts b/web/src/features/performance-metrics/lib/admin.ts new file mode 100644 index 000000000000..187646d0866b --- /dev/null +++ b/web/src/features/performance-metrics/lib/admin.ts @@ -0,0 +1,115 @@ +/* +Copyright (C) 2023-2026 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ +import type { + AdminPerformanceData, + AdminPerformanceHealth, + AdminPerformanceMetricChanges, + AdminPerformanceMetricValues, +} from '../types' + +export type AdminPerformanceTableRow = { + id: string + kind: 'model' | 'group' + model_name: string + group_name?: string + group_names: string[] + enabled: boolean + metrics_enabled: boolean + health: AdminPerformanceHealth + health_reasons: string[] + metrics: AdminPerformanceMetricValues + previous_metrics: AdminPerformanceMetricValues + changes: AdminPerformanceMetricChanges + children?: AdminPerformanceTableRow[] +} + +export type AdminPerformanceDisplayState = + | 'loading' + | 'error' + | 'disabled' + | 'no_complete_buckets' + | 'empty' + | 'ready' + +export function buildAdminPerformanceRows( + data?: AdminPerformanceData +): AdminPerformanceTableRow[] { + if (!data) return [] + + return data.models.map((model) => ({ + id: `model:${model.model_name}`, + kind: 'model', + model_name: model.model_name, + group_names: model.groups.map((group) => group.group), + enabled: model.enabled, + metrics_enabled: data.metrics_enabled, + health: model.health, + health_reasons: model.health_reasons, + metrics: model.metrics, + previous_metrics: model.previous_metrics, + changes: model.changes, + children: model.groups.map((group) => ({ + id: `model:${model.model_name}:group:${group.group}`, + kind: 'group', + model_name: model.model_name, + group_name: group.group, + group_names: [group.group], + enabled: group.enabled, + metrics_enabled: data.metrics_enabled, + health: group.health, + health_reasons: group.health_reasons, + metrics: group.metrics, + previous_metrics: group.previous_metrics, + changes: group.changes, + })), + })) +} + +export function getAdminPerformanceDisplayState(params: { + loading: boolean + error: boolean + hasData: boolean + metricsEnabled?: boolean + hasCompleteBuckets?: boolean + rowCount: number +}): AdminPerformanceDisplayState { + if (params.loading && !params.hasData) return 'loading' + if (params.error && !params.hasData) return 'error' + if (params.metricsEnabled === false) return 'disabled' + if (params.hasCompleteBuckets === false) return 'no_complete_buckets' + if (params.rowCount === 0) return 'empty' + return 'ready' +} + +export function adminPerformanceHealthRank( + health: AdminPerformanceHealth +): number { + switch (health) { + case 'critical': + return 0 + case 'degraded': + return 1 + case 'healthy': + return 2 + case 'insufficient_samples': + return 3 + default: + return 4 + } +} diff --git a/web/src/features/performance-metrics/types.ts b/web/src/features/performance-metrics/types.ts index 4e4450a51dc1..45a17e924da5 100644 --- a/web/src/features/performance-metrics/types.ts +++ b/web/src/features/performance-metrics/types.ts @@ -59,3 +59,79 @@ export type PerfSummaryAllData = { models: PerfModelSummary[] } } + +export type AdminPerformanceHealth = + | 'critical' + | 'degraded' + | 'healthy' + | 'insufficient_samples' + | 'no_samples' + +export type AdminPerformanceTimeRange = { + start: number + end: number +} + +export type AdminPerformanceMetricValues = { + request_count: number + success_count: number + failure_count: number + success_rate: number | null + avg_latency_ms: number | null + avg_ttft_ms: number | null + ttft_sample_count: number + output_tokens: number + avg_tps: number | null + active_group_count: number +} + +export type AdminPerformanceMetricChanges = { + request_count_pct: number | null + success_rate_pp: number | null + avg_latency_pct: number | null + avg_ttft_pct: number | null + avg_tps_pct: number | null +} + +export type AdminPerformanceGroup = { + group: string + enabled: boolean + health: AdminPerformanceHealth + health_reasons: string[] + metrics: AdminPerformanceMetricValues + previous_metrics: AdminPerformanceMetricValues + changes: AdminPerformanceMetricChanges +} + +export type AdminPerformanceModel = { + model_name: string + enabled: boolean + health: AdminPerformanceHealth + health_reasons: string[] + metrics: AdminPerformanceMetricValues + previous_metrics: AdminPerformanceMetricValues + changes: AdminPerformanceMetricChanges + groups: AdminPerformanceGroup[] +} + +export type AdminPerformanceData = { + metrics_enabled: boolean + generated_at: number + bucket_seconds: number + expected_max_lag_seconds: number + requested_period: AdminPerformanceTimeRange + actual_period: AdminPerformanceTimeRange + previous_period: AdminPerformanceTimeRange + available_range: { + oldest_bucket_ts: number | null + newest_bucket_ts: number | null + } + has_complete_buckets: boolean + models: AdminPerformanceModel[] +} + +export type AdminPerformanceResponse = { + success: boolean + message?: string + data: AdminPerformanceData +} diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index b48096a7fcfa..d7a8622b218f 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -55,6 +55,7 @@ "{{count}} override": "{{count}} override", "{{count}} selected targets available for bulk copy.": "{{count}} selected targets available for bulk copy.", "{{count}} tiers": "{{count}} tiers", + "{{count}} TTFT samples": "{{count}} TTFT samples", "{{count}} Uptime Kuma groups will be removed from the list.": "{{count}} Uptime Kuma groups will be removed from the list.", "{{count}} vendors": "{{count}} vendors", "{{count}} weeks ago": "{{count}} weeks ago", @@ -148,6 +149,7 @@ "Active apps": "Active apps", "Active Cache Count": "Active Cache Count", "Active Files": "Active Files", + "Active groups": "Active groups", "Active models": "Active models", "Active Tasks": "Active Tasks", "active users": "active users", @@ -264,6 +266,7 @@ "Aggregate traffic across every category": "Aggregate traffic across every category", "Aggregated across enabled groups": "Aggregated across enabled groups", "Aggregated across the apps below": "Aggregated across the apps below", + "Aggregated relay performance for all models.": "Aggregated relay performance for all models.", "Aggregated traffic by upstream model provider": "Aggregated traffic by upstream model provider", "Aggregated usage metrics and trend charts.": "Aggregated usage metrics and trend charts.", "aggregates 50+ AI providers behind one unified API. Manage access, track costs, and scale effortlessly.": "aggregates 50+ AI providers behind one unified API. Manage access, track costs, and scale effortlessly.", @@ -532,6 +535,7 @@ "Available reset credits": "Available reset credits", "Available Rewards": "Available Rewards", "Average latency": "Average latency", + "Average latency increased significantly": "Average latency increased significantly", "Average latency, TTFT, and success rate by group": "Average latency, TTFT, and success rate by group", "Average latency, TTFT, TPS, and success rate": "Average latency, TTFT, TPS, and success rate", "Average RPM": "Average RPM", @@ -706,6 +710,7 @@ "Call 1: the token group is premium": "Call 1: the token group is premium", "Call 2: the token group is default": "Call 2: the token group is default", "Call 3: the token has no group": "Call 3: the token has no group", + "Call analytics are filtered by user; model performance remains global.": "Call analytics are filtered by user; model performance remains global.", "Call Count Distribution": "Call Count Distribution", "Call Count Ranking": "Call Count Ranking", "Call Proportion": "Call Proportion", @@ -1182,6 +1187,7 @@ "Creem Payment": "Creem Payment", "Creem product ID from your Creem dashboard.": "Creem product ID from your Creem dashboard.", "Creem products must be a JSON array": "Creem products must be a JSON array", + "Critical": "Critical", "Cross-group": "Cross-group", "Cross-group retry": "Cross-group retry", "Currency": "Currency", @@ -1276,6 +1282,7 @@ "Define API endpoints for this model (JSON format)": "Define API endpoints for this model (JSON format)", "Define endpoint mappings for each provider.": "Define endpoint mappings for each provider.", "Define per-group rules to add, remove, or append selectable groups for specific user groups.": "Define per-group rules to add, remove, or append selectable groups for specific user groups.", + "Degraded": "Degraded", "Degraded performance recently": "Degraded performance recently", "Delete": "Delete", "Delete (": "Delete (", @@ -1754,6 +1761,7 @@ "Expand All": "Expand All", "Expected a JSON array of group identifiers": "Expected a JSON array of group identifiers", "Expected a JSON array.": "Expected a JSON array.", + "Expected aggregation delay: up to {{seconds}} seconds": "Expected aggregation delay: up to {{seconds}} seconds", "Experiment with prompts and models in real time.": "Experiment with prompts and models in real time.", "Expiration Time": "Expiration Time", "expired": "expired", @@ -1860,6 +1868,7 @@ "Failed to load key status": "Failed to load key status", "Failed to load login sessions": "Failed to load login sessions", "Failed to load logs": "Failed to load logs", + "Failed to load model performance": "Failed to load model performance", "Failed to load Passkey status": "Failed to load Passkey status", "Failed to load playground groups": "Failed to load playground groups", "Failed to load playground models": "Failed to load playground models", @@ -1926,6 +1935,7 @@ "Failed to update tag": "Failed to update tag", "Failed to update user": "Failed to update user", "Failure keywords": "Failure keywords", + "Failures": "Failures", "Fair": "Fair", "Fallback": "Fallback", "Fallback base URL": "Fallback base URL", @@ -2112,6 +2122,7 @@ "Global configuration and administrative tools.": "Global configuration and administrative tools.", "Global Coverage": "Global Coverage", "Global Model Configuration": "Global Model Configuration", + "Global model performance": "Global model performance", "Global throughput": "Global throughput", "Go Back": "Go Back", "Go back and edit": "Go back and edit", @@ -2338,6 +2349,7 @@ "Instance": "Instance", "Instances": "Instances", "Insufficient balance": "Insufficient balance", + "Insufficient samples": "Insufficient samples", "Integrations": "Integrations", "Inter-group overrides": "Inter-group overrides", "Inter-group ratio overrides": "Inter-group ratio overrides", @@ -2637,6 +2649,7 @@ "Merge into Other": "Merge into Other", "Message Priority": "Message Priority", "Metadata": "Metadata", + "Metrics disabled": "Metrics disabled", "min downtime": "min downtime", "Min Top-up": "Min Top-up", "Min Top-up:": "Min Top-up:", @@ -2702,6 +2715,7 @@ "Model names copied to clipboard": "Model names copied to clipboard", "Model not found": "Model not found", "Model performance metrics": "Model performance metrics", + "Model performance metrics are disabled.": "Model performance metrics are disabled.", "Model Price": "Model Price", "Model price is not configured. Please complete model pricing in settings.": "Model price is not configured. Please complete model pricing in settings.", "Model Price Not Configured": "Model Price Not Configured", @@ -2889,6 +2903,7 @@ "No channels selected": "No channels selected", "No chat presets configured. Click \"Add chat preset\" to get started.": "No chat presets configured. Click \"Add chat preset\" to get started.", "No chat presets match your search": "No chat presets match your search", + "No complete performance buckets": "No complete performance buckets", "No conflict entries available.": "No conflict entries available.", "No conflicts match your search.": "No conflicts match your search.", "No connection info found in clipboard": "No connection info found in clipboard", @@ -2905,6 +2920,7 @@ "No description available.": "No description available.", "No discount tiers configured. Click \"Add discount tier\" to get started.": "No discount tiers configured. Click \"Add discount tier\" to get started.", "No duplicate keys found": "No duplicate keys found", + "No enabled models or performance samples were found.": "No enabled models or performance samples were found.", "No enabled tokens available": "No enabled tokens available", "No encryption": "No encryption", "No endpoints configured. Switch to JSON mode or add rows to define endpoints.": "No endpoints configured. Switch to JSON mode or add rows to define endpoints.", @@ -2939,6 +2955,7 @@ "No missing models found.": "No missing models found.", "No model found.": "No model found.", "No model mappings configured. Click \"Add Mapping\" to get started.": "No model mappings configured. Click \"Add Mapping\" to get started.", + "No model performance data": "No model performance data", "No model price changes to save": "No model price changes to save", "No models available": "No models available", "No models available in this category": "No models available in this category", @@ -2969,6 +2986,7 @@ "No payment methods configured. Click \"Add method\" or use templates to get started.": "No payment methods configured. Click \"Add method\" or use templates to get started.", "No payment methods match your search": "No payment methods match your search", "No performance data available": "No performance data available", + "No performance samples": "No performance samples", "No permission to perform this action": "No permission to perform this action", "No plans available": "No plans available", "No preference": "No preference", @@ -2985,6 +3003,7 @@ "No redemption codes available. Create your first redemption code to get started.": "No redemption codes available. Create your first redemption code to get started.", "No Redemption Codes Found": "No Redemption Codes Found", "No related models available for this channel type": "No related models available for this channel type", + "No relay performance samples were recorded": "No relay performance samples were recorded", "No release notes provided.": "No release notes provided.", "No Reset": "No Reset", "No reset credits": "No reset credits", @@ -3134,6 +3153,7 @@ "Open in new tab": "Open in new tab", "Open in New Tab": "Open in New Tab", "Open menu": "Open menu", + "Open monitoring settings": "Open monitoring settings", "Open release": "Open release", "Open source": "Open source", "Open Source": "Open Source", @@ -3208,6 +3228,8 @@ "Output token price for generated tokens.": "Output token price for generated tokens.", "Output tokens": "Output tokens", "Output Tokens": "Output Tokens", + "Output TPS": "Output TPS", + "Output TPS decreased significantly": "Output TPS decreased significantly", "Overage limited": "Overage limited", "overall": "overall", "Overflow": "Overflow", @@ -4211,6 +4233,7 @@ "Showcase core capabilities with demo credentials and limited access.": "Showcase core capabilities with demo credentials and limited access.", "Showing": "Showing", "showing •": "showing •", + "Showing the most recent successful performance data.": "Showing the most recent successful performance data.", "Sidebar": "Sidebar", "Sidebar collapsed by default for new users": "Sidebar collapsed by default for new users", "Sidebar modules": "Sidebar modules", @@ -4363,6 +4386,10 @@ "succeeded": "succeeded", "Success": "Success", "Success rate": "Success rate", + "Success rate dropped by at least 10 percentage points": "Success rate dropped by at least 10 percentage points", + "Success rate dropped by at least 3 percentage points": "Success rate dropped by at least 3 percentage points", + "Success rate is below 90%": "Success rate is below 90%", + "Success rate is below 98%": "Success rate is below 98%", "Successfully created {{count}} API Key(s)": "Successfully created {{count}} API Key(s)", "Successfully created {{count}} redemption codes": "Successfully created {{count}} redemption codes", "Successfully deleted {{count}} API key(s)": "Successfully deleted {{count}} API key(s)", @@ -4519,9 +4546,11 @@ "The model that was requested": "The model that was requested", "The model you're looking for doesn't exist.": "The model you're looking for doesn't exist.", "The name displayed across the application": "The name displayed across the application", + "The performance query failed. Existing call analytics are unaffected.": "The performance query failed. Existing call analytics are unaffected.", "The public URL of your server, used for OAuth callbacks, webhooks, and other external integrations": "The public URL of your server, used for OAuth callbacks, webhooks, and other external integrations", "The requested chat preset does not exist or has been removed.": "The requested chat preset does not exist or has been removed.", "The reset request stays disabled until a credit is available.": "The reset request stays disabled until a credit is available.", + "The selected range does not contain a complete performance bucket.": "The selected range does not contain a complete performance bucket.", "The setup wizard will use this database during initialization.": "The setup wizard will use this database during initialization.", "The site is not available at the moment.": "The site is not available at the moment.", "The slug is appended to the URL:": "The slug is appended to the URL:", @@ -4540,6 +4569,7 @@ "Theme preset": "Theme preset", "Theme Settings": "Theme Settings", "There are both add and remove models pending, but you only selected one type. Confirm submitting only the selected items?": "There are both add and remove models pending, but you only selected one type. Confirm submitting only the selected items?", + "There are not enough requests to assess health": "There are not enough requests to assess health", "There is a rule for vip billed as premium → use its ratio 0.3": "There is a rule for vip billed as premium → use its ratio 0.3", "These models are still in your selection but were not returned by the upstream listing. Entries that are only model_mapping source aliases are omitted. Toggle to adjust before saving.": "These models are still in your selection but were not returned by the upstream listing. Entries that are only model_mapping source aliases are omitted. Toggle to adjust before saving.", "These toggles affect whether certain request fields are passed through to the upstream provider.": "These toggles affect whether certain request fields are passed through to the upstream provider.", @@ -4772,6 +4802,8 @@ "Trusted": "Trusted", "Try adjusting your search": "Try adjusting your search", "Try adjusting your search to locate a missing model.": "Try adjusting your search to locate a missing model.", + "TTFT": "TTFT", + "TTFT increased significantly": "TTFT increased significantly", "TTFT P50": "TTFT P50", "TTFT P95": "TTFT P95", "TTFT P99": "TTFT P99", @@ -4872,6 +4904,7 @@ "Updated daily": "Updated daily", "Updated successfully": "Updated successfully", "Updated system setting {{key}}": "Updated system setting {{key}}", + "Updated through {{time}}": "Updated through {{time}}", "Updated user {{username}} (ID: {{id}})": "Updated user {{username}} (ID: {{id}})", "Updating all channel balances. This may take a while. Please refresh to see results.": "Updating all channel balances. This may take a while. Please refresh to see results.", "Updating...": "Updating...", @@ -5119,6 +5152,7 @@ "Waffo Public Key (Production)": "Waffo Public Key (Production)", "Waffo Public Key (Sandbox)": "Waffo Public Key (Sandbox)", "Waiting": "Waiting", + "Waiting for complete data": "Waiting for complete data", "Waiting for email...": "Waiting for email...", "Wallet": "Wallet", "Wallet First": "Wallet First", diff --git a/web/src/i18n/locales/fr.json b/web/src/i18n/locales/fr.json index 638cf9abadd1..f0c60a194899 100644 --- a/web/src/i18n/locales/fr.json +++ b/web/src/i18n/locales/fr.json @@ -55,6 +55,7 @@ "{{count}} override": "{{count}} remplacement", "{{count}} selected targets available for bulk copy.": "{{count}} cibles sélectionnées disponibles pour la copie en lot.", "{{count}} tiers": "{{count}} paliers", + "{{count}} TTFT samples": "{{count}} échantillons TTFT", "{{count}} Uptime Kuma groups will be removed from the list.": "{{count}} groupes Uptime Kuma seront retirés de la liste.", "{{count}} vendors": "{{count}} fournisseurs", "{{count}} weeks ago": "il y a {{count}} semaines", @@ -148,6 +149,7 @@ "Active apps": "Applications actives", "Active Cache Count": "Nombre de caches actifs", "Active Files": "Fichiers actifs", + "Active groups": "Groupes actifs", "Active models": "Modèles actifs", "Active Tasks": "Tâches actives", "active users": "utilisateurs actifs", @@ -264,6 +266,7 @@ "Aggregate traffic across every category": "Trafic cumulé sur toutes les catégories", "Aggregated across enabled groups": "Agrégé sur les groupes activés", "Aggregated across the apps below": "Agrégé sur les applications ci-dessous", + "Aggregated relay performance for all models.": "Performances de relais agrégées pour tous les modèles.", "Aggregated traffic by upstream model provider": "Trafic agrégé par fournisseur de modèle amont", "Aggregated usage metrics and trend charts.": "Métriques d'utilisation agrégées et graphiques de tendances.", "aggregates 50+ AI providers behind one unified API. Manage access, track costs, and scale effortlessly.": "agrège plus de 50 fournisseurs IA derrière une API unifiée. Gérez l'accès, suivez les coûts et évoluez sans effort.", @@ -532,6 +535,7 @@ "Available reset credits": "Crédits de réinitialisation disponibles", "Available Rewards": "Récompenses disponibles", "Average latency": "Latence moyenne", + "Average latency increased significantly": "La latence moyenne a fortement augmenté", "Average latency, TTFT, and success rate by group": "Latence moyenne, TTFT et taux de réussite par groupe", "Average latency, TTFT, TPS, and success rate": "Latence moyenne, TTFT, TPS et taux de réussite", "Average RPM": "RPM moyen", @@ -706,6 +710,7 @@ "Call 1: the token group is premium": "Appel 1 : le groupe du jeton est premium", "Call 2: the token group is default": "Appel 2 : le groupe du jeton est default", "Call 3: the token has no group": "Appel 3 : le jeton n’a pas de groupe", + "Call analytics are filtered by user; model performance remains global.": "Les appels sont filtrés par utilisateur ; les performances restent globales.", "Call Count Distribution": "Distribution du nombre d'appels", "Call Count Ranking": "Classement du nombre d'appels", "Call Proportion": "Proportion d'appels", @@ -1182,6 +1187,7 @@ "Creem Payment": "Paiement Creem", "Creem product ID from your Creem dashboard.": "ID du produit Creem depuis votre tableau de bord Creem.", "Creem products must be a JSON array": "Les produits Creem doivent être un tableau JSON", + "Critical": "Critique", "Cross-group": "Inter-groupes", "Cross-group retry": "Nouvelle tentative inter-groupes", "Currency": "Devise", @@ -1276,6 +1282,7 @@ "Define API endpoints for this model (JSON format)": "Définir les points de terminaison API pour ce modèle (format JSON)", "Define endpoint mappings for each provider.": "Définissez les mappages d'endpoints pour chaque fournisseur.", "Define per-group rules to add, remove, or append selectable groups for specific user groups.": "Définir des règles par groupe pour ajouter, supprimer ou ajouter des groupes sélectionnables pour des groupes d'utilisateurs spécifiques.", + "Degraded": "Dégradé", "Degraded performance recently": "Performances dégradées récemment", "Delete": "Supprimer", "Delete (": "Supprimer (", @@ -1754,6 +1761,7 @@ "Expand All": "Tout développer", "Expected a JSON array of group identifiers": "Un tableau JSON d'identifiants de groupe est attendu", "Expected a JSON array.": "Un tableau JSON est attendu.", + "Expected aggregation delay: up to {{seconds}} seconds": "Délai d’agrégation prévu : jusqu’à {{seconds}} secondes", "Experiment with prompts and models in real time.": "Expérimentez avec des prompts et des modèles en temps réel.", "Expiration Time": "Heure d'expiration", "expired": "expiré", @@ -1860,6 +1868,7 @@ "Failed to load key status": "Échec du chargement du statut des clés", "Failed to load login sessions": "Impossible de charger les sessions de connexion", "Failed to load logs": "Échec du chargement des journaux", + "Failed to load model performance": "Échec du chargement des performances", "Failed to load Passkey status": "Échec du chargement du statut Passkey", "Failed to load playground groups": "Échec du chargement des groupes du playground", "Failed to load playground models": "Échec du chargement des modèles du playground", @@ -1926,6 +1935,7 @@ "Failed to update tag": "Échec de la mise à jour de l'étiquette", "Failed to update user": "Échec de la mise à jour de l'utilisateur", "Failure keywords": "Mots-clés d'échec", + "Failures": "Échecs", "Fair": "Correct", "Fallback": "Repli", "Fallback base URL": "Base URL de fallback", @@ -2112,6 +2122,7 @@ "Global configuration and administrative tools.": "Configuration globale et outils d'administration.", "Global Coverage": "Couverture mondiale", "Global Model Configuration": "Configuration globale du modèle", + "Global model performance": "Performances globales des modèles", "Global throughput": "Débit global", "Go Back": "Retour", "Go back and edit": "Retour et modifier", @@ -2338,6 +2349,7 @@ "Instance": "Instance", "Instances": "Instances", "Insufficient balance": "Solde insuffisant", + "Insufficient samples": "Échantillons insuffisants", "Integrations": "Intégrations", "Inter-group overrides": "Dérogations inter-groupes", "Inter-group ratio overrides": "Dérogations de ratio inter-groupes", @@ -2637,6 +2649,7 @@ "Merge into Other": "Fusionner dans Autres", "Message Priority": "Priorité du message", "Metadata": "Métadonnées", + "Metrics disabled": "Mesures désactivées", "min downtime": "min d'interruption", "Min Top-up": "Recharge min.", "Min Top-up:": "Recharge min. :", @@ -2702,6 +2715,7 @@ "Model names copied to clipboard": "Noms des modèles copiés dans le presse-papiers", "Model not found": "Modèle introuvable", "Model performance metrics": "Indicateurs de performance des modèles", + "Model performance metrics are disabled.": "Les mesures de performance des modèles sont désactivées.", "Model Price": "Prix du modèle", "Model price is not configured. Please complete model pricing in settings.": "Le prix du modèle n'est pas configuré. Veuillez compléter la tarification du modèle dans les paramètres.", "Model Price Not Configured": "Prix du modèle non configuré", @@ -2889,6 +2903,7 @@ "No channels selected": "Aucun canal sélectionné", "No chat presets configured. Click \"Add chat preset\" to get started.": "Aucun préréglage de chat configuré. Cliquez sur \"Ajouter un préréglage de chat\" pour commencer.", "No chat presets match your search": "Aucun préréglage de chat ne correspond à votre recherche", + "No complete performance buckets": "Aucun intervalle de performance complet", "No conflict entries available.": "Aucune entrée de conflit disponible.", "No conflicts match your search.": "Aucun conflit ne correspond à votre recherche.", "No connection info found in clipboard": "Aucune info de connexion trouvée dans le presse-papiers", @@ -2905,6 +2920,7 @@ "No description available.": "Aucune description disponible.", "No discount tiers configured. Click \"Add discount tier\" to get started.": "Aucun niveau de réduction configuré. Cliquez sur « Ajouter un niveau de réduction » pour commencer.", "No duplicate keys found": "Aucune clé dupliquée trouvée", + "No enabled models or performance samples were found.": "Aucun modèle activé ni échantillon de performance trouvé.", "No enabled tokens available": "Aucun token activé disponible", "No encryption": "Aucun chiffrement", "No endpoints configured. Switch to JSON mode or add rows to define endpoints.": "Aucun point de terminaison configuré. Passez en mode JSON ou ajoutez des lignes pour définir les points de terminaison.", @@ -2939,6 +2955,7 @@ "No missing models found.": "Aucun modèle manquant trouvé.", "No model found.": "Aucun modèle trouvé.", "No model mappings configured. Click \"Add Mapping\" to get started.": "Aucun mappage de modèle configuré. Cliquez sur « Ajouter un mappage » pour commencer.", + "No model performance data": "Aucune donnée de performance des modèles", "No model price changes to save": "Aucun changement de prix de modèle à sauvegarder", "No models available": "Aucun modèle disponible", "No models available in this category": "Aucun modèle disponible dans cette catégorie", @@ -2969,6 +2986,7 @@ "No payment methods configured. Click \"Add method\" or use templates to get started.": "Aucune méthode de paiement configurée. Cliquez sur \"Ajouter une méthode\" ou utilisez des modèles pour commencer.", "No payment methods match your search": "Aucune méthode de paiement ne correspond à votre recherche", "No performance data available": "Aucune donnée de performance disponible", + "No performance samples": "Aucun échantillon de performance", "No permission to perform this action": "Vous n’avez pas l’autorisation d’effectuer cette action", "No plans available": "Aucun plan disponible", "No preference": "Aucune préférence", @@ -2985,6 +3003,7 @@ "No redemption codes available. Create your first redemption code to get started.": "Aucun code d'échange disponible. Créez votre premier code d'échange pour commencer.", "No Redemption Codes Found": "Aucun code d'échange trouvé", "No related models available for this channel type": "Aucun modèle associé disponible pour ce type de canal", + "No relay performance samples were recorded": "Aucun échantillon de performance du relais enregistré", "No release notes provided.": "Aucune note de version fournie.", "No Reset": "Pas de réinitialisation", "No reset credits": "Aucun crédit de réinitialisation", @@ -3134,6 +3153,7 @@ "Open in new tab": "Ouvrir dans un nouvel onglet", "Open in New Tab": "Ouvrir dans un nouvel onglet", "Open menu": "Ouvrir le menu", + "Open monitoring settings": "Ouvrir les paramètres de surveillance", "Open release": "Ouvrir la version", "Open source": "Open source", "Open Source": "Open source", @@ -3208,6 +3228,8 @@ "Output token price for generated tokens.": "Prix des tokens de sortie générés.", "Output tokens": "Jetons de sortie", "Output Tokens": "Tokens de sortie", + "Output TPS": "TPS de sortie", + "Output TPS decreased significantly": "Le TPS de sortie a fortement diminué", "Overage limited": "Dépassement limité", "overall": "global", "Overflow": "Débordement", @@ -4211,6 +4233,7 @@ "Showcase core capabilities with demo credentials and limited access.": "Présenter les fonctionnalités principales avec des identifiants de démonstration et un accès limité.", "Showing": "Affichage de", "showing •": "affichage •", + "Showing the most recent successful performance data.": "Affichage des dernières données de performance disponibles.", "Sidebar": "Barre latérale", "Sidebar collapsed by default for new users": "Barre latérale masquée par défaut pour les nouveaux utilisateurs", "Sidebar modules": "Modules de la barre latérale", @@ -4363,6 +4386,10 @@ "succeeded": "réussi", "Success": "Succès", "Success rate": "Taux de réussite", + "Success rate dropped by at least 10 percentage points": "Le taux de réussite a baissé d’au moins 10 points", + "Success rate dropped by at least 3 percentage points": "Le taux de réussite a baissé d’au moins 3 points", + "Success rate is below 90%": "Le taux de réussite est inférieur à 90 %", + "Success rate is below 98%": "Le taux de réussite est inférieur à 98 %", "Successfully created {{count}} API Key(s)": "{{count}} clé(s) API créée(s) avec succès", "Successfully created {{count}} redemption codes": "{{count}} codes de réduction créés avec succès", "Successfully deleted {{count}} API key(s)": "{{count}} clé(s) API supprimée(s) avec succès", @@ -4519,9 +4546,11 @@ "The model that was requested": "Le modèle qui a été demandé", "The model you're looking for doesn't exist.": "Le modèle que vous recherchez n'existe pas.", "The name displayed across the application": "Le nom affiché dans l'application", + "The performance query failed. Existing call analytics are unaffected.": "La requête de performance a échoué. L’analyse des appels reste disponible.", "The public URL of your server, used for OAuth callbacks, webhooks, and other external integrations": "L'URL publique de votre serveur, utilisée pour les rappels OAuth, les webhooks et autres intégrations externes", "The requested chat preset does not exist or has been removed.": "Le préréglage de discussion demandé n'existe pas ou a été supprimé.", "The reset request stays disabled until a credit is available.": "La demande de réinitialisation reste désactivée tant qu’aucun crédit n’est disponible.", + "The selected range does not contain a complete performance bucket.": "La période choisie ne contient aucun intervalle de performance complet.", "The setup wizard will use this database during initialization.": "L'assistant de configuration utilisera cette base de données lors de l'initialisation.", "The site is not available at the moment.": "Le site n'est pas disponible pour le moment.", "The slug is appended to the URL:": "Le slug est ajouté à l'URL :", @@ -4540,6 +4569,7 @@ "Theme preset": "Préréglage du thème", "Theme Settings": "Paramètres du thème", "There are both add and remove models pending, but you only selected one type. Confirm submitting only the selected items?": "Il y a à la fois des modèles à ajouter et à supprimer, mais vous n'avez sélectionné qu'un seul type. Confirmer l'envoi uniquement des éléments sélectionnés ?", + "There are not enough requests to assess health": "Les requêtes sont trop peu nombreuses pour évaluer l’état", "There is a rule for vip billed as premium → use its ratio 0.3": "Il existe une règle pour vip facturé sous premium → son taux 0,3 s’applique", "These models are still in your selection but were not returned by the upstream listing. Entries that are only model_mapping source aliases are omitted. Toggle to adjust before saving.": "Ces modèles restent encore sélectionnés mais ne figurent pas dans la liste renvoyée par l'amont ; les noms qui sont uniquement des clés sources de model_mapping sont exclus. Modifiez la sélection avant d'enregistrer.", "These toggles affect whether certain request fields are passed through to the upstream provider.": "Ces bascules déterminent si certains champs de demande sont transmis au fournisseur en amont.", @@ -4772,6 +4802,8 @@ "Trusted": "Fiable", "Try adjusting your search": "Essayez d'ajuster votre recherche", "Try adjusting your search to locate a missing model.": "Essayez d'ajuster votre recherche pour localiser un modèle manquant.", + "TTFT": "TTFT", + "TTFT increased significantly": "Le TTFT a fortement augmenté", "TTFT P50": "TTFT P50", "TTFT P95": "TTFT P95", "TTFT P99": "TTFT P99", @@ -4872,6 +4904,7 @@ "Updated daily": "Mis à jour quotidiennement", "Updated successfully": "Mise à jour réussie", "Updated system setting {{key}}": "Paramètre système {{key}} mis à jour", + "Updated through {{time}}": "Données jusqu’au {{time}}", "Updated user {{username}} (ID: {{id}})": "Utilisateur {{username}} mis à jour (ID : {{id}})", "Updating all channel balances. This may take a while. Please refresh to see results.": "Mise à jour de tous les soldes des canaux. Cela peut prendre un certain temps. Veuillez actualiser pour voir les résultats.", "Updating...": "Mise à jour...", @@ -5119,6 +5152,7 @@ "Waffo Public Key (Production)": "Clé publique Waffo (Production)", "Waffo Public Key (Sandbox)": "Clé publique Waffo (Sandbox)", "Waiting": "En attente", + "Waiting for complete data": "En attente de données complètes", "Waiting for email...": "En attente de l'e-mail...", "Wallet": "Portefeuille", "Wallet First": "Portefeuille en priorité", diff --git a/web/src/i18n/locales/ja.json b/web/src/i18n/locales/ja.json index 3b9d4ce693cc..8e811ad532d1 100644 --- a/web/src/i18n/locales/ja.json +++ b/web/src/i18n/locales/ja.json @@ -55,6 +55,7 @@ "{{count}} override": "{{count}} 個のオーバーライド", "{{count}} selected targets available for bulk copy.": "一括コピーに使用できる対象が {{count}} 個選択されています。", "{{count}} tiers": "{{count}} 段階", + "{{count}} TTFT samples": "{{count}} 件の TTFT サンプル", "{{count}} Uptime Kuma groups will be removed from the list.": "{{count}} 件の Uptime Kuma グループがリストから削除されます。", "{{count}} vendors": "{{count}} ベンダー", "{{count}} weeks ago": "{{count}} 週間前", @@ -148,6 +149,7 @@ "Active apps": "アクティブなアプリ", "Active Cache Count": "アクティブキャッシュ数", "Active Files": "アクティブファイル", + "Active groups": "アクティブグループ", "Active models": "アクティブなモデル", "Active Tasks": "進行中のタスク", "active users": "アクティブユーザー", @@ -264,6 +266,7 @@ "Aggregate traffic across every category": "全カテゴリの合計トラフィック", "Aggregated across enabled groups": "有効なグループで集計", "Aggregated across the apps below": "下記アプリで集計", + "Aggregated relay performance for all models.": "全モデルのリレー性能を集計します。", "Aggregated traffic by upstream model provider": "上流モデルプロバイダー別の集計トラフィック", "Aggregated usage metrics and trend charts.": "集計された使用量メトリクスとトレンドチャート。", "aggregates 50+ AI providers behind one unified API. Manage access, track costs, and scale effortlessly.": "50以上のAIプロバイダーを統一APIで集約。アクセス管理、コスト追跡、スケーリングを簡単に。", @@ -532,6 +535,7 @@ "Available reset credits": "利用可能なリセット回数", "Available Rewards": "利用可能な報酬", "Average latency": "平均レイテンシ", + "Average latency increased significantly": "平均レイテンシーが大幅に増加しました", "Average latency, TTFT, and success rate by group": "グループ別の平均レイテンシ、TTFT、成功率", "Average latency, TTFT, TPS, and success rate": "平均レイテンシ、TTFT、TPS、成功率", "Average RPM": "平均RPM", @@ -706,6 +710,7 @@ "Call 1: the token group is premium": "呼び出し①:トークングループは premium", "Call 2: the token group is default": "呼び出し②:トークングループは default", "Call 3: the token has no group": "呼び出し③:トークンにグループなし", + "Call analytics are filtered by user; model performance remains global.": "呼び出し分析はユーザーで絞り込まれていますが、モデル性能は全体集計です。", "Call Count Distribution": "呼び出し回数分布", "Call Count Ranking": "呼び出し回数ランキング", "Call Proportion": "呼び出し比率", @@ -1182,6 +1187,7 @@ "Creem Payment": "Creem 決済", "Creem product ID from your Creem dashboard.": "Creem ダッシュボードから取得した Creem 製品 ID。", "Creem products must be a JSON array": "Creem 製品は JSON 配列でなければなりません", + "Critical": "重大", "Cross-group": "グループ横断", "Cross-group retry": "グループ横断リトライ", "Currency": "通貨", @@ -1276,6 +1282,7 @@ "Define API endpoints for this model (JSON format)": "このモデルのAPIエンドポイントを定義します (JSON形式)", "Define endpoint mappings for each provider.": "各プロバイダーごとにエンドポイントのマッピングを定義してください。", "Define per-group rules to add, remove, or append selectable groups for specific user groups.": "特定のユーザーグループに対して選択可能なグループを追加、削除、または追加するグループごとのルールを定義します。", + "Degraded": "性能低下", "Degraded performance recently": "最近パフォーマンスが低下しています", "Delete": "削除", "Delete (": "削除 (", @@ -1754,6 +1761,7 @@ "Expand All": "すべて展開", "Expected a JSON array of group identifiers": "グループ識別子の JSON 配列が必要です", "Expected a JSON array.": "JSON 配列が必要です。", + "Expected aggregation delay: up to {{seconds}} seconds": "集計遅延の目安:最大 {{seconds}} 秒", "Experiment with prompts and models in real time.": "プロンプトとモデルをリアルタイムで実験する。", "Expiration Time": "有効期限", "expired": "期限切れ", @@ -1860,6 +1868,7 @@ "Failed to load key status": "キー状態の読み込みに失敗しました", "Failed to load login sessions": "ログインセッションの読み込みに失敗しました", "Failed to load logs": "ログの読み込みに失敗しました", + "Failed to load model performance": "モデル性能を読み込めませんでした", "Failed to load Passkey status": "Passkeyのステータスの読み込みに失敗しました", "Failed to load playground groups": "プレイグラウンドのグループ読み込みに失敗しました", "Failed to load playground models": "プレイグラウンドのモデル読み込みに失敗しました", @@ -1926,6 +1935,7 @@ "Failed to update tag": "タグの更新に失敗しました", "Failed to update user": "ユーザーの更新に失敗しました", "Failure keywords": "失敗キーワード", + "Failures": "失敗数", "Fair": "公平", "Fallback": "フォールバック", "Fallback base URL": "フォールバック Base URL", @@ -2112,6 +2122,7 @@ "Global configuration and administrative tools.": "グローバル設定と管理ツール。", "Global Coverage": "グローバルカバレッジ", "Global Model Configuration": "グローバルモデル設定", + "Global model performance": "モデル全体の性能", "Global throughput": "全体スループット", "Go Back": "戻る", "Go back and edit": "戻って編集", @@ -2338,6 +2349,7 @@ "Instance": "インスタンス", "Instances": "インスタンス", "Insufficient balance": "残高が不足しています", + "Insufficient samples": "サンプル不足", "Integrations": "統合", "Inter-group overrides": "グループ間上書き", "Inter-group ratio overrides": "グループ間比率上書き", @@ -2637,6 +2649,7 @@ "Merge into Other": "その他にまとめる", "Message Priority": "メッセージの優先度", "Metadata": "メタデータ", + "Metrics disabled": "メトリクス無効", "min downtime": "分のダウンタイム", "Min Top-up": "最低チャージ額", "Min Top-up:": "最小チャージ額:", @@ -2702,6 +2715,7 @@ "Model names copied to clipboard": "モデル名がクリップボードにコピーされました", "Model not found": "モデルが見つかりません", "Model performance metrics": "モデル性能メトリクス", + "Model performance metrics are disabled.": "モデル性能メトリクスは無効です。", "Model Price": "モデル価格", "Model price is not configured. Please complete model pricing in settings.": "モデル価格が未設定です。設定でモデル料金を補完してください。", "Model Price Not Configured": "モデル価格が未設定", @@ -2889,6 +2903,7 @@ "No channels selected": "チャネルが選択されていません", "No chat presets configured. Click \"Add chat preset\" to get started.": "チャットプリセットが設定されていません。「チャットプリセットを追加」をクリックして開始してください。", "No chat presets match your search": "検索に一致するチャットプリセットがありません", + "No complete performance buckets": "完全な性能バケットがありません", "No conflict entries available.": "利用可能な競合エントリはありません。", "No conflicts match your search.": "検索条件に一致する競合はありません。", "No connection info found in clipboard": "クリップボードに接続情報が見つかりません", @@ -2905,6 +2920,7 @@ "No description available.": "説明はありません。", "No discount tiers configured. Click \"Add discount tier\" to get started.": "割引ティアは設定されていません。「割引ティアを追加」をクリックして開始してください。", "No duplicate keys found": "重複キーが見つかりませんでした", + "No enabled models or performance samples were found.": "有効なモデルまたは性能サンプルが見つかりません。", "No enabled tokens available": "有効なトークンがありません", "No encryption": "暗号化なし", "No endpoints configured. Switch to JSON mode or add rows to define endpoints.": "エンドポイントが設定されていません。JSONモードに切り替えるか、エンドポイントを定義するために行を追加してください。", @@ -2939,6 +2955,7 @@ "No missing models found.": "不足しているモデルは見つかりません。", "No model found.": "モデルが見つかりません。", "No model mappings configured. Click \"Add Mapping\" to get started.": "モデルマッピングは設定されていません。「マッピングを追加」をクリックして開始してください。", + "No model performance data": "モデル性能データがありません", "No model price changes to save": "保存するモデル価格の変更はありません", "No models available": "利用可能なモデルがありません", "No models available in this category": "このカテゴリにはモデルがありません", @@ -2969,6 +2986,7 @@ "No payment methods configured. Click \"Add method\" or use templates to get started.": "支払い方法が設定されていません。「メソッドを追加」をクリックするか、テンプレートを使用して開始してください。", "No payment methods match your search": "検索に一致する支払い方法がありません", "No performance data available": "利用可能なパフォーマンスデータはありません", + "No performance samples": "性能サンプルなし", "No permission to perform this action": "この操作を実行する権限がありません", "No plans available": "利用可能なプランがありません", "No preference": "設定なし", @@ -2985,6 +3003,7 @@ "No redemption codes available. Create your first redemption code to get started.": "利用可能な引き換えコードがありません。最初の引き換えコードを作成して開始してください。", "No Redemption Codes Found": "引き換えコードが見つかりません", "No related models available for this channel type": "このチャネルタイプに関連するモデルが利用できません", + "No relay performance samples were recorded": "リレー性能サンプルは記録されていません", "No release notes provided.": "リリースノートは提供されていません。", "No Reset": "リセットなし", "No reset credits": "リセット回数はありません", @@ -3134,6 +3153,7 @@ "Open in new tab": "新しいタブで開く", "Open in New Tab": "新しいタブで開く", "Open menu": "メニューを開く", + "Open monitoring settings": "監視設定を開く", "Open release": "リリースを開く", "Open source": "オープンソース", "Open Source": "オープンソース", @@ -3208,6 +3228,8 @@ "Output token price for generated tokens.": "生成された出力トークンの価格。", "Output tokens": "出力トークン", "Output Tokens": "出力トークン", + "Output TPS": "出力 TPS", + "Output TPS decreased significantly": "出力 TPS が大幅に低下しました", "Overage limited": "超過利用制限中", "overall": "全体", "Overflow": "オーバーフロー", @@ -4211,6 +4233,7 @@ "Showcase core capabilities with demo credentials and limited access.": "デモ用の認証情報と制限付きアクセスでコア機能を紹介します。", "Showing": "表示", "showing •": "表示中 •", + "Showing the most recent successful performance data.": "最後に取得できた性能データを表示しています。", "Sidebar": "サイドバー", "Sidebar collapsed by default for new users": "新規ユーザー向けにサイドバーをデフォルトで折りたたむ", "Sidebar modules": "サイドバーモジュール", @@ -4363,6 +4386,10 @@ "succeeded": "成功", "Success": "成功", "Success rate": "成功率", + "Success rate dropped by at least 10 percentage points": "成功率が 10 ポイント以上低下しました", + "Success rate dropped by at least 3 percentage points": "成功率が 3 ポイント以上低下しました", + "Success rate is below 90%": "成功率が 90% 未満です", + "Success rate is below 98%": "成功率が 98% 未満です", "Successfully created {{count}} API Key(s)": "{{count}}個のAPIキーが正常に作成されました", "Successfully created {{count}} redemption codes": "{{count}}件の引き換えコードが正常に作成されました", "Successfully deleted {{count}} API key(s)": "{{count}}個のAPIキーが正常に削除されました", @@ -4519,9 +4546,11 @@ "The model that was requested": "リクエストされたモデル", "The model you're looking for doesn't exist.": "お探しのモデルは存在しません。", "The name displayed across the application": "アプリケーション全体に表示される名前", + "The performance query failed. Existing call analytics are unaffected.": "性能クエリに失敗しました。既存の呼び出し分析には影響しません。", "The public URL of your server, used for OAuth callbacks, webhooks, and other external integrations": "OAuthコールバック、Webhook、その他の外部統合に使用されるサーバーの公開URL", "The requested chat preset does not exist or has been removed.": "要求されたチャットプリセットは存在しないか、削除されました。", "The reset request stays disabled until a credit is available.": "リセット回数が利用可能になるまで、リセット要求は無効です。", + "The selected range does not contain a complete performance bucket.": "選択した期間に完全な性能バケットがありません。", "The setup wizard will use this database during initialization.": "セットアップウィザードは初期化時にこのデータベースを使用します。", "The site is not available at the moment.": "現在、このサイトは利用できません。", "The slug is appended to the URL:": "スラッグがURLに追加されます:", @@ -4540,6 +4569,7 @@ "Theme preset": "テーマプリセット", "Theme Settings": "テーマ設定", "There are both add and remove models pending, but you only selected one type. Confirm submitting only the selected items?": "追加と削除の両方のモデルが保留中ですが、一方のタイプのみ選択されています。選択した項目のみ送信してよろしいですか?", + "There are not enough requests to assess health": "状態を評価するためのリクエスト数が不足しています", "There is a rule for vip billed as premium → use its ratio 0.3": "「vip が premium として課金」のルールあり → ルールの 0.3 を使用", "These models are still in your selection but were not returned by the upstream listing. Entries that are only model_mapping source aliases are omitted. Toggle to adjust before saving.": "これらはまだ選択中ですが上流のリストにありません。model_mapping にのみソース別名として載る名前は除外されています。保存前に選択を調整してください。", "These toggles affect whether certain request fields are passed through to the upstream provider.": "これらの切り替えは、特定の要求フィールドがアップストリームプロバイダーに渡されるかどうかに影響します。", @@ -4772,6 +4802,8 @@ "Trusted": "信頼済み", "Try adjusting your search": "検索条件を調整してみてください", "Try adjusting your search to locate a missing model.": "見つからないモデルを見つけるには、検索を調整してみてください。", + "TTFT": "TTFT", + "TTFT increased significantly": "TTFT が大幅に増加しました", "TTFT P50": "TTFT P50", "TTFT P95": "TTFT P95", "TTFT P99": "TTFT P99", @@ -4872,6 +4904,7 @@ "Updated daily": "毎日更新", "Updated successfully": "正常に更新されました", "Updated system setting {{key}}": "システム設定 {{key}} を更新しました", + "Updated through {{time}}": "{{time}} まで集計", "Updated user {{username}} (ID: {{id}})": "ユーザー {{username}} を更新しました(ID: {{id}})", "Updating all channel balances. This may take a while. Please refresh to see results.": "すべてのチャネル残高を更新中です。これには少し時間がかかる場合があります。結果を確認するには更新してください。", "Updating...": "更新中...", @@ -5119,6 +5152,7 @@ "Waffo Public Key (Production)": "Waffo公開鍵(本番)", "Waffo Public Key (Sandbox)": "Waffo公開鍵(サンドボックス)", "Waiting": "待機中", + "Waiting for complete data": "完全なデータを待機中", "Waiting for email...": "メールを待っています...", "Wallet": "ウォレット", "Wallet First": "ウォレット優先", diff --git a/web/src/i18n/locales/ru.json b/web/src/i18n/locales/ru.json index 853dc874d463..7ecfc7f79d55 100644 --- a/web/src/i18n/locales/ru.json +++ b/web/src/i18n/locales/ru.json @@ -55,6 +55,7 @@ "{{count}} override": "{{count}} переопределений", "{{count}} selected targets available for bulk copy.": "Для массового копирования выбрано целей: {{count}}.", "{{count}} tiers": "{{count}} уровней", + "{{count}} TTFT samples": "Образцов TTFT: {{count}}", "{{count}} Uptime Kuma groups will be removed from the list.": "{{count}} групп Uptime Kuma будут удалены из списка.", "{{count}} vendors": "поставщиков: {{count}}", "{{count}} weeks ago": "{{count}} недель назад", @@ -148,6 +149,7 @@ "Active apps": "Активные приложения", "Active Cache Count": "Активных кэшей", "Active Files": "Активных файлов", + "Active groups": "Активные группы", "Active models": "Активные модели", "Active Tasks": "Активные задачи", "active users": "активных пользователей", @@ -264,6 +266,7 @@ "Aggregate traffic across every category": "Совокупный трафик по всем категориям", "Aggregated across enabled groups": "Агрегировано по включённым группам", "Aggregated across the apps below": "Агрегировано по приложениям ниже", + "Aggregated relay performance for all models.": "Сводная производительность ретрансляции для всех моделей.", "Aggregated traffic by upstream model provider": "Агрегированный трафик по поставщикам моделей", "Aggregated usage metrics and trend charts.": "Агрегированные метрики использования и графики трендов.", "aggregates 50+ AI providers behind one unified API. Manage access, track costs, and scale effortlessly.": "объединяет 50+ ИИ-провайдеров за единым API. Управляйте доступом, отслеживайте затраты и масштабируйтесь без усилий.", @@ -532,6 +535,7 @@ "Available reset credits": "Доступные сбросы лимита", "Available Rewards": "Доступные награды", "Average latency": "Средняя задержка", + "Average latency increased significantly": "Средняя задержка значительно увеличилась", "Average latency, TTFT, and success rate by group": "Средняя задержка, TTFT и доля успешных запросов по группам", "Average latency, TTFT, TPS, and success rate": "Средняя задержка, TTFT, TPS и доля успешных запросов", "Average RPM": "Среднее число оборотов в минуту", @@ -706,6 +710,7 @@ "Call 1: the token group is premium": "Вызов 1: группа токена — premium", "Call 2: the token group is default": "Вызов 2: группа токена — default", "Call 3: the token has no group": "Вызов 3: у токена нет группы", + "Call analytics are filtered by user; model performance remains global.": "Аналитика вызовов отфильтрована по пользователю, а производительность моделей показана глобально.", "Call Count Distribution": "Распределение количества вызовов", "Call Count Ranking": "Рейтинг по количеству вызовов", "Call Proportion": "Доля вызовов", @@ -1182,6 +1187,7 @@ "Creem Payment": "Платеж Creem", "Creem product ID from your Creem dashboard.": "ID продукта Creem из вашего дашборда Creem.", "Creem products must be a JSON array": "Продукты Creem должны быть JSON-массивом", + "Critical": "Критично", "Cross-group": "Межгрупповой", "Cross-group retry": "Повтор между группами", "Currency": "Валюта", @@ -1276,6 +1282,7 @@ "Define API endpoints for this model (JSON format)": "Определить конечные точки API для этой модели (формат JSON)", "Define endpoint mappings for each provider.": "Определите сопоставления конечных точек для каждого провайдера.", "Define per-group rules to add, remove, or append selectable groups for specific user groups.": "Определите правила для групп, чтобы добавлять, удалять или дополнять доступные группы для конкретных групп пользователей.", + "Degraded": "Ухудшено", "Degraded performance recently": "Недавно наблюдалось снижение производительности", "Delete": "Удалить", "Delete (": "Удалить (", @@ -1754,6 +1761,7 @@ "Expand All": "Развернуть все", "Expected a JSON array of group identifiers": "Ожидается JSON-массив идентификаторов групп", "Expected a JSON array.": "Ожидается JSON-массив.", + "Expected aggregation delay: up to {{seconds}} seconds": "Ожидаемая задержка агрегации: до {{seconds}} секунд", "Experiment with prompts and models in real time.": "Экспериментируйте с промптами и моделями в реальном времени.", "Expiration Time": "Время истечения срока действия", "expired": "истек", @@ -1860,6 +1868,7 @@ "Failed to load key status": "Не удалось загрузить статус ключей", "Failed to load login sessions": "Не удалось загрузить сеансы входа", "Failed to load logs": "Не удалось загрузить логи", + "Failed to load model performance": "Не удалось загрузить производительность моделей", "Failed to load Passkey status": "Не удалось загрузить статус Passkey", "Failed to load playground groups": "Не удалось загрузить группы площадки", "Failed to load playground models": "Не удалось загрузить модели площадки", @@ -1926,6 +1935,7 @@ "Failed to update tag": "Не удалось обновить тег", "Failed to update user": "Не удалось обновить пользователя", "Failure keywords": "Ключевые слова сбоя", + "Failures": "Ошибки", "Fair": "Удовлетворительно", "Fallback": "Резерв", "Fallback base URL": "Base URL fallback", @@ -2112,6 +2122,7 @@ "Global configuration and administrative tools.": "Глобальная конфигурация и административные инструменты.", "Global Coverage": "Глобальное покрытие", "Global Model Configuration": "Глобальная конфигурация модели", + "Global model performance": "Общая производительность моделей", "Global throughput": "Общая пропускная способность", "Go Back": "Назад", "Go back and edit": "Вернуться и изменить", @@ -2338,6 +2349,7 @@ "Instance": "Экземпляр", "Instances": "Экземпляры", "Insufficient balance": "Недостаточно средств", + "Insufficient samples": "Недостаточно данных", "Integrations": "Интеграции", "Inter-group overrides": "Переопределения между группами", "Inter-group ratio overrides": "Переопределения соотношений между группами", @@ -2637,6 +2649,7 @@ "Merge into Other": "Объединить в «Другое»", "Message Priority": "Приоритет сообщения", "Metadata": "Метаданные", + "Metrics disabled": "Метрики отключены", "min downtime": "мин простоя", "Min Top-up": "Мин. пополнение", "Min Top-up:": "Мин. пополнение:", @@ -2702,6 +2715,7 @@ "Model names copied to clipboard": "Названия моделей скопированы в буфер обмена", "Model not found": "Модель не найдена", "Model performance metrics": "Метрики производительности моделей", + "Model performance metrics are disabled.": "Метрики производительности моделей отключены.", "Model Price": "Цена модели", "Model price is not configured. Please complete model pricing in settings.": "Цена модели не настроена. Заполните тарификацию модели в настройках.", "Model Price Not Configured": "Цена модели не настроена", @@ -2889,6 +2903,7 @@ "No channels selected": "Каналы не выбраны", "No chat presets configured. Click \"Add chat preset\" to get started.": "Пресеты чата не настроены. Нажмите \"Добавить пресет чата\", чтобы начать.", "No chat presets match your search": "Нет пресетов чата, соответствующих вашему поиску", + "No complete performance buckets": "Нет полных интервалов производительности", "No conflict entries available.": "Нет доступных записей конфликтов.", "No conflicts match your search.": "Конфликты, соответствующие вашему поиску, не найдены.", "No connection info found in clipboard": "В буфере обмена нет данных подключения", @@ -2905,6 +2920,7 @@ "No description available.": "Описание отсутствует.", "No discount tiers configured. Click \"Add discount tier\" to get started.": "Не настроены уровни скидок. Нажмите \"Добавить уровень скидки\", чтобы начать.", "No duplicate keys found": "Дубликаты ключей не найдены", + "No enabled models or performance samples were found.": "Не найдены включённые модели или образцы производительности.", "No enabled tokens available": "Нет доступных активных токенов", "No encryption": "Без шифрования", "No endpoints configured. Switch to JSON mode or add rows to define endpoints.": "Конечные точки не настроены. Переключитесь в режим JSON или добавьте строки для определения конечных точек.", @@ -2939,6 +2955,7 @@ "No missing models found.": "Недостающие модели не найдены.", "No model found.": "Модель не найдена.", "No model mappings configured. Click \"Add Mapping\" to get started.": "Не настроены сопоставления моделей. Нажмите \"Добавить сопоставление\", чтобы начать.", + "No model performance data": "Нет данных о производительности моделей", "No model price changes to save": "Нет изменений цен моделей для сохранения", "No models available": "Модели недоступны", "No models available in this category": "В этой категории нет моделей", @@ -2969,6 +2986,7 @@ "No payment methods configured. Click \"Add method\" or use templates to get started.": "Способы оплаты не настроены. Нажмите \"Добавить способ\" или используйте шаблоны, чтобы начать.", "No payment methods match your search": "Нет способов оплаты, соответствующих вашему поиску", "No performance data available": "Нет доступных данных о производительности", + "No performance samples": "Нет образцов производительности", "No permission to perform this action": "Нет прав для выполнения этого действия", "No plans available": "Нет доступных планов", "No preference": "Без предпочтений", @@ -2985,6 +3003,7 @@ "No redemption codes available. Create your first redemption code to get started.": "Нет доступных кодов активации. Создайте свой первый код активации, чтобы начать.", "No Redemption Codes Found": "Коды активации не найдены", "No related models available for this channel type": "Для этого типа канала нет доступных связанных моделей", + "No relay performance samples were recorded": "Образцы производительности ретрансляции не записаны", "No release notes provided.": "Примечания к выпуску не предоставлены.", "No Reset": "Без сброса", "No reset credits": "Нет сбросов лимита", @@ -3134,6 +3153,7 @@ "Open in new tab": "Открыть в новой вкладке", "Open in New Tab": "Открыть в новой вкладке", "Open menu": "Открыть меню", + "Open monitoring settings": "Открыть настройки мониторинга", "Open release": "Открыть выпуск", "Open source": "Открытый исходный код", "Open Source": "Открытый исходный код", @@ -3208,6 +3228,8 @@ "Output token price for generated tokens.": "Цена выходных токенов для сгенерированного текста.", "Output tokens": "Выходные токены", "Output Tokens": "Выходные токены", + "Output TPS": "Выходной TPS", + "Output TPS decreased significantly": "Выходной TPS значительно снизился", "Overage limited": "Ограничение перерасхода", "overall": "всего", "Overflow": "Переполнение", @@ -4211,6 +4233,7 @@ "Showcase core capabilities with demo credentials and limited access.": "Демонстрация основных возможностей с демо-учётными данными и ограниченным доступом.", "Showing": "Отображать", "showing •": "отображается •", + "Showing the most recent successful performance data.": "Показаны последние успешно полученные данные.", "Sidebar": "Боковая панель", "Sidebar collapsed by default for new users": "Боковая панель свернута по умолчанию для новых пользователей", "Sidebar modules": "Модули боковой панели", @@ -4363,6 +4386,10 @@ "succeeded": "успешно", "Success": "Успешно", "Success rate": "Доля успешных запросов", + "Success rate dropped by at least 10 percentage points": "Успешность снизилась как минимум на 10 п. п.", + "Success rate dropped by at least 3 percentage points": "Успешность снизилась как минимум на 3 п. п.", + "Success rate is below 90%": "Успешность ниже 90 %", + "Success rate is below 98%": "Успешность ниже 98 %", "Successfully created {{count}} API Key(s)": "Успешно создано {{count}} API-ключ(а/ей)", "Successfully created {{count}} redemption codes": "Успешно создано {{count}} кодов активации", "Successfully deleted {{count}} API key(s)": "Успешно удалено {{count}} API-ключ(а/ей)", @@ -4519,9 +4546,11 @@ "The model that was requested": "Запрошенная модель", "The model you're looking for doesn't exist.": "Модель, которую вы ищете, не существует.", "The name displayed across the application": "Имя, отображаемое в приложении", + "The performance query failed. Existing call analytics are unaffected.": "Запрос производительности завершился ошибкой. Аналитика вызовов не затронута.", "The public URL of your server, used for OAuth callbacks, webhooks, and other external integrations": "Публичный URL вашего сервера, используемый для OAuth-перенаправлений, вебхуков и других внешних интеграций", "The requested chat preset does not exist or has been removed.": "Запрошенный предустановленный чат не существует или был удален.", "The reset request stays disabled until a credit is available.": "Запрос сброса недоступен, пока нет доступного сброса.", + "The selected range does not contain a complete performance bucket.": "Выбранный диапазон не содержит полного интервала производительности.", "The setup wizard will use this database during initialization.": "Мастер настройки будет использовать эту базу данных при инициализации.", "The site is not available at the moment.": "Сайт в данный момент недоступен.", "The slug is appended to the URL:": "Слаг добавляется к URL:", @@ -4540,6 +4569,7 @@ "Theme preset": "Пресет темы", "Theme Settings": "Настройки темы", "There are both add and remove models pending, but you only selected one type. Confirm submitting only the selected items?": "Есть модели для добавления и удаления, но вы выбрали только один тип. Подтвердить отправку только выбранных элементов?", + "There are not enough requests to assess health": "Недостаточно запросов для оценки состояния", "There is a rule for vip billed as premium → use its ratio 0.3": "Есть правило «vip по premium» → используется его коэффициент 0,3", "These models are still in your selection but were not returned by the upstream listing. Entries that are only model_mapping source aliases are omitted. Toggle to adjust before saving.": "Эти имена всё ещё отмечены в выборе, но не возвращены в списке upstream; ключи только как источники model_mapping исключены. Скорректируйте выбор перед сохранением.", "These toggles affect whether certain request fields are passed through to the upstream provider.": "Эти переключатели влияют на то, передаются ли определенные поля запроса вышестоящему поставщику.", @@ -4772,6 +4802,8 @@ "Trusted": "Доверенный", "Try adjusting your search": "Попробуйте изменить условия поиска", "Try adjusting your search to locate a missing model.": "Попробуйте изменить параметры поиска, чтобы найти отсутствующую модель.", + "TTFT": "TTFT", + "TTFT increased significantly": "TTFT значительно увеличился", "TTFT P50": "TTFT P50", "TTFT P95": "TTFT P95", "TTFT P99": "TTFT P99", @@ -4872,6 +4904,7 @@ "Updated daily": "Обновляется ежедневно", "Updated successfully": "Обновлено успешно", "Updated system setting {{key}}": "Обновлён системный параметр {{key}}", + "Updated through {{time}}": "Данные по {{time}}", "Updated user {{username}} (ID: {{id}})": "Обновлён пользователь {{username}} (ID: {{id}})", "Updating all channel balances. This may take a while. Please refresh to see results.": "Обновление балансов всех каналов. Это может занять некоторое время. Пожалуйста, обновите страницу, чтобы увидеть результаты.", "Updating...": "Обновление...", @@ -5119,6 +5152,7 @@ "Waffo Public Key (Production)": "Публичный ключ Waffo (Продакшн)", "Waffo Public Key (Sandbox)": "Публичный ключ Waffo (Песочница)", "Waiting": "Ожидание", + "Waiting for complete data": "Ожидание полных данных", "Waiting for email...": "Ожидание письма...", "Wallet": "Кошелек", "Wallet First": "Кошелёк в приоритете", diff --git a/web/src/i18n/locales/vi.json b/web/src/i18n/locales/vi.json index cdc1a12626e1..61685c0e6cee 100644 --- a/web/src/i18n/locales/vi.json +++ b/web/src/i18n/locales/vi.json @@ -55,6 +55,7 @@ "{{count}} override": "{{count}} ghi đè", "{{count}} selected targets available for bulk copy.": "Có {{count}} mục tiêu đã chọn để sao chép hàng loạt.", "{{count}} tiers": "{{count}} bậc", + "{{count}} TTFT samples": "{{count}} mẫu TTFT", "{{count}} Uptime Kuma groups will be removed from the list.": "{{count}} nhóm Uptime Kuma sẽ bị xóa khỏi danh sách.", "{{count}} vendors": "{{count}} nhà cung cấp", "{{count}} weeks ago": "{{count}} tuần trước", @@ -148,6 +149,7 @@ "Active apps": "Ứng dụng đang hoạt động", "Active Cache Count": "Số bộ nhớ đệm hoạt động", "Active Files": "Tệp đang hoạt động", + "Active groups": "Nhóm đang hoạt động", "Active models": "Mô hình đang hoạt động", "Active Tasks": "Tác vụ đang hoạt động", "active users": "Người dùng tích cực", @@ -264,6 +266,7 @@ "Aggregate traffic across every category": "Tổng hợp lưu lượng tất cả danh mục", "Aggregated across enabled groups": "Tổng hợp các nhóm đang bật", "Aggregated across the apps below": "Tổng hợp các ứng dụng bên dưới", + "Aggregated relay performance for all models.": "Hiệu suất chuyển tiếp tổng hợp của tất cả mô hình.", "Aggregated traffic by upstream model provider": "Lưu lượng tổng hợp theo nhà cung cấp mô hình", "Aggregated usage metrics and trend charts.": "Chỉ số sử dụng tổng hợp và biểu đồ xu hướng.", "aggregates 50+ AI providers behind one unified API. Manage access, track costs, and scale effortlessly.": "tổng hợp hơn 50 nhà cung cấp AI sau một API thống nhất. Quản lý truy cập, theo dõi chi phí và mở rộng dễ dàng.", @@ -532,6 +535,7 @@ "Available reset credits": "Lượt đặt lại khả dụng", "Available Rewards": "Phần thưởng hiện có", "Average latency": "Độ trễ trung bình", + "Average latency increased significantly": "Độ trễ trung bình tăng đáng kể", "Average latency, TTFT, and success rate by group": "Độ trễ trung bình, TTFT và tỷ lệ thành công theo nhóm", "Average latency, TTFT, TPS, and success rate": "Độ trễ trung bình, TTFT, TPS và tỷ lệ thành công", "Average RPM": "RPM trung bình", @@ -706,6 +710,7 @@ "Call 1: the token group is premium": "Cuộc gọi 1: nhóm token là premium", "Call 2: the token group is default": "Cuộc gọi 2: nhóm token là default", "Call 3: the token has no group": "Cuộc gọi 3: token không có nhóm", + "Call analytics are filtered by user; model performance remains global.": "Phân tích lượt gọi đã lọc theo người dùng; hiệu suất mô hình vẫn là dữ liệu toàn hệ thống.", "Call Count Distribution": "Phân bổ số lượt gọi", "Call Count Ranking": "Xếp hạng số lượt gọi", "Call Proportion": "Tỷ lệ cuộc gọi", @@ -1182,6 +1187,7 @@ "Creem Payment": "Thanh toán Creem", "Creem product ID from your Creem dashboard.": "ID sản phẩm Creem từ bảng điều khiển Creem của bạn.", "Creem products must be a JSON array": "Sản phẩm Creem phải là mảng JSON", + "Critical": "Nghiêm trọng", "Cross-group": "Liên nhóm", "Cross-group retry": "Thử lại liên nhóm", "Currency": "Tiền tệ", @@ -1276,6 +1282,7 @@ "Define API endpoints for this model (JSON format)": "Định nghĩa các điểm cuối API cho mô hình này (định dạng JSON)", "Define endpoint mappings for each provider.": "Định nghĩa ánh xạ điểm cuối cho mỗi nhà cung cấp.", "Define per-group rules to add, remove, or append selectable groups for specific user groups.": "Định nghĩa quy tắc theo nhóm để thêm, xóa hoặc nối các nhóm có thể chọn cho các nhóm người dùng cụ thể.", + "Degraded": "Suy giảm", "Degraded performance recently": "Hiệu năng gần đây bị giảm", "Delete": "Xóa", "Delete (": "Xóa (", @@ -1754,6 +1761,7 @@ "Expand All": "Mở rộng tất cả", "Expected a JSON array of group identifiers": "Cần là một mảng JSON gồm các định danh nhóm", "Expected a JSON array.": "Cần là một mảng JSON.", + "Expected aggregation delay: up to {{seconds}} seconds": "Độ trễ tổng hợp dự kiến: tối đa {{seconds}} giây", "Experiment with prompts and models in real time.": "Thử nghiệm với prompt và mô hình theo thời gian thực.", "Expiration Time": "Thời gian hết hạn", "expired": "Đã hết hạn", @@ -1860,6 +1868,7 @@ "Failed to load key status": "Không thể tải trạng thái khóa", "Failed to load login sessions": "Không thể tải các phiên đăng nhập", "Failed to load logs": "Không tải được nhật ký", + "Failed to load model performance": "Không thể tải hiệu suất mô hình", "Failed to load Passkey status": "Không thể tải trạng thái Passkey", "Failed to load playground groups": "Tải nhóm playground thất bại", "Failed to load playground models": "Tải mô hình playground thất bại", @@ -1926,6 +1935,7 @@ "Failed to update tag": "Không thể cập nhật thẻ", "Failed to update user": "Không thể cập nhật người dùng", "Failure keywords": "Từ khóa thất bại", + "Failures": "Lỗi", "Fair": "Công bằng", "Fallback": "Dự phòng", "Fallback base URL": "Base URL fallback", @@ -2112,6 +2122,7 @@ "Global configuration and administrative tools.": "Cấu hình toàn cục và công cụ quản trị.", "Global Coverage": "Phạm vi toàn cầu", "Global Model Configuration": "Cấu hình Mô hình Toàn cầu", + "Global model performance": "Hiệu suất mô hình toàn hệ thống", "Global throughput": "Thông lượng toàn hệ thống", "Go Back": "Quay lại", "Go back and edit": "Quay lại và chỉnh sửa", @@ -2338,6 +2349,7 @@ "Instance": "Phiên bản", "Instances": "Phiên bản", "Insufficient balance": "Số dư không đủ", + "Insufficient samples": "Không đủ mẫu", "Integrations": "Tích hợp", "Inter-group overrides": "Ghi đè liên nhóm", "Inter-group ratio overrides": "Tỷ lệ liên nhóm ghi đè", @@ -2637,6 +2649,7 @@ "Merge into Other": "Gộp vào Khác", "Message Priority": "Ưu tiên tin nhắn", "Metadata": "Siêu dữ liệu", + "Metrics disabled": "Đã tắt chỉ số", "min downtime": "phút gián đoạn", "Min Top-up": "Nạp tối thiểu", "Min Top-up:": "Nạp tối thiểu:", @@ -2702,6 +2715,7 @@ "Model names copied to clipboard": "Tên mô hình đã được sao chép vào bộ nhớ tạm", "Model not found": "Không tìm thấy mô hình", "Model performance metrics": "Chỉ số hiệu năng mô hình", + "Model performance metrics are disabled.": "Chỉ số hiệu suất mô hình đang bị tắt.", "Model Price": "Giá mô hình", "Model price is not configured. Please complete model pricing in settings.": "Giá mô hình chưa được cấu hình. Vui lòng hoàn tất định giá mô hình trong cài đặt.", "Model Price Not Configured": "Giá mô hình chưa được cấu hình", @@ -2889,6 +2903,7 @@ "No channels selected": "Không có kênh nào được chọn", "No chat presets configured. Click \"Add chat preset\" to get started.": "Chưa cấu hình cài đặt trước trò chuyện. Nhấp vào \"Thêm cài đặt trước trò chuyện\" để bắt đầu.", "No chat presets match your search": "Không có cài đặt trước trò chuyện nào khớp với tìm kiếm của bạn", + "No complete performance buckets": "Không có khoảng hiệu suất hoàn chỉnh", "No conflict entries available.": "Không có mục xung đột nào khả dụng.", "No conflicts match your search.": "Không có xung đột nào khớp với tìm kiếm của bạn.", "No connection info found in clipboard": "Không tìm thấy thông tin kết nối trong bảng tạm", @@ -2905,6 +2920,7 @@ "No description available.": "Chưa có mô tả.", "No discount tiers configured. Click \"Add discount tier\" to get started.": "Chưa cấu hình cấp chiết khấu nào. Nhấp vào \"Thêm cấp chiết khấu\" để bắt đầu.", "No duplicate keys found": "Không tìm thấy khóa trùng lặp", + "No enabled models or performance samples were found.": "Không tìm thấy mô hình đã bật hoặc mẫu hiệu suất.", "No enabled tokens available": "Không có token nào được kích hoạt", "No encryption": "Không mã hóa", "No endpoints configured. Switch to JSON mode or add rows to define endpoints.": "Chưa cấu hình endpoint nào. Chuyển sang chế độ JSON hoặc thêm hàng để định nghĩa endpoint.", @@ -2939,6 +2955,7 @@ "No missing models found.": "Không tìm thấy mô hình nào bị thiếu.", "No model found.": "Không tìm thấy mô hình.", "No model mappings configured. Click \"Add Mapping\" to get started.": "Chưa có ánh xạ mô hình nào được cấu hình. Nhấp vào \"Thêm ánh xạ\" để bắt đầu.", + "No model performance data": "Không có dữ liệu hiệu suất mô hình", "No model price changes to save": "Không có thay đổi giá mô hình nào cần lưu", "No models available": "Không có mô hình nào khả dụng", "No models available in this category": "Không có mô hình nào trong danh mục này", @@ -2969,6 +2986,7 @@ "No payment methods configured. Click \"Add method\" or use templates to get started.": "Chưa cấu hình phương thức thanh toán. Nhấp vào \"Thêm phương thức\" hoặc sử dụng mẫu để bắt đầu.", "No payment methods match your search": "Không có phương thức thanh toán nào khớp với tìm kiếm của bạn", "No performance data available": "Không có dữ liệu hiệu năng", + "No performance samples": "Không có mẫu hiệu suất", "No permission to perform this action": "Không có quyền thực hiện thao tác này", "No plans available": "Không có gói nào khả dụng", "No preference": "Không có ưu tiên", @@ -2985,6 +3003,7 @@ "No redemption codes available. Create your first redemption code to get started.": "Hiện không có mã đổi thưởng nào. Hãy tạo mã đổi thưởng đầu tiên của bạn để bắt đầu.", "No Redemption Codes Found": "Không tìm thấy mã đổi thưởng", "No related models available for this channel type": "Không có mô hình liên quan nào cho loại kênh này", + "No relay performance samples were recorded": "Chưa ghi nhận mẫu hiệu suất chuyển tiếp", "No release notes provided.": "Không có ghi chú phát hành nào được cung cấp.", "No Reset": "Không đặt lại", "No reset credits": "Không có lượt đặt lại", @@ -3134,6 +3153,7 @@ "Open in new tab": "Mở trong tab mới", "Open in New Tab": "Mở trong tab mới", "Open menu": "Mở menu", + "Open monitoring settings": "Mở cài đặt giám sát", "Open release": "Phát hành mở", "Open source": "Mã nguồn mở", "Open Source": "Mã nguồn mở", @@ -3208,6 +3228,8 @@ "Output token price for generated tokens.": "Giá token đầu ra cho nội dung được tạo.", "Output tokens": "Token đầu ra", "Output Tokens": "Token đầu ra", + "Output TPS": "TPS đầu ra", + "Output TPS decreased significantly": "TPS đầu ra giảm đáng kể", "Overage limited": "Đã giới hạn vượt mức", "overall": "tổng", "Overflow": "Tràn trên", @@ -4211,6 +4233,7 @@ "Showcase core capabilities with demo credentials and limited access.": "Trình diễn các tính năng cốt lõi với thông tin đăng nhập demo và quyền truy cập hạn chế.", "Showing": "Đang hiển thị", "showing •": "hiển thị •", + "Showing the most recent successful performance data.": "Đang hiển thị dữ liệu hiệu suất thành công gần nhất.", "Sidebar": "Thanh bên", "Sidebar collapsed by default for new users": "Thanh bên được thu gọn theo mặc định đối với người dùng mới", "Sidebar modules": "Mô-đun thanh bên", @@ -4363,6 +4386,10 @@ "succeeded": "thành công", "Success": "Thành công", "Success rate": "Tỷ lệ thành công", + "Success rate dropped by at least 10 percentage points": "Tỷ lệ thành công giảm ít nhất 10 điểm phần trăm", + "Success rate dropped by at least 3 percentage points": "Tỷ lệ thành công giảm ít nhất 3 điểm phần trăm", + "Success rate is below 90%": "Tỷ lệ thành công dưới 90%", + "Success rate is below 98%": "Tỷ lệ thành công dưới 98%", "Successfully created {{count}} API Key(s)": "Đã tạo thành công {{count}} khóa API", "Successfully created {{count}} redemption codes": "Đã tạo thành công {{count}} mã đổi thưởng", "Successfully deleted {{count}} API key(s)": "Đã xóa thành công {{count}} khóa API", @@ -4519,9 +4546,11 @@ "The model that was requested": "Mô hình đã được yêu cầu", "The model you're looking for doesn't exist.": "Mô hình bạn đang tìm kiếm không tồn tại.", "The name displayed across the application": "Tên hiển thị trên ứng dụng", + "The performance query failed. Existing call analytics are unaffected.": "Truy vấn hiệu suất thất bại. Phân tích lượt gọi hiện có không bị ảnh hưởng.", "The public URL of your server, used for OAuth callbacks, webhooks, and other external integrations": "URL công khai của máy chủ, dùng cho callback OAuth, webhook và các tích hợp bên ngoài khác", "The requested chat preset does not exist or has been removed.": "Cài đặt sẵn cuộc trò chuyện được yêu cầu không tồn tại hoặc đã bị xóa.", "The reset request stays disabled until a credit is available.": "Yêu cầu đặt lại sẽ bị tắt cho đến khi có lượt khả dụng.", + "The selected range does not contain a complete performance bucket.": "Khoảng thời gian đã chọn không có khoảng hiệu suất hoàn chỉnh.", "The setup wizard will use this database during initialization.": "Trình hướng dẫn thiết lập sẽ sử dụng cơ sở dữ liệu này trong quá trình khởi tạo.", "The site is not available at the moment.": "Trang web hiện không khả dụng.", "The slug is appended to the URL:": "Slug được gắn vào URL:", @@ -4540,6 +4569,7 @@ "Theme preset": "Tùy chỉnh chủ đề", "Theme Settings": "Cài đặt chủ đề", "There are both add and remove models pending, but you only selected one type. Confirm submitting only the selected items?": "Có cả mô hình cần thêm và xóa đang chờ, nhưng bạn chỉ chọn một loại. Xác nhận chỉ gửi các mục đã chọn?", + "There are not enough requests to assess health": "Không đủ yêu cầu để đánh giá trạng thái", "There is a rule for vip billed as premium → use its ratio 0.3": "Có quy tắc «vip theo premium» → dùng hệ số 0.3 của quy tắc", "These models are still in your selection but were not returned by the upstream listing. Entries that are only model_mapping source aliases are omitted. Toggle to adjust before saving.": "Các model này vẫn được chọn nhưng không còn xuất hiện trong danh sách upstream; tên chỉ là khóa nguồn trong model_mapping đã được loại. Điều chỉnh trước khi lưu.", "These toggles affect whether certain request fields are passed through to the upstream provider.": "Các chuyển đổi này ảnh hưởng đến việc các trường yêu cầu nhất định có được chuyển đến nhà cung cấp dịch vụ đầu vào hay không.", @@ -4772,6 +4802,8 @@ "Trusted": "Đáng tin cậy", "Try adjusting your search": "Hãy thử điều chỉnh tìm kiếm", "Try adjusting your search to locate a missing model.": "Hãy thử điều chỉnh tìm kiếm của bạn để định vị một mô hình bị thiếu.", + "TTFT": "TTFT", + "TTFT increased significantly": "TTFT tăng đáng kể", "TTFT P50": "TTFT P50", "TTFT P95": "TTFT P95", "TTFT P99": "TTFT P99", @@ -4872,6 +4904,7 @@ "Updated daily": "Cập nhật hàng ngày", "Updated successfully": "Cập nhật thành công", "Updated system setting {{key}}": "Đã cập nhật cài đặt hệ thống {{key}}", + "Updated through {{time}}": "Dữ liệu đến {{time}}", "Updated user {{username}} (ID: {{id}})": "Đã cập nhật người dùng {{username}} (ID: {{id}})", "Updating all channel balances. This may take a while. Please refresh to see results.": "Đang cập nhật tất cả số dư kênh. Quá trình này có thể mất một chút thời gian. Vui lòng làm mới để xem kết quả.", "Updating...": "Đang cập nhật...", @@ -5119,6 +5152,7 @@ "Waffo Public Key (Production)": "Waffo Public Key (Sản xuất)", "Waffo Public Key (Sandbox)": "Khóa công khai Waffo (Sandbox)", "Waiting": "Đang chờ", + "Waiting for complete data": "Đang chờ dữ liệu hoàn chỉnh", "Waiting for email...": "Đang chờ email...", "Wallet": "Ví", "Wallet First": "Ưu tiên ví", diff --git a/web/src/i18n/locales/zh-TW.json b/web/src/i18n/locales/zh-TW.json index f8b5fafd8d2c..c76de8ae66ef 100644 --- a/web/src/i18n/locales/zh-TW.json +++ b/web/src/i18n/locales/zh-TW.json @@ -55,6 +55,7 @@ "{{count}} override": "{{count}} 個覆蓋", "{{count}} selected targets available for bulk copy.": "已選擇 {{count}} 個目標,可用於大量複製。", "{{count}} tiers": "{{count}} 檔", + "{{count}} TTFT samples": "{{count}} 個 TTFT 樣本", "{{count}} Uptime Kuma groups will be removed from the list.": "將從列表中移除 {{count}} 個 Uptime Kuma 分組。", "{{count}} vendors": "{{count}} 間供應商", "{{count}} weeks ago": "{{count}} 週前", @@ -148,6 +149,7 @@ "Active apps": "活躍套用程式", "Active Cache Count": "活躍緩存數", "Active Files": "活躍檔案", + "Active groups": "活躍用戶組", "Active models": "活躍模型", "Active Tasks": "進行中任務", "active users": "活躍用戶", @@ -264,6 +266,7 @@ "Aggregate traffic across every category": "聚合所有分類的整體流量", "Aggregated across enabled groups": "已聚合各啟用分組", "Aggregated across the apps below": "已聚合下方套用", + "Aggregated relay performance for all models.": "所有模型的中繼效能彙總。", "Aggregated traffic by upstream model provider": "按上游模型供應商聚合的流量", "Aggregated usage metrics and trend charts.": "聚合使用指標和趨勢圖表。", "aggregates 50+ AI providers behind one unified API. Manage access, track costs, and scale effortlessly.": "聚合 50+ AI 供應商於統一 API 之後。輕鬆管理存取、追蹤成本、彈性擴展。", @@ -532,6 +535,7 @@ "Available reset credits": "可用重置次數", "Available Rewards": "可用獎勵", "Average latency": "平均延遲", + "Average latency increased significantly": "平均延遲顯著增加", "Average latency, TTFT, and success rate by group": "各分組的平均延遲、首 Token 延遲和成功率", "Average latency, TTFT, TPS, and success rate": "平均延遲、TTFT、TPS 和成功率", "Average RPM": "平均 RPM", @@ -706,6 +710,7 @@ "Call 1: the token group is premium": "呼叫 ①:令牌分組是 premium", "Call 2: the token group is default": "呼叫 ②:令牌分組是 default", "Call 3: the token has no group": "呼叫 ③:令牌沒設定分組", + "Call analytics are filtered by user; model performance remains global.": "呼叫分析已按用戶篩選;模型效能仍為全站彙總。", "Call Count Distribution": "呼叫次數分佈", "Call Count Ranking": "呼叫次數排行", "Call Proportion": "呼叫比例", @@ -1182,6 +1187,7 @@ "Creem Payment": "Creem 支付", "Creem product ID from your Creem dashboard.": "從您的 Creem 儀表板獲取 Creem 產品 ID。", "Creem products must be a JSON array": "Creem 產品必須是 JSON 陣列", + "Critical": "嚴重異常", "Cross-group": "跨分組", "Cross-group retry": "跨分組重試", "Currency": "貨幣", @@ -1276,6 +1282,7 @@ "Define API endpoints for this model (JSON format)": "為此模型定義 API 端點(JSON 格式)", "Define endpoint mappings for each provider.": "為每個供應商定義端點映射。", "Define per-group rules to add, remove, or append selectable groups for specific user groups.": "為特定用戶組定義按分組規則,以新增、移除或追加可選分組。", + "Degraded": "效能下降", "Degraded performance recently": "近期性能有所下降", "Delete": "刪除", "Delete (": "刪除 (", @@ -1754,6 +1761,7 @@ "Expand All": "全部展開", "Expected a JSON array of group identifiers": "應為分組標識符的 JSON 陣列", "Expected a JSON array.": "應為 JSON 陣列。", + "Expected aggregation delay: up to {{seconds}} seconds": "預計彙總延遲最多 {{seconds}} 秒", "Experiment with prompts and models in real time.": "實時實驗提示詞和模型。", "Expiration Time": "過期時間", "expired": "已過期", @@ -1860,6 +1868,7 @@ "Failed to load key status": "載入金鑰狀態失敗", "Failed to load login sessions": "載入登入工作階段失敗", "Failed to load logs": "載入日誌失敗", + "Failed to load model performance": "模型效能載入失敗", "Failed to load Passkey status": "載入 Passkey 狀態失敗", "Failed to load playground groups": "載入 playground 分組失敗", "Failed to load playground models": "載入 playground 模型失敗", @@ -1926,6 +1935,7 @@ "Failed to update tag": "更新標籤失敗", "Failed to update user": "更新用戶失敗", "Failure keywords": "失敗關鍵詞", + "Failures": "失敗數", "Fair": "公平", "Fallback": "兜底", "Fallback base URL": "兜底 Base URL", @@ -2112,6 +2122,7 @@ "Global configuration and administrative tools.": "全局設定和管理工具。", "Global Coverage": "全球覆蓋", "Global Model Configuration": "全局模型設定", + "Global model performance": "全站模型效能", "Global throughput": "全局吞吐量", "Go Back": "返回", "Go back and edit": "返回修改", @@ -2338,6 +2349,7 @@ "Instance": "實例", "Instances": "實例", "Insufficient balance": "餘額不足", + "Insufficient samples": "樣本不足", "Integrations": "整合", "Inter-group overrides": "分組間覆蓋", "Inter-group ratio overrides": "分組間比例覆蓋", @@ -2637,6 +2649,7 @@ "Merge into Other": "合併為其他", "Message Priority": "訊息優先級", "Metadata": "元資訊", + "Metrics disabled": "指標已關閉", "min downtime": "分鐘停機", "Min Top-up": "最低儲值", "Min Top-up:": "最低儲值:", @@ -2702,6 +2715,7 @@ "Model names copied to clipboard": "模型名稱已複製到剪貼簿", "Model not found": "模型未找到", "Model performance metrics": "模型效能指標", + "Model performance metrics are disabled.": "模型效能指標目前已關閉。", "Model Price": "模型價格", "Model price is not configured. Please complete model pricing in settings.": "模型價格未設定,請前往設定補充模型價格。", "Model Price Not Configured": "模型價格未設定", @@ -2889,6 +2903,7 @@ "No channels selected": "未選擇渠道", "No chat presets configured. Click \"Add chat preset\" to get started.": "未設定聊天預設。點擊「新增聊天預設」開始。", "No chat presets match your search": "沒有匹配的聊天預設", + "No complete performance buckets": "沒有完整的效能時間桶", "No conflict entries available.": "沒有可用的衝突條目。", "No conflicts match your search.": "沒有衝突匹配您的搜尋。", "No connection info found in clipboard": "剪貼簿中未找到連線資訊", @@ -2905,6 +2920,7 @@ "No description available.": "暫無描述。", "No discount tiers configured. Click \"Add discount tier\" to get started.": "未設定折扣等級。點擊「新增折扣等級」即可開始使用。", "No duplicate keys found": "未發現重複金鑰", + "No enabled models or performance samples were found.": "未找到已啟用模型或效能樣本。", "No enabled tokens available": "目前沒有可用的啟用令牌", "No encryption": "無加密", "No endpoints configured. Switch to JSON mode or add rows to define endpoints.": "未設定端點。切換到 JSON 模式或新增列來定義端點。", @@ -2939,6 +2955,7 @@ "No missing models found.": "未找到缺失的模型。", "No model found.": "未找到模型。", "No model mappings configured. Click \"Add Mapping\" to get started.": "未設定模型映射。點擊「新增映射」即可開始使用。", + "No model performance data": "暫無模型效能資料", "No model price changes to save": "沒有模型價格變更需要儲存", "No models available": "沒有可用的模型", "No models available in this category": "該分類下沒有可用模型", @@ -2969,6 +2986,7 @@ "No payment methods configured. Click \"Add method\" or use templates to get started.": "未設定支付方式。點擊「新增方式」或使用模板開始。", "No payment methods match your search": "沒有匹配的支付方式", "No performance data available": "暫無效能數據", + "No performance samples": "無效能樣本", "No permission to perform this action": "無權進行此操作", "No plans available": "暫無可購買套餐", "No preference": "無偏好", @@ -2985,6 +3003,7 @@ "No redemption codes available. Create your first redemption code to get started.": "沒有可用的兌換碼。建立您的第一個兌換碼即可開始使用。", "No Redemption Codes Found": "未找到兌換碼", "No related models available for this channel type": "此渠道類型沒有相關模型可用", + "No relay performance samples were recorded": "未記錄到中繼效能樣本", "No release notes provided.": "未提供發佈說明。", "No Reset": "不重置", "No reset credits": "暫無重置次數", @@ -3134,6 +3153,7 @@ "Open in new tab": "在新標籤頁中打開", "Open in New Tab": "在新標籤頁中打開", "Open menu": "打開選單", + "Open monitoring settings": "開啟監控設定", "Open release": "打開版本", "Open source": "開源", "Open Source": "開源項目", @@ -3208,6 +3228,8 @@ "Output token price for generated tokens.": "生成內容的輸出 token 價格。", "Output tokens": "輸出 token", "Output Tokens": "輸出 Token", + "Output TPS": "輸出 TPS", + "Output TPS decreased significantly": "輸出 TPS 顯著下降", "Overage limited": "超額受限", "overall": "總體", "Overflow": "上溢", @@ -4211,6 +4233,7 @@ "Showcase core capabilities with demo credentials and limited access.": "使用演示憑證和有限存取權限展示核心功能。", "Showing": "顯示第", "showing •": "顯示 •", + "Showing the most recent successful performance data.": "正在顯示最近一次成功取得的效能資料。", "Sidebar": "側邊欄", "Sidebar collapsed by default for new users": "預設情況下為新用戶摺疊側邊欄", "Sidebar modules": "側邊欄模組", @@ -4363,6 +4386,10 @@ "succeeded": "已成功", "Success": "成功", "Success rate": "成功率", + "Success rate dropped by at least 10 percentage points": "成功率下降至少 10 個百分點", + "Success rate dropped by at least 3 percentage points": "成功率下降至少 3 個百分點", + "Success rate is below 90%": "成功率低於 90%", + "Success rate is below 98%": "成功率低於 98%", "Successfully created {{count}} API Key(s)": "成功建立了 {{count}} 個 API 金鑰", "Successfully created {{count}} redemption codes": "成功建立了 {{count}} 個兌換碼", "Successfully deleted {{count}} API key(s)": "成功刪除了 {{count}} 個 API 金鑰", @@ -4519,9 +4546,11 @@ "The model that was requested": "被請求的模型", "The model you're looking for doesn't exist.": "您查找的模型不存在。", "The name displayed across the application": "在整個套用程式中顯示的名稱", + "The performance query failed. Existing call analytics are unaffected.": "效能查詢失敗,現有呼叫分析不受影響。", "The public URL of your server, used for OAuth callbacks, webhooks, and other external integrations": "伺服器的公開URL,用於OAuthCallback、Webhook和其他外部整合", "The requested chat preset does not exist or has been removed.": "請求的聊天預設不存在或已被刪除。", "The reset request stays disabled until a credit is available.": "沒有可用次數時,重置請求會保持停用。", + "The selected range does not contain a complete performance bucket.": "所選時間範圍內沒有完整的效能時間桶。", "The setup wizard will use this database during initialization.": "設定精靈將在初始化過程中使用此資料庫。", "The site is not available at the moment.": "該站點目前不可用。", "The slug is appended to the URL:": "別名將附加到 URL:", @@ -4540,6 +4569,7 @@ "Theme preset": "主題預設", "Theme Settings": "主題設定", "There are both add and remove models pending, but you only selected one type. Confirm submitting only the selected items?": "目前有新增和刪除兩類待處理模型,但您只勾選了其中一類。確認僅提交已勾選的部分嗎?", + "There are not enough requests to assess health": "請求樣本不足,無法判斷健康狀態", "There is a rule for vip billed as premium → use its ratio 0.3": "存在「vip 按 premium 收費」的規則 → 用規則裡的 0.3", "These models are still in your selection but were not returned by the upstream listing. Entries that are only model_mapping source aliases are omitted. Toggle to adjust before saving.": "這些模型仍然在您的勾選列表中,但上游已不再返回該名稱;僅作為 model_mapping 來源鍵而不會出現在 upstream 列表的別名已從本視圖排除,請在儲存前調整勾選。", "These toggles affect whether certain request fields are passed through to the upstream provider.": "這些開關控制某些請求欄位是否透傳到上游服務。", @@ -4772,6 +4802,8 @@ "Trusted": "受信任", "Try adjusting your search": "請嘗試調整搜尋條件", "Try adjusting your search to locate a missing model.": "嘗試調整您的搜尋以找到缺失的模型。", + "TTFT": "TTFT", + "TTFT increased significantly": "TTFT 顯著增加", "TTFT P50": "TTFT P50", "TTFT P95": "TTFT P95", "TTFT P99": "TTFT P99", @@ -4872,6 +4904,7 @@ "Updated daily": "每日更新", "Updated successfully": "更新成功", "Updated system setting {{key}}": "修改系統設定 {{key}}", + "Updated through {{time}}": "統計截至 {{time}}", "Updated user {{username}} (ID: {{id}})": "更新用戶 {{username}}(ID: {{id}})", "Updating all channel balances. This may take a while. Please refresh to see results.": "正在更新所有渠道餘額。這可能需要一段時間。請重新整理以查看結果。", "Updating...": "正在更新...", @@ -5119,6 +5152,7 @@ "Waffo Public Key (Production)": "Waffo 公鑰(生產)", "Waffo Public Key (Sandbox)": "Waffo 公鑰(沙盒)", "Waiting": "等待中", + "Waiting for complete data": "等待完整資料", "Waiting for email...": "等待電郵...", "Wallet": "錢包", "Wallet First": "優先錢包", diff --git a/web/src/i18n/locales/zh.json b/web/src/i18n/locales/zh.json index 842991e714f0..cae4149ff439 100644 --- a/web/src/i18n/locales/zh.json +++ b/web/src/i18n/locales/zh.json @@ -55,6 +55,7 @@ "{{count}} override": "{{count}} 个覆盖", "{{count}} selected targets available for bulk copy.": "已选择 {{count}} 个目标,可用于批量复制。", "{{count}} tiers": "{{count}} 档", + "{{count}} TTFT samples": "{{count}} 个 TTFT 样本", "{{count}} Uptime Kuma groups will be removed from the list.": "将从列表中移除 {{count}} 个 Uptime Kuma 分组。", "{{count}} vendors": "{{count}} 家厂商", "{{count}} weeks ago": "{{count}} 周前", @@ -148,6 +149,7 @@ "Active apps": "活跃应用", "Active Cache Count": "活跃缓存数", "Active Files": "活跃文件", + "Active groups": "活跃用户组", "Active models": "活跃模型", "Active Tasks": "进行中任务", "active users": "活跃用户", @@ -264,6 +266,7 @@ "Aggregate traffic across every category": "聚合所有分类的整体流量", "Aggregated across enabled groups": "已聚合各启用分组", "Aggregated across the apps below": "已聚合下方应用", + "Aggregated relay performance for all models.": "所有模型的中继性能汇总。", "Aggregated traffic by upstream model provider": "按上游模型提供商聚合的流量", "Aggregated usage metrics and trend charts.": "聚合使用指标和趋势图表。", "aggregates 50+ AI providers behind one unified API. Manage access, track costs, and scale effortlessly.": "聚合 50+ AI 提供商于统一 API 之后。轻松管理访问、追踪成本、弹性扩展。", @@ -532,6 +535,7 @@ "Available reset credits": "可用重置次数", "Available Rewards": "可用奖励", "Average latency": "平均延迟", + "Average latency increased significantly": "平均延迟显著增加", "Average latency, TTFT, and success rate by group": "各分组的平均延迟、首 Token 延迟和成功率", "Average latency, TTFT, TPS, and success rate": "平均延迟、TTFT、TPS 和成功率", "Average RPM": "平均 RPM", @@ -706,6 +710,7 @@ "Call 1: the token group is premium": "调用 ①:令牌分组是 premium", "Call 2: the token group is default": "调用 ②:令牌分组是 default", "Call 3: the token has no group": "调用 ③:令牌没设置分组", + "Call analytics are filtered by user; model performance remains global.": "调用分析已按用户筛选;模型性能仍为全站汇总。", "Call Count Distribution": "调用次数分布", "Call Count Ranking": "调用次数排行", "Call Proportion": "调用比例", @@ -1182,6 +1187,7 @@ "Creem Payment": "Creem 支付", "Creem product ID from your Creem dashboard.": "从您的 Creem 仪表板获取 Creem 产品 ID。", "Creem products must be a JSON array": "Creem 产品必须是 JSON 数组", + "Critical": "严重异常", "Cross-group": "跨分组", "Cross-group retry": "跨分组重试", "Currency": "货币", @@ -1276,6 +1282,7 @@ "Define API endpoints for this model (JSON format)": "为此模型定义 API 端点(JSON 格式)", "Define endpoint mappings for each provider.": "为每个提供商定义端点映射。", "Define per-group rules to add, remove, or append selectable groups for specific user groups.": "为特定用户组定义按分组规则,以添加、移除或追加可选分组。", + "Degraded": "性能下降", "Degraded performance recently": "近期性能有所下降", "Delete": "删除", "Delete (": "删除 (", @@ -1754,6 +1761,7 @@ "Expand All": "全部展开", "Expected a JSON array of group identifiers": "应为分组标识符的 JSON 数组", "Expected a JSON array.": "应为 JSON 数组。", + "Expected aggregation delay: up to {{seconds}} seconds": "预计聚合延迟最多 {{seconds}} 秒", "Experiment with prompts and models in real time.": "实时实验提示词和模型。", "Expiration Time": "过期时间", "expired": "已过期", @@ -1860,6 +1868,7 @@ "Failed to load key status": "加载密钥状态失败", "Failed to load login sessions": "加载登录会话失败", "Failed to load logs": "加载日志失败", + "Failed to load model performance": "模型性能加载失败", "Failed to load Passkey status": "加载 Passkey 状态失败", "Failed to load playground groups": "加载 playground 分组失败", "Failed to load playground models": "加载 playground 模型失败", @@ -1926,6 +1935,7 @@ "Failed to update tag": "更新标签失败", "Failed to update user": "更新用户失败", "Failure keywords": "失败关键词", + "Failures": "失败数", "Fair": "公平", "Fallback": "兜底", "Fallback base URL": "兜底 Base URL", @@ -2112,6 +2122,7 @@ "Global configuration and administrative tools.": "全局配置和管理工具。", "Global Coverage": "全球覆盖", "Global Model Configuration": "全局模型配置", + "Global model performance": "全站模型性能", "Global throughput": "全局吞吐量", "Go Back": "返回", "Go back and edit": "返回修改", @@ -2338,6 +2349,7 @@ "Instance": "实例", "Instances": "实例", "Insufficient balance": "余额不足", + "Insufficient samples": "样本不足", "Integrations": "集成", "Inter-group overrides": "分组间覆盖", "Inter-group ratio overrides": "分组间比例覆盖", @@ -2637,6 +2649,7 @@ "Merge into Other": "合并为其他", "Message Priority": "消息优先级", "Metadata": "元信息", + "Metrics disabled": "指标已关闭", "min downtime": "分钟停机", "Min Top-up": "最低充值", "Min Top-up:": "最低充值:", @@ -2702,6 +2715,7 @@ "Model names copied to clipboard": "模型名称已复制到剪贴板", "Model not found": "模型未找到", "Model performance metrics": "模型性能指标", + "Model performance metrics are disabled.": "模型性能指标当前已关闭。", "Model Price": "模型价格", "Model price is not configured. Please complete model pricing in settings.": "模型价格未配置,请前往设置补充模型价格。", "Model Price Not Configured": "模型价格未配置", @@ -2889,6 +2903,7 @@ "No channels selected": "未选择渠道", "No chat presets configured. Click \"Add chat preset\" to get started.": "未配置聊天预设。点击\"添加聊天预设\"开始。", "No chat presets match your search": "没有匹配的聊天预设", + "No complete performance buckets": "没有完整的性能时间桶", "No conflict entries available.": "没有可用的冲突条目。", "No conflicts match your search.": "没有冲突匹配您的搜索。", "No connection info found in clipboard": "剪贴板中未找到连接信息", @@ -2905,6 +2920,7 @@ "No description available.": "暂无描述。", "No discount tiers configured. Click \"Add discount tier\" to get started.": "未配置折扣等级。点击“添加折扣等级”即可开始使用。", "No duplicate keys found": "未发现重复密钥", + "No enabled models or performance samples were found.": "未找到已启用模型或性能样本。", "No enabled tokens available": "当前没有可用的启用令牌", "No encryption": "无加密", "No endpoints configured. Switch to JSON mode or add rows to define endpoints.": "未配置端点。切换到 JSON 模式或添加行来定义端点。", @@ -2939,6 +2955,7 @@ "No missing models found.": "未找到缺失的模型。", "No model found.": "未找到模型。", "No model mappings configured. Click \"Add Mapping\" to get started.": "未配置模型映射。点击“添加映射”即可开始使用。", + "No model performance data": "暂无模型性能数据", "No model price changes to save": "没有模型价格变更需要保存", "No models available": "没有可用的模型", "No models available in this category": "该分类下没有可用模型", @@ -2969,6 +2986,7 @@ "No payment methods configured. Click \"Add method\" or use templates to get started.": "未配置支付方式。点击\"添加方式\"或使用模板开始。", "No payment methods match your search": "没有匹配的支付方式", "No performance data available": "暂无性能数据", + "No performance samples": "无性能样本", "No permission to perform this action": "无权进行此操作", "No plans available": "暂无可购买套餐", "No preference": "无偏好", @@ -2985,6 +3003,7 @@ "No redemption codes available. Create your first redemption code to get started.": "没有可用的兑换码。创建您的第一个兑换码即可开始使用。", "No Redemption Codes Found": "未找到兑换码", "No related models available for this channel type": "此渠道类型没有相关模型可用", + "No relay performance samples were recorded": "未记录到中继性能样本", "No release notes provided.": "未提供发布说明。", "No Reset": "不重置", "No reset credits": "暂无重置次数", @@ -3134,6 +3153,7 @@ "Open in new tab": "在新标签页中打开", "Open in New Tab": "在新标签页中打开", "Open menu": "打开菜单", + "Open monitoring settings": "打开监控设置", "Open release": "打开版本", "Open source": "开源", "Open Source": "开源项目", @@ -3208,6 +3228,8 @@ "Output token price for generated tokens.": "生成内容的输出 token 价格。", "Output tokens": "输出 token", "Output Tokens": "输出 Token", + "Output TPS": "输出 TPS", + "Output TPS decreased significantly": "输出 TPS 显著下降", "Overage limited": "超额受限", "overall": "总体", "Overflow": "上溢", @@ -4211,6 +4233,7 @@ "Showcase core capabilities with demo credentials and limited access.": "使用演示凭据和有限访问权限展示核心功能。", "Showing": "显示第", "showing •": "显示 •", + "Showing the most recent successful performance data.": "正在显示最近一次成功获取的性能数据。", "Sidebar": "侧边栏", "Sidebar collapsed by default for new users": "默认情况下为新用户折叠侧边栏", "Sidebar modules": "侧边栏模块", @@ -4363,6 +4386,10 @@ "succeeded": "已成功", "Success": "成功", "Success rate": "成功率", + "Success rate dropped by at least 10 percentage points": "成功率下降至少 10 个百分点", + "Success rate dropped by at least 3 percentage points": "成功率下降至少 3 个百分点", + "Success rate is below 90%": "成功率低于 90%", + "Success rate is below 98%": "成功率低于 98%", "Successfully created {{count}} API Key(s)": "成功创建了 {{count}} 个 API 密钥", "Successfully created {{count}} redemption codes": "成功创建了 {{count}} 个兑换码", "Successfully deleted {{count}} API key(s)": "成功删除了 {{count}} 个 API 密钥", @@ -4519,9 +4546,11 @@ "The model that was requested": "被请求的模型", "The model you're looking for doesn't exist.": "您查找的模型不存在。", "The name displayed across the application": "在整个应用程序中显示的名称", + "The performance query failed. Existing call analytics are unaffected.": "性能查询失败,现有调用分析不受影响。", "The public URL of your server, used for OAuth callbacks, webhooks, and other external integrations": "服务器的公开URL,用于OAuth回调、Webhook和其他外部集成", "The requested chat preset does not exist or has been removed.": "请求的聊天预设不存在或已被删除。", "The reset request stays disabled until a credit is available.": "没有可用次数时,重置请求会保持禁用。", + "The selected range does not contain a complete performance bucket.": "所选时间范围内没有完整的性能时间桶。", "The setup wizard will use this database during initialization.": "设置向导将在初始化过程中使用此数据库。", "The site is not available at the moment.": "该站点目前不可用。", "The slug is appended to the URL:": "别名将附加到 URL:", @@ -4540,6 +4569,7 @@ "Theme preset": "主题预设", "Theme Settings": "主题设置", "There are both add and remove models pending, but you only selected one type. Confirm submitting only the selected items?": "当前有新增和删除两类待处理模型,但您只勾选了其中一类。确认仅提交已勾选的部分吗?", + "There are not enough requests to assess health": "请求样本不足,无法判断健康状态", "There is a rule for vip billed as premium → use its ratio 0.3": "存在「vip 按 premium 计费」的规则 → 用规则里的 0.3", "These models are still in your selection but were not returned by the upstream listing. Entries that are only model_mapping source aliases are omitted. Toggle to adjust before saving.": "这些模型仍然在您的勾选列表中,但上游已不再返回该名称;仅作为 model_mapping 来源键而不会出现在 upstream 列表的别名已从本视图排除,请在保存前调整勾选。", "These toggles affect whether certain request fields are passed through to the upstream provider.": "这些开关控制某些请求字段是否透传到上游服务。", @@ -4772,6 +4802,8 @@ "Trusted": "受信任", "Try adjusting your search": "请尝试调整搜索条件", "Try adjusting your search to locate a missing model.": "尝试调整您的搜索以找到缺失的模型。", + "TTFT": "TTFT", + "TTFT increased significantly": "TTFT 显著增加", "TTFT P50": "TTFT P50", "TTFT P95": "TTFT P95", "TTFT P99": "TTFT P99", @@ -4872,6 +4904,7 @@ "Updated daily": "每日更新", "Updated successfully": "更新成功", "Updated system setting {{key}}": "修改系统设置 {{key}}", + "Updated through {{time}}": "统计截至 {{time}}", "Updated user {{username}} (ID: {{id}})": "更新用户 {{username}}(ID: {{id}})", "Updating all channel balances. This may take a while. Please refresh to see results.": "正在更新所有渠道余额。这可能需要一段时间。请刷新以查看结果。", "Updating...": "正在更新...", @@ -5119,6 +5152,7 @@ "Waffo Public Key (Production)": "Waffo 公钥(生产)", "Waffo Public Key (Sandbox)": "Waffo 公钥(沙盒)", "Waiting": "等待中", + "Waiting for complete data": "等待完整数据", "Waiting for email...": "等待电子邮件...", "Wallet": "钱包", "Wallet First": "优先钱包",