mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-19 21:29:09 +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:
@@ -125,7 +125,20 @@ func (h *handler) discoverProviderModels(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
out := api.AgentNetworkModelDiscoveryResponse{Models: make([]api.AgentNetworkDiscoveredModel, 0, len(models))}
|
||||
for _, m := range models {
|
||||
entry := api.AgentNetworkDiscoveredModel{Id: m.ID, PricingKnown: m.PricingKnown}
|
||||
entry := api.AgentNetworkDiscoveredModel{
|
||||
Id: m.ID,
|
||||
PricingKnown: m.PricingKnown,
|
||||
// Sent even when zero: the form prefills every discovered model as
|
||||
// an editable row, and an unpriced one is shown at zero and flagged
|
||||
// rather than left out.
|
||||
InputPer1k: m.InputPer1k,
|
||||
OutputPer1k: m.OutputPer1k,
|
||||
// Cache rates stay absent when unset, matching the catalog
|
||||
// response — a zero would read as "free", not "not applicable".
|
||||
CachedInputPer1k: positiveRatePtr(m.CachedInputPer1k),
|
||||
CacheReadPer1k: positiveRatePtr(m.CacheReadPer1k),
|
||||
CacheCreationPer1k: positiveRatePtr(m.CacheCreationPer1k),
|
||||
}
|
||||
if m.Label != "" {
|
||||
label := m.Label
|
||||
entry.Label = &label
|
||||
|
||||
@@ -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