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 {