mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-11 03:13:12 +00:00
fix: sanitize cliproxy provider model pickers
This commit is contained in:
1 parent
cfdae7be9f
commit
0e4677f558
6 files changed
+327
-45
No files matched your search
@@ -14,6 +14,21 @@ import {
|
|||||||
import { stripModelConfigurationSuffixes } from '../shared/extended-context-utils';
|
import { stripModelConfigurationSuffixes } from '../shared/extended-context-utils';
|
||||||
import { GEMINI_MINOR_VERSION_COMPATIBILITY_IDS } from '../shared/gemini-minor-version-compatibility';
|
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.
|
* Thinking support configuration for a model.
|
||||||
* Defines how thinking/reasoning budget can be controlled.
|
* 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',
|
id: AGY_GEMINI_PRO_HIGH_ID,
|
||||||
name: 'Gemini 3.1 Pro',
|
name: 'Gemini 3.1 Pro High',
|
||||||
description: 'Google latest Gemini Pro model via Antigravity',
|
description: 'Current Antigravity Gemini Pro route with higher reasoning budget',
|
||||||
nativeImageInput: true,
|
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,
|
extendedContext: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -465,6 +488,16 @@ export function findModel(provider: CLIProxyProvider, modelId: string): ModelEnt
|
|||||||
if (compatibilityId) {
|
if (compatibilityId) {
|
||||||
lookupCandidates.add(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()));
|
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);
|
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 { MODEL_CATALOG } = modelCatalog;
|
||||||
const gem3 = MODEL_CATALOG.agy.models.find((m) => m.id === 'gemini-3.1-pro-preview');
|
const gem3 = MODEL_CATALOG.agy.models.find((m) => m.id === 'gemini-3.1-pro-high');
|
||||||
assert(gem3, 'Should include Gemini 3.1 Pro');
|
assert(gem3, 'Should include Gemini 3.1 Pro High');
|
||||||
assert.strictEqual(gem3.name, 'Gemini 3.1 Pro');
|
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
|
// AGY models are all free - no paid tier
|
||||||
assert.strictEqual(gem3.tier, undefined, 'AGY models should not have 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');
|
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;
|
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');
|
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 { findModel, getSuggestedReplacementModel } = modelCatalog;
|
||||||
const legacyAgyGemini = findModel('agy', 'gemini-3-pro-preview');
|
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 legacyGemini = findModel('gemini', 'gemini-3-pro-preview');
|
||||||
const currentGemini = findModel('gemini', 'gemini-3.1-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(legacyGemini?.id, 'gemini-3.1-pro-preview');
|
||||||
assert.strictEqual(currentGemini?.id, 'gemini-3.1-pro-preview');
|
assert.strictEqual(currentGemini?.id, 'gemini-3.1-pro-preview');
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
@@ -371,7 +382,7 @@ describe('Model Catalog', () => {
|
|||||||
const sonnetThinkingIdx = models.findIndex((m) => m.id === 'claude-sonnet-4-6');
|
const sonnetThinkingIdx = models.findIndex((m) => m.id === 'claude-sonnet-4-6');
|
||||||
|
|
||||||
// Find indices of the remaining non-Claude model
|
// 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.
|
// Primary Claude choices should appear ahead of Gemini fallback.
|
||||||
assert(opusIdx < geminiIdx, 'Opus should be above Gemini');
|
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 { Skeleton } from '@/components/ui/skeleton';
|
||||||
import type { CliproxyProviderRoutingHints } from '@/lib/api-client';
|
import type { CliproxyProviderRoutingHints } from '@/lib/api-client';
|
||||||
import { getCodexEffortDisplay } from '@/lib/codex-effort';
|
import { getCodexEffortDisplay } from '@/lib/codex-effort';
|
||||||
import { getResolvedCatalogModels } from '@/lib/model-catalogs';
|
import { getResolvedCatalogModels, getSupplementalCatalogModels } from '@/lib/model-catalogs';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
/** Model entry from catalog */
|
/** Model entry from catalog */
|
||||||
@@ -329,6 +329,10 @@ export function FlexibleModelSelector({
|
|||||||
() => getResolvedCatalogModels(catalog, allModels),
|
() => getResolvedCatalogModels(catalog, allModels),
|
||||||
[allModels, catalog]
|
[allModels, catalog]
|
||||||
);
|
);
|
||||||
|
const supplementalModels = useMemo(
|
||||||
|
() => getSupplementalCatalogModels(catalog?.provider ?? '', catalog, allModels),
|
||||||
|
[allModels, catalog]
|
||||||
|
);
|
||||||
const catalogModelIds = new Set(resolvedCatalogModels.map((model) => model.id));
|
const catalogModelIds = new Set(resolvedCatalogModels.map((model) => model.id));
|
||||||
const routingHints = useMemo(
|
const routingHints = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -388,7 +392,7 @@ export function FlexibleModelSelector({
|
|||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const allModelOptions = allModels
|
const allModelOptions = supplementalModels
|
||||||
.filter((model) => !catalogModelIds.has(model.id))
|
.filter((model) => !catalogModelIds.has(model.id))
|
||||||
.filter(
|
.filter(
|
||||||
(model) =>
|
(model) =>
|
||||||
@@ -495,14 +499,20 @@ export function FlexibleModelSelector({
|
|||||||
<span className="text-xs text-primary">{t('providerModelSelector.recommended')}</span>
|
<span className="text-xs text-primary">{t('providerModelSelector.recommended')}</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
...(allModelOptions.length > 0
|
||||||
key: 'all',
|
? [
|
||||||
label: (
|
{
|
||||||
<span className="text-xs text-muted-foreground">
|
key: 'all',
|
||||||
{t('providerModelSelector.allModelsCount', { count: allModels.length })}
|
label: (
|
||||||
</span>
|
<span className="text-xs text-muted-foreground">
|
||||||
),
|
{t('providerModelSelector.allModelsCount', {
|
||||||
},
|
count: allModelOptions.length,
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
]}
|
]}
|
||||||
options={[
|
options={[
|
||||||
...(selectedValueMissing && legacySelectedOption ? [legacySelectedOption] : []),
|
...(selectedValueMissing && legacySelectedOption ? [legacySelectedOption] : []),
|
||||||
|
|||||||
@@ -9,6 +9,21 @@ import { GEMINI_MINOR_VERSION_COMPATIBILITY_IDS } from '@shared/gemini-minor-ver
|
|||||||
|
|
||||||
const GEMINI_PREVIEW_MODEL_ID_PATTERN =
|
const GEMINI_PREVIEW_MODEL_ID_PATTERN =
|
||||||
/^gemini-(\d+(?:[.-]\d+)*)-(pro|flash)-preview(-customtools)?$/i;
|
/^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 = {
|
export type CatalogAvailableModel = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -29,6 +44,27 @@ function normalizeModelId(modelId: string): string {
|
|||||||
return stripModelConfigurationSuffixes(modelId).toLowerCase();
|
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 {
|
function parseGeminiPreviewModelId(modelId: string): GeminiPreviewModelInfo | null {
|
||||||
const normalizedId = normalizeModelId(modelId);
|
const normalizedId = normalizeModelId(modelId);
|
||||||
const match = normalizedId.match(GEMINI_PREVIEW_MODEL_ID_PATTERN);
|
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',
|
id: AGY_GEMINI_PRO_HIGH_ID,
|
||||||
name: 'Gemini Pro',
|
name: 'Gemini Pro High',
|
||||||
description: 'Resolves to the best advertised Gemini Pro preview via Antigravity',
|
description: 'Current Antigravity Gemini Pro route with higher reasoning budget',
|
||||||
extendedContext: true,
|
extendedContext: true,
|
||||||
presetMapping: {
|
presetMapping: {
|
||||||
default: 'gemini-3.1-pro-preview',
|
default: AGY_GEMINI_PRO_HIGH_ID,
|
||||||
opus: 'gemini-3.1-pro-preview',
|
opus: AGY_GEMINI_PRO_HIGH_ID,
|
||||||
sonnet: 'gemini-3.1-pro-preview',
|
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',
|
haiku: 'gemini-3-1-flash-preview',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -684,6 +732,13 @@ function findCatalogModelInCatalog(catalog: ProviderCatalog | undefined, modelId
|
|||||||
if (!catalog) return undefined;
|
if (!catalog) return undefined;
|
||||||
|
|
||||||
const normalizedModelId = normalizeModelId(modelId);
|
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 =
|
const compatibilityModelId =
|
||||||
GEMINI_MINOR_VERSION_COMPATIBILITY_IDS[
|
GEMINI_MINOR_VERSION_COMPATIBILITY_IDS[
|
||||||
normalizedModelId.toLowerCase() as keyof typeof 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 [];
|
if (!catalog) return [];
|
||||||
|
|
||||||
|
const recommendedCatalog = MODEL_CATALOGS[catalog.provider.toLowerCase()] ?? catalog;
|
||||||
const seenModelIds = new Set<string>();
|
const seenModelIds = new Set<string>();
|
||||||
|
|
||||||
return catalog.models
|
return recommendedCatalog.models
|
||||||
.map((model) => {
|
.map((model) => {
|
||||||
const resolvedModelId = resolveCatalogModelId(model.id, availableModels);
|
const resolvedModelId = resolveCatalogModelId(model.id, availableModels);
|
||||||
const resolvedPresetModelMapping = model.presetMapping
|
const resolvedPresetModelMapping = model.presetMapping
|
||||||
? resolvePresetMapping(model.presetMapping, availableModels)
|
? resolvePresetMapping(model.presetMapping, availableModels)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
const liveModelMatch = catalog.models.find(
|
||||||
|
(catalogModel) => normalizeModelId(catalogModel.id) === normalizeModelId(resolvedModelId)
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...model,
|
...model,
|
||||||
id: resolvedModelId,
|
id: resolvedModelId,
|
||||||
|
name: liveModelMatch?.name || model.name,
|
||||||
|
description: liveModelMatch?.description ?? model.description,
|
||||||
presetMapping: resolvedPresetModelMapping,
|
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(
|
export function supportsExtendedContext(
|
||||||
provider: string,
|
provider: string,
|
||||||
modelId: 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,
|
MODEL_CATALOGS,
|
||||||
findCatalogModel,
|
findCatalogModel,
|
||||||
getResolvedCatalogModels,
|
getResolvedCatalogModels,
|
||||||
|
getSupplementalCatalogModels,
|
||||||
resolveCatalogModelId,
|
resolveCatalogModelId,
|
||||||
} from '@/lib/model-catalogs';
|
} from '@/lib/model-catalogs';
|
||||||
import { applyDefaultPreset } from '@/lib/preset-utils';
|
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 = [
|
const availableModels = [
|
||||||
{ id: 'gemini-3.9-pro-preview-customtools', owned_by: 'antigravity' },
|
{ id: 'agy/gemini-3.1-pro-high', owned_by: 'antigravity' },
|
||||||
{ id: 'gemini-3.9-pro-preview', owned_by: 'antigravity' },
|
{ id: 'gemini-3.1-pro-high', owned_by: 'antigravity' },
|
||||||
{ id: 'gemini-3-9-flash-preview-customtools', owned_by: 'antigravity' },
|
{ id: 'gemini-3.1-pro-low', owned_by: 'antigravity' },
|
||||||
{ id: 'gemini-3-9-flash-preview', owned_by: 'antigravity' },
|
|
||||||
{ id: 'gemini-3.1-pro-preview', 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(
|
expect(findCatalogModel('agy', 'gemini-3.1-pro-preview')?.id).toBe('gemini-3.1-pro-high');
|
||||||
'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');
|
|
||||||
|
|
||||||
const resolvedAgyModels = getResolvedCatalogModels(MODEL_CATALOGS.agy, availableModels);
|
const resolvedAgyModels = getResolvedCatalogModels(MODEL_CATALOGS.agy, availableModels);
|
||||||
expect(resolvedAgyModels.find((model) => model.name === 'Gemini Pro')?.id).toBe(
|
expect(resolvedAgyModels.find((model) => model.name === 'Gemini Pro High')?.id).toBe(
|
||||||
'gemini-3.9-pro-preview'
|
'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(
|
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