mirror of
https://github.com/netbirdio/netbird.git
synced 2026-08-28 02:21:30 +02:00
Field report: a Bedrock Claude Sonnet 4.6 request showing 3 input / 1514 output tokens displayed a cost of $0.1372 instead of the expected $0.0227. Root cause analysis: the calculation is correct — the request's first call also wrote a ~30.5k-token prompt cache (cache_creation_input_tokens, billed at 1.25x input per AWS pricing), which is folded into total_tokens and the cost but not visible next to the input/output counts. Lock the pipeline down with tests so any real calculation regression fails loudly: - Add a provider cost matrix test driving the real proxy pipeline (llm_request_parser -> llm_response_parser -> cost_meter) with realistic wire fixtures for every metered surface (OpenAI JSON/SSE incl. cached subset, Anthropic JSON/SSE incl. cache buckets, Bedrock InvokeModel and Converse in both buffered and streaming form, Vertex path-routed, Kimi Anthropic-shape, unpriced gateway-prefixed ids) against the embedded default pricing table, asserting exact USD amounts derived from the published per-million prices. Covers the reported scenario byte-for-byte ($0.022719 bare, $0.137199 with the 30,528-token cache write) and would catch a per-token-instead-of-per-1k-chunk regression as a 1000x blowup. - Pin the management catalog (dashboard-displayed prices) to the proxy's embedded pricing table so the two can never drift apart silently. - Validate cost end-to-end in the live e2e provider matrix: each provider's ingested access-log row must carry a cost_usd matching the vendor's published per-1k rates applied to the row's token counts (cache-aware for the additive Anthropic/Bedrock buckets, zero for gateway-prefixed model ids the meter deliberately skips). - Fix a real under-billing bug the audit surfaced: the Bedrock Converse shapes report prompt-cache usage as camelCase cacheReadInputTokens / cacheWriteInputTokens, which neither the buffered parser nor the converse-stream metadata handler read - cached Converse traffic was metered without its cache buckets. Parse both fields into the same Usage buckets as the InvokeModel snake_case fields.
63 lines
2.5 KiB
Go
63 lines
2.5 KiB
Go
package pricing
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/netbirdio/netbird/management/internals/modules/agentnetwork/catalog"
|
|
)
|
|
|
|
// catalogPricingProviders maps each metered catalog provider id to the
|
|
// pricing-table provider key(s) its requests are billed under (the
|
|
// llm.Parser surface the proxy meters that provider with). A catalog
|
|
// provider whose requests can hit more than one parser surface (Kimi
|
|
// serves both the OpenAI and Anthropic body shapes off one host) must
|
|
// carry matching entries in every listed table.
|
|
//
|
|
// Gateway/custom catalog entries (LiteLLM, Portkey, vLLM, …) publish no
|
|
// model list, so they never appear here.
|
|
var catalogPricingProviders = map[string][]string{
|
|
"openai_api": {"openai"},
|
|
"azure_openai_api": {"openai"},
|
|
"mistral_api": {"openai"},
|
|
"anthropic_api": {"anthropic"},
|
|
"vertex_ai_api": {"anthropic"}, // Anthropic-on-Vertex: bare claude-* ids, anthropic parser
|
|
"bedrock_api": {"bedrock"},
|
|
"kimi_api": {"openai", "anthropic"},
|
|
}
|
|
|
|
// TestDefaultPricing_MatchesCatalog pins the management catalog (the prices
|
|
// the dashboard displays) to the proxy's embedded default pricing table (the
|
|
// prices the cost meter bills). A drift between the two makes correct costs
|
|
// look wrong — the dashboard advertises one rate while the proxy meters
|
|
// another — so every catalog model must resolve to a pricing entry with
|
|
// byte-identical input/output per-1k rates.
|
|
func TestDefaultPricing_MatchesCatalog(t *testing.T) {
|
|
table := DefaultTable()
|
|
|
|
for _, p := range catalog.All() {
|
|
if len(p.Models) == 0 {
|
|
continue // gateways / custom endpoints publish no models
|
|
}
|
|
keys, metered := catalogPricingProviders[p.ID]
|
|
require.Truef(t, metered,
|
|
"catalog provider %q publishes models but has no pricing-table mapping — add it to catalogPricingProviders and defaults_pricing.yaml",
|
|
p.ID)
|
|
|
|
for _, m := range p.Models {
|
|
for _, key := range keys {
|
|
entry, ok := table.entries[key][m.ID]
|
|
require.Truef(t, ok,
|
|
"catalog model %s/%s must have a %q pricing entry or the cost meter silently skips it (cost.skipped=unknown_model)",
|
|
p.ID, m.ID, key)
|
|
assert.Equalf(t, m.InputPer1k, entry.InputPer1K,
|
|
"input rate drift for %s/%s: dashboard shows %v, proxy bills %v", p.ID, m.ID, m.InputPer1k, entry.InputPer1K)
|
|
assert.Equalf(t, m.OutputPer1k, entry.OutputPer1K,
|
|
"output rate drift for %s/%s: dashboard shows %v, proxy bills %v", p.ID, m.ID, m.OutputPer1k, entry.OutputPer1K)
|
|
}
|
|
}
|
|
}
|
|
}
|