mirror of
https://github.com/lukaszraczylo/traefik-llmgateway.git
synced 2026-10-12 04:54:45 +00:00
fix(pricing): content-digest snapshot instead of a wall-clock date
The generated pricing_data_gen.go header used to stamp today's date on every `make pricing-sync` run, so a re-run against genuinely unchanged upstream data still touched the file and produced a spurious daily diff with nothing real to review. The header now records a short SHA-256 digest of the FETCHED bytes instead — identical upstream data always reproduces a byte-identical generated file. Folded review minor.
This commit is contained in:
+3
-2
@@ -4,8 +4,9 @@ package traefikllmgateway
|
|||||||
// model_prices_and_context_window.json — DO NOT EDIT.
|
// model_prices_and_context_window.json — DO NOT EDIT.
|
||||||
//
|
//
|
||||||
// Source: https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json
|
// Source: https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json
|
||||||
// Snapshot: 2026-08-22, 236 entries kept (pruned from 3111 upstream entries —
|
// Source digest: sha256:0e8d23239c27
|
||||||
// see tools/pricing-sync/main.go's own doc comment for the exact pruning
|
// 236 entries kept (pruned from 3111 upstream entries — see
|
||||||
|
// tools/pricing-sync/main.go's own doc comment for the exact pruning
|
||||||
// rule and bare-id collision resolution).
|
// rule and bare-id collision resolution).
|
||||||
//
|
//
|
||||||
// Regenerate with: make pricing-sync
|
// Regenerate with: make pricing-sync
|
||||||
|
|||||||
@@ -92,6 +92,8 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"go/format"
|
"go/format"
|
||||||
@@ -272,7 +274,7 @@ func run() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
src, err := renderGoSource(table, sourceMeta{
|
src, err := renderGoSource(table, sourceMeta{
|
||||||
snapshot: time.Now().UTC().Format("2006-01-02"),
|
sourceDigest: contentDigest(data),
|
||||||
totalUpstream: totalUpstream,
|
totalUpstream: totalUpstream,
|
||||||
kept: len(table),
|
kept: len(table),
|
||||||
})
|
})
|
||||||
@@ -450,9 +452,32 @@ func providerPriorityIndex(provider string) int {
|
|||||||
return len(providerPriority)
|
return len(providerPriority)
|
||||||
}
|
}
|
||||||
|
|
||||||
// sourceMeta is renderGoSource's header-comment material.
|
// contentDigestLen is how many leading hex characters of the SHA-256
|
||||||
|
// digest contentDigest keeps — enough to be a stable, practically
|
||||||
|
// collision-free content fingerprint for a header comment, without
|
||||||
|
// printing a full 64-character hash nobody reads in full.
|
||||||
|
const contentDigestLen = 12
|
||||||
|
|
||||||
|
// contentDigest returns a short SHA-256 fingerprint of data, hex
|
||||||
|
// encoded — the generated file's snapshot label (review fix, folded
|
||||||
|
// minor), so a `make pricing-sync` re-run against UNCHANGED upstream
|
||||||
|
// bytes reproduces an identical file, with no diff at all, rather than a
|
||||||
|
// wall-clock date bumping every run regardless of whether the data
|
||||||
|
// itself changed.
|
||||||
|
func contentDigest(data []byte) string {
|
||||||
|
sum := sha256.Sum256(data)
|
||||||
|
return hex.EncodeToString(sum[:])[:contentDigestLen]
|
||||||
|
}
|
||||||
|
|
||||||
|
// sourceMeta is renderGoSource's header-comment material. sourceDigest is
|
||||||
|
// a short content hash of the FETCHED upstream bytes (review fix,
|
||||||
|
// folded minor), not a wall-clock date: a date-stamped header made every
|
||||||
|
// `make pricing-sync` run touch the generated file even when the
|
||||||
|
// upstream data had not changed at all, producing a spurious daily diff
|
||||||
|
// with nothing real to review. A content hash changes only when the
|
||||||
|
// underlying data actually changes.
|
||||||
type sourceMeta struct {
|
type sourceMeta struct {
|
||||||
snapshot string
|
sourceDigest string
|
||||||
totalUpstream int
|
totalUpstream int
|
||||||
kept int
|
kept int
|
||||||
}
|
}
|
||||||
@@ -476,8 +501,9 @@ func renderGoSource(table map[string]builtinEntry, meta sourceMeta) (string, err
|
|||||||
fmt.Fprintf(&b, "// model_prices_and_context_window.json — DO NOT EDIT.\n")
|
fmt.Fprintf(&b, "// model_prices_and_context_window.json — DO NOT EDIT.\n")
|
||||||
fmt.Fprintf(&b, "//\n")
|
fmt.Fprintf(&b, "//\n")
|
||||||
fmt.Fprintf(&b, "// Source: %s\n", litellmSourceURL)
|
fmt.Fprintf(&b, "// Source: %s\n", litellmSourceURL)
|
||||||
fmt.Fprintf(&b, "// Snapshot: %s, %d entries kept (pruned from %d upstream entries —\n", meta.snapshot, meta.kept, meta.totalUpstream)
|
fmt.Fprintf(&b, "// Source digest: sha256:%s\n", meta.sourceDigest)
|
||||||
fmt.Fprintf(&b, "// see tools/pricing-sync/main.go's own doc comment for the exact pruning\n")
|
fmt.Fprintf(&b, "// %d entries kept (pruned from %d upstream entries — see\n", meta.kept, meta.totalUpstream)
|
||||||
|
fmt.Fprintf(&b, "// tools/pricing-sync/main.go's own doc comment for the exact pruning\n")
|
||||||
fmt.Fprintf(&b, "// rule and bare-id collision resolution).\n")
|
fmt.Fprintf(&b, "// rule and bare-id collision resolution).\n")
|
||||||
fmt.Fprintf(&b, "//\n")
|
fmt.Fprintf(&b, "//\n")
|
||||||
fmt.Fprintf(&b, "// Regenerate with: make pricing-sync\n")
|
fmt.Fprintf(&b, "// Regenerate with: make pricing-sync\n")
|
||||||
|
|||||||
@@ -168,13 +168,13 @@ func TestParseAndPrune_Golden(t *testing.T) {
|
|||||||
// TestRenderGoSource_ValidSortedGo asserts renderGoSource produces
|
// TestRenderGoSource_ValidSortedGo asserts renderGoSource produces
|
||||||
// syntactically valid, gofmt-canonical Go source (parseable by go/
|
// syntactically valid, gofmt-canonical Go source (parseable by go/
|
||||||
// parser) whose entries appear in sorted key order, and that its header
|
// parser) whose entries appear in sorted key order, and that its header
|
||||||
// documents the pruning provenance (source URL, snapshot, counts).
|
// documents the pruning provenance (source URL, content digest, counts).
|
||||||
func TestRenderGoSource_ValidSortedGo(t *testing.T) {
|
func TestRenderGoSource_ValidSortedGo(t *testing.T) {
|
||||||
table := map[string]builtinEntry{
|
table := map[string]builtinEntry{
|
||||||
"zzz-model": {ContextTokens: 1, InputCostPerMTokMicroUSD: 1, OutputCostPerMTokMicroUSD: 1},
|
"zzz-model": {ContextTokens: 1, InputCostPerMTokMicroUSD: 1, OutputCostPerMTokMicroUSD: 1},
|
||||||
"aaa-model": {ContextTokens: 2, InputCostPerMTokMicroUSD: 2, OutputCostPerMTokMicroUSD: 2},
|
"aaa-model": {ContextTokens: 2, InputCostPerMTokMicroUSD: 2, OutputCostPerMTokMicroUSD: 2},
|
||||||
}
|
}
|
||||||
src, err := renderGoSource(table, sourceMeta{snapshot: "2026-08-22", totalUpstream: 3111, kept: 2})
|
src, err := renderGoSource(table, sourceMeta{sourceDigest: "deadbeefcafe", totalUpstream: 3111, kept: 2})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("renderGoSource: %v", err)
|
t.Fatalf("renderGoSource: %v", err)
|
||||||
}
|
}
|
||||||
@@ -188,8 +188,8 @@ func TestRenderGoSource_ValidSortedGo(t *testing.T) {
|
|||||||
if !strings.Contains(src, litellmSourceURL) {
|
if !strings.Contains(src, litellmSourceURL) {
|
||||||
t.Error("generated source missing source URL provenance comment")
|
t.Error("generated source missing source URL provenance comment")
|
||||||
}
|
}
|
||||||
if !strings.Contains(src, "2026-08-22") || !strings.Contains(src, "3111") {
|
if !strings.Contains(src, "deadbeefcafe") || !strings.Contains(src, "3111") {
|
||||||
t.Error("generated source missing snapshot date / upstream count provenance comment")
|
t.Error("generated source missing source-digest / upstream count provenance comment")
|
||||||
}
|
}
|
||||||
if got, want := strings.Index(src, `"aaa-model"`), strings.Index(src, `"zzz-model"`); got == -1 || want == -1 || got > want {
|
if got, want := strings.Index(src, `"aaa-model"`), strings.Index(src, `"zzz-model"`); got == -1 || want == -1 || got > want {
|
||||||
t.Errorf(`entries not sorted: index("aaa-model")=%d, index("zzz-model")=%d, want aaa before zzz`, got, want)
|
t.Errorf(`entries not sorted: index("aaa-model")=%d, index("zzz-model")=%d, want aaa before zzz`, got, want)
|
||||||
@@ -242,3 +242,25 @@ func TestApplyNamingBridges_DoesNotOverwriteExistingEntry(t *testing.T) {
|
|||||||
t.Errorf(`table["MiniMax-M2.1-highspeed"] = %+v, want %+v (its own direct LiteLLM entry, not the bridged duplicate)`, got, want)
|
t.Errorf(`table["MiniMax-M2.1-highspeed"] = %+v, want %+v (its own direct LiteLLM entry, not the bridged duplicate)`, got, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestContentDigest covers the review-fix replacement for a wall-clock
|
||||||
|
// snapshot date: identical bytes must always produce an identical
|
||||||
|
// digest (the whole point — a `make pricing-sync` re-run against
|
||||||
|
// unchanged upstream data must reproduce a byte-identical file, with no
|
||||||
|
// spurious diff), different bytes must produce a different digest, and
|
||||||
|
// the digest is always exactly contentDigestLen hex characters.
|
||||||
|
func TestContentDigest(t *testing.T) {
|
||||||
|
a := contentDigest([]byte(`{"gpt-5":{}}`))
|
||||||
|
b := contentDigest([]byte(`{"gpt-5":{}}`))
|
||||||
|
c := contentDigest([]byte(`{"gpt-5":{"changed":true}}`))
|
||||||
|
|
||||||
|
if a != b {
|
||||||
|
t.Errorf("contentDigest is not deterministic: %q != %q for identical input", a, b)
|
||||||
|
}
|
||||||
|
if a == c {
|
||||||
|
t.Errorf("contentDigest(%q) == contentDigest(%q) = %q, want different digests for different input", `{"gpt-5":{}}`, `{"gpt-5":{"changed":true}}`, a)
|
||||||
|
}
|
||||||
|
if len(a) != contentDigestLen {
|
||||||
|
t.Errorf("len(contentDigest(...)) = %d, want %d", len(a), contentDigestLen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user