diff --git a/src/web-server/model-pricing.ts b/src/web-server/model-pricing.ts index 9cf381e7..d8e40679 100644 --- a/src/web-server/model-pricing.ts +++ b/src/web-server/model-pricing.ts @@ -718,10 +718,16 @@ const MODEL_PRICING_ALIASES: Record = { 'qwen3-235b': 'qwen3-max', 'qwen3-vl-plus': 'qwen3.5-plus', 'qwen3-32b': 'qwen3.5-plus', + 'gemini-3-flash-preview': 'gemini-2.5-flash', + 'gemini-3-flash-preview-customtools': 'gemini-2.5-flash', 'gemini-3.1-pro-preview': 'gemini-3-pro-preview', + 'gemini-3.1-flash-preview': 'gemini-2.5-flash', 'gemini-3.1-pro-preview-customtools': 'gemini-3-pro-preview', + 'gemini-3.1-flash-preview-customtools': 'gemini-2.5-flash', 'gemini-3-1-pro-preview': 'gemini-3-pro-preview', + 'gemini-3-1-flash-preview': 'gemini-2.5-flash', 'gemini-3-1-pro-preview-customtools': 'gemini-3-pro-preview', + 'gemini-3-1-flash-preview-customtools': 'gemini-2.5-flash', }; // Default pricing for unknown models diff --git a/tests/unit/cliproxy/model-catalog.test.js b/tests/unit/cliproxy/model-catalog.test.js index e394ed1a..4d675641 100644 --- a/tests/unit/cliproxy/model-catalog.test.js +++ b/tests/unit/cliproxy/model-catalog.test.js @@ -261,6 +261,21 @@ describe('Model Catalog', () => { ); }); + it('falls back to the next supported model when the default is excluded', () => { + const { getSuggestedReplacementModel } = modelCatalog; + + expect(getSuggestedReplacementModel('agy', 'claude-opus-4-6-thinking')).toBe( + 'claude-sonnet-4-6' + ); + expect(getSuggestedReplacementModel('agy')).toBe('claude-opus-4-6-thinking'); + }); + + it('returns undefined when no provider catalog exists', () => { + const { getSuggestedReplacementModel } = modelCatalog; + + expect(getSuggestedReplacementModel('qwen')).toBeUndefined(); + }); + it('returns undefined for unknown model', () => { const { findModel } = modelCatalog; const model = findModel('agy', 'unknown-model'); diff --git a/tests/unit/model-pricing.test.ts b/tests/unit/model-pricing.test.ts index 3b483563..470ced04 100644 --- a/tests/unit/model-pricing.test.ts +++ b/tests/unit/model-pricing.test.ts @@ -76,6 +76,22 @@ describe('model-pricing', () => { expect(pricing).not.toEqual(getModelPricing('unknown-model-xyz')); }); + it('should map Gemini 3 and 3.1 Flash preview variants to flash pricing', () => { + const canonical = getModelPricing('gemini-2.5-flash'); + const aliases = [ + 'gemini-3-flash-preview', + 'gemini-3-flash-preview-customtools', + 'gemini-3.1-flash-preview', + 'gemini-3.1-flash-preview-customtools', + 'gemini-3-1-flash-preview', + 'gemini-3-1-flash-preview-customtools', + ]; + + for (const model of aliases) { + expect(getModelPricing(model)).toEqual(canonical); + } + }); + it('should return different pricing for different model tiers', () => { const sonnet = getModelPricing('claude-sonnet-4-5'); const opus = getModelPricing('claude-opus-4-5-20251101'); diff --git a/ui/src/lib/model-catalogs.ts b/ui/src/lib/model-catalogs.ts index 4ce9a65d..27b78d27 100644 --- a/ui/src/lib/model-catalogs.ts +++ b/ui/src/lib/model-catalogs.ts @@ -5,7 +5,7 @@ import type { ModelEntry, ProviderCatalog } from '@/components/cliproxy/provider-model-selector'; import { stripModelConfigurationSuffixes } from '@/lib/extended-context-utils'; -import { GEMINI_MINOR_VERSION_COMPATIBILITY_IDS } from '../../../src/shared/gemini-minor-version-compatibility'; +import { GEMINI_MINOR_VERSION_COMPATIBILITY_IDS } from '@shared/gemini-minor-version-compatibility'; const GEMINI_PREVIEW_MODEL_ID_PATTERN = /^gemini-(\d+(?:[.-]\d+)*)-(pro|flash)-preview(-customtools)?$/i; diff --git a/ui/tests/unit/ui/lib/preset-utils.test.ts b/ui/tests/unit/ui/lib/preset-utils.test.ts index b219f9c6..e4d0c618 100644 --- a/ui/tests/unit/ui/lib/preset-utils.test.ts +++ b/ui/tests/unit/ui/lib/preset-utils.test.ts @@ -92,4 +92,17 @@ describe('claude preset utils', () => { 'gemini-3-flash-preview' ); }); + + it('passes through non-Gemini model ids unchanged', () => { + expect(resolveCatalogModelId('claude-sonnet-4-6')).toBe('claude-sonnet-4-6'); + }); + + it('falls back to the catalog id when no live model matches', () => { + expect(resolveCatalogModelId('gemini-3.1-pro-preview', [])).toBe('gemini-3.1-pro-preview'); + expect( + resolveCatalogModelId('gemini-3.1-pro-preview', [ + { id: 'gemini-2.5-pro', owned_by: 'google' }, + ]) + ).toBe('gemini-3.1-pro-preview'); + }); }); diff --git a/ui/tsconfig.app.json b/ui/tsconfig.app.json index 99fbd39c..a751b0ca 100644 --- a/ui/tsconfig.app.json +++ b/ui/tsconfig.app.json @@ -26,7 +26,8 @@ /* Path alias */ "baseUrl": ".", "paths": { - "@/*": ["./src/*"] + "@/*": ["./src/*"], + "@shared/*": ["../src/shared/*"] } }, "include": ["src"] diff --git a/ui/vite.config.ts b/ui/vite.config.ts index 39c8d654..4a31e9f5 100644 --- a/ui/vite.config.ts +++ b/ui/vite.config.ts @@ -11,6 +11,7 @@ export default defineConfig({ plugins: [react(), tailwindcss()], resolve: { alias: { + '@shared': path.resolve(REPO_ROOT, './src/shared'), '@': path.resolve(__dirname, './src'), }, }, diff --git a/ui/vitest.config.ts b/ui/vitest.config.ts index a0501bb5..0b3f2697 100644 --- a/ui/vitest.config.ts +++ b/ui/vitest.config.ts @@ -45,6 +45,7 @@ export default defineConfig({ }, resolve: { alias: { + '@shared': path.resolve(__dirname, '../src/shared'), '@': path.resolve(__dirname, './src'), '@tests': path.resolve(__dirname, './tests'), },