diff --git a/ui/src/components/cliproxy/provider-model-selector.tsx b/ui/src/components/cliproxy/provider-model-selector.tsx
index a9c47779..37ba59c8 100644
--- a/ui/src/components/cliproxy/provider-model-selector.tsx
+++ b/ui/src/components/cliproxy/provider-model-selector.tsx
@@ -10,16 +10,30 @@ import { useTranslation } from 'react-i18next';
import { Badge } from '@/components/ui/badge';
import { SearchableSelect } from '@/components/ui/searchable-select';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
import { Skeleton } from '@/components/ui/skeleton';
import type { CliproxyProviderRoutingHints } from '@/lib/api-client';
import type { CodexEffort, CodexServiceTier } from '@/lib/codex-effort';
import {
+ applyCodexEffortSuffix,
getCodexEffortDisplay,
getCodexEffortVariants,
+ getSelectableCodexEfforts,
parseCodexEffort,
parseCodexServiceTier,
+ stripCodexEffortSuffix,
} from '@/lib/codex-effort';
-import { getResolvedCatalogModels, getSupplementalCatalogModels } from '@/lib/model-catalogs';
+import {
+ findCatalogModel,
+ getResolvedCatalogModels,
+ getSupplementalCatalogModels,
+} from '@/lib/model-catalogs';
import { cn } from '@/lib/utils';
/** Model entry from catalog */
@@ -561,6 +575,22 @@ export function FlexibleModelSelector({
}
: null;
const hasAvailableModels = recommendedOptions.length + allModelOptions.length > 0;
+ const currentEffort = isCodexProvider ? parseCodexEffort(value) : undefined;
+ const baseModelId = isCodexProvider
+ ? stripCodexEffortSuffix(normalizeModelValue(value, routing))
+ : '';
+ const selectedCatalogModel =
+ isCodexProvider && baseModelId && catalog
+ ? findCatalogModel(catalog.provider, baseModelId, catalog)
+ : undefined;
+ const selectableEfforts = getSelectableCodexEfforts(
+ selectedCatalogModel?.codexMaxEffort,
+ selectedCatalogModel?.codexEfforts
+ );
+ const effortOptions =
+ currentEffort && !selectableEfforts.includes(currentEffort)
+ ? [...selectableEfforts, currentEffort]
+ : selectableEfforts;
return (
- {t('providerModelSelector.currentValue')}
-
- ),
- },
- ]
- : []),
- {
- key: 'recommended',
- label: (
- {t('providerModelSelector.recommended')}
- ),
- },
- ...(isCodexProvider
- ? [
- {
- key: 'codex-reasoning',
- label: (
-
- {t('providerModelSelector.codexReasoningVariants')}
-
- ),
- },
- {
- key: 'codex-fast',
- label: (
-
- {t('providerModelSelector.codexFastVariants')}
-
- ),
- },
- ]
- : []),
- ...(allModelOptions.length > 0
- ? [
- {
- key: 'all',
- label: (
-
- {t('providerModelSelector.allModelsCount', {
- count: allModelOptions.length,
- })}
-
- ),
- },
- ]
- : []),
- {
- key: 'custom',
- label: (
-
- {t('providerModelSelector.customModel')}
-
- ),
- },
- ]}
- options={[
- ...(selectedValueMissing && legacySelectedOption ? [legacySelectedOption] : []),
- ...recommendedOptions,
- ...allModelOptions,
- ]}
- />
+
+
+ {t('providerModelSelector.currentValue')}
+
+ ),
+ },
+ ]
+ : []),
+ {
+ key: 'recommended',
+ label: (
+
+ {t('providerModelSelector.recommended')}
+
+ ),
+ },
+ ...(isCodexProvider
+ ? [
+ {
+ key: 'codex-reasoning',
+ label: (
+
+ {t('providerModelSelector.codexReasoningVariants')}
+
+ ),
+ },
+ {
+ key: 'codex-fast',
+ label: (
+
+ {t('providerModelSelector.codexFastVariants')}
+
+ ),
+ },
+ ]
+ : []),
+ ...(allModelOptions.length > 0
+ ? [
+ {
+ key: 'all',
+ label: (
+
+ {t('providerModelSelector.allModelsCount', {
+ count: allModelOptions.length,
+ })}
+
+ ),
+ },
+ ]
+ : []),
+ {
+ key: 'custom',
+ label: (
+
+ {t('providerModelSelector.customModel')}
+
+ ),
+ },
+ ]}
+ options={[
+ ...(selectedValueMissing && legacySelectedOption ? [legacySelectedOption] : []),
+ ...recommendedOptions,
+ ...allModelOptions,
+ ]}
+ />
+ {isCodexProvider && (
+
+ )}
+
{selectedRoutingHint ? (
string; auto: string }
diff --git a/ui/src/lib/i18n.ts b/ui/src/lib/i18n.ts
index 39988ff4..e9d047c9 100644
--- a/ui/src/lib/i18n.ts
+++ b/ui/src/lib/i18n.ts
@@ -228,6 +228,8 @@ const resources = {
'Pinned model is not currently advertised by the proxy: {{model}}',
codexReasoningVariants: 'Reasoning variants',
codexFastVariants: 'Fast variants',
+ effortLabel: 'Effort',
+ effortAuto: 'Auto',
},
createAuthProfileDialog: {
title: 'Create New Account',
@@ -2954,6 +2956,8 @@ const resources = {
pinnedModelNotAdvertised: '固定模型当前未被代理广播:{{model}}',
codexReasoningVariants: '推理变体',
codexFastVariants: '快速变体',
+ effortLabel: '推理强度',
+ effortAuto: '自动',
},
createAuthProfileDialog: {
title: '创建新账号',
@@ -5563,6 +5567,8 @@ const resources = {
pinnedModelNotAdvertised: 'Mô hình ghim hiện không được proxy quảng bá: {{model}}',
codexReasoningVariants: 'Biến thể lý luận',
codexFastVariants: 'Biến thể nhanh',
+ effortLabel: 'Mức suy luận',
+ effortAuto: 'Tự động',
},
createAuthProfileDialog: {
title: 'Tạo tài khoản mới',
@@ -8288,6 +8294,8 @@ const resources = {
pinnedModelNotAdvertised: 'ピンモデルは現在プロキシから通知されていません: {{model}}',
codexReasoningVariants: '推論バリアント',
codexFastVariants: '高速バリアント',
+ effortLabel: '推論レベル',
+ effortAuto: '自動',
},
createAuthProfileDialog: {
title: '新しいアカウントを作成',
@@ -11036,6 +11044,8 @@ const resources = {
pinnedModelNotAdvertised: '고정된 모델이 현재 프록시에서 알리지 않고 있습니다: {{model}}',
codexReasoningVariants: '추론 변형',
codexFastVariants: '빠른 변형',
+ effortLabel: '추론 강도',
+ effortAuto: '자동',
},
createAuthProfileDialog: {
title: '새 계정 생성',
diff --git a/ui/tests/unit/ui/lib/codex-effort.test.ts b/ui/tests/unit/ui/lib/codex-effort.test.ts
index 557da198..7031d89f 100644
--- a/ui/tests/unit/ui/lib/codex-effort.test.ts
+++ b/ui/tests/unit/ui/lib/codex-effort.test.ts
@@ -3,6 +3,7 @@ import {
applyCodexEffortSuffix,
getCodexEffortDisplay,
getCodexEffortVariants,
+ getSelectableCodexEfforts,
parseCodexEffort,
parseCodexServiceTier,
stripCodexEffortSuffix,
@@ -85,6 +86,15 @@ describe('codex effort helpers', () => {
]);
});
+ it('replaces effort while preserving the fast service tier', () => {
+ expect(applyCodexEffortSuffix('gpt-5.4-high-fast', 'low')).toBe('gpt-5.4-low-fast');
+ expect(applyCodexEffortSuffix('gpt-5.4-high-fast', undefined)).toBe('gpt-5.4-fast');
+ });
+
+ it('applies effort suffixes to routing-prefixed model ids', () => {
+ expect(applyCodexEffortSuffix('codex/gpt-5.6-sol', 'high')).toBe('codex/gpt-5.6-sol-high');
+ });
+
it('builds fast variants for models with a fast service tier', () => {
expect(getCodexEffortVariants('gpt-5.4', 'high', ['fast'])).toEqual([
'gpt-5.4',
@@ -100,3 +110,35 @@ describe('codex effort helpers', () => {
]);
});
});
+
+describe('getSelectableCodexEfforts', () => {
+ it('returns the explicit efforts list when provided', () => {
+ expect(getSelectableCodexEfforts('xhigh', ['low', 'medium', 'high', 'xhigh'])).toEqual([
+ 'low',
+ 'medium',
+ 'high',
+ 'xhigh',
+ ]);
+ });
+
+ it('returns efforts up to and including the max effort', () => {
+ expect(getSelectableCodexEfforts('xhigh')).toEqual([
+ 'minimal',
+ 'low',
+ 'medium',
+ 'high',
+ 'xhigh',
+ ]);
+ expect(getSelectableCodexEfforts('medium')).toEqual(['minimal', 'low', 'medium']);
+ });
+
+ it('returns all efforts for custom models without catalog metadata', () => {
+ expect(getSelectableCodexEfforts(undefined)).toEqual([
+ 'minimal',
+ 'low',
+ 'medium',
+ 'high',
+ 'xhigh',
+ ]);
+ });
+});