Local LLM + image-generation serving for .NET - desktop and browser - on SpawnDev.ILGPU.ML.
Read D:\users\tj\Projects\CLAUDE.md first (global rules).
SpawnDev.BlazorJS → SpawnDev.ILGPU → SpawnDev.ILGPU.ML → SpawnDev.AI → Demo
SpawnDev.AI- contracts only (chat, tools/IAiTool, transports, wire frames).SpawnDev.AI.Server- engines (chat/image), protocol router (OpenAI + Ollama + Anthropic + /v1/images/generations), worker server/client, model providers. WASM-compatible (no ASP.NET).SpawnDev.AI.ServerHost- the desktop HTTP host (Kestrel, :11434 Ollama drop-in).SpawnDev.AI.Demo- the Blazor WASM demo (GitHub Pages).
- NEVER trim, NEVER AOT the demo/host publishes. ILGPU resolves methods (Math.Clamp etc.) via
reflection at runtime; a trimmed publish kills the worker with
MissingMethodExceptioninRemappedIntrinsics(hit on the very first Pages deploy, 2026-07-04).PublishTrimmed=false,RunAOTCompilation=false- same standing rule as the ML demo. - One transport-free router. Protocol logic lives in
AiApiRouteragainstIAiServerTransport; hosts only implement transports (HTTP, worker MessagePort). Never fork protocol code per host. - Tools register once, serve three surfaces (internal agentic loop, MCP, protocol clients) via
AiToolRegistry. Binary outputs go through the artifact store, never through model context. - Per-kind model residency: one LLM (ModelRegistry) + one image model (AiImageEngine) resident; each kind swaps within its own slot.
- Kill gate servers + verify VRAM freed (
nvidia-smi) when done - resident models hold GBs. - Deploys:
.github/workflows/deploy-to-github-pages.yml(manual dispatch). All PackageReferences must resolve from nuget.org (CI has no local feed) - verify with a cold-cache nuget.org-only restore before triggering.
ML_NO_SESSION_CAPTURE=1- disable pipeline graph captures (SD capture is default-off anyway pendingSpawnDev.ILGPU.ML/Plans/sd-capture-pool-priming.md).SDTURBO_CAPTURE=clip|unet|vae,SDTURBO_FORCE_CAPTURE=1- capture scoping for that work.SPAWNDEV_AI_PORT- host port (default 11434).
Rule 5 applies: gate before Captain sees it.
The test suite: SpawnDev.AI.TestRunner (Captain 2026-08-30 chose the
SpawnDev.SpawnJS.WebWorkers harness style over PMT, and this is a port of it, so the console
contract READY: / TEST: name|result|ms|detail / RESULTS: is identical).
dotnet run --project SpawnDev.AI.TestRunner fast suite, no model download
dotnet run --project SpawnDev.AI.TestRunner -- --heavy + the model-backed chat tests
dotnet run --project SpawnDev.AI.TestRunner -- MultiTurn filter by name
dotnet run --project SpawnDev.AI.TestRunner -- --headed watch it
dotnet run --project SpawnDev.AI.TestRunner -- --dev dev BUILD, not a publish (fast loop)
dotnet run --project SpawnDev.AI.TestRunner -- --url http://localhost:5199/ reuse a running server
🔴 The runner PUBLISHES the demo and serves it (default since 2026-09-14). A dotnet run build is not
the app. Blazor WASM is only relinked and wasm-opt'd on publish, and this suite asserts on TIMINGS.
MEASURED, same card, same utterance, the voice test's warm synthesis: 9,089 ms against a dev-server
build, 4,493 ms published - so every performance number this runner produced before it published was
pessimistic by 2x or more, which is worse than no number: it reads as a product regression rather than as
the harness measuring the wrong artifact. --dev opts back into the fast loop and says so in its output.
PMT has always published for the same reason.
Exit code is the number of failures, so it is usable as a gate. Tests live in
SpawnDev.AI.Demo/Tests/, run in the WINDOW scope only, and are reached with ?tests=1
(&heavy=1, &filter=) so a normal visitor never triggers a model download. They drive
AiWorkerClient - the same client the UI uses - so the worker transport, GGUF decode and KV cache
are all real. Add a test class to AiTestSuiteRunner.TestTypes.
⚠️ Mark anything that loads a modelHeavy = true; the first run pulls hundreds of MB.⚠️ The runner prefers INSTALLED Chrome. Playwright's bundled chromium exposes a SOFTWARE WebGPU adapter, which reads as a hang rather than a config problem.⚠️ A precondition the test is ABOUT must be asserted, never skipped.SkipTestExceptionis for a genuinely absent capability only.
Other live gates: desktop HTTP (all 3 protocol surfaces + /v1/images/generations + the agentic
generate_image loop), and tools/drive-ai-demo.cs - a single-shot UI-level Playwright gate that
types into the composer and checks the answer. Keep it: it covers the Razor UI, which the suite
does not.