Skip to content

llmcost: per-namespace price overrides never take effect (ResolvePrice orders by namespace DESC without NULLS LAST, so the NULL-namespace global price always wins) #4562

Description

@ever1022

Summary

In openmeter/llmcost, a per-namespace manual price override (POST /api/v1/llm-cost/overridesCreateOverride) is silently ignored whenever a global/system price exists for the same (provider, model_id) — which is the normal case, because the background sync job (UpsertGlobalPrice) populates a global price for every known model.

The override row is written and is correctly selected as a candidate at read time, but it loses the final tie-break in ResolvePrice and the global price is returned instead. As a result, manual overrides have no effect on cost computation (e.g. Feature Cost / LLM unit cost).

Root cause

openmeter/llmcost/adapter/price.go, ResolvePrice:

entity, err := a.db.LLMCostPrice.Query().
    Where(pricedb.DeletedAtIsNil()).
    Where(pricedb.Or(
        pricedb.NamespaceEQ(input.Namespace), // override (namespace set)
        pricedb.NamespaceIsNil(),             // global/system price (NULL)
    )).
    Where(pricedb.ProviderEQ(string(input.Provider))).
    Where(pricedb.ModelIDEQ(input.ModelID)).
    Where(pricedb.EffectiveFromLTE(at)).
    Where(pricedb.Or(
        pricedb.EffectiveToIsNil(),
        pricedb.EffectiveToGT(at),
    )).
    Order(
        // Prioritize namespace overrides
        pricedb.ByNamespace(sql.OrderDesc()),   // <-- missing NULLS LAST
        pricedb.ByEffectiveFrom(sql.OrderDesc()),
    ).
    First(ctx)
  • Manual overrides are stored with a non-NULL namespace (CreateOverride calls SetNamespace(input.Namespace) with source = manual).
  • Global/system prices are stored with namespace = NULL (UpsertGlobalPrice never sets a namespace; the schema field is Optional().Nillable() and is documented as "Nil for global prices, set for namespace overrides").

The ordering intent ("Prioritize namespace overrides") relies on the non-NULL namespace sorting before the NULL namespace. But ByNamespace(sql.OrderDesc()) emits a bare ORDER BY namespace DESC with no NULLS clause, and PostgreSQL's default for DESC is NULLS FIRST. So the NULL-namespace (global) row sorts first and First(ctx) returns it — the override never wins.

ent confirms the emitted SQL has no NULLS clause (entgo.io/ent@v0.14.6, dialect/sql/sql.go, OrderFieldTerm.ToFunc):

if f.Desc { b.WriteString(" DESC") }
if f.NullsFirst { b.WriteString(" NULLS FIRST") } else if f.NullsLast { b.WriteString(" NULLS LAST") }
// neither option set -> bare " DESC", PostgreSQL default = NULLS FIRST

Reproduction (PostgreSQL 14)

CREATE TEMP TABLE t (id text, namespace text, price numeric);
INSERT INTO t VALUES ('global_sync', NULL, 30), ('override', 'default', 20);

-- What ResolvePrice emits today:
SELECT id, namespace, price FROM t ORDER BY namespace DESC LIMIT 1;
--  id          | namespace | price
-- -------------+-----------+-------
--  global_sync |           |    30      <-- override is ignored

-- With the fix:
SELECT id, namespace, price FROM t ORDER BY namespace DESC NULLS LAST LIMIT 1;
--  id       | namespace | price
-- ----------+-----------+-------
--  override | default   |    20      <-- override wins, as intended

Impact

  • Any per-namespace LLM price override created via POST /llm-cost/overrides does not affect cost resolution as long as a global price exists for the same (provider, model_id) (the common case, since sync seeds global prices).
  • This affects all consumers of ResolvePrice, including LLM unit cost / Feature Cost computation in openmeter/cost/adapter/adapter.go (getLLMPrices passes feat.Namespace correctly; the override is matched as a candidate but loses the sort).
  • The override row is persisted and is returned by ListOverrides, so it appears to have been applied — making this a silent, hard-to-notice correctness bug.

Why it wasn't caught

  • There is no integration test exercising the real ResolvePrice SQL ordering: openmeter/llmcost/service tests use a mockAdapter, and there is no openmeter/llmcost/adapter/*_test.go. The cost and e2e suites don't create overrides.
  • The same NULL-ordering pitfall is handled correctly elsewhere in the codebase — openmeter/subject/adapter/subject.go explicitly adds sql.OrderNullsLast() with the comment "Make sure null display names are last" — confirming the default is NULLS FIRST and must be overridden.

Suggested fix

Make the namespace ordering put NULLs last so non-NULL (override) namespaces sort first:

Order(
    pricedb.ByNamespace(sql.OrderDesc(), sql.OrderNullsLast()),
    pricedb.ByEffectiveFrom(sql.OrderDesc()),
)

A regression test that inserts both a global price and a namespace override for the same (provider, model_id) and asserts ResolvePrice returns the override would lock this down.

Environment

  • Package: openmeter/llmcost
  • ent: v0.14.6
  • PostgreSQL: 14 (default NULLS FIRST on DESC applies to all supported PostgreSQL versions)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions