mirror of
https://github.com/lukaszraczylo/traefik-llmgateway.git
synced 2026-10-19 05:47:48 +00:00
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.
This commit is contained in:
@@ -25,7 +25,7 @@ import { formatAgo, formatContextWindow, formatLatencyMs, formatUntil, refreshDe
|
|||||||
import { isModelDegraded } from '@/lib/provider-rate'
|
import { isModelDegraded } from '@/lib/provider-rate'
|
||||||
import { type ExpandState, clearExpandOverrides, computeExpandedItems, toggleItemExpand } from '@/lib/search-expand'
|
import { type ExpandState, clearExpandOverrides, computeExpandedItems, toggleItemExpand } from '@/lib/search-expand'
|
||||||
import { useDashboardStore } from '@/stores/dashboard'
|
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
|
// This component backs the "Providers" tab (App.vue) — a UI-label rename
|
||||||
// only. The data it renders still comes from GET /admin/api/overview
|
// 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}`
|
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) ---
|
// --- model aliases (sortable DataTable, operator directive) ---
|
||||||
//
|
//
|
||||||
// The alias id gets the ModelChip copy treatment (an alias name IS the
|
// 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) }}
|
non-streaming avg {{ formatLatencyMs(nonStreamingLatency(p)!.avgDurationMs as number) }}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
<!--
|
||||||
|
Usage provenance (feat: expose token-accounting
|
||||||
|
provenance): no badge at all for a kind with no
|
||||||
|
observations yet, mirroring the health/latency badges'
|
||||||
|
own convention above. estimated and unbilled are two
|
||||||
|
distinct badges — never merged (correctness rule, task
|
||||||
|
brief) — since one substitutes an approximation and the
|
||||||
|
other bills nothing at all.
|
||||||
|
-->
|
||||||
|
<Badge
|
||||||
|
v-if="estimatedProvenance(p)"
|
||||||
|
as="span"
|
||||||
|
variant="secondary"
|
||||||
|
class="font-normal tabular-nums"
|
||||||
|
:title="estimatedProvenanceTitle(estimatedProvenance(p) as AdminProvenanceView)"
|
||||||
|
>
|
||||||
|
estimated {{ estimatedProvenance(p)!.requests }}
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
v-if="unbilledProvenance(p)"
|
||||||
|
as="span"
|
||||||
|
variant="destructive"
|
||||||
|
class="font-normal tabular-nums"
|
||||||
|
:title="unbilledProvenanceTitle(unbilledProvenance(p) as AdminProvenanceView)"
|
||||||
|
>
|
||||||
|
unbilled {{ unbilledProvenance(p)!.requests }}
|
||||||
|
</Badge>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
v-if="p.lastErr"
|
v-if="p.lastErr"
|
||||||
:icon="faTriangleExclamation"
|
:icon="faTriangleExclamation"
|
||||||
|
|||||||
@@ -58,6 +58,26 @@ export interface AdminLatencyView {
|
|||||||
count?: number
|
count?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One provider's one non-reported provenance kind's compact usage-
|
||||||
|
* accounting summary (admin.go: adminProvenanceView — feat: expose
|
||||||
|
* token-accounting provenance). "estimated" means a non-streaming
|
||||||
|
* response carried no usage from the provider, so prompt was substituted
|
||||||
|
* with an estimate (ceil(request body length / 4)) and completion billed
|
||||||
|
* as zero. "unbilled" means a streaming response carried no usage, so
|
||||||
|
* the request was counted but zero tokens were billed — a provider
|
||||||
|
* silently ignoring stream_options.include_usage serves that completion
|
||||||
|
* entirely free against every budget. Billing itself is unaffected by
|
||||||
|
* this data; it only reports how an already-billed number was arrived
|
||||||
|
* at. requests/tokens are never fabricated zeros: this view only ever
|
||||||
|
* exists (see AdminProviderView.provenance) for a kind that was actually
|
||||||
|
* observed at least once.
|
||||||
|
*/
|
||||||
|
export interface AdminProvenanceView {
|
||||||
|
requests: number
|
||||||
|
tokens: number
|
||||||
|
}
|
||||||
|
|
||||||
export interface AdminProviderView {
|
export interface AdminProviderView {
|
||||||
name: string
|
name: string
|
||||||
type: string
|
type: string
|
||||||
@@ -142,6 +162,22 @@ export interface AdminProviderView {
|
|||||||
* together or present either one as a bare, unlabeled "latency".
|
* together or present either one as a bare, unlabeled "latency".
|
||||||
*/
|
*/
|
||||||
latency?: Partial<Record<'streaming' | 'non-streaming', AdminLatencyView>>
|
latency?: Partial<Record<'streaming' | 'non-streaming', AdminLatencyView>>
|
||||||
|
/**
|
||||||
|
* 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<Record<'estimated' | 'unbilled', AdminProvenanceView>>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AdminRedisView {
|
export interface AdminRedisView {
|
||||||
|
|||||||
Reference in New Issue
Block a user