diff --git a/src/cliproxy/__tests__/model-catalog-compat.test.ts b/src/cliproxy/__tests__/model-catalog-compat.test.ts index 4ec11c1f..9fe98a30 100644 --- a/src/cliproxy/__tests__/model-catalog-compat.test.ts +++ b/src/cliproxy/__tests__/model-catalog-compat.test.ts @@ -55,6 +55,18 @@ describe('model-catalog compatibility lookups', () => { expect(catalog?.models.map((model) => model.id)).toEqual(['gemini-2.5-pro']); }); + it('falls back to a visible live model when the static default is absent', () => { + const catalog = mergeCatalog('gemini', [ + { + id: 'gemini-live-only', + display_name: 'Gemini Live Only', + }, + ]); + + expect(catalog?.defaultModel).toBe('gemini-live-only'); + expect(catalog?.models.map((model) => model.id)).toContain(catalog?.defaultModel); + }); + it('preserves static maxLevel when live thinking metadata omits it', () => { const catalog = mergeCatalog('claude', [ { diff --git a/src/cliproxy/quota/quota-manager.ts b/src/cliproxy/quota/quota-manager.ts index 8574b079..041e2748 100644 --- a/src/cliproxy/quota/quota-manager.ts +++ b/src/cliproxy/quota/quota-manager.ts @@ -483,9 +483,8 @@ export async function reconcileExhaustedRotationAccounts( return []; } - const { pauseAccountForQuotaCooldown, restoreExpiredQuotaPauses } = await import( - '../accounts/account-safety' - ); + const { pauseAccountForQuotaCooldown, restoreExpiredQuotaPauses } = + await import('../accounts/account-safety'); restoreExpiredQuotaPauses(); const config = loadOrCreateUnifiedConfig(); @@ -593,9 +592,8 @@ export async function preflightCheck(provider: CLIProxyProvider): Promise(); @@ -260,14 +259,11 @@ export function mergeCatalog( } // Process remote models: merge with static entries - const mergedIds = new Set(); const mergedModels: ModelEntry[] = []; for (const remote of filteredRemoteModels) { const remoteEntry = mapRemoteToModelEntry(remote); const staticEntry = staticMap.get(remote.id.toLowerCase()); - mergedIds.add(remote.id.toLowerCase()); - if (staticEntry) { const mergedThinking = remoteEntry.thinking ? { @@ -292,6 +288,12 @@ export function mergeCatalog( } } + const staticDefaultModel = staticCatalog?.defaultModel; + const hasStaticDefaultModel = + typeof staticDefaultModel === 'string' && + mergedModels.some((model) => model.id.toLowerCase() === staticDefaultModel.toLowerCase()); + const defaultModel = hasStaticDefaultModel ? staticDefaultModel : (mergedModels[0]?.id ?? ''); + return { provider, displayName, diff --git a/src/management/checks/image-analysis-check.ts b/src/management/checks/image-analysis-check.ts index 50e9ba99..d6d2dc05 100644 --- a/src/management/checks/image-analysis-check.ts +++ b/src/management/checks/image-analysis-check.ts @@ -112,9 +112,8 @@ export async function runImageAnalysisCheck(results: HealthCheck): Promise * Fix image analysis configuration issues */ export async function fixImageAnalysisConfig(): Promise { - const { updateConfig, loadOrCreateUnifiedConfig } = await import( - '../../config/config-loader-facade' - ); + const { updateConfig, loadOrCreateUnifiedConfig } = + await import('../../config/config-loader-facade'); const config = loadOrCreateUnifiedConfig(); let fixed = false; diff --git a/ui/src/lib/model-catalogs.ts b/ui/src/lib/model-catalogs.ts index df7d35dd..74fec42c 100644 --- a/ui/src/lib/model-catalogs.ts +++ b/ui/src/lib/model-catalogs.ts @@ -792,11 +792,19 @@ export function buildUiCatalog( availableModels.some( (model) => normalizeModelId(model.id) === normalizeModelId(fallbackDefaultModel) ); + const hasLiveDefaultModel = availableModels.some( + (model) => normalizeModelId(model.id) === normalizeModelId(liveCatalog.defaultModel) + ); + const defaultModel = hasFallbackDefaultModel + ? fallbackDefaultModel + : hasLiveDefaultModel + ? liveCatalog.defaultModel + : (models[0]?.id ?? ''); return { provider: liveCatalog.provider, displayName: liveCatalog.displayName || staticCatalog?.displayName || provider, - defaultModel: hasFallbackDefaultModel ? fallbackDefaultModel : liveCatalog.defaultModel, + defaultModel, models, }; } diff --git a/ui/src/lib/preset-utils.ts b/ui/src/lib/preset-utils.ts index 04adc14c..7e4d4082 100644 --- a/ui/src/lib/preset-utils.ts +++ b/ui/src/lib/preset-utils.ts @@ -66,11 +66,12 @@ export async function applyDefaultPreset( const defaultModelEntry = resolvedCatalog.models.find((model) => model.id === resolvedCatalog.defaultModel) || resolvedCatalog.models[0]; + const selectedModelId = defaultModelEntry?.id ?? resolvedCatalog.defaultModel; const mapping = defaultModelEntry?.presetMapping || { - default: resolvedCatalog.defaultModel, - opus: resolvedCatalog.defaultModel, - sonnet: resolvedCatalog.defaultModel, - haiku: resolvedCatalog.defaultModel, + default: selectedModelId, + opus: selectedModelId, + sonnet: selectedModelId, + haiku: selectedModelId, }; // Fetch effective API key (respects user customization) diff --git a/ui/tests/unit/ui/lib/preset-utils.test.ts b/ui/tests/unit/ui/lib/preset-utils.test.ts index ecc472ff..4199e8b9 100644 --- a/ui/tests/unit/ui/lib/preset-utils.test.ts +++ b/ui/tests/unit/ui/lib/preset-utils.test.ts @@ -116,6 +116,55 @@ describe('claude preset utils', () => { ); }); + it('builds UI catalogs with a visible default when the live default is stale', () => { + const liveCatalogs = { + gemini: { + provider: 'gemini', + displayName: 'Gemini', + defaultModel: 'gemini-2.5-pro', + models: [{ id: 'gemini-live-only', name: 'Gemini Live Only' }], + }, + }; + + const catalogs = buildUiCatalogs(liveCatalogs); + + expect(catalogs.gemini?.defaultModel).toBe('gemini-live-only'); + expect(catalogs.gemini?.models.map((model) => model.id)).toContain( + catalogs.gemini?.defaultModel + ); + }); + + it('uses the selected visible model for quick setup fallback mappings', 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('gemini', undefined, { + provider: 'gemini', + displayName: 'Gemini', + defaultModel: 'gemini-2.5-pro', + models: [{ id: 'gemini-live-only', name: 'Gemini Live Only' }], + }); + + expect(result).toEqual({ success: true, presetName: 'Gemini Live Only' }); + + const [, requestInit] = fetchMock.mock.calls[1] ?? []; + const body = JSON.parse(String(requestInit?.body)); + + expect(body.settings.env).toMatchObject({ + ANTHROPIC_MODEL: 'gemini-live-only', + ANTHROPIC_DEFAULT_OPUS_MODEL: 'gemini-live-only', + ANTHROPIC_DEFAULT_SONNET_MODEL: 'gemini-live-only', + ANTHROPIC_DEFAULT_HAIKU_MODEL: 'gemini-live-only', + }); + }); + it('keeps Gemini presets on 3.1 Pro while resolving 3/3.1 alias variants', () => { const geminiCatalog = MODEL_CATALOGS.gemini; const latestPro = geminiCatalog.models.find((model) => model.id === 'gemini-3.1-pro-preview');