Development preview — open source, no release tagged yet
One address for your local AI.
Connect the AI apps you already use. ModelSaar sends each request to a suitable available model, chosen from the task, verified model capabilities and the limits you set.
Spend less time switching models and configuring apps. Keep control of where your requests run.
Needs Python 3.10+ and an inference server you already run (Ollama is detected automatically). The installer pins one candidate commit (see the roadmap for its review status) and never downloads models.
curl -fsSL https://modelsaar.com/install.sh | shPrefer to read it first? Inspect the installer or install by hand.
“Fix the failing pytest in src/app.py” + tools: [bash]
- task: coding
- needs: tool calling
- context: ~3 200 tokens
| deployment | hard checks |
|---|---|
| qwen3-coder (30B) — selected | ✓ tools verified by probe |
| gemma4 (31B, thinking) | ✓ tools verified by probe |
| qwen3.6 (35B, thinking) | ✓ tools verified by probe |
Tool calling is a hard requirement; every candidate passed the tools probe, so none is excluded. On coding tasks the measured calibration success favours qwen3-coder (2/2, versus 1/2 and 0/2 within the run's 400-token budget), and the request is forwarded unchanged — the client keeps its own agent loop and runs the tool itself.
Several good models, one app at a time
Once you run more than one local model, every app needs its own model setting, and every task needs you to remember which model handles code, which one can read an image, which one is fast enough for chat. Change a model in Ollama and you change it in four places.
ModelSaar sits between your apps and your inference servers. Apps connect once, to one address, and ask for a virtual model called auto. For each request the gateway checks which of your deployments can actually do the job, picks one on evidence, forwards the request unchanged and records why.
Three things stay distinct:
- Apps
- Open WebUI, OpenCode, agents, scripts — anything that speaks the OpenAI Chat Completions API. They keep their own tools, memory and loops.
- Inference backends
- Ollama, vLLM, llama.cpp server and similar. They load weights and run on your hardware. ModelSaar does not replace them.
- Models
- The weights those backends serve, with their own licences. ModelSaar does not ship models or lower their hardware requirements.
What a routing decision looks like
Every request produces a record like these. The scenarios below are scripted illustrations using the three models of a recorded test run — not live inference.
General chat a short question, no tools, no images
“Give me one sentence of encouragement before an exam.”
- task: conversation
- needs: nothing special
- context: ~40 tokens
| deployment | hard checks |
|---|---|
| qwen3-coder (30B) — selected | ✓ eligible |
| gemma4 (31B, thinking) | ✓ eligible |
| qwen3.6 (35B, thinking) | ✓ eligible |
All three can answer. Measured evidence from the calibration run gives qwen3-coder the best conversation success at the lowest latency, so the quality profile picks it. Had the evidence been missing, the tie would resolve to the lowest-overhead deployment and the record would say uncalibrated.
Coding with tools an agent asks to fix a test and offers a shell tool
“Fix the failing pytest in src/app.py” + tools: [bash]
- task: coding
- needs: tool calling
- context: ~3 200 tokens
| deployment | hard checks |
|---|---|
| qwen3-coder (30B) — selected | ✓ tools verified by probe |
| gemma4 (31B, thinking) | ✓ tools verified by probe |
| qwen3.6 (35B, thinking) | ✓ tools verified by probe |
Tool calling is a hard requirement; every candidate passed the tools probe, so none is excluded. On coding tasks the measured calibration success favours qwen3-coder (2/2, versus 1/2 and 0/2 within the run's 400-token budget), and the request is forwarded unchanged — the client keeps its own agent loop and runs the tool itself.
Question about an image the message carries a base64 image
“What is in this picture?” + image
- task: document_vision
- needs: image input
- context: ~1 250 tokens
| deployment | hard checks |
|---|---|
| qwen3-coder (30B) | ✗ image input unsupported (backend metadata) |
| gemma4 (31B, thinking) — selected | ✓ vision verified by probe |
| qwen3.6 (35B, thinking) | ✓ vision verified by probe |
A failed capability check can never be outscored: qwen3-coder is excluded with the reason recorded. Between the two vision-capable models the recorded run picked gemma4 on its measured evidence (calibration success 0.80 versus 0.47 overall). The image is never dropped or described away — if no deployment could take it, the request would fail with blockers and remedies instead.
Tool result comes back the second turn of a tool call issued by gemma4
assistant: tool_call(id=call_7f2…) → tool: “14:05” → continue
- task: agent
- needs: tool calling + continuity
- tool round trip: call_7f2…
| deployment | hard checks |
|---|---|
| qwen3-coder (30B) | ✗ would hand the tool history to another model |
| gemma4 (31B, thinking) — selected | ✓ issued the tool call |
| qwen3.6 (35B, thinking) | ✗ would hand the tool history to another model |
The gateway remembers which deployment issued the tool-call ids. The follow-up turn stays on it even when another candidate would score higher, and the record marks failover_allowed: false: if gemma4 fails now, the client gets the error rather than a silent switch to a model that never saw the tool call.
Nothing qualifies an image question when only qwen3-coder is configured
“What is in this picture?” + image — with one deployment
- task: document_vision
- needs: image input
- deployments: 1
| deployment | hard checks |
|---|---|
| qwen3-coder (30B) | ✗ image input unsupported (backend metadata) |
Automatic routing with one model simply uses that model whenever it qualifies. When it does not, the answer is an HTTP 422 with the blocker (image input required; capability vision is unsupported) and a remedy (use a deployment whose vision capability is supported) — never a degraded answer that pretends the image was read.
Read the full pipeline — requirements, hard filters, ranking, continuity — in How it works.
Three steps to a first routed request
-
Install ModelSaar
Python 3.10 or newer on macOS, Linux or WSL2. The one-line installer uses
uv,pipxorpip --user— whichever you have — and pins one candidate commit.curl -fsSL https://modelsaar.com/install.sh | sh -
Start the gateway
The installer has already run
modelsaar init: it found Ollama on127.0.0.1:11434(an inference server with at least one model must already be running), wrote a private config and keys, and listed the models it discovered. Now start the gateway and leave it running.modelsaar serveInstalled from source instead? Run
modelsaar initfirst (once; it refuses to overwrite an existing config), thenmodelsaar serve. -
Connect an app
Point the app at the gateway and pick
autoas the model. Print the API key in a terminal, then paste the printed line (it starts withmsk_) into the app's key field:head -1 ~/.modelsaar/keys/api_keysBase URL: http://127.0.0.1:8484/v1 Model: auto API key: msk_… (the line printed by the command above)One command per app prints its exact settings:
modelsaar client-config open-webui,modelsaar client-config opencode,modelsaar client-config openclawormodelsaar client-config hermes.
Verify from a second terminal with modelsaar test: it first identifies the gateway on the configured port (an unrelated service there is reported as a collision and receives no key), lists the models the gateway advertises and sends one chat completion through auto, printing which deployment answered and the request id — the routing attribution that shows ModelSaar handled it. Full details, including Docker and remote hosts, in Get started.
What works today, and how we know
Compatibility is a tested property of an exact client, protocol and backend combination. Each row says what kind of evidence backs it.
| area | what | evidence | notes |
|---|---|---|---|
| Interface | OpenAI-compatible Chat Completions (/v1/chat/completions, streaming SSE), /v1/models, /healthz, /readyz | fixture tests live | Responses API and Anthropic Messages return an explicit 501. |
| Interface | /v1/embeddings with identity-anchored alias contracts | fixture tests | Real-model embeddings NOT RUN; contract, identity anchoring, deadlines and output validation are fixture-tested only. |
| Backend | Ollama (discovery via /api/tags, /api/show, /api/ps; inference via its OpenAI-compatible layer) | fixture tests live | Ollama 0.32.13 with 8 models on an ASUS GX10, 2026-10-11. |
| Backend | Generic OpenAI-compatible server (vLLM, llama.cpp server, LM Studio, …) with declared models; vLLM /tokenize used for token counts | fixture tests | Not run against a real vLLM/llama.cpp server yet. |
| Client | Open WebUI | documented | Configuration generated by modelsaar client-config open-webui; real client workflow not run. |
| Client | OpenClaw | documented | modelsaar client-config openclaw; real client workflow not run. |
| Client | OpenCode (V1 and V2 config schemas) | documented | modelsaar client-config opencode (V1) or modelsaar client-config opencode-v2 (V2); real client workflow not run. |
| Client | Hermes Agent | documented | modelsaar client-config hermes; real client workflow not run. |
| Behaviour | Tool calls and tool-result continuation on the same deployment; session pinning via X-ModelSaar-Session / X-OpenWebUI-Chat-Id | fixture tests live | Live: direct HTTP checks (scripts/live_client_checks.py), not a client's own workflow. |
| Behaviour | Strict JSON schema, image input and context-size routing; unsupported media rejected before dispatch | fixture tests live | |
| Behaviour | Pool memory budgets (incl. assumed residency between polls), uncertain-occupancy holds on stream/probe/embedding timeouts, probes under gateway admission | fixture tests | Not exercised on real hardware; the live run predates the 2026-10-12 corrections. |
| Platform | macOS (arm64) and Linux (x86_64): source install, gateway, probes, evaluation | live | Windows: use WSL2; native Windows is untested. |
| Platform | Docker Compose (gateway only; models stay with your runtime) | fixture tests | Fresh-volume start is checked by scripts/compose-smoke.sh (defined in CI; no CI run exists yet for the current revision). |
live ran against a real backend; fixture tests are the repository's test suite against a controlled fake backend (run locally for every number on this site; the same suite runs in CI on push, and a CI run is reported only once one exists for the revision); documented means the configuration was written from the client's own documentation and has not been exercised. Product names identify compatible software; no partnership or endorsement is implied. Details per client and backend: Integrations.
Measured, not promised
In the recorded run of 2026-10-11 (three models on one machine, 32 held-out tasks, 400-token budget), automatic routing and the strongest single model each scored 30 of 32; automatic routing completed 32 requests versus 31 (the image task the fixed model cannot take), recorded zero capability violations versus one, and had a p95 end-to-end latency of 7.958 s versus 4.760 s because two requests went to a slower thinking model. Routing overhead — the decision itself, separate from the response — was 0.98 ms at p95.
That is one run on one machine with a small task set; the confidence intervals overlap. Read the methodology, numbers and limitations, or regenerate every aggregate from the raw per-request records.
Practical questions
What hardware do I need?
Whatever your inference backend needs to run the models you want; ModelSaar adds a small Python process and no GPU requirement of its own. It has been run on a MacBook Air talking to an ASUS GX10 over a private network, and on a plain Linux container against a test backend.
Do I need models already?
Yes. ModelSaar never downloads models and never accepts model licences on your behalf. Pull them with your backend (for example ollama pull gemma4:e4b) first. modelsaar demo starts a clearly labelled simulated backend if you want to try the commands without any model.
Does it use the internet?
Not for routing. The gateway binds to loopback, talks only to the backend addresses you configure, sends no telemetry and never fetches remote media. The installer downloads the ModelSaar source from GitHub at a pinned commit, and your package tool downloads its Python dependencies.
What about cloud models my backend forwards to?
By default (locality_mode: local_only) deployments that Ollama marks as cloud-forwarded are excluded, even when a client asks for them by name. Set allow_remote deliberately if you want them; the decision record shows the locality evidence either way.
I only have one model.
Then auto uses that model whenever it qualifies for the request, and you still get one address, generated keys, a health check and a decision record. Adding a second model later changes nothing in your apps.
Which models are supported?
Any model your backend serves. Capabilities (tools, image input, strict JSON, streaming) are discovered from the backend and verified with short probes; a capability that is unknown never qualifies a model for a request that needs it. A small catalogue supplies optional quality hints that are labelled as priors until you measure.
What happens when nothing qualifies?
You get an HTTP 422 with the blocker for every deployment and a remedy — never a silently truncated history, a dropped image or a weakened schema.
What does it cost? What is the licence?
ModelSaar is free, Apache-2.0 licensed open source. Model weights keep their own licences, which you accept through your backend.
How do I update or uninstall?
Re-run the installer: it pins the newer candidate commit and keeps your config, keys and state. modelsaar uninstall removes only the files ModelSaar created and lists anything it left in place; then remove the package with your package tool. Details in Installation.
How do I see why a model was chosen?
Every response carries X-ModelSaar-Request-Id and X-ModelSaar-Deployment. modelsaar decisions lists recent decisions, modelsaar explain -m "…" dry-runs a request, and /admin/ui shows the status page. Records hold metadata only, never your prompts.
Try it on the models you already have
Python and a running backend are all you need for a first routed request. If something does not work, the issue templates ask for exactly the decision record and doctor output that make it fixable.