Skip to content

fix(core): resolve aliases for dated provider model ids - #318

Open
pma1999 wants to merge 1 commit into
theam:mainfrom
pma1999:fix/core-dated-alias-pricing
Open

pma1999 wants to merge 1 commit into
theam:mainfrom
pma1999:fix/core-dated-alias-pricing

Conversation

@pma1999

@pma1999 pma1999 commented Sep 7, 2026

Copy link
Copy Markdown

What changes

normalizeModel now runs one resolution chain over both the id it receives and its undated form, so alias resolution and date suffix stripping compose. A provider id such as gpt-5.6-20260115 resolves to gpt-5.6-sol instead of returning null. Unknown models still resolve to null, and the alias table itself is unchanged.

Why

An alias is absent from the price book by definition; that is what makes it an alias. The previous chain checked the price book, then the alias table, then stripped the date suffix and checked only the price book again, so a dated aliased id fell through to null.

Model id Before After
claude-sonnet-5-20260115 claude-sonnet-5 unchanged
gpt-5.6 gpt-5.6-sol unchanged
gpt-5.6-20260115 null gpt-5.6-sol
private-model null unchanged

The consequence sits in CostBudgetService (services/api/src/insights/costs.ts):

  • With a project budget enabled, assertTurnAllowed throws BudgetPolicyError("budget_model_unpriced"), so a turn is denied for a model that is priced.
  • With no budget, record stores the turn as priced: false with a null cost, and the coalesce(sum(cost_cents), 0) in budgetState drops it, so project spend is under-counted.

The existing unit test covered a dated direct id (claude-opus-4-8-20260101) and an undated alias, which is why the composition of the two rules was never exercised.

I did not file an issue first: CONTRIBUTING.md asks for that before a substantial or behavior-changing feature, and this is a defect against behavior the function already documents ("Resolve aliases and provider date suffixes to a price-book entry"). Happy to move the discussion to an issue if you would prefer that.

Verification

The unit test asserts the invariant over MODEL_ALIASES itself rather than over hard-coded ids, so it keeps holding as the table changes. That is why MODEL_ALIASES is now exported. Both new assertions fail on main and pass with the fix:

  • packages/core/test/pricing.test.ts without the fix: expected null to be 'claude-haiku-4-5'.
  • services/api/test/insights-and-mirror.integration.test.ts without the fix: budget_model_unpriced where budget_exceeded is expected, which is the preflight denying a priced model. This is the money-sensitive integration coverage CONTRIBUTING.md requires, and it runs against the isolated facility_test database.

Environment: Windows 11, Node 22.14.0, pnpm 11.20.0, Postgres from docker-compose.dev.yml.

  • biome check .: clean, 225 files.
  • tsc --noEmit for @facility/core and @facility/api: clean.
  • @facility/core: 11 passed.
  • @facility/api insights-and-mirror.integration.test.ts: 7 passed, and budget-routes.integration.test.ts: 3 passed. Both in isolated processes with the test databases recreated first, following scripts/test-critical.mjs.
  • @facility/agents 10 passed, @facility/mcp 14 passed, @facility/sdk 10 passed.
  • node guards/run.mjs: 2 guards ran, 0 failed. node scripts/check-unused.mjs: clean.
  • pnpm verify passes locally

Left unchecked deliberately, because it would not be true. pnpm verify does not complete on this machine for reasons independent of this change, and the same suites fail on a pristine main checkout here: scripts/dependencies-security.test.mjs (needs registry access), scripts/images.integration.test.mjs and scripts/release.integration.test.mjs (need image builds and release tooling), and services/api/test/turn-dispatcher.integration.test.ts, which is non-deterministic in this environment (4 failed and 4 passed on pristine main, varying between runs; no failure mentions pricing, budgets or normalizeModel). I ran the individual verify steps instead, as listed above.

  • Behavior verified beyond the test suite

I reverted the fix, rebuilt @facility/core, and confirmed against a live database that the budget preflight refuses gpt-5.6-20260115 as budget_model_unpriced. With the fix restored, the same call is judged on spend instead.

  • Documentation updated, or no user-facing change

No user-facing change. The doc comment already described the intended behavior.

normalizeModel stripped a provider date suffix and then looked the result up
in the price book only, never in the alias table. An alias is absent from the
price book by definition, so a dated aliased id resolved to null while its
undated form resolved fine.

With a project budget enabled, assertTurnAllowed then refused the turn as
budget_model_unpriced, denying a turn for a model that is priced. With no
budget the turn was recorded unpriced and its cost coalesced away, so project
spend was under-counted.

Both forms now run the same resolution chain, so the two rules compose.
Unknown models still resolve to null, and the alias table is unchanged.

@adrian-lorenzo adrian-lorenzo 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.

Thanks for the fix!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants