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
af9c4a7d
Commit
af9c4a7d
authored
Mar 13, 2026
by
Peter
Browse files
feat(ops): make openai token stats optional
parent
826090e0
Changes
10
Hide whitespace changes
Inline
Side-by-side
backend/internal/service/ops_settings.go
View file @
af9c4a7d
...
@@ -371,6 +371,7 @@ func defaultOpsAdvancedSettings() *OpsAdvancedSettings {
...
@@ -371,6 +371,7 @@ func defaultOpsAdvancedSettings() *OpsAdvancedSettings {
IgnoreCountTokensErrors
:
true
,
// count_tokens 404 是预期行为,默认忽略
IgnoreCountTokensErrors
:
true
,
// count_tokens 404 是预期行为,默认忽略
IgnoreContextCanceled
:
true
,
// Default to true - client disconnects are not errors
IgnoreContextCanceled
:
true
,
// Default to true - client disconnects are not errors
IgnoreNoAvailableAccounts
:
false
,
// Default to false - this is a real routing issue
IgnoreNoAvailableAccounts
:
false
,
// Default to false - this is a real routing issue
DisplayOpenAITokenStats
:
false
,
AutoRefreshEnabled
:
false
,
AutoRefreshEnabled
:
false
,
AutoRefreshIntervalSec
:
30
,
AutoRefreshIntervalSec
:
30
,
}
}
...
...
backend/internal/service/ops_settings_advanced_test.go
0 → 100644
View file @
af9c4a7d
package
service
import
(
"context"
"testing"
)
func
TestGetOpsAdvancedSettings_DefaultHidesOpenAITokenStats
(
t
*
testing
.
T
)
{
repo
:=
newRuntimeSettingRepoStub
()
svc
:=
&
OpsService
{
settingRepo
:
repo
}
cfg
,
err
:=
svc
.
GetOpsAdvancedSettings
(
context
.
Background
())
if
err
!=
nil
{
t
.
Fatalf
(
"GetOpsAdvancedSettings() error = %v"
,
err
)
}
if
cfg
.
DisplayOpenAITokenStats
{
t
.
Fatalf
(
"DisplayOpenAITokenStats = true, want false by default"
)
}
if
repo
.
setCalls
!=
1
{
t
.
Fatalf
(
"expected defaults to be persisted once, got %d"
,
repo
.
setCalls
)
}
}
func
TestUpdateOpsAdvancedSettings_PersistsOpenAITokenStatsVisibility
(
t
*
testing
.
T
)
{
repo
:=
newRuntimeSettingRepoStub
()
svc
:=
&
OpsService
{
settingRepo
:
repo
}
cfg
:=
defaultOpsAdvancedSettings
()
cfg
.
DisplayOpenAITokenStats
=
true
updated
,
err
:=
svc
.
UpdateOpsAdvancedSettings
(
context
.
Background
(),
cfg
)
if
err
!=
nil
{
t
.
Fatalf
(
"UpdateOpsAdvancedSettings() error = %v"
,
err
)
}
if
!
updated
.
DisplayOpenAITokenStats
{
t
.
Fatalf
(
"DisplayOpenAITokenStats = false, want true"
)
}
reloaded
,
err
:=
svc
.
GetOpsAdvancedSettings
(
context
.
Background
())
if
err
!=
nil
{
t
.
Fatalf
(
"GetOpsAdvancedSettings() after update error = %v"
,
err
)
}
if
!
reloaded
.
DisplayOpenAITokenStats
{
t
.
Fatalf
(
"reloaded DisplayOpenAITokenStats = false, want true"
)
}
}
backend/internal/service/ops_settings_models.go
View file @
af9c4a7d
...
@@ -98,6 +98,7 @@ type OpsAdvancedSettings struct {
...
@@ -98,6 +98,7 @@ type OpsAdvancedSettings struct {
IgnoreContextCanceled
bool
`json:"ignore_context_canceled"`
IgnoreContextCanceled
bool
`json:"ignore_context_canceled"`
IgnoreNoAvailableAccounts
bool
`json:"ignore_no_available_accounts"`
IgnoreNoAvailableAccounts
bool
`json:"ignore_no_available_accounts"`
IgnoreInvalidApiKeyErrors
bool
`json:"ignore_invalid_api_key_errors"`
IgnoreInvalidApiKeyErrors
bool
`json:"ignore_invalid_api_key_errors"`
DisplayOpenAITokenStats
bool
`json:"display_openai_token_stats"`
AutoRefreshEnabled
bool
`json:"auto_refresh_enabled"`
AutoRefreshEnabled
bool
`json:"auto_refresh_enabled"`
AutoRefreshIntervalSec
int
`json:"auto_refresh_interval_seconds"`
AutoRefreshIntervalSec
int
`json:"auto_refresh_interval_seconds"`
}
}
...
...
frontend/src/api/admin/ops.ts
View file @
af9c4a7d
...
@@ -841,6 +841,7 @@ export interface OpsAdvancedSettings {
...
@@ -841,6 +841,7 @@ export interface OpsAdvancedSettings {
ignore_context_canceled
:
boolean
ignore_context_canceled
:
boolean
ignore_no_available_accounts
:
boolean
ignore_no_available_accounts
:
boolean
ignore_invalid_api_key_errors
:
boolean
ignore_invalid_api_key_errors
:
boolean
display_openai_token_stats
:
boolean
auto_refresh_enabled
:
boolean
auto_refresh_enabled
:
boolean
auto_refresh_interval_seconds
:
number
auto_refresh_interval_seconds
:
number
}
}
...
...
frontend/src/i18n/locales/en.ts
View file @
af9c4a7d
...
@@ -3651,6 +3651,9 @@ export default {
...
@@ -3651,6 +3651,9 @@ export default {
refreshInterval15s
:
'
15 seconds
'
,
refreshInterval15s
:
'
15 seconds
'
,
refreshInterval30s
:
'
30 seconds
'
,
refreshInterval30s
:
'
30 seconds
'
,
refreshInterval60s
:
'
60 seconds
'
,
refreshInterval60s
:
'
60 seconds
'
,
dashboardCards
:
'
Dashboard Cards
'
,
displayOpenAITokenStats
:
'
Display OpenAI token request stats
'
,
displayOpenAITokenStatsHint
:
'
Show or hide the OpenAI token request stats card on the ops dashboard. Hidden by default.
'
,
autoRefreshCountdown
:
'
Auto refresh: {seconds}s
'
,
autoRefreshCountdown
:
'
Auto refresh: {seconds}s
'
,
validation
:
{
validation
:
{
title
:
'
Please fix the following issues
'
,
title
:
'
Please fix the following issues
'
,
...
...
frontend/src/i18n/locales/zh.ts
View file @
af9c4a7d
...
@@ -3825,6 +3825,9 @@ export default {
...
@@ -3825,6 +3825,9 @@ export default {
refreshInterval15s
:
'
15 秒
'
,
refreshInterval15s
:
'
15 秒
'
,
refreshInterval30s
:
'
30 秒
'
,
refreshInterval30s
:
'
30 秒
'
,
refreshInterval60s
:
'
60 秒
'
,
refreshInterval60s
:
'
60 秒
'
,
dashboardCards
:
'
仪表盘卡片
'
,
displayOpenAITokenStats
:
'
展示 OpenAI Token 请求统计
'
,
displayOpenAITokenStatsHint
:
'
控制运维监控仪表盘中 OpenAI Token 请求统计卡片是否显示,默认关闭。
'
,
autoRefreshCountdown
:
'
自动刷新:{seconds}s
'
,
autoRefreshCountdown
:
'
自动刷新:{seconds}s
'
,
validation
:
{
validation
:
{
title
:
'
请先修正以下问题
'
,
title
:
'
请先修正以下问题
'
,
...
...
frontend/src/views/admin/ops/OpsDashboard.vue
View file @
af9c4a7d
...
@@ -85,7 +85,7 @@
...
@@ -85,7 +85,7 @@
</div>
</div>
<!-- Row: OpenAI Token Stats -->
<!-- Row: OpenAI Token Stats -->
<div
v-if=
"opsEnabled && !(loading && !hasLoadedOnce)"
class=
"grid grid-cols-1 gap-6"
>
<div
v-if=
"opsEnabled &&
showOpenAITokenStats &&
!(loading && !hasLoadedOnce)"
class=
"grid grid-cols-1 gap-6"
>
<OpsOpenAITokenStatsCard
<OpsOpenAITokenStatsCard
:platform-filter=
"platform"
:platform-filter=
"platform"
:group-id-filter=
"groupId"
:group-id-filter=
"groupId"
...
@@ -381,6 +381,7 @@ const showSettingsDialog = ref(false)
...
@@ -381,6 +381,7 @@ const showSettingsDialog = ref(false)
const
showAlertRulesCard
=
ref
(
false
)
const
showAlertRulesCard
=
ref
(
false
)
// Auto refresh settings
// Auto refresh settings
const
showOpenAITokenStats
=
ref
(
false
)
const
autoRefreshEnabled
=
ref
(
false
)
const
autoRefreshEnabled
=
ref
(
false
)
const
autoRefreshIntervalMs
=
ref
(
30000
)
// default 30 seconds
const
autoRefreshIntervalMs
=
ref
(
30000
)
// default 30 seconds
const
autoRefreshCountdown
=
ref
(
0
)
const
autoRefreshCountdown
=
ref
(
0
)
...
@@ -408,15 +409,20 @@ const { pause: pauseCountdown, resume: resumeCountdown } = useIntervalFn(
...
@@ -408,15 +409,20 @@ const { pause: pauseCountdown, resume: resumeCountdown } = useIntervalFn(
{
immediate
:
false
}
{
immediate
:
false
}
)
)
// Load
auto refresh
settings from backend
// Load
ops dashboard presentation
settings from backend
.
async
function
load
AutoRefresh
Settings
()
{
async
function
load
DashboardAdvanced
Settings
()
{
try
{
try
{
const
settings
=
await
opsAPI
.
getAdvancedSettings
()
const
settings
=
await
opsAPI
.
getAdvancedSettings
()
showOpenAITokenStats
.
value
=
settings
.
display_openai_token_stats
autoRefreshEnabled
.
value
=
settings
.
auto_refresh_enabled
autoRefreshEnabled
.
value
=
settings
.
auto_refresh_enabled
autoRefreshIntervalMs
.
value
=
settings
.
auto_refresh_interval_seconds
*
1000
autoRefreshIntervalMs
.
value
=
settings
.
auto_refresh_interval_seconds
*
1000
autoRefreshCountdown
.
value
=
settings
.
auto_refresh_interval_seconds
autoRefreshCountdown
.
value
=
settings
.
auto_refresh_interval_seconds
}
catch
(
err
)
{
}
catch
(
err
)
{
console
.
error
(
'
[OpsDashboard] Failed to load auto refresh settings
'
,
err
)
console
.
error
(
'
[OpsDashboard] Failed to load dashboard advanced settings
'
,
err
)
showOpenAITokenStats
.
value
=
false
autoRefreshEnabled
.
value
=
false
autoRefreshIntervalMs
.
value
=
30000
autoRefreshCountdown
.
value
=
0
}
}
}
}
...
@@ -464,7 +470,8 @@ function onCustomTimeRangeChange(startTime: string, endTime: string) {
...
@@ -464,7 +470,8 @@ function onCustomTimeRangeChange(startTime: string, endTime: string) {
customEndTime
.
value
=
endTime
customEndTime
.
value
=
endTime
}
}
function
onSettingsSaved
()
{
async
function
onSettingsSaved
()
{
await
loadDashboardAdvancedSettings
()
loadThresholds
()
loadThresholds
()
fetchData
()
fetchData
()
}
}
...
@@ -774,7 +781,7 @@ onMounted(async () => {
...
@@ -774,7 +781,7 @@ onMounted(async () => {
loadThresholds
()
loadThresholds
()
// Load auto refresh settings
// Load auto refresh settings
await
load
AutoRefresh
Settings
()
await
load
DashboardAdvanced
Settings
()
if
(
opsEnabled
.
value
)
{
if
(
opsEnabled
.
value
)
{
await
fetchData
()
await
fetchData
()
...
@@ -816,7 +823,7 @@ watch(autoRefreshEnabled, (enabled) => {
...
@@ -816,7 +823,7 @@ watch(autoRefreshEnabled, (enabled) => {
// Reload auto refresh settings after settings dialog is closed
// Reload auto refresh settings after settings dialog is closed
watch
(
showSettingsDialog
,
async
(
show
)
=>
{
watch
(
showSettingsDialog
,
async
(
show
)
=>
{
if
(
!
show
)
{
if
(
!
show
)
{
await
load
AutoRefresh
Settings
()
await
load
DashboardAdvanced
Settings
()
}
}
})
})
</
script
>
</
script
>
frontend/src/views/admin/ops/components/OpsOpenAITokenStatsCard.vue
View file @
af9c4a7d
...
@@ -208,35 +208,39 @@ function onNextPage() {
...
@@ -208,35 +208,39 @@ function onNextPage() {
:
description
=
"
t('admin.ops.openaiTokenStats.empty')
"
:
description
=
"
t('admin.ops.openaiTokenStats.empty')
"
/>
/>
<
div
v
-
else
class
=
"
overflow-x-auto
"
>
<
div
v
-
else
class
=
"
space-y-3
"
>
<
table
class
=
"
min-w-full text-left text-xs md:text-sm
"
>
<
div
class
=
"
overflow-hidden rounded-xl border border-gray-200 dark:border-dark-700
"
>
<
thead
>
<
div
class
=
"
max-h-[420px] overflow-auto
"
>
<
tr
class
=
"
border-b border-gray-200 text-gray-500 dark:border-dark-700 dark:text-gray-400
"
>
<
table
class
=
"
min-w-full text-left text-xs md:text-sm
"
>
<
th
class
=
"
px-2 py-2 font-semibold
"
>
{{
t
(
'
admin.ops.openaiTokenStats.table.model
'
)
}}
<
/th
>
<
thead
class
=
"
sticky top-0 z-10 bg-white dark:bg-dark-800
"
>
<
th
class
=
"
px-2 py-2 font-semibold
"
>
{{
t
(
'
admin.ops.openaiTokenStats.table.requestCount
'
)
}}
<
/th
>
<
tr
class
=
"
border-b border-gray-200 text-gray-500 dark:border-dark-700 dark:text-gray-400
"
>
<
th
class
=
"
px-2 py-2 font-semibold
"
>
{{
t
(
'
admin.ops.openaiTokenStats.table.avgTokensPerSec
'
)
}}
<
/th
>
<
th
class
=
"
px-2 py-2 font-semibold
"
>
{{
t
(
'
admin.ops.openaiTokenStats.table.model
'
)
}}
<
/th
>
<
th
class
=
"
px-2 py-2 font-semibold
"
>
{{
t
(
'
admin.ops.openaiTokenStats.table.avgFirstTokenMs
'
)
}}
<
/th
>
<
th
class
=
"
px-2 py-2 font-semibold
"
>
{{
t
(
'
admin.ops.openaiTokenStats.table.requestCount
'
)
}}
<
/th
>
<
th
class
=
"
px-2 py-2 font-semibold
"
>
{{
t
(
'
admin.ops.openaiTokenStats.table.totalOutputTokens
'
)
}}
<
/th
>
<
th
class
=
"
px-2 py-2 font-semibold
"
>
{{
t
(
'
admin.ops.openaiTokenStats.table.avgTokensPerSec
'
)
}}
<
/th
>
<
th
class
=
"
px-2 py-2 font-semibold
"
>
{{
t
(
'
admin.ops.openaiTokenStats.table.avgDurationMs
'
)
}}
<
/th
>
<
th
class
=
"
px-2 py-2 font-semibold
"
>
{{
t
(
'
admin.ops.openaiTokenStats.table.avgFirstTokenMs
'
)
}}
<
/th
>
<
th
class
=
"
px-2 py-2 font-semibold
"
>
{{
t
(
'
admin.ops.openaiTokenStats.table.requestsWithFirstToken
'
)
}}
<
/th
>
<
th
class
=
"
px-2 py-2 font-semibold
"
>
{{
t
(
'
admin.ops.openaiTokenStats.table.totalOutputTokens
'
)
}}
<
/th
>
<
/tr
>
<
th
class
=
"
px-2 py-2 font-semibold
"
>
{{
t
(
'
admin.ops.openaiTokenStats.table.avgDurationMs
'
)
}}
<
/th
>
<
/thead
>
<
th
class
=
"
px-2 py-2 font-semibold
"
>
{{
t
(
'
admin.ops.openaiTokenStats.table.requestsWithFirstToken
'
)
}}
<
/th
>
<
tbody
>
<
/tr
>
<
tr
<
/thead
>
v
-
for
=
"
row in items
"
<
tbody
>
:
key
=
"
row.model
"
<
tr
class
=
"
border-b border-gray-100 text-gray-700 dark:border-dark-800 dark:text-gray-200
"
v
-
for
=
"
row in items
"
>
:
key
=
"
row.model
"
<
td
class
=
"
px-2 py-2 font-medium
"
>
{{
row
.
model
}}
<
/td
>
class
=
"
border-b border-gray-100 text-gray-700 last:border-b-0 dark:border-dark-800 dark:text-gray-200
"
<
td
class
=
"
px-2 py-2
"
>
{{
formatInt
(
row
.
request_count
)
}}
<
/td
>
>
<
td
class
=
"
px-2 py-2
"
>
{{
formatRate
(
row
.
avg_tokens_per_sec
)
}}
<
/td
>
<
td
class
=
"
px-2 py-2 font-medium
"
>
{{
row
.
model
}}
<
/td
>
<
td
class
=
"
px-2 py-2
"
>
{{
formatRate
(
row
.
avg_first_token_ms
)
}}
<
/td
>
<
td
class
=
"
px-2 py-2
"
>
{{
formatInt
(
row
.
request_count
)
}}
<
/td
>
<
td
class
=
"
px-2 py-2
"
>
{{
formatInt
(
row
.
total_output_tokens
)
}}
<
/td
>
<
td
class
=
"
px-2 py-2
"
>
{{
formatRate
(
row
.
avg_tokens_per_sec
)
}}
<
/td
>
<
td
class
=
"
px-2 py-2
"
>
{{
formatInt
(
row
.
avg_duration_ms
)
}}
<
/td
>
<
td
class
=
"
px-2 py-2
"
>
{{
formatRate
(
row
.
avg_first_token_ms
)
}}
<
/td
>
<
td
class
=
"
px-2 py-2
"
>
{{
formatInt
(
row
.
requests_with_first_token
)
}}
<
/td
>
<
td
class
=
"
px-2 py-2
"
>
{{
formatInt
(
row
.
total_output_tokens
)
}}
<
/td
>
<
/tr
>
<
td
class
=
"
px-2 py-2
"
>
{{
formatInt
(
row
.
avg_duration_ms
)
}}
<
/td
>
<
/tbody
>
<
td
class
=
"
px-2 py-2
"
>
{{
formatInt
(
row
.
requests_with_first_token
)
}}
<
/td
>
<
/table
>
<
/tr
>
<
/tbody
>
<
/table
>
<
/div
>
<
/div
>
<
div
v
-
if
=
"
viewMode === 'topn'
"
class
=
"
mt-3 text-xs text-gray-500 dark:text-gray-400
"
>
<
div
v
-
if
=
"
viewMode === 'topn'
"
class
=
"
mt-3 text-xs text-gray-500 dark:text-gray-400
"
>
{{
t
(
'
admin.ops.openaiTokenStats.totalModels
'
,
{
total
}
)
}}
{{
t
(
'
admin.ops.openaiTokenStats.totalModels
'
,
{
total
}
)
}}
<
/div
>
<
/div
>
...
...
frontend/src/views/admin/ops/components/OpsSettingsDialog.vue
View file @
af9c4a7d
...
@@ -543,6 +543,21 @@ async function saveAllSettings() {
...
@@ -543,6 +543,21 @@ async function saveAllSettings() {
/>
/>
</div>
</div>
</div>
</div>
<!-- Dashboard Cards -->
<div
class=
"space-y-3"
>
<h5
class=
"text-xs font-semibold text-gray-700 dark:text-gray-300"
>
{{
t
(
'
admin.ops.settings.dashboardCards
'
)
}}
</h5>
<div
class=
"flex items-center justify-between"
>
<div>
<label
class=
"text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{
t
(
'
admin.ops.settings.displayOpenAITokenStats
'
)
}}
</label>
<p
class=
"mt-1 text-xs text-gray-500"
>
{{
t
(
'
admin.ops.settings.displayOpenAITokenStatsHint
'
)
}}
</p>
</div>
<Toggle
v-model=
"advancedSettings.display_openai_token_stats"
/>
</div>
</div>
</div>
</div>
</details>
</details>
</div>
</div>
...
...
frontend/src/views/admin/ops/components/__tests__/OpsOpenAITokenStatsCard.spec.ts
View file @
af9c4a7d
...
@@ -196,6 +196,23 @@ describe('OpsOpenAITokenStatsCard', () => {
...
@@ -196,6 +196,23 @@ describe('OpsOpenAITokenStatsCard', () => {
expect
(
wrapper
.
find
(
'
.empty-state
'
).
exists
()).
toBe
(
true
)
expect
(
wrapper
.
find
(
'
.empty-state
'
).
exists
()).
toBe
(
true
)
})
})
it
(
'
数据表使用固定高度滚动容器,避免纵向无限增长
'
,
async
()
=>
{
mockGetOpenAITokenStats
.
mockResolvedValue
(
sampleResponse
)
const
wrapper
=
mount
(
OpsOpenAITokenStatsCard
,
{
props
:
{
refreshToken
:
0
},
global
:
{
stubs
:
{
Select
:
SelectStub
,
EmptyState
:
EmptyStateStub
,
},
},
})
await
flushPromises
()
expect
(
wrapper
.
find
(
'
.max-h-
\\
[420px
\\
]
'
).
exists
()).
toBe
(
true
)
})
it
(
'
接口异常时显示错误提示
'
,
async
()
=>
{
it
(
'
接口异常时显示错误提示
'
,
async
()
=>
{
mockGetOpenAITokenStats
.
mockRejectedValue
(
new
Error
(
'
加载失败
'
))
mockGetOpenAITokenStats
.
mockRejectedValue
(
new
Error
(
'
加载失败
'
))
...
...
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