Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
94e067a2
Unverified
Commit
94e067a2
authored
Mar 16, 2026
by
Wesley Liddick
Committed by
GitHub
Mar 16, 2026
Browse files
Merge pull request #1040 from 0xObjc/codex/fix-user-spending-ranking-others
fix(admin): polish spending ranking and usage defaults
parents
4293c891
8147866c
Changes
10
Show whitespace changes
Inline
Side-by-side
backend/internal/handler/admin/dashboard_handler.go
View file @
94e067a2
...
@@ -512,6 +512,8 @@ func (h *DashboardHandler) GetUserSpendingRanking(c *gin.Context) {
...
@@ -512,6 +512,8 @@ func (h *DashboardHandler) GetUserSpendingRanking(c *gin.Context) {
payload
:=
gin
.
H
{
payload
:=
gin
.
H
{
"ranking"
:
ranking
.
Ranking
,
"ranking"
:
ranking
.
Ranking
,
"total_actual_cost"
:
ranking
.
TotalActualCost
,
"total_actual_cost"
:
ranking
.
TotalActualCost
,
"total_requests"
:
ranking
.
TotalRequests
,
"total_tokens"
:
ranking
.
TotalTokens
,
"start_date"
:
startTime
.
Format
(
"2006-01-02"
),
"start_date"
:
startTime
.
Format
(
"2006-01-02"
),
"end_date"
:
endTime
.
Add
(
-
24
*
time
.
Hour
)
.
Format
(
"2006-01-02"
),
"end_date"
:
endTime
.
Add
(
-
24
*
time
.
Hour
)
.
Format
(
"2006-01-02"
),
}
}
...
...
backend/internal/handler/admin/dashboard_handler_request_type_test.go
View file @
94e067a2
...
@@ -61,6 +61,8 @@ func (s *dashboardUsageRepoCapture) GetUserSpendingRanking(
...
@@ -61,6 +61,8 @@ func (s *dashboardUsageRepoCapture) GetUserSpendingRanking(
return
&
usagestats
.
UserSpendingRankingResponse
{
return
&
usagestats
.
UserSpendingRankingResponse
{
Ranking
:
s
.
ranking
,
Ranking
:
s
.
ranking
,
TotalActualCost
:
s
.
rankingTotal
,
TotalActualCost
:
s
.
rankingTotal
,
TotalRequests
:
44
,
TotalTokens
:
1234
,
},
nil
},
nil
}
}
...
@@ -164,6 +166,8 @@ func TestDashboardUsersRankingLimitAndCache(t *testing.T) {
...
@@ -164,6 +166,8 @@ func TestDashboardUsersRankingLimitAndCache(t *testing.T) {
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
require
.
Equal
(
t
,
50
,
repo
.
rankingLimit
)
require
.
Equal
(
t
,
50
,
repo
.
rankingLimit
)
require
.
Contains
(
t
,
rec
.
Body
.
String
(),
"
\"
total_actual_cost
\"
:88.8"
)
require
.
Contains
(
t
,
rec
.
Body
.
String
(),
"
\"
total_actual_cost
\"
:88.8"
)
require
.
Contains
(
t
,
rec
.
Body
.
String
(),
"
\"
total_requests
\"
:44"
)
require
.
Contains
(
t
,
rec
.
Body
.
String
(),
"
\"
total_tokens
\"
:1234"
)
require
.
Equal
(
t
,
"miss"
,
rec
.
Header
()
.
Get
(
"X-Snapshot-Cache"
))
require
.
Equal
(
t
,
"miss"
,
rec
.
Header
()
.
Get
(
"X-Snapshot-Cache"
))
req2
:=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/admin/dashboard/users-ranking?limit=100&start_date=2025-01-01&end_date=2025-01-02"
,
nil
)
req2
:=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/admin/dashboard/users-ranking?limit=100&start_date=2025-01-01&end_date=2025-01-02"
,
nil
)
...
...
backend/internal/pkg/usagestats/usage_log_types.go
View file @
94e067a2
...
@@ -125,6 +125,8 @@ type UserSpendingRankingItem struct {
...
@@ -125,6 +125,8 @@ type UserSpendingRankingItem struct {
type
UserSpendingRankingResponse
struct
{
type
UserSpendingRankingResponse
struct
{
Ranking
[]
UserSpendingRankingItem
`json:"ranking"`
Ranking
[]
UserSpendingRankingItem
`json:"ranking"`
TotalActualCost
float64
`json:"total_actual_cost"`
TotalActualCost
float64
`json:"total_actual_cost"`
TotalRequests
int64
`json:"total_requests"`
TotalTokens
int64
`json:"total_tokens"`
}
}
// APIKeyUsageTrendPoint represents API key usage trend data point
// APIKeyUsageTrendPoint represents API key usage trend data point
...
...
backend/internal/repository/usage_log_repo.go
View file @
94e067a2
...
@@ -2161,7 +2161,9 @@ func (r *usageLogRepository) GetUserSpendingRanking(ctx context.Context, startTi
...
@@ -2161,7 +2161,9 @@ func (r *usageLogRepository) GetUserSpendingRanking(ctx context.Context, startTi
actual_cost,
actual_cost,
requests,
requests,
tokens,
tokens,
COALESCE(SUM(actual_cost) OVER (), 0) as total_actual_cost
COALESCE(SUM(actual_cost) OVER (), 0) as total_actual_cost,
COALESCE(SUM(requests) OVER (), 0) as total_requests,
COALESCE(SUM(tokens) OVER (), 0) as total_tokens
FROM user_spend
FROM user_spend
ORDER BY actual_cost DESC, tokens DESC, user_id ASC
ORDER BY actual_cost DESC, tokens DESC, user_id ASC
LIMIT $3
LIMIT $3
...
@@ -2172,7 +2174,9 @@ func (r *usageLogRepository) GetUserSpendingRanking(ctx context.Context, startTi
...
@@ -2172,7 +2174,9 @@ func (r *usageLogRepository) GetUserSpendingRanking(ctx context.Context, startTi
actual_cost,
actual_cost,
requests,
requests,
tokens,
tokens,
total_actual_cost
total_actual_cost,
total_requests,
total_tokens
FROM ranked
FROM ranked
ORDER BY actual_cost DESC, tokens DESC, user_id ASC
ORDER BY actual_cost DESC, tokens DESC, user_id ASC
`
`
...
@@ -2190,9 +2194,11 @@ func (r *usageLogRepository) GetUserSpendingRanking(ctx context.Context, startTi
...
@@ -2190,9 +2194,11 @@ func (r *usageLogRepository) GetUserSpendingRanking(ctx context.Context, startTi
ranking
:=
make
([]
UserSpendingRankingItem
,
0
)
ranking
:=
make
([]
UserSpendingRankingItem
,
0
)
totalActualCost
:=
0.0
totalActualCost
:=
0.0
totalRequests
:=
int64
(
0
)
totalTokens
:=
int64
(
0
)
for
rows
.
Next
()
{
for
rows
.
Next
()
{
var
row
UserSpendingRankingItem
var
row
UserSpendingRankingItem
if
err
=
rows
.
Scan
(
&
row
.
UserID
,
&
row
.
Email
,
&
row
.
ActualCost
,
&
row
.
Requests
,
&
row
.
Tokens
,
&
totalActualCost
);
err
!=
nil
{
if
err
=
rows
.
Scan
(
&
row
.
UserID
,
&
row
.
Email
,
&
row
.
ActualCost
,
&
row
.
Requests
,
&
row
.
Tokens
,
&
totalActualCost
,
&
totalRequests
,
&
totalTokens
);
err
!=
nil
{
return
nil
,
err
return
nil
,
err
}
}
ranking
=
append
(
ranking
,
row
)
ranking
=
append
(
ranking
,
row
)
...
@@ -2204,6 +2210,8 @@ func (r *usageLogRepository) GetUserSpendingRanking(ctx context.Context, startTi
...
@@ -2204,6 +2210,8 @@ func (r *usageLogRepository) GetUserSpendingRanking(ctx context.Context, startTi
return
&
UserSpendingRankingResponse
{
return
&
UserSpendingRankingResponse
{
Ranking
:
ranking
,
Ranking
:
ranking
,
TotalActualCost
:
totalActualCost
,
TotalActualCost
:
totalActualCost
,
TotalRequests
:
totalRequests
,
TotalTokens
:
totalTokens
,
},
nil
},
nil
}
}
...
...
backend/internal/repository/usage_log_repo_request_type_test.go
View file @
94e067a2
...
@@ -259,10 +259,10 @@ func TestUsageLogRepositoryGetUserSpendingRanking(t *testing.T) {
...
@@ -259,10 +259,10 @@ func TestUsageLogRepositoryGetUserSpendingRanking(t *testing.T) {
start
:=
time
.
Date
(
2025
,
1
,
1
,
0
,
0
,
0
,
0
,
time
.
UTC
)
start
:=
time
.
Date
(
2025
,
1
,
1
,
0
,
0
,
0
,
0
,
time
.
UTC
)
end
:=
start
.
Add
(
24
*
time
.
Hour
)
end
:=
start
.
Add
(
24
*
time
.
Hour
)
rows
:=
sqlmock
.
NewRows
([]
string
{
"user_id"
,
"email"
,
"actual_cost"
,
"requests"
,
"tokens"
,
"total_actual_cost"
})
.
rows
:=
sqlmock
.
NewRows
([]
string
{
"user_id"
,
"email"
,
"actual_cost"
,
"requests"
,
"tokens"
,
"total_actual_cost"
,
"total_requests"
,
"total_tokens"
})
.
AddRow
(
int64
(
2
),
"beta@example.com"
,
12.5
,
int64
(
9
),
int64
(
900
),
40.0
)
.
AddRow
(
int64
(
2
),
"beta@example.com"
,
12.5
,
int64
(
9
),
int64
(
900
),
40.0
,
int64
(
30
),
int64
(
2600
)
)
.
AddRow
(
int64
(
1
),
"alpha@example.com"
,
12.5
,
int64
(
8
),
int64
(
800
),
40.0
)
.
AddRow
(
int64
(
1
),
"alpha@example.com"
,
12.5
,
int64
(
8
),
int64
(
800
),
40.0
,
int64
(
30
),
int64
(
2600
)
)
.
AddRow
(
int64
(
3
),
"gamma@example.com"
,
4.25
,
int64
(
5
),
int64
(
300
),
40.0
)
AddRow
(
int64
(
3
),
"gamma@example.com"
,
4.25
,
int64
(
5
),
int64
(
300
),
40.0
,
int64
(
30
),
int64
(
2600
)
)
mock
.
ExpectQuery
(
"WITH user_spend AS
\\
("
)
.
mock
.
ExpectQuery
(
"WITH user_spend AS
\\
("
)
.
WithArgs
(
start
,
end
,
12
)
.
WithArgs
(
start
,
end
,
12
)
.
...
@@ -277,6 +277,8 @@ func TestUsageLogRepositoryGetUserSpendingRanking(t *testing.T) {
...
@@ -277,6 +277,8 @@ func TestUsageLogRepositoryGetUserSpendingRanking(t *testing.T) {
{
UserID
:
3
,
Email
:
"gamma@example.com"
,
ActualCost
:
4.25
,
Requests
:
5
,
Tokens
:
300
},
{
UserID
:
3
,
Email
:
"gamma@example.com"
,
ActualCost
:
4.25
,
Requests
:
5
,
Tokens
:
300
},
},
},
TotalActualCost
:
40.0
,
TotalActualCost
:
40.0
,
TotalRequests
:
30
,
TotalTokens
:
2600
,
},
got
)
},
got
)
require
.
NoError
(
t
,
mock
.
ExpectationsWereMet
())
require
.
NoError
(
t
,
mock
.
ExpectationsWereMet
())
}
}
...
...
frontend/src/components/charts/ModelDistributionChart.vue
View file @
94e067a2
...
@@ -127,7 +127,7 @@
...
@@ -127,7 +127,7 @@
>
>
{{
t
(
'
admin.dashboard.failedToLoad
'
)
}}
{{
t
(
'
admin.dashboard.failedToLoad
'
)
}}
</div>
</div>
<div
v-else-if=
"rankingItems.length > 0 && rankingChartData"
class=
"flex items-center gap-6"
>
<div
v-else-if=
"ranking
Display
Items.length > 0 && rankingChartData"
class=
"flex items-center gap-6"
>
<div
class=
"h-48 w-48"
>
<div
class=
"h-48 w-48"
>
<Doughnut
:data=
"rankingChartData"
:options=
"rankingDoughnutOptions"
/>
<Doughnut
:data=
"rankingChartData"
:options=
"rankingDoughnutOptions"
/>
</div>
</div>
...
@@ -143,21 +143,24 @@
...
@@ -143,21 +143,24 @@
</thead>
</thead>
<tbody>
<tbody>
<tr
<tr
v-for=
"(item, index) in rankingItems"
v-for=
"(item, index) in rankingDisplayItems"
:key=
"`$
{item.user_id}-${index}`"
:key=
"item.isOther ? 'others' : `$
{item.user_id}-${index}`"
class="cursor-pointer border-t border-gray-100 transition-colors hover:bg-gray-50 dark:border-gray-700 dark:hover:bg-dark-700/40"
class="border-t border-gray-100 transition-colors dark:border-gray-700"
@click="emit('ranking-click', item)"
:class="item.isOther
? 'bg-gray-50/70 dark:bg-dark-700/20'
: 'cursor-pointer hover:bg-gray-50 dark:hover:bg-dark-700/40'"
@click="item.isOther ? undefined : emit('ranking-click', item)"
>
>
<td
class=
"py-1.5"
>
<td
class=
"py-1.5"
>
<div
class=
"flex min-w-0 items-center gap-2"
>
<div
class=
"flex min-w-0 items-center gap-2"
>
<span
class=
"shrink-0 text-[11px] font-semibold text-gray-500 dark:text-gray-400"
>
<span
class=
"shrink-0 text-[11px] font-semibold text-gray-500 dark:text-gray-400"
>
#
{{
index
+
1
}}
{{
item
.
isOther
?
'
Σ
'
:
`#${
index + 1
}
`
}}
<
/span
>
<
/span
>
<
span
<
span
class
=
"
block max-w-[140px] truncate font-medium text-gray-900 dark:text-white
"
class
=
"
block max-w-[140px] truncate font-medium text-gray-900 dark:text-white
"
:title=
"getRanking
User
Label(item)"
:
title
=
"
getRanking
Row
Label(item)
"
>
>
{{
getRanking
User
Label
(
item
)
}}
{{
getRanking
Row
Label
(
item
)
}}
<
/span
>
<
/span
>
<
/div
>
<
/div
>
<
/td
>
<
/td
>
...
@@ -197,11 +200,14 @@ ChartJS.register(ArcElement, Tooltip, Legend)
...
@@ -197,11 +200,14 @@ ChartJS.register(ArcElement, Tooltip, Legend)
const
{
t
}
=
useI18n
()
const
{
t
}
=
useI18n
()
type
DistributionMetric
=
'
tokens
'
|
'
actual_cost
'
type
DistributionMetric
=
'
tokens
'
|
'
actual_cost
'
type
RankingDisplayItem
=
UserSpendingRankingItem
&
{
isOther
?:
boolean
}
const
props
=
withDefaults
(
defineProps
<
{
const
props
=
withDefaults
(
defineProps
<
{
modelStats
:
ModelStat
[]
modelStats
:
ModelStat
[]
enableRankingView
?:
boolean
enableRankingView
?:
boolean
rankingItems
?:
UserSpendingRankingItem
[]
rankingItems
?:
UserSpendingRankingItem
[]
rankingTotalActualCost
?:
number
rankingTotalActualCost
?:
number
rankingTotalRequests
?:
number
rankingTotalTokens
?:
number
loading
?:
boolean
loading
?:
boolean
metric
?:
DistributionMetric
metric
?:
DistributionMetric
showMetricToggle
?:
boolean
showMetricToggle
?:
boolean
...
@@ -211,6 +217,8 @@ const props = withDefaults(defineProps<{
...
@@ -211,6 +217,8 @@ const props = withDefaults(defineProps<{
enableRankingView
:
false
,
enableRankingView
:
false
,
rankingItems
:
()
=>
[],
rankingItems
:
()
=>
[],
rankingTotalActualCost
:
0
,
rankingTotalActualCost
:
0
,
rankingTotalRequests
:
0
,
rankingTotalTokens
:
0
,
loading
:
false
,
loading
:
false
,
metric
:
'
tokens
'
,
metric
:
'
tokens
'
,
showMetricToggle
:
false
,
showMetricToggle
:
false
,
...
@@ -266,14 +274,14 @@ const chartData = computed(() => {
...
@@ -266,14 +274,14 @@ const chartData = computed(() => {
const
rankingChartData
=
computed
(()
=>
{
const
rankingChartData
=
computed
(()
=>
{
if
(
!
props
.
rankingItems
?.
length
)
return
null
if
(
!
props
.
rankingItems
?.
length
)
return
null
const
rankedTotal
=
props
.
rankingItems
.
reduce
((
sum
,
item
)
=>
sum
+
item
.
actual_cost
,
0
)
const
otherActualCost
=
Math
.
max
((
props
.
rankingTotalActualCost
||
0
)
-
rankedTotal
,
0
)
const
labels
=
props
.
rankingItems
.
map
((
item
,
index
)
=>
`#${index + 1
}
${getRankingUserLabel(item)
}
`
)
const
labels
=
props
.
rankingItems
.
map
((
item
,
index
)
=>
`#${index + 1
}
${getRankingUserLabel(item)
}
`
)
const
data
=
props
.
rankingItems
.
map
((
item
)
=>
item
.
actual_cost
)
const
data
=
props
.
rankingItems
.
map
((
item
)
=>
item
.
actual_cost
)
const
backgroundColor
=
chartColors
.
slice
(
0
,
props
.
rankingItems
.
length
)
if
(
other
ActualCost
>
0.000001
)
{
if
(
other
RankingItem
.
value
)
{
labels
.
push
(
t
(
'
admin.dashboard.spendingRankingOther
'
))
labels
.
push
(
t
(
'
admin.dashboard.spendingRankingOther
'
))
data
.
push
(
otherActualCost
)
data
.
push
(
otherRankingItem
.
value
.
actual_cost
)
backgroundColor
.
push
(
'
#94a3b8
'
)
}
}
return
{
return
{
...
@@ -281,13 +289,43 @@ const rankingChartData = computed(() => {
...
@@ -281,13 +289,43 @@ const rankingChartData = computed(() => {
datasets
:
[
datasets
:
[
{
{
data
,
data
,
backgroundColor
:
chartColors
.
slice
(
0
,
data
.
length
)
,
backgroundColor
,
borderWidth
:
0
borderWidth
:
0
}
}
]
]
}
}
}
)
}
)
const
otherRankingItem
=
computed
<
RankingDisplayItem
|
null
>
(()
=>
{
if
(
!
props
.
rankingItems
?.
length
)
return
null
const
rankedActualCost
=
props
.
rankingItems
.
reduce
((
sum
,
item
)
=>
sum
+
item
.
actual_cost
,
0
)
const
rankedRequests
=
props
.
rankingItems
.
reduce
((
sum
,
item
)
=>
sum
+
item
.
requests
,
0
)
const
rankedTokens
=
props
.
rankingItems
.
reduce
((
sum
,
item
)
=>
sum
+
item
.
tokens
,
0
)
const
otherActualCost
=
Math
.
max
((
props
.
rankingTotalActualCost
||
0
)
-
rankedActualCost
,
0
)
const
otherRequests
=
Math
.
max
((
props
.
rankingTotalRequests
||
0
)
-
rankedRequests
,
0
)
const
otherTokens
=
Math
.
max
((
props
.
rankingTotalTokens
||
0
)
-
rankedTokens
,
0
)
if
(
otherActualCost
<=
0.000001
&&
otherRequests
<=
0
&&
otherTokens
<=
0
)
return
null
return
{
user_id
:
0
,
email
:
''
,
actual_cost
:
otherActualCost
,
requests
:
otherRequests
,
tokens
:
otherTokens
,
isOther
:
true
}
}
)
const
rankingDisplayItems
=
computed
<
RankingDisplayItem
[]
>
(()
=>
{
if
(
!
props
.
rankingItems
?.
length
)
return
[]
return
otherRankingItem
.
value
?
[...
props
.
rankingItems
,
otherRankingItem
.
value
]
:
[...
props
.
rankingItems
]
}
)
const
doughnutOptions
=
computed
(()
=>
({
const
doughnutOptions
=
computed
(()
=>
({
responsive
:
true
,
responsive
:
true
,
maintainAspectRatio
:
false
,
maintainAspectRatio
:
false
,
...
@@ -351,6 +389,11 @@ const getRankingUserLabel = (item: UserSpendingRankingItem): string => {
...
@@ -351,6 +389,11 @@ const getRankingUserLabel = (item: UserSpendingRankingItem): string => {
return
t
(
'
admin.redeem.userPrefix
'
,
{
id
:
item
.
user_id
}
)
return
t
(
'
admin.redeem.userPrefix
'
,
{
id
:
item
.
user_id
}
)
}
}
const
getRankingRowLabel
=
(
item
:
RankingDisplayItem
):
string
=>
{
if
(
item
.
isOther
)
return
t
(
'
admin.dashboard.spendingRankingOther
'
)
return
getRankingUserLabel
(
item
)
}
const
formatCost
=
(
value
:
number
):
string
=>
{
const
formatCost
=
(
value
:
number
):
string
=>
{
if
(
value
>=
1000
)
{
if
(
value
>=
1000
)
{
return
(
value
/
1000
).
toFixed
(
2
)
+
'
K
'
return
(
value
/
1000
).
toFixed
(
2
)
+
'
K
'
...
...
frontend/src/components/charts/__tests__/ModelDistributionChart.spec.ts
View file @
94e067a2
...
@@ -5,6 +5,14 @@ import ModelDistributionChart from '../ModelDistributionChart.vue'
...
@@ -5,6 +5,14 @@ import ModelDistributionChart from '../ModelDistributionChart.vue'
const
messages
:
Record
<
string
,
string
>
=
{
const
messages
:
Record
<
string
,
string
>
=
{
'
admin.dashboard.modelDistribution
'
:
'
Model Distribution
'
,
'
admin.dashboard.modelDistribution
'
:
'
Model Distribution
'
,
'
admin.dashboard.spendingRankingTitle
'
:
'
User Spending Ranking
'
,
'
admin.dashboard.viewModelDistribution
'
:
'
Model Distribution
'
,
'
admin.dashboard.viewSpendingRanking
'
:
'
User Spending Ranking
'
,
'
admin.dashboard.spendingRankingUser
'
:
'
User
'
,
'
admin.dashboard.spendingRankingRequests
'
:
'
Requests
'
,
'
admin.dashboard.spendingRankingTokens
'
:
'
Tokens
'
,
'
admin.dashboard.spendingRankingSpend
'
:
'
Spend
'
,
'
admin.dashboard.spendingRankingOther
'
:
'
Others
'
,
'
admin.dashboard.model
'
:
'
Model
'
,
'
admin.dashboard.model
'
:
'
Model
'
,
'
admin.dashboard.requests
'
:
'
Requests
'
,
'
admin.dashboard.requests
'
:
'
Requests
'
,
'
admin.dashboard.tokens
'
:
'
Tokens
'
,
'
admin.dashboard.tokens
'
:
'
Tokens
'
,
...
@@ -13,6 +21,7 @@ const messages: Record<string, string> = {
...
@@ -13,6 +21,7 @@ const messages: Record<string, string> = {
'
admin.dashboard.metricTokens
'
:
'
By Tokens
'
,
'
admin.dashboard.metricTokens
'
:
'
By Tokens
'
,
'
admin.dashboard.metricActualCost
'
:
'
By Actual Cost
'
,
'
admin.dashboard.metricActualCost
'
:
'
By Actual Cost
'
,
'
admin.dashboard.noDataAvailable
'
:
'
No data available
'
,
'
admin.dashboard.noDataAvailable
'
:
'
No data available
'
,
'
admin.redeem.userPrefix
'
:
'
User #{id}
'
,
}
}
vi
.
mock
(
'
vue-i18n
'
,
async
()
=>
{
vi
.
mock
(
'
vue-i18n
'
,
async
()
=>
{
...
@@ -116,4 +125,47 @@ describe('ModelDistributionChart', () => {
...
@@ -116,4 +125,47 @@ describe('ModelDistributionChart', () => {
})
})
expect
(
label
).
toBe
(
'
model-b: $1.40 (87.5%)
'
)
expect
(
label
).
toBe
(
'
model-b: $1.40 (87.5%)
'
)
})
})
it
(
'
renders Others in the spending ranking table and uses a dedicated chart color
'
,
async
()
=>
{
const
wrapper
=
mount
(
ModelDistributionChart
,
{
props
:
{
modelStats
:
[],
enableRankingView
:
true
,
rankingItems
:
[
{
user_id
:
1
,
email
:
'
alpha@example.com
'
,
actual_cost
:
12
,
requests
:
10
,
tokens
:
1000
},
{
user_id
:
2
,
email
:
'
beta@example.com
'
,
actual_cost
:
8
,
requests
:
6
,
tokens
:
600
},
],
rankingTotalActualCost
:
30
,
rankingTotalRequests
:
20
,
rankingTotalTokens
:
2000
,
},
global
:
{
stubs
:
{
LoadingSpinner
:
true
,
},
},
})
const
rankingButton
=
wrapper
.
findAll
(
'
button
'
).
find
((
button
)
=>
button
.
text
()
===
'
User Spending Ranking
'
)
expect
(
rankingButton
).
toBeTruthy
()
await
rankingButton
!
.
trigger
(
'
click
'
)
const
chartData
=
JSON
.
parse
(
wrapper
.
find
(
'
.chart-data
'
).
text
())
expect
(
chartData
.
labels
).
toEqual
([
'
#1 alpha@example.com
'
,
'
#2 beta@example.com
'
,
'
Others
'
,
])
expect
(
chartData
.
datasets
[
0
].
data
).
toEqual
([
12
,
8
,
10
])
expect
(
chartData
.
datasets
[
0
].
backgroundColor
[
0
]).
toBe
(
'
#3b82f6
'
)
expect
(
chartData
.
datasets
[
0
].
backgroundColor
[
2
]).
toBe
(
'
#94a3b8
'
)
expect
(
chartData
.
datasets
[
0
].
backgroundColor
[
2
]).
not
.
toBe
(
chartData
.
datasets
[
0
].
backgroundColor
[
0
])
const
rows
=
wrapper
.
findAll
(
'
tbody tr
'
)
expect
(
rows
).
toHaveLength
(
3
)
expect
(
rows
[
2
].
text
()).
toContain
(
'
Others
'
)
expect
(
rows
[
2
].
text
()).
toContain
(
'
4
'
)
expect
(
rows
[
2
].
text
()).
toContain
(
'
400
'
)
expect
(
rows
[
2
].
text
()).
toContain
(
'
$10.00
'
)
})
})
})
frontend/src/types/index.ts
View file @
94e067a2
...
@@ -1209,6 +1209,8 @@ export interface UserSpendingRankingItem {
...
@@ -1209,6 +1209,8 @@ export interface UserSpendingRankingItem {
export
interface
UserSpendingRankingResponse
{
export
interface
UserSpendingRankingResponse
{
ranking
:
UserSpendingRankingItem
[]
ranking
:
UserSpendingRankingItem
[]
total_actual_cost
:
number
total_actual_cost
:
number
total_requests
:
number
total_tokens
:
number
start_date
:
string
start_date
:
string
end_date
:
string
end_date
:
string
}
}
...
...
frontend/src/views/admin/DashboardView.vue
View file @
94e067a2
...
@@ -241,6 +241,8 @@
...
@@ -241,6 +241,8 @@
:enable-ranking-view=
"true"
:enable-ranking-view=
"true"
:ranking-items=
"rankingItems"
:ranking-items=
"rankingItems"
:ranking-total-actual-cost=
"rankingTotalActualCost"
:ranking-total-actual-cost=
"rankingTotalActualCost"
:ranking-total-requests=
"rankingTotalRequests"
:ranking-total-tokens=
"rankingTotalTokens"
:loading=
"chartsLoading"
:loading=
"chartsLoading"
:ranking-loading=
"rankingLoading"
:ranking-loading=
"rankingLoading"
:ranking-error=
"rankingError"
:ranking-error=
"rankingError"
...
@@ -334,6 +336,8 @@ const modelStats = ref<ModelStat[]>([])
...
@@ -334,6 +336,8 @@ const modelStats = ref<ModelStat[]>([])
const
userTrend
=
ref
<
UserUsageTrendPoint
[]
>
([])
const
userTrend
=
ref
<
UserUsageTrendPoint
[]
>
([])
const
rankingItems
=
ref
<
UserSpendingRankingItem
[]
>
([])
const
rankingItems
=
ref
<
UserSpendingRankingItem
[]
>
([])
const
rankingTotalActualCost
=
ref
(
0
)
const
rankingTotalActualCost
=
ref
(
0
)
const
rankingTotalRequests
=
ref
(
0
)
const
rankingTotalTokens
=
ref
(
0
)
let
chartLoadSeq
=
0
let
chartLoadSeq
=
0
let
usersTrendLoadSeq
=
0
let
usersTrendLoadSeq
=
0
let
rankingLoadSeq
=
0
let
rankingLoadSeq
=
0
...
@@ -347,7 +351,7 @@ const formatLocalDate = (date: Date): string => {
...
@@ -347,7 +351,7 @@ const formatLocalDate = (date: Date): string => {
const
getTodayLocalDate
=
()
=>
formatLocalDate
(
new
Date
())
const
getTodayLocalDate
=
()
=>
formatLocalDate
(
new
Date
())
// Date range
// Date range
const
granularity
=
ref
<
'
day
'
|
'
hour
'
>
(
'
day
'
)
const
granularity
=
ref
<
'
day
'
|
'
hour
'
>
(
'
hour
'
)
const
startDate
=
ref
(
getTodayLocalDate
())
const
startDate
=
ref
(
getTodayLocalDate
())
const
endDate
=
ref
(
getTodayLocalDate
())
const
endDate
=
ref
(
getTodayLocalDate
())
...
@@ -630,11 +634,15 @@ const loadUserSpendingRanking = async () => {
...
@@ -630,11 +634,15 @@ const loadUserSpendingRanking = async () => {
if
(
currentSeq
!==
rankingLoadSeq
)
return
if
(
currentSeq
!==
rankingLoadSeq
)
return
rankingItems
.
value
=
response
.
ranking
||
[]
rankingItems
.
value
=
response
.
ranking
||
[]
rankingTotalActualCost
.
value
=
response
.
total_actual_cost
||
0
rankingTotalActualCost
.
value
=
response
.
total_actual_cost
||
0
rankingTotalRequests
.
value
=
response
.
total_requests
||
0
rankingTotalTokens
.
value
=
response
.
total_tokens
||
0
}
catch
(
error
)
{
}
catch
(
error
)
{
if
(
currentSeq
!==
rankingLoadSeq
)
return
if
(
currentSeq
!==
rankingLoadSeq
)
return
console
.
error
(
'
Error loading user spending ranking:
'
,
error
)
console
.
error
(
'
Error loading user spending ranking:
'
,
error
)
rankingItems
.
value
=
[]
rankingItems
.
value
=
[]
rankingTotalActualCost
.
value
=
0
rankingTotalActualCost
.
value
=
0
rankingTotalRequests
.
value
=
0
rankingTotalTokens
.
value
=
0
rankingError
.
value
=
true
rankingError
.
value
=
true
}
finally
{
}
finally
{
if
(
currentSeq
===
rankingLoadSeq
)
{
if
(
currentSeq
===
rankingLoadSeq
)
{
...
...
frontend/src/views/admin/UsageView.vue
View file @
94e067a2
...
@@ -122,7 +122,7 @@ type DistributionMetric = 'tokens' | 'actual_cost'
...
@@ -122,7 +122,7 @@ type DistributionMetric = 'tokens' | 'actual_cost'
type
EndpointSource
=
'
inbound
'
|
'
upstream
'
|
'
path
'
type
EndpointSource
=
'
inbound
'
|
'
upstream
'
|
'
path
'
const
route
=
useRoute
()
const
route
=
useRoute
()
const
usageStats
=
ref
<
AdminUsageStatsResponse
|
null
>
(
null
);
const
usageLogs
=
ref
<
AdminUsageLog
[]
>
([]);
const
loading
=
ref
(
false
);
const
exporting
=
ref
(
false
)
const
usageStats
=
ref
<
AdminUsageStatsResponse
|
null
>
(
null
);
const
usageLogs
=
ref
<
AdminUsageLog
[]
>
([]);
const
loading
=
ref
(
false
);
const
exporting
=
ref
(
false
)
const
trendData
=
ref
<
TrendDataPoint
[]
>
([]);
const
modelStats
=
ref
<
ModelStat
[]
>
([]);
const
groupStats
=
ref
<
GroupStat
[]
>
([]);
const
chartsLoading
=
ref
(
false
);
const
granularity
=
ref
<
'
day
'
|
'
hour
'
>
(
'
day
'
)
const
trendData
=
ref
<
TrendDataPoint
[]
>
([]);
const
modelStats
=
ref
<
ModelStat
[]
>
([]);
const
groupStats
=
ref
<
GroupStat
[]
>
([]);
const
chartsLoading
=
ref
(
false
);
const
granularity
=
ref
<
'
day
'
|
'
hour
'
>
(
'
hour
'
)
const
modelDistributionMetric
=
ref
<
DistributionMetric
>
(
'
tokens
'
)
const
modelDistributionMetric
=
ref
<
DistributionMetric
>
(
'
tokens
'
)
const
groupDistributionMetric
=
ref
<
DistributionMetric
>
(
'
tokens
'
)
const
groupDistributionMetric
=
ref
<
DistributionMetric
>
(
'
tokens
'
)
const
endpointDistributionMetric
=
ref
<
DistributionMetric
>
(
'
tokens
'
)
const
endpointDistributionMetric
=
ref
<
DistributionMetric
>
(
'
tokens
'
)
...
@@ -159,6 +159,7 @@ const formatLD = (d: Date) => {
...
@@ -159,6 +159,7 @@ const formatLD = (d: Date) => {
return
`
${
year
}
-
${
month
}
-
${
day
}
`
return
`
${
year
}
-
${
month
}
-
${
day
}
`
}
}
const
getTodayLocalDate
=
()
=>
formatLD
(
new
Date
())
const
getTodayLocalDate
=
()
=>
formatLD
(
new
Date
())
const
getGranularityForRange
=
(
start
:
string
,
end
:
string
):
'
day
'
|
'
hour
'
=>
start
===
end
?
'
hour
'
:
'
day
'
const
startDate
=
ref
(
getTodayLocalDate
());
const
endDate
=
ref
(
getTodayLocalDate
())
const
startDate
=
ref
(
getTodayLocalDate
());
const
endDate
=
ref
(
getTodayLocalDate
())
const
filters
=
ref
<
AdminUsageQueryParams
>
({
user_id
:
undefined
,
model
:
undefined
,
group_id
:
undefined
,
request_type
:
undefined
,
billing_type
:
null
,
start_date
:
startDate
.
value
,
end_date
:
endDate
.
value
})
const
filters
=
ref
<
AdminUsageQueryParams
>
({
user_id
:
undefined
,
model
:
undefined
,
group_id
:
undefined
,
request_type
:
undefined
,
billing_type
:
null
,
start_date
:
startDate
.
value
,
end_date
:
endDate
.
value
})
const
pagination
=
reactive
({
page
:
1
,
page_size
:
20
,
total
:
0
})
const
pagination
=
reactive
({
page
:
1
,
page_size
:
20
,
total
:
0
})
...
@@ -193,6 +194,7 @@ const applyRouteQueryFilters = () => {
...
@@ -193,6 +194,7 @@ const applyRouteQueryFilters = () => {
start_date
:
startDate
.
value
,
start_date
:
startDate
.
value
,
end_date
:
endDate
.
value
end_date
:
endDate
.
value
}
}
granularity
.
value
=
getGranularityForRange
(
startDate
.
value
,
endDate
.
value
)
}
}
const
loadLogs
=
async
()
=>
{
const
loadLogs
=
async
()
=>
{
...
@@ -258,7 +260,7 @@ const loadChartData = async () => {
...
@@ -258,7 +260,7 @@ const loadChartData = async () => {
}
}
const
applyFilters
=
()
=>
{
pagination
.
page
=
1
;
loadLogs
();
loadStats
();
loadChartData
()
}
const
applyFilters
=
()
=>
{
pagination
.
page
=
1
;
loadLogs
();
loadStats
();
loadChartData
()
}
const
refreshData
=
()
=>
{
loadLogs
();
loadStats
();
loadChartData
()
}
const
refreshData
=
()
=>
{
loadLogs
();
loadStats
();
loadChartData
()
}
const
resetFilters
=
()
=>
{
startDate
.
value
=
getTodayLocalDate
();
endDate
.
value
=
getTodayLocalDate
();
filters
.
value
=
{
start_date
:
startDate
.
value
,
end_date
:
endDate
.
value
,
request_type
:
undefined
,
billing_type
:
null
};
granularity
.
value
=
'
day
'
;
applyFilters
()
}
const
resetFilters
=
()
=>
{
startDate
.
value
=
getTodayLocalDate
();
endDate
.
value
=
getTodayLocalDate
();
filters
.
value
=
{
start_date
:
startDate
.
value
,
end_date
:
endDate
.
value
,
request_type
:
undefined
,
billing_type
:
null
};
granularity
.
value
=
getGranularityForRange
(
startDate
.
value
,
endDate
.
value
)
;
applyFilters
()
}
const
handlePageChange
=
(
p
:
number
)
=>
{
pagination
.
page
=
p
;
loadLogs
()
}
const
handlePageChange
=
(
p
:
number
)
=>
{
pagination
.
page
=
p
;
loadLogs
()
}
const
handlePageSizeChange
=
(
s
:
number
)
=>
{
pagination
.
page_size
=
s
;
pagination
.
page
=
1
;
loadLogs
()
}
const
handlePageSizeChange
=
(
s
:
number
)
=>
{
pagination
.
page_size
=
s
;
pagination
.
page
=
1
;
loadLogs
()
}
const
cancelExport
=
()
=>
exportAbortController
?.
abort
()
const
cancelExport
=
()
=>
exportAbortController
?.
abort
()
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment