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 (
@@ -568,87 +598,121 @@ export function FlexibleModelSelector({ {description &&

{description}

}
- - {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', + ]); + }); +});