mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 10:16:49 +00:00
fix: sanitize cliproxy provider model pickers
This commit is contained in:
@@ -14,6 +14,21 @@ import {
|
||||
import { stripModelConfigurationSuffixes } from '../shared/extended-context-utils';
|
||||
import { GEMINI_MINOR_VERSION_COMPATIBILITY_IDS } from '../shared/gemini-minor-version-compatibility';
|
||||
|
||||
const AGY_GEMINI_PRO_HIGH_ID = 'gemini-3.1-pro-high';
|
||||
const AGY_GEMINI_PRO_LOW_ID = 'gemini-3.1-pro-low';
|
||||
const AGY_GEMINI_PRO_COMPATIBILITY_IDS = Object.freeze({
|
||||
'gemini-3-pro-high': AGY_GEMINI_PRO_HIGH_ID,
|
||||
'gemini-3.1-pro-high': AGY_GEMINI_PRO_HIGH_ID,
|
||||
'gemini-3-pro-low': AGY_GEMINI_PRO_LOW_ID,
|
||||
'gemini-3.1-pro-low': AGY_GEMINI_PRO_LOW_ID,
|
||||
'gemini-3-pro-preview': AGY_GEMINI_PRO_HIGH_ID,
|
||||
'gemini-3-pro-preview-customtools': AGY_GEMINI_PRO_HIGH_ID,
|
||||
'gemini-3.1-pro-preview': AGY_GEMINI_PRO_HIGH_ID,
|
||||
'gemini-3.1-pro-preview-customtools': AGY_GEMINI_PRO_HIGH_ID,
|
||||
'gemini-3-1-pro-preview': AGY_GEMINI_PRO_HIGH_ID,
|
||||
'gemini-3-1-pro-preview-customtools': AGY_GEMINI_PRO_HIGH_ID,
|
||||
} satisfies Record<string, string>);
|
||||
|
||||
/**
|
||||
* Thinking support configuration for a model.
|
||||
* Defines how thinking/reasoning budget can be controlled.
|
||||
@@ -114,11 +129,19 @@ export const MODEL_CATALOG: Partial<Record<CLIProxyProvider, ProviderCatalog>> =
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'gemini-3.1-pro-preview',
|
||||
name: 'Gemini 3.1 Pro',
|
||||
description: 'Google latest Gemini Pro model via Antigravity',
|
||||
id: AGY_GEMINI_PRO_HIGH_ID,
|
||||
name: 'Gemini 3.1 Pro High',
|
||||
description: 'Current Antigravity Gemini Pro route with higher reasoning budget',
|
||||
nativeImageInput: true,
|
||||
thinking: { type: 'levels', levels: ['low', 'high'], dynamicAllowed: true },
|
||||
thinking: { type: 'none' },
|
||||
extendedContext: true,
|
||||
},
|
||||
{
|
||||
id: AGY_GEMINI_PRO_LOW_ID,
|
||||
name: 'Gemini 3.1 Pro Low',
|
||||
description: 'Current Antigravity Gemini Pro route with the lighter quota tier',
|
||||
nativeImageInput: true,
|
||||
thinking: { type: 'none' },
|
||||
extendedContext: true,
|
||||
},
|
||||
{
|
||||
@@ -465,6 +488,16 @@ export function findModel(provider: CLIProxyProvider, modelId: string): ModelEnt
|
||||
if (compatibilityId) {
|
||||
lookupCandidates.add(compatibilityId);
|
||||
}
|
||||
|
||||
if (isAntigravityProvider(provider)) {
|
||||
const agyCompatibilityId =
|
||||
AGY_GEMINI_PRO_COMPATIBILITY_IDS[
|
||||
candidate as keyof typeof AGY_GEMINI_PRO_COMPATIBILITY_IDS
|
||||
];
|
||||
if (agyCompatibilityId) {
|
||||
lookupCandidates.add(agyCompatibilityId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return catalog.models.find((m) => lookupCandidates.has(m.id.toLowerCase()));
|
||||
|
||||
@@ -91,11 +91,20 @@ describe('Model Catalog', () => {
|
||||
assert.strictEqual(ids.includes('claude-sonnet-4-5'), false);
|
||||
});
|
||||
|
||||
it('includes Gemini 3.1 Pro (free via Antigravity)', () => {
|
||||
it('includes Gemini 3.1 Pro High via Antigravity', () => {
|
||||
const { MODEL_CATALOG } = modelCatalog;
|
||||
const gem3 = MODEL_CATALOG.agy.models.find((m) => m.id === 'gemini-3.1-pro-preview');
|
||||
assert(gem3, 'Should include Gemini 3.1 Pro');
|
||||
assert.strictEqual(gem3.name, 'Gemini 3.1 Pro');
|
||||
const gem3 = MODEL_CATALOG.agy.models.find((m) => m.id === 'gemini-3.1-pro-high');
|
||||
assert(gem3, 'Should include Gemini 3.1 Pro High');
|
||||
assert.strictEqual(gem3.name, 'Gemini 3.1 Pro High');
|
||||
// AGY models are all free - no paid tier
|
||||
assert.strictEqual(gem3.tier, undefined, 'AGY models should not have paid tier');
|
||||
});
|
||||
|
||||
it('includes Gemini 3.1 Pro Low via Antigravity', () => {
|
||||
const { MODEL_CATALOG } = modelCatalog;
|
||||
const gem3 = MODEL_CATALOG.agy.models.find((m) => m.id === 'gemini-3.1-pro-low');
|
||||
assert(gem3, 'Should include Gemini 3.1 Pro Low');
|
||||
assert.strictEqual(gem3.name, 'Gemini 3.1 Pro Low');
|
||||
// AGY models are all free - no paid tier
|
||||
assert.strictEqual(gem3.tier, undefined, 'AGY models should not have paid tier');
|
||||
});
|
||||
@@ -108,9 +117,9 @@ describe('Model Catalog', () => {
|
||||
assert.strictEqual(flash.tier, undefined, 'AGY models should not have paid tier');
|
||||
});
|
||||
|
||||
it('has 4 models total', () => {
|
||||
it('has 5 models total', () => {
|
||||
const { MODEL_CATALOG } = modelCatalog;
|
||||
assert.strictEqual(MODEL_CATALOG.agy.models.length, 4);
|
||||
assert.strictEqual(MODEL_CATALOG.agy.models.length, 5);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -262,13 +271,15 @@ describe('Model Catalog', () => {
|
||||
assert.strictEqual(legacySonnet?.id, 'claude-sonnet-4-6');
|
||||
});
|
||||
|
||||
it('treats Gemini 3 and 3.1 preview IDs as the same catalog family', () => {
|
||||
it('maps legacy Antigravity Gemini Pro aliases onto the current 3.1 high/low models', () => {
|
||||
const { findModel, getSuggestedReplacementModel } = modelCatalog;
|
||||
const legacyAgyGemini = findModel('agy', 'gemini-3-pro-preview');
|
||||
const legacyAgyGeminiLow = findModel('agy', 'gemini-3-pro-low');
|
||||
const legacyGemini = findModel('gemini', 'gemini-3-pro-preview');
|
||||
const currentGemini = findModel('gemini', 'gemini-3.1-pro-preview');
|
||||
|
||||
assert.strictEqual(legacyAgyGemini?.id, 'gemini-3.1-pro-preview');
|
||||
assert.strictEqual(legacyAgyGemini?.id, 'gemini-3.1-pro-high');
|
||||
assert.strictEqual(legacyAgyGeminiLow?.id, 'gemini-3.1-pro-low');
|
||||
assert.strictEqual(legacyGemini?.id, 'gemini-3.1-pro-preview');
|
||||
assert.strictEqual(currentGemini?.id, 'gemini-3.1-pro-preview');
|
||||
assert.strictEqual(
|
||||
@@ -371,7 +382,7 @@ describe('Model Catalog', () => {
|
||||
const sonnetThinkingIdx = models.findIndex((m) => m.id === 'claude-sonnet-4-6');
|
||||
|
||||
// Find indices of the remaining non-Claude model
|
||||
const geminiIdx = models.findIndex((m) => m.id === 'gemini-3.1-pro-preview');
|
||||
const geminiIdx = models.findIndex((m) => m.id === 'gemini-3.1-pro-high');
|
||||
|
||||
// Primary Claude choices should appear ahead of Gemini fallback.
|
||||
assert(opusIdx < geminiIdx, 'Opus should be above Gemini');
|
||||
|
||||
@@ -13,7 +13,7 @@ import { SearchableSelect } from '@/components/ui/searchable-select';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import type { CliproxyProviderRoutingHints } from '@/lib/api-client';
|
||||
import { getCodexEffortDisplay } from '@/lib/codex-effort';
|
||||
import { getResolvedCatalogModels } from '@/lib/model-catalogs';
|
||||
import { getResolvedCatalogModels, getSupplementalCatalogModels } from '@/lib/model-catalogs';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
/** Model entry from catalog */
|
||||
@@ -329,6 +329,10 @@ export function FlexibleModelSelector({
|
||||
() => getResolvedCatalogModels(catalog, allModels),
|
||||
[allModels, catalog]
|
||||
);
|
||||
const supplementalModels = useMemo(
|
||||
() => getSupplementalCatalogModels(catalog?.provider ?? '', catalog, allModels),
|
||||
[allModels, catalog]
|
||||
);
|
||||
const catalogModelIds = new Set(resolvedCatalogModels.map((model) => model.id));
|
||||
const routingHints = useMemo(
|
||||
() =>
|
||||
@@ -388,7 +392,7 @@ export function FlexibleModelSelector({
|
||||
),
|
||||
}));
|
||||
|
||||
const allModelOptions = allModels
|
||||
const allModelOptions = supplementalModels
|
||||
.filter((model) => !catalogModelIds.has(model.id))
|
||||
.filter(
|
||||
(model) =>
|
||||
@@ -495,14 +499,20 @@ export function FlexibleModelSelector({
|
||||
<span className="text-xs text-primary">{t('providerModelSelector.recommended')}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'all',
|
||||
label: (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{t('providerModelSelector.allModelsCount', { count: allModels.length })}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
...(allModelOptions.length > 0
|
||||
? [
|
||||
{
|
||||
key: 'all',
|
||||
label: (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{t('providerModelSelector.allModelsCount', {
|
||||
count: allModelOptions.length,
|
||||
})}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]}
|
||||
options={[
|
||||
...(selectedValueMissing && legacySelectedOption ? [legacySelectedOption] : []),
|
||||
|
||||
@@ -9,6 +9,21 @@ import { GEMINI_MINOR_VERSION_COMPATIBILITY_IDS } from '@shared/gemini-minor-ver
|
||||
|
||||
const GEMINI_PREVIEW_MODEL_ID_PATTERN =
|
||||
/^gemini-(\d+(?:[.-]\d+)*)-(pro|flash)-preview(-customtools)?$/i;
|
||||
const AGY_GEMINI_PRO_HIGH_ID = 'gemini-3.1-pro-high';
|
||||
const AGY_GEMINI_PRO_LOW_ID = 'gemini-3.1-pro-low';
|
||||
const AGY_GEMINI_PRO_COMPATIBILITY_IDS = new Map<string, string>([
|
||||
['gemini-3-pro-high', AGY_GEMINI_PRO_HIGH_ID],
|
||||
['gemini-3.1-pro-high', AGY_GEMINI_PRO_HIGH_ID],
|
||||
['gemini-3-pro-low', AGY_GEMINI_PRO_LOW_ID],
|
||||
['gemini-3.1-pro-low', AGY_GEMINI_PRO_LOW_ID],
|
||||
['gemini-3-pro-preview', AGY_GEMINI_PRO_HIGH_ID],
|
||||
['gemini-3-pro-preview-customtools', AGY_GEMINI_PRO_HIGH_ID],
|
||||
['gemini-3.1-pro-preview', AGY_GEMINI_PRO_HIGH_ID],
|
||||
['gemini-3.1-pro-preview-customtools', AGY_GEMINI_PRO_HIGH_ID],
|
||||
['gemini-3-1-pro-preview', AGY_GEMINI_PRO_HIGH_ID],
|
||||
['gemini-3-1-pro-preview-customtools', AGY_GEMINI_PRO_HIGH_ID],
|
||||
]);
|
||||
const MANAGED_MODEL_PREFIXES = ['agy/', 'gcli/'] as const;
|
||||
|
||||
export type CatalogAvailableModel = {
|
||||
id: string;
|
||||
@@ -29,6 +44,27 @@ function normalizeModelId(modelId: string): string {
|
||||
return stripModelConfigurationSuffixes(modelId).toLowerCase();
|
||||
}
|
||||
|
||||
function stripManagedModelPrefix(modelId: string): string {
|
||||
const trimmedModelId = modelId.trim();
|
||||
const normalizedModelId = trimmedModelId.toLowerCase();
|
||||
|
||||
for (const prefix of MANAGED_MODEL_PREFIXES) {
|
||||
if (normalizedModelId.startsWith(prefix)) {
|
||||
return trimmedModelId.slice(prefix.length);
|
||||
}
|
||||
}
|
||||
|
||||
return trimmedModelId;
|
||||
}
|
||||
|
||||
function stripCustomtoolsSuffix(modelId: string): string {
|
||||
return modelId.replace(/-customtools$/i, '');
|
||||
}
|
||||
|
||||
function getAgyGeminiProCompatibilityId(modelId: string): string | undefined {
|
||||
return AGY_GEMINI_PRO_COMPATIBILITY_IDS.get(normalizeModelId(modelId));
|
||||
}
|
||||
|
||||
function parseGeminiPreviewModelId(modelId: string): GeminiPreviewModelInfo | null {
|
||||
const normalizedId = normalizeModelId(modelId);
|
||||
const match = normalizedId.match(GEMINI_PREVIEW_MODEL_ID_PATTERN);
|
||||
@@ -142,14 +178,26 @@ export const MODEL_CATALOGS: Record<string, ProviderCatalog> = {
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'gemini-3.1-pro-preview',
|
||||
name: 'Gemini Pro',
|
||||
description: 'Resolves to the best advertised Gemini Pro preview via Antigravity',
|
||||
id: AGY_GEMINI_PRO_HIGH_ID,
|
||||
name: 'Gemini Pro High',
|
||||
description: 'Current Antigravity Gemini Pro route with higher reasoning budget',
|
||||
extendedContext: true,
|
||||
presetMapping: {
|
||||
default: 'gemini-3.1-pro-preview',
|
||||
opus: 'gemini-3.1-pro-preview',
|
||||
sonnet: 'gemini-3.1-pro-preview',
|
||||
default: AGY_GEMINI_PRO_HIGH_ID,
|
||||
opus: AGY_GEMINI_PRO_HIGH_ID,
|
||||
sonnet: AGY_GEMINI_PRO_HIGH_ID,
|
||||
haiku: 'gemini-3-1-flash-preview',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: AGY_GEMINI_PRO_LOW_ID,
|
||||
name: 'Gemini Pro Low',
|
||||
description: 'Current Antigravity Gemini Pro route with the lighter quota tier',
|
||||
extendedContext: true,
|
||||
presetMapping: {
|
||||
default: AGY_GEMINI_PRO_LOW_ID,
|
||||
opus: AGY_GEMINI_PRO_LOW_ID,
|
||||
sonnet: AGY_GEMINI_PRO_LOW_ID,
|
||||
haiku: 'gemini-3-1-flash-preview',
|
||||
},
|
||||
},
|
||||
@@ -684,6 +732,13 @@ function findCatalogModelInCatalog(catalog: ProviderCatalog | undefined, modelId
|
||||
if (!catalog) return undefined;
|
||||
|
||||
const normalizedModelId = normalizeModelId(modelId);
|
||||
if (catalog.provider === 'agy') {
|
||||
const agyCompatibilityId = getAgyGeminiProCompatibilityId(normalizedModelId);
|
||||
if (agyCompatibilityId) {
|
||||
const compatibilityMatch = catalog.models.find((model) => model.id === agyCompatibilityId);
|
||||
if (compatibilityMatch) return compatibilityMatch;
|
||||
}
|
||||
}
|
||||
const compatibilityModelId =
|
||||
GEMINI_MINOR_VERSION_COMPATIBILITY_IDS[
|
||||
normalizedModelId.toLowerCase() as keyof typeof GEMINI_MINOR_VERSION_COMPATIBILITY_IDS
|
||||
@@ -835,18 +890,24 @@ export function getResolvedCatalogModels(
|
||||
) {
|
||||
if (!catalog) return [];
|
||||
|
||||
const recommendedCatalog = MODEL_CATALOGS[catalog.provider.toLowerCase()] ?? catalog;
|
||||
const seenModelIds = new Set<string>();
|
||||
|
||||
return catalog.models
|
||||
return recommendedCatalog.models
|
||||
.map((model) => {
|
||||
const resolvedModelId = resolveCatalogModelId(model.id, availableModels);
|
||||
const resolvedPresetModelMapping = model.presetMapping
|
||||
? resolvePresetMapping(model.presetMapping, availableModels)
|
||||
: undefined;
|
||||
const liveModelMatch = catalog.models.find(
|
||||
(catalogModel) => normalizeModelId(catalogModel.id) === normalizeModelId(resolvedModelId)
|
||||
);
|
||||
|
||||
return {
|
||||
...model,
|
||||
id: resolvedModelId,
|
||||
name: liveModelMatch?.name || model.name,
|
||||
description: liveModelMatch?.description ?? model.description,
|
||||
presetMapping: resolvedPresetModelMapping,
|
||||
};
|
||||
})
|
||||
@@ -857,6 +918,62 @@ export function getResolvedCatalogModels(
|
||||
});
|
||||
}
|
||||
|
||||
export function getSupplementalCatalogModels(
|
||||
provider: string,
|
||||
catalog: ProviderCatalog | undefined,
|
||||
availableModels: CatalogAvailableModel[] = []
|
||||
) {
|
||||
const normalizedProvider = provider.trim().toLowerCase();
|
||||
if (!catalog || !normalizedProvider) return [];
|
||||
|
||||
const staticCatalog = MODEL_CATALOGS[normalizedProvider] ?? catalog;
|
||||
const recommendedModels = getResolvedCatalogModels(catalog, availableModels);
|
||||
const recommendedIds = new Set(recommendedModels.map((model) => normalizeModelId(model.id)));
|
||||
const recommendedCanonicalIds = new Set(
|
||||
recommendedModels
|
||||
.map((model) => findCatalogModelInCatalog(staticCatalog, model.id)?.id)
|
||||
.filter((modelId): modelId is string => Boolean(modelId))
|
||||
.map((modelId) => normalizeModelId(modelId))
|
||||
);
|
||||
const seenCanonicalIds = new Set<string>();
|
||||
const normalizedAvailableIds = new Set(
|
||||
availableModels.map((model) => normalizeModelId(stripManagedModelPrefix(model.id)))
|
||||
);
|
||||
|
||||
return availableModels.filter((availableModel) => {
|
||||
const strippedModelId = stripManagedModelPrefix(availableModel.id);
|
||||
const baseModelId = stripCustomtoolsSuffix(strippedModelId);
|
||||
const matchedModel =
|
||||
findCatalogModelInCatalog(staticCatalog, strippedModelId) ??
|
||||
findCatalogModelInCatalog(staticCatalog, baseModelId);
|
||||
|
||||
if (!matchedModel) return false;
|
||||
|
||||
if (recommendedIds.has(normalizeModelId(strippedModelId))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const canonicalId = normalizeModelId(matchedModel.id);
|
||||
if (recommendedCanonicalIds.has(canonicalId)) {
|
||||
return false;
|
||||
}
|
||||
if (seenCanonicalIds.has(canonicalId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const normalizedBaseModelId = normalizeModelId(baseModelId);
|
||||
if (
|
||||
normalizedBaseModelId !== normalizeModelId(strippedModelId) &&
|
||||
normalizedAvailableIds.has(normalizedBaseModelId)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
seenCanonicalIds.add(canonicalId);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
export function supportsExtendedContext(
|
||||
provider: string,
|
||||
modelId: string,
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { FlexibleModelSelector } from '@/components/cliproxy/provider-model-selector';
|
||||
import { buildUiCatalogs } from '@/lib/model-catalogs';
|
||||
import { render, screen, userEvent } from '@tests/setup/test-utils';
|
||||
|
||||
const noisyAgyModels = [
|
||||
{ id: 'agy/gemini-3.1-pro-high', owned_by: 'antigravity' },
|
||||
{ id: 'gemini-3.1-pro-high', owned_by: 'antigravity' },
|
||||
{ id: 'gemini-3.1-pro-low', owned_by: 'antigravity' },
|
||||
{ id: 'gemini-3.1-pro-preview', owned_by: 'antigravity' },
|
||||
{ id: 'gemini-3.1-pro-preview-customtools', owned_by: 'antigravity' },
|
||||
{ id: 'gemini-3-pro-preview', owned_by: 'antigravity' },
|
||||
{ id: 'gemini-3-1-flash-preview', owned_by: 'antigravity' },
|
||||
{ id: 'gemini-3-1-flash-preview-customtools', owned_by: 'antigravity' },
|
||||
{ id: 'gpt-oss-120b-medium', owned_by: 'antigravity' },
|
||||
];
|
||||
|
||||
const catalog = buildUiCatalogs({
|
||||
agy: {
|
||||
provider: 'agy',
|
||||
displayName: 'Antigravity',
|
||||
defaultModel: 'claude-opus-4-6-thinking',
|
||||
models: noisyAgyModels.map(({ id }) => ({ id, name: id })),
|
||||
},
|
||||
}).agy;
|
||||
|
||||
describe('FlexibleModelSelector', () => {
|
||||
beforeEach(() => {
|
||||
Object.defineProperty(HTMLElement.prototype, 'hasPointerCapture', {
|
||||
configurable: true,
|
||||
value: vi.fn(() => false),
|
||||
});
|
||||
Object.defineProperty(HTMLElement.prototype, 'setPointerCapture', {
|
||||
configurable: true,
|
||||
value: vi.fn(),
|
||||
});
|
||||
Object.defineProperty(HTMLElement.prototype, 'releasePointerCapture', {
|
||||
configurable: true,
|
||||
value: vi.fn(),
|
||||
});
|
||||
Object.defineProperty(Element.prototype, 'scrollIntoView', {
|
||||
configurable: true,
|
||||
value: vi.fn(),
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps the picker focused on curated Antigravity live routes', async () => {
|
||||
render(
|
||||
<FlexibleModelSelector
|
||||
label="Primary model"
|
||||
value={undefined}
|
||||
onChange={vi.fn()}
|
||||
catalog={catalog}
|
||||
allModels={noisyAgyModels}
|
||||
/>
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: /select model/i }));
|
||||
|
||||
expect(screen.getByText('gemini-3.1-pro-high')).toBeInTheDocument();
|
||||
expect(screen.getByText('gemini-3.1-pro-low')).toBeInTheDocument();
|
||||
expect(screen.getByText('gemini-3-1-flash-preview')).toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByText('gemini-3.1-pro-preview')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('gemini-3-pro-preview')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('agy/gemini-3.1-pro-high')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('gemini-3-1-flash-preview-customtools')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('gpt-oss-120b-medium')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/All Models \(/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('preserves a filtered legacy value under the current-value fallback group', async () => {
|
||||
render(
|
||||
<FlexibleModelSelector
|
||||
label="Primary model"
|
||||
value="gemini-3.1-pro-preview"
|
||||
onChange={vi.fn()}
|
||||
catalog={catalog}
|
||||
allModels={noisyAgyModels}
|
||||
/>
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: /gemini-3\.1-pro-preview/i }));
|
||||
|
||||
expect(screen.getByText('Current value')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('gemini-3.1-pro-preview').length).toBeGreaterThan(0);
|
||||
expect(screen.getByText('gemini-3.1-pro-high')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
MODEL_CATALOGS,
|
||||
findCatalogModel,
|
||||
getResolvedCatalogModels,
|
||||
getSupplementalCatalogModels,
|
||||
resolveCatalogModelId,
|
||||
} from '@/lib/model-catalogs';
|
||||
import { applyDefaultPreset } from '@/lib/preset-utils';
|
||||
@@ -126,29 +127,49 @@ describe('claude preset utils', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('resolves Gemini preview presets to the best live family match', () => {
|
||||
it('maps legacy Antigravity Gemini Pro aliases to the current catalog entries', () => {
|
||||
expect(findCatalogModel('agy', 'gemini-3.1-pro-preview')?.id).toBe('gemini-3.1-pro-high');
|
||||
expect(findCatalogModel('agy', 'gemini-3-pro-preview')?.id).toBe('gemini-3.1-pro-high');
|
||||
expect(findCatalogModel('agy', 'gemini-3-pro-low')?.id).toBe('gemini-3.1-pro-low');
|
||||
});
|
||||
|
||||
it('resolves Antigravity Gemini presets to the current live-safe model ids', () => {
|
||||
const availableModels = [
|
||||
{ id: 'gemini-3.9-pro-preview-customtools', owned_by: 'antigravity' },
|
||||
{ id: 'gemini-3.9-pro-preview', owned_by: 'antigravity' },
|
||||
{ id: 'gemini-3-9-flash-preview-customtools', owned_by: 'antigravity' },
|
||||
{ id: 'gemini-3-9-flash-preview', owned_by: 'antigravity' },
|
||||
{ id: 'agy/gemini-3.1-pro-high', owned_by: 'antigravity' },
|
||||
{ id: 'gemini-3.1-pro-high', owned_by: 'antigravity' },
|
||||
{ id: 'gemini-3.1-pro-low', owned_by: 'antigravity' },
|
||||
{ id: 'gemini-3.1-pro-preview', owned_by: 'antigravity' },
|
||||
{ id: 'gemini-3.1-pro-preview-customtools', owned_by: 'antigravity' },
|
||||
{ id: 'gemini-3-pro-preview', owned_by: 'antigravity' },
|
||||
{ id: 'gemini-3-1-flash-preview-customtools', owned_by: 'antigravity' },
|
||||
{ id: 'gemini-3-1-flash-preview', owned_by: 'antigravity' },
|
||||
{ id: 'gpt-oss-120b-medium', owned_by: 'antigravity' },
|
||||
];
|
||||
|
||||
expect(resolveCatalogModelId('gemini-3.1-pro-preview', availableModels)).toBe(
|
||||
'gemini-3.9-pro-preview'
|
||||
);
|
||||
expect(resolveCatalogModelId('gemini-3-flash-preview', availableModels)).toBe(
|
||||
'gemini-3-9-flash-preview'
|
||||
);
|
||||
expect(findCatalogModel('agy', 'gemini-3.9-pro-preview')?.id).toBe('gemini-3.1-pro-preview');
|
||||
expect(findCatalogModel('agy', 'gemini-3.1-pro-preview')?.id).toBe('gemini-3.1-pro-high');
|
||||
|
||||
const resolvedAgyModels = getResolvedCatalogModels(MODEL_CATALOGS.agy, availableModels);
|
||||
expect(resolvedAgyModels.find((model) => model.name === 'Gemini Pro')?.id).toBe(
|
||||
'gemini-3.9-pro-preview'
|
||||
expect(resolvedAgyModels.find((model) => model.name === 'Gemini Pro High')?.id).toBe(
|
||||
'gemini-3.1-pro-high'
|
||||
);
|
||||
expect(resolvedAgyModels.find((model) => model.name === 'Gemini Pro Low')?.id).toBe(
|
||||
'gemini-3.1-pro-low'
|
||||
);
|
||||
expect(resolvedAgyModels.find((model) => model.name === 'Gemini Flash')?.id).toBe(
|
||||
'gemini-3-9-flash-preview'
|
||||
'gemini-3-1-flash-preview'
|
||||
);
|
||||
|
||||
const supplementalAgyModels = getSupplementalCatalogModels(
|
||||
'agy',
|
||||
MODEL_CATALOGS.agy,
|
||||
availableModels
|
||||
);
|
||||
expect(supplementalAgyModels.map((model) => model.id)).not.toContain('gpt-oss-120b-medium');
|
||||
expect(supplementalAgyModels.map((model) => model.id)).not.toContain('gemini-3-pro-preview');
|
||||
expect(supplementalAgyModels.map((model) => model.id)).not.toContain('gemini-3.1-pro-preview');
|
||||
expect(supplementalAgyModels.map((model) => model.id)).not.toContain('agy/gemini-3.1-pro-high');
|
||||
expect(supplementalAgyModels.map((model) => model.id)).not.toContain(
|
||||
'gemini-3-1-flash-preview-customtools'
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user