830 lines
28 KiB
TypeScript
Vendored
830 lines
28 KiB
TypeScript
Vendored
/*
|
|
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 <https://www.gnu.org/licenses/>.
|
|
|
|
For commercial licensing, please contact support@quantumnous.com
|
|
*/
|
|
import { useState } from 'react'
|
|
import { type ColumnDef } from '@tanstack/react-table'
|
|
import { CircleAlert, Sparkles, KeyRound } from 'lucide-react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { getUserAvatarFallback, getUserAvatarStyle } from '@/lib/avatar'
|
|
import { formatBillingCurrencyFromUSD } from '@/lib/currency'
|
|
import {
|
|
formatUseTime,
|
|
formatLogQuota,
|
|
formatTimestampToDate,
|
|
} from '@/lib/format'
|
|
import { cn } from '@/lib/utils'
|
|
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipProvider,
|
|
TooltipTrigger,
|
|
} from '@/components/ui/tooltip'
|
|
import { DataTableColumnHeader } from '@/components/data-table'
|
|
import { StatusBadge, type StatusBadgeProps } from '@/components/status-badge'
|
|
import { LOG_TYPE_ALL_VALUE } from '../../constants'
|
|
import type { UsageLog } from '../../data/schema'
|
|
import {
|
|
formatModelName,
|
|
getFirstResponseTimeColor,
|
|
getResponseTimeColor,
|
|
getTieredBillingSummary,
|
|
hasAnyCacheTokens,
|
|
parseLogOther,
|
|
isViolationFeeLog,
|
|
} from '../../lib/format'
|
|
import {
|
|
isDisplayableLogType,
|
|
isTimingLogType,
|
|
getLogTypeConfig,
|
|
isPerCallBilling,
|
|
} from '../../lib/utils'
|
|
import type { LogOtherData } from '../../types'
|
|
import { DetailsDialog } from '../dialogs/details-dialog'
|
|
import { ModelBadge } from '../model-badge'
|
|
import { useUsageLogsContext } from '../usage-logs-provider'
|
|
|
|
interface DetailSegment {
|
|
text: string
|
|
muted?: boolean
|
|
danger?: boolean
|
|
}
|
|
|
|
function formatRatioCompact(ratio: number | undefined): string {
|
|
if (ratio == null || !Number.isFinite(ratio)) return '-'
|
|
return ratio % 1 === 0
|
|
? String(ratio)
|
|
: ratio.toFixed(4).replace(/\.?0+$/, '')
|
|
}
|
|
|
|
function getGroupRatioText(other: LogOtherData | null): string | null {
|
|
const userGroupRatio = other?.user_group_ratio
|
|
if (
|
|
userGroupRatio != null &&
|
|
userGroupRatio !== -1 &&
|
|
Number.isFinite(userGroupRatio)
|
|
) {
|
|
return `${formatRatioCompact(userGroupRatio)}x`
|
|
}
|
|
|
|
const groupRatio = other?.group_ratio
|
|
if (groupRatio != null && groupRatio !== 1 && Number.isFinite(groupRatio)) {
|
|
return `${formatRatioCompact(groupRatio)}x`
|
|
}
|
|
|
|
return 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 buildDetailSegments(
|
|
log: UsageLog,
|
|
other: LogOtherData | null,
|
|
t: (key: string, opts?: Record<string, unknown>) => string
|
|
): DetailSegment[] {
|
|
if (log.type === 6) {
|
|
return [{ text: t('Async task refund') }]
|
|
}
|
|
|
|
if (log.type !== 2) return []
|
|
|
|
const isViolation = isViolationFeeLog(other)
|
|
if (isViolation) {
|
|
const segments: DetailSegment[] = []
|
|
segments.push({ text: t('Violation Fee'), danger: true })
|
|
if (other?.violation_fee_code) {
|
|
segments.push({
|
|
text: other.violation_fee_code,
|
|
muted: true,
|
|
})
|
|
}
|
|
segments.push({
|
|
text: `${t('Fee')}: ${formatLogQuota(other?.fee_quota ?? log.quota)}`,
|
|
muted: true,
|
|
})
|
|
return segments
|
|
}
|
|
|
|
if (!other) return []
|
|
|
|
const segments: DetailSegment[] = []
|
|
|
|
const priceOpts = { digitsLarge: 4, digitsSmall: 6, abbreviate: false }
|
|
const formatPrice = (price: number) =>
|
|
`${formatBillingCurrencyFromUSD(price, priceOpts)}/M`
|
|
const formatPriceCompact = (price: number) =>
|
|
formatBillingCurrencyFromUSD(price, priceOpts)
|
|
const formatPriceList = (prices: string[], showUnit: boolean) => {
|
|
const text = prices.join(' / ')
|
|
return showUnit ? `${text}/M` : text
|
|
}
|
|
const isTieredExpr = other.billing_mode === 'tiered_expr'
|
|
const tieredSummary = getTieredBillingSummary(other)
|
|
if (isTieredExpr) {
|
|
if (tieredSummary) {
|
|
const baseEntries = tieredSummary.priceEntries
|
|
.filter((entry) => ['inputPrice', 'outputPrice'].includes(entry.field))
|
|
.map((entry) => formatPriceCompact(entry.price))
|
|
if (baseEntries.length > 0) {
|
|
const tierLabel = tieredSummary.tier.label || t('Default')
|
|
segments.push({
|
|
text: `${tierLabel} · ${formatPriceList(baseEntries, true)}`,
|
|
})
|
|
}
|
|
|
|
const cacheEntries = tieredSummary.priceEntries
|
|
.filter((entry) =>
|
|
['cacheReadPrice', 'cacheCreatePrice', 'cacheCreate1hPrice'].includes(
|
|
entry.field
|
|
)
|
|
)
|
|
.map((entry) => {
|
|
return formatPriceCompact(entry.price)
|
|
})
|
|
if (cacheEntries.length > 0) {
|
|
segments.push({
|
|
text: `${t('Cache')} ${formatPriceList(cacheEntries, false)}`,
|
|
muted: true,
|
|
})
|
|
}
|
|
|
|
const otherEntries = tieredSummary.priceEntries
|
|
.filter(
|
|
(entry) =>
|
|
![
|
|
'inputPrice',
|
|
'outputPrice',
|
|
'cacheReadPrice',
|
|
'cacheCreatePrice',
|
|
'cacheCreate1hPrice',
|
|
].includes(entry.field)
|
|
)
|
|
.map((entry) => `${t(entry.shortLabel)} ${formatPrice(entry.price)}`)
|
|
if (otherEntries.length > 0) {
|
|
segments.push({
|
|
text: otherEntries.join(' · '),
|
|
muted: true,
|
|
})
|
|
}
|
|
} else {
|
|
segments.push({
|
|
text: `${t('Dynamic Pricing')} · ${t('No matching results')}`,
|
|
muted: true,
|
|
})
|
|
}
|
|
} else {
|
|
const isPerCall = isPerCallBilling(other.model_price)
|
|
if (isPerCall) {
|
|
segments.push({
|
|
text: `${t('Per-call')} · ${formatBillingCurrencyFromUSD(other.model_price!, priceOpts)}`,
|
|
})
|
|
} else if (other.model_ratio != null) {
|
|
const inputPriceUSD = other.model_ratio * 2.0
|
|
const baseEntries = [formatPriceCompact(inputPriceUSD)]
|
|
if (other.completion_ratio != null) {
|
|
baseEntries.push(
|
|
formatPriceCompact(inputPriceUSD * other.completion_ratio)
|
|
)
|
|
}
|
|
segments.push({
|
|
text: `${t('Standard')} · ${formatPriceList(baseEntries, true)}`,
|
|
})
|
|
|
|
if (hasAnyCacheTokens(other)) {
|
|
const cacheEntries = [
|
|
other.cache_ratio != null && other.cache_ratio !== 1
|
|
? formatPriceCompact(inputPriceUSD * other.cache_ratio)
|
|
: null,
|
|
other.cache_creation_ratio != null && other.cache_creation_ratio !== 1
|
|
? formatPriceCompact(inputPriceUSD * other.cache_creation_ratio)
|
|
: null,
|
|
other.cache_creation_ratio_1h != null &&
|
|
other.cache_creation_ratio_1h !== 0
|
|
? formatPriceCompact(inputPriceUSD * other.cache_creation_ratio_1h)
|
|
: null,
|
|
].filter(Boolean) as string[]
|
|
|
|
if (cacheEntries.length > 0) {
|
|
segments.push({
|
|
text: `${t('Cache')} ${formatPriceList(cacheEntries, false)}`,
|
|
muted: true,
|
|
})
|
|
}
|
|
}
|
|
} else {
|
|
const userGroupRatio = other.user_group_ratio
|
|
const groupRatio = other.group_ratio
|
|
const isUserGroup =
|
|
userGroupRatio != null &&
|
|
Number.isFinite(userGroupRatio) &&
|
|
userGroupRatio !== -1
|
|
const effectiveRatio = isUserGroup ? userGroupRatio : groupRatio
|
|
const ratioLabel = isUserGroup
|
|
? t('User Exclusive Ratio')
|
|
: t('Group Ratio')
|
|
|
|
if (effectiveRatio != null && Number.isFinite(effectiveRatio)) {
|
|
segments.push({
|
|
text: `${ratioLabel} ${formatRatioCompact(effectiveRatio)}x`,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
if (other.is_system_prompt_overwritten) {
|
|
segments.push({
|
|
text: t('System Prompt Override'),
|
|
danger: true,
|
|
})
|
|
}
|
|
|
|
return segments
|
|
}
|
|
|
|
export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
|
const { t } = useTranslation()
|
|
const columns: ColumnDef<UsageLog>[] = [
|
|
{
|
|
accessorKey: 'created_at',
|
|
header: ({ column }) => (
|
|
<DataTableColumnHeader column={column} title={t('Time')} />
|
|
),
|
|
cell: ({ row }) => {
|
|
const log = row.original
|
|
const timestamp = row.getValue('created_at') as number
|
|
const config = getLogTypeConfig(log.type)
|
|
|
|
return (
|
|
<div className='flex flex-col gap-0.5'>
|
|
<span className='font-mono text-xs tabular-nums'>
|
|
{formatTimestampToDate(timestamp)}
|
|
</span>
|
|
<StatusBadge
|
|
label={t(config.label)}
|
|
variant={config.color as StatusBadgeProps['variant']}
|
|
size='sm'
|
|
copyable={false}
|
|
className='!text-xs [&_span]:!text-xs'
|
|
/>
|
|
</div>
|
|
)
|
|
},
|
|
filterFn: (row, _id, value) => {
|
|
if (!Array.isArray(value) || value.length === 0) return true
|
|
if (value.includes(LOG_TYPE_ALL_VALUE)) return true
|
|
return value.includes(String(row.original.type))
|
|
},
|
|
enableHiding: false,
|
|
meta: { label: t('Time') },
|
|
},
|
|
]
|
|
|
|
if (isAdmin) {
|
|
columns.push(
|
|
{
|
|
id: 'channel',
|
|
accessorFn: (row) => row.channel,
|
|
header: ({ column }) => (
|
|
<DataTableColumnHeader column={column} title={t('Channel')} />
|
|
),
|
|
cell: function ChannelCell({ row }) {
|
|
const { sensitiveVisible, setAffinityTarget, setAffinityDialogOpen } =
|
|
useUsageLogsContext()
|
|
const log = row.original
|
|
|
|
if (!isDisplayableLogType(log.type)) return null
|
|
|
|
const other = parseLogOther(log.other)
|
|
const affinity = other?.admin_info?.channel_affinity
|
|
const useChannel = other?.admin_info?.use_channel
|
|
const channelChain =
|
|
useChannel && useChannel.length > 0
|
|
? useChannel.join(' → ')
|
|
: undefined
|
|
const channelDisplay = log.channel_name
|
|
? `${log.channel_name} #${log.channel}`
|
|
: `#${log.channel}`
|
|
const channelIdDisplay = `#${log.channel}`
|
|
const channelName = sensitiveVisible ? log.channel_name : '••••'
|
|
|
|
return (
|
|
<TooltipProvider>
|
|
<Tooltip>
|
|
<TooltipTrigger
|
|
render={
|
|
<div className='flex max-w-[160px] flex-col gap-0.5' />
|
|
}
|
|
>
|
|
<div className='relative inline-flex w-fit'>
|
|
<StatusBadge
|
|
label={channelIdDisplay}
|
|
autoColor={String(log.channel)}
|
|
copyText={String(log.channel)}
|
|
size='sm'
|
|
showDot={false}
|
|
className='font-mono'
|
|
/>
|
|
{affinity && (
|
|
<button
|
|
type='button'
|
|
className='absolute -top-1 -right-1 leading-none text-amber-500'
|
|
onClick={(e) => {
|
|
e.stopPropagation()
|
|
setAffinityTarget({
|
|
rule_name: affinity.rule_name || '',
|
|
using_group:
|
|
affinity.using_group ||
|
|
affinity.selected_group ||
|
|
'',
|
|
key_hint: affinity.key_hint || '',
|
|
key_fp: affinity.key_fp || '',
|
|
})
|
|
setAffinityDialogOpen(true)
|
|
}}
|
|
>
|
|
<Sparkles className='size-3 fill-current' />
|
|
</button>
|
|
)}
|
|
</div>
|
|
{log.channel_name && (
|
|
<span className='text-muted-foreground/70 truncate [font-family:var(--font-body)] !text-xs'>
|
|
{channelName}
|
|
</span>
|
|
)}
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
<div className='space-y-1'>
|
|
<p>
|
|
{sensitiveVisible ? channelDisplay : channelIdDisplay}
|
|
</p>
|
|
{channelChain && (
|
|
<p className='text-muted-foreground text-xs'>
|
|
{t('Chain')}: {channelChain}
|
|
</p>
|
|
)}
|
|
{affinity && (
|
|
<div className='border-t pt-1 text-xs'>
|
|
<p className='font-medium'>{t('Channel Affinity')}</p>
|
|
<p>
|
|
{t('Rule')}: {affinity.rule_name || '-'}
|
|
</p>
|
|
<p>
|
|
{t('Group')}:{' '}
|
|
{sensitiveVisible
|
|
? affinity.using_group ||
|
|
affinity.selected_group ||
|
|
'-'
|
|
: '••••'}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
)
|
|
},
|
|
meta: { label: t('Channel') },
|
|
},
|
|
{
|
|
id: 'user',
|
|
accessorFn: (row) => row.username,
|
|
header: ({ column }) => (
|
|
<DataTableColumnHeader column={column} title={t('User')} />
|
|
),
|
|
cell: function UserCell({ row }) {
|
|
const { sensitiveVisible, setSelectedUserId, setUserInfoDialogOpen } =
|
|
useUsageLogsContext()
|
|
const log = row.original
|
|
|
|
if (!log.username) return null
|
|
|
|
return (
|
|
<button
|
|
type='button'
|
|
className='flex items-center gap-1.5 text-left'
|
|
onClick={(e) => {
|
|
e.stopPropagation()
|
|
setSelectedUserId(log.user_id)
|
|
setUserInfoDialogOpen(true)
|
|
}}
|
|
>
|
|
<Avatar className='ring-border/60 size-6 ring-1 max-sm:hidden'>
|
|
<AvatarFallback
|
|
className={cn(
|
|
'text-[11px] font-semibold',
|
|
!sensitiveVisible && 'bg-muted text-muted-foreground'
|
|
)}
|
|
style={
|
|
sensitiveVisible
|
|
? getUserAvatarStyle(log.username)
|
|
: undefined
|
|
}
|
|
>
|
|
{sensitiveVisible ? getUserAvatarFallback(log.username) : '•'}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<TooltipProvider delay={300}>
|
|
<Tooltip>
|
|
<TooltipTrigger
|
|
render={
|
|
<span className='text-muted-foreground max-w-[100px] truncate text-sm hover:underline' />
|
|
}
|
|
>
|
|
{sensitiveVisible ? log.username : '••••'}
|
|
</TooltipTrigger>
|
|
{sensitiveVisible && log.username.length > 12 && (
|
|
<TooltipContent side='top'>{log.username}</TooltipContent>
|
|
)}
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
</button>
|
|
)
|
|
},
|
|
meta: { label: t('User') },
|
|
}
|
|
)
|
|
}
|
|
|
|
columns.push({
|
|
accessorKey: 'token_name',
|
|
header: ({ column }) => (
|
|
<DataTableColumnHeader column={column} title={t('Token')} />
|
|
),
|
|
cell: function TokenNameCell({ row }) {
|
|
const { sensitiveVisible } = useUsageLogsContext()
|
|
const log = row.original
|
|
if (!isDisplayableLogType(log.type)) return null
|
|
|
|
const tokenName = log.token_name
|
|
if (!tokenName) return null
|
|
|
|
const other = parseLogOther(log.other)
|
|
const displayName = sensitiveVisible ? tokenName : '••••'
|
|
let group = log.group
|
|
if (!group) group = other?.group || ''
|
|
|
|
const metaParts: string[] = []
|
|
const groupRatioText = getGroupRatioText(other)
|
|
if (group) {
|
|
metaParts.push(sensitiveVisible ? group : '••••')
|
|
}
|
|
if (groupRatioText) metaParts.push(groupRatioText)
|
|
|
|
return (
|
|
<div className='flex max-w-[200px] flex-col gap-0.5'>
|
|
<TooltipProvider delay={300}>
|
|
<Tooltip>
|
|
<TooltipTrigger render={<div className='max-w-full' />}>
|
|
<StatusBadge
|
|
label={displayName}
|
|
icon={KeyRound}
|
|
copyText={sensitiveVisible ? tokenName : undefined}
|
|
size='sm'
|
|
showDot={false}
|
|
className='border-border/60 bg-muted/30 text-foreground h-6 max-w-full gap-1.5 overflow-hidden rounded-md border px-2 py-0.5 [font-family:var(--font-body)]'
|
|
/>
|
|
</TooltipTrigger>
|
|
{sensitiveVisible && tokenName.length > 16 && (
|
|
<TooltipContent side='top' className='max-w-xs break-all'>
|
|
{tokenName}
|
|
</TooltipContent>
|
|
)}
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
{metaParts.length > 0 && (
|
|
<span className='text-muted-foreground/60 truncate [font-family:var(--font-body)] !text-xs'>
|
|
{metaParts.join(' · ')}
|
|
</span>
|
|
)}
|
|
</div>
|
|
)
|
|
},
|
|
meta: { label: t('Token') },
|
|
size: 160,
|
|
})
|
|
|
|
columns.push(
|
|
{
|
|
accessorKey: 'model_name',
|
|
header: ({ column }) => (
|
|
<DataTableColumnHeader column={column} title={t('Model')} />
|
|
),
|
|
cell: function ModelCell({ row }) {
|
|
const log = row.original
|
|
if (!isDisplayableLogType(log.type)) return null
|
|
|
|
const modelInfo = formatModelName(log)
|
|
|
|
return (
|
|
<div className='flex w-fit flex-col gap-0.5'>
|
|
<ModelBadge
|
|
modelName={modelInfo.name}
|
|
actualModel={modelInfo.actualModel}
|
|
/>
|
|
</div>
|
|
)
|
|
},
|
|
meta: { label: t('Model'), mobileTitle: true },
|
|
},
|
|
|
|
{
|
|
accessorKey: 'use_time',
|
|
header: ({ column }) => (
|
|
<DataTableColumnHeader column={column} title={t('Timing')} />
|
|
),
|
|
cell: ({ row }) => {
|
|
const log = row.original
|
|
if (!isTimingLogType(log.type)) return null
|
|
|
|
const useTime = row.getValue('use_time') as number
|
|
const other = parseLogOther(log.other)
|
|
const frt = other?.frt
|
|
const tokensPerSecond =
|
|
useTime > 0 && log.completion_tokens > 0
|
|
? log.completion_tokens / useTime
|
|
: null
|
|
const timeVariant = getResponseTimeColor(useTime, log.completion_tokens)
|
|
const frtVariant = frt
|
|
? getFirstResponseTimeColor(frt / 1000)
|
|
: 'neutral'
|
|
|
|
const timingBgMap: Record<string, string> = {
|
|
success:
|
|
'border border-emerald-200/40 bg-emerald-50/35 dark:border-emerald-900/40 dark:bg-emerald-950/15',
|
|
warning:
|
|
'border border-amber-200/45 bg-amber-50/35 dark:border-amber-900/40 dark:bg-amber-950/15',
|
|
danger:
|
|
'border border-rose-200/50 bg-rose-50/35 dark:border-rose-900/40 dark:bg-rose-950/15',
|
|
neutral:
|
|
'border border-border/60 bg-muted/30 dark:border-border/40 dark:bg-muted/20',
|
|
}
|
|
|
|
return (
|
|
<div className='flex flex-col gap-1'>
|
|
<div className='flex items-center gap-1.5'>
|
|
<StatusBadge
|
|
label={formatUseTime(useTime)}
|
|
variant={timeVariant as StatusBadgeProps['variant']}
|
|
size='sm'
|
|
copyable={false}
|
|
className={cn('rounded-md font-mono', timingBgMap[timeVariant])}
|
|
/>
|
|
{log.is_stream &&
|
|
(frt != null && frt > 0 ? (
|
|
<StatusBadge
|
|
label={formatUseTime(frt / 1000)}
|
|
variant={frtVariant as StatusBadgeProps['variant']}
|
|
size='sm'
|
|
showDot={false}
|
|
copyable={false}
|
|
className={cn(
|
|
'rounded-md font-mono',
|
|
timingBgMap[frtVariant]
|
|
)}
|
|
/>
|
|
) : (
|
|
<StatusBadge
|
|
label='N/A'
|
|
variant='neutral'
|
|
size='sm'
|
|
showDot={false}
|
|
copyable={false}
|
|
className={cn('rounded-md font-mono', timingBgMap.neutral)}
|
|
/>
|
|
))}
|
|
</div>
|
|
<div className='flex items-center gap-1 [font-family:var(--font-body)] !text-xs leading-none'>
|
|
<span className='text-muted-foreground/60 [font-family:var(--font-body)] !text-xs leading-none'>
|
|
{log.is_stream ? t('Stream') : t('Non-stream')}
|
|
{tokensPerSecond != null && (
|
|
<>
|
|
{' · '}
|
|
<span className='tabular-nums'>
|
|
{Math.round(tokensPerSecond)}
|
|
</span>
|
|
{' t/s'}
|
|
</>
|
|
)}
|
|
</span>
|
|
{log.is_stream &&
|
|
other?.stream_status &&
|
|
other.stream_status.status !== 'ok' && (
|
|
<TooltipProvider>
|
|
<Tooltip>
|
|
<TooltipTrigger
|
|
render={<CircleAlert className='size-3 text-red-500' />}
|
|
></TooltipTrigger>
|
|
<TooltipContent>
|
|
<div className='space-y-0.5 text-xs'>
|
|
<p>
|
|
{t('Stream Status')}: {t('Error')}
|
|
</p>
|
|
<p>{other.stream_status.end_reason || 'unknown'}</p>
|
|
{(other.stream_status.error_count ?? 0) > 0 && (
|
|
<p>
|
|
{t('Soft Errors')}:{' '}
|
|
{other.stream_status.error_count}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
},
|
|
meta: { label: t('Timing') },
|
|
},
|
|
|
|
{
|
|
accessorKey: 'prompt_tokens',
|
|
header: ({ column }) => (
|
|
<DataTableColumnHeader column={column} title='Tokens' />
|
|
),
|
|
cell: ({ row }) => {
|
|
const log = row.original
|
|
if (!isDisplayableLogType(log.type)) return null
|
|
|
|
const other = parseLogOther(log.other)
|
|
|
|
const promptTokens = log.prompt_tokens || 0
|
|
const completionTokens = log.completion_tokens || 0
|
|
if (promptTokens === 0 && completionTokens === 0) {
|
|
return <span className='text-muted-foreground text-xs'>-</span>
|
|
}
|
|
|
|
const cacheReadTokens = other?.cache_tokens || 0
|
|
const cacheWrite5m = other?.cache_creation_tokens_5m || 0
|
|
const cacheWrite1h = other?.cache_creation_tokens_1h || 0
|
|
const hasSplitCache = cacheWrite5m > 0 || cacheWrite1h > 0
|
|
const cacheWriteTokens = hasSplitCache
|
|
? cacheWrite5m + cacheWrite1h
|
|
: other?.cache_creation_tokens || 0
|
|
|
|
return (
|
|
<div className='flex flex-col gap-0.5'>
|
|
<span className='font-mono text-xs font-medium tabular-nums'>
|
|
{promptTokens.toLocaleString()} /{' '}
|
|
{completionTokens.toLocaleString()}
|
|
</span>
|
|
{(cacheReadTokens > 0 || cacheWriteTokens > 0) && (
|
|
<div className='flex items-center gap-1 text-[11px]'>
|
|
{cacheReadTokens > 0 && (
|
|
<span className='text-muted-foreground/60'>
|
|
{t('Cache')}↓ {cacheReadTokens.toLocaleString()}
|
|
</span>
|
|
)}
|
|
{cacheWriteTokens > 0 && (
|
|
<span className='text-muted-foreground/60'>
|
|
↑ {cacheWriteTokens.toLocaleString()}
|
|
</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
},
|
|
meta: { label: 'Tokens' },
|
|
},
|
|
|
|
{
|
|
accessorKey: 'quota',
|
|
header: ({ column }) => (
|
|
<DataTableColumnHeader column={column} title={t('Cost')} />
|
|
),
|
|
cell: ({ row }) => {
|
|
const log = row.original
|
|
if (!isDisplayableLogType(log.type)) return null
|
|
|
|
const quota = row.getValue('quota') as number
|
|
const other = parseLogOther(log.other)
|
|
const isSubscription = other?.billing_source === 'subscription'
|
|
|
|
if (isSubscription) {
|
|
return (
|
|
<TooltipProvider>
|
|
<Tooltip>
|
|
<TooltipTrigger
|
|
render={
|
|
<StatusBadge
|
|
label={t('Subscription')}
|
|
variant='success'
|
|
size='sm'
|
|
copyable={false}
|
|
className='cursor-help'
|
|
/>
|
|
}
|
|
/>
|
|
<TooltipContent>
|
|
<span>
|
|
{t('Deducted by subscription')}: {formatLogQuota(quota)}
|
|
</span>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
)
|
|
}
|
|
|
|
const quotaStr = formatLogQuota(quota)
|
|
const quotaDisplay = splitQuotaDisplay(quotaStr)
|
|
|
|
return (
|
|
<div className='flex flex-col gap-0.5'>
|
|
<span className='border-border/80 bg-muted/60 inline-flex h-6 w-fit items-center rounded-md border px-2 text-sm leading-none [font-family:var(--font-body)] font-semibold tabular-nums'>
|
|
{quotaDisplay.prefix && (
|
|
<span className='mr-1'>{quotaDisplay.prefix}</span>
|
|
)}
|
|
<span>{quotaDisplay.amount}</span>
|
|
</span>
|
|
</div>
|
|
)
|
|
},
|
|
meta: { label: t('Cost') },
|
|
},
|
|
|
|
{
|
|
accessorKey: 'content',
|
|
header: t('Details'),
|
|
cell: function DetailsCell({ row }) {
|
|
const [dialogOpen, setDialogOpen] = useState(false)
|
|
const log = row.original
|
|
const other = parseLogOther(log.other)
|
|
|
|
const segments = buildDetailSegments(log, other, t)
|
|
const primary = segments[0]
|
|
const hasMore = segments.length > 1
|
|
|
|
return (
|
|
<>
|
|
<button
|
|
type='button'
|
|
className='group flex max-w-[200px] items-center gap-1 text-left text-xs'
|
|
onClick={() => setDialogOpen(true)}
|
|
title={t('Click to view full details')}
|
|
>
|
|
{primary ? (
|
|
<span
|
|
className={cn(
|
|
'truncate leading-snug group-hover:underline',
|
|
primary.muted
|
|
? 'text-muted-foreground/60'
|
|
: primary.danger
|
|
? 'text-red-600 dark:text-red-400'
|
|
: 'text-foreground'
|
|
)}
|
|
>
|
|
{primary.text}
|
|
{hasMore && (
|
|
<span className='text-muted-foreground/40 ml-0.5'>
|
|
+{segments.length - 1}
|
|
</span>
|
|
)}
|
|
</span>
|
|
) : log.content ? (
|
|
<span className='text-muted-foreground truncate group-hover:underline'>
|
|
{log.content}
|
|
</span>
|
|
) : (
|
|
<span className='text-muted-foreground/40'>—</span>
|
|
)}
|
|
</button>
|
|
<DetailsDialog
|
|
log={log}
|
|
isAdmin={isAdmin}
|
|
open={dialogOpen}
|
|
onOpenChange={setDialogOpen}
|
|
/>
|
|
</>
|
|
)
|
|
},
|
|
meta: { label: t('Details') },
|
|
size: 180,
|
|
maxSize: 200,
|
|
}
|
|
)
|
|
|
|
return columns
|
|
}
|