feat(ui): add per-tier codex effort dropdown in model mapping

This commit is contained in:
milesnguyen2405 committed 2026-07-22 11:29:04 +07:00
1 parent 51e8716630
commit e57bded451
4 files changed
+209 -83

No files matched your search

@@ -10,16 +10,30 @@ import { useTranslation } from 'react-i18next';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { SearchableSelect } from '@/components/ui/searchable-select'; import { SearchableSelect } from '@/components/ui/searchable-select';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Skeleton } from '@/components/ui/skeleton'; import { Skeleton } from '@/components/ui/skeleton';
import type { CliproxyProviderRoutingHints } from '@/lib/api-client'; import type { CliproxyProviderRoutingHints } from '@/lib/api-client';
import type { CodexEffort, CodexServiceTier } from '@/lib/codex-effort'; import type { CodexEffort, CodexServiceTier } from '@/lib/codex-effort';
import { import {
applyCodexEffortSuffix,
getCodexEffortDisplay, getCodexEffortDisplay,
getCodexEffortVariants, getCodexEffortVariants,
getSelectableCodexEfforts,
parseCodexEffort, parseCodexEffort,
parseCodexServiceTier, parseCodexServiceTier,
stripCodexEffortSuffix,
} from '@/lib/codex-effort'; } from '@/lib/codex-effort';
import { getResolvedCatalogModels, getSupplementalCatalogModels } from '@/lib/model-catalogs'; import {
findCatalogModel,
getResolvedCatalogModels,
getSupplementalCatalogModels,
} from '@/lib/model-catalogs';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
/** Model entry from catalog */ /** Model entry from catalog */
@@ -561,6 +575,22 @@ export function FlexibleModelSelector({
} }
: null; : null;
const hasAvailableModels = recommendedOptions.length + allModelOptions.length > 0; 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 ( return (
<div className="space-y-1.5"> <div className="space-y-1.5">
@@ -568,87 +598,121 @@ export function FlexibleModelSelector({
<label className="text-xs font-medium">{label}</label> <label className="text-xs font-medium">{label}</label>
{description && <p className="text-[10px] text-muted-foreground">{description}</p>} {description && <p className="text-[10px] text-muted-foreground">{description}</p>}
</div> </div>
<SearchableSelect <div className="flex gap-2">
value={value || undefined} <SearchableSelect
onChange={onChange} value={value || undefined}
disabled={disabled} onChange={onChange}
placeholder={t('providerModelSelector.selectModel')} disabled={disabled}
searchPlaceholder={t('searchableSelect.searchModels')} placeholder={t('providerModelSelector.selectModel')}
emptyText={ searchPlaceholder={t('searchableSelect.searchModels')}
hasAvailableModels emptyText={
? t('searchableSelect.noResults') hasAvailableModels
: t('providerModelSelector.noModelsAvailable') ? t('searchableSelect.noResults')
} : t('providerModelSelector.noModelsAvailable')
triggerClassName="h-9" }
createOption={createCustomModelOption} className="min-w-0 flex-1"
groups={[ triggerClassName="h-9"
...(selectedValueMissing && legacySelectedOption createOption={createCustomModelOption}
? [ groups={[
{ ...(selectedValueMissing && legacySelectedOption
key: 'current', ? [
label: ( {
<span className="text-xs text-muted-foreground"> key: 'current',
{t('providerModelSelector.currentValue')} label: (
</span> <span className="text-xs text-muted-foreground">
), {t('providerModelSelector.currentValue')}
}, </span>
] ),
: []), },
{ ]
key: 'recommended', : []),
label: ( {
<span className="text-xs text-primary">{t('providerModelSelector.recommended')}</span> key: 'recommended',
), label: (
}, <span className="text-xs text-primary">
...(isCodexProvider {t('providerModelSelector.recommended')}
? [ </span>
{ ),
key: 'codex-reasoning', },
label: ( ...(isCodexProvider
<span className="text-xs text-muted-foreground"> ? [
{t('providerModelSelector.codexReasoningVariants')} {
</span> key: 'codex-reasoning',
), label: (
}, <span className="text-xs text-muted-foreground">
{ {t('providerModelSelector.codexReasoningVariants')}
key: 'codex-fast', </span>
label: ( ),
<span className="text-xs text-amber-600"> },
{t('providerModelSelector.codexFastVariants')} {
</span> key: 'codex-fast',
), label: (
}, <span className="text-xs text-amber-600">
] {t('providerModelSelector.codexFastVariants')}
: []), </span>
...(allModelOptions.length > 0 ),
? [ },
{ ]
key: 'all', : []),
label: ( ...(allModelOptions.length > 0
<span className="text-xs text-muted-foreground"> ? [
{t('providerModelSelector.allModelsCount', { {
count: allModelOptions.length, key: 'all',
})} label: (
</span> <span className="text-xs text-muted-foreground">
), {t('providerModelSelector.allModelsCount', {
}, count: allModelOptions.length,
] })}
: []), </span>
{ ),
key: 'custom', },
label: ( ]
<span className="text-xs text-muted-foreground"> : []),
{t('providerModelSelector.customModel')} {
</span> key: 'custom',
), label: (
}, <span className="text-xs text-muted-foreground">
]} {t('providerModelSelector.customModel')}
options={[ </span>
...(selectedValueMissing && legacySelectedOption ? [legacySelectedOption] : []), ),
...recommendedOptions, },
...allModelOptions, ]}
]} options={[
/> ...(selectedValueMissing && legacySelectedOption ? [legacySelectedOption] : []),
...recommendedOptions,
...allModelOptions,
]}
/>
{isCodexProvider && (
<Select
value={currentEffort ?? 'auto'}
onValueChange={(effort) =>
onChange(
applyCodexEffortSuffix(
value,
effort === 'auto' ? undefined : (effort as CodexEffort)
)
)
}
disabled={disabled || !value}
>
<SelectTrigger
className="h-9 w-28 shrink-0"
aria-label={t('providerModelSelector.effortLabel')}
>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="auto">{t('providerModelSelector.effortAuto')}</SelectItem>
{effortOptions.map((effort) => (
<SelectItem key={effort} value={effort}>
{effort}
</SelectItem>
))}
</SelectContent>
</Select>
)}
</div>
{selectedRoutingHint ? ( {selectedRoutingHint ? (
<div <div
className={cn( className={cn(
+11 -1
View File
@@ -2,7 +2,7 @@ export type CodexEffort = 'minimal' | 'low' | 'medium' | 'high' | 'xhigh';
export type CodexServiceTier = 'fast'; export type CodexServiceTier = 'fast';
const CODEX_TUNING_SUFFIX_TOKEN_REGEX = /-(minimal|low|medium|high|xhigh|fast)$/i; const CODEX_TUNING_SUFFIX_TOKEN_REGEX = /-(minimal|low|medium|high|xhigh|fast)$/i;
const CODEX_EFFORTS_IN_ORDER: readonly CodexEffort[] = [ export const CODEX_EFFORTS_IN_ORDER: readonly CodexEffort[] = [
'minimal', 'minimal',
'low', 'low',
'medium', 'medium',
@@ -102,6 +102,16 @@ export function getCodexEffortVariants(
return variantIds; return variantIds;
} }
export function getSelectableCodexEfforts(
maxEffort: CodexEffort | undefined,
efforts?: readonly CodexEffort[]
): CodexEffort[] {
if (efforts?.length) return [...efforts];
if (!maxEffort) return [...CODEX_EFFORTS_IN_ORDER];
const maxEffortIndex = CODEX_EFFORTS_IN_ORDER.indexOf(maxEffort);
return CODEX_EFFORTS_IN_ORDER.slice(0, maxEffortIndex + 1);
}
export function getCodexEffortDisplay( export function getCodexEffortDisplay(
modelId: string | undefined, modelId: string | undefined,
effortLabels?: { pinned: (effort: string) => string; auto: string } effortLabels?: { pinned: (effort: string) => string; auto: string }
+10
View File
@@ -228,6 +228,8 @@ const resources = {
'Pinned model is not currently advertised by the proxy: {{model}}', 'Pinned model is not currently advertised by the proxy: {{model}}',
codexReasoningVariants: 'Reasoning variants', codexReasoningVariants: 'Reasoning variants',
codexFastVariants: 'Fast variants', codexFastVariants: 'Fast variants',
effortLabel: 'Effort',
effortAuto: 'Auto',
}, },
createAuthProfileDialog: { createAuthProfileDialog: {
title: 'Create New Account', title: 'Create New Account',
@@ -2954,6 +2956,8 @@ const resources = {
pinnedModelNotAdvertised: '固定模型当前未被代理广播:{{model}}', pinnedModelNotAdvertised: '固定模型当前未被代理广播:{{model}}',
codexReasoningVariants: '推理变体', codexReasoningVariants: '推理变体',
codexFastVariants: '快速变体', codexFastVariants: '快速变体',
effortLabel: '推理强度',
effortAuto: '自动',
}, },
createAuthProfileDialog: { createAuthProfileDialog: {
title: '创建新账号', title: '创建新账号',
@@ -5563,6 +5567,8 @@ const resources = {
pinnedModelNotAdvertised: 'Mô hình ghim hiện không được proxy quảng bá: {{model}}', pinnedModelNotAdvertised: 'Mô hình ghim hiện không được proxy quảng bá: {{model}}',
codexReasoningVariants: 'Biến thể lý luận', codexReasoningVariants: 'Biến thể lý luận',
codexFastVariants: 'Biến thể nhanh', codexFastVariants: 'Biến thể nhanh',
effortLabel: 'Mức suy luận',
effortAuto: 'Tự động',
}, },
createAuthProfileDialog: { createAuthProfileDialog: {
title: 'Tạo tài khoản mới', title: 'Tạo tài khoản mới',
@@ -8288,6 +8294,8 @@ const resources = {
pinnedModelNotAdvertised: 'ピンモデルは現在プロキシから通知されていません: {{model}}', pinnedModelNotAdvertised: 'ピンモデルは現在プロキシから通知されていません: {{model}}',
codexReasoningVariants: '推論バリアント', codexReasoningVariants: '推論バリアント',
codexFastVariants: '高速バリアント', codexFastVariants: '高速バリアント',
effortLabel: '推論レベル',
effortAuto: '自動',
}, },
createAuthProfileDialog: { createAuthProfileDialog: {
title: '新しいアカウントを作成', title: '新しいアカウントを作成',
@@ -11036,6 +11044,8 @@ const resources = {
pinnedModelNotAdvertised: '고정된 모델이 현재 프록시에서 알리지 않고 있습니다: {{model}}', pinnedModelNotAdvertised: '고정된 모델이 현재 프록시에서 알리지 않고 있습니다: {{model}}',
codexReasoningVariants: '추론 변형', codexReasoningVariants: '추론 변형',
codexFastVariants: '빠른 변형', codexFastVariants: '빠른 변형',
effortLabel: '추론 강도',
effortAuto: '자동',
}, },
createAuthProfileDialog: { createAuthProfileDialog: {
title: '새 계정 생성', title: '새 계정 생성',
+42
View File
@@ -3,6 +3,7 @@ import {
applyCodexEffortSuffix, applyCodexEffortSuffix,
getCodexEffortDisplay, getCodexEffortDisplay,
getCodexEffortVariants, getCodexEffortVariants,
getSelectableCodexEfforts,
parseCodexEffort, parseCodexEffort,
parseCodexServiceTier, parseCodexServiceTier,
stripCodexEffortSuffix, 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', () => { it('builds fast variants for models with a fast service tier', () => {
expect(getCodexEffortVariants('gpt-5.4', 'high', ['fast'])).toEqual([ expect(getCodexEffortVariants('gpt-5.4', 'high', ['fast'])).toEqual([
'gpt-5.4', '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',
]);
});
});