mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 14:16:43 +00:00
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:
@@ -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({
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user