wiring diagram
API
Nergate speaks both wire formats over one pool. The prefix names the protocol; after it the vendor's own path is untouched, so any client works by swapping the base URL and the key.
https://universalvoronez.tech/openai/v1https://universalvoronez.tech/anthropicAnthropic SDKs append /v1/messages themselves, which is why their base stops at /anthropic.
Install
One command scans the machine, shows every supported client with a checkbox, and writes the config files for the ones you tick. Everything lands in each client's own config file — no environment variables are touched unless you pick the shell target.
Linux and macOS:
curl -fsSL https://universalvoronez.tech/install.sh | sh Windows, in PowerShell:
irm https://universalvoronez.tech/install.ps1 | iex Non-interactive, for scripts and containers:
curl -fsSL https://universalvoronez.tech/install.sh | sh -s -- --key nrg-... --all Remove it again — every change is reverted from the config files it touched:
curl -fsSL https://universalvoronez.tech/install.sh | sh -s -- --uninstall | --key nrg-... | Your key. Prompted for if unset (or NERGATE_API_KEY). |
| --targets a,b | Configure these clients only: opencode, anthropic, codex, crush, droid, continue, aider, zed, qwen, goose, llm, shell. |
| --models a,b|all | Which models to write into the multi-model clients (opencode, Crush, Zed, Droid, Continue, llm). Interactive runs ask for all of them or a hand-picked list. |
| --model <id> | Default model for single-model clients (Claude Code, Codex, Aider, Qwen, Goose, Droid). Interactive runs ask with a picker. |
| --all | Take every detected client, skip the prompts. |
| --uninstall | Strip the Nergate block out of the configs it wrote. |
Every file is backed up next to itself as *.nergate-backup before it is touched,
and only the nergate entries are rewritten — your other providers, models and
settings stay exactly as they were.
Quickstart
Issue a key in the console. It is shown once, starts with nrg-, and goes in the Authorization header.
curl https://universalvoronez.tech/openai/v1/chat/completions \
-H "Authorization: Bearer nrg-..." \
-H "content-type: application/json" \
-d '{
"model": "zai-org/GLM-5.2",
"messages": [{"role": "user", "content": "hello"}]
}' Python, official OpenAI client:
from openai import OpenAI
client = OpenAI(base_url="https://universalvoronez.tech/openai/v1", api_key="nrg-...")
client.chat.completions.create(
model="zai-org/GLM-5.2",
messages=[{"role": "user", "content": "hello"}],
) opencode, in ~/.config/opencode/opencode.jsonc:
{
"provider": {
"nergate": {
"npm": "@ai-sdk/openai-compatible",
"name": "Nergate",
"options": {
"baseURL": "https://universalvoronez.tech/openai/v1",
"apiKey": "{env:NERGATE_API_KEY}"
},
"models": {
"zai-org/GLM-5.2": { "name": "GLM 5.2" }
}
}
},
"agent": {
"nergate-max": { "options": { "reasoningEffort": "max" } }
}
} Anthropic SDKs need only the base — they append the rest:
from anthropic import Anthropic
client = Anthropic(base_url="https://universalvoronez.tech/anthropic", api_key="nrg-...")
client.messages.create(
model="zai-org/GLM-5.2",
max_tokens=256,
messages=[{"role": "user", "content": "hello"}],
) Inference
| POST | /openai/v1/chat/completions | Chat completion, streaming supported |
| GET | /openai/v1/models | Models you may call, with your rates |
| POST | /anthropic/v1/messages | Same pool, Anthropic wire format |
| POST | /anthropic/v1/messages/count_tokens | Token count for a request |
| GET | /anthropic/v1/models | Models in Anthropic shape |
| GET | /api/pricing | Public rate board, no key needed |
| GET | /api/status | Operational or degraded, model count |
| GET | /api/health | Pool, catalog and uptime |
Bodies are forwarded as they came, apart from what is described below, so provider
parameters work as documented upstream — stream, tools, reasoning_effort, temperature, max_tokens.
Streaming is a normal SSE stream. Nergate adds stream_options.include_usage so the final chunk carries token counts, which is
also how your spend gets metered.
System prompts
Three ways to set one. Each overrides the one before it.
On the key
Applies to every call made with that key. Set it in the console, or over the API:
curl -X PATCH https://universalvoronez.tech/api/me/keys/<key-id> \
-H "content-type: application/json" -b cookies.txt \
-d '{
"systemPrompt": "You are a terse code reviewer.",
"systemPromptMode": "replace",
"defaultModel": "zai-org/GLM-5.2"
}' | prepend | Your prompt goes first, the caller's own system message is kept. |
| replace | The caller's system and developer messages are dropped. Useful against clients that inject a large prompt of their own. |
Per call, with headers
X-Nergate-System-Prompt: Answer in one sentence.
X-Nergate-System-Mode: replace In the body
A plain {"role": "system"} message works as usual, and is what the two
settings above act on.
Reasoning effort
Nothing is applied unless you ask for it: a model you call plain gets no reasoning_effort at all, and behaves as its own default. To pick a level, append
it to the model id — the gateway strips the suffix and passes the effort up:
zai-org/GLM-5.2 # nothing added
zai-org/GLM-5.2-low # fast and cheap
zai-org/GLM-5.2-max # thinks hardest Not every family accepts every level. GPT-OSS takes only low/medium/high, while GLM, DeepSeek and Kimi also take xhigh and max. Asking for a level a model does not support is a 400 unsupported_effort listing what it does accept — the gateway never silently
substitutes another level. GET /openai/v1/models lists the variants each model
has.
Four places can set it, later wins:
| 1 | key default | Set on the key in the console, applies to every call |
| 2 | reasoning_effort | The usual field in the request body |
| 3 | X-Nergate-Effort | Header, for clients that will not send the field |
| 4 | -max suffix | On the model id, wins over everything else |
In opencode the level is an agent, not a model: the installer writes each model once and adds nergate-low…nergate-max alongside. Switch with opencode --agent nergate-max — the agent's effort applies to whichever model is
selected.
X-Nergate-Effort: max Models without reasoning — Llama, Gemma, Granite, Phi, Mellum — ignore all of this and have no variants.
Account and keys
These use a session cookie from /api/auth/login, not your API key — they are what
the console itself calls.
| POST | /api/auth/login | Sign in, sets the session cookie |
| POST | /api/auth/logout | Sign out |
| GET | /api/me | Quota, spend, usage by model, your keys |
| POST | /api/me/keys | Issue a key — the secret is returned once |
| PATCH | /api/me/keys/:id | Set system prompt, mode, default model |
| DELETE | /api/me/keys/:id | Revoke a key |
Catalog and rates
GET /api/pricing is public and lists every model with your price per 1M tokens
and the upstream list price. GET /openai/v1/models needs a key and returns the same
rates in OpenAI shape, filtered to what your key may call.
Billing is prompt_tokens x input rate + completion_tokens x output rate, charged
against your quota. Spend and remaining balance are on the console, and the full board is on the pricing page — 26 models.
Errors
Errors come back in OpenAI shape: {"error": {"message", "type", "code"}}.
| 401 | invalid_api_key | Key missing, unknown or revoked |
| 402 | quota_exceeded | Your credit quota is spent |
| 403 | model_not_allowed | This key may not call that model |
| 400 | unsupported_effort | Model does not take that effort level |
| 400 | context_length_exceeded | max_tokens is larger than the model window |
| 400 | unsupported_content | Images sent to a model without vision |
| 429 | rate_limit | Too many calls this minute |
| 502 | pool_exhausted | Every upstream attempt failed — retry |
| 503 | no_accounts | No capacity right now — retry shortly |
A failing upstream account is retried on another one automatically, so pool_exhausted means several accounts failed in a row for the same call.