mirror of
https://github.com/alibaba/open-code-review.git
synced 2026-07-09 17:28:58 +00:00
feat(viewer): make human readable token usage for session (#278)
* < 1,000: show the raw number (e.g. 842) * >= 1,000: show as K (e.g. 1.22K) * >= 1,000,000: show as M (e.g. 1.21M) hover shows exact number.
This commit is contained in:
parent
022ed75682
commit
64398465d4
3 changed files with 94 additions and 11 deletions
|
|
@ -6,6 +6,7 @@ import (
|
|||
"html/template"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
|
@ -79,6 +80,7 @@ func parseTemplate(name string) (*template.Template, error) {
|
|||
"formatDuration": formatDuration,
|
||||
"formatTime": formatTime,
|
||||
"truncate": truncateText,
|
||||
"formatNumber": formatNumber,
|
||||
"add": func(a, b int) int { return a + b },
|
||||
"cardCount": func(tasks map[TaskType][]*TaskCard) int {
|
||||
n := 0
|
||||
|
|
@ -164,6 +166,51 @@ func staticFS() fs.FS {
|
|||
return sub
|
||||
}
|
||||
|
||||
func formatNumber(n int) string {
|
||||
var display string
|
||||
switch {
|
||||
case n >= 1_000_000:
|
||||
if n%1_000_000 == 0 {
|
||||
display = fmt.Sprintf("%dM", n/1_000_000)
|
||||
} else {
|
||||
display = trimFloatSuffix(fmt.Sprintf("%.2fM", float64(n)/1_000_000))
|
||||
}
|
||||
case n >= 1_000:
|
||||
if n%1_000 == 0 {
|
||||
display = fmt.Sprintf("%dK", n/1_000)
|
||||
} else {
|
||||
display = trimFloatSuffix(fmt.Sprintf("%.2fK", float64(n)/1_000))
|
||||
}
|
||||
default:
|
||||
display = strconv.Itoa(n)
|
||||
}
|
||||
return display
|
||||
}
|
||||
|
||||
// trimFloatSuffix removes trailing zeros and the trailing dot from a
|
||||
// floating-point string like "1.10K" → "1.1K", "1.00K" → "1K".
|
||||
func trimFloatSuffix(s string) string {
|
||||
// Find the dot position before the suffix (K/M).
|
||||
// Input is always "%d.%dX" or "%dX".
|
||||
dot := strings.LastIndexByte(s, '.')
|
||||
if dot < 0 {
|
||||
return s
|
||||
}
|
||||
// Find the suffix letter (K or M) — it's always the last character.
|
||||
suffix := s[len(s)-1]
|
||||
mantissa := s[:len(s)-1] // strip suffix
|
||||
|
||||
// Trim trailing zeros from the fractional part.
|
||||
i := len(mantissa) - 1
|
||||
for i >= 0 && mantissa[i] == '0' {
|
||||
i--
|
||||
}
|
||||
if i >= 0 && mantissa[i] == '.' {
|
||||
i-- // also trim the dot if whole fractional part was zeros
|
||||
}
|
||||
return mantissa[:i+1] + string(suffix)
|
||||
}
|
||||
|
||||
func formatDuration(seconds float64) string {
|
||||
d := time.Duration(seconds * float64(time.Second))
|
||||
if d < time.Minute {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,42 @@ func TestTruncateText(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestFormatNumber(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
n int
|
||||
want string
|
||||
}{
|
||||
{"zero", 0, "0"},
|
||||
{"below 1K", 999, "999"},
|
||||
{"exactly 1K", 1000, "1K"},
|
||||
{"just above 1K rounds to 1K", 1001, "1K"},
|
||||
{"1.1K", 1100, "1.1K"},
|
||||
{"1.23K", 1234, "1.23K"},
|
||||
{"9.99K", 9995, "9.99K"},
|
||||
{"exactly 10K", 10000, "10K"},
|
||||
{"exactly 100K", 100000, "100K"},
|
||||
{"just above 100K rounds to 100K", 100001, "100K"},
|
||||
{"999.5K", 999500, "999.5K"},
|
||||
{"999999 just below 1M", 999999, "1000K"},
|
||||
{"exactly 1M", 1000000, "1M"},
|
||||
{"just above 1M rounds to 1M", 1000001, "1M"},
|
||||
{"1.1M", 1100000, "1.1M"},
|
||||
{"1.23M", 1234567, "1.23M"},
|
||||
{"1.5M", 1500000, "1.5M"},
|
||||
{"exactly 10M", 10000000, "10M"},
|
||||
{"12.35M", 12345678, "12.35M"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := formatNumber(tt.n)
|
||||
if got != tt.want {
|
||||
t.Errorf("formatNumber(%d) = %q, want %q", tt.n, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatDuration(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
|
|||
|
|
@ -32,28 +32,28 @@
|
|||
<h3>Token Usage</h3>
|
||||
<div class="token-stats">
|
||||
<div class="token-item">
|
||||
<div class="token-value">{{.Session.TokenUsage.TotalPromptTokens}}</div>
|
||||
<div class="token-value" title="{{.Session.TokenUsage.TotalPromptTokens}}">{{formatNumber .Session.TokenUsage.TotalPromptTokens}}</div>
|
||||
<div class="token-label">Prompt Tokens</div>
|
||||
</div>
|
||||
<div class="token-item">
|
||||
<div class="token-value">{{.Session.TokenUsage.TotalCompletionTokens}}</div>
|
||||
<div class="token-value" title="{{.Session.TokenUsage.TotalCompletionTokens}}">{{formatNumber .Session.TokenUsage.TotalCompletionTokens}}</div>
|
||||
<div class="token-label">Completion Tokens</div>
|
||||
</div>
|
||||
<div class="token-item">
|
||||
<div class="token-value">{{add .Session.TokenUsage.TotalPromptTokens .Session.TokenUsage.TotalCompletionTokens}}</div>
|
||||
<div class="token-value" title="{{add .Session.TokenUsage.TotalPromptTokens .Session.TokenUsage.TotalCompletionTokens}}">{{formatNumber (add .Session.TokenUsage.TotalPromptTokens .Session.TokenUsage.TotalCompletionTokens)}}</div>
|
||||
<div class="token-label">Total Tokens</div>
|
||||
</div>
|
||||
<div class="token-item">
|
||||
<div class="token-value">{{.Session.TokenUsage.RequestCount}}</div>
|
||||
<div class="token-value" title="{{.Session.TokenUsage.RequestCount}}">{{formatNumber .Session.TokenUsage.RequestCount}}</div>
|
||||
<div class="token-label">LLM Requests</div>
|
||||
</div>
|
||||
{{if or .Session.TokenUsage.TotalCacheReadTokens .Session.TokenUsage.TotalCacheWriteTokens}}
|
||||
<div class="token-item">
|
||||
<div class="token-value">{{.Session.TokenUsage.TotalCacheReadTokens}}</div>
|
||||
<div class="token-value" title="{{.Session.TokenUsage.TotalCacheReadTokens}}">{{formatNumber .Session.TokenUsage.TotalCacheReadTokens}}</div>
|
||||
<div class="token-label">Cache Read</div>
|
||||
</div>
|
||||
<div class="token-item">
|
||||
<div class="token-value">{{.Session.TokenUsage.TotalCacheWriteTokens}}</div>
|
||||
<div class="token-value" title="{{.Session.TokenUsage.TotalCacheWriteTokens}}">{{formatNumber .Session.TokenUsage.TotalCacheWriteTokens}}</div>
|
||||
<div class="token-label">Cache Write</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
|
@ -71,10 +71,10 @@
|
|||
{{range .}}
|
||||
<tr>
|
||||
<td title="{{.FilePath}}">{{.FilePath | truncate 60}}</td>
|
||||
<td>{{.PromptTokens}}</td>
|
||||
<td>{{.CompletionTokens}}</td>
|
||||
{{if or $.Session.TokenUsage.TotalCacheReadTokens $.Session.TokenUsage.TotalCacheWriteTokens}}<td>{{.CacheReadTokens}}</td><td>{{.CacheWriteTokens}}</td>{{end}}
|
||||
<td><strong>{{add .PromptTokens .CompletionTokens}}</strong></td>
|
||||
<td title="{{.PromptTokens}}">{{formatNumber .PromptTokens}}</td>
|
||||
<td title="{{.CompletionTokens}}">{{formatNumber .CompletionTokens}}</td>
|
||||
{{if or $.Session.TokenUsage.TotalCacheReadTokens $.Session.TokenUsage.TotalCacheWriteTokens}}<td title="{{.CacheReadTokens}}">{{formatNumber .CacheReadTokens}}</td><td title="{{.CacheWriteTokens}}">{{formatNumber .CacheWriteTokens}}</td>{{end}}
|
||||
<td title="{{add .PromptTokens .CompletionTokens}}"><strong>{{formatNumber (add .PromptTokens .CompletionTokens)}}</strong></td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
|
|
@ -112,7 +112,7 @@
|
|||
<div class="card-header">
|
||||
<span class="request-label">Request #{{.RequestNo}}</span>
|
||||
{{if .Model}}<span class="badge badge-model">{{.Model}}</span>{{end}}
|
||||
{{if or .PromptTokens .CompletionTokens}}<span class="badge badge-tokens">P:{{.PromptTokens}} C:{{.CompletionTokens}}{{if or .CacheReadTokens .CacheWriteTokens}} CR:{{.CacheReadTokens}} CW:{{.CacheWriteTokens}}{{end}}</span>{{end}}
|
||||
{{if or .PromptTokens .CompletionTokens}}<span class="badge badge-tokens">P:<span title="{{.PromptTokens}}">{{formatNumber .PromptTokens}}</span> C:<span title="{{.CompletionTokens}}">{{formatNumber .CompletionTokens}}</span>{{if or .CacheReadTokens .CacheWriteTokens}} CR:<span title="{{.CacheReadTokens}}">{{formatNumber .CacheReadTokens}}</span> CW:<span title="{{.CacheWriteTokens}}">{{formatNumber .CacheWriteTokens}}</span>{{end}}</span>{{end}}
|
||||
{{if .DurationMs}}<span class="badge badge-duration">{{.DurationMs}}ms</span>{{end}}
|
||||
{{if .Error}}<span class="badge badge-error">{{.Error}}</span>{{end}}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue