diff --git a/ui/src/components/cliproxy/provider-model-selector.tsx b/ui/src/components/cliproxy/provider-model-selector.tsx index 405c9f19..e6607c19 100644 --- a/ui/src/components/cliproxy/provider-model-selector.tsx +++ b/ui/src/components/cliproxy/provider-model-selector.tsx @@ -18,15 +18,22 @@ import { SelectValue, } from '@/components/ui/select'; import { Skeleton } from '@/components/ui/skeleton'; +import { Switch } from '@/components/ui/switch'; import type { CliproxyProviderRoutingHints } from '@/lib/api-client'; import type { CodexEffort, CodexServiceTier } from '@/lib/codex-effort'; import { + applyCodexServiceTierSuffix, getCodexEffortDisplay, getCodexEffortVariants, parseCodexEffort, parseCodexServiceTier, + stripCodexEffortSuffix, } from '@/lib/codex-effort'; -import { getResolvedCatalogModels, getSupplementalCatalogModels } from '@/lib/model-catalogs'; +import { + findCatalogModel, + getResolvedCatalogModels, + getSupplementalCatalogModels, +} from '@/lib/model-catalogs'; import { getReasoningAdapter } from '@/lib/reasoning-control'; import { cn } from '@/lib/utils'; @@ -586,6 +593,19 @@ export function FlexibleModelSelector({ !selectableReasoningLevels.includes(currentReasoningLevel) ? [...selectableReasoningLevels, currentReasoningLevel] : selectableReasoningLevels; + const currentServiceTier = isCodexProvider ? parseCodexServiceTier(value) : undefined; + const selectedCodexBaseModel = + isCodexProvider && catalog && value + ? findCatalogModel( + catalog.provider, + stripCodexEffortSuffix(normalizeModelValue(value, routing)), + catalog + ) + : undefined; + const showFastToggle = + isCodexProvider && + (selectedCodexBaseModel?.codexServiceTiers?.includes('fast') === true || + currentServiceTier === 'fast'); return (
@@ -702,6 +722,19 @@ export function FlexibleModelSelector({ )} + {showFastToggle && ( + + )}
{selectedRoutingHint ? (
{ }); }); +describe('applyCodexServiceTierSuffix', () => { + it('adds the fast tier while preserving the effort suffix', () => { + expect(applyCodexServiceTierSuffix('gpt-5.4-high', true)).toBe('gpt-5.4-high-fast'); + }); + + it('removes only the fast tier', () => { + expect(applyCodexServiceTierSuffix('gpt-5.4-high-fast', false)).toBe('gpt-5.4-high'); + expect(applyCodexServiceTierSuffix('gpt-5.4-fast', false)).toBe('gpt-5.4'); + }); + + it('preserves routing prefixes', () => { + expect(applyCodexServiceTierSuffix('codex/gpt-5.6-sol', true)).toBe('codex/gpt-5.6-sol-fast'); + }); + + it('is idempotent and safe on empty input', () => { + expect(applyCodexServiceTierSuffix('gpt-5.4-fast', true)).toBe('gpt-5.4-fast'); + expect(applyCodexServiceTierSuffix('gpt-5.4', false)).toBe('gpt-5.4'); + expect(applyCodexServiceTierSuffix(undefined, true)).toBe(''); + expect(applyCodexServiceTierSuffix('', true)).toBe(''); + }); + + it('composes with applyCodexEffortSuffix in both orders', () => { + expect(applyCodexEffortSuffix(applyCodexServiceTierSuffix('gpt-5.4', true), 'high')).toBe( + 'gpt-5.4-high-fast' + ); + expect(applyCodexServiceTierSuffix(applyCodexEffortSuffix('gpt-5.4', 'high'), true)).toBe( + 'gpt-5.4-high-fast' + ); + expect( + applyCodexEffortSuffix(applyCodexServiceTierSuffix('gpt-5.4-high-fast', false), 'low') + ).toBe('gpt-5.4-low'); + }); +}); + describe('getSelectableCodexEfforts', () => { it('returns the explicit efforts list when provided', () => { expect(getSelectableCodexEfforts('xhigh', ['low', 'medium', 'high', 'xhigh'])).toEqual([