diff --git a/src/cliproxy/catalog-routing.ts b/src/cliproxy/catalog-routing.ts index fc373df8..7b0c5ea3 100644 --- a/src/cliproxy/catalog-routing.ts +++ b/src/cliproxy/catalog-routing.ts @@ -3,7 +3,6 @@ import { type CliproxyProviderRoutingHints, } from '../shared/cliproxy-model-routing'; import { fetchCliproxyModels } from './stats-fetcher'; -import { ensureManagedModelPrefixes } from './managed-model-prefixes'; import { getResolvedCatalogSnapshot, type CatalogSource, @@ -20,12 +19,6 @@ export interface CatalogRoutingSnapshot { } export async function getCatalogRoutingSnapshot(): Promise { - try { - await ensureManagedModelPrefixes(); - } catch { - // Keep catalog rendering non-fatal when prefix sync is unavailable. - } - const snapshot: ResolvedCatalogSnapshot = await getResolvedCatalogSnapshot(); const modelsResponse = await fetchCliproxyModels(); const routing = buildCliproxyRoutingHints(snapshot.catalogs, modelsResponse?.models ?? []); diff --git a/src/cliproxy/managed-model-prefixes.ts b/src/cliproxy/managed-model-prefixes.ts index 6215ec67..2ab33041 100644 --- a/src/cliproxy/managed-model-prefixes.ts +++ b/src/cliproxy/managed-model-prefixes.ts @@ -10,6 +10,11 @@ interface ManagementAuthFileRecord { type?: string; } +interface AuthFileMetadata { + prefix: string | null; + provider: CLIProxyProvider | null; +} + export interface ManagedPrefixSyncResult { checked: number; updated: number; @@ -49,7 +54,7 @@ async function patchAuthFilePrefix(name: string, prefix: string): Promise } } -async function readAuthFilePrefix(name: string): Promise { +async function readAuthFileMetadata(name: string): Promise { const target = getProxyTarget(); const url = buildProxyUrl( target, @@ -65,10 +70,19 @@ async function readAuthFilePrefix(name: string): Promise { const content = await response.text(); try { - const parsed = JSON.parse(content) as { prefix?: unknown }; - return typeof parsed.prefix === 'string' ? parsed.prefix.trim() : null; + const parsed = JSON.parse(content) as { prefix?: unknown; provider?: unknown; type?: unknown }; + const providerName = + typeof parsed.provider === 'string' + ? parsed.provider + : typeof parsed.type === 'string' + ? parsed.type + : ''; + return { + prefix: typeof parsed.prefix === 'string' ? parsed.prefix.trim() : null, + provider: providerName ? mapExternalProviderName(providerName) : null, + }; } catch { - return null; + return { prefix: null, provider: null }; } } @@ -101,10 +115,18 @@ export async function ensureManagedModelPrefixes( try { checked += 1; - const currentPrefix = await readAuthFilePrefix(record.name); + const { prefix: currentPrefix, provider: fileProvider } = await readAuthFileMetadata( + record.name + ); + if (fileProvider !== provider) { + continue; + } if (currentPrefix === prefix) { continue; } + if (currentPrefix && currentPrefix !== prefix) { + continue; + } await patchAuthFilePrefix(record.name, prefix); updated += 1; } catch { diff --git a/src/commands/cliproxy/catalog-subcommand.ts b/src/commands/cliproxy/catalog-subcommand.ts index a26d05fb..13374e65 100644 --- a/src/commands/cliproxy/catalog-subcommand.ts +++ b/src/commands/cliproxy/catalog-subcommand.ts @@ -7,6 +7,7 @@ import { refreshCatalogFromProxy, } from '../../cliproxy/catalog-cache'; import { getCatalogRoutingSnapshot } from '../../cliproxy/catalog-routing'; +import { ensureManagedModelPrefixes } from '../../cliproxy/managed-model-prefixes'; import { getProxyTarget } from '../../cliproxy/proxy-target-resolver'; import type { CLIProxyProvider } from '../../cliproxy/types'; import type { RemoteModelInfo } from '../../cliproxy/management-api-types'; @@ -46,8 +47,17 @@ export async function handleCatalogStatus(verbose: boolean): Promise { console.log(header('Model Catalog')); console.log(''); - const routingSnapshot = await getCatalogRoutingSnapshot(); - const cacheAge = routingSnapshot.cacheAge ?? getCacheAge(); + let routingSnapshot: Awaited> | null = null; + if (verbose) { + try { + await ensureManagedModelPrefixes(); + routingSnapshot = await getCatalogRoutingSnapshot(); + } catch { + routingSnapshot = null; + } + } + + const cacheAge = routingSnapshot?.cacheAge ?? getCacheAge(); if (cacheAge) { console.log(` Cache: ${color('synced', 'success')} (${cacheAge})`); } else { @@ -58,10 +68,10 @@ export async function handleCatalogStatus(verbose: boolean): Promise { console.log(subheader('Providers:')); for (const provider of SYNCABLE_PROVIDERS) { - const catalog = routingSnapshot.catalogs[provider] ?? getResolvedCatalog(provider); + const catalog = routingSnapshot?.catalogs[provider] ?? getResolvedCatalog(provider); if (catalog) { const count = catalog.models.length; - const routing = routingSnapshot.routing[provider]; + const routing = routingSnapshot?.routing[provider]; const suffix = renderRoutingSummary(routing); console.log(` ${color(catalog.displayName.padEnd(20), 'command')} ${count} models${suffix}`); if (verbose) { @@ -112,7 +122,9 @@ function renderVerboseRouting( continue; } - console.log(dim(` preferred: ${hint.recommendedModelId}`)); + console.log( + dim(` ${hint.pinnedAvailable ? 'preferred' : 'suggested'}: ${hint.recommendedModelId}`) + ); if (hint.unprefixedStatus === 'safe') { console.log(dim(` unprefixed: resolves to ${routing.displayName}`)); continue; diff --git a/src/commands/cliproxy/variant-subcommand.ts b/src/commands/cliproxy/variant-subcommand.ts index 3a30533c..fd43ac93 100644 --- a/src/commands/cliproxy/variant-subcommand.ts +++ b/src/commands/cliproxy/variant-subcommand.ts @@ -10,9 +10,10 @@ import * as path from 'path'; import { getProviderAccounts } from '../../cliproxy/account-manager'; import { triggerOAuth } from '../../cliproxy/auth/oauth-handler'; import { CLIProxyProfileName, CLIPROXY_PROFILES } from '../../auth/profile-detector'; +import { getCatalogRoutingSnapshot } from '../../cliproxy/catalog-routing'; import { supportsModelConfig, getProviderCatalog, ModelEntry } from '../../cliproxy/model-catalog'; import { ensureManagedModelPrefixes } from '../../cliproxy/managed-model-prefixes'; -import { getManagedModelPrefix } from '../../shared/cliproxy-model-routing'; +import type { CliproxyProviderRoutingHints } from '../../shared/cliproxy-model-routing'; import { CLIProxyProvider, CLIProxyBackend } from '../../cliproxy/types'; import type { TargetType } from '../../targets/target-adapter'; import { getPersistedTargetChoices, isPersistedTargetType } from '../../targets/target-metadata'; @@ -117,9 +118,14 @@ function formatModelOption(model: ModelEntry): string { return `${model.name}${tierBadge}`; } -function getSelectableModelId(provider: CLIProxyProvider, modelId: string): string { - const prefix = getManagedModelPrefix(provider); - return prefix ? `${prefix}/${modelId}` : modelId; +function getSelectableModelId( + modelId: string, + routing: CliproxyProviderRoutingHints | undefined +): string { + const hint = routing?.models.find( + (entry) => entry.modelId.toLowerCase() === modelId.toLowerCase() + ); + return hint?.pinnedAvailable ? hint.recommendedModelId : modelId; } function getBackendLabel(backend: CLIProxyBackend): string { @@ -186,10 +192,11 @@ async function selectTierConfig( } catch { // Keep interactive model selection available even when prefix repair fails. } + const routing = (await getCatalogRoutingSnapshot()).routing[provider as CLIProxyProvider]; const catalog = getProviderCatalog(provider as CLIProxyProvider); if (catalog) { const modelOptions = catalog.models.map((m) => ({ - id: getSelectableModelId(provider as CLIProxyProvider, m.id), + id: getSelectableModelId(m.id, routing), label: formatModelOption(m), })); const defaultIdx = catalog.models.findIndex((m) => m.id === catalog.defaultModel); @@ -446,10 +453,11 @@ export async function handleCreate( } catch { // Keep variant creation available even when prefix repair fails. } + const routing = (await getCatalogRoutingSnapshot()).routing[provider as CLIProxyProvider]; const catalog = getProviderCatalog(provider as CLIProxyProvider); if (catalog) { const modelOptions = catalog.models.map((m) => ({ - id: getSelectableModelId(provider as CLIProxyProvider, m.id), + id: getSelectableModelId(m.id, routing), label: formatModelOption(m), })); const defaultIdx = catalog.models.findIndex((m) => m.id === catalog.defaultModel); @@ -695,10 +703,13 @@ export async function handleEdit( } catch { // Keep edit flow available even when prefix repair fails. } + const routing = (await getCatalogRoutingSnapshot()).routing[ + providerForModel as CLIProxyProvider + ]; const catalog = getProviderCatalog(providerForModel as CLIProxyProvider); if (catalog) { const modelOptions = catalog.models.map((m) => ({ - id: getSelectableModelId(providerForModel as CLIProxyProvider, m.id), + id: getSelectableModelId(m.id, routing), label: formatModelOption(m), })); const defaultIdx = catalog.models.findIndex((m) => m.id === catalog.defaultModel); diff --git a/src/shared/cliproxy-model-routing.ts b/src/shared/cliproxy-model-routing.ts index dcd06724..86c4e1ca 100644 --- a/src/shared/cliproxy-model-routing.ts +++ b/src/shared/cliproxy-model-routing.ts @@ -29,6 +29,7 @@ export interface CliproxyModelRoutingHint { prefix: string; pinnedModelId: string; recommendedModelId: string; + pinnedAvailable: boolean; unprefixedStatus: ModelRoutingStatus; effectiveProvider: string | null; effectiveDisplayName: string | null; @@ -108,9 +109,13 @@ function buildSummary( providerDisplayName: string, hint: Pick< CliproxyModelRoutingHint, - 'modelId' | 'pinnedModelId' | 'unprefixedStatus' | 'effectiveDisplayName' + 'modelId' | 'pinnedModelId' | 'pinnedAvailable' | 'unprefixedStatus' | 'effectiveDisplayName' > ): string { + if (!hint.pinnedAvailable) { + return `${hint.modelId} does not currently advertise a live pinned route for ${hint.pinnedModelId}. Reconnect or refresh managed prefixes before treating it as pinned.`; + } + if (hint.unprefixedStatus === 'safe') { return `${hint.modelId} currently resolves to ${providerDisplayName}. Use ${hint.pinnedModelId} to keep it pinned.`; } @@ -151,6 +156,15 @@ export function buildCliproxyRoutingHints( let prefixOnlyCount = 0; const models = catalog.models.map((model) => { + const pinnedCandidates = mergedModels + .filter((candidate) => normalize(candidate.id).endsWith(`/${normalize(model.id)}`)) + .filter((candidate) => inferProvider(candidate) === providerKey) + .map((candidate) => candidate.id) + .sort((left, right) => left.localeCompare(right)); + const managedPinnedId = `${prefix}/${model.id}`; + const recommendedModelId = pinnedCandidates.includes(managedPinnedId) + ? managedPinnedId + : (pinnedCandidates[0] ?? managedPinnedId); const mergedModel = mergedModelMap.get(normalize(model.id)); const effectiveProvider = mergedModel ? inferProvider(mergedModel) : null; const effectiveDisplayName = @@ -173,8 +187,9 @@ export function buildCliproxyRoutingHints( modelId: model.id, modelName: model.name?.trim() || model.id, prefix, - pinnedModelId: `${prefix}/${model.id}`, - recommendedModelId: `${prefix}/${model.id}`, + pinnedModelId: managedPinnedId, + recommendedModelId, + pinnedAvailable: pinnedCandidates.length > 0, unprefixedStatus, effectiveProvider, effectiveDisplayName, diff --git a/src/web-server/index.ts b/src/web-server/index.ts index fd2de5a6..9f5ff51f 100644 --- a/src/web-server/index.ts +++ b/src/web-server/index.ts @@ -13,6 +13,8 @@ import { WebSocketServer } from 'ws'; import { setupWebSocket } from './websocket'; import { createSessionMiddleware, authMiddleware } from './middleware/auth-middleware'; import { requestLoggingMiddleware } from './middleware/request-logging-middleware'; +import { ensureManagedModelPrefixes } from '../cliproxy/managed-model-prefixes'; +import { getProxyTarget } from '../cliproxy/proxy-target-resolver'; import { startAutoSyncWatcher, stopAutoSyncWatcher } from '../cliproxy/sync'; import { shutdownUsageAggregator } from './usage/aggregator'; import { createLogger } from '../services/logging'; @@ -119,6 +121,14 @@ export async function startServer(options: ServerOptions): Promise { + logger.warn('cliproxy.prefix_sync_failed', 'Managed model prefix repair failed', { + error: error instanceof Error ? error.message : String(error), + }); + }); + } + // Combined cleanup function const cleanup = () => { wsCleanup(); diff --git a/src/web-server/routes/cliproxy-auth-routes.ts b/src/web-server/routes/cliproxy-auth-routes.ts index 8cf2d4b0..686278df 100644 --- a/src/web-server/routes/cliproxy-auth-routes.ts +++ b/src/web-server/routes/cliproxy-auth-routes.ts @@ -316,12 +316,6 @@ export function getStartAuthNicknameError( */ router.get('/', async (_req: Request, res: Response): Promise => { try { - try { - await ensureManagedModelPrefixes(); - } catch { - // Keep auth status available even when prefix repair cannot run. - } - // Check if remote mode is enabled const target = getProxyTarget(); if (target.isRemote) { @@ -393,12 +387,6 @@ router.get('/', async (_req: Request, res: Response): Promise => { */ router.get('/accounts', async (_req: Request, res: Response): Promise => { try { - try { - await ensureManagedModelPrefixes(); - } catch { - // Non-fatal: account listing should still work without prefix repair. - } - // Check if remote mode is enabled const target = getProxyTarget(); if (target.isRemote) { diff --git a/tests/unit/cliproxy/model-routing-hints.test.ts b/tests/unit/cliproxy/model-routing-hints.test.ts index 5074621c..07734dcb 100644 --- a/tests/unit/cliproxy/model-routing-hints.test.ts +++ b/tests/unit/cliproxy/model-routing-hints.test.ts @@ -30,6 +30,7 @@ describe('cliproxy model routing hints', () => { expect(routing.gemini?.models[0]).toMatchObject({ recommendedModelId: 'gcli/gemini-3-flash-preview', + pinnedAvailable: false, unprefixedStatus: 'shadowed', effectiveProvider: 'agy', effectiveDisplayName: 'Antigravity', @@ -37,6 +38,7 @@ describe('cliproxy model routing hints', () => { expect(routing.agy?.models[0]).toMatchObject({ recommendedModelId: 'agy/gemini-3-flash', + pinnedAvailable: false, unprefixedStatus: 'safe', effectiveProvider: 'agy', }); @@ -57,6 +59,7 @@ describe('cliproxy model routing hints', () => { expect(routing.gemini?.prefixOnlyCount).toBe(1); expect(routing.gemini?.models[0]).toMatchObject({ recommendedModelId: 'gcli/gemini-3.1-pro-preview', + pinnedAvailable: false, unprefixedStatus: 'prefix-only', effectiveProvider: null, }); diff --git a/ui/src/components/cliproxy/provider-editor/custom-preset-dialog.tsx b/ui/src/components/cliproxy/provider-editor/custom-preset-dialog.tsx index 3b3671e2..2116a44d 100644 --- a/ui/src/components/cliproxy/provider-editor/custom-preset-dialog.tsx +++ b/ui/src/components/cliproxy/provider-editor/custom-preset-dialog.tsx @@ -20,6 +20,25 @@ import { FlexibleModelSelector } from '../provider-model-selector'; import type { CustomPresetDialogProps, ModelMappingValues } from './types'; import { useTranslation } from 'react-i18next'; +function normalizePresetValues( + values: ModelMappingValues, + routing: CustomPresetDialogProps['routing'] +): ModelMappingValues { + const toPreferredModelId = (modelId: string): string => { + const hint = routing?.models.find( + (entry) => entry.modelId.toLowerCase() === modelId.toLowerCase() + ); + return hint?.pinnedAvailable ? hint.recommendedModelId : modelId; + }; + + return { + default: toPreferredModelId(values.default), + opus: toPreferredModelId(values.opus), + sonnet: toPreferredModelId(values.sonnet), + haiku: toPreferredModelId(values.haiku), + }; +} + export function CustomPresetDialog({ open, onClose, @@ -32,13 +51,15 @@ export function CustomPresetDialog({ routing, }: CustomPresetDialogProps) { const { t } = useTranslation(); - const [values, setValues] = useState(currentValues); + const [values, setValues] = useState( + normalizePresetValues(currentValues, routing) + ); const [presetName, setPresetName] = useState(''); // Reset values when dialog opens with current values const handleOpenChange = (isOpen: boolean) => { if (isOpen) { - setValues(currentValues); + setValues(normalizePresetValues(currentValues, routing)); setPresetName(''); } else { onClose(); diff --git a/ui/src/components/cliproxy/provider-editor/model-config-section.tsx b/ui/src/components/cliproxy/provider-editor/model-config-section.tsx index 52163244..eb62216e 100644 --- a/ui/src/components/cliproxy/provider-editor/model-config-section.tsx +++ b/ui/src/components/cliproxy/provider-editor/model-config-section.tsx @@ -53,13 +53,16 @@ export function ModelConfigSection({ onDeletePreset, isDeletePending, }: ModelConfigSectionProps) { + const pinningReady = (routing?.models ?? []).some((hint) => hint.pinnedAvailable); const routingHintMap = useMemo( () => new Map((routing?.models ?? []).map((hint) => [hint.modelId.toLowerCase(), hint] as const)), [routing] ); const toPreferredModelId = (modelId: string): string => - routingHintMap.get(modelId.toLowerCase())?.recommendedModelId ?? modelId; + routingHintMap.get(modelId.toLowerCase())?.pinnedAvailable + ? (routingHintMap.get(modelId.toLowerCase())?.recommendedModelId ?? modelId) + : modelId; const extendedContextModels = useMemo(() => { if (!catalog) return []; @@ -160,10 +163,10 @@ export function ModelConfigSection({ className="text-xs h-7 gap-1 pr-6" onClick={() => { onApplyPreset({ - ANTHROPIC_MODEL: preset.default, - ANTHROPIC_DEFAULT_OPUS_MODEL: preset.opus, - ANTHROPIC_DEFAULT_SONNET_MODEL: preset.sonnet, - ANTHROPIC_DEFAULT_HAIKU_MODEL: preset.haiku, + ANTHROPIC_MODEL: toPreferredModelId(preset.default), + ANTHROPIC_DEFAULT_OPUS_MODEL: toPreferredModelId(preset.opus), + ANTHROPIC_DEFAULT_SONNET_MODEL: toPreferredModelId(preset.sonnet), + ANTHROPIC_DEFAULT_HAIKU_MODEL: toPreferredModelId(preset.haiku), }); }} > @@ -209,8 +212,17 @@ export function ModelConfigSection({

{routing ? (

- Preferred pinned model names use the {routing.prefix}/ prefix. Unprefixed - names can still resolve to a different backend when providers overlap. + {pinningReady ? ( + <> + Preferred pinned model names use the {routing.prefix}/ prefix. + Unprefixed names can still resolve to a different backend when providers overlap. + + ) : ( + <> + Managed pinning for {routing.prefix}/ is not currently advertised by + the proxy. Unprefixed names may still be ambiguous until prefix repair completes. + + )}

) : null} {provider === 'codex' && ( diff --git a/ui/src/components/cliproxy/provider-model-selector.tsx b/ui/src/components/cliproxy/provider-model-selector.tsx index d2924f50..0fa58b82 100644 --- a/ui/src/components/cliproxy/provider-model-selector.tsx +++ b/ui/src/components/cliproxy/provider-model-selector.tsx @@ -306,6 +306,16 @@ function normalizeModelValue( return value.startsWith(prefix) ? value.slice(prefix.length) : value; } +function getPreferredOptionValue( + modelId: string, + routingHint: CliproxyProviderRoutingHints['models'][number] | undefined +): string { + if (!routingHint?.pinnedAvailable) { + return modelId; + } + return routingHint.recommendedModelId; +} + export function FlexibleModelSelector({ label, description, @@ -334,16 +344,16 @@ export function FlexibleModelSelector({ ); const recommendedOptions = resolvedCatalogModels.map((model) => ({ - value: routingHints.get(model.id.toLowerCase())?.recommendedModelId ?? model.id, + value: getPreferredOptionValue(model.id, routingHints.get(model.id.toLowerCase())), groupKey: 'recommended', searchText: `${model.id} ${model.name} ${routingHints.get(model.id.toLowerCase())?.recommendedModelId ?? ''}`, keywords: [model.tier ?? '', catalog?.provider ?? ''], triggerContent: (
- {routingHints.get(model.id.toLowerCase())?.recommendedModelId ?? model.id} + {getPreferredOptionValue(model.id, routingHints.get(model.id.toLowerCase()))} - {routingHints.get(model.id.toLowerCase()) ? ( + {routingHints.get(model.id.toLowerCase())?.pinnedAvailable ? ( {routingHints.get(model.id.toLowerCase())?.prefix} @@ -354,7 +364,7 @@ export function FlexibleModelSelector({ itemContent: (
- {routingHints.get(model.id.toLowerCase())?.recommendedModelId ?? model.id} + {getPreferredOptionValue(model.id, routingHints.get(model.id.toLowerCase()))} {model.tier === 'paid' && } {routingHints.get(model.id.toLowerCase())?.unprefixedStatus === 'shadowed' ? ( @@ -375,19 +385,38 @@ export function FlexibleModelSelector({ const allModelOptions = allModels .filter((model) => !catalogModelIds.has(model.id)) .map((model) => ({ - value: model.id, + value: getPreferredOptionValue(model.id, routingHints.get(model.id.toLowerCase())), groupKey: 'all', - searchText: model.id, + searchText: `${model.id} ${routingHints.get(model.id.toLowerCase())?.recommendedModelId ?? ''}`, keywords: [model.owned_by], triggerContent: (
- {model.id} + + {getPreferredOptionValue(model.id, routingHints.get(model.id.toLowerCase()))} + + {routingHints.get(model.id.toLowerCase())?.pinnedAvailable ? ( + + {routingHints.get(model.id.toLowerCase())?.prefix} + + ) : null} {isCodexProvider && }
), itemContent: (
- {model.id} + + {getPreferredOptionValue(model.id, routingHints.get(model.id.toLowerCase()))} + + {routingHints.get(model.id.toLowerCase())?.unprefixedStatus === 'shadowed' ? ( + + Shadowed + + ) : null} + {routingHints.get(model.id.toLowerCase())?.unprefixedStatus === 'prefix-only' ? ( + + Prefix only + + ) : null} {isCodexProvider && }
), @@ -479,14 +508,21 @@ export function FlexibleModelSelector({ )} >
- Preferred pinned model: {selectedRoutingHint.recommendedModelId} + {selectedRoutingHint.pinnedAvailable + ? 'Preferred pinned model:' + : 'Pinned route status:'}{' '} + + {selectedRoutingHint.pinnedAvailable + ? selectedRoutingHint.recommendedModelId + : selectedRoutingHint.pinnedModelId} +

{selectedRoutingHint.summary}

) : null} {value && !selectedRoutingHint && normalizeModelValue(value, routing) !== value ? ( -
- Using pinned model: {value} +
+ Pinned model is not currently advertised by the proxy: {value}
) : null}
diff --git a/ui/src/lib/api-client.ts b/ui/src/lib/api-client.ts index 9b28b188..d8493a99 100644 --- a/ui/src/lib/api-client.ts +++ b/ui/src/lib/api-client.ts @@ -474,6 +474,7 @@ export interface CliproxyModelRoutingHint { prefix: string; pinnedModelId: string; recommendedModelId: string; + pinnedAvailable: boolean; unprefixedStatus: 'safe' | 'shadowed' | 'prefix-only'; effectiveProvider: string | null; effectiveDisplayName: string | null; diff --git a/ui/tests/unit/components/cliproxy/provider-editor/model-config-section.test.tsx b/ui/tests/unit/components/cliproxy/provider-editor/model-config-section.test.tsx index 923e01e6..7fdabedb 100644 --- a/ui/tests/unit/components/cliproxy/provider-editor/model-config-section.test.tsx +++ b/ui/tests/unit/components/cliproxy/provider-editor/model-config-section.test.tsx @@ -137,6 +137,7 @@ describe('ModelConfigSection presets', () => { prefix: 'gcli', pinnedModelId: 'gcli/gemini-3.1-pro-preview', recommendedModelId: 'gcli/gemini-3.1-pro-preview', + pinnedAvailable: true, unprefixedStatus: 'shadowed', effectiveProvider: 'agy', effectiveDisplayName: 'Antigravity', @@ -149,6 +150,7 @@ describe('ModelConfigSection presets', () => { prefix: 'gcli', pinnedModelId: 'gcli/gemini-3-flash-preview', recommendedModelId: 'gcli/gemini-3-flash-preview', + pinnedAvailable: true, unprefixedStatus: 'shadowed', effectiveProvider: 'agy', effectiveDisplayName: 'Antigravity', @@ -175,4 +177,79 @@ describe('ModelConfigSection presets', () => { ANTHROPIC_DEFAULT_HAIKU_MODEL: 'gcli/gemini-3-flash-preview', }); }); + + it('normalizes saved presets through preferred pinned model ids when live pinning is available', async () => { + const onApplyPreset = vi.fn(); + + render( + + ); + + await userEvent.click(screen.getByRole('button', { name: 'legacy' })); + + expect(onApplyPreset).toHaveBeenCalledWith({ + ANTHROPIC_MODEL: 'gcli/gemini-3-flash-preview', + ANTHROPIC_DEFAULT_OPUS_MODEL: 'gcli/gemini-3.1-pro-preview', + ANTHROPIC_DEFAULT_SONNET_MODEL: 'gcli/gemini-3.1-pro-preview', + ANTHROPIC_DEFAULT_HAIKU_MODEL: 'gcli/gemini-3-flash-preview', + }); + }); });