mirror of
https://github.com/lukaszraczylo/traefik-llmgateway.git
synced 2026-10-15 05:29:28 +00:00
Operator scope addition: raw token/request counts reaching millions/
billions were unreadable as bare digits. formatCompactCount (lib/
format.ts, shipped in the previous commit alongside this feature's own
helpers) renders an SI-style label: below 10,000 the exact integer with
thousands separators, at or above it three significant figures with a
k/M/B suffix ("12.3k", "456k", "1.23M", "45.6M", "1.23B") —
every digit past the third significant one is FLOORED, never rounded,
the same honesty rule formatRatePercent already applies to the
success-rate badge: a count of 999,950 must read "999k", never
"1,000k", since it has not actually crossed that boundary.
CompactNumber.vue is the one shared, reusable presentational component
(component-first: no per-call-site wrapper markup) — renders the
compact form as text, with the exact value on both title and aria-label,
mirroring ProviderRateBadge.vue/ModelChip.vue's own hover-parity
pattern.
Applied everywhere a count renders: usage-columns.ts's numericColumn
(reqMin/reqDay/tokIn-out per day and month — cost columns explicitly
opt out via a new compact:false argument, since formatCost already
shows full precision), target-columns.ts's MCP/agent request counters,
UsageView.vue's Total card and group-accordion trigger/detail stat
grid, and UsageChart.vue's Chart.js y-axis ticks (compact only — no
room for the raw figure on an axis label) and tooltip labels (compact
form WITH the precise raw value alongside, e.g. "Tokens in: 1.23M
(1,234,567)").
Sorting is unaffected by construction: every numericColumn's accessorFn
still returns the raw number (what getSortedRowModel actually sorts by)
regardless of what the cell renders — usage-columns.spec.ts pins this
directly, including a sort-order assertion that would fail under a
lexicographic (string) sort but passes under the numeric one.
91 lines
3.7 KiB
TypeScript
91 lines
3.7 KiB
TypeScript
import type { ColumnDef } from '@tanstack/vue-table'
|
|
import { h } from 'vue'
|
|
|
|
import CompactNumber from '@/components/CompactNumber.vue'
|
|
import { formatCost, formatLimits } from '@/lib/format'
|
|
import type { AdminUsageEntryView } from '@/types/api'
|
|
|
|
/**
|
|
* numericColumn builds one right-aligned, sortable numeric column that
|
|
* reads '?' instead of a stale number while the entry's own scope has a
|
|
* down store — DataTable.vue's cell renderer, not the accessor, does this
|
|
* check, so sorting itself still uses the entry's real underlying value
|
|
* (a storeDown row sorts by its last-known number, same as the rest of
|
|
* the table; only the DISPLAYED text is masked). Mirrors the pre-DataTable
|
|
* UsageTable.vue template's own `entry.storeDown ? '?' : ...` convention.
|
|
*
|
|
* compact (default true) renders the value through CompactNumber (SI-
|
|
* style, with the exact figure on title/aria-label) instead of the plain
|
|
* `format`/String path — used for every raw token/request COUNT column
|
|
* below; the two cost columns pass compact:false, since formatCost
|
|
* already shows full precision and compacting a dollar figure is not
|
|
* this feature's concern. The accessorFn (and therefore sorting) always
|
|
* reads the true raw number either way — only the cell's rendered text
|
|
* changes.
|
|
*/
|
|
function numericColumn(
|
|
id: string,
|
|
header: string,
|
|
read: (entry: AdminUsageEntryView) => number,
|
|
format: (n: number) => string = String,
|
|
compact = true,
|
|
): ColumnDef<AdminUsageEntryView, unknown> {
|
|
return {
|
|
id,
|
|
header,
|
|
accessorFn: read,
|
|
meta: { align: 'right' },
|
|
cell: ({ row }) => {
|
|
if (row.original.storeDown) return '?'
|
|
const value = read(row.original)
|
|
return compact ? h(CompactNumber, { value }) : format(value)
|
|
},
|
|
}
|
|
}
|
|
|
|
/**
|
|
* usageColumns builds the shared TanStack `ColumnDef` set for one
|
|
* AdminUsageEntryView table. Used by UsageTable.vue (the flat Users list,
|
|
* and — reused unmodified — a group's nested member-user list inside the
|
|
* Usage view's Groups accordion) and, headlessly (no `<Table>` render, see
|
|
* that view's own doc comment for why), by UsageView.vue's Groups sort
|
|
* bar, so both surfaces sort by the exact same column semantics.
|
|
* idLabel/secondaryColumnLabel name the first two columns; secondaryValue
|
|
* reads the second column's per-row value (a user's group name, or a
|
|
* group's member count).
|
|
*/
|
|
export function usageColumns(
|
|
idLabel: string,
|
|
secondaryColumnLabel: string,
|
|
secondaryValue: (entry: AdminUsageEntryView) => string,
|
|
): ColumnDef<AdminUsageEntryView, unknown>[] {
|
|
return [
|
|
{
|
|
id: 'id',
|
|
header: idLabel,
|
|
accessorFn: (entry) => entry.id,
|
|
cell: ({ row }) => h('span', { class: 'font-medium' }, row.original.id),
|
|
},
|
|
{
|
|
id: 'secondary',
|
|
header: secondaryColumnLabel,
|
|
accessorFn: secondaryValue,
|
|
cell: ({ row }) => h('span', { class: 'text-muted-foreground' }, secondaryValue(row.original)),
|
|
},
|
|
{
|
|
id: 'limits',
|
|
header: 'Limits',
|
|
accessorFn: (entry) => formatLimits(entry.limits),
|
|
cell: ({ row }) => h('span', { class: 'text-muted-foreground' }, formatLimits(row.original.limits)),
|
|
},
|
|
numericColumn('reqMin', 'req/min', (e) => e.requestsPerMinute),
|
|
numericColumn('reqDay', 'req/day', (e) => e.requestsPerDay),
|
|
numericColumn('tokInDay', 'tokIn/day', (e) => e.tokensInPerDay),
|
|
numericColumn('tokOutDay', 'tokOut/day', (e) => e.tokensOutPerDay),
|
|
numericColumn('tokInMonth', 'tokIn/month', (e) => e.tokensInPerMonth),
|
|
numericColumn('tokOutMonth', 'tokOut/month', (e) => e.tokensOutPerMonth),
|
|
numericColumn('costDay', 'cost/day', (e) => e.costPerDayMicroUsd, formatCost, false),
|
|
numericColumn('costMonth', 'cost/month', (e) => e.costPerMonthMicroUsd, formatCost, false),
|
|
]
|
|
}
|