Providers
locac talks to a model over one of four wire protocols. A "provider" is one of those protocol adapters plus a base URL and an API key, which is why pointing locac at a self-hosted vLLM is the same operation as pointing it at Anthropic, only with different values.
Two ways to name a model
# 1. A built-in provider, by name
locac run "…" --provider anthropic --model claude-opus-4-8
# 2. Any endpoint, by protocol + URL
locac run "…" --api openai-completions --base-url https://your-host/v1 --model your-model
--model is always required. Give neither form and locac refuses to start:
Provide either --provider <name> --model <id>, or --api <api> --base-url <url> --model <id>.
The same two shapes exist in ~/.locac/config.json as provider + model, or api + baseUrl +
model. A third form, customProvider, names a saved endpoint from the config's providers[]
array, which expands to the second form before resolution. See
Configuration.
Wire protocols
--api value | Request shape | Notes |
|---|---|---|
anthropic-messages | Anthropic Messages | Auth style is selectable; see below |
openai-completions | OpenAI Chat Completions | The adapter appends /chat/completions to your base URL |
openai-responses | OpenAI Responses | Carries a reasoning budget as reasoning.effort |
ollama-chat | Ollama chat | Local, no API key |
anthropic-messages is not Claude-only. Amazon Bedrock, Google Vertex, z.ai, DeepSeek, Moonshot
and any local server exposing /v1/messages speak it too.
openai-completions base URLs must include the version segment the server expects (/v1,
/openai/v1, /api/v1), because the adapter appends only /chat/completions.
Built-in providers
Anthropic-compatible
All auth with a bearer token except native Anthropic, which uses x-api-key.
--provider | Default base URL | Base-URL override | API key | Context |
|---|---|---|---|---|
anthropic | https://api.anthropic.com | ANTHROPIC_BASE_URL | ANTHROPIC_API_KEY | 200,000 |
zai | https://api.z.ai/api/anthropic | ZAI_BASE_URL | ZAI_API_KEY | 128,000 |
deepseek | https://api.deepseek.com/anthropic | DEEPSEEK_BASE_URL | DEEPSEEK_API_KEY | 128,000 |
moonshot | https://api.moonshot.ai/anthropic | MOONSHOT_BASE_URL | MOONSHOT_API_KEY | 128,000 |
The three non-Anthropic vendors are pinned to 128,000 rather than inheriting the 200,000
default, and the direction of that error is deliberate. Claiming a window a model does not have is
the one mistake that kills a run outright: compaction believes it has tens of thousands of tokens of
headroom, never fires, and the provider rejects the request. 128k is the floor across each vendor's
current lineup. Under-claiming costs only some early elision, and if your model is bigger you can raise it
per-model with the config's models{} map.
OpenAI-compatible
All use Authorization: Bearer; there is no auth-scheme knob for this protocol.
--provider | Default base URL | Base-URL override | API key |
|---|---|---|---|
openai | https://api.openai.com/v1 | OPENAI_BASE_URL | OPENAI_API_KEY |
groq | https://api.groq.com/openai/v1 | GROQ_BASE_URL | GROQ_API_KEY |
openrouter | https://openrouter.ai/api/v1 | OPENROUTER_BASE_URL | OPENROUTER_API_KEY |
together | https://api.together.xyz/v1 | TOGETHER_BASE_URL | TOGETHER_API_KEY |
xai | https://api.x.ai/v1 | XAI_BASE_URL | XAI_API_KEY |
deepseek-openai | https://api.deepseek.com/v1 | DEEPSEEK_OPENAI_BASE_URL | DEEPSEEK_API_KEY |
moonshot-openai | https://api.moonshot.ai/v1 | MOONSHOT_OPENAI_BASE_URL | MOONSHOT_API_KEY |
moonshot-openai-cn | https://api.moonshot.cn/v1 | MOONSHOT_OPENAI_CN_BASE_URL | MOONSHOT_API_KEY |
vllm | http://localhost:8000/v1 | VLLM_BASE_URL | VLLM_API_KEY |
lmstudio | http://localhost:1234/v1 | LMSTUDIO_BASE_URL | LMSTUDIO_API_KEY |
ollama-openai | http://localhost:11434/v1 | OLLAMA_OPENAI_BASE_URL | OLLAMA_API_KEY |
Defaults for this family: context window 128,000, output cap 8,192, reasoning off.
The three local servers carry a placeholder key (local, lm-studio and ollama respectively)
used when their *_API_KEY variable is unset. They accept any value, but some of them return 400 on
a missing Authorization header, so locac always sends one.
Note that DeepSeek and Moonshot appear twice. Each vendor exposes both an Anthropic-shaped and an
OpenAI-shaped endpoint; pick whichever protocol you prefer. Both read the same DEEPSEEK_API_KEY /
MOONSHOT_API_KEY.
Ollama (native)
--provider | Default base URL | Base-URL override | API key |
|---|---|---|---|
ollama | http://localhost:11434 | OLLAMA_HOST | none |
Context window 8,192, output cap 2,048, cost zero. This is the native ollama-chat protocol; the
ollama-openai row above is the same server through its OpenAI-compatible surface.
Authentication
| Protocol | Header | Configurable |
|---|---|---|
anthropic-messages | x-api-key (default) or Authorization: Bearer | --auth-scheme x-api-key|bearer |
openai-completions | Authorization: Bearer | No. Uniform across every server |
openai-responses | Authorization: Bearer | No |
ollama-chat | none | n/a |
Key resolution, highest precedence first:
--api-keyon the command lineapiKeyin the config file, either literal or a"$VAR"/"${VAR}"reference expanded at run time- The provider's own environment variable from the tables above
An unset $VAR reference resolves to nothing rather than to an empty string, so the run fails
loudly instead of sending a blank credential.
The key is never written to a finding, a transcript, a session record or a log, and never reaches the dashboard; the web UI is handed the placeholder
•••• redacted ••••instead. The one place it does appear islocac config show, which prints the effective configuration including a literal key. Do not paste that output anywhere.
Output-token field
openai-completions servers disagree about which field caps output. OpenAI's o-series and GPT-5.x
reject max_tokens outright, while most other servers implement only that one. locac resolves it
like this:
| Model | Field sent |
|---|---|
The built-in openai provider | max_completion_tokens (pinned) |
Every other built-in openai-completions provider | max_tokens |
A custom --api openai-completions endpoint | max_completion_tokens, because a custom model defaults to reasoning: true |
…with reasoning: false in its config entry | max_tokens |
So the reasoning flag on a custom model is the switch: it says "this is a reasoning model", and
its only effect is choosing between the two output-cap fields. If your self-hosted server rejects
max_completion_tokens, set "reasoning": false on that model.
Reasoning effort
Only two adapters carry a reasoning budget:
| Protocol | Field sent |
|---|---|
openai-completions | reasoning_effort |
openai-responses | reasoning.effort |
anthropic-messages, ollama-chat and anything added later carry no such field, so setting a level
against them is refused rather than silently dropped:
locac: reasoning effort "high" ignored — anthropic-messages does not carry a reasoning budget.
Two vocabularies exist, on purpose:
--effortand the TUI's/effortmenu offer exactlylow,medium,high. A picker has to produce a value that lands on whatever endpoint happens to be configured, and those three are what every OpenAI-compatible server implements. Offeringxhighwould promise a level that 400s on vLLM, LM Studio, Groq, OpenRouter and Together.reasoningEffortinconfig.jsonaccepts any lowercase token matching^[a-z][a-z0-9-]{0,15}$, sominimal,xhighandmaxall pass. There you are naming one endpoint you built and understand, so your own file is the authority. The charset is narrow because that string lands in the TUI footer (where a control byte garbles the frame), back intoconfig.json, and on the wire.
A level that arrived from the config still shows up as a row in the /effort menu, so the picker
can never contradict the badge drawn above it.
Custom endpoints
Anything that speaks one of the four protocols works. On the command line:
locac run "…" \
--api openai-completions \
--base-url http://10.0.0.7:8000/v1 \
--api-key '$MY_LAB_KEY' \
--model qwen3-coder-480b \
--context-window 262144
Saved in ~/.locac/config.json as a named provider, selectable with --custom-provider or from the
TUI's /model menu:
{
"providers": [
{
"name": "lab-vllm",
"api": "openai-completions",
"baseUrl": "http://10.0.0.7:8000/v1",
"apiKey": "$MY_LAB_KEY",
"models": [
{ "id": "qwen3-coder-480b", "contextWindow": 262144, "reasoningEffort": "xhigh" }
]
}
]
}
Defaults applied to a fully custom --api + --base-url model: context window 200,000, output
cap 8,192, reasoning on, cost zero. Set contextWindow per model, because it drives compaction
pressure, and it is the one value locac cannot infer for an endpoint it has never seen.
finishReasonOptional
Some proxies never send finish_reason. locac normally reads its absence as a truncated stream and
retries, which against such an endpoint means retrying every turn. Setting finishReasonOptional
on a custom model turns that detection off for it.
Only set it if the endpoint genuinely never sends the field. It is the signal that catches a proxy silently cutting off a long reasoning turn, and turning it off costs you that.
Cost accounting
Every built-in provider ships with zero cost rates, so the footer's $ segment stays hidden
until you supply real numbers. Rates are per million tokens, in a custom provider's model entry:
{ "id": "your-model", "cost": { "input": 3, "output": 15, "cacheRead": 0.3, "cacheWrite": 3.75 } }
Cost is reported, never acted on: no budget is denominated in dollars, and locac eval ab reports
each arm's token cost but never scores it.
Next
Environment variables lists every variable named above, plus the ones that control the shell, the editor and colour output.