llmux

One endpoint. Every model.

Nothing leaves your box unless you say so.

llmux is a Go library firstimport 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.

the whole change · any OpenAI client
- base_url = "https://api.openai.com/v1"
+ base_url = "http://localhost:4000/v1"
self-hosted MIT or Apache-2.0 no accounts, bearer tokens only no telemetry v0.1.7
1Go binary — no required runtime dependencies
4sovereignty tiers: local, sovereign, brokered, external
6provider types, one JSON block each
0accounts — an operator-issued bearer token is the whole login

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.

llmuxgate · router · budgets · cache
Your app Any OpenAI SDK, unchanged

One base_url, one virtual key. The gateway decides which port above actually gets dialed.

POST /v1/chat/completions
This is the default posture out of the box. Opting a provider in — 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.
CH·01The wire OpenAI HTTP API, verbatim

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.

↑ request · curl
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.

↓ response · usage block
{ "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.

Coverage Every core route

chat/completions, completions, embeddings, models, plus responses, rerank, moderations, images/generations, audio/speech, and audio/transcriptions / audio/translations (multipart speech-to-text).

Two models Routed vs. forwarded

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.

Accounting Cost, in the response you already read

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.

SelectThe route never touches the data path
Six channels arrive at the core's input edge; exactly one of them is dialed, and what dials it is a resolved route entering on the select line rather than anywhere on the data path. Everything your app holds is the one connection leaving the output edge: the request token and the response token there are the same outline, because the translation happens inside the body. Streaming included — "stream":true returns byte-identical OpenAI SSE no matter which channel answered.
MatrixEndpoint surface 12 routes
MethodRouteModeBehaviour
POST/v1/chat/completionsroutedNative per-provider translation; streaming is byte-identical OpenAI SSE.
POST/v1/embeddingsroutedNative translation on passthrough, gemini, cohere and azure.
GET/v1/modelscatalogEvery model with live price and context window.
GET/v1/catalog.jsoncatalogThe full merged pricing catalog behind that list.
POST/v1/completionsforwardedLegacy text completions — passthrough providers only.
POST/v1/responsesforwardedOpenAI Responses API; the get/cancel lifecycle isn't ported yet.
POST/v1/rerankforwardedReranking, passed through as-is.
POST/v1/moderationsforwardedModeration, passed through as-is.
POST/v1/images/generationsforwardedImage generation, passed through as-is.
POST/v1/audio/speechforwardedText-to-speech, passed through as-is.
POST/v1/audio/transcriptionsforwardedMultipart 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/translationsforwardedSame as transcription, plus translation to English. Also multipart.

17 languages already ship an OpenAI client that works against llmux unchanged — curl, plus:

PythonNode.jsTypeScriptGoRuby PHPJavaC#RustC++C SwiftKotlinElixirRDart

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 →

CH·02The library core/gateway · in your process

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.

in-process · Go
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.

in-process · any language · C ABI
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.

ModesFour ways to run it none of them is the default answer
ModeWhat it isYou getYou pay
Serverllmux serve, shared by many clientsVirtual keys, budgets, rate limits, the console, one place to change routing for a fleetA service to run, a port to secure, an HTTP hop
SidecarThe same binary, spawned and supervised by a language packageAll of the above, with no server to operate; native streaming in every languageA child process, a loopback port
Go libraryimport core/gatewayNo process, no port, no socket — plus per-request facts the HTTP shell flattens awayYou are inside the trust boundary: auth is a call you make
C ABIdlopen libllmux from any languageThe library's benefits from a non-Go hostThe Go runtime in your process, ~12–17 MB, no fork-safety, and real platform gaps
Measured Latency is not the reason

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.

Honest Where the library does not exist

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.

Zero The console is not in your binary

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 →

CH·03The packages 7 direct · 7 sidecar · 1 either

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.

In your processDirect by default7
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 The one that is not a wrapper

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.

Streaming In-process, on your own thread

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.

Platforms Where direct mode exists

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 →

CH·04The gate default deny

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.

TIER 1

Local

Always allowed

Inference 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.

TIER 2

Sovereign

On your say-so

An off-box endpoint the operator personally vouches for — "tier": "sovereign", unverified by anyone else, allowed without allow_egress.

TIER 3

Brokered

Opt-in required

A named third party under a claimed no-train agreement — "tier": "brokered" plus allow_brokered (or allow_egress).

TIER 4

External

Blocked by default

Anything else off-box — OpenAI, Anthropic, Gemini, Cohere, Bedrock, Azure. Requires an explicit "allow_egress": true on that provider.

The gate sits between route resolution and the network call — never after it. An allowed provider gets dialed and logged with its tier; a blocked one is never dialed at all: it terminates on your side of the boundary, the request fails with 403 before a single byte leaves the box, and llmux_egress_blocked_total counts it.
llmux.json
{
  "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 }
  ]
}
403 · blocked, and told why
{
  "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": [] } }

CH·05The routes six provider types

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.

Native adaptersfive, each translating tool-calling, vision and streaming into the canonical OpenAI shape
  • anthropic
  • gemini
  • cohere
  • bedrock SigV4
  • azure
Passthroughone type covering every OpenAI-shaped upstream, including the one running on this box
  • local ollama · llama.cpp · vLLM
  • openai
  • deepseek
  • groq
  • mistral
  • together
  • fireworks
  • xai
  • openrouter
OrderHow a model string resolves first match wins
StepPatternResolves 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.

One logical request, up to three tries. A primary that errors is retried with backoff on 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.
CH·06The controls budgets · keys · cache · cost

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 is 402, over the RPM limit is 429. Spend persists to Postgres; tokens are sha256-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 /ui via go:embed. Plus Prometheus /metrics, structured logs, /health.
Every virtual key carries its own USD budget. Each response's real cost fills the bar; a request that would cross the cap is refused before it is ever dialed402, not a surprise invoice — while everything under the line is billed and added to that key's running spend.
Metering Fail-closed

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.

Open core An optional seam, not a default

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.

BYOK Per-account provider keys

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.

CH·07The panel 3,690 requests · real capture

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.

/ui#usage
llmux admin dashboard, usage tab: request, token and cost totals as cards, then a per-model spend table.
What it cost, by modelRequests, total tokens and cost across every key, then the same numbers broken down per model — exactly what usage.cost already put in each response.
/ui#keys
llmux admin dashboard, keys tab: every virtual key with its name, masked token, budget, current spend as a bar, and RPM limit.
Every key, one bar eachName, masked token, budget vs. spend as a bar that turns red near the cap, and the RPM ceiling — the same shape GET /admin/keys returns.
/ui#models
llmux admin dashboard, models tab: a filterable live catalog with input and output price per million tokens and context window per model.
The live catalog, filterableEvery model llmux can price, with input/output cost per million tokens and context window — filtered client-side as you type.
Live, on a poll
The dashboard refreshes itself every 15s by default (a Live / Paused toggle controls it), and every fetch is same-origin unless you point it at a remote gateway URL.
Ops surface beyond the UI
GET /metrics is a Prometheus endpoint; GET /health is 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.
CH·08The start go 1.25+, nothing else

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.

terminal
# 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
main.go
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")}},
})
quickstart.py
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}
llmux.json
{
  "keys": [
    { "key": "sk-team-a", "name": "team-a",
      "budget_usd": 25.0,          // 0 = unlimited
      "rpm": 60,
      "allowed_models": ["assistant", "cheapest"] }
  ]
}
  1. 01
    BuildGo 1.25+, and at least one provider API key. One binary, no required runtime dependencies — or use the repo's Dockerfile.
  2. 02
    ConfigureDeclare providers in JSON, or set env vars and let llmux auto-detect them — including an on-box local provider from OLLAMA_HOST.
  3. 03
    Point your SDK at itSet base_url = http://host:4000/v1 and a virtual key. The model string picks the route; usage.cost.total_cost carries the price.
  4. 04
    Scale 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.
RefusalsWhat a rejection means error ledger
StatusCodeMeaning
402budget_exceededThe key or account is over its USD budget. Fail-closed: an unreadable spend store is treated as over budget, not unspent.
403egress_not_allowedThe sovereignty gate blocked an off-box provider the operator hasn't opted in.
403model_not_allowedThe key's model allow-list doesn't include the requested model.
429rate_limit_exceededThe key's RPM limit — or a control-plane per-account cap — was hit.
404model_not_foundNo route matches the requested model string.

MIT OR Apache-2.0 · one binary · runs on your own box.