[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", # Current llama.cpp requires an integer; 999 means full offload and --fit may reduce it. "--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" ) # Some hosts inject both Path and PATH. Windows PowerShell's Start-Process # rejects that duplicate even though Windows normally treats them identically. $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."