Goal
Route Colonizer subagents (and optionally background work) to DeepSeek V4.1 Flash served on a local AMD Strix Halo box, reached over the operator's tailnet, for example subagent_model = strix/deepseek-v4-flash.
Model routing landed in #1, but it was verified only against a stub on the host. This server behaves very differently from that stub: it is on another machine, it is slow, it serves one session at a time and its context is capped. This issue is about making that setup work reliably, not just connect.
The endpoint
|
|
| Server |
antirez's DwarfStar ds4-server, ROCm build from antirez/ds4#1036, in Docker |
| Model |
DeepSeek V4.1 Flash Q2 GGUF (763B total, 16B active per decoded token), experts streamed from SSD |
| Hardware |
Ryzen AI MAX+ 395 / Radeon 8060S (gfx1151), 128 GB unified memory |
| URL |
http://<tailnet-ip>:8000 (tailnet only, plain HTTP, no auth) |
| APIs |
Anthropic POST /v1/messages (tools, SSE, thinking), OpenAI chat/responses/completions, GET /v1/models |
| Model id |
deepseek-v4-flash (a compatibility alias; the loaded GGUF decides the model) |
| Concurrency |
one session (--batched-session 1); other requests queue |
| Context |
131072 tokens allocated on the server |
| Speed |
reported on the same chip in the PR: ~160-250 tok/s prefill, ~5-9 tok/s decode (ours not measured yet) |
| Thinking |
on by default; disabled with a thinking: {"type": "disabled"} object |
Provider entry this should work with:
{"id": "strix", "name": "Strix Halo (DeepSeek V4.1 Flash)", "preset": "local",
"base_url": "http://<tailnet-ip>:8000", "auth": "none", "models": ["deepseek-v4-flash"]}
Gaps found in the code (at 59e4506)
- A colony may not reach a tailnet or LAN provider.
colony_routes only special-cases loopback hosts, rewriting them to host.microsandbox.internal and adding the host profile (providers.rs#L127-L135). Any other host goes out on the public profile (sessions.rs#L497-L522). It is unverified whether that profile allows 100.64.0.0/10 or RFC 1918 addresses. The bundled Headscale mesh also allocates from 100.64.0.0/10 (mesh.rs#L209), so a tailnet provider address can collide with the colony mesh. One option: treat private and CGNAT provider hosts like loopback and forward them from the mothership (which is on the tailnet) through host.microsandbox.internal. Another: add an explicit net rule for exactly the provider's host and port.
- Long silent prefill vs. timeouts. The router uses the global
fetch with no dispatcher (router.mjs#L137-L150). That means undici's default 300 s headers and body timeouts. A 50K-token subagent prompt at ~160 tok/s is about 5 minutes before the first token. Claude Code's own stream idle timeout needs raising too (ds4's client guide uses CLAUDE_STREAM_IDLE_TIMEOUT_MS=600000). Proposal: an optional per-provider timeout_secs, applied in the router and mapped to the Claude Code idle timeout when a slow provider is routed.
- No per-provider concurrency. Parallelism is capped globally and per org (sessions.rs#L313-L328), not per provider. Several colonies sending subagents to a one-session server serialize behind each other and then hit the timeouts above. Proposal: an optional per-provider
max_concurrent, coordinated by the mothership since each colony runs its own router, with waiting or fallback when it is full.
- No per-model context window. The server allocates 131072 tokens, but Claude Code sizes compaction for Claude models, so a long subagent can exceed the allocation and fail. Proposal: an optional
context_tokens per provider model, passed to the runner so the agent compacts early, or so the orchestrator refuses to route to a model that is too small.
- The watchdog can read a slow model as a stall. Stall detection is wall-clock time without progress (watchdog.rs#L72-L78). A subagent at 5-9 tok/s, or one still in prefill, may be nudged or flagged. An in-flight provider request should count as activity.
- No health check or fallback. When the box is rebooting or the model is still loading, the router answers
502 provider "…" is unreachable and the subagent fails. Proposal: probe GET /v1/models in Settings (reachable, model loaded) and at colony start, and optionally fall back to a configured Claude model.
- Not yet exercised against a real non-Claude Anthropic-compatible server. The README says so. Claude Code sends tools, thinking,
anthropic-beta headers and count_tokens (the router already estimates when the upstream returns 404, 405 or 501). All of these need an end-to-end check against ds4-server.
Acceptance
Notes
- Given ~5-9 tok/s decode, this fits subagents and background work, not the orchestrator.
- Only one model fits in memory on a 128 GB Strix Halo at a time. The server stays single-purpose, so routing is the only place to manage contention.
Goal
Route Colonizer subagents (and optionally background work) to DeepSeek V4.1 Flash served on a local AMD Strix Halo box, reached over the operator's tailnet, for example
subagent_model = strix/deepseek-v4-flash.Model routing landed in #1, but it was verified only against a stub on the host. This server behaves very differently from that stub: it is on another machine, it is slow, it serves one session at a time and its context is capped. This issue is about making that setup work reliably, not just connect.
The endpoint
ds4-server, ROCm build from antirez/ds4#1036, in Dockerhttp://<tailnet-ip>:8000(tailnet only, plain HTTP, no auth)POST /v1/messages(tools, SSE, thinking), OpenAI chat/responses/completions,GET /v1/modelsdeepseek-v4-flash(a compatibility alias; the loaded GGUF decides the model)--batched-session 1); other requests queuethinking: {"type": "disabled"}objectProvider entry this should work with:
{"id": "strix", "name": "Strix Halo (DeepSeek V4.1 Flash)", "preset": "local", "base_url": "http://<tailnet-ip>:8000", "auth": "none", "models": ["deepseek-v4-flash"]}Gaps found in the code (at 59e4506)
colony_routesonly special-cases loopback hosts, rewriting them tohost.microsandbox.internaland adding thehostprofile (providers.rs#L127-L135). Any other host goes out on thepublicprofile (sessions.rs#L497-L522). It is unverified whether that profile allows 100.64.0.0/10 or RFC 1918 addresses. The bundled Headscale mesh also allocates from 100.64.0.0/10 (mesh.rs#L209), so a tailnet provider address can collide with the colony mesh. One option: treat private and CGNAT provider hosts like loopback and forward them from the mothership (which is on the tailnet) throughhost.microsandbox.internal. Another: add an explicit net rule for exactly the provider's host and port.fetchwith no dispatcher (router.mjs#L137-L150). That means undici's default 300 s headers and body timeouts. A 50K-token subagent prompt at ~160 tok/s is about 5 minutes before the first token. Claude Code's own stream idle timeout needs raising too (ds4's client guide usesCLAUDE_STREAM_IDLE_TIMEOUT_MS=600000). Proposal: an optional per-providertimeout_secs, applied in the router and mapped to the Claude Code idle timeout when a slow provider is routed.max_concurrent, coordinated by the mothership since each colony runs its own router, with waiting or fallback when it is full.context_tokensper provider model, passed to the runner so the agent compacts early, or so the orchestrator refuses to route to a model that is too small.502 provider "…" is unreachableand the subagent fails. Proposal: probeGET /v1/modelsin Settings (reachable, model loaded) and at colony start, and optionally fall back to a configured Claude model.anthropic-betaheaders andcount_tokens(the router already estimates when the upstream returns 404, 405 or 501). All of these need an end-to-end check againstds4-server.Acceptance
subagent_model = strix/deepseek-v4-flashcompletes a subagent task end to end, with the mesh module both on and off. The request arrives asmodel: deepseek-v4-flashand no credential headers are sent.docs/protocol.md§6.1 document the private-network provider path and the new provider fields.Notes