mirror of
https://github.com/lukaszraczylo/traefik-llmgateway.git
synced 2026-10-12 04:54:45 +00:00
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.
105 lines
3.6 KiB
Go
105 lines
3.6 KiB
Go
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)
|
|
}
|
|
}
|