mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 14:15:56 +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 parent
270810c06e
commit
49c4338f28
4 files changed
+105
-42
No files matched your search
@@ -1,6 +1,10 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { ProviderLogo } from '@/components/cliproxy/provider-logo';
|
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 { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
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) {
|
function parseModelAliasLines(value: string) {
|
||||||
return value
|
return parseRequestedUpstreamModelRules(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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function TextArea({
|
function TextArea({
|
||||||
@@ -241,9 +230,7 @@ function formatExcludedModels(entry?: AiProviderEntryView | null): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatModelAliases(entry?: AiProviderEntryView | null): string {
|
function formatModelAliases(entry?: AiProviderEntryView | null): string {
|
||||||
return (entry?.models || [])
|
return formatRequestedUpstreamModelRules(entry?.models);
|
||||||
.map((item) => (item.alias.trim() ? `${item.name}=${item.alias}` : item.name))
|
|
||||||
.join('\n');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ChecklistCard({
|
function ChecklistCard({
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
PROVIDER_CAPABILITIES,
|
PROVIDER_CAPABILITIES,
|
||||||
getProvidersByOAuthFlow,
|
getProvidersByOAuthFlow,
|
||||||
} from '../../../src/cliproxy/provider-capabilities';
|
} 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
|
// Monorepo contract: UI consumes provider capability constants directly from backend
|
||||||
// to enforce one source of truth and prevent provider drift across surfaces.
|
// 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 {
|
export function getProviderLogoAsset(provider: unknown): string | undefined {
|
||||||
const normalized = normalizeProviderInput(provider);
|
const normalized = normalizeProviderInput(provider);
|
||||||
if (!isProviderVisualId(normalized)) {
|
if (!isProviderVisualId(normalized)) {
|
||||||
|
|||||||
@@ -17,7 +17,12 @@ import { Input } from '@/components/ui/input';
|
|||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
import { Skeleton } from '@/components/ui/skeleton';
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
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 { cn } from '@/lib/utils';
|
||||||
import { FamilyRail, ProviderEntryDialog } from '@/components/cliproxy/ai-providers';
|
import { FamilyRail, ProviderEntryDialog } from '@/components/cliproxy/ai-providers';
|
||||||
import {
|
import {
|
||||||
@@ -339,22 +344,7 @@ function parseKeyValueLines(value: string): Array<{ key: string; value: string }
|
|||||||
}
|
}
|
||||||
|
|
||||||
function parseModelAliasLines(value: string) {
|
function parseModelAliasLines(value: string) {
|
||||||
return value
|
return parseRequestedUpstreamModelRules(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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatHeaders(entry?: AiProviderEntryView | null): string {
|
function formatHeaders(entry?: AiProviderEntryView | null): string {
|
||||||
@@ -366,9 +356,7 @@ function formatExcludedModels(entry?: AiProviderEntryView | null): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatModelAliases(entry?: AiProviderEntryView | null): string {
|
function formatModelAliases(entry?: AiProviderEntryView | null): string {
|
||||||
return (entry?.models || [])
|
return formatRequestedUpstreamModelRules(entry?.models);
|
||||||
.map((item) => (item.alias.trim() ? `${item.name}=${item.alias}` : item.name))
|
|
||||||
.join('\n');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildEntryEditorDraft(entry: AiProviderEntryView): EntryEditorDraft {
|
function buildEntryEditorDraft(entry: AiProviderEntryView): EntryEditorDraft {
|
||||||
@@ -579,7 +567,7 @@ function buildSettingsPreview(
|
|||||||
item.name.trim()
|
item.name.trim()
|
||||||
);
|
);
|
||||||
if (primaryModel?.name) {
|
if (primaryModel?.name) {
|
||||||
env.ANTHROPIC_MODEL = primaryModel.name;
|
env.ANTHROPIC_MODEL = getRequestedModelId(primaryModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { env };
|
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