mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-03 03:59:07 +02:00
[management] Return default rates with each discovered model
The endpoint reported pricing_known and then made the operator find the price themselves. The dashboard has nothing to prefill a model row with, so a discovered model either arrived at zero — silently metering every request against it as free — or had to be priced by hand against a table NetBird already ships. Each model now carries the rates it would actually be billed at. Rates come from the live default pricing table rather than the compiled-in catalog, because that is the table the synthesiser ships to the proxy: an operator running a defaults_llm_pricing.yaml would otherwise be shown one price in the form and charged another. It is the same lookup the catalog endpoint prefills from, so a model reached by either route prices identically — pinned by TestDiscoveredRatesMatchTheCatalogEndpoint, since the two are separate call paths that would otherwise drift. pricing_known now derives from that same lookup instead of a second pass over the compiled catalog, so "we can price this" and "here is the price" can no longer disagree. input_per_1k and output_per_1k are required and sent even at zero: an unpriced model is offered at zero and flagged rather than withheld — the vendor says the credential can reach it, and hiding it would hide a model the operator genuinely has. The cache rates stay absent when unset, matching the catalog response, because a zero there reads as "free" rather than "not applicable".
This commit is contained in:
@@ -31,6 +31,7 @@ import (
|
||||
"golang.org/x/oauth2/google"
|
||||
|
||||
"github.com/netbirdio/netbird/management/internals/modules/agentnetwork/catalog"
|
||||
"github.com/netbirdio/netbird/management/internals/modules/agentnetwork/pricing"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -75,6 +76,17 @@ type Model struct {
|
||||
// model. False means the operator must set rates, or the request would
|
||||
// meter at zero.
|
||||
PricingKnown bool
|
||||
// The rates below are the defaults for this model, taken from the same
|
||||
// table the proxy bills with, so the form prefills exactly what a request
|
||||
// would cost. All zero when PricingKnown is false — an unpriced model is
|
||||
// offered at zero and flagged, rather than withheld: the vendor says the
|
||||
// credential can reach it, and refusing to show it would hide a model the
|
||||
// operator genuinely has.
|
||||
InputPer1k float64
|
||||
OutputPer1k float64
|
||||
CachedInputPer1k float64
|
||||
CacheReadPer1k float64
|
||||
CacheCreationPer1k float64
|
||||
}
|
||||
|
||||
// Request identifies which vendor to ask and with what credential.
|
||||
@@ -329,14 +341,15 @@ func mintGCPToken(ctx context.Context, saKeyB64 string) (string, error) {
|
||||
return tok.AccessToken, nil
|
||||
}
|
||||
|
||||
// decorate turns raw vendor ids into the models the caller renders, marking
|
||||
// each with whether the shipped pricing table can price it.
|
||||
// decorate turns raw vendor ids into the models the caller renders, attaching
|
||||
// the rates the request would actually be billed at.
|
||||
//
|
||||
// Rates come from the live default pricing table rather than the compiled-in
|
||||
// catalog, because that is the table the synthesiser ships to the proxy: an
|
||||
// operator running a defaults_llm_pricing.yaml would otherwise be shown one
|
||||
// price in the form and charged another. It is also the same lookup the catalog
|
||||
// endpoint prefills from, so a model reached by either route prices identically.
|
||||
func decorate(entry catalog.Provider, ids []listedModel) []Model {
|
||||
priced := make(map[string]struct{}, len(entry.Models))
|
||||
for _, m := range entry.Models {
|
||||
priced[m.ID] = struct{}{}
|
||||
}
|
||||
|
||||
out := make([]Model, 0, len(ids))
|
||||
seen := make(map[string]struct{}, len(ids))
|
||||
for _, listed := range ids {
|
||||
@@ -348,11 +361,19 @@ func decorate(entry catalog.Provider, ids []listedModel) []Model {
|
||||
}
|
||||
seen[listed.id] = struct{}{}
|
||||
|
||||
// The catalog keys pricing by the normalised id while the vendor
|
||||
// issues the wire form, so normalise before asking whether we can
|
||||
// price it — otherwise every Bedrock profile would report unpriced.
|
||||
_, known := priced[normalizeForPricing(entry.ID, listed.id)]
|
||||
out = append(out, Model{ID: listed.id, Label: listed.label, PricingKnown: known})
|
||||
// The table keys pricing by the normalised id while the vendor issues
|
||||
// the wire form, so normalise before looking it up — otherwise every
|
||||
// Bedrock profile would report unpriced.
|
||||
model := Model{ID: listed.id, Label: listed.label}
|
||||
if rate, known := pricing.LookupDefault(entry.PricingSurfaces, normalizeForPricing(entry.ID, listed.id)); known {
|
||||
model.PricingKnown = true
|
||||
model.InputPer1k = rate.InputPer1k
|
||||
model.OutputPer1k = rate.OutputPer1k
|
||||
model.CachedInputPer1k = rate.CachedInputPer1k
|
||||
model.CacheReadPer1k = rate.CacheReadPer1k
|
||||
model.CacheCreationPer1k = rate.CacheCreationPer1k
|
||||
}
|
||||
out = append(out, model)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/netbirdio/netbird/management/internals/modules/agentnetwork/catalog"
|
||||
"github.com/netbirdio/netbird/management/internals/modules/agentnetwork/pricing"
|
||||
)
|
||||
|
||||
// stubTransport answers every request with one canned response and records the
|
||||
@@ -146,6 +147,42 @@ func TestFetchBedrockUsesTheControlPlaneAndKeepsWireIDs(t *testing.T) {
|
||||
"the catalog prices anthropic.claude-haiku-4-5, which this id normalises to")
|
||||
assert.False(t, models[1].PricingKnown,
|
||||
"cohere embed is not in the shipped Bedrock catalog, so the operator must price it")
|
||||
|
||||
// The rates travel with the model, so the form can prefill an editable row
|
||||
// rather than making the operator look every price up by hand.
|
||||
assert.Positive(t, models[0].InputPer1k, "a priced model must carry its input rate")
|
||||
assert.Positive(t, models[0].OutputPer1k, "a priced model must carry its output rate")
|
||||
// An unpriced model is offered at zero and flagged, not withheld: the
|
||||
// vendor says the credential can reach it.
|
||||
assert.Zero(t, models[1].InputPer1k)
|
||||
assert.Zero(t, models[1].OutputPer1k)
|
||||
}
|
||||
|
||||
// TestDiscoveredRatesMatchTheCatalogEndpoint pins the two prefill paths to one
|
||||
// table. The provider form fills a model row either from the catalog response
|
||||
// or from a discovery response, and an operator who switches between them must
|
||||
// not see the price change — both must equal what the proxy will bill.
|
||||
func TestDiscoveredRatesMatchTheCatalogEndpoint(t *testing.T) {
|
||||
cl, _ := newStubClient(http.StatusOK, openAIListing)
|
||||
|
||||
models, err := cl.Fetch(context.Background(), Request{
|
||||
CatalogID: "openai_api",
|
||||
UpstreamURL: "https://api.openai.com",
|
||||
APIKey: "sk-test",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, models)
|
||||
|
||||
entry, ok := catalog.Lookup("openai_api")
|
||||
require.True(t, ok)
|
||||
|
||||
for _, m := range models {
|
||||
want, known := pricing.LookupDefault(entry.PricingSurfaces, m.ID)
|
||||
require.True(t, known, "%s should be priced by the default table", m.ID)
|
||||
assert.Equal(t, want.InputPer1k, m.InputPer1k, "input rate for %s", m.ID)
|
||||
assert.Equal(t, want.OutputPer1k, m.OutputPer1k, "output rate for %s", m.ID)
|
||||
assert.Equal(t, want.CachedInputPer1k, m.CachedInputPer1k, "cached-input rate for %s", m.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchVertexJoinsNameAndVersion(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user