One endpoint. Every model.
Nothing leaves your box unless you say so.
llmux is a Go library first — import the gateway straight into a Go
program and dispatch in-process, no port and nothing started until you ask. The same code also
ships as a single binary that speaks the OpenAI HTTP API: point any OpenAI SDK at it and
get routing, fallback chains, per-key budgets, caching and live cost — across OpenAI, Anthropic,
Gemini, Cohere, Bedrock and Azure, with zero per-language code.
A default-deny sovereignty gate runs before every dispatch, in-process or over HTTP, so inference stays on your box until you explicitly, loggably opt a provider out.
- base_url = "https://api.openai.com/v1"
+ base_url = "http://localhost:4000/v1"
local always allowed
Loopback or a unix socket — Ollama, llama.cpp, vLLM on this box.
sovereign on your declaration
An off-box endpoint you vouch for — set "tier":"sovereign".
brokered opt-in required
A named third party under a claimed no-train agreement.
external blocked by default
OpenAI, Anthropic, Gemini, Cohere, Bedrock, Azure — until you set allow_egress.
One base_url, one virtual key. The gateway decides which port above actually gets
dialed.
allow_egress, or a sovereign/brokered tier — moves it from
blocked to logged-and-allowed; nothing changes silently. Every permitted off-box call is logged with
its tier, and every blocked one increments llmux_egress_blocked_total.Nothing about your client code changes.
Every language already ships a mature OpenAI client that accepts a custom
base_url. Point it at llmux and routing, budgets, caching and cost accounting happen
underneath — no new SDK, no new wire format to learn.
curl http://localhost:4000/v1/chat/completions \
-H "Authorization: Bearer sk-team-a" \
-H "Content-Type: application/json" \
-d '{"model":"assistant",
"messages":[{"role":"user","content":"Hello!"}]}'
The model string is what selects the route — an alias, a
provider/model prefix, a wildcard, or a least-cost pseudo-model. Streaming
("stream":true) returns byte-identical OpenAI SSE, so every language's stream parser
just works unchanged.
{ "usage": {
"prompt_tokens": 9, "completion_tokens": 12,
"total_tokens": 21,
"cost": { "input_cost": 0.0000014,
"output_cost": 0.0000072,
"total_cost": 0.0000086,
"currency": "USD" } } }
The standard OpenAI usage shape plus exactly one additive
extension — a cost object priced from the live catalog, which any OpenAI client
ignores harmlessly if it isn't looking for it.
chat/completions, completions, embeddings,
models, plus responses, rerank, moderations,
images/generations, audio/speech, and
audio/transcriptions / audio/translations (multipart
speech-to-text).
chat/completions and embeddings go through native per-provider
translation on every adapter that has an embeddings API (passthrough, Gemini,
Cohere and Azure — Anthropic and Bedrock have none upstream and return 501). The
other modality routes are proxied only to passthrough providers — a translating
adapter like Anthropic or Gemini answers them with 501, stated here rather than
discovered the hard way.
Each response's usage.cost object carries input_cost,
output_cost, total_cost and currency from the live
pricing catalog — no separate call, no separate dashboard tab to reconcile against.
"stream":true returns
byte-identical OpenAI SSE no matter which channel answered.| Method | Route | Mode | Behaviour |
|---|---|---|---|
| POST | /v1/chat/completions | routed | Native per-provider translation; streaming is byte-identical OpenAI SSE. |
| POST | /v1/embeddings | routed | Native translation on passthrough, gemini, cohere and azure. |
| GET | /v1/models | catalog | Every model with live price and context window. |
| GET | /v1/catalog.json | catalog | The full merged pricing catalog behind that list. |
| POST | /v1/completions | forwarded | Legacy text completions — passthrough providers only. |
| POST | /v1/responses | forwarded | OpenAI Responses API; the get/cancel lifecycle isn't ported yet. |
| POST | /v1/rerank | forwarded | Reranking, passed through as-is. |
| POST | /v1/moderations | forwarded | Moderation, passed through as-is. |
| POST | /v1/images/generations | forwarded | Image generation, passed through as-is. |
| POST | /v1/audio/speech | forwarded | Text-to-speech, passed through as-is. |
| POST | /v1/audio/transcriptions | forwarded | Multipart upload, speech-to-text. No per-minute price exists yet, so a served call meters as an auditable $0 line rather than billing silently. |
| POST | /v1/audio/translations | forwarded | Same as transcription, plus translation to English. Also multipart. |
17 languages already ship an OpenAI client that works against llmux unchanged — curl, plus:
Every one of those talks to llmux over HTTP. It does not have to be a server on the far end of that socket — see the next channel. See every example →
Or don't run a server at all.
llmux is a library first. core/gateway is the whole dispatch
path — routing, retries, failover, the sovereignty gate, BYOK, caching, pricing and metering —
with no HTTP surface of its own. core/server is one shell over it, and not the only
possible one. Import the library and there is no listener, no port, no loopback surface to secure
and no second process to supervise.
gw, err := gateway.New(cfg)
defer gw.Close()
res, err := gw.Chat(ctx, &openai.ChatCompletionRequest{
Model: "gpt-4o-mini",
Messages: []openai.Message{{Role: "user",
Content: openai.Str("hi")}},
})
res.Provider // who actually served, after failover
res.CacheHit // facts the HTTP shell has no field for
The provider call is the only socket this program opens.
gateway.New starts no goroutines and, unless you configured a Postgres DSN, opens
nothing at all.
uint64_t h = llmux_new(NULL, &err);
char* out = llmux_call(h, "chat", req, &err);
puts(out);
llmux_free(out); /* never free() */
llmux_stream(h, "chat", req, on_chunk, NULL, &err);
llmux_close(h); /* idempotent */
Six functions, JSON in and JSON out — the same JSON the HTTP API uses, so
a body that works against /v1/chat/completions works here unchanged.
| Mode | What it is | You get | You pay |
|---|---|---|---|
| Server | llmux serve, shared by many clients | Virtual keys, budgets, rate limits, the console, one place to change routing for a fleet | A service to run, a port to secure, an HTTP hop |
| Sidecar | The same binary, spawned and supervised by a language package | All of the above, with no server to operate; native streaming in every language | A child process, a loopback port |
| Go library | import core/gateway | No process, no port, no socket — plus per-request facts the HTTP shell flattens away | You are inside the trust boundary: auth is a call you make |
| C ABI | dlopen libllmux from any language | The library's benefits from a non-Go host | The Go runtime in your process, ~12–17 MB, no fork-safety, and real platform gaps |
The boundary itself is ~4 µs in-process against ~47 µs over loopback HTTP. Inside a real chat call: ~80–92 µs against ~102–109 µs. Against a model that takes hundreds of milliseconds, that saving is a rounding error, and anyone embedding for the microseconds is optimising the wrong thing.
Prebuilt shared libraries: darwin/arm64 and linux/arm64. linux/amd64 is built in CI only. windows/amd64 and darwin/amd64 have never been built by anyone. If you ship there, the sidecar is the path, and choosing it is a supported outcome rather than a fallback.
Only core/server imports web/, so a host importing
core/gateway alone links zero console bytes —
build tag or no build tag. Two gateways in one process never interfere either: no
package-level state, no package logger.
Two things gateway.New does regardless, said
here rather than left to be discovered: it connects and migrates
eagerly when a Postgres DSN is configured, and it reads os.Getenv for any provider
configured with api_key_env. And Authorize returns a
release function that is never nil and must always be
called — skip it and a budget reservation leaks.
Choose a mode →
Embed in Go →
The C ABI →
Fifteen languages. One engine behind all of them.
Fifteen language packages ship in this repo, and they are wrappers, not reimplementations — one routing engine, one sovereignty gate, one budget ledger, one price catalog, reached from all fifteen. What differs is only how your process reaches it, and the honest default is not the same in every language.
- A child process on loopbackSidecar by default7
-
Each of the seven has a stated reason — a pre-forking host, a runtime whose signal handlers the library replaces, a thread that cannot be retired, or a platform with no library to load. Elixir has no direct mode at all, on purpose.
- Your deployment decidesDepends1
-
Unicorn, Passenger and clustered Puma fork, so they need the sidecar; single-mode Puma, Falcon, Sidekiq and CLI tools do not, and direct is fine there.
Go does not load a shared library and does not cross a C boundary: it
imports core/gateway and calls it. No FFI, no
libllmux, no platform matrix to check — if your
program builds, the gateway is in it. Every other language is reading its own row against
this one.
Direct mode streams: llmux_stream delivers one chunk per callback, and the
callback runs on the thread that made the call — measured
with pthread_self() on darwin/arm64 and linux/arm64, not assumed. Languages that
cannot safely take a callback there say so in their own row above.
Prebuilt shared libraries are darwin/arm64 and linux/arm64. linux/amd64 is built in CI only, and no Windows library ships — not untested, never built. The sidecar has no such gap: it is the same binary, and it cross-compiles everywhere.
A first call in every language → The package index, and the measurement behind each recommendation →
A default-deny gate runs before every dispatch.
Most gateways route your prompts to someone else's cloud and call that
"integration." llmux inverts the default: a loopback or unix-socket base_url is
local and always allowed; any off-box endpoint is blocked until you opt it in,
per provider, never globally. A blocked provider is never dialed — the request gets a
403 and the denial is counted.
Local
Always allowedInference on this box. A loopback URL is always classified local regardless of how
it's marked — you can't mislabel an on-box endpoint into looking remote.
Sovereign
On your say-soAn off-box endpoint the operator personally vouches for — "tier": "sovereign",
unverified by anyone else, allowed without allow_egress.
Brokered
Opt-in requiredA named third party under a claimed no-train agreement — "tier": "brokered" plus
allow_brokered (or allow_egress).
External
Blocked by defaultAnything else off-box — OpenAI, Anthropic, Gemini, Cohere, Bedrock, Azure. Requires an explicit
"allow_egress": true on that provider.
403 before a
single byte leaves the box, and llmux_egress_blocked_total counts it.{
"providers": [
// loopback = local, always allowed
{ "name": "local", "type": "passthrough",
"base_url": "http://127.0.0.1:11434/v1" },
// explicit opt-in for an off-box provider
{ "name": "openai", "type": "passthrough",
"base_url": "https://api.openai.com/v1",
"api_key_env": "OPENAI_API_KEY",
"allow_egress": true },
// a named no-train third party
{ "name": "broker", "type": "passthrough",
"base_url": "https://inference.example.com/v1",
"tier": "brokered", "allow_brokered": true }
]
}
{
"error": {
"message": "sovereignty: provider \"openai\" is a non-local endpoint and egress is not enabled; set \"allow_egress\": true on this provider to permit off-box calls",
"type": "sovereignty_error",
"code": "egress_not_allowed"
}
}
On a fallback chain, a sovereignty-blocked primary is simply skipped so a local fallback can still serve — the gate costs availability of one route, never a silent privacy leak.
The one outbound call the gate does not cover — stated, not buried
The gate governs inference dispatch only. A stock gateway also ships two public
price-catalog feeds (openrouter.ai, raw.githubusercontent.com) and issues a plain
GET at startup and every sync_interval_minutes (default 360) — no
prompt, no completion, no API key, no usage data, just a public price list.
If "no network call unless I ask" matters to you, turn it off — cost accounting still works
offline from the built-in seed catalog: { "pricing": { "sources": [] } }
Native translation where it earns its keep, passthrough for everything else.
Six provider types cover the field: five native adapters that translate tool-calling,
vision and streaming per provider, plus passthrough for any OpenAI-shaped upstream —
including a local Ollama/llama.cpp/vLLM server.
- anthropic
- gemini
- cohere
- bedrock SigV4
- azure
- local ollama · llama.cpp · vLLM
- openai
- deepseek
- groq
- mistral
- together
- fireworks
- xai
- openrouter
| Step | Pattern | Resolves to |
|---|---|---|
| 1 | "assistant" | Exact match — the route whose model is that string. |
| 2 | "claude-*" | The longest matching trailing-* wildcard route. |
| 3 | "*" | The catch-all route, forwarding the requested name unchanged. |
| 4 | "openai/gpt-4o" | Only if no route matched at all: provider/model prefix syntax dials that named provider directly. |
| — | "strategy": "least-cost" | A route with a candidates[] list picks the cheapest by catalog price (input+output per MTok) at request time; the rest become its fallback chain in cost order, unpriced candidates last. |
fallbacks name providers to try in order when the primary fails —
retryable on 429/500/502/503/504 and transport errors, with exponential backoff
(max_retries 2, backoff_ms 200 by default). A sovereignty-blocked primary
is skipped so a local fallback can still serve.
429/500/502/503/504; a fallback the sovereignty gate blocks is skipped
outright, the same as any other unusable candidate — until one serves the call. Your application
sees one response, not the attempts behind it.The controls a fleet of applications actually needs — built in, not bolted on.
Every application gets its own virtual key rather than the master key. Caches never cross a key boundary, and every response already knows what it cost.
- keysVirtual keys & budgets
- Per-key USD
budget_usd,rpm, and a model allow-list. Over budget is402, over the RPM limit is429. Spend persists to Postgres; tokens aresha256-hashed at rest. - cacheExact and semantic
- LRU + TTL exact-match, or embedding-similarity semantic matching — in-memory or shared via Redis, scoped per virtual key so keys never see each other's completions.
- pricingLive, offline-first
- A built-in seed prices requests with zero network access; it auto-syncs from OpenRouter and
LiteLLM when allowed. Merged catalog at
GET /v1/catalog.json. - consoleEmbedded dashboard
- Usage by model, key budgets, and the live catalog — served from the binary at
/uiviago:embed. Plus Prometheus/metrics, structured logs,/health.
402, not a surprise invoice — while everything under the line is billed and added to
that key's running spend.A budgeted key requesting an unpriced model is
refused pre-flight — 403 model_not_priced — before any upstream spend, so an
unmeterable request can never quietly burn real provider budget.
Self-hosted is the default: no telemetry, no central billing. Setting LLMUX_CP_URL
links an optional control-plane for centralized billing across a fleet — same binary, either
way.
With a key-encryption key configured, an account can register its own provider key via
PUT /admin/byok/{account}/{provider}; those calls are then unmetered and never
billed centrally.
One binary, one UI. No separate service to run.
The admin console ships inside the gateway itself — web/ui.html, a
single hand-written page with no framework and no build step, go:embeded into the binary
and served at /ui. There is no Node toolchain anywhere in this repo. The screenshots
below are the real thing, swapped for whichever theme you're reading this page in — captured from a
running gateway after 3,690 requests across seven models and four virtual keys, so every figure in
them was computed by the binary rather than typed in by hand.
usage.cost
already put in each response.GET /admin/keys returns.- Live, on a poll
- The dashboard refreshes itself every 15s by default (a
Live/Pausedtoggle controls it), and every fetch is same-origin unless you point it at a remote gateway URL. - Ops surface beyond the UI
GET /metricsis a Prometheus endpoint;GET /healthis unauthenticated liveness, and with the master key it additionally discloses the full provider/sovereignty posture. Structured (slog) access logs never carry prompt or completion content.
Build it, point a key at it, call it. No account to create.
Go 1.25+ and nothing else — the admin console is a hand-written file, so there's no
Node toolchain anywhere in this repo. No database or cache is required for a single replica. Writing
Go? Skip the binary — gateway.New builds the same dispatch path in-process, with no
goroutines started and no sockets opened beyond an explicitly configured Postgres DSN.
# 1. Build the binary (embeds web/ui.html — no Node, no build step)
make build
# 2. Configure providers
export OPENAI_API_KEY=...
export ANTHROPIC_API_KEY=...
# 3. Run — gateway on :4000, dashboard at /ui
cp llmux.example.json llmux.json
./dist/llmux -config llmux.json
import (
"github.com/vul-os/llmux/core/config"
"github.com/vul-os/llmux/core/gateway"
"github.com/vul-os/llmux/core/openai"
)
gw, err := gateway.New(config.Default()) // no goroutines, no sockets*
defer gw.Close()
res, err := gw.Chat(ctx, &openai.ChatCompletionRequest{
Model: "cheapest",
Messages: []openai.Message{{Role: "user", Content: openai.Str("hi")}},
})
from openai import OpenAI
client = OpenAI(base_url="http://localhost:4000/v1", api_key="sk-team-a")
resp = client.chat.completions.create(
model="cheapest", # least-cost route from your config
messages=[{"role": "user", "content": "hi"}],
)
print(resp.usage)
# usage.cost = {input_cost, output_cost, total_cost, currency}
{
"keys": [
{ "key": "sk-team-a", "name": "team-a",
"budget_usd": 25.0, // 0 = unlimited
"rpm": 60,
"allowed_models": ["assistant", "cheapest"] }
]
}
- 01BuildGo 1.25+, and at least one provider API key. One binary, no required runtime dependencies — or use the repo's
Dockerfile. - 02ConfigureDeclare providers in JSON, or set env vars and let llmux auto-detect them — including an on-box
localprovider fromOLLAMA_HOST. - 03Point your SDK at itSet
base_url = http://host:4000/v1and a virtual key. Themodelstring picks the route;usage.cost.total_costcarries the price. - 04Scale when you need toAdd Postgres (
DATABASE_URL) and Redis (LLMUX_REDIS) for multi-replica keys, spend, rate limits and cache — both optional on one replica.
| Status | Code | Meaning |
|---|---|---|
| 402 | budget_exceeded | The key or account is over its USD budget. Fail-closed: an unreadable spend store is treated as over budget, not unspent. |
| 403 | egress_not_allowed | The sovereignty gate blocked an off-box provider the operator hasn't opted in. |
| 403 | model_not_allowed | The key's model allow-list doesn't include the requested model. |
| 429 | rate_limit_exceeded | The key's RPM limit — or a control-plane per-account cap — was hit. |
| 404 | model_not_found | No route matches the requested model string. |
MIT OR Apache-2.0 · one binary · runs on your own box.