[] {
const quota = row.getValue('quota') as number
const other = parseLogOther(log.other)
- const isSubscription = other?.billing_source === 'subscription'
-
- if (isSubscription) {
- return (
-
-
-
- }
- />
-
-
- {t('Deducted by subscription')}: {formatLogQuota(quota)}
-
-
-
-
- )
- }
-
- const quotaStr = formatLogQuota(quota)
- const quotaDisplay = splitQuotaDisplay(quotaStr)
-
- return (
-
-
- {quotaDisplay.prefix && (
- {quotaDisplay.prefix}
- )}
- {quotaDisplay.amount}
-
-
- )
+ return
},
},
diff --git a/web/src/features/usage-logs/components/log-cost-display.tsx b/web/src/features/usage-logs/components/log-cost-display.tsx
new file mode 100644
index 00000000..edd0772a
--- /dev/null
+++ b/web/src/features/usage-logs/components/log-cost-display.tsx
@@ -0,0 +1,144 @@
+/*
+Copyright (C) 2023-2026 QuantumNous
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .
+
+For commercial licensing, please contact support@quantumnous.com
+*/
+import { Wrench01Icon } from '@hugeicons/core-free-icons'
+import { HugeiconsIcon } from '@hugeicons/react'
+import { useTranslation } from 'react-i18next'
+
+import { StatusBadge } from '@/components/status-badge'
+import { Badge } from '@/components/ui/badge'
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from '@/components/ui/tooltip'
+import { formatLogQuota } from '@/lib/format'
+
+import { hasToolSurcharge } from '../lib/format'
+import type { LogOtherData } from '../types'
+
+interface LogCostDisplayProps {
+ quota: number
+ other: LogOtherData | null
+}
+
+function splitQuotaDisplay(value: string): { prefix: string; amount: string } {
+ const match = value.match(/^([^0-9+\-.,\s]+)(.+)$/)
+ if (!match) return { prefix: '', amount: value }
+ return { prefix: match[1], amount: match[2] }
+}
+
+function ToolSurchargeMarker() {
+ const { t } = useTranslation()
+ const label = t('Includes tool-call surcharge')
+
+ return (
+
+
+
+
+ +
+
+
+ }
+ />
+ {label}
+
+ )
+}
+
+function QuotaBadge(props: { quota: number }) {
+ const quotaDisplay = splitQuotaDisplay(formatLogQuota(props.quota))
+
+ return (
+
+ {quotaDisplay.prefix ? (
+ {quotaDisplay.prefix}
+ ) : null}
+ {quotaDisplay.amount}
+
+ )
+}
+
+function SubscriptionBadge(props: { quota: number }) {
+ const { t } = useTranslation()
+
+ return (
+
+
+ }
+ />
+
+
+ {t('Deducted by subscription')}: {formatLogQuota(props.quota)}
+
+
+
+ )
+}
+
+export function LogCostDisplay(props: LogCostDisplayProps) {
+ const isSubscription = props.other?.billing_source === 'subscription'
+ const showToolSurcharge = hasToolSurcharge(props.other)
+
+ if (!isSubscription && !showToolSurcharge) {
+ return (
+
+
+
+ )
+ }
+
+ return (
+
+
+ {isSubscription ? (
+
+ ) : (
+
+ )}
+ {showToolSurcharge ? : null}
+
+
+ )
+}
diff --git a/web/src/features/usage-logs/lib/__tests__/tool-surcharge.test.ts b/web/src/features/usage-logs/lib/__tests__/tool-surcharge.test.ts
new file mode 100644
index 00000000..fb3ea348
--- /dev/null
+++ b/web/src/features/usage-logs/lib/__tests__/tool-surcharge.test.ts
@@ -0,0 +1,99 @@
+/*
+Copyright (C) 2023-2026 QuantumNous
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .
+
+For commercial licensing, please contact support@quantumnous.com
+*/
+import assert from 'node:assert/strict'
+import { describe, test } from 'node:test'
+
+import type { LogOtherData } from '../../types'
+import { hasToolSurcharge } from '../format'
+
+describe('tool surcharge detection', () => {
+ test('shows the marker for a charged structured tool surcharge', () => {
+ assert.equal(
+ hasToolSurcharge({
+ tool_surcharges: [{ name: 'lookup_customer', count: 2, price: 5 }],
+ }),
+ true
+ )
+ })
+
+ const legacyCases: Array<{
+ name: string
+ other: LogOtherData
+ }> = [
+ {
+ name: 'Web Search',
+ other: {
+ web_search: true,
+ web_search_call_count: 1,
+ web_search_price: 10,
+ },
+ },
+ {
+ name: 'File Search',
+ other: {
+ file_search: true,
+ file_search_call_count: 2,
+ file_search_price: 2.5,
+ },
+ },
+ {
+ name: 'Image Generation',
+ other: {
+ image_generation_call: true,
+ image_generation_call_price: 0.04,
+ },
+ },
+ ]
+
+ for (const scenario of legacyCases) {
+ test(`keeps the marker visible for legacy ${scenario.name} charges`, () => {
+ assert.equal(hasToolSurcharge(scenario.other), true)
+ })
+ }
+
+ test('hides the marker when surcharge entries are empty or not chargeable', () => {
+ const invalidCases: Array = [
+ null,
+ {},
+ { tool_surcharges: [] },
+ {
+ tool_surcharges: [{ name: 'lookup_customer', count: 0, price: 5 }],
+ },
+ {
+ tool_surcharges: [{ name: 'lookup_customer', count: 1, price: 0 }],
+ },
+ {
+ tool_surcharges: [{ name: ' ', count: 1, price: 5 }],
+ },
+ {
+ web_search: true,
+ web_search_call_count: 1,
+ web_search_price: 0,
+ },
+ {
+ image_generation_call: false,
+ image_generation_call_price: 0.04,
+ },
+ ]
+
+ for (const other of invalidCases) {
+ assert.equal(hasToolSurcharge(other), false)
+ }
+ })
+})
diff --git a/web/src/features/usage-logs/lib/format.ts b/web/src/features/usage-logs/lib/format.ts
index 5ec0e368..c2d64887 100644
--- a/web/src/features/usage-logs/lib/format.ts
+++ b/web/src/features/usage-logs/lib/format.ts
@@ -92,6 +92,67 @@ export function isViolationFeeLog(other: LogOtherData | null): boolean {
)
}
+function isPositiveFiniteNumber(value: unknown): value is number {
+ return typeof value === 'number' && Number.isFinite(value) && value > 0
+}
+
+function hasLegacySearchSurcharge(
+ enabled: boolean | undefined,
+ count: number | undefined,
+ price: number | undefined
+): boolean {
+ return (
+ enabled === true &&
+ isPositiveFiniteNumber(count) &&
+ isPositiveFiniteNumber(price)
+ )
+}
+
+/**
+ * Check whether a consume log includes an actual tool-call surcharge.
+ * Structured surcharge items cover current logs, while the legacy fields keep
+ * historical Web Search, File Search, and Image Generation logs visible.
+ */
+export function hasToolSurcharge(other: LogOtherData | null): boolean {
+ if (!other) return false
+
+ const hasStructuredSurcharge =
+ Array.isArray(other.tool_surcharges) &&
+ other.tool_surcharges.some(
+ (item) =>
+ typeof item?.name === 'string' &&
+ item.name.trim() !== '' &&
+ isPositiveFiniteNumber(item.count) &&
+ isPositiveFiniteNumber(item.price)
+ )
+ if (hasStructuredSurcharge) return true
+
+ if (
+ hasLegacySearchSurcharge(
+ other.web_search,
+ other.web_search_call_count,
+ other.web_search_price
+ )
+ ) {
+ return true
+ }
+
+ if (
+ hasLegacySearchSurcharge(
+ other.file_search,
+ other.file_search_call_count,
+ other.file_search_price
+ )
+ ) {
+ return true
+ }
+
+ return (
+ other.image_generation_call === true &&
+ isPositiveFiniteNumber(other.image_generation_call_price)
+ )
+}
+
/**
* Parse the 'other' field from JSON string to object
*/
diff --git a/web/src/features/usage-logs/types.ts b/web/src/features/usage-logs/types.ts
index a03d3935..2cd43e13 100644
--- a/web/src/features/usage-logs/types.ts
+++ b/web/src/features/usage-logs/types.ts
@@ -106,6 +106,12 @@ export const USAGE_BILLING_PATH = {
export type UsageBillingPath =
(typeof USAGE_BILLING_PATH)[keyof typeof USAGE_BILLING_PATH]
+export interface ToolSurchargeItem {
+ name: string
+ count: number
+ price: number
+}
+
export interface LogOtherData {
admin_info?: {
is_multi_key?: boolean
@@ -197,11 +203,13 @@ export interface LogOtherData {
file_search?: boolean
file_search_call_count?: number
file_search_price?: number
+ tool_surcharges?: ToolSurchargeItem[]
audio_input_seperate_price?: boolean
audio_input_token_count?: number
audio_input_price?: number
image_generation_call?: boolean
image_generation_call_price?: number
+ image_generation_call_count?: number
is_system_prompt_overwritten?: boolean
po?: string[]
billing_source?: string
diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json
index f17db8f5..5c0ecd3b 100644
--- a/web/src/i18n/locales/en.json
+++ b/web/src/i18n/locales/en.json
@@ -2291,6 +2291,7 @@
"Include Model": "Include Model",
"Include Rule Name": "Include Rule Name",
"Includes request rules": "Includes request rules",
+ "Includes tool-call surcharge": "Includes tool-call surcharge",
"Including failed requests, 0 = unlimited": "Including failed requests, 0 = unlimited",
"Incoming path": "Incoming path",
"Incoming path is required": "Incoming path is required",
diff --git a/web/src/i18n/locales/fr.json b/web/src/i18n/locales/fr.json
index aa342fc9..f7257863 100644
--- a/web/src/i18n/locales/fr.json
+++ b/web/src/i18n/locales/fr.json
@@ -2291,6 +2291,7 @@
"Include Model": "Inclure le modèle",
"Include Rule Name": "Inclure le nom de la règle",
"Includes request rules": "Inclut des règles de requête",
+ "Includes tool-call surcharge": "Inclut un supplément pour appel d’outil",
"Including failed requests, 0 = unlimited": "Y compris les requêtes échouées, 0 = illimité",
"Incoming path": "Chemin entrant",
"Incoming path is required": "Le chemin entrant est requis",
diff --git a/web/src/i18n/locales/ja.json b/web/src/i18n/locales/ja.json
index 2abd64c1..0048ee3c 100644
--- a/web/src/i18n/locales/ja.json
+++ b/web/src/i18n/locales/ja.json
@@ -2291,6 +2291,7 @@
"Include Model": "モデルを含む",
"Include Rule Name": "ルール名を含む",
"Includes request rules": "リクエストルールを含む",
+ "Includes tool-call surcharge": "ツール呼び出しの追加料金を含む",
"Including failed requests, 0 = unlimited": "失敗したリクエストを含む、0 = 無制限",
"Incoming path": "受信パス",
"Incoming path is required": "受信パスは必須です",
diff --git a/web/src/i18n/locales/ru.json b/web/src/i18n/locales/ru.json
index 97568a29..4cd11458 100644
--- a/web/src/i18n/locales/ru.json
+++ b/web/src/i18n/locales/ru.json
@@ -2291,6 +2291,7 @@
"Include Model": "Включить модель",
"Include Rule Name": "Включить имя правила",
"Includes request rules": "Включает правила запросов",
+ "Includes tool-call surcharge": "Включает доплату за вызов инструмента",
"Including failed requests, 0 = unlimited": "Включая неудачные запросы, 0 = без ограничений",
"Incoming path": "Входящий путь",
"Incoming path is required": "Входящий путь обязателен",
diff --git a/web/src/i18n/locales/vi.json b/web/src/i18n/locales/vi.json
index 80957757..e90175e3 100644
--- a/web/src/i18n/locales/vi.json
+++ b/web/src/i18n/locales/vi.json
@@ -2291,6 +2291,7 @@
"Include Model": "Bao gồm mô hình",
"Include Rule Name": "Bao gồm tên quy tắc",
"Includes request rules": "Bao gồm quy tắc yêu cầu",
+ "Includes tool-call surcharge": "Bao gồm phụ phí gọi công cụ",
"Including failed requests, 0 = unlimited": "Bao gồm các yêu cầu thất bại, 0 = không giới hạn",
"Incoming path": "Path đầu vào",
"Incoming path is required": "Bắt buộc path đầu vào",
diff --git a/web/src/i18n/locales/zh-TW.json b/web/src/i18n/locales/zh-TW.json
index c04a7356..847069a2 100644
--- a/web/src/i18n/locales/zh-TW.json
+++ b/web/src/i18n/locales/zh-TW.json
@@ -2291,6 +2291,7 @@
"Include Model": "包含模型",
"Include Rule Name": "包含規則名",
"Includes request rules": "包含請求規則",
+ "Includes tool-call surcharge": "包含工具呼叫附加費",
"Including failed requests, 0 = unlimited": "包括失敗的請求,0 = 無限制",
"Incoming path": "入口路徑",
"Incoming path is required": "入口路徑不能為空",
diff --git a/web/src/i18n/locales/zh.json b/web/src/i18n/locales/zh.json
index b19ec7c3..922bf81b 100644
--- a/web/src/i18n/locales/zh.json
+++ b/web/src/i18n/locales/zh.json
@@ -2291,6 +2291,7 @@
"Include Model": "包含模型",
"Include Rule Name": "包含规则名",
"Includes request rules": "包含请求规则",
+ "Includes tool-call surcharge": "包含工具调用附加费",
"Including failed requests, 0 = unlimited": "包括失败的请求,0 = 无限制",
"Incoming path": "入口路径",
"Incoming path is required": "入口路径不能为空",
diff --git a/web/src/lib/lobe-icon.tsx b/web/src/lib/lobe-icon.tsx
index 73cac55e..bcce6208 100644
--- a/web/src/lib/lobe-icon.tsx
+++ b/web/src/lib/lobe-icon.tsx
@@ -26,6 +26,13 @@ For commercial licensing, please contact support@quantumnous.com
* - Size parameter: getLobeIcon("OpenAI", 20)
*/
import * as LobeIcons from '@lobehub/icons'
+import type React from 'react'
+
+import { IconSub2api } from '@/assets/custom/icon-sub2api'
+
+const CUSTOM_ICONS: Record> = {
+ Sub2API: IconSub2api,
+}
/**
* Parse a property value from string to appropriate type
@@ -102,6 +109,10 @@ export function getLobeIcon(
// Parse component path and chained properties
const segments = trimmedName.split('.')
const baseKey = segments[0]
+ const CustomIcon = CUSTOM_ICONS[baseKey]
+ if (CustomIcon) {
+ return
+ }
const BaseIcon = (LobeIcons as Record)[baseKey] as
| Record
| undefined