From 564ed3def716d837e648feec82c340c1442c2878 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Wed, 8 Apr 2026 17:52:45 -0400 Subject: [PATCH] fix(ui): reuse cached cliproxy catalogs for presets - let applyDefaultPreset accept a caller-provided provider catalog - pass cached catalog data from cliproxy and quick-setup flows - add regression coverage for skipping the extra catalog fetch --- .../components/account/add-account-dialog.tsx | 7 +++-- ui/src/components/setup/wizard/index.tsx | 6 ++++- ui/src/lib/preset-utils.ts | 27 ++++++++++++------- ui/src/pages/cliproxy.tsx | 1 + ui/tests/unit/ui/lib/preset-utils.test.ts | 25 +++++++++++++++++ 5 files changed, 53 insertions(+), 13 deletions(-) diff --git a/ui/src/components/account/add-account-dialog.tsx b/ui/src/components/account/add-account-dialog.tsx index 646a862a..294d5d76 100644 --- a/ui/src/components/account/add-account-dialog.tsx +++ b/ui/src/components/account/add-account-dialog.tsx @@ -29,6 +29,7 @@ import { Loader2, ExternalLink, User, Download, Copy, Check, ShieldAlert } from import { useKiroImport } from '@/hooks/use-cliproxy'; import { useCliproxyAuthFlow } from '@/hooks/use-cliproxy-auth-flow'; import { applyDefaultPreset } from '@/lib/preset-utils'; +import type { CliproxyProviderCatalog } from '@/lib/api-client'; import { AccountSafetyWarningCard } from '@/components/account/account-safety-warning-card'; import { AntigravityResponsibilityChecklist } from '@/components/account/antigravity-responsibility-checklist'; import { @@ -56,6 +57,7 @@ interface AddAccountDialogProps { onClose: () => void; provider: string; displayName: string; + catalog?: CliproxyProviderCatalog; /** Whether this is the first account being added (shows different toast message) */ isFirstAccount?: boolean; } @@ -74,6 +76,7 @@ export function AddAccountDialog({ onClose, provider, displayName, + catalog, isFirstAccount = false, }: AddAccountDialogProps) { const [nickname, setNickname] = useState(''); @@ -244,7 +247,7 @@ export function AddAccountDialog({ wasAuthenticatingRef.current = false; const applyPresetAndClose = async () => { try { - const result = await applyDefaultPreset(provider); + const result = await applyDefaultPreset(provider, undefined, catalog); if (result.success && result.presetName && isFirstAccount) { toast.success(`Applied "${result.presetName}" preset`); } @@ -331,7 +334,7 @@ export function AddAccountDialog({ wasAuthenticatingRef.current = true; kiroImportMutation.mutate(undefined, { onSuccess: async () => { - const result = await applyDefaultPreset('kiro'); + const result = await applyDefaultPreset('kiro', undefined, catalog); if (result.success && result.presetName && isFirstAccount) { toast.success(`Applied "${result.presetName}" preset`); } diff --git a/ui/src/components/setup/wizard/index.tsx b/ui/src/components/setup/wizard/index.tsx index 98fdfdc6..290b8298 100644 --- a/ui/src/components/setup/wizard/index.tsx +++ b/ui/src/components/setup/wizard/index.tsx @@ -101,7 +101,11 @@ export function QuickSetupWizard({ open, onClose }: QuickSetupWizardProps) { { onSuccess: async (data) => { if (isFirstAccount) { - const result = await applyDefaultPreset(selectedProvider); + const result = await applyDefaultPreset( + selectedProvider, + undefined, + catalogs[selectedProvider] + ); if (result.success && result.presetName) { toast.success(`Applied "${result.presetName}" preset`); } else if (!result.success) { diff --git a/ui/src/lib/preset-utils.ts b/ui/src/lib/preset-utils.ts index fb61145d..04adc14c 100644 --- a/ui/src/lib/preset-utils.ts +++ b/ui/src/lib/preset-utils.ts @@ -3,6 +3,7 @@ * Shared functions for applying default presets to provider settings */ +import type { CliproxyProviderCatalog } from './api-client'; import { MODEL_CATALOGS } from './model-catalogs'; import { buildUiCatalogs } from './model-catalogs'; import { CLIPROXY_DEFAULT_PORT } from './default-ports'; @@ -26,7 +27,11 @@ async function fetchEffectiveApiKey(): Promise { } } -async function fetchProviderCatalog(provider: string) { +async function fetchProviderCatalog(provider: string, catalog?: CliproxyProviderCatalog) { + if (catalog) { + return catalog; + } + try { const response = await fetch('/api/cliproxy/catalog'); if (!response.ok) { @@ -52,18 +57,20 @@ async function fetchProviderCatalog(provider: string) { */ export async function applyDefaultPreset( provider: string, - port?: number + port?: number, + catalog?: CliproxyProviderCatalog ): Promise<{ success: boolean; presetName?: string }> { - const catalog = await fetchProviderCatalog(provider); - if (!catalog) return { success: false }; + const resolvedCatalog = await fetchProviderCatalog(provider, catalog); + if (!resolvedCatalog) return { success: false }; const defaultModelEntry = - catalog.models.find((model) => model.id === catalog.defaultModel) || catalog.models[0]; + resolvedCatalog.models.find((model) => model.id === resolvedCatalog.defaultModel) || + resolvedCatalog.models[0]; const mapping = defaultModelEntry?.presetMapping || { - default: catalog.defaultModel, - opus: catalog.defaultModel, - sonnet: catalog.defaultModel, - haiku: catalog.defaultModel, + default: resolvedCatalog.defaultModel, + opus: resolvedCatalog.defaultModel, + sonnet: resolvedCatalog.defaultModel, + haiku: resolvedCatalog.defaultModel, }; // Fetch effective API key (respects user customization) @@ -89,7 +96,7 @@ export async function applyDefaultPreset( }); return { success: res.ok, - presetName: defaultModelEntry?.name || catalog.defaultModel, + presetName: defaultModelEntry?.name || resolvedCatalog.defaultModel, }; } catch { return { success: false }; diff --git a/ui/src/pages/cliproxy.tsx b/ui/src/pages/cliproxy.tsx index 95cca80b..5a05cd95 100644 --- a/ui/src/pages/cliproxy.tsx +++ b/ui/src/pages/cliproxy.tsx @@ -561,6 +561,7 @@ export function CliproxyPage() { onClose={() => setAddAccountProvider(null)} provider={addAccountProvider?.provider || ''} displayName={addAccountProvider?.displayName || ''} + catalog={addAccountProvider?.provider ? catalogs[addAccountProvider.provider] : undefined} isFirstAccount={addAccountProvider?.isFirstAccount || false} /> diff --git a/ui/tests/unit/ui/lib/preset-utils.test.ts b/ui/tests/unit/ui/lib/preset-utils.test.ts index 9fc4e890..dbc852a2 100644 --- a/ui/tests/unit/ui/lib/preset-utils.test.ts +++ b/ui/tests/unit/ui/lib/preset-utils.test.ts @@ -58,6 +58,31 @@ describe('claude preset utils', () => { }); }); + it('skips catalog fetch when the caller already has the provider catalog cached', async () => { + const fetchMock = vi + .fn() + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ apiKey: { value: 'managed-key' } }), + }) + .mockResolvedValueOnce({ ok: true }); + + vi.stubGlobal('fetch', fetchMock); + + const result = await applyDefaultPreset('claude', undefined, MODEL_CATALOGS.claude); + + expect(result).toEqual({ success: true, presetName: 'Claude Sonnet 4.6' }); + expect(fetchMock).toHaveBeenCalledTimes(2); + expect(fetchMock).toHaveBeenNthCalledWith(1, '/api/settings/auth/tokens/raw'); + expect(fetchMock).toHaveBeenNthCalledWith( + 2, + '/api/settings/claude', + expect.objectContaining({ + method: 'PUT', + }) + ); + }); + it('builds UI catalogs from upstream provider models without requiring static dropdown edits', () => { const liveCatalogs = { gemini: {