From ef36ad4600282aae7680316a084ec1eb2d74ab63 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Wed, 18 Mar 2026 12:15:30 -0400 Subject: [PATCH] fix(ui): reflect cliproxy preset plan tiers --- .../provider-editor/model-config-section.tsx | 192 +++++++++++------- .../model-config-section.test.tsx | 73 +++++++ .../unit/ui/lib/model-catalogs-codex.test.ts | 6 +- 3 files changed, 198 insertions(+), 73 deletions(-) create mode 100644 ui/tests/unit/components/cliproxy/provider-editor/model-config-section.test.tsx diff --git a/ui/src/components/cliproxy/provider-editor/model-config-section.tsx b/ui/src/components/cliproxy/provider-editor/model-config-section.tsx index b9b92c0a..d22a9632 100644 --- a/ui/src/components/cliproxy/provider-editor/model-config-section.tsx +++ b/ui/src/components/cliproxy/provider-editor/model-config-section.tsx @@ -5,6 +5,7 @@ import { useMemo } from 'react'; import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; import { Separator } from '@/components/ui/separator'; import { Sparkles, Zap, Star, X, Plus } from 'lucide-react'; import { FlexibleModelSelector } from '../provider-model-selector'; @@ -12,6 +13,24 @@ import { ExtendedContextToggle } from '../extended-context-toggle'; import { stripExtendedContextSuffix } from '@/lib/extended-context-utils'; import type { ModelConfigSectionProps } from './types'; +type CatalogPresetModel = NonNullable['models'][number]; + +function getPresetUpdates(model: CatalogPresetModel): Record { + const mapping = model.presetMapping || { + default: model.id, + opus: model.id, + sonnet: model.id, + haiku: model.id, + }; + + return { + ANTHROPIC_MODEL: mapping.default, + ANTHROPIC_DEFAULT_OPUS_MODEL: mapping.opus, + ANTHROPIC_DEFAULT_SONNET_MODEL: mapping.sonnet, + ANTHROPIC_DEFAULT_HAIKU_MODEL: mapping.haiku, + }; +} + export function ModelConfigSection({ catalog, savedPresets, @@ -29,8 +48,6 @@ export function ModelConfigSection({ onDeletePreset, isDeletePending, }: ModelConfigSectionProps) { - const showPresets = (catalog && catalog.models.length > 0) || savedPresets.length > 0; - // Find current model entry to check for extended context support // Strip [1m] suffix when looking up in catalog since catalog IDs don't have suffix const currentModelEntry = useMemo(() => { @@ -39,6 +56,37 @@ export function ModelConfigSection({ return catalog.models.find((m) => m.id === baseModelId); }, [catalog, currentModel]); + const presetGroups = useMemo(() => { + const presetModels = (catalog?.models ?? []).filter((model) => model.presetMapping); + if (presetModels.length === 0) return []; + + const hasPaidPresets = presetModels.some((model) => model.tier === 'paid'); + if (!hasPaidPresets) { + return [{ key: 'default', models: presetModels.slice(0, 4) }]; + } + + return [ + { + key: 'free', + label: 'Free Tier', + description: 'Available on free or paid plans', + badgeClassName: 'text-[10px] bg-green-100 text-green-700 border-green-200', + iconClassName: 'text-green-600', + models: presetModels.filter((model) => model.tier !== 'paid'), + }, + { + key: 'paid', + label: 'Paid Tier', + description: 'Requires paid access', + badgeClassName: 'text-[10px] bg-amber-100 text-amber-700 border-amber-200', + iconClassName: 'text-amber-700', + models: presetModels.filter((model) => model.tier === 'paid'), + }, + ].filter((group) => group.models.length > 0); + }, [catalog]); + + const showPresets = presetGroups.length > 0 || savedPresets.length > 0; + return ( <> {/* Quick Presets */} @@ -49,77 +97,81 @@ export function ModelConfigSection({ Presets

Apply pre-configured model mappings

-
- {/* Recommended presets from catalog */} - {catalog?.models.slice(0, 4).map((model) => ( - - ))} - - {/* User saved presets */} - {savedPresets.map((preset) => ( -
- - +
+ {presetGroups.map((group) => ( +
+ {'label' in group && group.label && ( +
+ + {group.label} + + {group.description} +
+ )} +
+ {group.models.map((model) => ( + + ))} +
))} - +
+ {/* User saved presets */} + {savedPresets.map((preset) => ( +
+ + +
+ ))} + + +
)} diff --git a/ui/tests/unit/components/cliproxy/provider-editor/model-config-section.test.tsx b/ui/tests/unit/components/cliproxy/provider-editor/model-config-section.test.tsx new file mode 100644 index 00000000..9fb5d916 --- /dev/null +++ b/ui/tests/unit/components/cliproxy/provider-editor/model-config-section.test.tsx @@ -0,0 +1,73 @@ +import { describe, expect, it, vi } from 'vitest'; +import { render, screen, userEvent } from '@tests/setup/test-utils'; + +vi.mock('@/components/cliproxy/provider-model-selector', () => ({ + FlexibleModelSelector: () =>
, +})); + +vi.mock('@/components/cliproxy/extended-context-toggle', () => ({ + ExtendedContextToggle: () =>
, +})); + +import { ModelConfigSection } from '@/components/cliproxy/provider-editor/model-config-section'; +import { MODEL_CATALOGS } from '@/lib/model-catalogs'; + +describe('ModelConfigSection presets', () => { + it('groups codex presets by free and paid tiers', async () => { + const onApplyPreset = vi.fn(); + + render( + + ); + + expect(screen.getByText('Free Tier')).toBeInTheDocument(); + expect(screen.getByText('Paid Tier')).toBeInTheDocument(); + expect(screen.getByText('Available on free or paid plans')).toBeInTheDocument(); + expect(screen.getByText('Requires paid access')).toBeInTheDocument(); + + await userEvent.click(screen.getByRole('button', { name: 'GPT-5.4' })); + + expect(onApplyPreset).toHaveBeenCalledWith({ + ANTHROPIC_MODEL: 'gpt-5.4', + ANTHROPIC_DEFAULT_OPUS_MODEL: 'gpt-5.4', + ANTHROPIC_DEFAULT_SONNET_MODEL: 'gpt-5.4', + ANTHROPIC_DEFAULT_HAIKU_MODEL: 'gpt-5-codex-mini', + }); + }); + + it('keeps non-tiered provider presets ungrouped', () => { + render( + + ); + + expect(screen.queryByText('Free Tier')).not.toBeInTheDocument(); + expect(screen.queryByText('Paid Tier')).not.toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Claude Opus 4.6 Thinking' })).toBeInTheDocument(); + }); +}); diff --git a/ui/tests/unit/ui/lib/model-catalogs-codex.test.ts b/ui/tests/unit/ui/lib/model-catalogs-codex.test.ts index ed57cc8a..8e302458 100644 --- a/ui/tests/unit/ui/lib/model-catalogs-codex.test.ts +++ b/ui/tests/unit/ui/lib/model-catalogs-codex.test.ts @@ -2,12 +2,12 @@ import { describe, expect, it } from 'vitest'; import { MODEL_CATALOGS } from '@/lib/model-catalogs'; describe('codex model catalog defaults', () => { - it('uses gpt-5.1-codex-mini as the haiku mapping for codex presets', () => { + it('uses gpt-5-codex-mini as the haiku mapping for cross-plan codex presets', () => { const codexCatalog = MODEL_CATALOGS.codex; const codex53 = codexCatalog.models.find((model) => model.id === 'gpt-5.3-codex'); const codex52 = codexCatalog.models.find((model) => model.id === 'gpt-5.2-codex'); - expect(codex53?.presetMapping?.haiku).toBe('gpt-5.1-codex-mini'); - expect(codex52?.presetMapping?.haiku).toBe('gpt-5.1-codex-mini'); + expect(codex53?.presetMapping?.haiku).toBe('gpt-5-codex-mini'); + expect(codex52?.presetMapping?.haiku).toBe('gpt-5-codex-mini'); }); });