From 108ff843f142e0a88d000db2aaa731dbc89f450f Mon Sep 17 00:00:00 2001 From: milesnguyen2405 Date: Wed, 22 Jul 2026 15:13:42 +0700 Subject: [PATCH 1/2] feat(ui): add codex fast service-tier toggle --- .../cliproxy/provider-model-selector.tsx | 35 ++++++++++++++++++- ui/src/lib/codex-effort.ts | 6 ++++ ui/src/lib/i18n.ts | 5 +++ ui/tests/unit/ui/lib/codex-effort.test.ts | 35 +++++++++++++++++++ 4 files changed, 80 insertions(+), 1 deletion(-) diff --git a/ui/src/components/cliproxy/provider-model-selector.tsx b/ui/src/components/cliproxy/provider-model-selector.tsx index 405c9f19..e6607c19 100644 --- a/ui/src/components/cliproxy/provider-model-selector.tsx +++ b/ui/src/components/cliproxy/provider-model-selector.tsx @@ -18,15 +18,22 @@ import { SelectValue, } from '@/components/ui/select'; import { Skeleton } from '@/components/ui/skeleton'; +import { Switch } from '@/components/ui/switch'; import type { CliproxyProviderRoutingHints } from '@/lib/api-client'; import type { CodexEffort, CodexServiceTier } from '@/lib/codex-effort'; import { + applyCodexServiceTierSuffix, getCodexEffortDisplay, getCodexEffortVariants, parseCodexEffort, parseCodexServiceTier, + stripCodexEffortSuffix, } from '@/lib/codex-effort'; -import { getResolvedCatalogModels, getSupplementalCatalogModels } from '@/lib/model-catalogs'; +import { + findCatalogModel, + getResolvedCatalogModels, + getSupplementalCatalogModels, +} from '@/lib/model-catalogs'; import { getReasoningAdapter } from '@/lib/reasoning-control'; import { cn } from '@/lib/utils'; @@ -586,6 +593,19 @@ export function FlexibleModelSelector({ !selectableReasoningLevels.includes(currentReasoningLevel) ? [...selectableReasoningLevels, currentReasoningLevel] : selectableReasoningLevels; + const currentServiceTier = isCodexProvider ? parseCodexServiceTier(value) : undefined; + const selectedCodexBaseModel = + isCodexProvider && catalog && value + ? findCatalogModel( + catalog.provider, + stripCodexEffortSuffix(normalizeModelValue(value, routing)), + catalog + ) + : undefined; + const showFastToggle = + isCodexProvider && + (selectedCodexBaseModel?.codexServiceTiers?.includes('fast') === true || + currentServiceTier === 'fast'); return (
@@ -702,6 +722,19 @@ export function FlexibleModelSelector({ )} + {showFastToggle && ( + + )}
{selectedRoutingHint ? (
{ }); }); +describe('applyCodexServiceTierSuffix', () => { + it('adds the fast tier while preserving the effort suffix', () => { + expect(applyCodexServiceTierSuffix('gpt-5.4-high', true)).toBe('gpt-5.4-high-fast'); + }); + + it('removes only the fast tier', () => { + expect(applyCodexServiceTierSuffix('gpt-5.4-high-fast', false)).toBe('gpt-5.4-high'); + expect(applyCodexServiceTierSuffix('gpt-5.4-fast', false)).toBe('gpt-5.4'); + }); + + it('preserves routing prefixes', () => { + expect(applyCodexServiceTierSuffix('codex/gpt-5.6-sol', true)).toBe('codex/gpt-5.6-sol-fast'); + }); + + it('is idempotent and safe on empty input', () => { + expect(applyCodexServiceTierSuffix('gpt-5.4-fast', true)).toBe('gpt-5.4-fast'); + expect(applyCodexServiceTierSuffix('gpt-5.4', false)).toBe('gpt-5.4'); + expect(applyCodexServiceTierSuffix(undefined, true)).toBe(''); + expect(applyCodexServiceTierSuffix('', true)).toBe(''); + }); + + it('composes with applyCodexEffortSuffix in both orders', () => { + expect(applyCodexEffortSuffix(applyCodexServiceTierSuffix('gpt-5.4', true), 'high')).toBe( + 'gpt-5.4-high-fast' + ); + expect(applyCodexServiceTierSuffix(applyCodexEffortSuffix('gpt-5.4', 'high'), true)).toBe( + 'gpt-5.4-high-fast' + ); + expect( + applyCodexEffortSuffix(applyCodexServiceTierSuffix('gpt-5.4-high-fast', false), 'low') + ).toBe('gpt-5.4-low'); + }); +}); + describe('getSelectableCodexEfforts', () => { it('returns the explicit efforts list when provided', () => { expect(getSelectableCodexEfforts('xhigh', ['low', 'medium', 'high', 'xhigh'])).toEqual([ From 8d1a093cf94341e010903f2918c0dc18da19f94a Mon Sep 17 00:00:00 2001 From: milesnguyen2405 Date: Wed, 22 Jul 2026 15:14:03 +0700 Subject: [PATCH 2/2] fix(ui): preserve codex tuning metadata in live catalog merge --- ui/src/lib/model-catalogs.ts | 3 ++ .../ui/lib/model-catalogs-codex-merge.test.ts | 30 +++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 ui/tests/unit/ui/lib/model-catalogs-codex-merge.test.ts diff --git a/ui/src/lib/model-catalogs.ts b/ui/src/lib/model-catalogs.ts index ea566f9f..e6b03dcb 100644 --- a/ui/src/lib/model-catalogs.ts +++ b/ui/src/lib/model-catalogs.ts @@ -1150,6 +1150,9 @@ export function buildUiCatalog( : (model.extendedContext ?? staticModel?.extendedContext), presetMapping: staticModel?.presetMapping, reasoningLevels: model.reasoningLevels ?? staticModel?.reasoningLevels, + codexMaxEffort: model.codexMaxEffort ?? staticModel?.codexMaxEffort, + codexEfforts: model.codexEfforts ?? staticModel?.codexEfforts, + codexServiceTiers: model.codexServiceTiers ?? staticModel?.codexServiceTiers, }; }); diff --git a/ui/tests/unit/ui/lib/model-catalogs-codex-merge.test.ts b/ui/tests/unit/ui/lib/model-catalogs-codex-merge.test.ts new file mode 100644 index 00000000..79b13e09 --- /dev/null +++ b/ui/tests/unit/ui/lib/model-catalogs-codex-merge.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from 'vitest'; +import { buildUiCatalog } from '@/lib/model-catalogs'; + +describe('buildUiCatalog codex tuning metadata merge', () => { + it('preserves static codex effort and service-tier metadata when the live catalog omits them', () => { + const merged = buildUiCatalog('codex', { + provider: 'codex', + displayName: 'Codex', + defaultModel: 'gpt-5.6-sol', + models: [{ id: 'gpt-5.6-sol', name: 'GPT-5.6 Sol' }], + }); + + const model = merged?.models.find((entry) => entry.id === 'gpt-5.6-sol'); + expect(model?.codexMaxEffort).toBe('xhigh'); + expect(model?.codexEfforts).toEqual(['low', 'medium', 'high', 'xhigh']); + expect(model?.codexServiceTiers).toEqual(['fast']); + }); + + it('prefers live metadata over static when the live catalog provides it', () => { + const merged = buildUiCatalog('codex', { + provider: 'codex', + displayName: 'Codex', + defaultModel: 'gpt-5.6-sol', + models: [{ id: 'gpt-5.6-sol', name: 'GPT-5.6 Sol', codexServiceTiers: [] }], + }); + + const model = merged?.models.find((entry) => entry.id === 'gpt-5.6-sol'); + expect(model?.codexServiceTiers).toEqual([]); + }); +});