mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-11 12:09:03 +00:00
feat(ui): add per-tier codex effort dropdown in model mapping
This commit is contained in:
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(
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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: '새 계정 생성',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user