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([