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
1cf51b14
Unverified
Commit
1cf51b14
authored
Feb 15, 2026
by
程序猿MT
Committed by
GitHub
Feb 15, 2026
Browse files
Merge branch 'Wei-Shaw:main' into main
parents
174d7c77
a817cafe
Changes
35
Show whitespace changes
Inline
Side-by-side
backend/internal/service/ratelimit_service_anthropic_test.go
0 → 100644
View file @
1cf51b14
package
service
import
(
"net/http"
"testing"
"time"
)
func
TestCalculateAnthropic429ResetTime_Only5hExceeded
(
t
*
testing
.
T
)
{
headers
:=
http
.
Header
{}
headers
.
Set
(
"anthropic-ratelimit-unified-5h-utilization"
,
"1.02"
)
headers
.
Set
(
"anthropic-ratelimit-unified-5h-reset"
,
"1770998400"
)
headers
.
Set
(
"anthropic-ratelimit-unified-7d-utilization"
,
"0.32"
)
headers
.
Set
(
"anthropic-ratelimit-unified-7d-reset"
,
"1771549200"
)
result
:=
calculateAnthropic429ResetTime
(
headers
)
assertAnthropicResult
(
t
,
result
,
1770998400
)
if
result
.
fiveHourReset
==
nil
||
!
result
.
fiveHourReset
.
Equal
(
time
.
Unix
(
1770998400
,
0
))
{
t
.
Errorf
(
"expected fiveHourReset=1770998400, got %v"
,
result
.
fiveHourReset
)
}
}
func
TestCalculateAnthropic429ResetTime_Only7dExceeded
(
t
*
testing
.
T
)
{
headers
:=
http
.
Header
{}
headers
.
Set
(
"anthropic-ratelimit-unified-5h-utilization"
,
"0.50"
)
headers
.
Set
(
"anthropic-ratelimit-unified-5h-reset"
,
"1770998400"
)
headers
.
Set
(
"anthropic-ratelimit-unified-7d-utilization"
,
"1.05"
)
headers
.
Set
(
"anthropic-ratelimit-unified-7d-reset"
,
"1771549200"
)
result
:=
calculateAnthropic429ResetTime
(
headers
)
assertAnthropicResult
(
t
,
result
,
1771549200
)
// fiveHourReset should still be populated for session window calculation
if
result
.
fiveHourReset
==
nil
||
!
result
.
fiveHourReset
.
Equal
(
time
.
Unix
(
1770998400
,
0
))
{
t
.
Errorf
(
"expected fiveHourReset=1770998400, got %v"
,
result
.
fiveHourReset
)
}
}
func
TestCalculateAnthropic429ResetTime_BothExceeded
(
t
*
testing
.
T
)
{
headers
:=
http
.
Header
{}
headers
.
Set
(
"anthropic-ratelimit-unified-5h-utilization"
,
"1.10"
)
headers
.
Set
(
"anthropic-ratelimit-unified-5h-reset"
,
"1770998400"
)
headers
.
Set
(
"anthropic-ratelimit-unified-7d-utilization"
,
"1.02"
)
headers
.
Set
(
"anthropic-ratelimit-unified-7d-reset"
,
"1771549200"
)
result
:=
calculateAnthropic429ResetTime
(
headers
)
assertAnthropicResult
(
t
,
result
,
1771549200
)
}
func
TestCalculateAnthropic429ResetTime_NoPerWindowHeaders
(
t
*
testing
.
T
)
{
headers
:=
http
.
Header
{}
headers
.
Set
(
"anthropic-ratelimit-unified-reset"
,
"1771549200"
)
result
:=
calculateAnthropic429ResetTime
(
headers
)
if
result
!=
nil
{
t
.
Errorf
(
"expected nil result when no per-window headers, got resetAt=%v"
,
result
.
resetAt
)
}
}
func
TestCalculateAnthropic429ResetTime_NoHeaders
(
t
*
testing
.
T
)
{
result
:=
calculateAnthropic429ResetTime
(
http
.
Header
{})
if
result
!=
nil
{
t
.
Errorf
(
"expected nil result for empty headers, got resetAt=%v"
,
result
.
resetAt
)
}
}
func
TestCalculateAnthropic429ResetTime_SurpassedThreshold
(
t
*
testing
.
T
)
{
headers
:=
http
.
Header
{}
headers
.
Set
(
"anthropic-ratelimit-unified-5h-surpassed-threshold"
,
"true"
)
headers
.
Set
(
"anthropic-ratelimit-unified-5h-reset"
,
"1770998400"
)
headers
.
Set
(
"anthropic-ratelimit-unified-7d-surpassed-threshold"
,
"false"
)
headers
.
Set
(
"anthropic-ratelimit-unified-7d-reset"
,
"1771549200"
)
result
:=
calculateAnthropic429ResetTime
(
headers
)
assertAnthropicResult
(
t
,
result
,
1770998400
)
}
func
TestCalculateAnthropic429ResetTime_UtilizationExactlyOne
(
t
*
testing
.
T
)
{
headers
:=
http
.
Header
{}
headers
.
Set
(
"anthropic-ratelimit-unified-5h-utilization"
,
"1.0"
)
headers
.
Set
(
"anthropic-ratelimit-unified-5h-reset"
,
"1770998400"
)
headers
.
Set
(
"anthropic-ratelimit-unified-7d-utilization"
,
"0.5"
)
headers
.
Set
(
"anthropic-ratelimit-unified-7d-reset"
,
"1771549200"
)
result
:=
calculateAnthropic429ResetTime
(
headers
)
assertAnthropicResult
(
t
,
result
,
1770998400
)
}
func
TestCalculateAnthropic429ResetTime_NeitherExceeded_UsesShorter
(
t
*
testing
.
T
)
{
headers
:=
http
.
Header
{}
headers
.
Set
(
"anthropic-ratelimit-unified-5h-utilization"
,
"0.95"
)
headers
.
Set
(
"anthropic-ratelimit-unified-5h-reset"
,
"1770998400"
)
// sooner
headers
.
Set
(
"anthropic-ratelimit-unified-7d-utilization"
,
"0.80"
)
headers
.
Set
(
"anthropic-ratelimit-unified-7d-reset"
,
"1771549200"
)
// later
result
:=
calculateAnthropic429ResetTime
(
headers
)
assertAnthropicResult
(
t
,
result
,
1770998400
)
}
func
TestCalculateAnthropic429ResetTime_Only5hResetHeader
(
t
*
testing
.
T
)
{
headers
:=
http
.
Header
{}
headers
.
Set
(
"anthropic-ratelimit-unified-5h-utilization"
,
"1.05"
)
headers
.
Set
(
"anthropic-ratelimit-unified-5h-reset"
,
"1770998400"
)
result
:=
calculateAnthropic429ResetTime
(
headers
)
assertAnthropicResult
(
t
,
result
,
1770998400
)
}
func
TestCalculateAnthropic429ResetTime_Only7dResetHeader
(
t
*
testing
.
T
)
{
headers
:=
http
.
Header
{}
headers
.
Set
(
"anthropic-ratelimit-unified-7d-utilization"
,
"1.03"
)
headers
.
Set
(
"anthropic-ratelimit-unified-7d-reset"
,
"1771549200"
)
result
:=
calculateAnthropic429ResetTime
(
headers
)
assertAnthropicResult
(
t
,
result
,
1771549200
)
if
result
.
fiveHourReset
!=
nil
{
t
.
Errorf
(
"expected fiveHourReset=nil when no 5h headers, got %v"
,
result
.
fiveHourReset
)
}
}
func
TestIsAnthropicWindowExceeded
(
t
*
testing
.
T
)
{
tests
:=
[]
struct
{
name
string
headers
http
.
Header
window
string
expected
bool
}{
{
name
:
"utilization above 1.0"
,
headers
:
makeHeader
(
"anthropic-ratelimit-unified-5h-utilization"
,
"1.02"
),
window
:
"5h"
,
expected
:
true
,
},
{
name
:
"utilization exactly 1.0"
,
headers
:
makeHeader
(
"anthropic-ratelimit-unified-5h-utilization"
,
"1.0"
),
window
:
"5h"
,
expected
:
true
,
},
{
name
:
"utilization below 1.0"
,
headers
:
makeHeader
(
"anthropic-ratelimit-unified-5h-utilization"
,
"0.99"
),
window
:
"5h"
,
expected
:
false
,
},
{
name
:
"surpassed-threshold true"
,
headers
:
makeHeader
(
"anthropic-ratelimit-unified-7d-surpassed-threshold"
,
"true"
),
window
:
"7d"
,
expected
:
true
,
},
{
name
:
"surpassed-threshold True (case insensitive)"
,
headers
:
makeHeader
(
"anthropic-ratelimit-unified-7d-surpassed-threshold"
,
"True"
),
window
:
"7d"
,
expected
:
true
,
},
{
name
:
"surpassed-threshold false"
,
headers
:
makeHeader
(
"anthropic-ratelimit-unified-7d-surpassed-threshold"
,
"false"
),
window
:
"7d"
,
expected
:
false
,
},
{
name
:
"no headers"
,
headers
:
http
.
Header
{},
window
:
"5h"
,
expected
:
false
,
},
}
for
_
,
tc
:=
range
tests
{
t
.
Run
(
tc
.
name
,
func
(
t
*
testing
.
T
)
{
got
:=
isAnthropicWindowExceeded
(
tc
.
headers
,
tc
.
window
)
if
got
!=
tc
.
expected
{
t
.
Errorf
(
"expected %v, got %v"
,
tc
.
expected
,
got
)
}
})
}
}
// assertAnthropicResult is a test helper that verifies the result is non-nil and
// has the expected resetAt unix timestamp.
func
assertAnthropicResult
(
t
*
testing
.
T
,
result
*
anthropic429Result
,
wantUnix
int64
)
{
t
.
Helper
()
if
result
==
nil
{
t
.
Fatal
(
"expected non-nil result"
)
return
// unreachable, but satisfies staticcheck SA5011
}
want
:=
time
.
Unix
(
wantUnix
,
0
)
if
!
result
.
resetAt
.
Equal
(
want
)
{
t
.
Errorf
(
"expected resetAt=%v, got %v"
,
want
,
result
.
resetAt
)
}
}
func
makeHeader
(
key
,
value
string
)
http
.
Header
{
h
:=
http
.
Header
{}
h
.
Set
(
key
,
value
)
return
h
}
backend/internal/service/usage_log.go
View file @
1cf51b14
...
@@ -26,8 +26,8 @@ type UsageLog struct {
...
@@ -26,8 +26,8 @@ type UsageLog struct {
CacheCreationTokens
int
CacheCreationTokens
int
CacheReadTokens
int
CacheReadTokens
int
CacheCreation5mTokens
int
CacheCreation5mTokens
int
`gorm:"column:cache_creation_5m_tokens"`
CacheCreation1hTokens
int
CacheCreation1hTokens
int
`gorm:"column:cache_creation_1h_tokens"`
InputCost
float64
InputCost
float64
OutputCost
float64
OutputCost
float64
...
...
backend/migrations/054_drop_legacy_cache_columns.sql
0 → 100644
View file @
1cf51b14
-- Drop legacy cache token columns that lack the underscore separator.
-- These were created by GORM's automatic snake_case conversion:
-- CacheCreation5mTokens → cache_creation5m_tokens (incorrect)
-- CacheCreation1hTokens → cache_creation1h_tokens (incorrect)
--
-- The canonical columns are:
-- cache_creation_5m_tokens (defined in 001_init.sql)
-- cache_creation_1h_tokens (defined in 001_init.sql)
--
-- Migration 009 already copied data from legacy → canonical columns.
-- This migration drops the legacy columns to avoid confusion.
ALTER
TABLE
usage_logs
DROP
COLUMN
IF
EXISTS
cache_creation5m_tokens
;
ALTER
TABLE
usage_logs
DROP
COLUMN
IF
EXISTS
cache_creation1h_tokens
;
deploy/docker-compose.yml
View file @
1cf51b14
...
@@ -164,6 +164,7 @@ services:
...
@@ -164,6 +164,7 @@ services:
-
POSTGRES_USER=${POSTGRES_USER:-sub2api}
-
POSTGRES_USER=${POSTGRES_USER:-sub2api}
-
POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
-
POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
-
POSTGRES_DB=${POSTGRES_DB:-sub2api}
-
POSTGRES_DB=${POSTGRES_DB:-sub2api}
-
PGDATA=/var/lib/postgresql/data
-
TZ=${TZ:-Asia/Shanghai}
-
TZ=${TZ:-Asia/Shanghai}
networks
:
networks
:
-
sub2api-network
-
sub2api-network
...
...
frontend/src/api/admin/accounts.ts
View file @
1cf51b14
...
@@ -32,6 +32,7 @@ export async function list(
...
@@ -32,6 +32,7 @@ export async function list(
platform
?:
string
platform
?:
string
type
?:
string
type
?:
string
status
?:
string
status
?:
string
group
?:
string
search
?:
string
search
?:
string
},
},
options
?:
{
options
?:
{
...
...
frontend/src/components/account/AccountGroupsCell.vue
View file @
1cf51b14
...
@@ -41,7 +41,7 @@
...
@@ -41,7 +41,7 @@
>
>
<div
class=
"mb-2 flex items-center justify-between"
>
<div
class=
"mb-2 flex items-center justify-between"
>
<span
class=
"text-xs font-medium text-gray-500 dark:text-gray-400"
>
<span
class=
"text-xs font-medium text-gray-500 dark:text-gray-400"
>
{{
t
(
'
admin.accounts.
allGroups
'
,
{
count
:
groups
.
length
}
)
}}
{{
t
(
'
admin.accounts.
groupCountTotal
'
,
{
count
:
groups
.
length
}
)
}}
<
/span
>
<
/span
>
<
button
<
button
@
click
=
"
showPopover = false
"
@
click
=
"
showPopover = false
"
...
...
frontend/src/components/admin/account/AccountTableFilters.vue
View file @
1cf51b14
...
@@ -10,16 +10,21 @@
...
@@ -10,16 +10,21 @@
<Select
:model-value=
"filters.platform"
class=
"w-40"
:options=
"pOpts"
@
update:model-value=
"updatePlatform"
@
change=
"$emit('change')"
/>
<Select
:model-value=
"filters.platform"
class=
"w-40"
:options=
"pOpts"
@
update:model-value=
"updatePlatform"
@
change=
"$emit('change')"
/>
<Select
:model-value=
"filters.type"
class=
"w-40"
:options=
"tOpts"
@
update:model-value=
"updateType"
@
change=
"$emit('change')"
/>
<Select
:model-value=
"filters.type"
class=
"w-40"
:options=
"tOpts"
@
update:model-value=
"updateType"
@
change=
"$emit('change')"
/>
<Select
:model-value=
"filters.status"
class=
"w-40"
:options=
"sOpts"
@
update:model-value=
"updateStatus"
@
change=
"$emit('change')"
/>
<Select
:model-value=
"filters.status"
class=
"w-40"
:options=
"sOpts"
@
update:model-value=
"updateStatus"
@
change=
"$emit('change')"
/>
<Select
:model-value=
"filters.group"
class=
"w-40"
:options=
"gOpts"
@
update:model-value=
"updateGroup"
@
change=
"$emit('change')"
/>
</div>
</div>
</
template
>
</
template
>
<
script
setup
lang=
"ts"
>
<
script
setup
lang=
"ts"
>
import
{
computed
}
from
'
vue
'
;
import
{
useI18n
}
from
'
vue-i18n
'
;
import
Select
from
'
@/components/common/Select.vue
'
;
import
SearchInput
from
'
@/components/common/SearchInput.vue
'
import
{
computed
}
from
'
vue
'
;
import
{
useI18n
}
from
'
vue-i18n
'
;
import
Select
from
'
@/components/common/Select.vue
'
;
import
SearchInput
from
'
@/components/common/SearchInput.vue
'
const
props
=
defineProps
([
'
searchQuery
'
,
'
filters
'
]);
const
emit
=
defineEmits
([
'
update:searchQuery
'
,
'
update:filters
'
,
'
change
'
]);
const
{
t
}
=
useI18n
()
import
type
{
AdminGroup
}
from
'
@/types
'
const
props
=
defineProps
<
{
searchQuery
:
string
;
filters
:
Record
<
string
,
any
>
;
groups
?:
AdminGroup
[]
}
>
()
const
emit
=
defineEmits
([
'
update:searchQuery
'
,
'
update:filters
'
,
'
change
'
]);
const
{
t
}
=
useI18n
()
const
updatePlatform
=
(
value
:
string
|
number
|
boolean
|
null
)
=>
{
emit
(
'
update:filters
'
,
{
...
props
.
filters
,
platform
:
value
})
}
const
updatePlatform
=
(
value
:
string
|
number
|
boolean
|
null
)
=>
{
emit
(
'
update:filters
'
,
{
...
props
.
filters
,
platform
:
value
})
}
const
updateType
=
(
value
:
string
|
number
|
boolean
|
null
)
=>
{
emit
(
'
update:filters
'
,
{
...
props
.
filters
,
type
:
value
})
}
const
updateType
=
(
value
:
string
|
number
|
boolean
|
null
)
=>
{
emit
(
'
update:filters
'
,
{
...
props
.
filters
,
type
:
value
})
}
const
updateStatus
=
(
value
:
string
|
number
|
boolean
|
null
)
=>
{
emit
(
'
update:filters
'
,
{
...
props
.
filters
,
status
:
value
})
}
const
updateStatus
=
(
value
:
string
|
number
|
boolean
|
null
)
=>
{
emit
(
'
update:filters
'
,
{
...
props
.
filters
,
status
:
value
})
}
const
updateGroup
=
(
value
:
string
|
number
|
boolean
|
null
)
=>
{
emit
(
'
update:filters
'
,
{
...
props
.
filters
,
group
:
value
})
}
const
pOpts
=
computed
(()
=>
[{
value
:
''
,
label
:
t
(
'
admin.accounts.allPlatforms
'
)
},
{
value
:
'
anthropic
'
,
label
:
'
Anthropic
'
},
{
value
:
'
openai
'
,
label
:
'
OpenAI
'
},
{
value
:
'
gemini
'
,
label
:
'
Gemini
'
},
{
value
:
'
antigravity
'
,
label
:
'
Antigravity
'
}])
const
pOpts
=
computed
(()
=>
[{
value
:
''
,
label
:
t
(
'
admin.accounts.allPlatforms
'
)
},
{
value
:
'
anthropic
'
,
label
:
'
Anthropic
'
},
{
value
:
'
openai
'
,
label
:
'
OpenAI
'
},
{
value
:
'
gemini
'
,
label
:
'
Gemini
'
},
{
value
:
'
antigravity
'
,
label
:
'
Antigravity
'
}])
const
tOpts
=
computed
(()
=>
[{
value
:
''
,
label
:
t
(
'
admin.accounts.allTypes
'
)
},
{
value
:
'
oauth
'
,
label
:
t
(
'
admin.accounts.oauthType
'
)
},
{
value
:
'
setup-token
'
,
label
:
t
(
'
admin.accounts.setupToken
'
)
},
{
value
:
'
apikey
'
,
label
:
t
(
'
admin.accounts.apiKey
'
)
}])
const
tOpts
=
computed
(()
=>
[{
value
:
''
,
label
:
t
(
'
admin.accounts.allTypes
'
)
},
{
value
:
'
oauth
'
,
label
:
t
(
'
admin.accounts.oauthType
'
)
},
{
value
:
'
setup-token
'
,
label
:
t
(
'
admin.accounts.setupToken
'
)
},
{
value
:
'
apikey
'
,
label
:
t
(
'
admin.accounts.apiKey
'
)
}])
const
sOpts
=
computed
(()
=>
[{
value
:
''
,
label
:
t
(
'
admin.accounts.allStatus
'
)
},
{
value
:
'
active
'
,
label
:
t
(
'
admin.accounts.status.active
'
)
},
{
value
:
'
inactive
'
,
label
:
t
(
'
admin.accounts.status.inactive
'
)
},
{
value
:
'
error
'
,
label
:
t
(
'
admin.accounts.status.error
'
)
},
{
value
:
'
rate_limited
'
,
label
:
t
(
'
admin.accounts.status.rateLimited
'
)
}])
const
sOpts
=
computed
(()
=>
[{
value
:
''
,
label
:
t
(
'
admin.accounts.allStatus
'
)
},
{
value
:
'
active
'
,
label
:
t
(
'
admin.accounts.status.active
'
)
},
{
value
:
'
inactive
'
,
label
:
t
(
'
admin.accounts.status.inactive
'
)
},
{
value
:
'
error
'
,
label
:
t
(
'
admin.accounts.status.error
'
)
},
{
value
:
'
rate_limited
'
,
label
:
t
(
'
admin.accounts.status.rateLimited
'
)
}])
const
gOpts
=
computed
(()
=>
[{
value
:
''
,
label
:
t
(
'
admin.accounts.allGroups
'
)
},
...(
props
.
groups
||
[]).
map
(
g
=>
({
value
:
String
(
g
.
id
),
label
:
g
.
name
}))])
</
script
>
</
script
>
frontend/src/components/admin/usage/UsageTable.vue
View file @
1cf51b14
...
@@ -70,6 +70,7 @@
...
@@ -70,6 +70,7 @@
<div
v-if=
"row.cache_creation_tokens > 0"
class=
"inline-flex items-center gap-1"
>
<div
v-if=
"row.cache_creation_tokens > 0"
class=
"inline-flex items-center gap-1"
>
<svg
class=
"h-3.5 w-3.5 text-amber-500"
fill=
"none"
stroke=
"currentColor"
viewBox=
"0 0 24 24"
><path
stroke-linecap=
"round"
stroke-linejoin=
"round"
stroke-width=
"2"
d=
"M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
/></svg>
<svg
class=
"h-3.5 w-3.5 text-amber-500"
fill=
"none"
stroke=
"currentColor"
viewBox=
"0 0 24 24"
><path
stroke-linecap=
"round"
stroke-linejoin=
"round"
stroke-width=
"2"
d=
"M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
/></svg>
<span
class=
"font-medium text-amber-600 dark:text-amber-400"
>
{{
formatCacheTokens
(
row
.
cache_creation_tokens
)
}}
</span>
<span
class=
"font-medium text-amber-600 dark:text-amber-400"
>
{{
formatCacheTokens
(
row
.
cache_creation_tokens
)
}}
</span>
<span
v-if=
"row.cache_creation_1h_tokens > 0"
class=
"inline-flex items-center rounded px-1 py-px text-[10px] font-medium leading-tight bg-orange-100 text-orange-600 ring-1 ring-inset ring-orange-200 dark:bg-orange-500/20 dark:text-orange-400 dark:ring-orange-500/30"
>
1h
</span>
</div>
</div>
</div>
</div>
</div>
</div>
...
@@ -157,10 +158,30 @@
...
@@ -157,10 +158,30 @@
<span
class=
"text-gray-400"
>
{{ t('admin.usage.outputTokens') }}
</span>
<span
class=
"text-gray-400"
>
{{ t('admin.usage.outputTokens') }}
</span>
<span
class=
"font-medium text-white"
>
{{ tokenTooltipData.output_tokens.toLocaleString() }}
</span>
<span
class=
"font-medium text-white"
>
{{ tokenTooltipData.output_tokens.toLocaleString() }}
</span>
</div>
</div>
<div
v-if=
"tokenTooltipData && tokenTooltipData.cache_creation_tokens > 0"
class=
"flex items-center justify-between gap-4"
>
<div
v-if=
"tokenTooltipData && tokenTooltipData.cache_creation_tokens > 0"
>
<!-- 有 5m/1h 明细时,展开显示 -->
<
template
v-if=
"tokenTooltipData.cache_creation_5m_tokens > 0 || tokenTooltipData.cache_creation_1h_tokens > 0"
>
<div
v-if=
"tokenTooltipData.cache_creation_5m_tokens > 0"
class=
"flex items-center justify-between gap-4"
>
<span
class=
"text-gray-400 flex items-center gap-1.5"
>
{{
t
(
'
admin.usage.cacheCreation5mTokens
'
)
}}
<span
class=
"inline-flex items-center rounded px-1 py-px text-[10px] font-medium leading-tight bg-amber-500/20 text-amber-400 ring-1 ring-inset ring-amber-500/30"
>
5m
</span>
</span>
<span
class=
"font-medium text-white"
>
{{
tokenTooltipData
.
cache_creation_5m_tokens
.
toLocaleString
()
}}
</span>
</div>
<div
v-if=
"tokenTooltipData.cache_creation_1h_tokens > 0"
class=
"flex items-center justify-between gap-4"
>
<span
class=
"text-gray-400 flex items-center gap-1.5"
>
{{
t
(
'
admin.usage.cacheCreation1hTokens
'
)
}}
<span
class=
"inline-flex items-center rounded px-1 py-px text-[10px] font-medium leading-tight bg-orange-500/20 text-orange-400 ring-1 ring-inset ring-orange-500/30"
>
1h
</span>
</span>
<span
class=
"font-medium text-white"
>
{{
tokenTooltipData
.
cache_creation_1h_tokens
.
toLocaleString
()
}}
</span>
</div>
</
template
>
<!-- 无明细时,只显示聚合值 -->
<div
v-else
class=
"flex items-center justify-between gap-4"
>
<span
class=
"text-gray-400"
>
{{ t('admin.usage.cacheCreationTokens') }}
</span>
<span
class=
"text-gray-400"
>
{{ t('admin.usage.cacheCreationTokens') }}
</span>
<span
class=
"font-medium text-white"
>
{{ tokenTooltipData.cache_creation_tokens.toLocaleString() }}
</span>
<span
class=
"font-medium text-white"
>
{{ tokenTooltipData.cache_creation_tokens.toLocaleString() }}
</span>
</div>
</div>
</div>
<div
v-if=
"tokenTooltipData && tokenTooltipData.cache_read_tokens > 0"
class=
"flex items-center justify-between gap-4"
>
<div
v-if=
"tokenTooltipData && tokenTooltipData.cache_read_tokens > 0"
class=
"flex items-center justify-between gap-4"
>
<span
class=
"text-gray-400"
>
{{ t('admin.usage.cacheReadTokens') }}
</span>
<span
class=
"text-gray-400"
>
{{ t('admin.usage.cacheReadTokens') }}
</span>
<span
class=
"font-medium text-white"
>
{{ tokenTooltipData.cache_read_tokens.toLocaleString() }}
</span>
<span
class=
"font-medium text-white"
>
{{ tokenTooltipData.cache_read_tokens.toLocaleString() }}
</span>
...
...
frontend/src/components/common/StatCard.vue
View file @
1cf51b14
...
@@ -6,7 +6,7 @@
...
@@ -6,7 +6,7 @@
<div
class=
"min-w-0 flex-1"
>
<div
class=
"min-w-0 flex-1"
>
<p
class=
"stat-label truncate"
>
{{
title
}}
</p>
<p
class=
"stat-label truncate"
>
{{
title
}}
</p>
<div
class=
"mt-1 flex items-baseline gap-2"
>
<div
class=
"mt-1 flex items-baseline gap-2"
>
<p
class=
"stat-value"
>
{{
formattedValue
}}
</p>
<p
class=
"stat-value"
:title=
"String(formattedValue)"
>
{{
formattedValue
}}
</p>
<span
v-if=
"change !== undefined"
:class=
"['stat-trend', trendClass]"
>
<span
v-if=
"change !== undefined"
:class=
"['stat-trend', trendClass]"
>
<Icon
<Icon
v-if=
"changeType !== 'neutral'"
v-if=
"changeType !== 'neutral'"
...
...
frontend/src/components/layout/AppSidebar.vue
View file @
1cf51b14
...
@@ -10,7 +10,7 @@
...
@@ -10,7 +10,7 @@
<div
class=
"sidebar-header"
>
<div
class=
"sidebar-header"
>
<!-- Custom Logo or Default Logo -->
<!-- Custom Logo or Default Logo -->
<div
class=
"flex h-9 w-9 items-center justify-center overflow-hidden rounded-xl shadow-glow"
>
<div
class=
"flex h-9 w-9 items-center justify-center overflow-hidden rounded-xl shadow-glow"
>
<img
:src=
"siteLogo || '/logo.png'"
alt=
"Logo"
class=
"h-full w-full object-contain"
/>
<img
v-if=
"settingsLoaded"
:src=
"siteLogo || '/logo.png'"
alt=
"Logo"
class=
"h-full w-full object-contain"
/>
</div>
</div>
<transition
name=
"fade"
>
<transition
name=
"fade"
>
<div
v-if=
"!sidebarCollapsed"
class=
"flex flex-col"
>
<div
v-if=
"!sidebarCollapsed"
class=
"flex flex-col"
>
...
@@ -167,6 +167,7 @@ const isDark = ref(document.documentElement.classList.contains('dark'))
...
@@ -167,6 +167,7 @@ const isDark = ref(document.documentElement.classList.contains('dark'))
const
siteName
=
computed
(()
=>
appStore
.
siteName
)
const
siteName
=
computed
(()
=>
appStore
.
siteName
)
const
siteLogo
=
computed
(()
=>
appStore
.
siteLogo
)
const
siteLogo
=
computed
(()
=>
appStore
.
siteLogo
)
const
siteVersion
=
computed
(()
=>
appStore
.
siteVersion
)
const
siteVersion
=
computed
(()
=>
appStore
.
siteVersion
)
const
settingsLoaded
=
computed
(()
=>
appStore
.
publicSettingsLoaded
)
// SVG Icon Components
// SVG Icon Components
const
DashboardIcon
=
{
const
DashboardIcon
=
{
...
...
frontend/src/i18n/locales/en.ts
View file @
1cf51b14
...
@@ -1335,6 +1335,7 @@ export default {
...
@@ -1335,6 +1335,7 @@ export default {
allPlatforms
:
'
All Platforms
'
,
allPlatforms
:
'
All Platforms
'
,
allTypes
:
'
All Types
'
,
allTypes
:
'
All Types
'
,
allStatus
:
'
All Status
'
,
allStatus
:
'
All Status
'
,
allGroups
:
'
All Groups
'
,
oauthType
:
'
OAuth
'
,
oauthType
:
'
OAuth
'
,
setupToken
:
'
Setup Token
'
,
setupToken
:
'
Setup Token
'
,
apiKey
:
'
API Key
'
,
apiKey
:
'
API Key
'
,
...
@@ -1344,7 +1345,7 @@ export default {
...
@@ -1344,7 +1345,7 @@ export default {
schedulableEnabled
:
'
Scheduling enabled
'
,
schedulableEnabled
:
'
Scheduling enabled
'
,
schedulableDisabled
:
'
Scheduling disabled
'
,
schedulableDisabled
:
'
Scheduling disabled
'
,
failedToToggleSchedulable
:
'
Failed to toggle scheduling status
'
,
failedToToggleSchedulable
:
'
Failed to toggle scheduling status
'
,
allGroups
:
'
{count} groups total
'
,
groupCountTotal
:
'
{count} groups total
'
,
platforms
:
{
platforms
:
{
anthropic
:
'
Anthropic
'
,
anthropic
:
'
Anthropic
'
,
claude
:
'
Claude
'
,
claude
:
'
Claude
'
,
...
@@ -2359,6 +2360,8 @@ export default {
...
@@ -2359,6 +2360,8 @@ export default {
inputTokens
:
'
Input Tokens
'
,
inputTokens
:
'
Input Tokens
'
,
outputTokens
:
'
Output Tokens
'
,
outputTokens
:
'
Output Tokens
'
,
cacheCreationTokens
:
'
Cache Creation Tokens
'
,
cacheCreationTokens
:
'
Cache Creation Tokens
'
,
cacheCreation5mTokens
:
'
Cache Write
'
,
cacheCreation1hTokens
:
'
Cache Write
'
,
cacheReadTokens
:
'
Cache Read Tokens
'
,
cacheReadTokens
:
'
Cache Read Tokens
'
,
failedToLoad
:
'
Failed to load usage records
'
,
failedToLoad
:
'
Failed to load usage records
'
,
billingType
:
'
Billing Type
'
,
billingType
:
'
Billing Type
'
,
...
...
frontend/src/i18n/locales/zh.ts
View file @
1cf51b14
...
@@ -1426,6 +1426,7 @@ export default {
...
@@ -1426,6 +1426,7 @@ export default {
allPlatforms
:
'
全部平台
'
,
allPlatforms
:
'
全部平台
'
,
allTypes
:
'
全部类型
'
,
allTypes
:
'
全部类型
'
,
allStatus
:
'
全部状态
'
,
allStatus
:
'
全部状态
'
,
allGroups
:
'
全部分组
'
,
oauthType
:
'
OAuth
'
,
oauthType
:
'
OAuth
'
,
// Schedulable toggle
// Schedulable toggle
schedulable
:
'
参与调度
'
,
schedulable
:
'
参与调度
'
,
...
@@ -1433,7 +1434,7 @@ export default {
...
@@ -1433,7 +1434,7 @@ export default {
schedulableEnabled
:
'
调度已开启
'
,
schedulableEnabled
:
'
调度已开启
'
,
schedulableDisabled
:
'
调度已关闭
'
,
schedulableDisabled
:
'
调度已关闭
'
,
failedToToggleSchedulable
:
'
切换调度状态失败
'
,
failedToToggleSchedulable
:
'
切换调度状态失败
'
,
allGroups
:
'
共 {count} 个分组
'
,
groupCountTotal
:
'
共 {count} 个分组
'
,
columns
:
{
columns
:
{
name
:
'
名称
'
,
name
:
'
名称
'
,
platformType
:
'
平台/类型
'
,
platformType
:
'
平台/类型
'
,
...
@@ -2526,6 +2527,8 @@ export default {
...
@@ -2526,6 +2527,8 @@ export default {
inputTokens
:
'
输入 Token
'
,
inputTokens
:
'
输入 Token
'
,
outputTokens
:
'
输出 Token
'
,
outputTokens
:
'
输出 Token
'
,
cacheCreationTokens
:
'
缓存创建 Token
'
,
cacheCreationTokens
:
'
缓存创建 Token
'
,
cacheCreation5mTokens
:
'
缓存创建
'
,
cacheCreation1hTokens
:
'
缓存创建
'
,
cacheReadTokens
:
'
缓存读取 Token
'
,
cacheReadTokens
:
'
缓存读取 Token
'
,
failedToLoad
:
'
加载使用记录失败
'
,
failedToLoad
:
'
加载使用记录失败
'
,
billingType
:
'
计费类型
'
,
billingType
:
'
计费类型
'
,
...
...
frontend/src/style.css
View file @
1cf51b14
...
@@ -243,7 +243,7 @@
...
@@ -243,7 +243,7 @@
}
}
.stat-value
{
.stat-value
{
@apply
text-2xl
font-bold
text-gray-900
dark
:
text-white
;
@apply
text-2xl
font-bold
text-gray-900
dark
:
text-white
truncate
;
}
}
.stat-label
{
.stat-label
{
...
...
frontend/src/views/admin/AccountsView.vue
View file @
1cf51b14
...
@@ -6,6 +6,7 @@
...
@@ -6,6 +6,7 @@
<AccountTableFilters
<AccountTableFilters
v-model:searchQuery=
"params.search"
v-model:searchQuery=
"params.search"
:filters=
"params"
:filters=
"params"
:groups=
"groups"
@
update:filters=
"(newFilters) => Object.assign(params, newFilters)"
@
update:filters=
"(newFilters) => Object.assign(params, newFilters)"
@
change=
"debouncedReload"
@
change=
"debouncedReload"
@
update:searchQuery=
"debouncedReload"
@
update:searchQuery=
"debouncedReload"
...
@@ -439,7 +440,7 @@ const isColumnVisible = (key: string) => !hiddenColumns.has(key)
...
@@ -439,7 +440,7 @@ const isColumnVisible = (key: string) => !hiddenColumns.has(key)
const
{
items
:
accounts
,
loading
,
params
,
pagination
,
load
,
reload
,
debouncedReload
,
handlePageChange
,
handlePageSizeChange
}
=
useTableLoader
<
Account
,
any
>
({
const
{
items
:
accounts
,
loading
,
params
,
pagination
,
load
,
reload
,
debouncedReload
,
handlePageChange
,
handlePageSizeChange
}
=
useTableLoader
<
Account
,
any
>
({
fetchFn
:
adminAPI
.
accounts
.
list
,
fetchFn
:
adminAPI
.
accounts
.
list
,
initialParams
:
{
platform
:
''
,
type
:
''
,
status
:
''
,
search
:
''
}
initialParams
:
{
platform
:
''
,
type
:
''
,
status
:
''
,
group
:
''
,
search
:
''
}
}
)
}
)
const
isAnyModalOpen
=
computed
(()
=>
{
const
isAnyModalOpen
=
computed
(()
=>
{
...
...
frontend/src/views/user/UsageView.vue
View file @
1cf51b14
...
@@ -233,6 +233,7 @@
...
@@ -233,6 +233,7 @@
<span
class=
"font-medium text-amber-600 dark:text-amber-400"
>
{{
<span
class=
"font-medium text-amber-600 dark:text-amber-400"
>
{{
formatCacheTokens
(
row
.
cache_creation_tokens
)
formatCacheTokens
(
row
.
cache_creation_tokens
)
}}
</span>
}}
</span>
<span
v-if=
"row.cache_creation_1h_tokens > 0"
class=
"inline-flex items-center rounded px-1 py-px text-[10px] font-medium leading-tight bg-orange-100 text-orange-600 ring-1 ring-inset ring-orange-200 dark:bg-orange-500/20 dark:text-orange-400 dark:ring-orange-500/30"
>
1h
</span>
</div>
</div>
</div>
</div>
</div>
</div>
...
@@ -350,10 +351,30 @@
...
@@ -350,10 +351,30 @@
<span
class=
"text-gray-400"
>
{{ t('admin.usage.outputTokens') }}
</span>
<span
class=
"text-gray-400"
>
{{ t('admin.usage.outputTokens') }}
</span>
<span
class=
"font-medium text-white"
>
{{ tokenTooltipData.output_tokens.toLocaleString() }}
</span>
<span
class=
"font-medium text-white"
>
{{ tokenTooltipData.output_tokens.toLocaleString() }}
</span>
</div>
</div>
<div
v-if=
"tokenTooltipData && tokenTooltipData.cache_creation_tokens > 0"
class=
"flex items-center justify-between gap-4"
>
<div
v-if=
"tokenTooltipData && tokenTooltipData.cache_creation_tokens > 0"
>
<!-- 有 5m/1h 明细时,展开显示 -->
<
template
v-if=
"tokenTooltipData.cache_creation_5m_tokens > 0 || tokenTooltipData.cache_creation_1h_tokens > 0"
>
<div
v-if=
"tokenTooltipData.cache_creation_5m_tokens > 0"
class=
"flex items-center justify-between gap-4"
>
<span
class=
"text-gray-400 flex items-center gap-1.5"
>
{{
t
(
'
admin.usage.cacheCreation5mTokens
'
)
}}
<span
class=
"inline-flex items-center rounded px-1 py-px text-[10px] font-medium leading-tight bg-amber-500/20 text-amber-400 ring-1 ring-inset ring-amber-500/30"
>
5m
</span>
</span>
<span
class=
"font-medium text-white"
>
{{
tokenTooltipData
.
cache_creation_5m_tokens
.
toLocaleString
()
}}
</span>
</div>
<div
v-if=
"tokenTooltipData.cache_creation_1h_tokens > 0"
class=
"flex items-center justify-between gap-4"
>
<span
class=
"text-gray-400 flex items-center gap-1.5"
>
{{
t
(
'
admin.usage.cacheCreation1hTokens
'
)
}}
<span
class=
"inline-flex items-center rounded px-1 py-px text-[10px] font-medium leading-tight bg-orange-500/20 text-orange-400 ring-1 ring-inset ring-orange-500/30"
>
1h
</span>
</span>
<span
class=
"font-medium text-white"
>
{{
tokenTooltipData
.
cache_creation_1h_tokens
.
toLocaleString
()
}}
</span>
</div>
</
template
>
<!-- 无明细时,只显示聚合值 -->
<div
v-else
class=
"flex items-center justify-between gap-4"
>
<span
class=
"text-gray-400"
>
{{ t('admin.usage.cacheCreationTokens') }}
</span>
<span
class=
"text-gray-400"
>
{{ t('admin.usage.cacheCreationTokens') }}
</span>
<span
class=
"font-medium text-white"
>
{{ tokenTooltipData.cache_creation_tokens.toLocaleString() }}
</span>
<span
class=
"font-medium text-white"
>
{{ tokenTooltipData.cache_creation_tokens.toLocaleString() }}
</span>
</div>
</div>
</div>
<div
v-if=
"tokenTooltipData && tokenTooltipData.cache_read_tokens > 0"
class=
"flex items-center justify-between gap-4"
>
<div
v-if=
"tokenTooltipData && tokenTooltipData.cache_read_tokens > 0"
class=
"flex items-center justify-between gap-4"
>
<span
class=
"text-gray-400"
>
{{ t('admin.usage.cacheReadTokens') }}
</span>
<span
class=
"text-gray-400"
>
{{ t('admin.usage.cacheReadTokens') }}
</span>
<span
class=
"font-medium text-white"
>
{{ tokenTooltipData.cache_read_tokens.toLocaleString() }}
</span>
<span
class=
"font-medium text-white"
>
{{ tokenTooltipData.cache_read_tokens.toLocaleString() }}
</span>
...
...
Prev
1
2
Next
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