fix(api-access): preserve model aliases when saving providers

Keep same-source alias entries and their metadata when rebuilding provider models, preventing API connection edits from deleting aliases created in Advanced Settings.

Fixes #271
This commit is contained in:
lzt404 committed 2026-09-18 18:17:31 +08:00
1 parent 911a20a91a
commit 93525f0ac7
2 files changed
+66 -8

No files matched your search

+35 -8
View File
@@ -372,18 +372,34 @@ export const stripResponseFields = (record: Record<string, unknown>) => {
const mergeModelRecords = (current: unknown, selected: ModelOption[]) => {
const existing = Array.isArray(current) ? current : [];
const consumedExistingIndexes = new Set<number>();
const selectedNames = new Set<string>();
const seen = new Set<string>();
return selected.reduce<Record<string, unknown>[]>((models, model) => {
const models = selected.reduce<Record<string, unknown>[]>((result, model) => {
const name = model.name.trim();
const key = name.toLowerCase();
if (!name || seen.has(key)) return models;
if (!name || seen.has(key)) return result;
seen.add(key);
const matched = existing.find(
(item) => isRecord(item) && readString(item, 'name').toLowerCase() === name.toLowerCase(),
);
selectedNames.add(key);
const requested = (model.alias ?? '').trim();
const requestedAlias = requested.toLowerCase();
let matchedIndex = existing.findIndex((item, index) => (
!consumedExistingIndexes.has(index)
&& isRecord(item)
&& readString(item, 'name').toLowerCase() === key
&& readString(item, 'alias').toLowerCase() === requestedAlias
));
if (matchedIndex < 0) {
matchedIndex = existing.findIndex((item, index) => (
!consumedExistingIndexes.has(index)
&& isRecord(item)
&& readString(item, 'name').toLowerCase() === key
));
}
if (matchedIndex >= 0) consumedExistingIndexes.add(matchedIndex);
const matched = matchedIndex >= 0 ? existing[matchedIndex] : undefined;
const next: Record<string, unknown> = isRecord(matched) ? { ...matched } : {};
next.name = name;
const requested = (model.alias ?? '').trim();
const storedAlias = readString(next, 'alias');
const alias = usableModelAlias(requested);
if (alias && alias !== name) {
@@ -401,9 +417,20 @@ const mergeModelRecords = (current: unknown, selected: ModelOption[]) => {
delete next.alias;
}
if (model.thinking) next.thinking = { ...model.thinking };
models.push(next);
return models;
result.push(next);
return result;
}, []);
existing.forEach((item, index) => {
if (
consumedExistingIndexes.has(index)
|| !isRecord(item)
|| !selectedNames.has(readString(item, 'name').toLowerCase())
) return;
models.push({ ...item });
});
return models;
};
export const exclusionsForModelSelection = (
+31
View File
@@ -17,6 +17,7 @@ import {
sectionRecordsFromConfig,
stripResponseFields,
} from '../src/pages/ApiAccessPage';
import { modelsFromRecord } from '../src/services/modelService';
it('saves non-empty custom model names and removes duplicate or blank entries', () => {
const result = buildProviderRecord('openai-compatibility', {
@@ -104,6 +105,36 @@ it('keeps an existing spaced alias when only the letter case changes', () => {
]);
});
it('preserves aliases added in advanced settings when saving an API connection', () => {
const current = {
'api-key': 'codex-key',
'base-url': 'https://foobar.com/v1',
headers: { 'User-Agent': '$User-Agent' },
models: [
{ name: 'gpt-5.6-luna' },
{
name: 'gpt-5.6-luna',
alias: 'claude-sonnet-5-luna',
custom: { keep: true },
},
],
};
const result = buildProviderRecord(
'codex-api-key',
{
name: '',
apiKey: 'codex-key',
baseUrl: 'https://foobar.com/v1',
priority: '',
models: modelsFromRecord(current.models),
headersText: 'User-Agent: $User-Agent',
},
current,
);
expect(result.models).toEqual(current.models);
});
it('parses multiline API keys into unique trimmed entries', () => {
expect(parseProviderApiKeys(' key-a\n\nkey-b\r\nkey-a ')).toEqual(['key-a', 'key-b']);
});