fix(ai): correct apiKeySet detection and prevent stale key reuse

- Fix apiKeySet reporting false positives for local no-key configs by
  resolving the actual key via resolveApiKey and excluding placeholders
- Prevent editing a config from reusing an incompatible API key when
  provider or connection mode has changed
- Pass summary strategy to single-session generation for consistency

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
diguaandCursor authored and digua committed 2026-05-27 23:47:27 +08:00
1 parent a9b8599417
commit 88ab0ae172
11 files changed
+232 -18

No files matched your search

@@ -0,0 +1,52 @@
import { describe, it } from 'node:test'
import assert from 'node:assert/strict'
import { toLlmConfigDisplay } from './ai-config-display'
describe('toLlmConfigDisplay', () => {
it('marks apiKeySet when provider fallback auth resolves a key', () => {
const result = toLlmConfigDisplay(
{
id: 'cfg-1',
name: 'OpenAI compatible',
provider: 'openai-compatible',
apiKey: '',
},
(provider, authProfile) => (provider === 'openai-compatible' && !authProfile ? 'sk-provider-fallback' : '')
)
assert.equal(result.apiKey, '')
assert.equal(result.apiKeySet, true)
})
it('does not mark the local no-key placeholder as a real API key', () => {
const result = toLlmConfigDisplay(
{
id: 'cfg-2',
name: 'LAN Ollama',
provider: 'openai-compatible',
apiKey: '',
baseUrl: 'http://127.0.0.1:11434',
authProfile: 'lan-ollama',
},
() => 'sk-no-key-required'
)
assert.equal(result.apiKey, '')
assert.equal(result.apiKeySet, false)
})
it('does not mark legacy plaintext apiKey as reusable', () => {
const result = toLlmConfigDisplay(
{
id: 'cfg-3',
name: 'Legacy OpenAI',
provider: 'openai',
apiKey: 'sk-legacy-plaintext',
},
() => ''
)
assert.equal(result.apiKey, '')
assert.equal(result.apiKeySet, false)
})
})
@@ -0,0 +1,21 @@
type LlmConfigRecord = Record<string, unknown>
type ResolveApiKey = (provider: string, authProfile?: string) => string | undefined
const NO_KEY_PLACEHOLDER = 'sk-no-key-required'
function hasRealApiKey(apiKey: string): boolean {
return !!apiKey && apiKey !== NO_KEY_PLACEHOLDER
}
export function toLlmConfigDisplay(config: LlmConfigRecord, resolveApiKey: ResolveApiKey): LlmConfigRecord {
const { apiKey: _rawApiKey, ...rest } = config
const provider = typeof config.provider === 'string' ? config.provider : ''
const authProfile = typeof config.authProfile === 'string' ? config.authProfile : undefined
const resolvedApiKey = provider ? resolveApiKey(provider, authProfile) : ''
return {
...rest,
apiKey: '',
apiKeySet: hasRealApiKey(resolvedApiKey || ''),
}
}
+3 -4
View File
@@ -46,6 +46,8 @@ import {
deleteCustomModel,
} from '../../ai/custom-store'
import { validateApiKey, fetchRemoteModels } from '../../ai/remote-api'
import { resolveApiKey } from '@openchatlab/config'
import { toLlmConfigDisplay } from './ai-config-display'
function getAiDir(dbManager: DatabaseManager): string {
const pathProvider = (dbManager as any)['pathProvider']
@@ -177,10 +179,7 @@ export function registerAiRoutes(
try {
const data = JSON.parse(fs.readFileSync(configPath, 'utf-8'))
const configs = (data.configs || []).map((c: Record<string, unknown>) => {
const { apiKey: _k, ...rest } = c
return { ...rest, apiKey: '', apiKeySet: !!c.authProfile }
})
const configs = (data.configs || []).map((c: Record<string, unknown>) => toLlmConfigDisplay(c, resolveApiKey))
return {
configs,
defaultAssistant: data.defaultAssistant ?? null,