Commit fb99ceac authored by 墨颜's avatar 墨颜
Browse files

feat(计费): 支持账号计费倍率快照与统计展示

- 新增 accounts.rate_multiplier(默认 1.0,允许 0)
- 使用 usage_logs.account_rate_multiplier 记录倍率快照,避免历史回算
- 统计/导出/管理端展示账号口径费用(total_cost * account_rate_multiplier)
parent 55fced39
......@@ -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>
......@@ -114,6 +115,9 @@
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,7 +81,8 @@
</template>
<template #cell-cost="{ row }">
<div class="flex items-center gap-1.5 text-sm">
<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
......@@ -94,6 +95,10 @@
</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>
<template #cell-first_token="{ row }">
......@@ -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>
......@@ -193,6 +198,7 @@ const cols = computed(() => {
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 },
......
......@@ -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 || '',
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment