From 933d869e323586b50dd52c60601882039885b0f9 Mon Sep 17 00:00:00 2001 From: Lukasz Raczylo Date: Sat, 22 Aug 2026 02:11:22 +0100 Subject: [PATCH] fix(modelmeta): free-tier suffix layer (review IMPORTANT-1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A ":free"-suffixed model id (a real provider naming convention, e.g. OpenRouter's own live API) is free by naming convention alone, independent of whatever the built-in table does or does not track for that exact id. resolveModelMeta's cost switch gains a new case, ranked above the built-in table and below an operator's own config override — so a provider's free-tier naming is never shadowed by a stale or coincidental paid built-in price under the identical key, and an operator can still override it explicitly if they choose. Context is untouched by this rule: it keeps resolving through the normal config/ discovery/builtin/absent layers, never inherited from an unrelated entry. Also makes resolveModelMeta's own Free case assign 0, 0, true explicitly (matching resolveAliasModelMeta's existing style) instead of relying on the zero-value default — defensive consistency, folded review minor. Independently reviewed and reproduced against the same live LiteLLM fetch: zero ":free"-suffixed keys exist anywhere in the 3111-entry upstream file, confirming the earlier grounding note in the v0.23 report. This layer is a general resolution rule for a naming convention provider APIs use in live discovery, not something the pruned build-time table needs to carry any entries for. --- modelmeta.go | 29 +++++++++++++++++++--- modelmeta_test.go | 63 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 4 deletions(-) diff --git a/modelmeta.go b/modelmeta.go index a7d5fcc..da056cf 100644 --- a/modelmeta.go +++ b/modelmeta.go @@ -3,6 +3,7 @@ package traefikllmgateway import ( "fmt" "sort" + "strings" ) // microUSDPerUSD scales a micro-USD integer (ModelMetaConfig/ @@ -15,6 +16,17 @@ import ( // not be conflated just because they share a value. const microUSDPerUSD = 1_000_000 +// freeTierSuffix marks a discovered model id as free by naming +// convention alone (review addendum, feature v0.23): a real provider +// (OpenRouter's own live API is the known case) reports free-tier model +// ids with this exact suffix, independent of whatever LiteLLM's static +// pricing file does or does not track for that provider. resolveModelMeta +// checks this AFTER an operator's own config override and BEFORE the +// built-in table, so a ":free"-suffixed id never falls through to a +// builtin entry's paid price for that same key, and an operator can +// still override it (e.g. to attach a context window) if they choose. +const freeTierSuffix = ":free" + // resolvedModelMeta is resolveModelMeta's result: a model's context // window and per-token cost, each resolved independently through its own // layering (feature v0.23). ContextKnown/CostKnown are authoritative — a @@ -44,12 +56,19 @@ type resolvedModelMeta struct { // names, so an exact "provider/model" form makes no sense for that // table the way it does for cfg; otherwise unknown. // - Cost: the config override wins when it sets Free (an explicit, -// known zero) or either cost field > 0; otherwise the built-in -// table. Discovery never reports cost, so there is no discovery -// layer for it. +// known zero) or either cost field > 0; otherwise a ":free"-suffixed +// model id (freeTierSuffix — review addendum) wins next, ahead of +// the built-in table, so a provider's own free-tier naming +// convention is never shadowed by a paid built-in price under the +// identical key; otherwise the built-in table. Discovery never +// reports cost, so there is no discovery layer for it. // // Absent at every applicable layer is a genuinely unknown value and is // never invented — ContextKnown/CostKnown both stay false in that case. +// The freeTierSuffix rule affects ONLY cost: a ":free" id's context +// still runs through the normal context layering above (config/ +// discovery/builtin/absent), never inherited from some other, unrelated +// entry. func resolveModelMeta(provider, model string, cfg map[string]*ModelMetaConfig, discoveredContext int, discoveredKnown bool) resolvedModelMeta { var out resolvedModelMeta override := lookupModelMetaConfig(provider, model, cfg) @@ -66,11 +85,13 @@ func resolveModelMeta(provider, model string, cfg map[string]*ModelMetaConfig, d switch { case override != nil && override.Free: - out.CostKnown = true // both cost fields stay 0 — explicit, known zero + out.InputCostPerMTokMicroUSD, out.OutputCostPerMTokMicroUSD, out.CostKnown = 0, 0, true case override != nil && (override.InputCostPerMTokMicroUSD > 0 || override.OutputCostPerMTokMicroUSD > 0): out.InputCostPerMTokMicroUSD = override.InputCostPerMTokMicroUSD out.OutputCostPerMTokMicroUSD = override.OutputCostPerMTokMicroUSD out.CostKnown = true + case strings.HasSuffix(model, freeTierSuffix): + out.InputCostPerMTokMicroUSD, out.OutputCostPerMTokMicroUSD, out.CostKnown = 0, 0, true case builtinOK && (builtin.InputCostPerMTokMicroUSD > 0 || builtin.OutputCostPerMTokMicroUSD > 0): out.InputCostPerMTokMicroUSD = builtin.InputCostPerMTokMicroUSD out.OutputCostPerMTokMicroUSD = builtin.OutputCostPerMTokMicroUSD diff --git a/modelmeta_test.go b/modelmeta_test.go index 5114b25..1ffe81f 100644 --- a/modelmeta_test.go +++ b/modelmeta_test.go @@ -126,6 +126,69 @@ func TestResolveModelMeta_BuiltinTable(t *testing.T) { } } +// TestResolveModelMeta_FreeTierSuffix covers freeTierSuffix's layer +// (review addendum): a ":free"-suffixed id is free by naming convention +// alone, ranked above the built-in table but below an operator's own +// config override, and never invents a context window. +func TestResolveModelMeta_FreeTierSuffix(t *testing.T) { + t.Run("free-by-suffix wins over a builtin paid entry under the identical key", func(t *testing.T) { + const id = "review-fixture-model:free" + t.Cleanup(func() { delete(builtinModelMetaTable, id) }) + builtinModelMetaTable[id] = builtinModelMeta{ContextTokens: 999, InputCostPerMTokMicroUSD: 5_000_000, OutputCostPerMTokMicroUSD: 5_000_000} + + got := resolveModelMeta("openrouter", id, nil, 0, false) + if !got.CostKnown || got.InputCostPerMTokMicroUSD != 0 || got.OutputCostPerMTokMicroUSD != 0 { + t.Errorf("cost = (%d, %d, known=%v), want (0, 0, true) — free-by-suffix must beat a builtin paid entry", got.InputCostPerMTokMicroUSD, got.OutputCostPerMTokMicroUSD, got.CostKnown) + } + // Context still resolves independently — the builtin entry's own + // ContextTokens is a real, known value for this exact key, so it + // is NOT suppressed by the free-tier cost rule (freeTierSuffix + // affects cost only). + if !got.ContextKnown || got.ContextTokens != 999 { + t.Errorf("context = (%d, known=%v), want (999, true) — freeTierSuffix must not touch context resolution", got.ContextTokens, got.ContextKnown) + } + }) + + t.Run("free-by-suffix never invents context when nothing else knows it", func(t *testing.T) { + got := resolveModelMeta("openrouter", "some-model-nobody-prices:free", nil, 0, false) + if got.ContextKnown { + t.Errorf("context = %+v, want unknown — free-by-suffix must never invent a context window", got) + } + if !got.CostKnown || got.InputCostPerMTokMicroUSD != 0 || got.OutputCostPerMTokMicroUSD != 0 { + t.Errorf("cost = %+v, want known zero", got) + } + }) + + t.Run("an explicit operator override still beats free-by-suffix", func(t *testing.T) { + const id = "review-fixture-model-2:free" + cfg := map[string]*ModelMetaConfig{id: {InputCostPerMTokMicroUSD: 1, OutputCostPerMTokMicroUSD: 2}} + got := resolveModelMeta("openrouter", id, cfg, 0, false) + if !got.CostKnown || got.InputCostPerMTokMicroUSD != 1 || got.OutputCostPerMTokMicroUSD != 2 { + t.Errorf("cost = (%d, %d, known=%v), want (1, 2, true) — an explicit operator override must win over the free-by-suffix rule", got.InputCostPerMTokMicroUSD, got.OutputCostPerMTokMicroUSD, got.CostKnown) + } + }) + + t.Run("a non-free config override still beats free-by-suffix even at zero context", func(t *testing.T) { + const id = "review-fixture-model-3:free" + cfg := map[string]*ModelMetaConfig{id: {Free: false, InputCostPerMTokMicroUSD: 0, OutputCostPerMTokMicroUSD: 0}} + // A present-but-all-zero, non-Free override entry sets nothing at + // this layer (matches ModelMetaConfig's own "zero falls through" + // contract) — the free-tier suffix rule still applies underneath + // it, not the (absent) override. + got := resolveModelMeta("openrouter", id, cfg, 0, false) + if !got.CostKnown || got.InputCostPerMTokMicroUSD != 0 { + t.Errorf("cost = %+v, want known zero via the free-by-suffix fallback", got) + } + }) + + t.Run("an id without the suffix is unaffected", func(t *testing.T) { + got := resolveModelMeta("openrouter", "plain-model-id", nil, 0, false) + if got.CostKnown { + t.Errorf("cost = %+v, want unknown for a plain id with no :free suffix and no other layer", got) + } + }) +} + // TestLookupModelMetaConfig covers lookupModelMetaConfig's own // precedence and nil-safety directly. func TestLookupModelMetaConfig(t *testing.T) {