chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import {
|
||||
LLM_DIR_PATH,
|
||||
LLM_MANIFEST_PATH,
|
||||
LLM_HIGH_TIER_MINIMUM_TOTAL_VRAM,
|
||||
LLM_MINIMUM_TOTAL_VRAM,
|
||||
LLAMACPP_RELEASE_VERSION
|
||||
} from '@/constants'
|
||||
import { FileHelper } from '@/helpers/file-helper'
|
||||
import { NetworkHelper } from '@/helpers/network-helper'
|
||||
|
||||
import inspectLocalAICapability from './local-ai-capability'
|
||||
import { createSetupStatus } from './setup-status'
|
||||
|
||||
/**
|
||||
* Download and set up the default local LLM
|
||||
* 1. Check minimum hardware requirements
|
||||
* 2. Select the default model according to total VRAM
|
||||
* 3. Download the model from Hugging Face or mirror
|
||||
* 4. Create manifest file with the default installed model path
|
||||
*/
|
||||
|
||||
const DEFAULT_LLM_OPTIONS = [
|
||||
{
|
||||
minimumTotalVRAM: LLM_HIGH_TIER_MINIMUM_TOTAL_VRAM,
|
||||
name: 'Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive',
|
||||
version: 'Q4_K_M',
|
||||
fileName: 'Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive-Q4_K_M.gguf',
|
||||
downloadURL:
|
||||
'https://huggingface.co/HauhauCS/Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive/resolve/main/Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive-Q4_K_M.gguf?download=true'
|
||||
},
|
||||
{
|
||||
minimumTotalVRAM: LLM_MINIMUM_TOTAL_VRAM,
|
||||
name: 'Qwen3.5-9B-Uncensored-HauhauCS-Aggressive',
|
||||
version: 'Q4_K_M',
|
||||
fileName: 'Qwen3.5-9B-Uncensored-HauhauCS-Aggressive-Q4_K_M.gguf',
|
||||
downloadURL:
|
||||
'https://huggingface.co/HauhauCS/Qwen3.5-9B-Uncensored-HauhauCS-Aggressive/resolve/main/Qwen3.5-9B-Uncensored-HauhauCS-Aggressive-Q4_K_M.gguf?download=true'
|
||||
}
|
||||
]
|
||||
const LOCAL_AI_CHECK_TEXT = 'Checking local AI requirements...'
|
||||
|
||||
function readManifest() {
|
||||
if (!fs.existsSync(LLM_MANIFEST_PATH)) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(LLM_MANIFEST_PATH, 'utf8'))
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeModelPath(modelPath) {
|
||||
return path.resolve(modelPath)
|
||||
}
|
||||
|
||||
async function removePreviousDefaultModel(previousModelPath, nextModelPath) {
|
||||
if (!previousModelPath || previousModelPath === nextModelPath) {
|
||||
return
|
||||
}
|
||||
|
||||
const resolvedPreviousModelPath = normalizeModelPath(previousModelPath)
|
||||
|
||||
// Only delete the previous default model we installed under Leon's managed
|
||||
// shared models directory.
|
||||
if (!resolvedPreviousModelPath.startsWith(`${LLM_DIR_PATH}${path.sep}`)) {
|
||||
return
|
||||
}
|
||||
|
||||
await fs.promises.rm(resolvedPreviousModelPath, { force: true })
|
||||
}
|
||||
|
||||
function getSelectedModel(totalVRAM) {
|
||||
return (
|
||||
DEFAULT_LLM_OPTIONS.find(
|
||||
({ minimumTotalVRAM }) => totalVRAM >= minimumTotalVRAM
|
||||
) || null
|
||||
)
|
||||
}
|
||||
|
||||
async function downloadLLM(selectedModel) {
|
||||
const manifest = readManifest()
|
||||
const targetPath = path.join(LLM_DIR_PATH, selectedModel.fileName)
|
||||
const defaultInstalledLLMPath = normalizeModelPath(targetPath)
|
||||
const isCurrentModelInstalled =
|
||||
manifest?.name === selectedModel.name &&
|
||||
manifest?.version === selectedModel.version &&
|
||||
manifest?.defaultInstalledLLMPath === defaultInstalledLLMPath &&
|
||||
fs.existsSync(targetPath)
|
||||
|
||||
if (isCurrentModelInstalled) {
|
||||
return {
|
||||
installed: false,
|
||||
targetPath
|
||||
}
|
||||
}
|
||||
|
||||
await fs.promises.mkdir(LLM_DIR_PATH, { recursive: true })
|
||||
await removePreviousDefaultModel(manifest?.defaultInstalledLLMPath, defaultInstalledLLMPath)
|
||||
await fs.promises.rm(targetPath, { force: true })
|
||||
|
||||
const llmDownloadURL = await NetworkHelper.setHuggingFaceURL(
|
||||
selectedModel.downloadURL
|
||||
)
|
||||
|
||||
await FileHelper.downloadFile(llmDownloadURL, targetPath)
|
||||
|
||||
await FileHelper.createManifestFile(
|
||||
LLM_MANIFEST_PATH,
|
||||
selectedModel.name,
|
||||
selectedModel.version,
|
||||
{
|
||||
llamaCPPVersion: LLAMACPP_RELEASE_VERSION,
|
||||
defaultInstalledLLMPath
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
installed: true,
|
||||
targetPath
|
||||
}
|
||||
}
|
||||
|
||||
function getLocalAISummary(selectedModel, hardware) {
|
||||
const gpuLabel = hardware.hasGPU ? hardware.gpuDeviceNames[0] : 'CPU'
|
||||
const computeAPILabel = hardware.hasGPU
|
||||
? String(hardware.graphicsComputeAPI).toUpperCase()
|
||||
: 'CPU'
|
||||
|
||||
return `${selectedModel.name} (${selectedModel.version}, ${gpuLabel}, ${computeAPILabel}, ${hardware.totalVRAM} GB VRAM)`
|
||||
}
|
||||
|
||||
export default async function setupLocalLLM(localAICapability) {
|
||||
const status = createSetupStatus(LOCAL_AI_CHECK_TEXT).start()
|
||||
|
||||
const hardware = localAICapability || (await inspectLocalAICapability())
|
||||
|
||||
if (!hardware.canInstallLocalAI) {
|
||||
status.succeed(
|
||||
`Local LLM support requires at least ${LLM_MINIMUM_TOTAL_VRAM} GB of total VRAM and a supported GPU setup. Current total VRAM is ${hardware.totalVRAM} GB. I will continue without installing a default local LLM.`
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const selectedModel = getSelectedModel(hardware.totalVRAM)
|
||||
|
||||
if (!selectedModel) {
|
||||
status.succeed(
|
||||
`No default local LLM matches the current total VRAM (${hardware.totalVRAM} GB).`
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
status.pause()
|
||||
|
||||
const { installed } = await downloadLLM(selectedModel)
|
||||
status.text = 'Finalizing local AI...'
|
||||
status.start()
|
||||
|
||||
if (installed) {
|
||||
status.succeed(
|
||||
`Local AI: ready - ${getLocalAISummary(selectedModel, hardware)}`
|
||||
)
|
||||
} else {
|
||||
status.succeed(
|
||||
`Local AI: ready - ${getLocalAISummary(selectedModel, hardware)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user