Providers
Provider implementations live in go-llm-router (v0.6.0), an external module extracted from Agenvoy. Agenvoy builds every agent through router.New(cfg) and calls one Agent.Send() surface — no per-vendor code remains in this repository.
Supported list
Eleven vendors. Where a vendor offers both pay-per-token and subscription access, both routes sit under one entry in the TUI — it asks which one after you pick the provider, so Codex and xAI OAuth are authentication methods there, not separate providers. The TUI /model add list therefore has twelve rows: the eleven vendors plus Local/Custom (compat). GET /v1/providers flattens the same set into thirteen entries, giving codex and grok-oauth their own IDs so an API client can address one auth route directly; compat is returned alongside them as a fourteenth row. Each row carries logged_in, which is meaningful only for the three OAuth routes (codex, grok-oauth, copilot).
| Provider | ID | Auth | Notes |
|---|---|---|---|
| OpenAI | openai · codex |
API key or OAuth | API key uses Chat Completions / Responses. The OAuth route runs on your ChatGPT / Codex subscription with no API key, over SSE |
| Anthropic Claude | claude |
API key | Messages API; parallel tool use on by default |
| Google Gemini | gemini |
API key | gemini-2.x / 3.x families |
| xAI Grok | grok · grok-oauth |
API key or OAuth | API key is pay-per-token; the OAuth route runs on your xAI subscription |
| GitHub Copilot | copilot |
OAuth | Device-code login flow, on your GitHub subscription |
| DeepSeek | deepseek |
API key | deepseek-chat (tool use) and deepseek-reasoner |
| Mistral | mistral |
API key | Mistral's own hosted models |
| NVIDIA NIM | nvidia |
API key | Nemotron, Llama, Mistral, and other hosted open-weight models; free tier needs no billing setup |
| Ollama Cloud | ollama-cloud |
API key | Ollama's hosted models; quota shown as a percentage |
| OpenRouter | openrouter |
API key | Aggregator — routes to models from many vendors through one key |
| Cloudflare | cloudflare |
API token + account ID | Workers AI; optional gateway ID |
Anything OpenAI-compatible
Beyond those eleven, the compat entry (Local/Custom) is not a twelfth vendor — it is the escape hatch that makes the list open-ended. Point it at any OpenAI-compatible /v1 base URL, with an optional key, and it becomes a usable backend: Ollama, LM Studio, vLLM, LiteLLM, a self-hosted gateway, a vendor that shipped an OpenAI-shaped API last week. Register as many as you need — each one is its own named entry.
Two local endpoints are built in (configs/jsons/local_compat.json): Ollama Local at http://localhost:11434/v1 and Llama.cpp Local at http://localhost:8080/v1. /model add probes both with GET /models in parallel (500 ms budget) and lists the ones that answer at the top of the provider list, going straight to model selection with nothing written to config.json. Endpoint models are registered as <name>@<model> with the endpoint name lowercased (ollama@gemma3:4b); the older compat[NAME]@<model> form is still accepted and rewritten whenever config.json is loaded or saved.
Model lists are fetched live when a model is added (TUI /model → add, or GET /v1/provider/:provider/models); for a local or custom endpoint the list comes from the endpoint's own GET /models. There are no static model catalogs in the repository.
Configuration
Everything is managed from the TUI or the local HTTP API — there is no agen model CLI subcommand:
| Task | TUI | HTTP |
|---|---|---|
| Add / remove a provider or model | /model → add; d on a model row removes it |
POST /v1/models, DELETE /v1/models/*name |
| Reorder fallback priority | — | GET POST /v1/model/priority — {models} |
| Set a model's tier | /model, t on a model row |
POST /v1/model/tier — {model, tier} |
| Pick the dispatcher model | /model → dispatch |
GET POST /v1/model — {dispatcher} |
| Pick the summary model | /model → summary |
GET POST /v1/model — {summary} |
| Pick the image generator | /model → image |
GET POST /v1/model — {image}, a provider endpoint rather than a model name |
| Pick the speech-to-text model | /model → stt |
GET POST /v1/model — {stt}, chosen from GET /v1/model/audio |
| Pick the text-to-speech model | /model → tts |
GET POST /v1/model — {tts}, chosen from GET /v1/model/audio |
| Pick a session's model | /model (or Shift+W / Shift+S to cycle auto and registered models) |
POST /v1/session/:id — {model, reasoning} |
| Store a credential | /key |
POST /v1/provider/:provider/key |
| OAuth login | /model |
GET /v1/provider/:provider/oauth (SSE device code) |
| Clear an OAuth login | /model |
DELETE /v1/provider/:provider/oauth |
| Check quota / balance | Shift+U |
GET /v1/providers/quota |
Model routing is one object: GET / POST /v1/model reads and partially updates dispatcher, summary, image, stt, and tts together. A field left out is untouched, "" clears it. In config.json these land as dispatcher_model, summary_model, image_generator, stt_model, and tts_model.
Audio routing is separate from the model registry. stt and tts are not picked from the models you registered with /model add — they are queried live from whichever of OpenAI and Gemini currently hold a credential, and only those two providers back audio today. Selecting off for tts also removes generate_audio from the tool set; selecting off for stt stops read_files from transcribing audio and video, and makes inbound Telegram / Discord voice messages refuse with a hint to pick a model.
Credentials (API keys, OAuth tokens) live in the OS keychain under service agenvoy, never in plain JSON. config.json keeps only the list of stored key names under keys.
The daemon watches config.json; a write reloads the agent registry (and reconnects Telegram / Discord) without a restart.
Model priority and tiers
Registered models are stored as an ordered list under models in config.json. That order is the fallback priority: the first entry is tried first, the last is the final line of defense. POST /v1/model/priority moves the listed names to the front in the given order and keeps the rest after them.
Each model can carry a tier in model_tag ({"<provider>@<model>": "<tier>"}):
| Tier | Meaning |
|---|---|
S |
Strongest — code and work that asks for depth or precision |
A |
Default for most work, one step below the flagship |
B |
Mainstream mid tier |
C |
Fast and cheap; calls tools reliably as instructed |
pass |
Never picked by auto routing or subagents; last in fallback; usable when set for a session |
An untiered model follows the built-in naming rules. Tiers are read per request, so a change applies without a restart.
Dispatcher model
The dispatcher LLM decides which worker model handles each task. It runs in exec.Start through ResolveAgent → SelectAgentNames, before Execute() enters its iteration loop, receiving the registered model list, the current tier assignments, the user input, and a hint about any matched skill. Its routing call is issued at ReasoningNone with a 30-second timeout.
Routing is skipped when it cannot matter: a model named explicitly by the caller is used as-is (and fails if unregistered), a session bound to a model other than auto uses that model, and a registry with only one model returns it directly.
The dispatcher returns a comma-separated list of model names. Names that are unknown or cooling down are dropped; the remaining registered models are appended in priority order, and pass-tier models always go last. When the same base model is registered under several providers, the list prefers codex / grok-oauth, then copilot, then the direct API, then openrouter. If the dispatcher call fails, the next dispatcher candidate is chosen by a fixed provider ranking; if none answers, the priority order alone decides.
Set the dispatcher with /model → dispatch in the TUI, or POST /v1/model with a dispatcher field.
Reasoning levels
go-llm-router normalizes reasoning onto one scale — none, low, medium (default), high, xhigh, max — and maps it per provider (Claude thinking budgets, Gemini thinking budgets, OpenAI effort, ...). Aliases minimal, extra, and ultra map to low, xhigh, and max. Levels outside a model's supported range are clamped rather than rejected.
The level is passed explicitly through each Send call; there is no global reasoning setting in config.json. Cycle it with Shift+A / Shift+D in the TUI.
Fast mode
Shift+F toggles fast mode, which passes provider.ModeFast through the router so supported backends request a faster service tier. Support is model-specific (core.SupportFast) — for example recent OpenAI generations, Claude Opus 4.8 / Opus 5, most Grok models, and selected Gemini families. Unsupported models silently fall back to the default tier. Fast mode is process-local and not persisted.
Adding a custom OpenAI-compatible endpoint
Use Local/Custom (compat) and point it at any endpoint that accepts the OpenAI Chat Completions schema. URL convention follows Zed: enter the URL up to /v1 (e.g. http://192.168.1.10:4000/v1); the router appends /chat/completions. The built-in Ollama and llama.cpp ports need no entry.
Storage split (URL vs key)
| What | Where | API |
|---|---|---|
| URL | ~/.config/agenvoy/config.json compats[] — {provider, url}, provider name uppercased |
config.UpsertCompat / config.GetCompatURL (internal/session/config) |
| API key | OS keychain | keychain.Set("COMPAT_<NAME>_API_KEY", value) |
GetCompatURL checks compats first, then the built-in local endpoints. There is no COMPAT_<NAME>_URL keychain key — it was removed after a bug where the TUI wrote the URL to config while the runtime read the keychain and always fell back to localhost.
Tested compat targets
| Target | Works | Notes |
|---|---|---|
| Ollama | Yes | built in at http://localhost:11434/v1 |
| llama.cpp server | Yes | built in at http://localhost:8080/v1 |
| LM Studio | Yes | |
| vLLM | Yes | --enable-auto-tool-choice --tool-call-parser <name> for tool use |
| LiteLLM proxy | Yes | virtual key as Bearer token |
| Groq / Together / DeepInfra / Fireworks | Yes | |
| Azure OpenAI | No | needs an api-key header (not Bearer) plus ?api-version= — not supported |
Send timeout and failure handling
| Layer | Value | Catches |
|---|---|---|
| Provider HTTP client | set inside go-llm-router per provider |
Transport-level stalls |
AgentSendTimeoutSec |
limits.agent_send_timeout_seconds in config.json, default 600 |
Exec-layer ceiling via context.WithTimeout |
| Unresponsive watchdog | probe every 30 s; after a failed probe, retry every 10 s; 3 failures switch model | A stream that hangs without erroring |
| Health check | 10 s | Liveness probe of each fallback candidate |
On failure the exec layer makes up to three attempts on the same model for a timeout (15 s apart), retries a rate limit on the same model after 5 / 10 / 15 s, and registers a 30-minute cooldown for rate-limit and quota errors; a quota error switches immediately. It then falls back to the next healthy model from a different provider. See the Execution Engine page for the full escalation table.