feat(admin,webui): metadata exposure

admin.go: adminProviderView gains ModelMeta (map[string]adminModelMetaView,
keyed by upstream model id) and adminAliasView gains its own ModelMeta —
both built via g.registry.resolveMetaFor/resolveMetaForAliasName
(feature v0.23). adminModelMetaView uses pointer fields
(*int/*float64), not plain zero values: a nil pointer means unknown, a
non-nil pointer (even one pointing at 0) means known, so a free model's
real zero cost never gets confused with an absent one over JSON.
resolveMetaForAliasName resolves an alias's metadata for this
unrestricted admin view via an internal &group{} (GroupConfig's own
documented empty-means-all semantics) — buildAdminOverview already
shows every provider/alias unconditionally, with no group filtering
anywhere else in its response either.

webui/src/types/api.ts: AdminModelMetaView mirrors the Go shape
(optional numbers, undefined means unknown); AdminProviderView.modelMeta
and AdminAliasView.modelMeta wire it through.

webui/src/lib/format.ts: formatContextWindow (binary-K "256k"-style),
formatModelCostHover ("in $X / out $Y per MTok", or "free"), and
their shared formatUsdPerMTok building block.

ModelChip.vue (operator scope refinement): every chip — the accordion's
model chips, the alias table's chips, a group's usage-detail chips —
gains optional contextTokens/inputPerMTokUsd/outputPerMTokUsd props.
When known, they append a resolved-metadata phrase to the existing
copy-state hover text, bound to BOTH title and aria-label (the same
mouse-vs-keyboard/screen-reader parity ProviderRateBadge.vue already
applies); when nothing is known, the hover is unchanged from before this
feature. An alias's chip carries its INHERITED metadata, matching the
alias-inheritance rule the backend already applies.

ProvidersView.vue: the accordion's model list gains a visible, muted
context-only chip ("256k") beside each ModelChip — cost stays
hover-only on the chip itself, per the operator's own "chips +
hover would be redundant on price" resolution.
This commit is contained in:
2026-08-22 01:36:16 +01:00
parent 0c61296c73
commit eac0862d68
7 changed files with 438 additions and 18 deletions
+104
View File
@@ -0,0 +1,104 @@
package traefikllmgateway
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
)
// TestAdminOverview_ModelMeta_KnownAndUnknownFields proves GET
// /admin/api/overview's per-provider ModelMeta map (feature v0.23)
// carries a resolved entry for every known model, with unknown fields
// omitted (nil pointers) rather than zeroed.
func TestAdminOverview_ModelMeta_KnownAndUnknownFields(t *testing.T) {
t.Parallel()
cfg := newAdminTestConfig()
cfg.ModelMeta = map[string]*ModelMetaConfig{
"alpha/a-model-1": {ContextTokens: 128000, InputCostPerMTokMicroUSD: 1_250_000, OutputCostPerMTokMicroUSD: 10_000_000},
}
h, _ := newAdminGatewayHandle(t, cfg)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, adminRequest(http.MethodGet, adminOverviewPath, "sk-admin1"))
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want 200, body=%s", rec.Code, rec.Body.String())
}
var got adminOverviewResponse
if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
t.Fatalf("decode: %v", err)
}
var alpha *adminProviderView
for i := range got.Providers {
if got.Providers[i].Name == "alpha" {
alpha = &got.Providers[i]
}
}
if alpha == nil {
t.Fatalf("providers missing alpha: %+v", got.Providers)
}
known, ok := alpha.ModelMeta["a-model-1"]
if !ok {
t.Fatalf("alpha.ModelMeta missing a-model-1: %+v", alpha.ModelMeta)
}
if known.ContextTokens == nil || *known.ContextTokens != 128000 {
t.Errorf("a-model-1 ContextTokens = %v, want *128000", known.ContextTokens)
}
if known.InputPerMTokUSD == nil || *known.InputPerMTokUSD != 1.25 {
t.Errorf("a-model-1 InputPerMTokUSD = %v, want *1.25", known.InputPerMTokUSD)
}
if known.OutputPerMTokUSD == nil || *known.OutputPerMTokUSD != 10 {
t.Errorf("a-model-1 OutputPerMTokUSD = %v, want *10", known.OutputPerMTokUSD)
}
unknown, ok := alpha.ModelMeta["a-model-2"]
if !ok {
t.Fatalf("alpha.ModelMeta missing a-model-2 entry entirely (must be present, just all-unknown): %+v", alpha.ModelMeta)
}
if unknown.ContextTokens != nil || unknown.InputPerMTokUSD != nil || unknown.OutputPerMTokUSD != nil {
t.Errorf("a-model-2 (nothing known) = %+v, want every field nil", unknown)
}
}
// TestAdminOverview_AliasModelMeta_Inherited proves GET
// /admin/api/overview's alias rows carry the alias's INHERITED metadata
// from its target (feature v0.23, hover-detail refinement).
func TestAdminOverview_AliasModelMeta_Inherited(t *testing.T) {
t.Parallel()
cfg := newAdminTestConfig()
cfg.ModelAliases = map[string]string{"aliased/coding": "alpha/a-model-1"}
cfg.ModelMeta = map[string]*ModelMetaConfig{
"alpha/a-model-1": {ContextTokens: 200000, Free: true},
}
h, _ := newAdminGatewayHandle(t, cfg)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, adminRequest(http.MethodGet, adminOverviewPath, "sk-admin1"))
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want 200, body=%s", rec.Code, rec.Body.String())
}
var got adminOverviewResponse
if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
t.Fatalf("decode: %v", err)
}
var alias *adminAliasView
for i := range got.Aliases {
if got.Aliases[i].Alias == "aliased/coding" {
alias = &got.Aliases[i]
}
}
if alias == nil {
t.Fatalf("aliases missing aliased/coding: %+v", got.Aliases)
}
if alias.ModelMeta.ContextTokens == nil || *alias.ModelMeta.ContextTokens != 200000 {
t.Errorf("alias ContextTokens = %v, want *200000 (inherited)", alias.ModelMeta.ContextTokens)
}
if alias.ModelMeta.InputPerMTokUSD == nil || *alias.ModelMeta.InputPerMTokUSD != 0 {
t.Errorf("alias InputPerMTokUSD = %v, want *0 (inherited free)", alias.ModelMeta.InputPerMTokUSD)
}
}