Merge pull request #2476 from router-for-me/cherry-pick/pr-2438-to-dev
Cherry-pick PR #2438 onto dev
This commit is contained in:
@@ -201,6 +201,7 @@ var zhStrings = map[string]string{
|
|||||||
"usage_output": "输出",
|
"usage_output": "输出",
|
||||||
"usage_cached": "缓存",
|
"usage_cached": "缓存",
|
||||||
"usage_reasoning": "思考",
|
"usage_reasoning": "思考",
|
||||||
|
"usage_time": "时间",
|
||||||
|
|
||||||
// ── Logs ──
|
// ── Logs ──
|
||||||
"logs_title": "📋 日志",
|
"logs_title": "📋 日志",
|
||||||
@@ -352,6 +353,7 @@ var enStrings = map[string]string{
|
|||||||
"usage_output": "Output",
|
"usage_output": "Output",
|
||||||
"usage_cached": "Cached",
|
"usage_cached": "Cached",
|
||||||
"usage_reasoning": "Reasoning",
|
"usage_reasoning": "Reasoning",
|
||||||
|
"usage_time": "Time",
|
||||||
|
|
||||||
// ── Logs ──
|
// ── Logs ──
|
||||||
"logs_title": "📋 Logs",
|
"logs_title": "📋 Logs",
|
||||||
|
|||||||
@@ -248,6 +248,9 @@ func (m usageTabModel) renderContent() string {
|
|||||||
|
|
||||||
// Token type breakdown from details
|
// Token type breakdown from details
|
||||||
sb.WriteString(m.renderTokenBreakdown(stats))
|
sb.WriteString(m.renderTokenBreakdown(stats))
|
||||||
|
|
||||||
|
// Latency breakdown from details
|
||||||
|
sb.WriteString(m.renderLatencyBreakdown(stats))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -308,6 +311,57 @@ func (m usageTabModel) renderTokenBreakdown(modelStats map[string]any) string {
|
|||||||
lipgloss.NewStyle().Foreground(colorMuted).Render(strings.Join(parts, " ")))
|
lipgloss.NewStyle().Foreground(colorMuted).Render(strings.Join(parts, " ")))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// renderLatencyBreakdown aggregates latency_ms from model details and displays avg/min/max.
|
||||||
|
func (m usageTabModel) renderLatencyBreakdown(modelStats map[string]any) string {
|
||||||
|
details, ok := modelStats["details"]
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
detailList, ok := details.([]any)
|
||||||
|
if !ok || len(detailList) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalLatency int64
|
||||||
|
var count int
|
||||||
|
var minLatency, maxLatency int64
|
||||||
|
first := true
|
||||||
|
|
||||||
|
for _, d := range detailList {
|
||||||
|
dm, ok := d.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
latencyMs := int64(getFloat(dm, "latency_ms"))
|
||||||
|
if latencyMs <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
totalLatency += latencyMs
|
||||||
|
count++
|
||||||
|
if first {
|
||||||
|
minLatency = latencyMs
|
||||||
|
maxLatency = latencyMs
|
||||||
|
first = false
|
||||||
|
} else {
|
||||||
|
if latencyMs < minLatency {
|
||||||
|
minLatency = latencyMs
|
||||||
|
}
|
||||||
|
if latencyMs > maxLatency {
|
||||||
|
maxLatency = latencyMs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if count == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
avgLatency := totalLatency / int64(count)
|
||||||
|
return fmt.Sprintf(" │ %s: avg %dms min %dms max %dms\n",
|
||||||
|
lipgloss.NewStyle().Foreground(colorMuted).Render(T("usage_time")),
|
||||||
|
avgLatency, minLatency, maxLatency)
|
||||||
|
}
|
||||||
|
|
||||||
// renderBarChart renders a simple ASCII horizontal bar chart.
|
// renderBarChart renders a simple ASCII horizontal bar chart.
|
||||||
func renderBarChart(data map[string]any, maxBarWidth int, barColor lipgloss.Color) string {
|
func renderBarChart(data map[string]any, maxBarWidth int, barColor lipgloss.Color) string {
|
||||||
if maxBarWidth < 10 {
|
if maxBarWidth < 10 {
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
package tui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRenderLatencyBreakdown(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
modelStats map[string]any
|
||||||
|
wantEmpty bool
|
||||||
|
wantContains string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no details",
|
||||||
|
modelStats: map[string]any{},
|
||||||
|
wantEmpty: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty details",
|
||||||
|
modelStats: map[string]any{
|
||||||
|
"details": []any{},
|
||||||
|
},
|
||||||
|
wantEmpty: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "details with zero latency",
|
||||||
|
modelStats: map[string]any{
|
||||||
|
"details": []any{
|
||||||
|
map[string]any{
|
||||||
|
"latency_ms": float64(0),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantEmpty: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single request with latency",
|
||||||
|
modelStats: map[string]any{
|
||||||
|
"details": []any{
|
||||||
|
map[string]any{
|
||||||
|
"latency_ms": float64(1500),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantEmpty: false,
|
||||||
|
wantContains: "avg 1500ms min 1500ms max 1500ms",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple requests with varying latency",
|
||||||
|
modelStats: map[string]any{
|
||||||
|
"details": []any{
|
||||||
|
map[string]any{
|
||||||
|
"latency_ms": float64(100),
|
||||||
|
},
|
||||||
|
map[string]any{
|
||||||
|
"latency_ms": float64(200),
|
||||||
|
},
|
||||||
|
map[string]any{
|
||||||
|
"latency_ms": float64(300),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantEmpty: false,
|
||||||
|
wantContains: "avg 200ms min 100ms max 300ms",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mixed valid and invalid latency values",
|
||||||
|
modelStats: map[string]any{
|
||||||
|
"details": []any{
|
||||||
|
map[string]any{
|
||||||
|
"latency_ms": float64(500),
|
||||||
|
},
|
||||||
|
map[string]any{
|
||||||
|
"latency_ms": float64(0),
|
||||||
|
},
|
||||||
|
map[string]any{
|
||||||
|
"latency_ms": float64(1500),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantEmpty: false,
|
||||||
|
wantContains: "avg 1000ms min 500ms max 1500ms",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
m := usageTabModel{}
|
||||||
|
result := m.renderLatencyBreakdown(tt.modelStats)
|
||||||
|
|
||||||
|
if tt.wantEmpty {
|
||||||
|
if result != "" {
|
||||||
|
t.Errorf("renderLatencyBreakdown() = %q, want empty string", result)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if result == "" {
|
||||||
|
t.Errorf("renderLatencyBreakdown() = empty, want non-empty string")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.wantContains != "" && !strings.Contains(result, tt.wantContains) {
|
||||||
|
t.Errorf("renderLatencyBreakdown() = %q, want to contain %q", result, tt.wantContains)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUsageTimeTranslations(t *testing.T) {
|
||||||
|
prevLocale := CurrentLocale()
|
||||||
|
t.Cleanup(func() {
|
||||||
|
SetLocale(prevLocale)
|
||||||
|
})
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
locale string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{locale: "en", want: "Time"},
|
||||||
|
{locale: "zh", want: "时间"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.locale, func(t *testing.T) {
|
||||||
|
SetLocale(tt.locale)
|
||||||
|
if got := T("usage_time"); got != tt.want {
|
||||||
|
t.Fatalf("T(usage_time) = %q, want %q", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user