From b378183c6e9dccd7c535c945f4b26adb0968977d Mon Sep 17 00:00:00 2001 From: Hai Huang Date: Wed, 30 Sep 2026 16:49:30 -0400 Subject: [PATCH 1/2] Feat: Price IBM Bob in Bobcoins in the built-in local config A new install (install.sh or make dev-install, both via authbridge-proxy --local --write-config) left every Bob request unpriced: Bob's models -- premium-ide, router, openai/gpt-oss-20b -- are in no bundled table, so abctl showed "-" for Bob until its user found the worked example in docs/pricing.md and copied it in by hand. The built-in config now carries an endpoint entry for api.us-east.bob.ibm.com with unit Bobcoins and a "*" model at the flat 2 per million tokens Bob bills on every tier. The unit is what keeps those figures out of the dollar totals (#1153, #1195). A config entry rather than a shipped pricing rule: the rate is not on any vendor list, so it belongs where the user can see and edit it, not hidden in the binary where it would go stale silently. The trade is that an existing ~/.cortex/config.yaml is never rewritten, so existing installs still have to add the block themselves. The test resolves through pricing.Build rather than reading the struct, so a unit or model key the table would not match fails in CI, and pins that the "*" model does not reprice other hosts' traffic in Bobcoins. Signed-off-by: Hai Huang Assisted-By: Claude (Anthropic AI) --- cmd/authbridge-proxy/local.go | 21 ++++++++++-- cmd/authbridge-proxy/local_test.go | 51 ++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 3 deletions(-) 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) + } +} From 00a57e3e6c9366607a9b64843f08f9a6b6c48ee2 Mon Sep 17 00:00:00 2001 From: Hai Huang Date: Wed, 30 Sep 2026 16:52:07 -0400 Subject: [PATCH 2/2] Docs: Say the built-in config prices Bob, and spell its unit Bobcoins docs/pricing.md used Bob as the worked example of unpriced traffic and showed its entry with unit "credits" and a single premium-ide model. The built-in local config now ships the entry, so the example matches it (Bobcoins, "*"), and the gap paragraph says which installs still have the gap: any ~/.cortex/config.yaml written before this, since the installers never rewrite one. Signed-off-by: Hai Huang Assisted-By: Claude (Anthropic AI) --- docs/pricing.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) 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