Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changes
normalizeModelnow 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 asgpt-5.6-20260115resolves togpt-5.6-solinstead of returningnull. Unknown models still resolve tonull, 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.claude-sonnet-5-20260115claude-sonnet-5gpt-5.6gpt-5.6-solgpt-5.6-20260115nullgpt-5.6-solprivate-modelnullThe consequence sits in
CostBudgetService(services/api/src/insights/costs.ts):assertTurnAllowedthrowsBudgetPolicyError("budget_model_unpriced"), so a turn is denied for a model that is priced.recordstores the turn aspriced: falsewith a null cost, and thecoalesce(sum(cost_cents), 0)inbudgetStatedrops 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.mdasks 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_ALIASESitself rather than over hard-coded ids, so it keeps holding as the table changes. That is whyMODEL_ALIASESis now exported. Both new assertions fail onmainand pass with the fix:packages/core/test/pricing.test.tswithout the fix:expected null to be 'claude-haiku-4-5'.services/api/test/insights-and-mirror.integration.test.tswithout the fix:budget_model_unpricedwherebudget_exceededis expected, which is the preflight denying a priced model. This is the money-sensitive integration coverageCONTRIBUTING.mdrequires, and it runs against the isolatedfacility_testdatabase.Environment: Windows 11, Node 22.14.0, pnpm 11.20.0, Postgres from
docker-compose.dev.yml.biome check .: clean, 225 files.tsc --noEmitfor@facility/coreand@facility/api: clean.@facility/core: 11 passed.@facility/apiinsights-and-mirror.integration.test.ts: 7 passed, andbudget-routes.integration.test.ts: 3 passed. Both in isolated processes with the test databases recreated first, followingscripts/test-critical.mjs.@facility/agents10 passed,@facility/mcp14 passed,@facility/sdk10 passed.node guards/run.mjs: 2 guards ran, 0 failed.node scripts/check-unused.mjs: clean.pnpm verifypasses locallyLeft unchecked deliberately, because it would not be true.
pnpm verifydoes not complete on this machine for reasons independent of this change, and the same suites fail on a pristinemaincheckout here:scripts/dependencies-security.test.mjs(needs registry access),scripts/images.integration.test.mjsandscripts/release.integration.test.mjs(need image builds and release tooling), andservices/api/test/turn-dispatcher.integration.test.ts, which is non-deterministic in this environment (4 failed and 4 passed on pristinemain, varying between runs; no failure mentions pricing, budgets ornormalizeModel). I ran the individualverifysteps instead, as listed above.I reverted the fix, rebuilt
@facility/core, and confirmed against a live database that the budget preflight refusesgpt-5.6-20260115asbudget_model_unpriced. With the fix restored, the same call is judged on spend instead.No user-facing change. The doc comment already described the intended behavior.