Add vLLM docker-compose and switch runtime model to Qwen3-32B-NVFP4 - #5
Conversation
Stand up the local vLLM server that serves lepenseur's runtime model, and re-point the agent's brain from Nemotron-120B to nvidia/Qwen3-32B-NVFP4. - docker-compose.yml + .env.example: NGC nvcr.io/nvidia/vllm image serving an OpenAI-compatible API on :8000 for the acp backend, tuned for DGX Spark (GB10 Blackwell, 128 GB unified memory; NVFP4 modelopt_fp4, fp8 KV cache, Qwen3 reasoning parser, conservative gpu-memory-utilization). - README "Running the model locally (vLLM)" section. - Switch model refs Nemotron -> Qwen3-32B-NVFP4 across culture.yaml, AGENTS.md (mirrored), explain/catalog.py, README.md, CLAUDE.md; update the test assertion accordingly. - Bump 0.1.0 -> 0.2.0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Review Summary by QodoAdd vLLM Docker Compose and switch to Qwen3-32B-NVFP4 model
WalkthroughsDescription• Add vLLM Docker Compose infrastructure for local model serving - NGC nvcr.io/nvidia/vllm image serving OpenAI-compatible API on :8000 - Tuned for DGX Spark (GB10 Blackwell, 128 GB unified memory) - .env.example with documented tunables (HF_TOKEN, VLLM_MODEL, VLLM_GPU_MEM_UTIL, etc.) • Switch runtime model from Nemotron-120B to Qwen3-32B-NVFP4 - 32B dense reasoning model with thinking mode and extended context (32K native, ~131K via YaRN) - Updated across culture.yaml, AGENTS.md, CLAUDE.md, lepenseur/explain/catalog.py, README.md • Add "Running the model locally (vLLM)" section to README with setup instructions • Bump version from 0.1.0 to 0.2.0 and update CHANGELOG Diagramflowchart LR
A["Nemotron-120B<br/>LatentMoE 120B/12B"] -->|"Model Switch"| B["Qwen3-32B-NVFP4<br/>Dense 32B Reasoning"]
C["Manual Setup"] -->|"Docker Compose"| D["vLLM Server<br/>OpenAI API :8000"]
B -->|"Served by"| D
E[".env.example<br/>Configuration"] -->|"Configures"| D
D -->|"Connected by"| F["acp Backend<br/>Culture Mesh"]
File Changes1. docker-compose.yml
|
Code Review by Qodo
1.
|
- Security: drop --trust-remote-code (Qwen3-32B-NVFP4 loads without it; the flag
would let model-repo code run in-container alongside HF_TOKEN + the cache mount).
Documented the swap caveat in README.
- Reliability: env_file -> { path: .env, required: false } so a fresh checkout
without a .env still runs (only HF_TOKEN needs it).
- Reliability: HF cache mount now ${HF_CACHE:-${HOME:-/root}/.cache/huggingface}
so it never collapses to /.cache/huggingface when HOME is unset; HF_CACHE
documented in .env.example.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Thanks Qodo — all three fixed in a3ac62e:
|
|
…st-remote-code guidance - Qodo #4 (bug): `model switch --apply` to a non-MTP model now writes .env but does NOT recreate the container (the template ships the MTP primary's incompatible flags); prints the lines to remove. New --force overrides. Guards against taking a healthy deployment down. Tests added. - Qodo #5 (maintainability): MTP compose flags centralized in catalog.mtp_compose_command_items() — one source of truth for the templates and switch's removal notice; new drift test asserts the packaged templates contain them. - Qodo #1 (security): added --trust-remote-code + HF_TOKEN safety guidance to both compose templates and env.example (leave HF_TOKEN empty for public defaults / use a minimal-scope read-only token; pin trusted revisions). - Qodo #3 (doc): documented the ticket's unsloth baseline serve command and why we deliberately deviate (the baseline export drops the MTP head) + that the decode comparison is against the load-tested mmangkad baseline. Qodo #2 (test unsloth not sakamakismile): pushback in the PR thread — the unsloth baseline NVFP4 export drops the MTP head (~0% acceptance), so it cannot exercise MTP; sakamakismile is the grafted re-export that makes MTP work (issue #26). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Promote the MTP 27B to fleet default primary; archive mmangkad Promote sakamakismile/Qwen3.6-27B-Text-NVFP4-MTP from candidate to the fleet's default primary, replacing mmangkad/Qwen3.6-27B-NVFP4. The tool-calling gate that kept it a candidate is now closed: served through the production compose it emits a valid qwen3_coder tool call, completes a full tool round-trip, keeps its reasoning trace, and runs MTP spec-decode at 78.6% draft acceptance with tool calling on (~2.4x decode, 8 -> ~19 tok/s, ~71 GB; both `model assess` probes finish=stop). - catalog: role_hint primary <-> candidate swap (sakamakismile <-> mmangkad) - gateway _DEFAULT_PRIMARY, whoami _DEFAULT_MODEL - both env.example + docker-compose templates (single + fleet vllm-primary): bake the MTP serve flags (--speculative-config / --trust-remote-code / --language-model-only / --tokenizer override / --max-num-seqs=2, quant modelopt) so a fresh `model init && model serve` of the default just works - switch: invert _serve_notices (non-MTP target -> "remove these 4 lines"; was "add" for the candidate); force the MTP seq cap to 2 - culture.yaml model field; docs reframed (candidate -> primary, baseline -> archived former primary) - tests updated; mmangkad retained as the MTP tokenizer source + only vision-capable 27B Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Address Qodo review on #28: switch --apply safety, DRY MTP flags, trust-remote-code guidance - Qodo #4 (bug): `model switch --apply` to a non-MTP model now writes .env but does NOT recreate the container (the template ships the MTP primary's incompatible flags); prints the lines to remove. New --force overrides. Guards against taking a healthy deployment down. Tests added. - Qodo #5 (maintainability): MTP compose flags centralized in catalog.mtp_compose_command_items() — one source of truth for the templates and switch's removal notice; new drift test asserts the packaged templates contain them. - Qodo #1 (security): added --trust-remote-code + HF_TOKEN safety guidance to both compose templates and env.example (leave HF_TOKEN empty for public defaults / use a minimal-scope read-only token; pin trusted revisions). - Qodo #3 (doc): documented the ticket's unsloth baseline serve command and why we deliberately deviate (the baseline export drops the MTP head) + that the decode comparison is against the load-tested mmangkad baseline. Qodo #2 (test unsloth not sakamakismile): pushback in the PR thread — the unsloth baseline NVFP4 export drops the MTP head (~0% acceptance), so it cannot exercise MTP; sakamakismile is the grafted re-export that makes MTP work (issue #26). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * switch: drop the unused 'messages' param from _apply_env_only It writes .env + emits the blocked-on-compose-edits result; it never used the parser/quant messages (those are only for the restart path in _apply_switch). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Triage of Qodo findings on #37: - #1 (rule 796119): CHANGELOG `~/.model-gear` → `$HOME/.model-gear`. (The .py --help/docstring keep `~/.model-gear` to match the repo-wide CLI help text; Qodo's docs/config rule only flags the markdown.) - #2 (unreadable/dir .env crash): main() preflights that .env is a regular file and wraps the read/update/write in try/except OSError -> EXIT_ENV_ERROR, matching _read_key()'s graceful degradation. - #3 (under-scoped bump): a new documented capability is a minor, not a patch — 0.18.1 -> 0.19.0. - #4 (unhandled chmod): os.chmod is now best-effort (try/except OSError with a note), so a chmod-unsupported FS doesn't crash after a successful write. - #5 (unvalidated --bytes): reject `< 16` (128-bit floor) with a user error before generating, so no weak key or token_urlsafe stack trace. New tests: too-few-bytes and non-regular-file .env. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ey (#37) * Add scripts/gen-api-key.py to generate/rotate the served-API bearer key `CULTURE_VLLM_API_KEY` gates the vLLM API (and is mandatory before exposing it via `model tunnel`), but there was no first-class way to mint it. This adds a small, stdlib-only generator that: - creates the key with `secrets.token_urlsafe` and NEVER hardcodes a secret, so the script is safe in the open-source repo; the key only lands in the gitignored deployment `.env` (written 0o600); - hides the key by default (no echo into logs/scrollback); `--show` prints it, `--force` rotates an existing key; - resolves the deployment dir like the `model` CLI (`--dir` > $MODEL_GEAR_DIR > ~/.model-gear), and runs from a wheel install (no model_gear import). Tests cover set/rotate/refuse-overwrite/missing-dir/show/no-leak/0o600. README "Expose the API" section now points at it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Address PR #37 review: harden gen-api-key.py + correct semver bump Triage of Qodo findings on #37: - #1 (rule 796119): CHANGELOG `~/.model-gear` → `$HOME/.model-gear`. (The .py --help/docstring keep `~/.model-gear` to match the repo-wide CLI help text; Qodo's docs/config rule only flags the markdown.) - #2 (unreadable/dir .env crash): main() preflights that .env is a regular file and wraps the read/update/write in try/except OSError -> EXIT_ENV_ERROR, matching _read_key()'s graceful degradation. - #3 (under-scoped bump): a new documented capability is a minor, not a patch — 0.18.1 -> 0.19.0. - #4 (unhandled chmod): os.chmod is now best-effort (try/except OSError with a note), so a chmod-unsupported FS doesn't crash after a successful write. - #5 (unvalidated --bytes): reject `< 16` (128-bit floor) with a user error before generating, so no weak key or token_urlsafe stack trace. New tests: too-few-bytes and non-regular-file .env. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Scaffold _readiness.py via AUDIO_TEMPLATES so `model init --fleet --audio` writes it (Dockerfile.parakeet COPYs it — clean init would otherwise fail `docker compose build stt`). [Qodo #2, comment 3402397497] - Remove the inline fallback copy of the readiness decision from listen_server.py and add a drift test asserting the vendored twin matches the canonical model_gear/realtime/_readiness.py. [Qodo #4] - Log CUDA readiness-probe failures (type + message) before returning 503 so operators can tell driver-down / OOM / stale-context apart. [Qodo #3] - audio-smoke.py now exercises /v1/audio/speech and wires --stt-url to a direct-Parakeet check (was claimed-but-untested). [Qodo #5] - docs/realtime-pipeline.md uses $HOME/.model-gear, not ~/.model-gear (portability rule). [Qodo #1, comment 3402397490] 275 tests pass; black/isort/flake8 + markdownlint clean. Bump 0.20.0 -> 0.20.1. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… + smoke (closes #39, #40) (#41) * spec: model-gear owns the audio I/O surface (devague /think) Work issues #39 (Parakeet STT 500s, CUDA-unknown) and #40 (deployed :8080 facade missing /v1/audio/transcriptions) backwards into one converged spec: model-gear takes ownership of the audio stack, wiring Parakeet STT + Magpie TTS + the vendored realtime facade into its compose template and `model fleet up`. Decisions captured: full-stack ownership (both /v1/audio/* routes), cheap model-ready Parakeet healthcheck, restart-clears-stale-CUDA-context runbook. Unblocks reachy-mini-cli's "hey reachy" wake-word. Includes the converged spec + the .devague frame state (evidence trail). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * plan: model-gear audio surface — 6 tasks, 5 waves (devague /spec-to-plan) Forward leg from the converged audio-ownership spec. Six file-disjoint, TDD-gated tasks covering all 24 coverage targets: t1 vendor Parakeet server + cheap model-ready probe t2 build Dockerfiles (facade + parakeet) t3 wire the audio stack into the compose template (sole compose owner) t4 bring it up via `model fleet up` t5 docs/realtime-pipeline.md + README Audio I/O + CUDA restart runbook t6 smoke/acceptance harness (openapi, WAV 200, healthcheck flip) Waves: [t1,t5] -> t2 -> t3 -> t4 -> t6 (serial after wave 0 by the compose chokepoint). Risks carried: CUDA root-cause unconfirmed, fleet gating on the GB10 budget, cheap-probe wording reconciliation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(audio): real Parakeet readiness probe + realtime-pipeline docs + smoke Implements the three real gaps found after reconnaissance (t2/t3/t4 — the audio overlay, Dockerfiles, fleet wiring — already existed in templates/fleet/): - t1: Parakeet /v1/health/ready now reflects REAL readiness (model loaded + a trivial CUDA op) instead of returning {"status":"ready"} unconditionally — the misleading-health bug behind #39. The pure decision lives in stdlib-only model_gear/realtime/_readiness.py (CI-unit-tested, 8 cases), vendored into the Parakeet build context and COPY'd into the image so the container imports it without the wheel. (decision c16: a cheap probe, not a full transcription each interval.) - t5: docs/realtime-pipeline.md — ownership of the live :8080 facade, fleet bring-up, the prior drift (#39/#40), health/readiness, and the stale-CUDA restart runbook. - t6: scripts/audio-smoke.py — stdlib-only live smoke (openapi lists both routes; WAV POST -> 200 {text}); reproduces #39's repro. Live run is GPU-box-bound, not CI. 271 tests pass; black/isort/flake8 + markdownlint clean. Refs #39 #40 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: bump version 0.19.0 -> 0.20.0 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: sync uv.lock to 0.20.0 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style: black-format listen_server.py (fix lint) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: markdownlint ignores docs/specs + docs/plans (devague artifacts) The devague-exported spec/plan H1 is the verbatim announcement sentence, which ends in a period (MD026/no-trailing-punctuation). These are generated planning artifacts like docs/superpowers/** — ignore them in lint. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(audio): address Qodo review of #41 - Scaffold _readiness.py via AUDIO_TEMPLATES so `model init --fleet --audio` writes it (Dockerfile.parakeet COPYs it — clean init would otherwise fail `docker compose build stt`). [Qodo #2, comment 3402397497] - Remove the inline fallback copy of the readiness decision from listen_server.py and add a drift test asserting the vendored twin matches the canonical model_gear/realtime/_readiness.py. [Qodo #4] - Log CUDA readiness-probe failures (type + message) before returning 503 so operators can tell driver-down / OOM / stale-context apart. [Qodo #3] - audio-smoke.py now exercises /v1/audio/speech and wires --stt-url to a direct-Parakeet check (was claimed-but-untested). [Qodo #5] - docs/realtime-pipeline.md uses $HOME/.model-gear, not ~/.model-gear (portability rule). [Qodo #1, comment 3402397490] 275 tests pass; black/isort/flake8 + markdownlint clean. Bump 0.20.0 -> 0.20.1. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>



What & why
lepenseur names a vLLM runtime model in
culture.yaml/AGENTS.mdbut shipped no way to actually serve it. This PR adds that serving infrastructure and switches lepenseur's brain tonvidia/Qwen3-32B-NVFP4(a 32B dense NVFP4 reasoning model with a thinking mode — it still emits a reasoning trace before its answer, the property that suits a deep thinker).Changes
Serving infra (new):
docker-compose.yml— NGCnvcr.io/nvidia/vllmimage serving an OpenAI-compatible API on:8000for theacpbackend. Tuned for DGX Spark (GB10 Blackwell, 128 GB unified memory): NVFP4modelopt_fp4, fp8 KV cache, Qwen3 reasoning parser,ipc: host, HF-cache volume, conservativegpu-memory-utilization=0.6, generous healthcheckstart_periodfor the first-run weight download..env.example— all tunables documented (HF_TOKEN,VLLM_MODEL,VLLM_SERVED_NAME,VLLM_PORT,VLLM_MAX_MODEL_LEN,VLLM_GPU_MEM_UTIL).Model switch (Nemotron-120B → Qwen3-32B-NVFP4):
culture.yamlmodel:+ Runtime paragraph,AGENTS.md(kept verbatim-mirrored),lepenseur/explain/catalog.py,README.md,CLAUDE.md.tests/test_cli.py:assert "Nemotron"→assert "Qwen3"(thevllm-local/assertions stay valid).Not touched on purpose:
docs/superpowers/*(dated scaffold records) and the historical0.1.0CHANGELOG entry.Verification
uv run pytest -n auto— 21 passed.black/isort/flake8/bandit/markdownlint-cli2— clean (matching CI invocations).docker compose config— parses with env substitution.lepenseur whoami→model: vllm-local/nvidia/Qwen3-32B-NVFP4;explain backendmentions Qwen3, no Nemotron.Notes for review
Image tag
nvcr.io/nvidia/vllm:26.04-py3is the main unknown — confirm against build.nvidia.com/spark/vllm that the pinned tag has NVFP4 support.The
nvidia/checkpoint is ModelOpt (TRT-LLM-oriented). If vLLM rejects it, the README documents the vLLM-nativeRedHatAI/Qwen3-32B-NVFP4fallback.lepenseur (Claude)
🤖 Generated with Claude Code