diff --git a/cmd/authbridge-proxy/local.go b/cmd/authbridge-proxy/local.go index 9b04245ae..58441b7d9 100644 --- a/cmd/authbridge-proxy/local.go +++ b/cmd/authbridge-proxy/local.go @@ -233,10 +233,9 @@ tls_bridge: # Gateways matching a shipped rule already have their discount applied and need # nothing here. If yours is not one of them and it bills a fraction of list, say # so once: one scalar tracks upstream repricing, where a copied rate card freezes -# today's numbers and goes stale with nothing to say it has. +# today's numbers and goes stale with nothing to say it has. Add it under +# endpoints below: # -# pricing: -# endpoints: # - hosts: ["my-gateway.example.com"] # multiplier: 0.80 # a FRACTION of list, so 0.80 is a 20% discount # @@ -244,6 +243,22 @@ tls_bridge: # derived from list, give rates instead of a multiplier -- see # docs/pricing.md. # +# IBM Bob bills in Bobcoins, not dollars, at a flat rate no vendor list carries: +# its models (premium-ide, router, openai/gpt-oss-20b) are in no bundled table, +# so without this entry every Bob request is unpriced. "unit" keeps its figures +# out of the dollar totals -- abctl shows them in Bobcoins and never adds the +# two. Edit the rate here if IBM changes it; nothing else will. +pricing: + endpoints: + - hosts: ["api.us-east.bob.ibm.com"] + unit: Bobcoins + models: + "*": # every Bob model, every tier: 2 per million tokens + input_cost_per_million: 2.00 + output_cost_per_million: 2.00 + cache_read_cost_per_million: 2.00 # no cache discount on this gateway + cache_write_cost_per_million: 2.00 +# # Cost history on disk. Per-minute totals only -- no prompts, no completions, # no tool arguments -- under ~/.cortex/cost/YYYY-MM-DD.jsonl, kept 31 days # (roughly 10 MB). The in-memory counters are a 6-hour ring and this proxy diff --git a/cmd/authbridge-proxy/local_test.go b/cmd/authbridge-proxy/local_test.go index 34c9abab6..832fec74c 100644 --- a/cmd/authbridge-proxy/local_test.go +++ b/cmd/authbridge-proxy/local_test.go @@ -10,6 +10,7 @@ import ( "encoding/json" "encoding/pem" "fmt" + "math" "math/big" "os" "path/filepath" @@ -21,6 +22,7 @@ import ( "time" "github.com/rossoctl/cortex/core/config" + "github.com/rossoctl/cortex/core/cost/pricing" "github.com/rossoctl/cortex/core/cost/usage" "github.com/rossoctl/cortex/core/session" ) @@ -635,3 +637,52 @@ func TestBuiltinConfig_SeparatesCodingAgentSessions(t *testing.T) { t.Errorf("built-in config: id_headers = %v, want it to include %s", cfg.Session.SessionIDHeaders(), session.BobSessionHeader) } } + +// TestBuiltinConfig_PricesBobInBobcoins: the built-in config prices IBM Bob's gateway at its +// flat 2 Bobcoins per million tokens on every tier. Without the entry, every Bob request is +// unpriced (its models — premium-ide, router, openai/gpt-oss-20b — are in no bundled table), and +// a new install shows "—" for Bob until its user finds docs/pricing.md. The rate is resolved +// through pricing.Build rather than read off the struct, so a unit or model key the table would +// not match fails here and not only on a laptop. +func TestBuiltinConfig_PricesBobInBobcoins(t *testing.T) { + cortexDir := t.TempDir() + p, err := writeBuiltinConfig(cortexDir, filepath.Join(cortexDir, "ca")) + if err != nil { + t.Fatal(err) + } + cfg, err := config.Load(p) + if err != nil { + t.Fatal(err) + } + tab, err := pricing.Build(cfg.Pricing) + if err != nil { + t.Fatalf("pricing.Build: %v", err) + } + + const bob = "api.us-east.bob.ibm.com" + for _, model := range []string{"premium-ide", "router", "openai/gpt-oss-20b"} { + rates, prov := tab.Resolve(bob, model, 0) + if prov != pricing.ProvConfigured { + t.Errorf("%s on %s: provenance %v, want configured", model, bob, prov) + } + // Rates are stored per token; the config states them per million. + for tier := pricing.TierInput; tier <= pricing.TierOutput; tier++ { + if perMillion := rates.Base[tier] * 1e6; !rates.Set[tier] || math.Abs(perMillion-2) > 1e-9 { + t.Errorf("%s on %s: tier %d = %v per Mtok (set %v), want 2", model, bob, tier, perMillion, rates.Set[tier]) + } + } + if got := tab.CurrencyFor(bob, model); got != "Bobcoins" { + t.Errorf("%s on %s: unit %q, want Bobcoins", model, bob, got) + } + } + + // The entry is scoped to Bob's host: its "*" model must not reprice anyone else's traffic + // in Bobcoins. + const other = "api.anthropic.com" + if _, prov := tab.Resolve(other, "claude-opus-5-5", 0); prov != pricing.ProvBundled { + t.Errorf("claude-opus-5-5 on %s: provenance %v, want bundled", other, prov) + } + if got := tab.CurrencyFor(other, "claude-opus-5-5"); got != pricing.CurrencyUSD { + t.Errorf("claude-opus-5-5 on %s: unit %q, want %s", other, got, pricing.CurrencyUSD) + } +} diff --git a/docs/pricing.md b/docs/pricing.md index ce0add91b..677918137 100644 --- a/docs/pricing.md +++ b/docs/pricing.md @@ -163,9 +163,12 @@ curl localhost:47601/v1/usage | jq .unpricedBy `pricing:` entry. The Bob endpoint above is the worked example for [Billing units](#billing-units): it bills in -credits rather than dollars, so closing its gap means giving that endpoint a `unit:` as well as -rates. Pricing it without one would record credits as dollars — the figure would look right and be -neither. +Bobcoins rather than dollars, so closing its gap means giving that endpoint a `unit:` as well as +rates. Pricing it without one would record Bobcoins as dollars — the figure would look right and be +neither. The built-in local config (`authbridge-proxy --local --write-config`, which both +`install.sh` and `make dev-install` run) ships that entry, so a new install prices Bob already; a +`~/.cortex/config.yaml` written before it is never rewritten, so an older install still shows this +gap until the block below is added. The TUI annotates the same thing from the other direction: `abctl observe`'s cost total carries `[bundled]` or `[configured]` when a total is wholly one provenance, and names the @@ -314,15 +317,16 @@ host invites the copies to drift. Each host becomes its own table row. ### Billing units -Rates are in **US dollars unless an endpoint says otherwise**: +Rates are in **US dollars unless an endpoint says otherwise**. This is the entry the built-in +local config ships for IBM Bob, a flat rate on every model and tier: ```yaml pricing: endpoints: - hosts: ["api.us-east.bob.ibm.com"] - unit: credits # absent means USD + unit: Bobcoins # absent means USD models: - "premium-ide": + "*": input_cost_per_million: 2.00 output_cost_per_million: 2.00 cache_read_cost_per_million: 2.00 # deliberate: this gateway gives no cache discount