feat(runner): advertise local models and their capabilities on heartbeat #57
feat/advertise-inference-models
into main
Why
The routed model picker in Anvil is populated by the runner, not the server: Anvil.Agents.Inference.list_inference_models/1 unions metadata["inference_models"] across the online inference runners in scope.
The heartbeat only ever sent parallel and slots_busy. That union was therefore always empty, and the picker rendered the configured model as unchangeable text — indistinguishable from a provider that cannot enumerate. This is the missing runner-side half, documented on the server since the feature landed (docs/agents.md, “Routed discovery has a runner-side half”).
What it sends
{"name": "llama3.2:latest", "capabilities": ["completion", "tools"]}
Anvil filters on these. An agent chat always sends a tool set, so a model without tools fails on its first call — offering it is offering a guaranteed error.
Compatibility and failure behaviour
- A model whose capabilities Ollama does not report is sent with the key omitted, not as an empty list. The server treats that as unknown and keeps the model — the right behaviour when nothing is known.
- A bare string is still a valid entry server-side, so a fleet on an older runner keeps working.
- Best-effort by design. Ollama being down or slow must never cost a heartbeat: a failed lookup omits the key and the heartbeat goes out regardless. The previously reported list then stays in place server-side, where sending an empty list would retract it. The lookup has its own 3s timeout, well inside the heartbeat interval.
Verification
End to end against a local Anvil dev instance with this runner and a local Ollama:
- the picker lists the local models, grouped by provider, each tagged with the runner serving it
- selecting one routes the run through this runner and the reply comes back credited to it
gemma3:1breports["completion"]and is correctly hidden when the chat requirestools(menu 23 → 19)
Pairs with fangorn/anvil#246, which adds the capability filtering on the server.
🤖 Generated with Claude Code