mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-15 02:16:27 +00:00
feat(cliproxy): add pinned model routing prefixes
This commit is contained in:
1 parent
850f1ef95c
commit
4d5e52828b
17 files changed
+791
-24
No files matched your search
@@ -29,6 +29,7 @@ export function CustomPresetDialog({
|
||||
isSaving,
|
||||
catalog,
|
||||
allModels,
|
||||
routing,
|
||||
}: CustomPresetDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const [values, setValues] = useState<ModelMappingValues>(currentValues);
|
||||
@@ -78,6 +79,7 @@ export function CustomPresetDialog({
|
||||
onChange={(model) => setValues({ ...values, default: model })}
|
||||
catalog={catalog}
|
||||
allModels={allModels}
|
||||
routing={routing}
|
||||
/>
|
||||
<FlexibleModelSelector
|
||||
label={t('customPresetDialog.opusModel')}
|
||||
@@ -86,6 +88,7 @@ export function CustomPresetDialog({
|
||||
onChange={(model) => setValues({ ...values, opus: model })}
|
||||
catalog={catalog}
|
||||
allModels={allModels}
|
||||
routing={routing}
|
||||
/>
|
||||
<FlexibleModelSelector
|
||||
label={t('customPresetDialog.sonnetModel')}
|
||||
@@ -94,6 +97,7 @@ export function CustomPresetDialog({
|
||||
onChange={(model) => setValues({ ...values, sonnet: model })}
|
||||
catalog={catalog}
|
||||
allModels={allModels}
|
||||
routing={routing}
|
||||
/>
|
||||
<FlexibleModelSelector
|
||||
label={t('customPresetDialog.haikuModel')}
|
||||
@@ -102,6 +106,7 @@ export function CustomPresetDialog({
|
||||
onChange={(model) => setValues({ ...values, haiku: model })}
|
||||
catalog={catalog}
|
||||
allModels={allModels}
|
||||
routing={routing}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter className="gap-2 sm:gap-0">
|
||||
|
||||
@@ -33,6 +33,7 @@ export function ProviderEditor({
|
||||
displayName,
|
||||
authStatus,
|
||||
catalog,
|
||||
routing,
|
||||
logoProvider,
|
||||
baseProvider,
|
||||
isRemoteMode,
|
||||
@@ -263,6 +264,7 @@ export function ProviderEditor({
|
||||
sonnetModel={sonnetModel}
|
||||
haikuModel={haikuModel}
|
||||
providerModels={providerModels}
|
||||
routing={routing}
|
||||
extendedContextEnabled={extendedContextEnabled}
|
||||
onExtendedContextToggle={toggleExtendedContext}
|
||||
onApplyPreset={handleApplyPreset}
|
||||
@@ -349,6 +351,7 @@ export function ProviderEditor({
|
||||
isSaving={createPresetMutation.isPending}
|
||||
catalog={catalog}
|
||||
allModels={providerModels}
|
||||
routing={routing}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -16,7 +16,10 @@ import type { ModelConfigSectionProps } from './types';
|
||||
|
||||
type CatalogPresetModel = NonNullable<ModelConfigSectionProps['catalog']>['models'][number];
|
||||
|
||||
function getPresetUpdates(model: CatalogPresetModel): Record<string, string> {
|
||||
function getPresetUpdates(
|
||||
model: CatalogPresetModel,
|
||||
toPreferredModelId: (modelId: string) => string
|
||||
): Record<string, string> {
|
||||
const mapping = model.presetMapping || {
|
||||
default: model.id,
|
||||
opus: model.id,
|
||||
@@ -25,10 +28,10 @@ function getPresetUpdates(model: CatalogPresetModel): Record<string, string> {
|
||||
};
|
||||
|
||||
return {
|
||||
ANTHROPIC_MODEL: mapping.default,
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: mapping.opus,
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: mapping.sonnet,
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: mapping.haiku,
|
||||
ANTHROPIC_MODEL: toPreferredModelId(mapping.default),
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: toPreferredModelId(mapping.opus),
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: toPreferredModelId(mapping.sonnet),
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: toPreferredModelId(mapping.haiku),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -40,6 +43,7 @@ export function ModelConfigSection({
|
||||
sonnetModel,
|
||||
haikuModel,
|
||||
providerModels,
|
||||
routing,
|
||||
provider,
|
||||
extendedContextEnabled,
|
||||
onExtendedContextToggle,
|
||||
@@ -49,6 +53,14 @@ export function ModelConfigSection({
|
||||
onDeletePreset,
|
||||
isDeletePending,
|
||||
}: ModelConfigSectionProps) {
|
||||
const routingHintMap = useMemo(
|
||||
() =>
|
||||
new Map((routing?.models ?? []).map((hint) => [hint.modelId.toLowerCase(), hint] as const)),
|
||||
[routing]
|
||||
);
|
||||
const toPreferredModelId = (modelId: string): string =>
|
||||
routingHintMap.get(modelId.toLowerCase())?.recommendedModelId ?? modelId;
|
||||
|
||||
const extendedContextModels = useMemo(() => {
|
||||
if (!catalog) return [];
|
||||
|
||||
@@ -126,7 +138,7 @@ export function ModelConfigSection({
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-xs h-7 gap-1"
|
||||
onClick={() => onApplyPreset(getPresetUpdates(model))}
|
||||
onClick={() => onApplyPreset(getPresetUpdates(model, toPreferredModelId))}
|
||||
>
|
||||
<Zap
|
||||
className={`w-3 h-3 ${'iconClassName' in group ? group.iconClassName : ''}`}
|
||||
@@ -195,6 +207,12 @@ export function ModelConfigSection({
|
||||
<p className="text-xs text-muted-foreground mb-4">
|
||||
Configure which models to use for each tier
|
||||
</p>
|
||||
{routing ? (
|
||||
<p className="text-[11px] text-muted-foreground mb-3 rounded-md border bg-muted/30 px-2.5 py-2">
|
||||
Preferred pinned model names use the <code>{routing.prefix}/</code> prefix. Unprefixed
|
||||
names can still resolve to a different backend when providers overlap.
|
||||
</p>
|
||||
) : null}
|
||||
{provider === 'codex' && (
|
||||
<p className="text-[11px] text-muted-foreground mb-3 rounded-md border bg-muted/30 px-2.5 py-2">
|
||||
Codex tip: suffixes <code>-medium</code>, <code>-high</code>, and <code>-xhigh</code>{' '}
|
||||
@@ -209,6 +227,7 @@ export function ModelConfigSection({
|
||||
onChange={(model) => onUpdateEnvValue('ANTHROPIC_MODEL', model)}
|
||||
catalog={catalog}
|
||||
allModels={providerModels}
|
||||
routing={routing}
|
||||
/>
|
||||
{/* Extended Context Toggle - shows when any saved mapping supports it */}
|
||||
{extendedContextModels.length > 0 && onExtendedContextToggle && (
|
||||
@@ -226,6 +245,7 @@ export function ModelConfigSection({
|
||||
onChange={(model) => onUpdateEnvValue('ANTHROPIC_DEFAULT_OPUS_MODEL', model)}
|
||||
catalog={catalog}
|
||||
allModels={providerModels}
|
||||
routing={routing}
|
||||
/>
|
||||
<FlexibleModelSelector
|
||||
label="Sonnet (Balanced)"
|
||||
@@ -234,6 +254,7 @@ export function ModelConfigSection({
|
||||
onChange={(model) => onUpdateEnvValue('ANTHROPIC_DEFAULT_SONNET_MODEL', model)}
|
||||
catalog={catalog}
|
||||
allModels={providerModels}
|
||||
routing={routing}
|
||||
/>
|
||||
<FlexibleModelSelector
|
||||
label="Haiku (Fast)"
|
||||
@@ -242,6 +263,7 @@ export function ModelConfigSection({
|
||||
onChange={(model) => onUpdateEnvValue('ANTHROPIC_DEFAULT_HAIKU_MODEL', model)}
|
||||
catalog={catalog}
|
||||
allModels={providerModels}
|
||||
routing={routing}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@ import { ModelConfigSection } from './model-config-section';
|
||||
import { AccountsSection } from './accounts-section';
|
||||
import { api } from '@/lib/api-client';
|
||||
import type { ProviderCatalog } from '../provider-model-selector';
|
||||
import type { OAuthAccount } from '@/lib/api-client';
|
||||
import type { OAuthAccount, CliproxyProviderRoutingHints } from '@/lib/api-client';
|
||||
import { QUOTA_SUPPORTED_PROVIDERS, type QuotaSupportedProvider } from '@/hooks/use-cliproxy-stats';
|
||||
|
||||
interface ModelConfigTabProps {
|
||||
@@ -28,6 +28,7 @@ interface ModelConfigTabProps {
|
||||
sonnetModel?: string;
|
||||
haikuModel?: string;
|
||||
providerModels: Array<{ id: string; owned_by: string }>;
|
||||
routing?: CliproxyProviderRoutingHints;
|
||||
/** Whether extended context (1M tokens) is enabled */
|
||||
extendedContextEnabled?: boolean;
|
||||
/** Callback when extended context toggle changes */
|
||||
@@ -71,6 +72,7 @@ export function ModelConfigTab({
|
||||
sonnetModel,
|
||||
haikuModel,
|
||||
providerModels,
|
||||
routing,
|
||||
extendedContextEnabled,
|
||||
onExtendedContextToggle,
|
||||
onApplyPreset,
|
||||
@@ -160,6 +162,7 @@ export function ModelConfigTab({
|
||||
sonnetModel={sonnetModel}
|
||||
haikuModel={haikuModel}
|
||||
providerModels={providerModels}
|
||||
routing={routing}
|
||||
provider={provider}
|
||||
extendedContextEnabled={extendedContextEnabled}
|
||||
onExtendedContextToggle={onExtendedContextToggle}
|
||||
|
||||
@@ -3,7 +3,12 @@
|
||||
*/
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
import type { AuthStatus, OAuthAccount, CliTarget } from '@/lib/api-client';
|
||||
import type {
|
||||
AuthStatus,
|
||||
OAuthAccount,
|
||||
CliTarget,
|
||||
CliproxyProviderRoutingHints,
|
||||
} from '@/lib/api-client';
|
||||
import type { ProviderCatalog } from '../provider-model-selector';
|
||||
|
||||
export interface SettingsResponse {
|
||||
@@ -20,6 +25,7 @@ export interface ProviderEditorProps {
|
||||
displayName: string;
|
||||
authStatus: AuthStatus;
|
||||
catalog?: ProviderCatalog;
|
||||
routing?: CliproxyProviderRoutingHints;
|
||||
/** Provider type for logo display (defaults to provider) */
|
||||
logoProvider?: string;
|
||||
/** Base provider for model filtering (defaults to provider). For variants, this is the parent provider. */
|
||||
@@ -92,6 +98,7 @@ export interface CustomPresetDialogProps {
|
||||
isSaving?: boolean;
|
||||
catalog?: ProviderCatalog;
|
||||
allModels: { id: string; owned_by: string }[];
|
||||
routing?: CliproxyProviderRoutingHints;
|
||||
}
|
||||
|
||||
export interface RawEditorSectionProps {
|
||||
@@ -118,6 +125,7 @@ export interface ModelConfigSectionProps {
|
||||
sonnetModel?: string;
|
||||
haikuModel?: string;
|
||||
providerModels: Array<{ id: string; owned_by: string }>;
|
||||
routing?: CliproxyProviderRoutingHints;
|
||||
/** Provider name for display */
|
||||
provider: string;
|
||||
/** Whether extended context (1M tokens) is enabled */
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
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 { cn } from '@/lib/utils';
|
||||
@@ -291,9 +292,20 @@ interface FlexibleModelSelectorProps {
|
||||
onChange: (model: string) => void;
|
||||
catalog?: ProviderCatalog;
|
||||
allModels: { id: string; owned_by: string }[];
|
||||
routing?: CliproxyProviderRoutingHints;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
function normalizeModelValue(
|
||||
value: string | undefined,
|
||||
routing?: CliproxyProviderRoutingHints
|
||||
): string {
|
||||
if (!value) return '';
|
||||
if (!routing?.prefix) return value;
|
||||
const prefix = `${routing.prefix}/`;
|
||||
return value.startsWith(prefix) ? value.slice(prefix.length) : value;
|
||||
}
|
||||
|
||||
export function FlexibleModelSelector({
|
||||
label,
|
||||
description,
|
||||
@@ -301,6 +313,7 @@ export function FlexibleModelSelector({
|
||||
onChange,
|
||||
catalog,
|
||||
allModels,
|
||||
routing,
|
||||
disabled,
|
||||
}: FlexibleModelSelectorProps) {
|
||||
const { t } = useTranslation();
|
||||
@@ -310,22 +323,50 @@ export function FlexibleModelSelector({
|
||||
[allModels, catalog]
|
||||
);
|
||||
const catalogModelIds = new Set(resolvedCatalogModels.map((model) => model.id));
|
||||
const routingHints = useMemo(
|
||||
() =>
|
||||
new Map((routing?.models ?? []).map((hint) => [hint.modelId.toLowerCase(), hint] as const)),
|
||||
[routing]
|
||||
);
|
||||
const selectedRoutingHint = useMemo(
|
||||
() => routingHints.get(normalizeModelValue(value, routing).toLowerCase()),
|
||||
[routing, routingHints, value]
|
||||
);
|
||||
|
||||
const recommendedOptions = resolvedCatalogModels.map((model) => ({
|
||||
value: model.id,
|
||||
value: routingHints.get(model.id.toLowerCase())?.recommendedModelId ?? model.id,
|
||||
groupKey: 'recommended',
|
||||
searchText: `${model.id} ${model.name}`,
|
||||
searchText: `${model.id} ${model.name} ${routingHints.get(model.id.toLowerCase())?.recommendedModelId ?? ''}`,
|
||||
keywords: [model.tier ?? '', catalog?.provider ?? ''],
|
||||
triggerContent: (
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<span className="truncate font-mono text-xs">{model.id}</span>
|
||||
<span className="truncate font-mono text-xs">
|
||||
{routingHints.get(model.id.toLowerCase())?.recommendedModelId ?? model.id}
|
||||
</span>
|
||||
{routingHints.get(model.id.toLowerCase()) ? (
|
||||
<Badge variant="secondary" className="text-[9px] h-4 px-1 uppercase">
|
||||
{routingHints.get(model.id.toLowerCase())?.prefix}
|
||||
</Badge>
|
||||
) : null}
|
||||
{isCodexProvider && <CodexEffortBadge modelId={model.id} />}
|
||||
</div>
|
||||
),
|
||||
itemContent: (
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<span className="truncate font-mono text-xs">{model.id}</span>
|
||||
<span className="truncate font-mono text-xs">
|
||||
{routingHints.get(model.id.toLowerCase())?.recommendedModelId ?? model.id}
|
||||
</span>
|
||||
{model.tier === 'paid' && <PaidBadge label={t('providerModelSelector.paid')} />}
|
||||
{routingHints.get(model.id.toLowerCase())?.unprefixedStatus === 'shadowed' ? (
|
||||
<Badge variant="outline" className="text-[9px] h-4 px-1">
|
||||
Shadowed
|
||||
</Badge>
|
||||
) : null}
|
||||
{routingHints.get(model.id.toLowerCase())?.unprefixedStatus === 'prefix-only' ? (
|
||||
<Badge variant="outline" className="text-[9px] h-4 px-1">
|
||||
Prefix only
|
||||
</Badge>
|
||||
) : null}
|
||||
{isCodexProvider && <CodexEffortBadge modelId={model.id} />}
|
||||
</div>
|
||||
),
|
||||
@@ -351,6 +392,33 @@ export function FlexibleModelSelector({
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
const selectedValueMissing =
|
||||
Boolean(value) &&
|
||||
!recommendedOptions.some((option) => option.value === value) &&
|
||||
!allModelOptions.some((option) => option.value === value);
|
||||
const legacySelectedOption = value
|
||||
? {
|
||||
value,
|
||||
groupKey: 'current',
|
||||
searchText: value,
|
||||
triggerContent: (
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<span className="truncate font-mono text-xs">{value}</span>
|
||||
<Badge variant="outline" className="text-[9px] h-4 px-1">
|
||||
Current
|
||||
</Badge>
|
||||
</div>
|
||||
),
|
||||
itemContent: (
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<span className="truncate font-mono text-xs">{value}</span>
|
||||
<Badge variant="outline" className="text-[9px] h-4 px-1">
|
||||
Current
|
||||
</Badge>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
: null;
|
||||
const hasAvailableModels = recommendedOptions.length + allModelOptions.length > 0;
|
||||
|
||||
return (
|
||||
@@ -372,6 +440,14 @@ export function FlexibleModelSelector({
|
||||
}
|
||||
triggerClassName="h-9"
|
||||
groups={[
|
||||
...(selectedValueMissing && legacySelectedOption
|
||||
? [
|
||||
{
|
||||
key: 'current',
|
||||
label: <span className="text-xs text-muted-foreground">Current value</span>,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
key: 'recommended',
|
||||
label: (
|
||||
@@ -387,8 +463,32 @@ export function FlexibleModelSelector({
|
||||
),
|
||||
},
|
||||
]}
|
||||
options={[...recommendedOptions, ...allModelOptions]}
|
||||
options={[
|
||||
...(selectedValueMissing && legacySelectedOption ? [legacySelectedOption] : []),
|
||||
...recommendedOptions,
|
||||
...allModelOptions,
|
||||
]}
|
||||
/>
|
||||
{selectedRoutingHint ? (
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-md border px-2.5 py-2 text-[11px]',
|
||||
selectedRoutingHint.unprefixedStatus === 'safe'
|
||||
? 'border-border/70 bg-muted/25 text-muted-foreground'
|
||||
: 'border-amber-300/60 bg-amber-50 text-amber-900 dark:border-amber-500/30 dark:bg-amber-950/25 dark:text-amber-100'
|
||||
)}
|
||||
>
|
||||
<div className="font-medium">
|
||||
Preferred pinned model: <code>{selectedRoutingHint.recommendedModelId}</code>
|
||||
</div>
|
||||
<p className="mt-1 leading-5">{selectedRoutingHint.summary}</p>
|
||||
</div>
|
||||
) : null}
|
||||
{value && !selectedRoutingHint && normalizeModelValue(value, routing) !== value ? (
|
||||
<div className="rounded-md border border-border/70 bg-muted/25 px-2.5 py-2 text-[11px] text-muted-foreground">
|
||||
Using pinned model: <code>{value}</code>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -468,6 +468,29 @@ export interface CliproxyCatalogModel {
|
||||
presetMapping?: CliproxyCatalogPresetMapping;
|
||||
}
|
||||
|
||||
export interface CliproxyModelRoutingHint {
|
||||
modelId: string;
|
||||
modelName: string;
|
||||
prefix: string;
|
||||
pinnedModelId: string;
|
||||
recommendedModelId: string;
|
||||
unprefixedStatus: 'safe' | 'shadowed' | 'prefix-only';
|
||||
effectiveProvider: string | null;
|
||||
effectiveDisplayName: string | null;
|
||||
effectiveOwnedBy: string | null;
|
||||
summary: string;
|
||||
}
|
||||
|
||||
export interface CliproxyProviderRoutingHints {
|
||||
provider: string;
|
||||
displayName: string;
|
||||
prefix: string;
|
||||
safeCount: number;
|
||||
shadowedCount: number;
|
||||
prefixOnlyCount: number;
|
||||
models: CliproxyModelRoutingHint[];
|
||||
}
|
||||
|
||||
export interface CliproxyProviderCatalog {
|
||||
provider: string;
|
||||
displayName: string;
|
||||
@@ -477,6 +500,7 @@ export interface CliproxyProviderCatalog {
|
||||
|
||||
export interface CliproxyCatalogResponse {
|
||||
catalogs: Partial<Record<string, CliproxyProviderCatalog>>;
|
||||
routing?: Partial<Record<string, CliproxyProviderRoutingHints>>;
|
||||
source: 'live' | 'cache' | 'static';
|
||||
cache: {
|
||||
synced: boolean;
|
||||
|
||||
@@ -264,6 +264,7 @@ export function CliproxyPage() {
|
||||
const isRemoteMode = authData?.source === 'remote';
|
||||
const variants = useMemo(() => variantsData?.variants || [], [variantsData?.variants]);
|
||||
const catalogs = useMemo(() => buildUiCatalogs(catalogData?.catalogs), [catalogData?.catalogs]);
|
||||
const routingHints = catalogData?.routing ?? {};
|
||||
const fetchedCatalogsReady = Boolean(catalogData);
|
||||
|
||||
// Wrapper to persist provider selection to localStorage
|
||||
@@ -459,6 +460,7 @@ export function CliproxyPage() {
|
||||
})}
|
||||
authStatus={parentAuthForVariant}
|
||||
catalog={catalogs[selectedVariantData.provider]}
|
||||
routing={routingHints[selectedVariantData.provider]}
|
||||
logoProvider={selectedVariantData.provider}
|
||||
baseProvider={selectedVariantData.provider}
|
||||
defaultTarget={selectedVariantData.target}
|
||||
@@ -512,6 +514,7 @@ export function CliproxyPage() {
|
||||
displayName={selectedStatus.displayName}
|
||||
authStatus={selectedStatus}
|
||||
catalog={catalogs[selectedStatus.provider]}
|
||||
routing={routingHints[selectedStatus.provider]}
|
||||
isRemoteMode={isRemoteMode}
|
||||
topNotice={
|
||||
showAccountSafetyWarning ? (
|
||||
|
||||
@@ -110,4 +110,69 @@ describe('ModelConfigSection presets', () => {
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'gemini-3-9-flash-preview',
|
||||
});
|
||||
});
|
||||
|
||||
it('applies managed short prefixes when routing guidance is available', async () => {
|
||||
const onApplyPreset = vi.fn();
|
||||
|
||||
render(
|
||||
<ModelConfigSection
|
||||
catalog={MODEL_CATALOGS.gemini}
|
||||
savedPresets={[]}
|
||||
currentModel="gemini-3-flash-preview"
|
||||
opusModel="gemini-3.1-pro-preview"
|
||||
sonnetModel="gemini-3.1-pro-preview"
|
||||
haikuModel="gemini-3-flash-preview"
|
||||
providerModels={[]}
|
||||
routing={{
|
||||
provider: 'gemini',
|
||||
displayName: 'Gemini',
|
||||
prefix: 'gcli',
|
||||
safeCount: 0,
|
||||
shadowedCount: 2,
|
||||
prefixOnlyCount: 0,
|
||||
models: [
|
||||
{
|
||||
modelId: 'gemini-3.1-pro-preview',
|
||||
modelName: 'Gemini Pro',
|
||||
prefix: 'gcli',
|
||||
pinnedModelId: 'gcli/gemini-3.1-pro-preview',
|
||||
recommendedModelId: 'gcli/gemini-3.1-pro-preview',
|
||||
unprefixedStatus: 'shadowed',
|
||||
effectiveProvider: 'agy',
|
||||
effectiveDisplayName: 'Antigravity',
|
||||
effectiveOwnedBy: 'antigravity',
|
||||
summary: 'shadowed by Antigravity',
|
||||
},
|
||||
{
|
||||
modelId: 'gemini-3-flash-preview',
|
||||
modelName: 'Gemini Flash',
|
||||
prefix: 'gcli',
|
||||
pinnedModelId: 'gcli/gemini-3-flash-preview',
|
||||
recommendedModelId: 'gcli/gemini-3-flash-preview',
|
||||
unprefixedStatus: 'shadowed',
|
||||
effectiveProvider: 'agy',
|
||||
effectiveDisplayName: 'Antigravity',
|
||||
effectiveOwnedBy: 'antigravity',
|
||||
summary: 'shadowed by Antigravity',
|
||||
},
|
||||
],
|
||||
}}
|
||||
provider="gemini"
|
||||
onExtendedContextToggle={vi.fn()}
|
||||
onApplyPreset={onApplyPreset}
|
||||
onUpdateEnvValue={vi.fn()}
|
||||
onOpenCustomPreset={vi.fn()}
|
||||
onDeletePreset={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Gemini Flash' }));
|
||||
|
||||
expect(onApplyPreset).toHaveBeenCalledWith({
|
||||
ANTHROPIC_MODEL: 'gcli/gemini-3-flash-preview',
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: 'gcli/gemini-3.1-pro-preview',
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: 'gcli/gemini-3.1-pro-preview',
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'gcli/gemini-3-flash-preview',
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user