Skip to content

Fix: Refuse a non-USD unit on a pricing block for every endpoint - #1242

Merged
huang195 merged 1 commit into
rossoctl:mainfrom
huang195:fix/refuse-unit-on-hostless-block
Oct 2, 2026
Merged

huang195 merged 1 commit into
rossoctl:mainfrom
huang195:fix/refuse-unit-on-hostless-block

Conversation

@huang195

@huang195 huang195 commented Oct 2, 2026

Copy link
Copy Markdown
Member

Summary

Fixes #1190. A pricing block with a non-USD unit: and no hosts: (or "*" among them) now fails startup, beside the existing refusal for a unit on a multiplier-only block.

Why: after #1153 a unit is host-wide. Table.unitFor gives an endpoint the unit of its best-ranked row, and only rows in that unit may price its traffic. So a catch-all block with a unit puts every endpoint that no more specific block names into that unit:

  • the bundled dollar table prices nothing there. This is loud, because unpricedBy names every pair;
  • every charge those endpoints report themselves is labelled in that unit. This one is silent.

Measured in the issue at 80348bf with hosts: ["*"], unit: credits and the bundled table on: api.anthropic.com claude-opus-4-1 went from unit=USD prov=bundled to unit=credits prov=none.

A unit belongs to one gateway, so that gateway's hosts can be named. The error names the block:

pricing.endpoints[0] (hosts [*]): unit "credits" needs hosts naming the gateways that bill in it; a block for every endpoint would put all of them in that unit

Changes

  • core/cost/pricing/config.go: Config.entries refuses a block whose unit is not USD and whose hosts include a catch-all (anyHost: "" or "*"). The check covers a catch-all anywhere in the list, since ["gw.bob", "*"] still covers every endpoint. Any spelling of USD ("", usd) is accepted, because it is no unit claim. This matches the multiplier-only refusal.
  • core/cost/pricing/config_test.go:
    • new TestConfig_AUnitOnACatchAllBlockIsRefused, a sibling of TestConfig_AUnitOnAMultiplierOnlyBlockIsRefused:
      • refused: no hosts, ["*"], ["gw.bob", "*"];
      • accepted: hostless "" / usd, ["*"] with USD, a named gateway with credits.
    • removed the "any-endpoint spelled two ways" case from TestConfig_BlocksForOneHostMustAgreeOnTheUnit. It paired a hostless credits block with a "*" USD one. Since a catch-all can no longer carry a unit, two spellings of it always agree, and the new refusal fires first. A comment on the test now points at the new one.
  • docs/pricing.md: the Billing units section lists this refusal beside the other two.

Unaffected: the built-in local config's Bob entry (cmd/cortex/local.go) and agentop config migrate-pricing both name api.us-east.bob.ibm.com.

Test plan

  • The new test fails on unmodified main (all three refusal cases build successfully there).
  • Mutation-checked: a predicate that refuses only hostless blocks (len(ep.Hosts) == 0) fails the ["*"] and ["gw.bob", "*"] cases.
  • go test ./... in core; GOWORK=off go test ./... in cmd/cortex and cmd/agentop (with SSL_CERT_FILE unset). go vet ./cost/... and gofmt -l ./cost clean.

Assisted-By: Claude (Anthropic AI) noreply@anthropic.com

After rossoctl#1153 a unit is host-wide: Table.unitFor gives an endpoint the
unit of its best-ranked row, and only rows in that unit may price its
traffic. A block with a unit and no hosts, or with "*" among them,
covers every endpoint, so every endpoint no more specific block names
took that unit. The bundled dollar table then priced nothing there,
which is loud: unpricedBy names every pair. The other half was silent:
a charge any of those endpoints reported itself was labelled in the
unit. Measured with hosts ["*"], unit credits and the bundled table on,
api.anthropic.com/claude-opus-4-1 went from unit=USD prov=bundled to
unit=credits prov=none.

Config.entries now refuses that block at startup, beside the
multiplier-only refusal, naming the block. A unit is a gateway's, so its
hosts can be named. Any spelling of USD is still accepted, as it is no
unit claim.

TestConfig_BlocksForOneHostMustAgreeOnTheUnit loses its "any-endpoint
spelled two ways" case: a catch-all can no longer carry a unit, so two
spellings of it always agree, and the new refusal fires first.
docs/pricing.md lists the refusal beside the other two.

Fixes rossoctl#1190.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
@coderabbitai

coderabbitai Bot commented Oct 2, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 48 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: b0ce5017-4855-4227-ad36-6292de7c8175

📥 Commits

Reviewing files that changed from the base of the PR and between ccb33e4 and 310d4cc.

📒 Files selected for processing (3)
  • core/cost/pricing/config.go
  • core/cost/pricing/config_test.go
  • docs/pricing.md
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@mrsabath mrsabath left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified the mechanism this guards against: Table.unitFor ranks provenance first, so a configured catch-all row with a unit does outrank the bundled USD rows and would relabel every endpoint no more specific block names — the failure in #1190 is real, not theoretical.

The check is correctly placed after the len(ep.Models) == 0 branch, so it cannot collide with the multiplier-only refusal (that path continues first). slices.ContainsFunc over the normalised hosts catches both the hostless case and "*" anywhere in the list, and EqualFold against CurrencyUSD after normaliseUnit means no spelling of the default is treated as a unit claim. Echoing ep.Unit rather than the normalised value in the error is the right call — it shows the operator what they typed.

Confirmed the "unaffected" claim: cmd/cortex/local.go and cmd_config_migrate_pricing.go both name api.us-east.bob.ibm.com, and the only unit: example in docs/pricing.md names a concrete host — so nothing shipped or documented regresses. The removed "any-endpoint spelled two ways" case is genuinely subsumed, and it left a pointer to the new test behind rather than vanishing. The mutation check in the test plan (a len(ep.Hosts) == 0-only predicate failing the ["*"] cases) is exactly the right thing to verify for this predicate.

No must-fix issues, and nothing I would raise as a suggestion either.

Areas reviewed: Go (core/cost/pricing), tests, docs
Commits: 1, signed off
CI status: passing (28 checks, Spellcheck skipped)

@huang195
huang195 merged commit 3e317ba into rossoctl:main Oct 2, 2026
28 checks passed
@huang195
huang195 deleted the fix/refuse-unit-on-hostless-block branch October 2, 2026 21:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

pricing: a non-USD unit on a hostless or "*" block unprices the bundled table everywhere

3 participants