fix(cliproxy): harden routing prefix sync and UI pinning

This commit is contained in:
Tam Nhu Tran
2026-04-10 01:16:31 -04:00
parent b8a8f84153
commit 0c10cb6f47
13 changed files with 260 additions and 59 deletions
-7
View File
@@ -3,7 +3,6 @@ import {
type CliproxyProviderRoutingHints, type CliproxyProviderRoutingHints,
} from '../shared/cliproxy-model-routing'; } from '../shared/cliproxy-model-routing';
import { fetchCliproxyModels } from './stats-fetcher'; import { fetchCliproxyModels } from './stats-fetcher';
import { ensureManagedModelPrefixes } from './managed-model-prefixes';
import { import {
getResolvedCatalogSnapshot, getResolvedCatalogSnapshot,
type CatalogSource, type CatalogSource,
@@ -20,12 +19,6 @@ export interface CatalogRoutingSnapshot {
} }
export async function getCatalogRoutingSnapshot(): Promise<CatalogRoutingSnapshot> { export async function getCatalogRoutingSnapshot(): Promise<CatalogRoutingSnapshot> {
try {
await ensureManagedModelPrefixes();
} catch {
// Keep catalog rendering non-fatal when prefix sync is unavailable.
}
const snapshot: ResolvedCatalogSnapshot = await getResolvedCatalogSnapshot(); const snapshot: ResolvedCatalogSnapshot = await getResolvedCatalogSnapshot();
const modelsResponse = await fetchCliproxyModels(); const modelsResponse = await fetchCliproxyModels();
const routing = buildCliproxyRoutingHints(snapshot.catalogs, modelsResponse?.models ?? []); const routing = buildCliproxyRoutingHints(snapshot.catalogs, modelsResponse?.models ?? []);
+27 -5
View File
@@ -10,6 +10,11 @@ interface ManagementAuthFileRecord {
type?: string; type?: string;
} }
interface AuthFileMetadata {
prefix: string | null;
provider: CLIProxyProvider | null;
}
export interface ManagedPrefixSyncResult { export interface ManagedPrefixSyncResult {
checked: number; checked: number;
updated: number; updated: number;
@@ -49,7 +54,7 @@ async function patchAuthFilePrefix(name: string, prefix: string): Promise<void>
} }
} }
async function readAuthFilePrefix(name: string): Promise<string | null> { async function readAuthFileMetadata(name: string): Promise<AuthFileMetadata> {
const target = getProxyTarget(); const target = getProxyTarget();
const url = buildProxyUrl( const url = buildProxyUrl(
target, target,
@@ -65,10 +70,19 @@ async function readAuthFilePrefix(name: string): Promise<string | null> {
const content = await response.text(); const content = await response.text();
try { try {
const parsed = JSON.parse(content) as { prefix?: unknown }; const parsed = JSON.parse(content) as { prefix?: unknown; provider?: unknown; type?: unknown };
return typeof parsed.prefix === 'string' ? parsed.prefix.trim() : null; 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 { } catch {
return null; return { prefix: null, provider: null };
} }
} }
@@ -101,10 +115,18 @@ export async function ensureManagedModelPrefixes(
try { try {
checked += 1; checked += 1;
const currentPrefix = await readAuthFilePrefix(record.name); const { prefix: currentPrefix, provider: fileProvider } = await readAuthFileMetadata(
record.name
);
if (fileProvider !== provider) {
continue;
}
if (currentPrefix === prefix) { if (currentPrefix === prefix) {
continue; continue;
} }
if (currentPrefix && currentPrefix !== prefix) {
continue;
}
await patchAuthFilePrefix(record.name, prefix); await patchAuthFilePrefix(record.name, prefix);
updated += 1; updated += 1;
} catch { } catch {
+17 -5
View File
@@ -7,6 +7,7 @@ import {
refreshCatalogFromProxy, refreshCatalogFromProxy,
} from '../../cliproxy/catalog-cache'; } from '../../cliproxy/catalog-cache';
import { getCatalogRoutingSnapshot } from '../../cliproxy/catalog-routing'; import { getCatalogRoutingSnapshot } from '../../cliproxy/catalog-routing';
import { ensureManagedModelPrefixes } from '../../cliproxy/managed-model-prefixes';
import { getProxyTarget } from '../../cliproxy/proxy-target-resolver'; import { getProxyTarget } from '../../cliproxy/proxy-target-resolver';
import type { CLIProxyProvider } from '../../cliproxy/types'; import type { CLIProxyProvider } from '../../cliproxy/types';
import type { RemoteModelInfo } from '../../cliproxy/management-api-types'; import type { RemoteModelInfo } from '../../cliproxy/management-api-types';
@@ -46,8 +47,17 @@ export async function handleCatalogStatus(verbose: boolean): Promise<void> {
console.log(header('Model Catalog')); console.log(header('Model Catalog'));
console.log(''); console.log('');
const routingSnapshot = await getCatalogRoutingSnapshot(); let routingSnapshot: Awaited<ReturnType<typeof getCatalogRoutingSnapshot>> | null = null;
const cacheAge = routingSnapshot.cacheAge ?? getCacheAge(); if (verbose) {
try {
await ensureManagedModelPrefixes();
routingSnapshot = await getCatalogRoutingSnapshot();
} catch {
routingSnapshot = null;
}
}
const cacheAge = routingSnapshot?.cacheAge ?? getCacheAge();
if (cacheAge) { if (cacheAge) {
console.log(` Cache: ${color('synced', 'success')} (${cacheAge})`); console.log(` Cache: ${color('synced', 'success')} (${cacheAge})`);
} else { } else {
@@ -58,10 +68,10 @@ export async function handleCatalogStatus(verbose: boolean): Promise<void> {
console.log(subheader('Providers:')); console.log(subheader('Providers:'));
for (const provider of SYNCABLE_PROVIDERS) { for (const provider of SYNCABLE_PROVIDERS) {
const catalog = routingSnapshot.catalogs[provider] ?? getResolvedCatalog(provider); const catalog = routingSnapshot?.catalogs[provider] ?? getResolvedCatalog(provider);
if (catalog) { if (catalog) {
const count = catalog.models.length; const count = catalog.models.length;
const routing = routingSnapshot.routing[provider]; const routing = routingSnapshot?.routing[provider];
const suffix = renderRoutingSummary(routing); const suffix = renderRoutingSummary(routing);
console.log(` ${color(catalog.displayName.padEnd(20), 'command')} ${count} models${suffix}`); console.log(` ${color(catalog.displayName.padEnd(20), 'command')} ${count} models${suffix}`);
if (verbose) { if (verbose) {
@@ -112,7 +122,9 @@ function renderVerboseRouting(
continue; continue;
} }
console.log(dim(` preferred: ${hint.recommendedModelId}`)); console.log(
dim(` ${hint.pinnedAvailable ? 'preferred' : 'suggested'}: ${hint.recommendedModelId}`)
);
if (hint.unprefixedStatus === 'safe') { if (hint.unprefixedStatus === 'safe') {
console.log(dim(` unprefixed: resolves to ${routing.displayName}`)); console.log(dim(` unprefixed: resolves to ${routing.displayName}`));
continue; continue;
+18 -7
View File
@@ -10,9 +10,10 @@ import * as path from 'path';
import { getProviderAccounts } from '../../cliproxy/account-manager'; import { getProviderAccounts } from '../../cliproxy/account-manager';
import { triggerOAuth } from '../../cliproxy/auth/oauth-handler'; import { triggerOAuth } from '../../cliproxy/auth/oauth-handler';
import { CLIProxyProfileName, CLIPROXY_PROFILES } from '../../auth/profile-detector'; import { CLIProxyProfileName, CLIPROXY_PROFILES } from '../../auth/profile-detector';
import { getCatalogRoutingSnapshot } from '../../cliproxy/catalog-routing';
import { supportsModelConfig, getProviderCatalog, ModelEntry } from '../../cliproxy/model-catalog'; import { supportsModelConfig, getProviderCatalog, ModelEntry } from '../../cliproxy/model-catalog';
import { ensureManagedModelPrefixes } from '../../cliproxy/managed-model-prefixes'; 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 { CLIProxyProvider, CLIProxyBackend } from '../../cliproxy/types';
import type { TargetType } from '../../targets/target-adapter'; import type { TargetType } from '../../targets/target-adapter';
import { getPersistedTargetChoices, isPersistedTargetType } from '../../targets/target-metadata'; import { getPersistedTargetChoices, isPersistedTargetType } from '../../targets/target-metadata';
@@ -117,9 +118,14 @@ function formatModelOption(model: ModelEntry): string {
return `${model.name}${tierBadge}`; return `${model.name}${tierBadge}`;
} }
function getSelectableModelId(provider: CLIProxyProvider, modelId: string): string { function getSelectableModelId(
const prefix = getManagedModelPrefix(provider); modelId: string,
return prefix ? `${prefix}/${modelId}` : modelId; 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 { function getBackendLabel(backend: CLIProxyBackend): string {
@@ -186,10 +192,11 @@ async function selectTierConfig(
} catch { } catch {
// Keep interactive model selection available even when prefix repair fails. // Keep interactive model selection available even when prefix repair fails.
} }
const routing = (await getCatalogRoutingSnapshot()).routing[provider as CLIProxyProvider];
const catalog = getProviderCatalog(provider as CLIProxyProvider); const catalog = getProviderCatalog(provider as CLIProxyProvider);
if (catalog) { if (catalog) {
const modelOptions = catalog.models.map((m) => ({ const modelOptions = catalog.models.map((m) => ({
id: getSelectableModelId(provider as CLIProxyProvider, m.id), id: getSelectableModelId(m.id, routing),
label: formatModelOption(m), label: formatModelOption(m),
})); }));
const defaultIdx = catalog.models.findIndex((m) => m.id === catalog.defaultModel); const defaultIdx = catalog.models.findIndex((m) => m.id === catalog.defaultModel);
@@ -446,10 +453,11 @@ export async function handleCreate(
} catch { } catch {
// Keep variant creation available even when prefix repair fails. // Keep variant creation available even when prefix repair fails.
} }
const routing = (await getCatalogRoutingSnapshot()).routing[provider as CLIProxyProvider];
const catalog = getProviderCatalog(provider as CLIProxyProvider); const catalog = getProviderCatalog(provider as CLIProxyProvider);
if (catalog) { if (catalog) {
const modelOptions = catalog.models.map((m) => ({ const modelOptions = catalog.models.map((m) => ({
id: getSelectableModelId(provider as CLIProxyProvider, m.id), id: getSelectableModelId(m.id, routing),
label: formatModelOption(m), label: formatModelOption(m),
})); }));
const defaultIdx = catalog.models.findIndex((m) => m.id === catalog.defaultModel); const defaultIdx = catalog.models.findIndex((m) => m.id === catalog.defaultModel);
@@ -695,10 +703,13 @@ export async function handleEdit(
} catch { } catch {
// Keep edit flow available even when prefix repair fails. // Keep edit flow available even when prefix repair fails.
} }
const routing = (await getCatalogRoutingSnapshot()).routing[
providerForModel as CLIProxyProvider
];
const catalog = getProviderCatalog(providerForModel as CLIProxyProvider); const catalog = getProviderCatalog(providerForModel as CLIProxyProvider);
if (catalog) { if (catalog) {
const modelOptions = catalog.models.map((m) => ({ const modelOptions = catalog.models.map((m) => ({
id: getSelectableModelId(providerForModel as CLIProxyProvider, m.id), id: getSelectableModelId(m.id, routing),
label: formatModelOption(m), label: formatModelOption(m),
})); }));
const defaultIdx = catalog.models.findIndex((m) => m.id === catalog.defaultModel); const defaultIdx = catalog.models.findIndex((m) => m.id === catalog.defaultModel);
+18 -3
View File
@@ -29,6 +29,7 @@ export interface CliproxyModelRoutingHint {
prefix: string; prefix: string;
pinnedModelId: string; pinnedModelId: string;
recommendedModelId: string; recommendedModelId: string;
pinnedAvailable: boolean;
unprefixedStatus: ModelRoutingStatus; unprefixedStatus: ModelRoutingStatus;
effectiveProvider: string | null; effectiveProvider: string | null;
effectiveDisplayName: string | null; effectiveDisplayName: string | null;
@@ -108,9 +109,13 @@ function buildSummary(
providerDisplayName: string, providerDisplayName: string,
hint: Pick< hint: Pick<
CliproxyModelRoutingHint, CliproxyModelRoutingHint,
'modelId' | 'pinnedModelId' | 'unprefixedStatus' | 'effectiveDisplayName' 'modelId' | 'pinnedModelId' | 'pinnedAvailable' | 'unprefixedStatus' | 'effectiveDisplayName'
> >
): string { ): 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') { if (hint.unprefixedStatus === 'safe') {
return `${hint.modelId} currently resolves to ${providerDisplayName}. Use ${hint.pinnedModelId} to keep it pinned.`; return `${hint.modelId} currently resolves to ${providerDisplayName}. Use ${hint.pinnedModelId} to keep it pinned.`;
} }
@@ -151,6 +156,15 @@ export function buildCliproxyRoutingHints(
let prefixOnlyCount = 0; let prefixOnlyCount = 0;
const models = catalog.models.map((model) => { 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 mergedModel = mergedModelMap.get(normalize(model.id));
const effectiveProvider = mergedModel ? inferProvider(mergedModel) : null; const effectiveProvider = mergedModel ? inferProvider(mergedModel) : null;
const effectiveDisplayName = const effectiveDisplayName =
@@ -173,8 +187,9 @@ export function buildCliproxyRoutingHints(
modelId: model.id, modelId: model.id,
modelName: model.name?.trim() || model.id, modelName: model.name?.trim() || model.id,
prefix, prefix,
pinnedModelId: `${prefix}/${model.id}`, pinnedModelId: managedPinnedId,
recommendedModelId: `${prefix}/${model.id}`, recommendedModelId,
pinnedAvailable: pinnedCandidates.length > 0,
unprefixedStatus, unprefixedStatus,
effectiveProvider, effectiveProvider,
effectiveDisplayName, effectiveDisplayName,
+10
View File
@@ -13,6 +13,8 @@ import { WebSocketServer } from 'ws';
import { setupWebSocket } from './websocket'; import { setupWebSocket } from './websocket';
import { createSessionMiddleware, authMiddleware } from './middleware/auth-middleware'; import { createSessionMiddleware, authMiddleware } from './middleware/auth-middleware';
import { requestLoggingMiddleware } from './middleware/request-logging-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 { startAutoSyncWatcher, stopAutoSyncWatcher } from '../cliproxy/sync';
import { shutdownUsageAggregator } from './usage/aggregator'; import { shutdownUsageAggregator } from './usage/aggregator';
import { createLogger } from '../services/logging'; import { createLogger } from '../services/logging';
@@ -119,6 +121,14 @@ export async function startServer(options: ServerOptions): Promise<ServerInstanc
// Start auto-sync watcher (if enabled in config) // Start auto-sync watcher (if enabled in config)
startAutoSyncWatcher(); startAutoSyncWatcher();
if (!getProxyTarget().isRemote) {
void ensureManagedModelPrefixes().catch((error) => {
logger.warn('cliproxy.prefix_sync_failed', 'Managed model prefix repair failed', {
error: error instanceof Error ? error.message : String(error),
});
});
}
// Combined cleanup function // Combined cleanup function
const cleanup = () => { const cleanup = () => {
wsCleanup(); wsCleanup();
@@ -316,12 +316,6 @@ export function getStartAuthNicknameError(
*/ */
router.get('/', async (_req: Request, res: Response): Promise<void> => { router.get('/', async (_req: Request, res: Response): Promise<void> => {
try { try {
try {
await ensureManagedModelPrefixes();
} catch {
// Keep auth status available even when prefix repair cannot run.
}
// Check if remote mode is enabled // Check if remote mode is enabled
const target = getProxyTarget(); const target = getProxyTarget();
if (target.isRemote) { if (target.isRemote) {
@@ -393,12 +387,6 @@ router.get('/', async (_req: Request, res: Response): Promise<void> => {
*/ */
router.get('/accounts', async (_req: Request, res: Response): Promise<void> => { router.get('/accounts', async (_req: Request, res: Response): Promise<void> => {
try { try {
try {
await ensureManagedModelPrefixes();
} catch {
// Non-fatal: account listing should still work without prefix repair.
}
// Check if remote mode is enabled // Check if remote mode is enabled
const target = getProxyTarget(); const target = getProxyTarget();
if (target.isRemote) { if (target.isRemote) {
@@ -30,6 +30,7 @@ describe('cliproxy model routing hints', () => {
expect(routing.gemini?.models[0]).toMatchObject({ expect(routing.gemini?.models[0]).toMatchObject({
recommendedModelId: 'gcli/gemini-3-flash-preview', recommendedModelId: 'gcli/gemini-3-flash-preview',
pinnedAvailable: false,
unprefixedStatus: 'shadowed', unprefixedStatus: 'shadowed',
effectiveProvider: 'agy', effectiveProvider: 'agy',
effectiveDisplayName: 'Antigravity', effectiveDisplayName: 'Antigravity',
@@ -37,6 +38,7 @@ describe('cliproxy model routing hints', () => {
expect(routing.agy?.models[0]).toMatchObject({ expect(routing.agy?.models[0]).toMatchObject({
recommendedModelId: 'agy/gemini-3-flash', recommendedModelId: 'agy/gemini-3-flash',
pinnedAvailable: false,
unprefixedStatus: 'safe', unprefixedStatus: 'safe',
effectiveProvider: 'agy', effectiveProvider: 'agy',
}); });
@@ -57,6 +59,7 @@ describe('cliproxy model routing hints', () => {
expect(routing.gemini?.prefixOnlyCount).toBe(1); expect(routing.gemini?.prefixOnlyCount).toBe(1);
expect(routing.gemini?.models[0]).toMatchObject({ expect(routing.gemini?.models[0]).toMatchObject({
recommendedModelId: 'gcli/gemini-3.1-pro-preview', recommendedModelId: 'gcli/gemini-3.1-pro-preview',
pinnedAvailable: false,
unprefixedStatus: 'prefix-only', unprefixedStatus: 'prefix-only',
effectiveProvider: null, effectiveProvider: null,
}); });
@@ -20,6 +20,25 @@ import { FlexibleModelSelector } from '../provider-model-selector';
import type { CustomPresetDialogProps, ModelMappingValues } from './types'; import type { CustomPresetDialogProps, ModelMappingValues } from './types';
import { useTranslation } from 'react-i18next'; 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({ export function CustomPresetDialog({
open, open,
onClose, onClose,
@@ -32,13 +51,15 @@ export function CustomPresetDialog({
routing, routing,
}: CustomPresetDialogProps) { }: CustomPresetDialogProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const [values, setValues] = useState<ModelMappingValues>(currentValues); const [values, setValues] = useState<ModelMappingValues>(
normalizePresetValues(currentValues, routing)
);
const [presetName, setPresetName] = useState(''); const [presetName, setPresetName] = useState('');
// Reset values when dialog opens with current values // Reset values when dialog opens with current values
const handleOpenChange = (isOpen: boolean) => { const handleOpenChange = (isOpen: boolean) => {
if (isOpen) { if (isOpen) {
setValues(currentValues); setValues(normalizePresetValues(currentValues, routing));
setPresetName(''); setPresetName('');
} else { } else {
onClose(); onClose();
@@ -53,13 +53,16 @@ export function ModelConfigSection({
onDeletePreset, onDeletePreset,
isDeletePending, isDeletePending,
}: ModelConfigSectionProps) { }: ModelConfigSectionProps) {
const pinningReady = (routing?.models ?? []).some((hint) => hint.pinnedAvailable);
const routingHintMap = useMemo( const routingHintMap = useMemo(
() => () =>
new Map((routing?.models ?? []).map((hint) => [hint.modelId.toLowerCase(), hint] as const)), new Map((routing?.models ?? []).map((hint) => [hint.modelId.toLowerCase(), hint] as const)),
[routing] [routing]
); );
const toPreferredModelId = (modelId: string): string => 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(() => { const extendedContextModels = useMemo(() => {
if (!catalog) return []; if (!catalog) return [];
@@ -160,10 +163,10 @@ export function ModelConfigSection({
className="text-xs h-7 gap-1 pr-6" className="text-xs h-7 gap-1 pr-6"
onClick={() => { onClick={() => {
onApplyPreset({ onApplyPreset({
ANTHROPIC_MODEL: preset.default, ANTHROPIC_MODEL: toPreferredModelId(preset.default),
ANTHROPIC_DEFAULT_OPUS_MODEL: preset.opus, ANTHROPIC_DEFAULT_OPUS_MODEL: toPreferredModelId(preset.opus),
ANTHROPIC_DEFAULT_SONNET_MODEL: preset.sonnet, ANTHROPIC_DEFAULT_SONNET_MODEL: toPreferredModelId(preset.sonnet),
ANTHROPIC_DEFAULT_HAIKU_MODEL: preset.haiku, ANTHROPIC_DEFAULT_HAIKU_MODEL: toPreferredModelId(preset.haiku),
}); });
}} }}
> >
@@ -209,8 +212,17 @@ export function ModelConfigSection({
</p> </p>
{routing ? ( {routing ? (
<p className="text-[11px] text-muted-foreground mb-3 rounded-md border bg-muted/30 px-2.5 py-2"> <p className="text-[11px] text-muted-foreground mb-3 rounded-md border bg-muted/30 px-2.5 py-2">
Preferred pinned model names use the <code>{routing.prefix}/</code> prefix. Unprefixed {pinningReady ? (
names can still resolve to a different backend when providers overlap. <>
Preferred pinned model names use the <code>{routing.prefix}/</code> prefix.
Unprefixed names can still resolve to a different backend when providers overlap.
</>
) : (
<>
Managed pinning for <code>{routing.prefix}/</code> is not currently advertised by
the proxy. Unprefixed names may still be ambiguous until prefix repair completes.
</>
)}
</p> </p>
) : null} ) : null}
{provider === 'codex' && ( {provider === 'codex' && (
@@ -306,6 +306,16 @@ function normalizeModelValue(
return value.startsWith(prefix) ? value.slice(prefix.length) : value; 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({ export function FlexibleModelSelector({
label, label,
description, description,
@@ -334,16 +344,16 @@ export function FlexibleModelSelector({
); );
const recommendedOptions = resolvedCatalogModels.map((model) => ({ 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', groupKey: 'recommended',
searchText: `${model.id} ${model.name} ${routingHints.get(model.id.toLowerCase())?.recommendedModelId ?? ''}`, searchText: `${model.id} ${model.name} ${routingHints.get(model.id.toLowerCase())?.recommendedModelId ?? ''}`,
keywords: [model.tier ?? '', catalog?.provider ?? ''], keywords: [model.tier ?? '', catalog?.provider ?? ''],
triggerContent: ( triggerContent: (
<div className="flex min-w-0 items-center gap-2"> <div className="flex min-w-0 items-center gap-2">
<span className="truncate font-mono text-xs"> <span className="truncate font-mono text-xs">
{routingHints.get(model.id.toLowerCase())?.recommendedModelId ?? model.id} {getPreferredOptionValue(model.id, routingHints.get(model.id.toLowerCase()))}
</span> </span>
{routingHints.get(model.id.toLowerCase()) ? ( {routingHints.get(model.id.toLowerCase())?.pinnedAvailable ? (
<Badge variant="secondary" className="text-[9px] h-4 px-1 uppercase"> <Badge variant="secondary" className="text-[9px] h-4 px-1 uppercase">
{routingHints.get(model.id.toLowerCase())?.prefix} {routingHints.get(model.id.toLowerCase())?.prefix}
</Badge> </Badge>
@@ -354,7 +364,7 @@ export function FlexibleModelSelector({
itemContent: ( itemContent: (
<div className="flex min-w-0 items-center gap-2"> <div className="flex min-w-0 items-center gap-2">
<span className="truncate font-mono text-xs"> <span className="truncate font-mono text-xs">
{routingHints.get(model.id.toLowerCase())?.recommendedModelId ?? model.id} {getPreferredOptionValue(model.id, routingHints.get(model.id.toLowerCase()))}
</span> </span>
{model.tier === 'paid' && <PaidBadge label={t('providerModelSelector.paid')} />} {model.tier === 'paid' && <PaidBadge label={t('providerModelSelector.paid')} />}
{routingHints.get(model.id.toLowerCase())?.unprefixedStatus === 'shadowed' ? ( {routingHints.get(model.id.toLowerCase())?.unprefixedStatus === 'shadowed' ? (
@@ -375,19 +385,38 @@ export function FlexibleModelSelector({
const allModelOptions = allModels const allModelOptions = allModels
.filter((model) => !catalogModelIds.has(model.id)) .filter((model) => !catalogModelIds.has(model.id))
.map((model) => ({ .map((model) => ({
value: model.id, value: getPreferredOptionValue(model.id, routingHints.get(model.id.toLowerCase())),
groupKey: 'all', groupKey: 'all',
searchText: model.id, searchText: `${model.id} ${routingHints.get(model.id.toLowerCase())?.recommendedModelId ?? ''}`,
keywords: [model.owned_by], keywords: [model.owned_by],
triggerContent: ( triggerContent: (
<div className="flex min-w-0 items-center gap-2"> <div className="flex min-w-0 items-center gap-2">
<span className="truncate font-mono text-xs">{model.id}</span> <span className="truncate font-mono text-xs">
{getPreferredOptionValue(model.id, routingHints.get(model.id.toLowerCase()))}
</span>
{routingHints.get(model.id.toLowerCase())?.pinnedAvailable ? (
<Badge variant="secondary" className="text-[9px] h-4 px-1 uppercase">
{routingHints.get(model.id.toLowerCase())?.prefix}
</Badge>
) : null}
{isCodexProvider && <CodexEffortBadge modelId={model.id} />} {isCodexProvider && <CodexEffortBadge modelId={model.id} />}
</div> </div>
), ),
itemContent: ( itemContent: (
<div className="flex min-w-0 items-center gap-2"> <div className="flex min-w-0 items-center gap-2">
<span className="truncate font-mono text-xs">{model.id}</span> <span className="truncate font-mono text-xs">
{getPreferredOptionValue(model.id, routingHints.get(model.id.toLowerCase()))}
</span>
{routingHints.get(model.id.toLowerCase())?.unprefixedStatus === 'shadowed' ? (
<Badge variant="outline" className="text-[9px] h-4 px-1">
Shadowed
</Badge>
) : null}
{routingHints.get(model.id.toLowerCase())?.unprefixedStatus === 'prefix-only' ? (
<Badge variant="outline" className="text-[9px] h-4 px-1">
Prefix only
</Badge>
) : null}
{isCodexProvider && <CodexEffortBadge modelId={model.id} />} {isCodexProvider && <CodexEffortBadge modelId={model.id} />}
</div> </div>
), ),
@@ -479,14 +508,21 @@ export function FlexibleModelSelector({
)} )}
> >
<div className="font-medium"> <div className="font-medium">
Preferred pinned model: <code>{selectedRoutingHint.recommendedModelId}</code> {selectedRoutingHint.pinnedAvailable
? 'Preferred pinned model:'
: 'Pinned route status:'}{' '}
<code>
{selectedRoutingHint.pinnedAvailable
? selectedRoutingHint.recommendedModelId
: selectedRoutingHint.pinnedModelId}
</code>
</div> </div>
<p className="mt-1 leading-5">{selectedRoutingHint.summary}</p> <p className="mt-1 leading-5">{selectedRoutingHint.summary}</p>
</div> </div>
) : null} ) : null}
{value && !selectedRoutingHint && normalizeModelValue(value, routing) !== value ? ( {value && !selectedRoutingHint && normalizeModelValue(value, routing) !== value ? (
<div className="rounded-md border border-border/70 bg-muted/25 px-2.5 py-2 text-[11px] text-muted-foreground"> <div className="rounded-md border border-amber-300/60 bg-amber-50 px-2.5 py-2 text-[11px] text-amber-900 dark:border-amber-500/30 dark:bg-amber-950/25 dark:text-amber-100">
Using pinned model: <code>{value}</code> Pinned model is not currently advertised by the proxy: <code>{value}</code>
</div> </div>
) : null} ) : null}
</div> </div>
+1
View File
@@ -474,6 +474,7 @@ export interface CliproxyModelRoutingHint {
prefix: string; prefix: string;
pinnedModelId: string; pinnedModelId: string;
recommendedModelId: string; recommendedModelId: string;
pinnedAvailable: boolean;
unprefixedStatus: 'safe' | 'shadowed' | 'prefix-only'; unprefixedStatus: 'safe' | 'shadowed' | 'prefix-only';
effectiveProvider: string | null; effectiveProvider: string | null;
effectiveDisplayName: string | null; effectiveDisplayName: string | null;
@@ -137,6 +137,7 @@ describe('ModelConfigSection presets', () => {
prefix: 'gcli', prefix: 'gcli',
pinnedModelId: 'gcli/gemini-3.1-pro-preview', pinnedModelId: 'gcli/gemini-3.1-pro-preview',
recommendedModelId: 'gcli/gemini-3.1-pro-preview', recommendedModelId: 'gcli/gemini-3.1-pro-preview',
pinnedAvailable: true,
unprefixedStatus: 'shadowed', unprefixedStatus: 'shadowed',
effectiveProvider: 'agy', effectiveProvider: 'agy',
effectiveDisplayName: 'Antigravity', effectiveDisplayName: 'Antigravity',
@@ -149,6 +150,7 @@ describe('ModelConfigSection presets', () => {
prefix: 'gcli', prefix: 'gcli',
pinnedModelId: 'gcli/gemini-3-flash-preview', pinnedModelId: 'gcli/gemini-3-flash-preview',
recommendedModelId: 'gcli/gemini-3-flash-preview', recommendedModelId: 'gcli/gemini-3-flash-preview',
pinnedAvailable: true,
unprefixedStatus: 'shadowed', unprefixedStatus: 'shadowed',
effectiveProvider: 'agy', effectiveProvider: 'agy',
effectiveDisplayName: 'Antigravity', effectiveDisplayName: 'Antigravity',
@@ -175,4 +177,79 @@ describe('ModelConfigSection presets', () => {
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'gcli/gemini-3-flash-preview', 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(
<ModelConfigSection
catalog={MODEL_CATALOGS.gemini}
savedPresets={[
{
name: 'legacy',
default: 'gemini-3-flash-preview',
opus: 'gemini-3.1-pro-preview',
sonnet: 'gemini-3.1-pro-preview',
haiku: 'gemini-3-flash-preview',
},
]}
currentModel="gemini-3-flash-preview"
opusModel="gemini-3.1-pro-preview"
sonnetModel="gemini-3.1-pro-preview"
haikuModel="gemini-3-flash-preview"
providerModels={[]}
routing={{
provider: 'gemini',
displayName: 'Gemini',
prefix: 'gcli',
safeCount: 0,
shadowedCount: 2,
prefixOnlyCount: 0,
models: [
{
modelId: 'gemini-3.1-pro-preview',
modelName: 'Gemini Pro',
prefix: 'gcli',
pinnedModelId: 'gcli/gemini-3.1-pro-preview',
recommendedModelId: 'gcli/gemini-3.1-pro-preview',
pinnedAvailable: true,
unprefixedStatus: 'shadowed',
effectiveProvider: 'agy',
effectiveDisplayName: 'Antigravity',
effectiveOwnedBy: 'antigravity',
summary: 'shadowed by Antigravity',
},
{
modelId: 'gemini-3-flash-preview',
modelName: 'Gemini Flash',
prefix: 'gcli',
pinnedModelId: 'gcli/gemini-3-flash-preview',
recommendedModelId: 'gcli/gemini-3-flash-preview',
pinnedAvailable: true,
unprefixedStatus: 'shadowed',
effectiveProvider: 'agy',
effectiveDisplayName: 'Antigravity',
effectiveOwnedBy: 'antigravity',
summary: 'shadowed by Antigravity',
},
],
}}
provider="gemini"
onExtendedContextToggle={vi.fn()}
onApplyPreset={onApplyPreset}
onUpdateEnvValue={vi.fn()}
onOpenCustomPreset={vi.fn()}
onDeletePreset={vi.fn()}
/>
);
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',
});
});
}); });