feat(计费): 支持账号计费倍率快照与统计展示
- 新增 accounts.rate_multiplier(默认 1.0,允许 0) - 使用 usage_logs.account_rate_multiplier 记录倍率快照,避免历史回算 - 统计/导出/管理端展示账号口径费用(total_cost * account_rate_multiplier)
This commit is contained in:
@@ -16,6 +16,7 @@ export interface AdminUsageStatsResponse {
|
||||
total_tokens: number
|
||||
total_cost: number
|
||||
total_actual_cost: number
|
||||
total_account_cost?: number
|
||||
average_duration_ms: number
|
||||
}
|
||||
|
||||
|
||||
@@ -73,11 +73,12 @@
|
||||
</p>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.accounts.stats.accumulatedCost') }}
|
||||
<span class="text-gray-400 dark:text-gray-500"
|
||||
>({{ t('admin.accounts.stats.standardCost') }}: ${{
|
||||
<span class="text-gray-400 dark:text-gray-500">
|
||||
({{ t('usage.userBilled') }}: ${{ formatCost(stats.summary.total_user_cost) }} ·
|
||||
{{ t('admin.accounts.stats.standardCost') }}: ${{
|
||||
formatCost(stats.summary.total_standard_cost)
|
||||
}})</span
|
||||
>
|
||||
}})
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -121,12 +122,15 @@
|
||||
<p class="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
${{ formatCost(stats.summary.avg_daily_cost) }}
|
||||
</p>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{
|
||||
t('admin.accounts.stats.basedOnActualDays', {
|
||||
days: stats.summary.actual_days_used
|
||||
})
|
||||
}}
|
||||
<span class="text-gray-400 dark:text-gray-500">
|
||||
({{ t('usage.userBilled') }}: ${{ formatCost(stats.summary.avg_daily_user_cost) }})
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -189,13 +193,17 @@
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{
|
||||
t('admin.accounts.stats.cost')
|
||||
}}</span>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('usage.accountBilled') }}</span>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white"
|
||||
>${{ formatCost(stats.summary.today?.cost || 0) }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('usage.userBilled') }}</span>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white"
|
||||
>${{ formatCost(stats.summary.today?.user_cost || 0) }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{
|
||||
t('admin.accounts.stats.requests')
|
||||
@@ -240,13 +248,17 @@
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{
|
||||
t('admin.accounts.stats.cost')
|
||||
}}</span>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('usage.accountBilled') }}</span>
|
||||
<span class="text-sm font-semibold text-orange-600 dark:text-orange-400"
|
||||
>${{ formatCost(stats.summary.highest_cost_day?.cost || 0) }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('usage.userBilled') }}</span>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white"
|
||||
>${{ formatCost(stats.summary.highest_cost_day?.user_cost || 0) }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{
|
||||
t('admin.accounts.stats.requests')
|
||||
@@ -291,13 +303,17 @@
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{
|
||||
t('admin.accounts.stats.cost')
|
||||
}}</span>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('usage.accountBilled') }}</span>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white"
|
||||
>${{ formatCost(stats.summary.highest_request_day?.cost || 0) }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('usage.userBilled') }}</span>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white"
|
||||
>${{ formatCost(stats.summary.highest_request_day?.user_cost || 0) }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -397,13 +413,17 @@
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{
|
||||
t('admin.accounts.stats.todayCost')
|
||||
}}</span>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('usage.accountBilled') }}</span>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white"
|
||||
>${{ formatCost(stats.summary.today?.cost || 0) }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('usage.userBilled') }}</span>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white"
|
||||
>${{ formatCost(stats.summary.today?.user_cost || 0) }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -517,14 +537,24 @@ const trendChartData = computed(() => {
|
||||
labels: stats.value.history.map((h) => h.label),
|
||||
datasets: [
|
||||
{
|
||||
label: t('admin.accounts.stats.cost') + ' (USD)',
|
||||
data: stats.value.history.map((h) => h.cost),
|
||||
label: t('usage.accountBilled') + ' (USD)',
|
||||
data: stats.value.history.map((h) => h.actual_cost),
|
||||
borderColor: '#3b82f6',
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
||||
fill: true,
|
||||
tension: 0.3,
|
||||
yAxisID: 'y'
|
||||
},
|
||||
{
|
||||
label: t('usage.userBilled') + ' (USD)',
|
||||
data: stats.value.history.map((h) => h.user_cost),
|
||||
borderColor: '#10b981',
|
||||
backgroundColor: 'rgba(16, 185, 129, 0.08)',
|
||||
fill: false,
|
||||
tension: 0.3,
|
||||
borderDash: [5, 5],
|
||||
yAxisID: 'y'
|
||||
},
|
||||
{
|
||||
label: t('admin.accounts.stats.requests'),
|
||||
data: stats.value.history.map((h) => h.requests),
|
||||
@@ -602,7 +632,7 @@ const lineChartOptions = computed(() => ({
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: t('admin.accounts.stats.cost') + ' (USD)',
|
||||
text: t('usage.accountBilled') + ' (USD)',
|
||||
color: '#3b82f6',
|
||||
font: {
|
||||
size: 11
|
||||
|
||||
@@ -32,15 +32,20 @@
|
||||
formatTokens(stats.tokens)
|
||||
}}</span>
|
||||
</div>
|
||||
<!-- Cost -->
|
||||
<!-- Cost (Account) -->
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="text-gray-500 dark:text-gray-400"
|
||||
>{{ t('admin.accounts.stats.cost') }}:</span
|
||||
>
|
||||
<span class="text-gray-500 dark:text-gray-400">{{ t('usage.accountBilled') }}:</span>
|
||||
<span class="font-medium text-emerald-600 dark:text-emerald-400">{{
|
||||
formatCurrency(stats.cost)
|
||||
}}</span>
|
||||
</div>
|
||||
<!-- Cost (User/API Key) -->
|
||||
<div v-if="stats.user_cost != null" class="flex items-center gap-1">
|
||||
<span class="text-gray-500 dark:text-gray-400">{{ t('usage.userBilled') }}:</span>
|
||||
<span class="font-medium text-gray-700 dark:text-gray-300">{{
|
||||
formatCurrency(stats.user_cost)
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- No data -->
|
||||
|
||||
@@ -459,7 +459,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Concurrency & Priority -->
|
||||
<div class="grid grid-cols-2 gap-4 border-t border-gray-200 pt-4 dark:border-dark-600">
|
||||
<div class="grid grid-cols-2 gap-4 border-t border-gray-200 pt-4 dark:border-dark-600 lg:grid-cols-3">
|
||||
<div>
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<label
|
||||
@@ -516,6 +516,36 @@
|
||||
aria-labelledby="bulk-edit-priority-label"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<label
|
||||
id="bulk-edit-rate-multiplier-label"
|
||||
class="input-label mb-0"
|
||||
for="bulk-edit-rate-multiplier-enabled"
|
||||
>
|
||||
{{ t('admin.accounts.billingRateMultiplier') }}
|
||||
</label>
|
||||
<input
|
||||
v-model="enableRateMultiplier"
|
||||
id="bulk-edit-rate-multiplier-enabled"
|
||||
type="checkbox"
|
||||
aria-controls="bulk-edit-rate-multiplier"
|
||||
class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
v-model.number="rateMultiplier"
|
||||
id="bulk-edit-rate-multiplier"
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
:disabled="!enableRateMultiplier"
|
||||
class="input"
|
||||
:class="!enableRateMultiplier && 'cursor-not-allowed opacity-50'"
|
||||
aria-labelledby="bulk-edit-rate-multiplier-label"
|
||||
/>
|
||||
<p class="input-hint">{{ t('admin.accounts.billingRateMultiplierHint') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
@@ -655,6 +685,7 @@ const enableInterceptWarmup = ref(false)
|
||||
const enableProxy = ref(false)
|
||||
const enableConcurrency = ref(false)
|
||||
const enablePriority = ref(false)
|
||||
const enableRateMultiplier = ref(false)
|
||||
const enableStatus = ref(false)
|
||||
const enableGroups = ref(false)
|
||||
|
||||
@@ -670,6 +701,7 @@ const interceptWarmupRequests = ref(false)
|
||||
const proxyId = ref<number | null>(null)
|
||||
const concurrency = ref(1)
|
||||
const priority = ref(1)
|
||||
const rateMultiplier = ref(1)
|
||||
const status = ref<'active' | 'inactive'>('active')
|
||||
const groupIds = ref<number[]>([])
|
||||
|
||||
@@ -863,6 +895,10 @@ const buildUpdatePayload = (): Record<string, unknown> | null => {
|
||||
updates.priority = priority.value
|
||||
}
|
||||
|
||||
if (enableRateMultiplier.value) {
|
||||
updates.rate_multiplier = rateMultiplier.value
|
||||
}
|
||||
|
||||
if (enableStatus.value) {
|
||||
updates.status = status.value
|
||||
}
|
||||
@@ -923,6 +959,7 @@ const handleSubmit = async () => {
|
||||
enableProxy.value ||
|
||||
enableConcurrency.value ||
|
||||
enablePriority.value ||
|
||||
enableRateMultiplier.value ||
|
||||
enableStatus.value ||
|
||||
enableGroups.value
|
||||
|
||||
@@ -977,6 +1014,7 @@ watch(
|
||||
enableProxy.value = false
|
||||
enableConcurrency.value = false
|
||||
enablePriority.value = false
|
||||
enableRateMultiplier.value = false
|
||||
enableStatus.value = false
|
||||
enableGroups.value = false
|
||||
|
||||
@@ -991,6 +1029,7 @@ watch(
|
||||
proxyId.value = null
|
||||
concurrency.value = 1
|
||||
priority.value = 1
|
||||
rateMultiplier.value = 1
|
||||
status.value = 'active'
|
||||
groupIds.value = []
|
||||
}
|
||||
|
||||
@@ -1196,7 +1196,7 @@
|
||||
<ProxySelector v-model="form.proxy_id" :proxies="proxies" />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid grid-cols-2 gap-4 lg:grid-cols-3">
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.accounts.concurrency') }}</label>
|
||||
<input v-model.number="form.concurrency" type="number" min="1" class="input" />
|
||||
@@ -1212,6 +1212,11 @@
|
||||
/>
|
||||
<p class="input-hint">{{ t('admin.accounts.priorityHint') }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.accounts.billingRateMultiplier') }}</label>
|
||||
<input v-model.number="form.rate_multiplier" type="number" min="0" step="0.01" class="input" />
|
||||
<p class="input-hint">{{ t('admin.accounts.billingRateMultiplierHint') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
||||
<label class="input-label">{{ t('admin.accounts.expiresAt') }}</label>
|
||||
@@ -1832,6 +1837,7 @@ const form = reactive({
|
||||
proxy_id: null as number | null,
|
||||
concurrency: 10,
|
||||
priority: 1,
|
||||
rate_multiplier: 1,
|
||||
group_ids: [] as number[],
|
||||
expires_at: null as number | null
|
||||
})
|
||||
@@ -2119,6 +2125,7 @@ const resetForm = () => {
|
||||
form.proxy_id = null
|
||||
form.concurrency = 10
|
||||
form.priority = 1
|
||||
form.rate_multiplier = 1
|
||||
form.group_ids = []
|
||||
form.expires_at = null
|
||||
accountCategory.value = 'oauth-based'
|
||||
@@ -2272,6 +2279,7 @@ const createAccountAndFinish = async (
|
||||
proxy_id: form.proxy_id,
|
||||
concurrency: form.concurrency,
|
||||
priority: form.priority,
|
||||
rate_multiplier: form.rate_multiplier,
|
||||
group_ids: form.group_ids,
|
||||
expires_at: form.expires_at,
|
||||
auto_pause_on_expired: autoPauseOnExpired.value
|
||||
@@ -2490,6 +2498,7 @@ const handleCookieAuth = async (sessionKey: string) => {
|
||||
proxy_id: form.proxy_id,
|
||||
concurrency: form.concurrency,
|
||||
priority: form.priority,
|
||||
rate_multiplier: form.rate_multiplier,
|
||||
group_ids: form.group_ids,
|
||||
expires_at: form.expires_at,
|
||||
auto_pause_on_expired: autoPauseOnExpired.value
|
||||
|
||||
@@ -549,7 +549,7 @@
|
||||
<ProxySelector v-model="form.proxy_id" :proxies="proxies" />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid grid-cols-2 gap-4 lg:grid-cols-3">
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.accounts.concurrency') }}</label>
|
||||
<input v-model.number="form.concurrency" type="number" min="1" class="input" />
|
||||
@@ -564,6 +564,11 @@
|
||||
data-tour="account-form-priority"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.accounts.billingRateMultiplier') }}</label>
|
||||
<input v-model.number="form.rate_multiplier" type="number" min="0" step="0.01" class="input" />
|
||||
<p class="input-hint">{{ t('admin.accounts.billingRateMultiplierHint') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
||||
<label class="input-label">{{ t('admin.accounts.expiresAt') }}</label>
|
||||
@@ -807,6 +812,7 @@ const form = reactive({
|
||||
proxy_id: null as number | null,
|
||||
concurrency: 1,
|
||||
priority: 1,
|
||||
rate_multiplier: 1,
|
||||
status: 'active' as 'active' | 'inactive',
|
||||
group_ids: [] as number[],
|
||||
expires_at: null as number | null
|
||||
@@ -834,6 +840,7 @@ watch(
|
||||
form.proxy_id = newAccount.proxy_id
|
||||
form.concurrency = newAccount.concurrency
|
||||
form.priority = newAccount.priority
|
||||
form.rate_multiplier = newAccount.rate_multiplier ?? 1
|
||||
form.status = newAccount.status as 'active' | 'inactive'
|
||||
form.group_ids = newAccount.group_ids || []
|
||||
form.expires_at = newAccount.expires_at ?? null
|
||||
|
||||
@@ -15,7 +15,13 @@
|
||||
<span class="rounded bg-gray-100 px-1.5 py-0.5 dark:bg-gray-800">
|
||||
{{ formatTokens }}
|
||||
</span>
|
||||
<span class="rounded bg-gray-100 px-1.5 py-0.5 dark:bg-gray-800"> ${{ formatCost }} </span>
|
||||
<span class="rounded bg-gray-100 px-1.5 py-0.5 dark:bg-gray-800"> A ${{ formatAccountCost }} </span>
|
||||
<span
|
||||
v-if="windowStats?.user_cost != null"
|
||||
class="rounded bg-gray-100 px-1.5 py-0.5 dark:bg-gray-800"
|
||||
>
|
||||
U ${{ formatUserCost }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -149,8 +155,13 @@ const formatTokens = computed(() => {
|
||||
return t.toString()
|
||||
})
|
||||
|
||||
const formatCost = computed(() => {
|
||||
const formatAccountCost = computed(() => {
|
||||
if (!props.windowStats) return '0.00'
|
||||
return props.windowStats.cost.toFixed(2)
|
||||
})
|
||||
|
||||
const formatUserCost = computed(() => {
|
||||
if (!props.windowStats || props.windowStats.user_cost == null) return '0.00'
|
||||
return props.windowStats.user_cost.toFixed(2)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -61,11 +61,12 @@
|
||||
</p>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.accounts.stats.accumulatedCost') }}
|
||||
<span class="text-gray-400 dark:text-gray-500"
|
||||
>({{ t('admin.accounts.stats.standardCost') }}: ${{
|
||||
<span class="text-gray-400 dark:text-gray-500">
|
||||
({{ t('usage.userBilled') }}: ${{ formatCost(stats.summary.total_user_cost) }} ·
|
||||
{{ t('admin.accounts.stats.standardCost') }}: ${{
|
||||
formatCost(stats.summary.total_standard_cost)
|
||||
}})</span
|
||||
>
|
||||
}})
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -108,12 +109,15 @@
|
||||
<p class="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
${{ formatCost(stats.summary.avg_daily_cost) }}
|
||||
</p>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{
|
||||
t('admin.accounts.stats.basedOnActualDays', {
|
||||
days: stats.summary.actual_days_used
|
||||
})
|
||||
}}
|
||||
<span class="text-gray-400 dark:text-gray-500">
|
||||
({{ t('usage.userBilled') }}: ${{ formatCost(stats.summary.avg_daily_user_cost) }})
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -164,13 +168,17 @@
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{
|
||||
t('admin.accounts.stats.cost')
|
||||
}}</span>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('usage.accountBilled') }}</span>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white"
|
||||
>${{ formatCost(stats.summary.today?.cost || 0) }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('usage.userBilled') }}</span>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white"
|
||||
>${{ formatCost(stats.summary.today?.user_cost || 0) }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{
|
||||
t('admin.accounts.stats.requests')
|
||||
@@ -210,13 +218,17 @@
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{
|
||||
t('admin.accounts.stats.cost')
|
||||
}}</span>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('usage.accountBilled') }}</span>
|
||||
<span class="text-sm font-semibold text-orange-600 dark:text-orange-400"
|
||||
>${{ formatCost(stats.summary.highest_cost_day?.cost || 0) }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('usage.userBilled') }}</span>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white"
|
||||
>${{ formatCost(stats.summary.highest_cost_day?.user_cost || 0) }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{
|
||||
t('admin.accounts.stats.requests')
|
||||
@@ -260,13 +272,17 @@
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{
|
||||
t('admin.accounts.stats.cost')
|
||||
}}</span>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('usage.accountBilled') }}</span>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white"
|
||||
>${{ formatCost(stats.summary.highest_request_day?.cost || 0) }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('usage.userBilled') }}</span>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white"
|
||||
>${{ formatCost(stats.summary.highest_request_day?.user_cost || 0) }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -485,14 +501,24 @@ const trendChartData = computed(() => {
|
||||
labels: stats.value.history.map((h) => h.label),
|
||||
datasets: [
|
||||
{
|
||||
label: t('admin.accounts.stats.cost') + ' (USD)',
|
||||
data: stats.value.history.map((h) => h.cost),
|
||||
label: t('usage.accountBilled') + ' (USD)',
|
||||
data: stats.value.history.map((h) => h.actual_cost),
|
||||
borderColor: '#3b82f6',
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
||||
fill: true,
|
||||
tension: 0.3,
|
||||
yAxisID: 'y'
|
||||
},
|
||||
{
|
||||
label: t('usage.userBilled') + ' (USD)',
|
||||
data: stats.value.history.map((h) => h.user_cost),
|
||||
borderColor: '#10b981',
|
||||
backgroundColor: 'rgba(16, 185, 129, 0.08)',
|
||||
fill: false,
|
||||
tension: 0.3,
|
||||
borderDash: [5, 5],
|
||||
yAxisID: 'y'
|
||||
},
|
||||
{
|
||||
label: t('admin.accounts.stats.requests'),
|
||||
data: stats.value.history.map((h) => h.requests),
|
||||
@@ -570,7 +596,7 @@ const lineChartOptions = computed(() => ({
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: t('admin.accounts.stats.cost') + ' (USD)',
|
||||
text: t('usage.accountBilled') + ' (USD)',
|
||||
color: '#3b82f6',
|
||||
font: {
|
||||
size: 11
|
||||
|
||||
@@ -27,9 +27,18 @@
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-xs font-medium text-gray-500">{{ t('usage.totalCost') }}</p>
|
||||
<p class="text-xl font-bold text-green-600">${{ (stats?.total_actual_cost || 0).toFixed(4) }}</p>
|
||||
<p class="text-xs text-gray-400">
|
||||
{{ t('usage.standardCost') }}: <span class="line-through">${{ (stats?.total_cost || 0).toFixed(4) }}</span>
|
||||
<p class="text-xl font-bold text-green-600">
|
||||
${{ ((stats?.total_account_cost ?? stats?.total_actual_cost) || 0).toFixed(4) }}
|
||||
</p>
|
||||
<p class="text-xs text-gray-400" v-if="stats?.total_account_cost != null">
|
||||
{{ t('usage.userBilled') }}:
|
||||
<span class="text-gray-300">${{ (stats?.total_actual_cost || 0).toFixed(4) }}</span>
|
||||
· {{ t('usage.standardCost') }}:
|
||||
<span class="text-gray-300">${{ (stats?.total_cost || 0).toFixed(4) }}</span>
|
||||
</p>
|
||||
<p class="text-xs text-gray-400" v-else>
|
||||
{{ t('usage.standardCost') }}:
|
||||
<span class="line-through">${{ (stats?.total_cost || 0).toFixed(4) }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -81,18 +81,23 @@
|
||||
</template>
|
||||
|
||||
<template #cell-cost="{ row }">
|
||||
<div class="flex items-center gap-1.5 text-sm">
|
||||
<span class="font-medium text-green-600 dark:text-green-400">${{ row.actual_cost?.toFixed(6) || '0.000000' }}</span>
|
||||
<!-- Cost Detail Tooltip -->
|
||||
<div
|
||||
class="group relative"
|
||||
@mouseenter="showTooltip($event, row)"
|
||||
@mouseleave="hideTooltip"
|
||||
>
|
||||
<div class="flex h-4 w-4 cursor-help items-center justify-center rounded-full bg-gray-100 transition-colors group-hover:bg-blue-100 dark:bg-gray-700 dark:group-hover:bg-blue-900/50">
|
||||
<Icon name="infoCircle" size="xs" class="text-gray-400 group-hover:text-blue-500 dark:text-gray-500 dark:group-hover:text-blue-400" />
|
||||
<div class="text-sm">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span class="font-medium text-green-600 dark:text-green-400">${{ row.actual_cost?.toFixed(6) || '0.000000' }}</span>
|
||||
<!-- Cost Detail Tooltip -->
|
||||
<div
|
||||
class="group relative"
|
||||
@mouseenter="showTooltip($event, row)"
|
||||
@mouseleave="hideTooltip"
|
||||
>
|
||||
<div class="flex h-4 w-4 cursor-help items-center justify-center rounded-full bg-gray-100 transition-colors group-hover:bg-blue-100 dark:bg-gray-700 dark:group-hover:bg-blue-900/50">
|
||||
<Icon name="infoCircle" size="xs" class="text-gray-400 group-hover:text-blue-500 dark:text-gray-500 dark:group-hover:text-blue-400" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="row.account_rate_multiplier != null" class="mt-0.5 text-[11px] text-gray-400">
|
||||
A ${{ (row.total_cost * row.account_rate_multiplier).toFixed(6) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -202,14 +207,24 @@
|
||||
<span class="text-gray-400">{{ t('usage.rate') }}</span>
|
||||
<span class="font-semibold text-blue-400">{{ (tooltipData?.rate_multiplier || 1).toFixed(2) }}x</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-6">
|
||||
<span class="text-gray-400">{{ t('usage.accountMultiplier') }}</span>
|
||||
<span class="font-semibold text-blue-400">{{ (tooltipData?.account_rate_multiplier ?? 1).toFixed(2) }}x</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-6">
|
||||
<span class="text-gray-400">{{ t('usage.original') }}</span>
|
||||
<span class="font-medium text-white">${{ tooltipData?.total_cost?.toFixed(6) || '0.000000' }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-6 border-t border-gray-700 pt-1.5">
|
||||
<span class="text-gray-400">{{ t('usage.billed') }}</span>
|
||||
<div class="flex items-center justify-between gap-6">
|
||||
<span class="text-gray-400">{{ t('usage.userBilled') }}</span>
|
||||
<span class="font-semibold text-green-400">${{ tooltipData?.actual_cost?.toFixed(6) || '0.000000' }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-6 border-t border-gray-700 pt-1.5">
|
||||
<span class="text-gray-400">{{ t('usage.accountBilled') }}</span>
|
||||
<span class="font-semibold text-green-400">
|
||||
${{ (((tooltipData?.total_cost || 0) * (tooltipData?.account_rate_multiplier ?? 1)) || 0).toFixed(6) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="absolute right-full top-1/2 h-0 w-0 -translate-y-1/2 border-b-[6px] border-r-[6px] border-t-[6px] border-b-transparent border-r-gray-900 border-t-transparent dark:border-r-gray-800"></div>
|
||||
</div>
|
||||
|
||||
@@ -429,6 +429,9 @@ export default {
|
||||
totalCost: 'Total Cost',
|
||||
standardCost: 'Standard',
|
||||
actualCost: 'Actual',
|
||||
userBilled: 'User billed',
|
||||
accountBilled: 'Account billed',
|
||||
accountMultiplier: 'Account rate',
|
||||
avgDuration: 'Avg Duration',
|
||||
inSelectedRange: 'in selected range',
|
||||
perRequest: 'per request',
|
||||
@@ -1058,6 +1061,7 @@ export default {
|
||||
concurrencyStatus: 'Concurrency',
|
||||
notes: 'Notes',
|
||||
priority: 'Priority',
|
||||
billingRateMultiplier: 'Billing Rate',
|
||||
weight: 'Weight',
|
||||
status: 'Status',
|
||||
schedulable: 'Schedulable',
|
||||
@@ -1225,6 +1229,8 @@ export default {
|
||||
concurrency: 'Concurrency',
|
||||
priority: 'Priority',
|
||||
priorityHint: 'Lower value accounts are used first',
|
||||
billingRateMultiplier: 'Billing Rate Multiplier',
|
||||
billingRateMultiplierHint: '>=0, 0 means free. Affects account billing only',
|
||||
expiresAt: 'Expires At',
|
||||
expiresAtHint: 'Leave empty for no expiration',
|
||||
higherPriorityFirst: 'Lower value means higher priority',
|
||||
|
||||
@@ -426,6 +426,9 @@ export default {
|
||||
totalCost: '总消费',
|
||||
standardCost: '标准',
|
||||
actualCost: '实际',
|
||||
userBilled: '用户扣费',
|
||||
accountBilled: '账号计费',
|
||||
accountMultiplier: '账号倍率',
|
||||
avgDuration: '平均耗时',
|
||||
inSelectedRange: '所选范围内',
|
||||
perRequest: '每次请求',
|
||||
@@ -1108,6 +1111,7 @@ export default {
|
||||
concurrencyStatus: '并发',
|
||||
notes: '备注',
|
||||
priority: '优先级',
|
||||
billingRateMultiplier: '账号倍率',
|
||||
weight: '权重',
|
||||
status: '状态',
|
||||
schedulable: '调度',
|
||||
@@ -1359,6 +1363,8 @@ export default {
|
||||
concurrency: '并发数',
|
||||
priority: '优先级',
|
||||
priorityHint: '优先级越小的账号优先使用',
|
||||
billingRateMultiplier: '账号计费倍率',
|
||||
billingRateMultiplierHint: '>=0,0 表示该账号计费为 0;仅影响账号计费口径',
|
||||
expiresAt: '过期时间',
|
||||
expiresAtHint: '留空表示不过期',
|
||||
higherPriorityFirst: '数值越小优先级越高',
|
||||
|
||||
@@ -428,6 +428,7 @@ export interface Account {
|
||||
concurrency: number
|
||||
current_concurrency?: number // Real-time concurrency count from Redis
|
||||
priority: number
|
||||
rate_multiplier?: number // Account billing multiplier (>=0, 0 means free)
|
||||
status: 'active' | 'inactive' | 'error'
|
||||
error_message: string | null
|
||||
last_used_at: string | null
|
||||
@@ -457,7 +458,9 @@ export interface Account {
|
||||
export interface WindowStats {
|
||||
requests: number
|
||||
tokens: number
|
||||
cost: number
|
||||
cost: number // Account cost (account multiplier)
|
||||
standard_cost?: number
|
||||
user_cost?: number
|
||||
}
|
||||
|
||||
export interface UsageProgress {
|
||||
@@ -522,6 +525,7 @@ export interface CreateAccountRequest {
|
||||
proxy_id?: number | null
|
||||
concurrency?: number
|
||||
priority?: number
|
||||
rate_multiplier?: number // Account billing multiplier (>=0, 0 means free)
|
||||
group_ids?: number[]
|
||||
expires_at?: number | null
|
||||
auto_pause_on_expired?: boolean
|
||||
@@ -537,6 +541,7 @@ export interface UpdateAccountRequest {
|
||||
proxy_id?: number | null
|
||||
concurrency?: number
|
||||
priority?: number
|
||||
rate_multiplier?: number // Account billing multiplier (>=0, 0 means free)
|
||||
schedulable?: boolean
|
||||
status?: 'active' | 'inactive'
|
||||
group_ids?: number[]
|
||||
@@ -593,6 +598,7 @@ export interface UsageLog {
|
||||
total_cost: number
|
||||
actual_cost: number
|
||||
rate_multiplier: number
|
||||
account_rate_multiplier?: number | null
|
||||
|
||||
stream: boolean
|
||||
duration_ms: number
|
||||
@@ -852,23 +858,27 @@ export interface AccountUsageHistory {
|
||||
requests: number
|
||||
tokens: number
|
||||
cost: number
|
||||
actual_cost: number
|
||||
actual_cost: number // Account cost (account multiplier)
|
||||
user_cost: number // User/API key billed cost (group multiplier)
|
||||
}
|
||||
|
||||
export interface AccountUsageSummary {
|
||||
days: number
|
||||
actual_days_used: number
|
||||
total_cost: number
|
||||
total_cost: number // Account cost (account multiplier)
|
||||
total_user_cost: number
|
||||
total_standard_cost: number
|
||||
total_requests: number
|
||||
total_tokens: number
|
||||
avg_daily_cost: number
|
||||
avg_daily_cost: number // Account cost
|
||||
avg_daily_user_cost: number
|
||||
avg_daily_requests: number
|
||||
avg_daily_tokens: number
|
||||
avg_duration_ms: number
|
||||
today: {
|
||||
date: string
|
||||
cost: number
|
||||
user_cost: number
|
||||
requests: number
|
||||
tokens: number
|
||||
} | null
|
||||
@@ -876,6 +886,7 @@ export interface AccountUsageSummary {
|
||||
date: string
|
||||
label: string
|
||||
cost: number
|
||||
user_cost: number
|
||||
requests: number
|
||||
} | null
|
||||
highest_request_day: {
|
||||
@@ -883,6 +894,7 @@ export interface AccountUsageSummary {
|
||||
label: string
|
||||
requests: number
|
||||
cost: number
|
||||
user_cost: number
|
||||
} | null
|
||||
}
|
||||
|
||||
|
||||
@@ -61,6 +61,11 @@
|
||||
<template #cell-usage="{ row }">
|
||||
<AccountUsageCell :account="row" />
|
||||
</template>
|
||||
<template #cell-rate_multiplier="{ row }">
|
||||
<span class="text-sm font-mono text-gray-700 dark:text-gray-300">
|
||||
{{ (row.rate_multiplier ?? 1).toFixed(2) }}x
|
||||
</span>
|
||||
</template>
|
||||
<template #cell-priority="{ value }">
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">{{ value }}</span>
|
||||
</template>
|
||||
@@ -190,10 +195,11 @@ const cols = computed(() => {
|
||||
if (!authStore.isSimpleMode) {
|
||||
c.push({ key: 'groups', label: t('admin.accounts.columns.groups'), sortable: false })
|
||||
}
|
||||
c.push(
|
||||
{ key: 'usage', label: t('admin.accounts.columns.usageWindows'), sortable: false },
|
||||
{ key: 'priority', label: t('admin.accounts.columns.priority'), sortable: true },
|
||||
{ key: 'last_used_at', label: t('admin.accounts.columns.lastUsed'), sortable: true },
|
||||
c.push(
|
||||
{ key: 'usage', label: t('admin.accounts.columns.usageWindows'), sortable: false },
|
||||
{ key: 'priority', label: t('admin.accounts.columns.priority'), sortable: true },
|
||||
{ key: 'rate_multiplier', label: t('admin.accounts.columns.billingRateMultiplier'), sortable: true },
|
||||
{ key: 'last_used_at', label: t('admin.accounts.columns.lastUsed'), sortable: true },
|
||||
{ key: 'expires_at', label: t('admin.accounts.columns.expiresAt'), sortable: true },
|
||||
{ key: 'notes', label: t('admin.accounts.columns.notes'), sortable: false },
|
||||
{ key: 'actions', label: t('admin.accounts.columns.actions'), sortable: false }
|
||||
|
||||
@@ -94,7 +94,7 @@ const exportToExcel = async () => {
|
||||
t('admin.usage.cacheReadTokens'), t('admin.usage.cacheCreationTokens'),
|
||||
t('admin.usage.inputCost'), t('admin.usage.outputCost'),
|
||||
t('admin.usage.cacheReadCost'), t('admin.usage.cacheCreationCost'),
|
||||
t('usage.rate'), t('usage.original'), t('usage.billed'),
|
||||
t('usage.rate'), t('usage.accountMultiplier'), t('usage.original'), t('usage.userBilled'), t('usage.accountBilled'),
|
||||
t('usage.firstToken'), t('usage.duration'),
|
||||
t('admin.usage.requestId'), t('usage.userAgent'), t('admin.usage.ipAddress')
|
||||
]
|
||||
@@ -115,8 +115,10 @@ const exportToExcel = async () => {
|
||||
log.cache_read_cost?.toFixed(6) || '0.000000',
|
||||
log.cache_creation_cost?.toFixed(6) || '0.000000',
|
||||
log.rate_multiplier?.toFixed(2) || '1.00',
|
||||
(log.account_rate_multiplier ?? 1).toFixed(2),
|
||||
log.total_cost?.toFixed(6) || '0.000000',
|
||||
log.actual_cost?.toFixed(6) || '0.000000',
|
||||
(log.total_cost * (log.account_rate_multiplier ?? 1)).toFixed(6),
|
||||
log.first_token_ms ?? '',
|
||||
log.duration_ms,
|
||||
log.request_id || '',
|
||||
|
||||
Reference in New Issue
Block a user