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 {