diff --git a/internal/billing/compile.go b/internal/billing/compile.go index 532ebc9..474e846 100644 --- a/internal/billing/compile.go +++ b/internal/billing/compile.go @@ -200,13 +200,24 @@ func applyRule(prices map[string]interface{}, rule *config.BillingRule, srcName } models[modelID] = entry } - // The source itself needs an entry too, otherwise priceFor() marks the - // request priced only when the model happens to be listed, and a model - // served from this URL but absent from the rule would be billed at the - // DEFAULT (zero) rate — indistinguishable from unpriced in the totals. - if _, ok := prices["sources"].(map[string]interface{})[srcName]; !ok { - prices["sources"].(map[string]interface{})[srcName] = map[string]interface{}{ - "prompt": 0.0, "completion": 0.0, "per_request": 0.0, + // A specific-URL token rule needs a source entry so a model served + // from THIS url but absent from `rule.Models` is still recognised as + // belonging to a priced provider. + // + // A WILDCARD rule ("*") must NOT get one. "*" is a model catalogue — it + // says "these model ids have these prices, wherever they come from" — + // and marking every source priced at {0,0} would make priceFor() set + // priced=true for every model the catalogue does NOT list. Paid + // sources (commandcode, alittokenplan, …) would then record $0 instead + // of showing up in unpriced_reqs, which is precisely the silent + // zero-billing failure this whole plugin is built to make visible. + // The model lookup p.models[payload.model] already marks a listed + // model priced on its own, so the wildcard loses nothing. + if rule.URL != "*" { + if _, ok := prices["sources"].(map[string]interface{})[srcName]; !ok { + prices["sources"].(map[string]interface{})[srcName] = map[string]interface{}{ + "prompt": 0.0, "completion": 0.0, "per_request": 0.0, + } } } return nil diff --git a/internal/billing/wildcard_test.go b/internal/billing/wildcard_test.go new file mode 100644 index 0000000..ac25fb3 --- /dev/null +++ b/internal/billing/wildcard_test.go @@ -0,0 +1,88 @@ +package billing + +import ( + "testing" + + "llmsproxy/internal/config" +) + +// The wildcard token rule must not blanket-mark every source as priced. +// +// Why this is a hazard and not a style question: priceFor() sets priced=true as +// soon as the request's SOURCE has an entry in the price table, and that flag +// is what keeps a request OUT of unpriced_reqs. A "*" rule is a model +// catalogue — it prices specific model ids wherever they come from — but +// compiling it into a {0,0,0} entry on all 21 sources made every model the +// catalogue does not list look "priced at zero". Paid providers (commandcode, +// alittokenplan, …) then stopped appearing in the unpriced warnings, which is +// exactly the silent zero-billing failure the plugin exists to surface. +func TestWildcardTokenRuleDoesNotMarkEverySourcePriced(t *testing.T) { + sources := []config.Source{ + {Name: "free1", BaseURL: "https://free1.example.com/v1"}, + {Name: "paid1", BaseURL: "https://paid1.example.com/v1"}, + {Name: "paid2", BaseURL: "https://paid2.example.com/v1"}, + } + profile := &config.BillingProfile{ + ID: "p", + Rules: []config.BillingRule{ + {URL: "https://free1.example.com/v1", Mode: "free"}, + { + URL: "*", Mode: "token", + Models: map[string]config.BillingToken{ + "only-catalogued-model": {Prompt: "1.00", Completion: "2.00"}, + }, + }, + }, + } + prices, err := Compile(profile, sources) + if err != nil { + t.Fatalf("compile: %v", err) + } + srcs := prices["sources"].(map[string]interface{}) + + // The explicitly free source keeps its entry (free is a deliberate 0). + if _, ok := srcs["free1"]; !ok { + t.Error("the explicit free rule lost its source entry") + } + // Paid sources must NOT be marked, or their unlisted models bill $0. + for _, name := range []string{"paid1", "paid2"} { + if _, ok := srcs[name]; ok { + t.Errorf("★ wildcard token rule marked %q as priced at 0 — its unlisted "+ + "models will bill $0 instead of appearing as unpriced", name) + } + } + // The catalogue itself must still be present, or the rule prices nothing. + models := prices["models"].(map[string]interface{}) + if _, ok := models["only-catalogued-model"]; !ok { + t.Error("the wildcard rule did not publish its model price") + } +} + +// The inverse guard: a token rule for a SPECIFIC url still marks that source, +// because it genuinely declares pricing for the models served from it. +func TestSpecificURLTokenRuleStillMarksItsSource(t *testing.T) { + sources := []config.Source{ + {Name: "mine", BaseURL: "https://mine.example.com/v1"}, + {Name: "other", BaseURL: "https://other.example.com/v1"}, + } + profile := &config.BillingProfile{ + ID: "p", + Rules: []config.BillingRule{{ + URL: "https://mine.example.com/v1", Mode: "token", + Models: map[string]config.BillingToken{ + "m": {Prompt: "1.00", Completion: "2.00"}, + }, + }}, + } + prices, err := Compile(profile, sources) + if err != nil { + t.Fatalf("compile: %v", err) + } + srcs := prices["sources"].(map[string]interface{}) + if _, ok := srcs["mine"]; !ok { + t.Error("a specific-url token rule must still mark its own source priced") + } + if _, ok := srcs["other"]; ok { + t.Error("an unrelated source was marked priced by a specific-url rule") + } +}