From 0eb98eae64888aed76826082aa830b26569ab3f5 Mon Sep 17 00:00:00 2001 From: Lukasz Raczylo Date: Sun, 23 Aug 2026 19:45:47 +0100 Subject: [PATCH] feat(webui): show usage-provenance badges in the Providers tab Add AdminProvenanceView and AdminProviderView.provenance to types/api.ts, mirroring admin.go's new fields, then surface two badges beside the existing health and latency badges in ProvidersView.vue: "estimated N" (secondary variant) and "unbilled N" (destructive variant), each with a hover title explaining what happened and why it matters. Neither badge renders when that provenance kind has no observations yet, matching the existing health/latency badges' own convention. Estimated and unbilled stay two separate badges, never merged into one indicator: collapsing them would hide the one thing this feature exists to surface, that a provider serving completions entirely free against every budget is a materially different failure than an approximated bill. Built with shadcn-vue's existing Badge component; no hand-rolled markup. --- webui/src/components/ProvidersView.vue | 63 +++++++++++++++++++++++++- webui/src/types/api.ts | 36 +++++++++++++++ 2 files changed, 98 insertions(+), 1 deletion(-) diff --git a/webui/src/components/ProvidersView.vue b/webui/src/components/ProvidersView.vue index 664f501..0484164 100644 --- a/webui/src/components/ProvidersView.vue +++ b/webui/src/components/ProvidersView.vue @@ -25,7 +25,7 @@ import { formatAgo, formatContextWindow, formatLatencyMs, formatUntil, refreshDe import { isModelDegraded } from '@/lib/provider-rate' import { type ExpandState, clearExpandOverrides, computeExpandedItems, toggleItemExpand } from '@/lib/search-expand' import { useDashboardStore } from '@/stores/dashboard' -import type { AdminAliasView, AdminLatencyView, AdminModelMetaView, AdminModelRateView, AdminProviderView } from '@/types/api' +import type { AdminAliasView, AdminLatencyView, AdminModelMetaView, AdminModelRateView, AdminProvenanceView, AdminProviderView } from '@/types/api' // This component backs the "Providers" tab (App.vue) — a UI-label rename // only. The data it renders still comes from GET /admin/api/overview @@ -251,6 +251,40 @@ function nonStreamingLatencyTitle(v: AdminLatencyView): string { return `Average total response time, including generation — not a load-sensitive signal like streaming ttfb, since this provider buffers the whole completion before sending anything. ${latencyObservationCount(v)}. ${PER_REPLICA_CAVEAT}` } +// --- usage provenance (feat: expose token-accounting provenance) --- +// +// Token accounting has three provenances (see AdminProvenanceView's own +// doc comment): reported (the healthy default — no badge, mirroring the +// health/latency badges' own "nothing to show reads as no badge" +// convention), estimated (a non-streaming response carried no usage, so +// prompt was substituted from request body size), and unbilled (a +// streaming response carried no usage, so the request was counted but +// zero tokens were billed — a provider potentially serving completions +// entirely free against every budget). estimated/unbilled are two +// DISTINCT badges, never merged into one "not reported" indicator — +// collapsing them would hide exactly the fact this feature exists to +// surface (task brief's own correctness rule). + +/** estimatedProvenance/unbilledProvenance pull p.provenance's two known kind keys (admin.go's buildAdminProvenanceViews emits only these two strings, never "reported") — undefined when that kind has no observations yet. */ +function estimatedProvenance(p: AdminProviderView): AdminProvenanceView | undefined { + return p.provenance?.estimated +} +function unbilledProvenance(p: AdminProviderView): AdminProvenanceView | undefined { + return p.provenance?.unbilled +} + +/** estimatedProvenanceTitle is the estimated-accounting badge's hover/title detail. */ +function estimatedProvenanceTitle(v: AdminProvenanceView): string { + const plural = v.requests === 1 ? '' : 's' + return `${v.requests} non-streaming response${plural} carried no usage from the provider — prompt tokens were estimated from request body size instead (${v.tokens} tokens billed from the estimate), completion billed as zero. ${PER_REPLICA_CAVEAT}` +} + +/** unbilledProvenanceTitle is the unbilled-accounting badge's hover/title detail — explicit about the budget-exposure implication (task brief's own framing). */ +function unbilledProvenanceTitle(v: AdminProvenanceView): string { + const plural = v.requests === 1 ? '' : 's' + return `${v.requests} streaming response${plural} carried no usage from the provider — the request was counted but zero tokens were billed. If this keeps happening, this provider may be serving completions entirely free against every budget. ${PER_REPLICA_CAVEAT}` +} + // --- model aliases (sortable DataTable, operator directive) --- // // The alias id gets the ModelChip copy treatment (an alias name IS the @@ -419,6 +453,33 @@ const aliasEmptyMessage = computed(() => > non-streaming avg {{ formatLatencyMs(nonStreamingLatency(p)!.avgDurationMs as number) }} + + + estimated {{ estimatedProvenance(p)!.requests }} + + + unbilled {{ unbilledProvenance(p)!.requests }} + > + /** + * provenance summarizes non-reported usage accounting for this + * provider (admin.go: adminProviderView.Provenance — feat: expose + * token-accounting provenance), keyed by provenance kind: 'estimated' + * or 'unbilled' only — 'reported' (the default, healthy case) is never + * a key here, see AdminProvenanceView's own doc comment for why. + * Undefined entirely for a provider with no estimated/unbilled outcome + * yet, or a deployment with metrics collection gated off — mirroring + * latency's identical "undefined, never an empty object" convention + * above. + * + * PER-REPLICA, IN-PROCESS: sourced from the same in-process accumulator + * as latency above — see that field's own caveat, which applies here + * unchanged. + */ + provenance?: Partial> } export interface AdminRedisView {