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',
+ });
+ });
});