diff --git a/backend/check_billing_logs.py b/backend/check_billing_logs.py new file mode 100644 index 0000000000..a3ce4cc17a --- /dev/null +++ b/backend/check_billing_logs.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +"""检查最近的计费记录,诊断重复扣费问题""" + +import sys +import os + +# 添加项目路径 +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "open_webui")) + +from internal.db import get_db +from models.billing import BillingLog +from sqlalchemy import desc + +def check_recent_logs(limit=10): + """查询最近的计费记录""" + with get_db() as db: + logs = ( + db.query(BillingLog) + .order_by(desc(BillingLog.created_at)) + .limit(limit) + .all() + ) + + print(f"最近 {len(logs)} 条计费记录:\n") + print("=" * 150) + + for i, log in enumerate(logs, 1): + # 转换时间戳(纳秒 → 秒) + import datetime + created_time = datetime.datetime.fromtimestamp(log.created_at / 1000000000) + + print(f"{i}. ID: {log.id[:8]}...") + print(f" 用户: {log.user_id}") + print(f" 模型: {log.model_id}") + print(f" 类型: {log.log_type}") + print(f" 状态: {log.status}") + print(f" Tokens: {log.prompt_tokens}+{log.completion_tokens}") + print(f" 费用: {log.total_cost / 10000:.4f} 元") + print(f" 余额: {log.balance_after / 10000:.4f} 元" if log.balance_after else " 余额: None") + print(f" 预扣费ID: {log.precharge_id[:8] if log.precharge_id else 'None'}...") + print(f" 时间: {created_time}") + print("-" * 150) + +if __name__ == "__main__": + check_recent_logs(15) diff --git a/backend/open_webui/utils/billing.py b/backend/open_webui/utils/billing.py index 97ac179211..e0601f44dd 100644 --- a/backend/open_webui/utils/billing.py +++ b/backend/open_webui/utils/billing.py @@ -120,11 +120,23 @@ def calculate_cost( # 2. 计算费用(毫) # 公式: (tokens * price_per_million) / 1000000 - input_cost = (prompt_tokens * input_price) // 1000000 - output_cost = (completion_tokens * output_price) // 1000000 - total_cost = input_cost + output_cost + # 使用整数计算,先相加再向上取整,避免浮点数精度问题和重复向上取整 - return int(total_cost) + # 计算原始费用(未除以 1000000) + input_cost_raw = prompt_tokens * input_price + output_cost_raw = completion_tokens * output_price + total_cost_raw = input_cost_raw + output_cost_raw + + # 如果有 token 消耗 + if total_cost_raw > 0: + # 向上取整: ceil(a/b) = (a + b - 1) // b + total_cost = (total_cost_raw + 999999) // 1000000 + # 如果计算结果 < 1 毫,至少扣 1 毫 + total_cost = max(1, total_cost) + else: + total_cost = 0 + + return total_cost def deduct_balance( diff --git a/src/lib/components/admin/Settings/Models.svelte b/src/lib/components/admin/Settings/Models.svelte index d8e1d60080..c735b9a44d 100644 --- a/src/lib/components/admin/Settings/Models.svelte +++ b/src/lib/components/admin/Settings/Models.svelte @@ -15,6 +15,7 @@ updateModelById, importModels } from '$lib/apis/models'; + import { listModelPricing } from '$lib/apis/billing'; import { copyToClipboard } from '$lib/utils'; import { page } from '$app/stores'; @@ -34,6 +35,7 @@ import Download from '$lib/components/icons/Download.svelte'; import ManageModelsModal from './Models/ManageModelsModal.svelte'; import ModelMenu from '$lib/components/admin/Settings/Models/ModelMenu.svelte'; + import ModelPricingModal from '$lib/components/admin/Settings/Models/ModelPricingModal.svelte'; import EllipsisHorizontal from '$lib/components/icons/EllipsisHorizontal.svelte'; import EyeSlash from '$lib/components/icons/EyeSlash.svelte'; import Eye from '$lib/components/icons/Eye.svelte'; @@ -56,6 +58,11 @@ let showConfigModal = false; let showManageModal = false; + // 定价相关 + let modelPricings = {}; + let showPricingModal = false; + let selectedModelForPricing = null; + $: if (models) { filteredModels = models .filter((m) => searchValue === '' || m.name.toLowerCase().includes(searchValue.toLowerCase())) @@ -84,13 +91,27 @@ workspaceModels = await getBaseModels(localStorage.token); baseModels = await getModels(localStorage.token, null, true); + // 加载所有模型定价 + try { + const pricingList = await listModelPricing(); + modelPricings = pricingList.reduce((acc, p) => { + acc[p.model_id] = p; + return acc; + }, {}); + } catch (error) { + console.error('加载定价失败:', error); + modelPricings = {}; + } + models = baseModels.map((m) => { const workspaceModel = workspaceModels.find((wm) => wm.id === m.id); + const pricing = modelPricings[m.id]; // 获取定价 if (workspaceModel) { return { ...m, - ...workspaceModel + ...workspaceModel, + pricing // 附加定价信息 }; } else { return { @@ -98,7 +119,8 @@ id: m.id, name: m.name, - is_active: true + is_active: true, + pricing // 附加定价信息 }; } }); @@ -245,6 +267,14 @@ + { + await init(); // 重新加载数据 + }} +/> {#if models !== null} {#if selectedModelId === null} @@ -365,6 +395,63 @@ : model.id} + + + {#if model.pricing || modelPricings[model.id]} + {@const pricing = model.pricing || modelPricings[model.id]} +
+ + + + ¥{(pricing.input_price / 10000).toFixed(2)} + | + + ¥{(pricing.output_price / 10000).toFixed(2)} + /M + + + + {#if $user?.role === 'admin'} + + {/if} +
+ {:else} + +
+ + + ¥1.00 + | + + ¥2.00 + /M + + + {$i18n.t('默认')} + + + + {#if $user?.role === 'admin'} + + {/if} +
+ {/if}
diff --git a/src/lib/components/admin/Settings/Models/ModelPricingModal.svelte b/src/lib/components/admin/Settings/Models/ModelPricingModal.svelte new file mode 100644 index 0000000000..5b4b2d5c3b --- /dev/null +++ b/src/lib/components/admin/Settings/Models/ModelPricingModal.svelte @@ -0,0 +1,157 @@ + + + +
+
+
+ {$i18n.t('编辑模型定价')} - {model?.name} +
+ +
+ +
+ +
+
+ {$i18n.t('模型ID')}: {model?.id} +
+
+ + +
+ + +
+ {$i18n.t('当前')}: ¥{(inputPrice || 0).toFixed(2)}/M = {((inputPrice || 0) * 10000).toFixed( + 0 + )} {$i18n.t('毫')}/M +
+
+ + +
+ + +
+ {$i18n.t('当前')}: ¥{(outputPrice || 0).toFixed(2)}/M = {((outputPrice || 0) * 10000).toFixed( + 0 + )} {$i18n.t('毫')}/M +
+
+ + +
+
+ {$i18n.t('计费预览')} ({$i18n.t('以')} 1000 tokens {$i18n.t('为例')}) +
+
+
{$i18n.t('输入')}: ¥{((inputPrice || 0) * 0.001).toFixed(4)}
+
{$i18n.t('输出')}: ¥{((outputPrice || 0) * 0.001).toFixed(4)}
+
+ {$i18n.t('总计')}: ¥{(((inputPrice || 0) + (outputPrice || 0)) * 0.001).toFixed(4)} +
+
+
+ + +
+ + +
+
+
+
diff --git a/src/lib/components/billing/BillingLogsTable.svelte b/src/lib/components/billing/BillingLogsTable.svelte index c0f5755e82..4b4e23b082 100644 --- a/src/lib/components/billing/BillingLogsTable.svelte +++ b/src/lib/components/billing/BillingLogsTable.svelte @@ -207,9 +207,17 @@ 实际消费: {formatCurrency(log.cost, true)}
-
- 退款金额: - -{formatCurrency(log.refundAmount, true)} +
0} class:text-red-600={log.refundAmount < 0} class:dark:text-green-400={log.refundAmount > 0} class:dark:text-red-400={log.refundAmount < 0}> + {log.refundAmount >= 0 ? '退款金额' : '补扣金额'}: + + {#if log.refundAmount > 0} + +{formatCurrency(log.refundAmount, true)} + {:else if log.refundAmount < 0} + {formatCurrency(log.refundAmount, true)} + {:else} + {formatCurrency(0, true)} + {/if} +
{#if log.precharge_id}