From 2c8ad2df6a7fd37caa462dd46d7344083d8d5e61 Mon Sep 17 00:00:00 2001 From: hugocasa Date: Thu, 13 Aug 2026 20:42:56 +0200 Subject: [PATCH] fix: keep alias suffixes resolving to their model's price --- .../src/lib/components/copilot/modelConfig.ts | 15 ++++++++++----- .../lib/components/copilot/modelPricing.test.ts | 12 +++++++++++- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/frontend/src/lib/components/copilot/modelConfig.ts b/frontend/src/lib/components/copilot/modelConfig.ts index a148f3a599..42e1f8df37 100644 --- a/frontend/src/lib/components/copilot/modelConfig.ts +++ b/frontend/src/lib/components/copilot/modelConfig.ts @@ -116,6 +116,9 @@ function normalizeVersionSeparators(model: string): string { return model.replace(/\./g, '-') } +/** Suffixes that name a route to a model rather than a different model. */ +const DECORATIVE_SUFFIXES = ['latest', 'preview', 'beta', 'stable'] + /** * Compile a most-specific-first `[name, value]` table into matchers against the * bare model id. Shared with the pricing table so both resolve the same set of @@ -140,11 +143,13 @@ export function buildModelMatchers( /\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])' : '' + // entry does not match when a further *name* segment follows. What follows + // is only a decoration when it is a date (`-20251101`), Bedrock's `-v1`, or + // one of the alias words below (`claude-3-5-haiku-latest` is the same model + // as `claude-3-5-haiku`, and is a shipped default). 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|${DECORATIVE_SUFFIXES.join('|')})[a-z])` : '' ].join('') return [new RegExp(pattern + guards), value] }) diff --git a/frontend/src/lib/components/copilot/modelPricing.test.ts b/frontend/src/lib/components/copilot/modelPricing.test.ts index c34e125d04..4d87a81360 100644 --- a/frontend/src/lib/components/copilot/modelPricing.test.ts +++ b/frontend/src/lib/components/copilot/modelPricing.test.ts @@ -39,12 +39,22 @@ describe('resolveModelPrice', () => { }) 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. + // Dates, Bedrock's -v1 and floating aliases are ways of spelling one model, + // not sub-models. `claude-3-5-haiku-latest` is a shipped picker default, so + // unpricing it would silently disable cost tracking out of the box. 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) + expect(resolveModelPrice('anthropic', 'claude-3-5-haiku-latest', undefined)?.price.input).toBe( + 0.8 + ) + expect( + resolveModelPrice('openrouter', '~anthropic/claude-sonnet-latest', undefined)?.price.input + ).toBe(3) + // …while a genuine sub-model stays unpriced. + expect(resolveModelPrice('openai', 'gpt-5-pro', undefined)).toBeUndefined() }) it('prefers a workspace override, keeping the model’s own cache ratios', () => {