fix(cliproxy): denylist deprecated agy 4.5 models

This commit is contained in:
Tam Nhu Tran committed 2026-03-03 03:10:53 +07:00
1 parent 5913f730ac
commit f63e2cd5a3
29 files changed
+850 -77

No files matched your search

@@ -14,10 +14,12 @@ import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
import { toast } from 'sonner';
import { useTranslation } from 'react-i18next';
import { useCreateVariant, useCliproxyAuth } from '@/hooks/use-cliproxy';
import { usePrivacy } from '@/contexts/privacy-context';
import { CLIPROXY_PROVIDERS, getProviderDisplayName } from '@/lib/provider-config';
import { isDeniedAgyModelId } from '@/lib/utils';
const singleProviderSchema = z.object({
name: z
@@ -68,6 +70,12 @@ const providerOptions = CLIPROXY_PROVIDERS.map((id) => ({
value: id,
label: getProviderDisplayName(id),
}));
const AGY_DENYLIST_MESSAGE =
'Antigravity denylist: Claude Opus 4.5 and Claude Sonnet 4.5 are deprecated.';
function isDeniedAgyModelForProvider(provider: string, modelId: string | undefined): boolean {
return provider === 'agy' && typeof modelId === 'string' && isDeniedAgyModelId(modelId);
}
export function CliproxyDialog({ open, onClose }: CliproxyDialogProps) {
const { t } = useTranslation();
@@ -99,6 +107,12 @@ export function CliproxyDialog({ open, onClose }: CliproxyDialogProps) {
const providerAccounts = providerAuth?.accounts || [];
const onSubmitSingle = async (data: SingleProviderFormData) => {
if (isDeniedAgyModelForProvider(data.provider, data.model)) {
singleForm.setError('model', { message: AGY_DENYLIST_MESSAGE });
toast.error(AGY_DENYLIST_MESSAGE);
return;
}
try {
await createMutation.mutateAsync(data);
singleForm.reset();
@@ -109,6 +123,14 @@ export function CliproxyDialog({ open, onClose }: CliproxyDialogProps) {
};
const onSubmitComposite = async (data: CompositeFormData) => {
for (const tier of ['opus', 'sonnet', 'haiku'] as const) {
const tierConfig = data.tiers[tier];
if (!isDeniedAgyModelForProvider(tierConfig.provider, tierConfig.model)) continue;
compositeForm.setError(`tiers.${tier}.model`, { message: AGY_DENYLIST_MESSAGE });
toast.error(AGY_DENYLIST_MESSAGE);
return;
}
try {
await createMutation.mutateAsync({
name: data.name,
@@ -12,10 +12,12 @@ import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
import { toast } from 'sonner';
import { useTranslation } from 'react-i18next';
import { useUpdateVariant } from '@/hooks/use-cliproxy';
import { CLIPROXY_PROVIDERS, getProviderDisplayName } from '@/lib/provider-config';
import type { UpdateVariant, Variant } from '@/lib/api-client';
import { isDeniedAgyModelId } from '@/lib/utils';
const singleProviderSchema = z.object({
provider: z.enum(CLIPROXY_PROVIDERS, { message: 'Provider is required' }),
@@ -61,12 +63,18 @@ const providerOptions = CLIPROXY_PROVIDERS.map((id) => ({
}));
const COMPOSITE_TIERS = ['opus', 'sonnet', 'haiku'] as const;
const AGY_DENYLIST_MESSAGE =
'Antigravity denylist: Claude Opus 4.5 and Claude Sonnet 4.5 are deprecated.';
function normalizeOptionalValue(value?: string): string | undefined {
const trimmed = value?.trim();
return trimmed ? trimmed : undefined;
}
function isDeniedAgyModelForProvider(provider: string, modelId: string | undefined): boolean {
return provider === 'agy' && typeof modelId === 'string' && isDeniedAgyModelId(modelId);
}
function isSingleVariantOnlyTargetChange(variant: Variant, data: SingleProviderFormData): boolean {
const currentTarget = variant.target || 'claude';
const currentModel = normalizeOptionalValue(variant.model);
@@ -190,6 +198,14 @@ export function CliproxyEditDialog({ variant, open, onOpenChange }: CliproxyEdit
}
}
const nextProvider = payload.provider || data.provider || variant.provider;
const nextModelForValidation = payload.model ?? normalizeOptionalValue(data.model);
if (isDeniedAgyModelForProvider(nextProvider, nextModelForValidation)) {
singleForm.setError('model', { message: AGY_DENYLIST_MESSAGE });
toast.error(AGY_DENYLIST_MESSAGE);
return;
}
if (Object.keys(payload).length === 0) {
onOpenChange(false);
return;
@@ -243,6 +259,15 @@ export function CliproxyEditDialog({ variant, open, onOpenChange }: CliproxyEdit
}
}
const tiersToValidate = payload.tiers ?? data.tiers;
for (const tier of COMPOSITE_TIERS) {
const tierConfig = tiersToValidate[tier];
if (!isDeniedAgyModelForProvider(tierConfig.provider, tierConfig.model)) continue;
compositeForm.setError(`tiers.${tier}.model`, { message: AGY_DENYLIST_MESSAGE });
toast.error(AGY_DENYLIST_MESSAGE);
return;
}
if (Object.keys(payload).length === 0) {
onOpenChange(false);
return;
@@ -17,6 +17,7 @@ import {
useDeletePreset,
} from '@/hooks/use-cliproxy';
import { CLIPROXY_DEFAULT_PORT } from '@/lib/preset-utils';
import { isDeniedAgyModelId } from '@/lib/utils';
import i18n from '@/lib/i18n';
import { usePrivacy } from '@/contexts/privacy-context';
import { useProviderEditor } from './use-provider-editor';
@@ -57,10 +58,23 @@ export function ProviderEditor({
const { data: presetsData } = usePresets(provider);
const createPresetMutation = useCreatePreset();
const deletePresetMutation = useDeletePreset();
const savedPresets = presetsData?.presets || [];
// Use baseProvider for model filtering (for variants, this is the parent provider)
const modelFilterProvider = baseProvider || provider;
const isAgyProvider = modelFilterProvider.toLowerCase() === 'agy';
const savedPresets = useMemo(() => {
const presets = presetsData?.presets || [];
if (!isAgyProvider) return presets;
return presets.filter(
(preset) =>
!isDeniedAgyModelId(preset.default) &&
!isDeniedAgyModelId(preset.opus) &&
!isDeniedAgyModelId(preset.sonnet) &&
!isDeniedAgyModelId(preset.haiku)
);
}, [isAgyProvider, presetsData?.presets]);
const providerModels = useMemo(() => {
if (!modelsData?.models) return [];
@@ -77,10 +91,12 @@ export function ProviderEditor({
const owners = ownerMap[modelFilterProvider.toLowerCase()] || [
modelFilterProvider.toLowerCase(),
];
return modelsData.models.filter((m) =>
owners.some((o) => m.owned_by.toLowerCase().includes(o))
);
}, [modelsData, modelFilterProvider]);
return modelsData.models.filter((m) => {
if (!owners.some((o) => m.owned_by.toLowerCase().includes(o))) return false;
if (!isAgyProvider) return true;
return !isDeniedAgyModelId(m.id);
});
}, [isAgyProvider, modelsData, modelFilterProvider]);
const providerRoute = (baseProvider || provider).toLowerCase();
@@ -131,6 +147,19 @@ export function ProviderEditor({
const effectiveApiKey = authTokens?.apiKey?.value ?? 'ccs-internal-managed';
const handleApplyPreset = (updates: Record<string, string>) => {
if (
isAgyProvider &&
[
updates.ANTHROPIC_MODEL,
updates.ANTHROPIC_DEFAULT_OPUS_MODEL,
updates.ANTHROPIC_DEFAULT_SONNET_MODEL,
updates.ANTHROPIC_DEFAULT_HAIKU_MODEL,
].some((modelId) => typeof modelId === 'string' && isDeniedAgyModelId(modelId))
) {
toast.error('Antigravity denylist: Claude Opus 4.5 and Claude Sonnet 4.5 are deprecated.');
return;
}
const effectivePort = port ?? CLIPROXY_DEFAULT_PORT;
updateEnvValues({
ANTHROPIC_BASE_URL: `http://127.0.0.1:${effectivePort}/api/provider/${providerRoute}`,
@@ -141,6 +170,16 @@ export function ProviderEditor({
};
const handleCustomPresetApply = (values: ModelMappingValues, presetName?: string) => {
if (
isAgyProvider &&
[values.default, values.opus, values.sonnet, values.haiku].some((modelId) =>
isDeniedAgyModelId(modelId)
)
) {
toast.error('Antigravity denylist: Claude Opus 4.5 and Claude Sonnet 4.5 are deprecated.');
return;
}
const effectivePort = port ?? CLIPROXY_DEFAULT_PORT;
updateEnvValues({
ANTHROPIC_BASE_URL: `http://127.0.0.1:${effectivePort}/api/provider/${providerRoute}`,
@@ -159,6 +198,15 @@ export function ProviderEditor({
toast.error(i18n.t('commonToast.enterPresetName'));
return;
}
if (
isAgyProvider &&
[values.default, values.opus, values.sonnet, values.haiku].some((modelId) =>
isDeniedAgyModelId(modelId)
)
) {
toast.error('Antigravity denylist: Claude Opus 4.5 and Claude Sonnet 4.5 are deprecated.');
return;
}
createPresetMutation.mutate({ profile: provider, data: { name: presetName, ...values } });
setCustomPresetOpen(false);
};
@@ -14,7 +14,7 @@ import {
SelectValue,
} from '@/components/ui/select';
import { ArrowLeft, User } from 'lucide-react';
import { cn } from '@/lib/utils';
import { cn, isDeniedAgyModelId } from '@/lib/utils';
import { PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
import { MODEL_CATALOGS } from '@/lib/model-catalogs';
import type { VariantStepProps } from '../types';
@@ -40,6 +40,10 @@ export function VariantStep({
const catalogModels = MODEL_CATALOGS[selectedProvider]?.models || [];
const isCustomModel = modelName && !catalogModels.some((m) => m.id === modelName);
const [showCustomInput, setShowCustomInput] = useState(isCustomModel);
const deniedCustomModel =
selectedProvider === 'agy' && modelName.trim().length > 0
? isDeniedAgyModelId(modelName)
: false;
const handleModelSelect = (value: string) => {
if (value === CUSTOM_MODEL_VALUE) {
@@ -87,10 +91,20 @@ export function VariantStep({
onChange={(e) => onModelChange(e.target.value)}
placeholder={t('setupVariant.modelPlaceholder')}
/>
{deniedCustomModel && (
<p className="text-xs text-destructive">
Antigravity denylist: Claude Opus 4.5 and Claude Sonnet 4.5 are deprecated.
</p>
)}
<button
type="button"
className="text-xs text-primary hover:underline"
onClick={() => setShowCustomInput(false)}
onClick={() => {
setShowCustomInput(false);
if (selectedProvider === 'agy' && isDeniedAgyModelId(modelName)) {
onModelChange('');
}
}}
>
{t('setupVariant.choosePresetInstead')}
</button>
@@ -140,7 +154,7 @@ export function VariantStep({
<Button variant="ghost" onClick={onSkip}>
{t('setupVariant.skip')}
</Button>
<Button onClick={onCreate} disabled={!variantName || isPending}>
<Button onClick={onCreate} disabled={!variantName || isPending || deniedCustomModel}>
{isPending ? t('setupVariant.creating') : t('setupVariant.createVariant')}
</Button>
</div>
+2 -2
View File
@@ -110,7 +110,7 @@ const resources = {
variantNamePlaceholder: 'e.g., my-gemini, g3, flash',
invokeHintPrefix: 'Use this name to invoke:',
model: 'Model',
modelPlaceholder: 'e.g., gemini-2.5-pro, claude-opus-4-5',
modelPlaceholder: 'e.g., gemini-2.5-pro, claude-opus-4-6-thinking',
choosePresetInstead: 'Choose from presets instead',
selectModel: 'Select a model',
customModelName: 'Custom model name...',
@@ -1241,7 +1241,7 @@ const resources = {
variantNamePlaceholder: '例如:my-gemini、g3、flash',
invokeHintPrefix: '可通过该名称调用:',
model: '模型',
modelPlaceholder: '例如:gemini-2.5-pro、claude-opus-4-5',
modelPlaceholder: '例如:gemini-2.5-pro、claude-opus-4-6-thinking',
choosePresetInstead: '改为从预设中选择',
selectModel: '选择模型',
customModelName: '自定义模型名称...',
+8 -3
View File
@@ -217,10 +217,15 @@ export function getClaudeResetTime<
}
// Known primary models to show when exhausted (removed from API response)
const AGY_DENYLIST_REGEX = /claude-(?:opus|sonnet)-4(?:[.-])5(?:-thinking)?(?=(?:$|[^a-z0-9]))/i;
export function isDeniedAgyModelId(modelId: string): boolean {
return AGY_DENYLIST_REGEX.test((modelId || '').trim());
}
const KNOWN_PRIMARY_MODELS = [
{ name: 'claude-opus-4-5-thinking', displayName: 'Claude Opus 4.5 (Thinking)' },
{ name: 'claude-sonnet-4-5', displayName: 'Claude Sonnet 4.5' },
{ name: 'claude-sonnet-4-5-thinking', displayName: 'Claude Sonnet 4.5 (Thinking)' },
{ name: 'claude-opus-4-6-thinking', displayName: 'Claude Opus 4.6 (Thinking)' },
{ name: 'claude-sonnet-4-6', displayName: 'Claude Sonnet 4.6' },
{ name: 'gpt-oss-120b', displayName: 'GPT-OSS 120B (Medium)' },
];