feat: distinguish default and custom AI model prices - #28204
Conversation
This stack of pull requests is managed by Graphite. Learn more about stacking. |
beab408 to
c2143fa
Compare
c2143fa to
29a2d26
Compare
| WHERE CASE | ||
| -- A custom price claims any row, including one the price book wrote. | ||
| WHEN @source::ai_model_price_source = 'custom' THEN true | ||
| -- The price book leaves custom rows alone. | ||
| ELSE ai_model_prices.source <> 'custom' | ||
| END | ||
| AND ( | ||
| ai_model_prices.input_price, | ||
| ai_model_prices.output_price, | ||
| ai_model_prices.cache_read_price, | ||
| ai_model_prices.cache_write_price, | ||
| ai_model_prices.source | ||
| ) IS DISTINCT FROM ( | ||
| EXCLUDED.input_price, | ||
| EXCLUDED.output_price, | ||
| EXCLUDED.cache_read_price, | ||
| EXCLUDED.cache_write_price, | ||
| EXCLUDED.source | ||
| ); |
There was a problem hiding this comment.
We could alternatively keep both rows with the custom and default source and then join / filter when querying? This would allow an administrator to delete their custom price and revert to the default from the price book.
There was a problem hiding this comment.
This is actually a great idea! 🏆 This way we won't lose the default price book! We just need to guarantee that the read queries resolve the model row: if custom is present return it, otherwise fallback to default, which will be used by the CLI/API as well as when calculating the interception cost. Will make these changes 👍
We currently don't have a way of deleting custom prices, but we can add a delete command to the CLI, for instance 🤔
There was a problem hiding this comment.
We don't need to add the delete command now. For the sorting, you can do something like ORDER BY CASE WHEN ... THEN 1 ELSE 0
There was a problem hiding this comment.
I think I prefer to do it now because this changes the logic for the new column source which will also need to be considered as part of the PK for ai_model_prices. Should be easy to do 👍
e6affe7 to
4af28ec
Compare
4217edb to
350bc71
Compare
350bc71 to
8e53141
Compare
Merge activity
|
## Description The experimental CLI could only price models the price book does not cover, so an operator with a negotiated rate for a model Coder already prices had no way to record it. The restriction existed because a single row served both the price book and prices set through the API, and the startup seeder would overwrite an override on the next restart. Each source now keeps its own row (#28204), so that no longer applies. This removes the restriction: a price set through the CLI takes effect over the price book for any model, and the book stays underneath it. ## Changes - Remove the validation that rejected models the price book covers. - Delete `prices.IsDefaultPriced` and the index behind it, which existed only for that check. - Generalize the `coder exp ai-model-prices update` help. - Document that a custom price overrides the price book and does not follow its updates. - Rework the spend approximation callout around what a custom price can and cannot correct. Closes https://linear.app/codercom/issue/AIGOV-594 > [!NOTE] > Initially generated by Claude Opus 5, modified and reviewed by @ssncferreira
## Description Prices in `ai_model_prices` come from two places: the price book embedded in each Coder release, and prices set through the experimental CLI (#27926). Until now, they were indistinguishable, so the startup seeder re-applied the book over every row, and a price someone set would silently revert on the next restart. This adds a `source` column with values `default` and `custom`, and keeps a row for each. Readers resolve a model to its custom price when one exists and fall back to the book, including the cost path that prices an interception. Keeping both rows means the price book stays current underneath an override, so a future delete can restore it. Note: the endpoint still only accepts models the price book does not cover. Overriding a price the book already carries is handled by the follow-up issue https://linear.app/codercom/issue/AIGOV-594. ## Changes - Add a migration for the `ai_model_price_source` enum and a `source` column, backfilled to `default` and then `NOT NULL`. `source` joins the primary key, so a model can hold one row per source. - Take a `source` parameter on `UpsertAIModelPrices`. Each source keeps its own row, so the price book and a custom price never overwrite each other. - Resolve to the custom price in `GetAIModelPriceByProviderModel` and `GetAIModelPrices`, so both the interception cost snapshot and the list endpoint use it. - Seed the price book as `default`, and write prices set through the endpoint as `custom`. Closes https://linear.app/codercom/issue/AIGOV-592 > [!NOTE] > Initially generated by Claude Opus 5, modified and reviewed by @ssncferreira
## Description The experimental CLI could only price models the price book does not cover, so an operator with a negotiated rate for a model Coder already prices had no way to record it. The restriction existed because a single row served both the price book and prices set through the API, and the startup seeder would overwrite an override on the next restart. Each source now keeps its own row (#28204), so that no longer applies. This removes the restriction: a price set through the CLI takes effect over the price book for any model, and the book stays underneath it. ## Changes - Remove the validation that rejected models the price book covers. - Delete `prices.IsDefaultPriced` and the index behind it, which existed only for that check. - Generalize the `coder exp ai-model-prices update` help. - Document that a custom price overrides the price book and does not follow its updates. - Rework the spend approximation callout around what a custom price can and cannot correct. Closes https://linear.app/codercom/issue/AIGOV-594 > [!NOTE] > Initially generated by Claude Opus 5, modified and reviewed by @ssncferreira

Description
Prices in
ai_model_pricescome from two places: the price book embedded in each Coder release, and prices set through the experimental CLI (#27926). Until now, they were indistinguishable, so the startup seeder re-applied the book over every row, and a price someone set would silently revert on the next restart.This adds a
sourcecolumn with valuesdefaultandcustom, and keeps a row for each. Readers resolve a model to its custom price when one exists and fall back to the book, including the cost path that prices an interception.Keeping both rows means the price book stays current underneath an override, so a future delete can restore it.
Note: the endpoint still only accepts models the price book does not cover. Overriding a price the book already carries is handled by the follow-up issue https://linear.app/codercom/issue/AIGOV-594.
Changes
ai_model_price_sourceenum and asourcecolumn, backfilled todefaultand thenNOT NULL.sourcejoins the primary key, so a model can hold one row per source.sourceparameter onUpsertAIModelPrices. Each source keeps its own row, so the price book and a custom price never overwrite each other.GetAIModelPriceByProviderModelandGetAIModelPrices, so both the interception cost snapshot and the list endpoint use it.default, and write prices set through the endpoint ascustom.Closes https://linear.app/codercom/issue/AIGOV-592
Note
Initially generated by Claude Opus 5, modified and reviewed by @ssncferreira