mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-08 08:04:25 +00:00
fix: stop a sub-model inheriting its family's price
This commit is contained in:
@@ -129,10 +129,24 @@ function normalizeVersionSeparators(model: string): string {
|
||||
* Family fallbacks ending on a letter get no such guard — a version welded
|
||||
* straight onto the name (`llama3.1`) is exactly what they exist to catch.
|
||||
*/
|
||||
export function buildModelMatchers<T>(entries: [name: string, value: T][]): [RegExp, T][] {
|
||||
export function buildModelMatchers<T>(
|
||||
entries: [name: string, value: T][],
|
||||
{ strictVariants = false }: { strictVariants?: boolean } = {}
|
||||
): [RegExp, T][] {
|
||||
return entries.map(([name, value]) => {
|
||||
const pattern = normalizeVersionSeparators(name).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
return [new RegExp(/\d$/.test(pattern) ? `${pattern}(?!\\d)` : pattern), value]
|
||||
const guards = [
|
||||
// An entry ending on a version digit must not run into a longer version.
|
||||
/\d$/.test(pattern) ? '(?!\\d)' : '',
|
||||
// A named sub-model (`gpt-5-pro`, `gpt-5-mini`) is a different model with
|
||||
// its own price, not another route to this one — so under strictVariants an
|
||||
// entry does not match when a further *name* segment follows. Date suffixes
|
||||
// (`-20251101`) and Bedrock's `-v1` are route decorations, not sub-models,
|
||||
// and still match. Off by default: for a context window an inherited value
|
||||
// is a safe approximation, for a price it is a wrong number.
|
||||
strictVariants ? '(?!-(?!v\\d)[a-z])' : ''
|
||||
].join('')
|
||||
return [new RegExp(pattern + guards), value]
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -27,15 +27,26 @@ describe('resolveModelPrice', () => {
|
||||
expect(resolveModelPrice('customai', 'some-in-house-model', undefined)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('does not let a newer revision inherit an older one’s price', () => {
|
||||
// `gpt-5.6` normalizes to `gpt-5-6`, which the `gpt-5` entry would otherwise
|
||||
// claim — pricing a model we do not track at a rate that is several times off.
|
||||
it('does not let another model inherit a price through a shared prefix', () => {
|
||||
// A sub-model (`-pro`) or a newer revision (`gpt-5.6` → `gpt-5-6`) is a
|
||||
// different model at a different rate; inheriting `gpt-5`'s would be off by
|
||||
// an order of magnitude, and silently so.
|
||||
expect(resolveModelPrice('openai', 'gpt-5', undefined)?.price.input).toBe(1.25)
|
||||
expect(resolveModelPrice('openai', 'gpt-5-mini', undefined)?.price.input).toBe(0.25)
|
||||
expect(resolveModelPrice('openai', 'gpt-5-pro', undefined)).toBeUndefined()
|
||||
expect(resolveModelPrice('openai', 'gpt-5.6', undefined)).toBeUndefined()
|
||||
expect(resolveModelPrice('googleai', 'gemini-3.1', undefined)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('still resolves the route decorations that name the same model', () => {
|
||||
// Dates and Bedrock's -v1 are ways of spelling one model, not sub-models.
|
||||
expect(resolveModelPrice('anthropic', 'claude-opus-4-5-20251101', undefined)?.price.input).toBe(5)
|
||||
expect(
|
||||
resolveModelPrice('bedrock', 'anthropic.claude-sonnet-4-6-20250101-v1:0', undefined)?.price
|
||||
.input
|
||||
).toBe(3)
|
||||
})
|
||||
|
||||
it('prefers a workspace override, keeping the model’s own cache ratios', () => {
|
||||
const resolved = resolveModelPrice('anthropic', 'claude-opus-5', {
|
||||
'anthropic:claude-opus-5': { input: 2, output: 8 }
|
||||
|
||||
@@ -53,10 +53,17 @@ type PriceEntry = {
|
||||
* Providers whose catalogue turns over too quickly to track (DeepSeek, Mistral,
|
||||
* Groq, TogetherAI, custom deployments) are deliberately absent.
|
||||
*
|
||||
* `null` marks a model that is known to exist but whose rates are not: it stops a
|
||||
* newer revision from falling through to an older one's family entry and being
|
||||
* quietly mispriced. Unpriced is a supported state (the UI says so and points at
|
||||
* the override); a confidently wrong number is not.
|
||||
* `null` marks a model that is known to exist but whose rates are not. Unpriced is
|
||||
* a supported state (the UI says so and points at the override); a confidently
|
||||
* wrong number is not — which is also why these matchers are built with
|
||||
* `strictVariants`, so an unlisted sub-model (`gpt-5-pro`) reports no rate instead
|
||||
* of inheriting its family's.
|
||||
*
|
||||
* One known gap the per-model shape cannot express: a few vendors charge more
|
||||
* above a context threshold (Gemini 2.5 Pro past 200k input, Anthropic's 1M-context
|
||||
* beta). Usage is aggregated per model before pricing, so those requests are
|
||||
* estimated at the standard tier and understate. An affected workspace can set the
|
||||
* higher rate as its override.
|
||||
*/
|
||||
const MODEL_PRICES: [name: string, price: PriceEntry | null][] = [
|
||||
// Anthropic — Opus 4.1 and older bill at the pre-4.5 Opus rate, so the family
|
||||
@@ -84,8 +91,9 @@ const MODEL_PRICES: [name: string, price: PriceEntry | null][] = [
|
||||
// rate. There is no charge for writing the cache and no usage field reporting
|
||||
// one, so the write rate never applies. The -mini/-nano entries must precede
|
||||
// the family entry, which would otherwise claim them.
|
||||
// Revisions past gpt-5 are priced separately by OpenAI and are not tracked here;
|
||||
// without these the `gpt-5` entry below would claim them at gpt-5's rates.
|
||||
// Revisions past gpt-5 are priced separately by OpenAI and are not tracked here.
|
||||
// `strictVariants` does not cover these: a version bump is digits, not a name
|
||||
// segment, so `gpt-5-6` would otherwise still match `gpt-5`.
|
||||
['gpt-5.6', null],
|
||||
['gpt-5.5', null],
|
||||
['gpt-5.4', null],
|
||||
@@ -120,7 +128,8 @@ const MODEL_PRICE_MATCHERS = buildModelMatchers(
|
||||
cacheRead: entry.cacheRead ?? entry.input * CACHE_READ_RATIO,
|
||||
cacheWrite: entry.cacheWrite ?? entry.input * CACHE_WRITE_RATIO
|
||||
}
|
||||
])
|
||||
]),
|
||||
{ strictVariants: true }
|
||||
)
|
||||
|
||||
export function getKnownModelPrice(model: string): ModelPrice | undefined {
|
||||
|
||||
Reference in New Issue
Block a user