fix(ui): preserve requested provider model aliases

- keep requested=upstream semantics stable in the AI Providers editors

- preview the client-visible ANTHROPIC_MODEL instead of the upstream target

- add UI coverage for alias parsing and formatting helpers

Refs #941
This commit is contained in:
Tam Nhu Tran
2026-04-10 17:28:18 -04:00
parent 270810c06e
commit 49c4338f28
4 changed files with 105 additions and 42 deletions
@@ -1,6 +1,10 @@
import { useMemo, useState } from 'react';
import { ProviderLogo } from '@/components/cliproxy/provider-logo';
import { getAiProviderFamilyVisual } from '@/lib/provider-config';
import {
formatRequestedUpstreamModelRules,
getAiProviderFamilyVisual,
parseRequestedUpstreamModelRules,
} from '@/lib/provider-config';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
@@ -192,22 +196,7 @@ function parseKeyValueLines(value: string): Array<{ key: string; value: string }
}
function parseModelAliasLines(value: string) {
return value
.split('\n')
.map((line) => line.trim())
.filter((line) => line.length > 0)
.map((line) => {
const separatorIndex = line.indexOf('=');
if (separatorIndex === -1) {
return { name: line.trim(), alias: '' };
}
return {
name: line.slice(0, separatorIndex).trim(),
alias: line.slice(separatorIndex + 1).trim(),
};
})
.filter((item) => item.name.length > 0 || item.alias.length > 0);
return parseRequestedUpstreamModelRules(value);
}
function TextArea({
@@ -241,9 +230,7 @@ function formatExcludedModels(entry?: AiProviderEntryView | null): string {
}
function formatModelAliases(entry?: AiProviderEntryView | null): string {
return (entry?.models || [])
.map((item) => (item.alias.trim() ? `${item.name}=${item.alias}` : item.name))
.join('\n');
return formatRequestedUpstreamModelRules(entry?.models);
}
function ChecklistCard({
+53 -1
View File
@@ -9,7 +9,7 @@ import {
PROVIDER_CAPABILITIES,
getProvidersByOAuthFlow,
} from '../../../src/cliproxy/provider-capabilities';
import type { AiProviderFamilyId } from '../../../src/cliproxy/ai-providers';
import type { AiProviderFamilyId, AiProviderModelAlias } from '../../../src/cliproxy/ai-providers';
// Monorepo contract: UI consumes provider capability constants directly from backend
// to enforce one source of truth and prevent provider drift across surfaces.
@@ -115,6 +115,58 @@ export function getAiProviderFamilyVisual(familyId: AiProviderFamilyId): Provide
}
}
/**
* Parse UI model rules that use the requested=upstream convention into the
* provider config shape where `name` is upstream and `alias` is client-visible.
*/
export function parseRequestedUpstreamModelRules(value: string): AiProviderModelAlias[] {
return value
.split('\n')
.map((line) => line.trim())
.filter((line) => line.length > 0)
.map((line) => {
const separatorIndex = line.indexOf('=');
if (separatorIndex === -1) {
return { name: line.trim(), alias: '' };
}
const requested = line.slice(0, separatorIndex).trim();
const upstream = line.slice(separatorIndex + 1).trim();
if (!upstream) {
return { name: requested, alias: '' };
}
return {
name: upstream,
alias: requested,
};
})
.filter((item) => item.name.length > 0 || item.alias.length > 0);
}
/**
* Format stored provider config back into the UI-facing requested=upstream form.
*/
export function formatRequestedUpstreamModelRules(
models: AiProviderModelAlias[] | null | undefined
): string {
return (models || [])
.map((item) => {
const requested = item.alias.trim();
const upstream = item.name.trim();
return requested ? `${requested}=${upstream}` : upstream;
})
.join('\n');
}
/**
* Return the client-visible model ID for previews and generated settings.
*/
export function getRequestedModelId(model: AiProviderModelAlias): string {
const requested = model.alias.trim();
return requested || model.name.trim();
}
export function getProviderLogoAsset(provider: unknown): string | undefined {
const normalized = normalizeProviderInput(provider);
if (!isProviderVisualId(normalized)) {
+9 -21
View File
@@ -17,7 +17,12 @@ import { Input } from '@/components/ui/input';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Skeleton } from '@/components/ui/skeleton';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { getAiProviderFamilyVisual } from '@/lib/provider-config';
import {
formatRequestedUpstreamModelRules,
getAiProviderFamilyVisual,
getRequestedModelId,
parseRequestedUpstreamModelRules,
} from '@/lib/provider-config';
import { cn } from '@/lib/utils';
import { FamilyRail, ProviderEntryDialog } from '@/components/cliproxy/ai-providers';
import {
@@ -339,22 +344,7 @@ function parseKeyValueLines(value: string): Array<{ key: string; value: string }
}
function parseModelAliasLines(value: string) {
return value
.split('\n')
.map((line) => line.trim())
.filter((line) => line.length > 0)
.map((line) => {
const separatorIndex = line.indexOf('=');
if (separatorIndex === -1) {
return { name: line.trim(), alias: '' };
}
return {
name: line.slice(0, separatorIndex).trim(),
alias: line.slice(separatorIndex + 1).trim(),
};
})
.filter((item) => item.name.length > 0 || item.alias.length > 0);
return parseRequestedUpstreamModelRules(value);
}
function formatHeaders(entry?: AiProviderEntryView | null): string {
@@ -366,9 +356,7 @@ function formatExcludedModels(entry?: AiProviderEntryView | null): string {
}
function formatModelAliases(entry?: AiProviderEntryView | null): string {
return (entry?.models || [])
.map((item) => (item.alias.trim() ? `${item.name}=${item.alias}` : item.name))
.join('\n');
return formatRequestedUpstreamModelRules(entry?.models);
}
function buildEntryEditorDraft(entry: AiProviderEntryView): EntryEditorDraft {
@@ -579,7 +567,7 @@ function buildSettingsPreview(
item.name.trim()
);
if (primaryModel?.name) {
env.ANTHROPIC_MODEL = primaryModel.name;
env.ANTHROPIC_MODEL = getRequestedModelId(primaryModel);
}
return { env };
@@ -0,0 +1,36 @@
import { describe, expect, it } from 'vitest';
import {
formatRequestedUpstreamModelRules,
getRequestedModelId,
parseRequestedUpstreamModelRules,
} from '@/lib/provider-config';
describe('provider model mapping helpers', () => {
it('parses requested=upstream rules into stored upstream+alias pairs', () => {
expect(
parseRequestedUpstreamModelRules('claude-sonnet-4-5=gpt-4.1\nminimax/minimax-m2.7')
).toEqual([
{ name: 'gpt-4.1', alias: 'claude-sonnet-4-5' },
{ name: 'minimax/minimax-m2.7', alias: '' },
]);
});
it('formats stored model rules back into requested=upstream text', () => {
expect(
formatRequestedUpstreamModelRules([
{ name: 'gpt-4.1', alias: 'claude-sonnet-4-5' },
{ name: 'minimax/minimax-m2.7', alias: '' },
])
).toBe('claude-sonnet-4-5=gpt-4.1\nminimax/minimax-m2.7');
});
it('prefers the requested alias for generated settings previews', () => {
expect(getRequestedModelId({ name: 'gpt-4.1', alias: 'claude-sonnet-4-5' })).toBe(
'claude-sonnet-4-5'
);
expect(getRequestedModelId({ name: 'minimax/minimax-m2.7', alias: '' })).toBe(
'minimax/minimax-m2.7'
);
});
});