Skip to content

Commit d773b52

Browse files
committed
feat: Enhance Llama.cpp backend management with persistence
This commit introduces significant improvements to how the Llama.cpp extension manages and updates its backend installations, focusing on user preference persistence and smarter auto-updates. Key changes include: * **Persistent Backend Type Preference:** The extension now stores the user's preferred backend type (e.g., `cuda`, `cpu`, `metal`) in `localStorage`. This ensures that even after updates or restarts, the system attempts to use the user's previously selected backend type, if available. * **Intelligent Auto-Update:** The auto-update mechanism has been refined to prioritize updating to the **latest version of the *currently selected backend type*** rather than always defaulting to the "best available" backend (which might change). This respects user choice while keeping the chosen backend type up-to-date. * **Improved Initial Installation/Configuration:** For fresh installations or cases where the `version_backend` setting is invalid, the system now intelligently determines and installs the best available backend, then persists its type. * **Refined Old Backend Cleanup:** The `removeOldBackends` function has been renamed to `removeOldBackend` and modified to specifically clean up *older versions of the currently selected backend type*, preventing the accumulation of unnecessary files while preserving other backend types the user might switch to. * **Robust Local Storage Handling:** New private methods (`getStoredBackendType`, `setStoredBackendType`, `clearStoredBackendType`) are introduced to safely interact with `localStorage`, including error handling for potential `localStorage` access issues. * **Version Filtering Utility:** A new utility `findLatestVersionForBackend` helps in identifying the latest available version for a specific backend type from a list of supported backends. These changes provide a more stable, user-friendly, and maintainable backend management experience for the Llama.cpp extension. Fixes: #5883
1 parent 5d00cf6 commit d773b52

File tree

1 file changed

+242
-43
lines changed
  • extensions/llamacpp-extension/src

1 file changed

+242
-43
lines changed

extensions/llamacpp-extension/src/index.ts

Lines changed: 242 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,49 @@ export default class llamacpp_extension extends AIEngine {
177177
this.configureBackends()
178178
}
179179

180+
private getStoredBackendType(): string | null {
181+
try {
182+
return localStorage.getItem('llama_cpp_backend_type')
183+
} catch (error) {
184+
logger.warn('Failed to read backend type from localStorage:', error)
185+
return null
186+
}
187+
}
188+
189+
private setStoredBackendType(backendType: string): void {
190+
try {
191+
localStorage.setItem('llama_cpp_backend_type', backendType)
192+
logger.info(`Stored backend type preference: ${backendType}`)
193+
} catch (error) {
194+
logger.warn('Failed to store backend type in localStorage:', error)
195+
}
196+
}
197+
198+
private clearStoredBackendType(): void {
199+
try {
200+
localStorage.removeItem('llama_cpp_backend_type')
201+
logger.info('Cleared stored backend type preference')
202+
} catch (error) {
203+
logger.warn('Failed to clear backend type from localStorage:', error)
204+
}
205+
}
206+
207+
private findLatestVersionForBackend(
208+
version_backends: { version: string; backend: string }[],
209+
backendType: string
210+
): string | null {
211+
const matchingBackends = version_backends.filter(
212+
(vb) => vb.backend === backendType
213+
)
214+
if (matchingBackends.length === 0) {
215+
return null
216+
}
217+
218+
// Sort by version (newest first) and get the latest
219+
matchingBackends.sort((a, b) => b.version.localeCompare(a.version))
220+
return `${matchingBackends[0].version}/${matchingBackends[0].backend}`
221+
}
222+
180223
async configureBackends(): Promise<void> {
181224
if (this.isConfiguringBackends) {
182225
logger.info(
@@ -207,8 +250,33 @@ export default class llamacpp_extension extends AIEngine {
207250
)
208251
}
209252

210-
let bestAvailableBackendString =
211-
this.determineBestBackend(version_backends)
253+
// Get stored backend preference
254+
const storedBackendType = this.getStoredBackendType()
255+
let bestAvailableBackendString = ''
256+
257+
if (storedBackendType) {
258+
// Find the latest version of the stored backend type
259+
const preferredBackendString = this.findLatestVersionForBackend(
260+
version_backends,
261+
storedBackendType
262+
)
263+
if (preferredBackendString) {
264+
bestAvailableBackendString = preferredBackendString
265+
logger.info(
266+
`Using stored backend preference: ${bestAvailableBackendString}`
267+
)
268+
} else {
269+
logger.warn(
270+
`Stored backend type '${storedBackendType}' not available, falling back to best backend`
271+
)
272+
// Clear the invalid stored preference
273+
this.clearStoredBackendType()
274+
bestAvailableBackendString =
275+
this.determineBestBackend(version_backends)
276+
}
277+
} else {
278+
bestAvailableBackendString = this.determineBestBackend(version_backends)
279+
}
212280

213281
let settings = structuredClone(SETTINGS)
214282
const backendSettingIndex = settings.findIndex(
@@ -231,11 +299,30 @@ export default class llamacpp_extension extends AIEngine {
231299
originalDefaultBackendValue
232300
)
233301

234-
const initialUiDefault =
302+
// Determine initial UI default based on priority:
303+
// 1. Saved setting (if valid and not original default)
304+
// 2. Best available for stored backend type
305+
// 3. Original default
306+
let initialUiDefault = originalDefaultBackendValue
307+
308+
if (
235309
savedBackendSetting &&
236310
savedBackendSetting !== originalDefaultBackendValue
237-
? savedBackendSetting
238-
: bestAvailableBackendString || originalDefaultBackendValue
311+
) {
312+
initialUiDefault = savedBackendSetting
313+
// Store the backend type from the saved setting
314+
const [, backendType] = savedBackendSetting.split('/')
315+
if (backendType) {
316+
this.setStoredBackendType(backendType)
317+
}
318+
} else if (bestAvailableBackendString) {
319+
initialUiDefault = bestAvailableBackendString
320+
// Store the backend type from the best available
321+
const [, backendType] = bestAvailableBackendString.split('/')
322+
if (backendType) {
323+
this.setStoredBackendType(backendType)
324+
}
325+
}
239326

240327
backendSetting.controllerProps.value = initialUiDefault
241328
logger.info(
@@ -253,6 +340,37 @@ export default class llamacpp_extension extends AIEngine {
253340
let effectiveBackendString = this.config.version_backend
254341
let backendWasDownloaded = false
255342

343+
// Handle fresh installation case where version_backend might be 'none' or invalid
344+
if (
345+
!effectiveBackendString ||
346+
effectiveBackendString === 'none' ||
347+
!effectiveBackendString.includes('/')
348+
) {
349+
effectiveBackendString = bestAvailableBackendString
350+
logger.info(
351+
`Fresh installation or invalid backend detected, using: ${effectiveBackendString}`
352+
)
353+
354+
// Update the config immediately
355+
this.config.version_backend = effectiveBackendString
356+
}
357+
358+
// Download and install the backend if not already present
359+
if (effectiveBackendString) {
360+
const [version, backend] = effectiveBackendString.split('/')
361+
if (version && backend) {
362+
const isInstalled = await isBackendInstalled(backend, version)
363+
if (!isInstalled) {
364+
logger.info(`Installing initial backend: ${effectiveBackendString}`)
365+
await this.ensureBackendReady(backend, version)
366+
backendWasDownloaded = true
367+
logger.info(
368+
`Successfully installed initial backend: ${effectiveBackendString}`
369+
)
370+
}
371+
}
372+
}
373+
256374
if (this.config.auto_update_engine) {
257375
const updateResult = await this.handleAutoUpdate(
258376
bestAvailableBackendString
@@ -263,12 +381,8 @@ export default class llamacpp_extension extends AIEngine {
263381
}
264382
}
265383

266-
if (!backendWasDownloaded) {
384+
if (!backendWasDownloaded && effectiveBackendString) {
267385
await this.ensureFinalBackendInstallation(effectiveBackendString)
268-
} else {
269-
logger.info(
270-
'Skipping final installation check - backend was just downloaded during auto-update'
271-
)
272386
}
273387
} finally {
274388
this.isConfiguringBackends = false
@@ -350,65 +464,141 @@ export default class llamacpp_extension extends AIEngine {
350464
return { wasUpdated: false, newBackend: this.config.version_backend }
351465
}
352466

467+
// If version_backend is empty, invalid, or 'none', use the best available backend
468+
if (
469+
!this.config.version_backend ||
470+
this.config.version_backend === '' ||
471+
this.config.version_backend === 'none' ||
472+
!this.config.version_backend.includes('/')
473+
) {
474+
logger.info(
475+
'No valid backend currently selected, using best available backend'
476+
)
477+
try {
478+
const [bestVersion, bestBackend] = bestAvailableBackendString.split('/')
479+
480+
// Download new backend
481+
await this.ensureBackendReady(bestBackend, bestVersion)
482+
483+
// Add delay on Windows
484+
if (IS_WINDOWS) {
485+
await new Promise((resolve) => setTimeout(resolve, 1000))
486+
}
487+
488+
// Update configuration
489+
this.config.version_backend = bestAvailableBackendString
490+
491+
// Store the backend type preference
492+
this.setStoredBackendType(bestBackend)
493+
494+
// Update settings
495+
const settings = await this.getSettings()
496+
await this.updateSettings(
497+
settings.map((item) => {
498+
if (item.key === 'version_backend') {
499+
item.controllerProps.value = bestAvailableBackendString
500+
}
501+
return item
502+
})
503+
)
504+
505+
logger.info(
506+
`Successfully set initial backend: ${bestAvailableBackendString}`
507+
)
508+
return { wasUpdated: true, newBackend: bestAvailableBackendString }
509+
} catch (error) {
510+
logger.error('Failed to set initial backend:', error)
511+
return { wasUpdated: false, newBackend: this.config.version_backend }
512+
}
513+
}
514+
515+
// Parse current backend configuration
353516
const [currentVersion, currentBackend] = (
354517
this.config.version_backend || ''
355518
).split('/')
356-
const [bestVersion, bestBackend] = bestAvailableBackendString.split('/')
357519

358-
// Check if update is needed
359-
if (currentBackend === bestBackend && currentVersion === bestVersion) {
360-
logger.info('Auto-update: Already using the best available backend')
520+
if (!currentVersion || !currentBackend) {
521+
logger.warn(
522+
`Invalid current backend format: ${this.config.version_backend}`
523+
)
361524
return { wasUpdated: false, newBackend: this.config.version_backend }
362525
}
363526

364-
// Perform update
527+
// Find the latest version for the currently selected backend type
528+
const version_backends = await listSupportedBackends()
529+
const targetBackendString = this.findLatestVersionForBackend(
530+
version_backends,
531+
currentBackend
532+
)
533+
534+
if (!targetBackendString) {
535+
logger.warn(
536+
`No available versions found for current backend type: ${currentBackend}`
537+
)
538+
return { wasUpdated: false, newBackend: this.config.version_backend }
539+
}
540+
541+
const [latestVersion] = targetBackendString.split('/')
542+
543+
// Check if update is needed (only version comparison for same backend type)
544+
if (currentVersion === latestVersion) {
545+
logger.info(
546+
'Auto-update: Already using the latest version of the selected backend'
547+
)
548+
return { wasUpdated: false, newBackend: this.config.version_backend }
549+
}
550+
551+
// Perform version update for the same backend type
365552
try {
366553
logger.info(
367-
`Auto-updating from ${this.config.version_backend} to ${bestAvailableBackendString}`
554+
`Auto-updating from ${this.config.version_backend} to ${targetBackendString} (preserving backend type)`
368555
)
369556

370-
// Download new backend first
371-
await this.ensureBackendReady(bestBackend, bestVersion)
557+
// Download new version of the same backend type
558+
await this.ensureBackendReady(currentBackend, latestVersion)
372559

373-
// Add a small delay on Windows to ensure file operations complete
560+
// Add delay on Windows
374561
if (IS_WINDOWS) {
375562
await new Promise((resolve) => setTimeout(resolve, 1000))
376563
}
377564

378565
// Update configuration
379-
this.config.version_backend = bestAvailableBackendString
566+
this.config.version_backend = targetBackendString
567+
568+
// Update stored backend type preference (in case it changed)
569+
this.setStoredBackendType(currentBackend)
380570

381571
// Update settings
382572
const settings = await this.getSettings()
383573
await this.updateSettings(
384574
settings.map((item) => {
385575
if (item.key === 'version_backend') {
386-
item.controllerProps.value = bestAvailableBackendString
576+
item.controllerProps.value = targetBackendString
387577
}
388578
return item
389579
})
390580
)
391581

392582
logger.info(
393-
`Successfully updated to backend: ${bestAvailableBackendString}`
583+
`Successfully updated to backend: ${targetBackendString} (preserved backend type: ${currentBackend})`
394584
)
395585

396-
// Clean up old backends (with additional delay on Windows)
586+
// Clean up old versions of the same backend type
397587
if (IS_WINDOWS) {
398588
await new Promise((resolve) => setTimeout(resolve, 500))
399589
}
400-
await this.removeOldBackends(bestVersion, bestBackend)
590+
await this.removeOldBackend(latestVersion, currentBackend)
401591

402-
return { wasUpdated: true, newBackend: bestAvailableBackendString }
592+
return { wasUpdated: true, newBackend: targetBackendString }
403593
} catch (error) {
404594
logger.error('Auto-update failed:', error)
405595
return { wasUpdated: false, newBackend: this.config.version_backend }
406596
}
407597
}
408598

409-
private async removeOldBackends(
410-
bestVersion: string,
411-
bestBackend: string
599+
private async removeOldBackend(
600+
latestVersion: string,
601+
backendType: string
412602
): Promise<void> {
413603
try {
414604
const janDataFolderPath = await getJanDataFolderPath()
@@ -426,32 +616,35 @@ export default class llamacpp_extension extends AIEngine {
426616

427617
for (const versionDir of versionDirs) {
428618
const versionPath = await joinPath([backendsDir, versionDir])
429-
const backendTypeDirs = await fs.readdirSync(versionPath)
619+
const versionName = await basename(versionDir)
430620

431-
for (const backendTypeDir of backendTypeDirs) {
432-
const versionName = await basename(versionDir)
433-
const backendName = await basename(backendTypeDir)
621+
// Skip the latest version
622+
if (versionName === latestVersion) {
623+
continue
624+
}
434625

435-
// Skip if it's the best version/backend
436-
if (versionName === bestVersion && backendName === bestBackend) {
437-
continue
438-
}
626+
// Check if this version has the specific backend type we're interested in
627+
const backendTypePath = await joinPath([versionPath, backendType])
439628

440-
// If this other backend is installed, remove it
441-
const isInstalled = await isBackendInstalled(backendName, versionName)
629+
if (await fs.existsSync(backendTypePath)) {
630+
const isInstalled = await isBackendInstalled(backendType, versionName)
442631
if (isInstalled) {
443-
const toRemove = await joinPath([versionPath, backendTypeDir])
444632
try {
445-
await fs.rm(toRemove)
446-
logger.info(`Removed old backend: ${toRemove}`)
633+
await fs.rm(backendTypePath)
634+
logger.info(
635+
`Removed old version of ${backendType}: ${backendTypePath}`
636+
)
447637
} catch (e) {
448-
logger.warn(`Failed to remove old backend: ${toRemove}`, e)
638+
logger.warn(
639+
`Failed to remove old backend version: ${backendTypePath}`,
640+
e
641+
)
449642
}
450643
}
451644
}
452645
}
453646
} catch (error) {
454-
logger.error('Error during old backend cleanup:', error)
647+
logger.error('Error during old backend version cleanup:', error)
455648
}
456649
}
457650

@@ -526,6 +719,12 @@ export default class llamacpp_extension extends AIEngine {
526719
const valueStr = value as string
527720
const [version, backend] = valueStr.split('/')
528721

722+
// Store the backend type preference in localStorage
723+
if (backend) {
724+
this.setStoredBackendType(backend)
725+
logger.info(`Updated backend type preference to: ${backend}`)
726+
}
727+
529728
// Reset device setting when backend changes
530729
this.config.device = ''
531730

0 commit comments

Comments
 (0)