- config.yml: default/commit -> frank-medium (dankserver); task/tiny/smol/vision -> frank-small (dankclaw-3090); vision xhigh - models.yml: add llamacpp (dankclaw-3090) provider; frank-large preserve_thinking + context notes - niri: focus-ring/border/shadow layout, hot-corners off, re-add noctalia.kdl include; blankpad eDP-1 scale 1.0 - memories: flush rollouts; retire frank_large + marreta_authentik skills; add auth-chain/kv-pool/quadlet-log skills - managed-skills: 40+ new fleet runbook skills - agent/skills: import built-in skill set
98 lines
5.3 KiB
Markdown
98 lines
5.3 KiB
Markdown
---
|
||
name: fleet-second-llm-endpoint
|
||
description: "Add a second LLM endpoint to a host that already serves one on the Dank fleet (new port, unit shape, co-tenancy budget, harness provider/roles, monitoring, cutover, verification) — with the port-bind race, the reasoning_effort template trap, and the tools/--jinja trap that silently break subagents."
|
||
---
|
||
|
||
# Adding a second LLM endpoint to a Dank fleet host
|
||
|
||
Use when a host must serve two models at once (e.g. an iGPU runtime plus a
|
||
second card), or when one endpoint is moving and a new one takes the freed
|
||
hardware. Rebuilt from the 2026-09-16 Dankclaw split (halogen `frank-large` on
|
||
:7770 + llama.cpp `frank-small` on :7771).
|
||
|
||
## 0. Decide the shape before touching hardware
|
||
|
||
- **One provider per HOST:PORT** in `~/.omp/agent/models.yml` (on the box where
|
||
the harness runs, not on the servers). A second endpoint is a new provider
|
||
block, not a second model in the existing one.
|
||
- **Read the budget as an inequality, not a wish.** For a unified-memory runtime
|
||
the engine's own fit check is the arbiter (`budget = host − pinned weights −
|
||
reserve`; `need = KV + arena + vision`); for a discrete card, weight the KV by
|
||
counting only the full-attention layers — and remember `--parallel N` DIVIDES
|
||
`--ctx-size`, it does not multiply.
|
||
- **A co-tenant is a budget line, not a footnote.** Free host RAM by changing
|
||
the POOL or the ARENA (the reserve is a fit-check term, never an allocation),
|
||
and drop optional charges (a vision tower is ~2 GiB) rather than hoping.
|
||
|
||
## 1. Repo first, and retire in the same commit
|
||
|
||
1. New service files: `scripts/<svc>.env` (documented tunables), `.service`
|
||
(root unit for a native binary; a Quadlet only for a container),
|
||
`serve-<svc>.sh` (defaults + hard guards for missing artifacts).
|
||
2. Planner: add the service to `SERVICE_ORDER` AND the target's service set in
|
||
`scripts/deploy-plan.py`, map its paths (including "the unit/env/serve trio
|
||
selects only me"), add it to the inventories in `deploy.sh` and
|
||
`deploy-remote.sh`, and the dispatch case.
|
||
3. Retire what the cutover obsoletes into `.AGENTTRASH/` with a suffix — and
|
||
**check what pointed at it**: bench harnesses, a chat template only the old
|
||
runtime read, and `scripts/test-changed.sh` mappings are the usual victims
|
||
(a mapping to a test file retired earlier fails the pre-commit wrapper
|
||
silently for weeks).
|
||
4. Seams: one bats file per service asserting the *invariants* (`pool == 2 ×
|
||
ctx`, KV bytes ≤ card budget, port separation), not just constant values.
|
||
A negative grep must strip comments (`grep -vE '^\s*#' file | grep -q ...`)
|
||
or it will match the prose that explains the very thing you removed.
|
||
|
||
## 2. The port-bind race (rootless podman)
|
||
|
||
pasta binds the published port at container CREATE. A predecessor that has just
|
||
stopped still holds it, so a naive handler starts a doomed container:
|
||
|
||
```
|
||
Deactivated successfully <- predecessor gone
|
||
pasta: Listen failed for HOST TCP port */PORT: Address already in use
|
||
exit 126 <- only Restart=on-failure saves you
|
||
```
|
||
|
||
Order the handler: **retire the old manager's unit → stop THIS service's own
|
||
unit (no-op on a first cutover) → wait for every socket on the port to clear
|
||
(listener, accepted, TIME_WAIT) → install → start.** Without the second step a
|
||
redeploy deadlocks against its own gate.
|
||
|
||
## 3. Wire the surfaces, and refuse impossible signals
|
||
|
||
- Harness roles: point the small/fast + image roles at the new endpoint, and
|
||
move the vision role with the vision tower (only one endpoint should own
|
||
each capability).
|
||
- `shared/homepage/services.json`: a card per endpoint, probed from the
|
||
monitor's own host (loopback is legal there; the LAN address otherwise).
|
||
- Prometheus: only add a job if the runtime actually exposes Prometheus text.
|
||
Halogen publishes `/health`, `/cache` and `/v1/*` and nothing else — scraping
|
||
it would be red by construction, so the job is REMOVED with the reason written
|
||
down rather than left as a permanent red.
|
||
|
||
## 4. Cutover, then verify by driving it
|
||
|
||
```bash
|
||
./scripts/deploy.sh <target> --service <large> --service <small> # order matters
|
||
```
|
||
|
||
Then, per endpoint: `/health`; `/v1/models` reports the alias; a chat completion
|
||
returns content AND `reasoning_content`; **a request carrying `tools` returns
|
||
`tool_calls`**; an image request is answered (or refused with a named reason);
|
||
the runtime's own speculation counters move; and the unit is `enabled` /
|
||
`generated` with linger on. Read the engine's boot ledger for a silent clamp.
|
||
|
||
Finally spend one real request through the harness per changed role — routing
|
||
labels lie, the servers' own journals do not: `journalctl -u <svc>` shows which
|
||
endpoint actually served it.
|
||
|
||
## Traps that look like model faults
|
||
|
||
| symptom | cause |
|
||
|---|---|
|
||
| `500 tools param requires --jinja flag` | the endpoint's runtime lacks the Jinja chat-template path, so every tool-using agent on it is broken. mainline llama.cpp defaults it on — pin it |
|
||
| `500 Jinja Exception: Unexpected reasoning effort <level>` | the model's own template has a narrower accepted set (Qwen3.8: xhigh/medium/low only) than the harness's levels; fold them in `compat.reasoningEffortMap` |
|
||
| the same request works by hand but a subagent dies | subagents run at a different thinking level — check both |
|
||
| the container dies instantly although the predecessor "stopped" | the port race above |
|