fix: allow provider entries with shared credentials

- Compare full configurations when checking for duplicates
- Keep edits, toggles, reordering, deletion, and remarks isolated
- Migrate existing remarks and add regression coverage

Fixes #276
This commit is contained in:
lzt404 committed 2026-09-19 20:42:30 +08:00
1 parent 327b824ef5
commit cb7c701f04
7 files changed
+408 -45

No files matched your search

+108 -15
View File
@@ -87,6 +87,10 @@ fn api_access_locator_identity(
identity.extend_from_slice(&(hash.len() as u64).to_be_bytes());
identity.extend_from_slice(hash.as_bytes());
}
if !locator.config_identity.is_empty() {
identity.extend_from_slice(&(locator.config_identity.len() as u64).to_be_bytes());
identity.extend_from_slice(locator.config_identity.as_bytes());
}
Some((sha256_bytes(&identity), api_key_hashes))
}
@@ -119,6 +123,12 @@ fn resolve_api_access_remark(
return Ok(exact_remark);
}
if !query.locator.config_identity.is_empty() {
let mut legacy_query = query.clone();
legacy_query.locator.config_identity.clear();
return resolve_api_access_remark(config, &legacy_query);
}
Ok(api_key_hashes
.iter()
.find_map(|hash| {
@@ -160,28 +170,30 @@ fn apply_api_access_remark_update(
.map(|(record_hash, _)| record_hash.clone())
.collect::<HashSet<_>>();
let provider_section = update.provider_section;
let legacy_entries = config
.api_access_remarks
let legacy_migrations = update
.all_records
.iter()
.filter(|entry| entry.provider_section == provider_section && entry.record_hash.is_empty())
.cloned()
.collect::<Vec<_>>();
let legacy_migrations = all_records
.iter()
.filter_map(|(record_hash, api_key_hashes)| {
let remark = api_key_hashes.iter().find_map(|api_key_hash| {
legacy_entries
.iter()
.find(|entry| entry.api_key_hash == *api_key_hash)
.map(|entry| entry.remark.clone())
})?;
Some((record_hash.clone(), api_key_hashes.clone(), remark))
.filter_map(|locator| {
let (record_hash, api_key_hashes) =
api_access_locator_identity(&provider_section, locator)?;
let remark = resolve_api_access_remark(
config,
&ApiAccessRemarkQuery {
provider_section: provider_section.clone(),
locator: locator.clone(),
},
)
.ok()?;
Some((record_hash, api_key_hashes, remark))
})
.collect::<Vec<_>>();
config.api_access_remarks.retain(|entry| {
entry.provider_section != provider_section
|| (!entry.record_hash.is_empty()
&& all_records
.iter()
.any(|(hash, _)| *hash == entry.record_hash)
&& !record_hashes_to_replace.contains(&entry.record_hash))
});
let mut inserted_record_hashes = config
@@ -236,6 +248,7 @@ mod tests {
provider_name: String::new(),
base_url: base_url.to_string(),
api_keys: api_keys.iter().map(|key| (*key).to_string()).collect(),
config_identity: String::new(),
}
}
@@ -399,6 +412,86 @@ mod tests {
.all(|entry| !entry.record_hash.is_empty()));
}
#[test]
fn shared_credentials_keep_separate_config_remarks_and_migrate_old_records() {
let mut config = GuiConfigFile::default();
let legacy = locator("https://api.example/v1", &["shared-key"]);
apply_api_access_remark_update(
&mut config,
update(
Vec::new(),
vec![legacy.clone()],
vec![legacy.clone()],
"original",
),
)
.unwrap();
let mut first = legacy.clone();
first.config_identity = r#"{"models":[{"name":"a"}],"priority":10}"#.into();
let mut second = legacy.clone();
second.config_identity = r#"{"models":[{"name":"b"}],"priority":1}"#.into();
assert_eq!(
resolve_api_access_remark(&config, &query(first.clone())).unwrap(),
"original"
);
apply_api_access_remark_update(
&mut config,
update(
Vec::new(),
vec![second.clone()],
vec![first.clone(), second.clone()],
"second",
),
)
.unwrap();
assert_eq!(
resolve_api_access_remark(&config, &query(first.clone())).unwrap(),
"original"
);
assert_eq!(
resolve_api_access_remark(&config, &query(second.clone())).unwrap(),
"second"
);
assert_eq!(config.api_access_remarks.len(), 2);
let mut edited = second.clone();
edited.config_identity = r#"{"models":[{"name":"edited"}],"priority":2}"#.into();
apply_api_access_remark_update(
&mut config,
update(
vec![second],
vec![edited.clone()],
vec![first.clone(), edited.clone()],
"",
),
)
.unwrap();
assert_eq!(
resolve_api_access_remark(&config, &query(edited.clone())).unwrap(),
""
);
assert_eq!(
resolve_api_access_remark(&config, &query(first.clone())).unwrap(),
"original"
);
apply_api_access_remark_update(
&mut config,
update(vec![edited], Vec::new(), vec![first.clone()], ""),
)
.unwrap();
assert_eq!(config.api_access_remarks.len(), 1);
assert_eq!(
resolve_api_access_remark(&config, &query(first)).unwrap(),
"original"
);
assert!(!toml::to_string(&config.api_access_remarks[0])
.unwrap()
.contains("shared-key"));
}
#[test]
fn legacy_remark_entries_deserialize_without_a_record_hash() {
let entry = toml::from_str::<GuiApiAccessRemark>(
+2
View File
@@ -876,6 +876,8 @@ struct ApiAccessRemarkLocator {
provider_name: String,
base_url: String,
api_keys: Vec<String>,
#[serde(default)]
config_identity: String,
}
#[derive(Clone, Debug, Deserialize)]
+86 -30
View File
@@ -116,10 +116,11 @@ export type ApiAccessRemarkLocator = {
providerName: string;
baseUrl: string;
apiKeys: string[];
configIdentity?: string;
};
const providerDragId = (
row: Pick<ProviderRow, 'section' | 'name' | 'apiKey' | 'baseUrl'>,
export const providerDragId = (
row: Pick<ProviderRow, 'section' | 'index' | 'name' | 'apiKey' | 'baseUrl'>,
) => {
const identity = `${row.section}\u0000${row.name}\u0000${row.apiKey}\u0000${row.baseUrl}`;
let hash = 2166136261;
@@ -127,7 +128,7 @@ const providerDragId = (
hash ^= identity.charCodeAt(index);
hash = Math.imul(hash, 16777619);
}
return `${row.section}:${(hash >>> 0).toString(36)}`;
return `${row.section}:${(hash >>> 0).toString(36)}:${row.index}`;
};
function SortableProviderRow({
@@ -330,7 +331,7 @@ const rowFromRecord = (
export const providerRemarkIdentity = (
section: ProviderSection,
locator: ApiAccessRemarkLocator,
) => JSON.stringify([section, locator.providerName, locator.baseUrl, locator.apiKeys]);
) => JSON.stringify([section, locator.providerName, locator.baseUrl, locator.apiKeys, locator.configIdentity ?? '']);
export const apiAccessRemarkLocatorFromRecord = (
section: ProviderSection,
@@ -341,14 +342,12 @@ export const apiAccessRemarkLocatorFromRecord = (
providerName: readString(record, 'name'),
baseUrl: row.baseUrl,
apiKeys: row.apiKeys,
configIdentity: providerRemarkConfigIdentity(record),
};
};
const apiAccessRemarkLocatorFromRow = (row: ProviderRow): ApiAccessRemarkLocator => ({
providerName: readString(row.record, 'name'),
baseUrl: row.baseUrl,
apiKeys: row.apiKeys,
});
const apiAccessRemarkLocatorFromRow = (row: ProviderRow): ApiAccessRemarkLocator =>
apiAccessRemarkLocatorFromRecord(row.section, row.record);
const providerHealthIdentity = (row: ProviderRow) => [
row.section,
@@ -396,6 +395,52 @@ export const stripResponseFields = (record: Record<string, unknown>) => {
return next;
};
const normalizeProviderIdentity = (value: unknown): unknown => {
if (value == null || value === '') return undefined;
if (Array.isArray(value)) return value.length ? value.map(normalizeProviderIdentity) : undefined;
if (isRecord(value)) {
const entries = Object.keys(value).sort().flatMap((key) => {
const normalized = normalizeProviderIdentity(value[key]);
return normalized === undefined ? [] : [[key, normalized]];
});
return entries.length ? Object.fromEntries(entries) : undefined;
}
return value;
};
const providerConfigIdentity = (record: Record<string, unknown>) => {
const config = stripResponseFields(record);
if (config.priority === 0) delete config.priority;
if (config.websockets === false) delete config.websockets;
if (config.disabled === false) delete config.disabled;
return JSON.stringify(normalizeProviderIdentity(config) ?? {});
};
const providerRemarkConfigIdentity = (record: Record<string, unknown>) => {
const config = stripResponseFields(record);
for (const key of ['name', 'api-key', 'apiKey', 'api-key-entries', 'base-url', 'baseUrl', 'disabled']) {
delete config[key];
}
if (Array.isArray(config['excluded-models'])) {
config['excluded-models'] = config['excluded-models'].filter((model) => String(model).trim() !== '*');
}
return providerConfigIdentity(config);
};
export const hasDuplicateProviderRecord = (
section: ProviderSection,
records: Record<string, unknown>[],
candidates: Record<string, unknown>[],
targetIndex = -1,
) => records.some((record, index) => index !== targetIndex && candidates.some((candidate) => {
if (definitionFor(section).openAi) return readString(record, 'name') === readString(candidate, 'name');
if (section === 'gemini-api-key') {
return readString(record, 'api-key', 'apiKey') === readString(candidate, 'api-key', 'apiKey')
&& readString(record, 'base-url', 'baseUrl') === readString(candidate, 'base-url', 'baseUrl');
}
return providerConfigIdentity(record) === providerConfigIdentity(candidate);
}));
const mergeModelRecords = (current: unknown, selected: ModelOption[]) => {
const existing = Array.isArray(current) ? current : [];
const consumedExistingIndexes = new Set<number>();
@@ -797,7 +842,7 @@ const providerIdentityMatches = (
export type ProviderRecordIdentity = Pick<
ProviderRow,
'section' | 'index' | 'name' | 'apiKey' | 'baseUrl'
>;
> & Partial<Pick<ProviderRow, 'record'>>;
const providerPrimaryIdentityMatches = (
row: ProviderRecordIdentity,
@@ -810,8 +855,29 @@ export const resolveProviderRecordIndex = (
records: Record<string, unknown>[],
row: ProviderRecordIdentity,
) => {
const exactIndex = records.findIndex((record) => providerIdentityMatches(row, record));
if (exactIndex >= 0) return exactIndex;
if (row.record) {
const identity = providerConfigIdentity(row.record);
const matches = records.flatMap((record, index) => (
providerConfigIdentity(record) === identity ? [index] : []
));
if (matches.length === 1) return matches[0];
if (matches.length > 1) return matches.includes(row.index) ? row.index : -1;
const withoutBaseUrl = (record: Record<string, unknown>) => {
const { 'base-url': _baseUrl, baseUrl: _camelBaseUrl, ...rest } = record;
return providerConfigIdentity(rest);
};
const defaultUrlMatches = records.flatMap((record, index) => (
providerPrimaryIdentityMatches(row, record)
&& (!readString(record, 'base-url', 'baseUrl') || !row.baseUrl)
&& withoutBaseUrl(record) === withoutBaseUrl(row.record!) ? [index] : []
));
return defaultUrlMatches.length === 1 ? defaultUrlMatches[0] : -1;
}
const exactMatches = records.flatMap((record, index) => providerIdentityMatches(row, record) ? [index] : []);
if (exactMatches.length === 1) return exactMatches[0];
if (exactMatches.length > 1) return -1;
const indexedRecord = records[row.index];
if (indexedRecord && providerPrimaryIdentityMatches(row, indexedRecord)) {
@@ -1106,16 +1172,6 @@ export function ApiAccessPage() {
currentRecord = current[targetIndex];
}
const duplicate = current.some((record, index) => {
if (index === targetIndex) return false;
if (definition.openAi) return readString(record, 'name') === preparedDraft.name.trim();
return parsedApiKeys.some((apiKey) => (
readString(record, 'api-key', 'apiKey').trim() === apiKey
&& readString(record, 'base-url', 'baseUrl').trim() === baseUrl
));
});
if (duplicate) throw new Error(t('apiAccess.error.duplicate'));
const recordsToSave = definition.openAi
? [buildProviderRecord(activeSection, draftToSave, currentRecord)]
: parsedApiKeys.map((apiKey) => buildProviderRecord(
@@ -1123,6 +1179,9 @@ export function ApiAccessPage() {
{ ...draftToSave, apiKey },
currentRecord,
));
if (hasDuplicateProviderRecord(activeSection, current, recordsToSave, targetIndex)) {
throw new Error(t('apiAccess.error.duplicate'));
}
nextList = editingRow
? [
...current.slice(0, targetIndex),
@@ -1161,15 +1220,12 @@ export function ApiAccessPage() {
setBusy(true);
setError('');
try {
if (definitionFor(row.section).openAi) {
await managementApi.delete('/openai-compatibility', { query: { name: row.name } });
} else {
await managementApi.delete(`/${row.section}`, {
query: { 'api-key': row.apiKey, 'base-url': row.baseUrl },
});
}
const latestConfig = await managementApi.get('/config');
const remainingRecords = sectionRecordsFromConfig(latestConfig, row.section);
const current = sectionRecordsFromConfig(latestConfig, row.section);
const targetIndex = resolveProviderRecordIndex(current, row);
if (targetIndex < 0) throw new Error(t('apiAccess.error.stale'));
const remainingRecords = current.filter((_, index) => index !== targetIndex).map(stripResponseFields);
await managementApi.put(`/${row.section}`, remainingRecords);
await invoke('save_api_access_remark', {
update: {
providerSection: row.section,
+1
View File
@@ -0,0 +1 @@
<html><body><div id="root"></div><script type="module" src="/tests/fixtures/provider-duplicates.tsx"></script></body></html>
+56
View File
@@ -0,0 +1,56 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import { mockIPC } from '@tauri-apps/api/mocks';
import { I18nProvider } from '../../src/i18n';
import { ApiAccessPage, providerRemarkIdentity, type ApiAccessRemarkLocator, type ProviderSection } from '../../src/pages/ApiAccessPage';
import '../../src/styles.css';
localStorage.setItem('easy-cli-proxy-api.locale', 'en');
const fixture = window as typeof window & {
providerFixture: { records: Record<string, unknown>[]; writes: { method: string; body: unknown }[] };
};
fixture.providerFixture = {
records: [{
'api-key': 'shared-test-key', 'base-url': 'https://claude.example.test', priority: 10,
models: [{ name: 'upstream-a', alias: 'claude-a' }],
}],
writes: [],
};
const remarks = new Map<string, string>();
mockIPC((cmd, rawArgs) => {
if (cmd === 'set_app_locale') return null;
if (cmd === 'resolve_api_access_remarks') {
const { queries } = rawArgs as { queries: (ApiAccessRemarkLocator & { providerSection: ProviderSection })[] };
return queries.map((query) => remarks.get(providerRemarkIdentity(query.providerSection, query)) ?? '');
}
if (cmd === 'save_api_access_remark') {
const { update } = rawArgs as { update: {
providerSection: ProviderSection; previousRecords: ApiAccessRemarkLocator[];
records: ApiAccessRemarkLocator[]; remark: string;
} };
for (const record of update.previousRecords) remarks.delete(providerRemarkIdentity(update.providerSection, record));
for (const record of update.records) remarks.set(providerRemarkIdentity(update.providerSection, record), update.remark);
return null;
}
if (cmd === 'management_request') {
const { request } = rawArgs as { request: { method: string; path: string; body: Record<string, unknown>[] } };
const section = request.path.slice(1);
if (request.method === 'GET') {
if (section === 'config') return { 'claude-api-key': structuredClone(fixture.providerFixture.records) };
return { [section]: section === 'claude-api-key'
? fixture.providerFixture.records.map((record, index) => ({ ...record, 'auth-index': `auth-${index}` }))
: [] };
}
fixture.providerFixture.writes.push({ method: request.method, body: structuredClone(request.body) });
if (request.method !== 'PUT' || section !== 'claude-api-key') throw new Error('Unexpected provider mutation');
fixture.providerFixture.records = structuredClone(request.body);
return { status: 'ok' };
}
throw new Error(`Unhandled fixture command: ${cmd}`);
});
createRoot(document.getElementById('root')!).render(
<I18nProvider><div className="app-shell"><div className="workspace"><main className="content">
<ApiAccessPage />
</main></div></div></I18nProvider>,
);
+74
View File
@@ -0,0 +1,74 @@
const { chromium } = require(process.env.PLAYWRIGHT_MODULE || 'playwright');
const assert = require('node:assert/strict');
const base = 'http://127.0.0.1:1421';
(async () => {
const browser = await chromium.launch({ channel: 'msedge', headless: true, args: ['--no-proxy-server'] });
try {
const page = await browser.newPage({ viewport: { width: 1280, height: 1000 } });
page.setDefaultTimeout(10000);
const errors = [];
page.on('pageerror', (error) => errors.push(String(error)));
page.on('console', (message) => { if (message.type() === 'error') errors.push(message.text()); });
await page.route('**/*', (route) => route.request().url().startsWith(`${base}/`) ? route.continue() : route.abort());
await page.goto(`${base}/tests/fixtures/provider-duplicates.html`, { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.locator('.provider-category-panel button').filter({ hasText: 'Claude' }).click();
const rows = page.locator('.real-provider-row');
const records = () => page.evaluate(() => window.providerFixture.records);
const form = page.locator('.api-provider-dialog');
const waitForSave = () => form.waitFor({ state: 'detached' });
await page.getByRole('button', { name: 'Add', exact: true }).click();
await form.getByLabel('API Keys (one per line)', { exact: true }).fill('shared-test-key');
await form.getByLabel('Base URL', { exact: true }).fill('https://claude.example.test');
await form.getByLabel('Priority', { exact: true }).fill('1');
await form.getByLabel('Remark', { exact: true }).fill('Second route');
await form.getByRole('button', { name: 'Add Custom Model', exact: true }).click();
await form.locator('.model-config-entry input').nth(0).fill('upstream-b');
await form.locator('.model-config-entry input').nth(1).fill('claude-b');
await form.getByRole('button', { name: 'Save', exact: true }).click();
await waitForSave();
assert.equal((await records()).length, 2, 'A shared key and URL can have a second model/priority route');
await rows.filter({ hasText: 'Second route' }).waitFor();
assert.equal(await rows.count(), 2);
await rows.filter({ hasText: 'Second route' }).getByRole('button', { name: 'Edit', exact: true }).click();
await form.getByLabel('Priority', { exact: true }).fill('2');
await form.locator('.model-config-entry input').nth(1).fill('claude-b-edited');
await page.evaluate(() => window.providerFixture.records.reverse());
await form.getByRole('button', { name: 'Save', exact: true }).click();
await waitForSave();
let saved = await records();
assert.equal(saved[0].priority, 2);
assert.equal(saved[0].models[0].alias, 'claude-b-edited');
assert.equal(saved[1].priority, 10);
assert.equal(saved[1].models[0].alias, 'claude-a');
await rows.filter({ hasText: 'Second route' }).getByRole('checkbox').uncheck();
await page.waitForFunction(() => window.providerFixture.records[0]['excluded-models']?.includes('*'));
assert.equal((await records())[1]['excluded-models'], undefined);
await rows.filter({ hasText: 'Second route' }).waitFor();
const handle = rows.filter({ hasText: 'Second route' }).locator('.provider-drag-handle');
await page.waitForFunction(() => [...document.querySelectorAll('.provider-drag-handle')].every((button) => !button.disabled));
await handle.focus();
await page.keyboard.press('Space');
await page.waitForTimeout(100);
await page.keyboard.press('ArrowDown');
await page.waitForTimeout(250);
await page.keyboard.press('Space');
await page.waitForFunction(() => window.providerFixture.records[1].priority === 2);
assert.equal((await records())[0].priority, 10);
await rows.filter({ hasText: 'Second route' }).getByRole('button', { name: 'Delete', exact: true }).click();
await page.getByRole('alertdialog').getByRole('button', { name: 'Delete', exact: true }).click();
await page.waitForFunction(() => window.providerFixture.records.length === 1);
saved = await records();
assert.equal(saved[0].models[0].alias, 'claude-a', 'Deleting the second route preserves the first');
assert.ok((await page.evaluate(() => window.providerFixture.writes)).every((write) => write.method === 'PUT'));
assert.deepEqual(errors, []);
console.log('PASS: #276 create, edit after external reorder, disable, drag, and delete preserve shared-credential sibling routes.');
} finally {
await browser.close();
}
})().catch((error) => { console.error(error); process.exitCode = 1; });
+81
View File
@@ -5,12 +5,14 @@ import {
applyProviderPreset,
buildProviderRecord,
createProviderDraft,
hasDuplicateProviderRecord,
DEEPSEEK_BASE_URL,
exclusionsForModelSelection,
modelSelectionForDiscovery,
parseProviderHeaders,
parseProviderApiKeys,
providerCategoryMatchesRecord,
providerDragId,
providerRecordWithDisabledState,
providerRemarkIdentity,
providerSectionOrder,
@@ -21,6 +23,85 @@ import {
} from '../src/pages/ApiAccessPage';
import { modelsFromRecord } from '../src/services/modelService';
describe('shared-credential provider entries (#276)', () => {
const first = {
'api-key': 'shared-key',
'base-url': 'https://claude.example.test',
priority: 10,
models: [{ name: 'upstream-a', alias: 'claude-a' }],
};
const second = {
...first, priority: 1, models: [{ name: 'upstream-b', alias: 'claude-b' }],
};
const row = (record: Record<string, unknown>, index: number) => ({
section: 'claude-api-key' as const, index, name: 'Claude',
apiKey: 'shared-key', baseUrl: first['base-url'], record,
});
it('allows different model mappings, aliases, priorities and routing settings', () => {
for (const candidate of [
second,
{ ...first, priority: 1 },
{ ...first, models: [{ name: 'upstream-a', alias: 'claude-other' }] },
{ ...first, prefix: 'team-b' },
{ ...first, headers: { 'X-Team': 'b' } },
{ ...first, 'disable-cooling': false },
{ ...first, 'request-retry': 0 },
{ ...first, weight: 0 },
]) {
expect(hasDuplicateProviderRecord('claude-api-key', [first], [candidate])).toBe(false);
}
});
it('still rejects identical configurations, ignoring runtime IDs and empty defaults', () => {
const persisted = { ...first, 'auth-index': 'runtime-only', headers: {}, prefix: '', cloak: null };
const candidate = buildProviderRecord('claude-api-key', {
...createProviderDraft('claude-api-key'), apiKey: 'shared-key',
baseUrl: first['base-url'], priority: '10', models: first.models,
});
expect(hasDuplicateProviderRecord('claude-api-key', [persisted], [candidate])).toBe(true);
expect(hasDuplicateProviderRecord('claude-api-key', [persisted], [candidate], 0)).toBe(false);
expect(hasDuplicateProviderRecord('claude-api-key', [first], [second, candidate])).toBe(true);
expect(hasDuplicateProviderRecord('openai-compatibility', [{ name: 'same' }], [{ name: 'same', models: first.models }])).toBe(true);
expect(hasDuplicateProviderRecord('gemini-api-key', [first], [second])).toBe(true);
expect(hasDuplicateProviderRecord('codex-api-key', [first], [second])).toBe(false);
});
it('locates the second entry for edit/toggle/delete, even after an external reorder', () => {
const selected = row({ ...second, 'auth-index': 'runtime-second' }, 1);
expect(resolveProviderRecordIndex([first, second], selected)).toBe(1);
expect(resolveProviderRecordIndex([second, first], selected)).toBe(0);
expect(resolveProviderRecordIndex([first], selected)).toBe(-1);
expect(resolveProviderRecordIndex([first, { ...second, priority: 5 }], selected)).toBe(-1);
});
it('retains default URL compatibility without guessing among sibling records', () => {
const { 'base-url': _base, ...withoutBase } = second;
expect(resolveProviderRecordIndex([first, withoutBase], row(second, 1))).toBe(1);
expect(resolveProviderRecordIndex([first, withoutBase, { ...withoutBase }], row(second, 1))).toBe(-1);
});
it('uses distinct drag IDs and reorders only the intended entries', () => {
const firstRow = row(first, 0);
const secondRow = row(second, 1);
expect(providerDragId(firstRow)).not.toBe(providerDragId(secondRow));
expect(reorderProviderRecords([first, second], [firstRow, secondRow], secondRow, firstRow)).toEqual([second, first]);
});
it('keeps remarks separate and stable through enable/disable and property ordering', () => {
const firstLocator = apiAccessRemarkLocatorFromRecord('claude-api-key', first);
const secondLocator = apiAccessRemarkLocatorFromRecord('claude-api-key', second);
expect(providerRemarkIdentity('claude-api-key', firstLocator))
.not.toBe(providerRemarkIdentity('claude-api-key', secondLocator));
const disabled = providerRecordWithDisabledState('claude-api-key', first, true);
expect(apiAccessRemarkLocatorFromRecord('claude-api-key', disabled)).toEqual(firstLocator);
expect(apiAccessRemarkLocatorFromRecord('claude-api-key', {
models: first.models, priority: 10, 'base-url': first['base-url'], 'api-key': first['api-key'],
'auth-index': 'runtime', websockets: false,
})).toEqual(firstLocator);
});
});
it('saves non-empty custom model names and removes duplicate or blank entries', () => {
const result = buildProviderRecord('openai-compatibility', {
name: 'custom-provider',