| [CmdletBinding()]
|
| param(
|
| [string]$HostAddress = "127.0.0.1",
|
| [int]$Port = 8080,
|
| [int]$StartupTimeoutSeconds = 120
|
| )
|
|
|
| $ErrorActionPreference = "Stop"
|
| if ($HostAddress -notin @("127.0.0.1", "localhost")) {
|
| throw "Refusing non-loopback llama.cpp host: $HostAddress"
|
| }
|
|
|
| $repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
|
| $runtimeRoot = Join-Path $repoRoot ".local\llama.cpp"
|
| $manifestPath = Join-Path $runtimeRoot "installed.json"
|
| if (-not (Test-Path $manifestPath)) {
|
| throw "llama.cpp is not installed. Run scripts\Install-LlamaCpp.ps1 first."
|
| }
|
| $manifest = Get-Content $manifestPath -Raw | ConvertFrom-Json
|
| $server = Join-Path $runtimeRoot "$($manifest.tag)\llama-server.exe"
|
| $model = Join-Path $repoRoot "Assets\Model\Qwen3-4B-Instruct-2507-Q4_K_M.gguf"
|
| $keyPath = Join-Path $runtimeRoot "api-key.txt"
|
| $pidPath = Join-Path $runtimeRoot "server.pid"
|
| $logPath = Join-Path $runtimeRoot "server.log"
|
| $errorLogPath = Join-Path $runtimeRoot "server-error.log"
|
|
|
| if (-not (Test-Path $server)) { throw "llama-server.exe is missing at $server" }
|
| if (-not (Test-Path $model)) { throw "GGUF model is missing at $model" }
|
| if (-not (Test-Path $keyPath)) {
|
| $bytes = New-Object byte[] 32
|
| $generator = [System.Security.Cryptography.RandomNumberGenerator]::Create()
|
| try {
|
| $generator.GetBytes($bytes)
|
| }
|
| finally {
|
| $generator.Dispose()
|
| }
|
| (($bytes | ForEach-Object { $_.ToString("x2") }) -join "") |
|
| Set-Content -LiteralPath $keyPath -Encoding ASCII -NoNewline
|
| }
|
| if (Test-Path $pidPath) {
|
| $recordedPid = Get-Content $pidPath -Raw
|
| if (-not [string]::IsNullOrWhiteSpace($recordedPid)) {
|
| $recordedPid = $recordedPid.Trim()
|
| $existingPid = [int]$recordedPid
|
| if (Get-Process -Id $existingPid -ErrorAction SilentlyContinue) {
|
| throw "A llama.cpp process recorded by $pidPath is already running."
|
| }
|
| }
|
| Set-Content -LiteralPath $pidPath -Value "" -Encoding ASCII -NoNewline
|
| }
|
|
|
| $arguments = @(
|
| "--model", $model,
|
| "--alias", "qwen3-4b-instruct-2507",
|
| "--host", $HostAddress,
|
| "--port", "$Port",
|
| "--api-key-file", $keyPath,
|
| "--ctx-size", "8192",
|
|
|
| "--gpu-layers", "999",
|
| "--fit", "on",
|
| "--flash-attn", "auto",
|
| "--cache-type-k", "q8_0",
|
| "--cache-type-v", "q8_0",
|
| "--parallel", "1",
|
| "--cache-ram", "1024",
|
| "--cache-prompt",
|
| "--jinja",
|
| "--reasoning", "off",
|
| "--no-ui",
|
| "--offline"
|
| )
|
|
|
|
|
|
|
| $processPath = [Environment]::GetEnvironmentVariable("Path", "Process")
|
| [Environment]::SetEnvironmentVariable("PATH", $null, "Process")
|
| [Environment]::SetEnvironmentVariable("Path", $processPath, "Process")
|
|
|
| $process = Start-Process -FilePath $server -ArgumentList $arguments `
|
| -WorkingDirectory (Split-Path $server) -WindowStyle Hidden -PassThru `
|
| -RedirectStandardOutput $logPath -RedirectStandardError $errorLogPath
|
| $process.Id | Set-Content -LiteralPath $pidPath -Encoding ASCII -NoNewline
|
|
|
| $deadline = (Get-Date).AddSeconds($StartupTimeoutSeconds)
|
| while ((Get-Date) -lt $deadline) {
|
| if ($process.HasExited) {
|
| throw "llama-server exited during startup. Inspect $logPath"
|
| }
|
| try {
|
| & (Join-Path $PSScriptRoot "Test-LlamaServer.ps1") -Port $Port -KeyPath $keyPath | Out-Null
|
| Write-Output "llama-server is ready on http://127.0.0.1:$Port (PID $($process.Id))."
|
| exit 0
|
| }
|
| catch {
|
| Start-Sleep -Milliseconds 750
|
| }
|
| }
|
| throw "llama-server did not become healthy within $StartupTimeoutSeconds seconds."
|
|
|