diff --git a/src/components/common/Settings/AI/apiKeyReuse.test.ts b/src/components/common/Settings/AI/apiKeyReuse.test.ts index 3fe80d2a..920953f1 100644 --- a/src/components/common/Settings/AI/apiKeyReuse.test.ts +++ b/src/components/common/Settings/AI/apiKeyReuse.test.ts @@ -44,4 +44,52 @@ describe('canReuseExistingApiKey', () => { assert.equal(result, false) }) + + it('requires a new key when base URL changes in openai-compat mode', () => { + const result = canReuseExistingApiKey({ + mode: 'edit', + existingApiKeySet: true, + hasNewApiKey: false, + originalProvider: 'openai-compatible', + currentProvider: 'openai-compatible', + originalConnectionMode: 'openai-compat', + currentConnectionMode: 'openai-compat', + originalBaseUrl: 'https://api.provider-a.com', + currentBaseUrl: 'https://api.provider-b.com', + }) + + assert.equal(result, false) + }) + + it('allows reusing key when base URL is unchanged in openai-compat mode (trailing slash normalized)', () => { + const result = canReuseExistingApiKey({ + mode: 'edit', + existingApiKeySet: true, + hasNewApiKey: false, + originalProvider: 'openai-compatible', + currentProvider: 'openai-compatible', + originalConnectionMode: 'openai-compat', + currentConnectionMode: 'openai-compat', + originalBaseUrl: 'https://api.provider-a.com/', + currentBaseUrl: 'https://api.provider-a.com', + }) + + assert.equal(result, true) + }) + + it('allows reusing key when base URL is unchanged in openai-compat mode', () => { + const result = canReuseExistingApiKey({ + mode: 'edit', + existingApiKeySet: true, + hasNewApiKey: false, + originalProvider: 'openai-compatible', + currentProvider: 'openai-compatible', + originalConnectionMode: 'openai-compat', + currentConnectionMode: 'openai-compat', + originalBaseUrl: 'https://api.provider-a.com', + currentBaseUrl: 'https://api.provider-a.com', + }) + + assert.equal(result, true) + }) }) diff --git a/src/components/common/Settings/AI/apiKeyReuse.ts b/src/components/common/Settings/AI/apiKeyReuse.ts index bb09b996..6ec3e4aa 100644 --- a/src/components/common/Settings/AI/apiKeyReuse.ts +++ b/src/components/common/Settings/AI/apiKeyReuse.ts @@ -8,6 +8,10 @@ export interface ApiKeyReuseInput { currentProvider: string originalConnectionMode?: ConnectionMode currentConnectionMode: ConnectionMode + /** Original base URL (for openai-compat mode endpoint change detection) */ + originalBaseUrl?: string + /** Current base URL entered by the user */ + currentBaseUrl?: string } export function getConnectionModeForConfig(config: { @@ -23,11 +27,21 @@ export function getConnectionModeForConfig(config: { } export function canReuseExistingApiKey(input: ApiKeyReuseInput): boolean { - return ( + const baseConditions = input.mode === 'edit' && !!input.existingApiKeySet && !input.hasNewApiKey && input.originalProvider === input.currentProvider && input.originalConnectionMode === input.currentConnectionMode - ) + + if (!baseConditions) return false + + // For openai-compat mode, a base URL change means a different endpoint/service, + // so the existing credential must not be silently reused without re-validation. + if (input.currentConnectionMode === 'openai-compat') { + const normalizeUrl = (url?: string) => url?.trim().replace(/\/+$/, '') || '' + return normalizeUrl(input.originalBaseUrl) === normalizeUrl(input.currentBaseUrl) + } + + return true } diff --git a/src/components/common/Settings/AI/useAIConfigForm.ts b/src/components/common/Settings/AI/useAIConfigForm.ts index d01e3c70..ed8c3897 100644 --- a/src/components/common/Settings/AI/useAIConfigForm.ts +++ b/src/components/common/Settings/AI/useAIConfigForm.ts @@ -174,6 +174,8 @@ export function useAIConfigForm(props: { currentProvider: provider, originalConnectionMode: props.config.value ? getConnectionModeForConfig(props.config.value) : undefined, currentConnectionMode: connectionMode.value, + originalBaseUrl: props.config.value?.baseUrl, + currentBaseUrl: baseUrl.trim() || undefined, }) if (isLocalMode.value) { @@ -709,6 +711,8 @@ export function useAIConfigForm(props: { currentProvider: formData.value.provider, originalConnectionMode: props.config.value ? getConnectionModeForConfig(props.config.value) : undefined, currentConnectionMode: connectionMode.value, + originalBaseUrl: props.config.value?.baseUrl, + currentBaseUrl: formData.value.baseUrl.trim() || undefined, }) if (isEditWithExistingKey) { return doSave()