From e57bded451de16fb31f639df8e92ebd554288946 Mon Sep 17 00:00:00 2001 From: milesnguyen2405 Date: Wed, 22 Jul 2026 11:29:04 +0700 Subject: [PATCH 1/4] feat(ui): add per-tier codex effort dropdown in model mapping --- .../cliproxy/provider-model-selector.tsx | 228 +++++++++++------- ui/src/lib/codex-effort.ts | 12 +- ui/src/lib/i18n.ts | 10 + ui/tests/unit/ui/lib/codex-effort.test.ts | 42 ++++ 4 files changed, 209 insertions(+), 83 deletions(-) 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', + ]); + }); +}); From e81fd80944304be4278db9ef4cf2cff98978bb47 Mon Sep 17 00:00:00 2001 From: milesnguyen2405 Date: Wed, 22 Jul 2026 11:29:45 +0700 Subject: [PATCH 2/4] chore(ui): forward dev proxy origin for dashboard csrf guard --- ui/vite.config.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/ui/vite.config.ts b/ui/vite.config.ts index 4a31e9f5..3ba84058 100644 --- a/ui/vite.config.ts +++ b/ui/vite.config.ts @@ -62,10 +62,18 @@ export default defineConfig({ allow: [UI_ROOT, REPO_ROOT], }, proxy: { - '/api': 'http://localhost:3000', + // The dashboard's CSRF guard rejects loopback origins on a different + // port, so the dev proxy must present the API server's own origin. + '/api': { + target: 'http://localhost:3000', + changeOrigin: true, + headers: { origin: 'http://localhost:3000' }, + }, '/ws': { target: 'ws://localhost:3000', ws: true, + changeOrigin: true, + headers: { origin: 'http://localhost:3000' }, }, }, }, From 30b54a09666642d65f80c01b60da9d19324cafdd Mon Sep 17 00:00:00 2001 From: milesnguyen2405 Date: Wed, 22 Jul 2026 11:53:07 +0700 Subject: [PATCH 3/4] feat(ui): extend reasoning control to xai grok models --- .../cliproxy/provider-model-selector.tsx | 62 +++---- ui/src/lib/model-catalogs.ts | 3 + ui/src/lib/reasoning-control.ts | 63 +++++++ .../unit/ui/lib/reasoning-control.test.ts | 168 ++++++++++++++++++ 4 files changed, 260 insertions(+), 36 deletions(-) create mode 100644 ui/src/lib/reasoning-control.ts create mode 100644 ui/tests/unit/ui/lib/reasoning-control.test.ts diff --git a/ui/src/components/cliproxy/provider-model-selector.tsx b/ui/src/components/cliproxy/provider-model-selector.tsx index 37ba59c8..405c9f19 100644 --- a/ui/src/components/cliproxy/provider-model-selector.tsx +++ b/ui/src/components/cliproxy/provider-model-selector.tsx @@ -21,19 +21,13 @@ 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 { - findCatalogModel, - getResolvedCatalogModels, - getSupplementalCatalogModels, -} from '@/lib/model-catalogs'; +import { getResolvedCatalogModels, getSupplementalCatalogModels } from '@/lib/model-catalogs'; +import { getReasoningAdapter } from '@/lib/reasoning-control'; import { cn } from '@/lib/utils'; /** Model entry from catalog */ @@ -61,6 +55,8 @@ export interface ModelEntry { codexEfforts?: CodexEffort[]; /** Additional Codex service-tier suffixes supported by this model in the dashboard UI. */ codexServiceTiers?: CodexServiceTier[]; + /** Reasoning-level suffixes this model supports via paren syntax in the dashboard UI. */ + reasoningLevels?: string[]; } /** Provider catalog */ @@ -575,22 +571,21 @@ 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; + const reasoningAdapter = getReasoningAdapter(catalog?.provider); + const currentReasoningLevel = reasoningAdapter?.parse(value); + const selectableReasoningLevels = + reasoningAdapter && catalog + ? reasoningAdapter.getOptions( + reasoningAdapter.strip(normalizeModelValue(value, routing)), + catalog + ) + : null; + const reasoningOptions = + selectableReasoningLevels && + currentReasoningLevel && + !selectableReasoningLevels.includes(currentReasoningLevel) + ? [...selectableReasoningLevels, currentReasoningLevel] + : selectableReasoningLevels; return (
@@ -683,16 +678,11 @@ export function FlexibleModelSelector({ ...allModelOptions, ]} /> - {isCodexProvider && ( + {reasoningAdapter && reasoningOptions && (