diff --git a/src/pages/ApiAccessPage.tsx b/src/pages/ApiAccessPage.tsx index 8f1a36a..821c18f 100644 --- a/src/pages/ApiAccessPage.tsx +++ b/src/pages/ApiAccessPage.tsx @@ -372,18 +372,34 @@ export const stripResponseFields = (record: Record) => { const mergeModelRecords = (current: unknown, selected: ModelOption[]) => { const existing = Array.isArray(current) ? current : []; + const consumedExistingIndexes = new Set(); + const selectedNames = new Set(); const seen = new Set(); - return selected.reduce[]>((models, model) => { + const models = selected.reduce[]>((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 = 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 = ( diff --git a/tests/providerConfig.test.ts b/tests/providerConfig.test.ts index 835c7c0..ab8d206 100644 --- a/tests/providerConfig.test.ts +++ b/tests/providerConfig.test.ts @@ -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']); });