# Installs LoRA adapters into the package's Ollama model store at build time. # # Reads manifest\lora-adapters.json (see manifest\lora-adapters.example.json): # each entry declares a base Ollama model, a GGUF LoRA adapter file (local path # or URL + sha256), optional system prompt and inference parameters. The script # stages the adapter under models\lora\, generates an Ollama Modelfile, and # runs `ollama create` against the builder's Ollama store so the fine-tuned # model ships on the key like any other model. # # Requires the builder Ollama running (same convention as model pulls): # OLLAMA_HOST / OLLAMA_MODELS must point at the target store. [CmdletBinding()] param( [string]$DriveRoot = (Resolve-Path "$PSScriptRoot\..\..").Path, [string]$ManifestPath = "manifest\lora-adapters.json", [string]$OllamaExe = "" ) $ErrorActionPreference = "Stop" Set-Location $DriveRoot $full = Join-Path $DriveRoot $ManifestPath if (!(Test-Path $full)) { Write-Host "No LoRA manifest found at $ManifestPath - nothing to do." exit 0 } if (-not $OllamaExe) { $candidates = @( (Join-Path $DriveRoot "backends\ollama\windows\ollama.exe"), "ollama" ) $OllamaExe = $candidates | Where-Object { (Test-Path $_) -or (Get-Command $_ -ErrorAction SilentlyContinue) } | Select-Object -First 1 } if (-not $OllamaExe) { throw "ollama.exe not found. Stage backends\ollama\windows\ollama.exe first." } $manifest = Get-Content $full -Raw | ConvertFrom-Json $loraDir = Join-Path $DriveRoot "models\lora" New-Item -ItemType Directory -Force -Path $loraDir | Out-Null foreach ($adapter in $manifest.adapters) { if (-not $adapter.name) { throw "Each adapter needs a 'name' (the Ollama model name to create)." } if (-not $adapter.base_model) { throw "Adapter $($adapter.name): 'base_model' is required (e.g. qwen3.5:4b)." } # Stage the adapter GGUF locally (URL + sha256, or a path already on disk). $adapterFile = Join-Path $loraDir ("{0}.gguf" -f $adapter.name) if ($adapter.url) { if (!(Test-Path $adapterFile) -or ((Get-FileHash $adapterFile -Algorithm SHA256).Hash.ToLowerInvariant() -ne $adapter.sha256.ToLowerInvariant())) { Write-Host "Downloading LoRA adapter $($adapter.name)" -ForegroundColor Cyan Invoke-WebRequest -Uri $adapter.url -OutFile $adapterFile -UseBasicParsing } if ($adapter.sha256) { $h = (Get-FileHash $adapterFile -Algorithm SHA256).Hash.ToLowerInvariant() if ($h -ne $adapter.sha256.ToLowerInvariant()) { throw "SHA256 mismatch for adapter $($adapter.name)" } } } elseif ($adapter.path) { $source = Join-Path $DriveRoot $adapter.path if (!(Test-Path $source)) { throw "Adapter file not found: $source" } Copy-Item $source $adapterFile -Force } else { throw "Adapter $($adapter.name): provide 'url' (+sha256) or 'path'." } # Generate the Modelfile next to the adapter for traceability. $lines = @("FROM $($adapter.base_model)", "ADAPTER $adapterFile") if ($adapter.system) { $lines += "SYSTEM `"`"`"$($adapter.system)`"`"`"" } if ($adapter.parameters) { foreach ($p in $adapter.parameters.PSObject.Properties) { $lines += "PARAMETER $($p.Name) $($p.Value)" } } $modelfile = Join-Path $loraDir ("{0}.Modelfile" -f $adapter.name) $lines -join "`n" | Set-Content -Encoding utf8 $modelfile Write-Host "Creating Ollama model $($adapter.name) (base $($adapter.base_model) + LoRA)" -ForegroundColor Cyan & $OllamaExe create $adapter.name -f $modelfile if ($LASTEXITCODE -ne 0) { throw "ollama create failed for $($adapter.name) (exit $LASTEXITCODE)" } Write-Host "Created: $($adapter.name)" -ForegroundColor Green Write-Host "Reminder: add '$($adapter.name)' to config\model-catalog.json (Add-ModelToCatalog.ps1) so the runtime can select it, and record the adapter license in licenses\THIRD_PARTY_NOTICES.md." }