
Unifies diverse LLM providers behind a single OpenAI-style API: streaming-first chat/embeddings/voice/realtime/batch modalities, per-model health-aware fallback chains, outbound control-plane auth and extensibility.
llmleaf is a llm proxy. It proxies different llm providers and their slighty different apis and converts it to a single api surface (enhanced openai-compatible or anthropic).
settings.upstream_streaming = "always" is the default, so chat calls use
the provider's incremental upstream transport even when the consumer requests a non-streaming
response. Set the per-provider policy to "when_requested" or "never" to opt out.POST /v1/responses to any routed
provider — upstreams without a Responses endpoint are served over their
chat-completions wire transparently. Upstream, OpenAI and xAI speak their
Responses APIs by default; OpenRouter's beta POST /responses (signed
open-reasoning replay, routed cost), Groq's beta POST /responses (open
unsigned reasoning), and Azure OpenAI's v1 surface (resource-scoped POST /openai/v1/responses) are per-provider opt-ins
(chat_api = "responses").The use of the websocket api is recommended, this greatly improves the chances of prompt caching actually working.
zai-coding (GLM Coding Plan, /api/coding/paas/v4) and kimi-coding (Kimi for Coding,
api.kimi.com/coding/v1); MiniMax's Token Plan shares the standard endpoint, so
minimax-token-plan is an alias of minimax (only the key differs).echo for local testing.# Run with the embedded dev config (echo provider, key `local-dev:s3cret`)
cargo run -p llmleaf
# …or point at your own config
cargo run -p llmleaf -- llmleaf.tomlCopy llmleaf.example.toml, fill in provider credentials (use env:VAR indirection — secrets
never live in the file), and pass it as the argument. Container image: docker buildx bake image
(listens on :8080). Send a request:
curl localhost:8080/v1/chat/completions \
-H "Authorization: Bearer $(printf 'local-dev:s3cret' | base64)" \
-d '{"model":"demo","messages":[{"role":"user","content":"hi"}]}'Base64 the
id:passwordcredential with no trailing newline — useprintf(orbase64 -w0), notecho. A stray newline is encoded into the value, so the decoded password becomespw\nand fails the hash check →401 unknown api key, even when the configuredpw_hashis correct.
See llmleaf.example.toml for the full configuration surface (providers, routes, keys, control plane).
Consumer endpoints (OpenAI-compatible unless noted):
| Endpoint | Purpose |
|---|---|
POST /v1/chat/completions |
Chat (SSE streaming) |
POST /v1/messages |
Anthropic Messages dialect |
POST /v1/responses |
OpenAI Responses dialect (stateless; store always false, GET /v1/responses/{id} is a 404-by-design stub) |
POST /v1/embeddings |
Embeddings |
POST /v1/rerank |
Rerank (Cohere/Jina/OpenRouter dialect) |
POST /v1/audio/speech, GET /v1/audio/voices
|
Text-to-speech |
POST /v1/audio/transcriptions |
Speech-to-text |
GET /v1/realtime |
OpenAI Realtime (WebSocket) |
POST /v1/batches, GET /v1/batches/{id}[/results]
|
Batch jobs (ids HMAC-signed + owner-bound with [server].batch_id_secret) |
GET /v1/models, GET /v1/openapi.json, GET /healthz
|
Discovery & health |
Read-only admin (optional token): GET /admin/routes, /admin/health, /admin/keys.
Official client SDKs for 6 languages live in clients/.
Two strictly separated planes. The core (data plane) is the proxy; the control plane is
reached only outbound — the core pulls identity/verdicts/topology and pushes usage, never the
reverse. A pulled topology ([control.topology]) lets the controller also serve provider and route
configuration, diffed against the previous pull on every refresh so resources are added, updated,
and removed incrementally on top of the immutable config file. See SOUL.md for the full
design constitution. To build a compatible controller, see the
external control-plane implementation guide.
flowchart LR
Cons["Consumers<br/>OpenAI · OpenRouter · Anthropic"] --> Surf["Compat surfaces"]
subgraph Core["llmleaf core — data plane"]
direction LR
Surf --> Auth["authenticate"] --> In["map in"] --> Route["route + fallback"] --> Stream["stream"] --> Out["map out"] --> Ev["emit events"]
end
Route --> Prov["Providers<br/>compiled-in traits · WASM plugins"]
Prov --> Up["LLM providers"]
Ctrl[["Control plane (outbound)"]]
Auth -. "pull identity / verdicts" .-> Ctrl
Route -. "pull topology (providers + routes)" .-> Ctrl
Ev -. "push usage" .-> CtrlThis project is being developed with AI assistance.
Copyright (C) 2026 Fionn Langhans fionnlanghans@codefionn.eu.
llmleaf and its clients are dual-licensed under either the MIT License or the Apache License 2.0, at your option.
llmleaf is a llm proxy. It proxies different llm providers and their slighty different apis and converts it to a single api surface (enhanced openai-compatible or anthropic).
settings.upstream_streaming = "always" is the default, so chat calls use
the provider's incremental upstream transport even when the consumer requests a non-streaming
response. Set the per-provider policy to "when_requested" or "never" to opt out.POST /v1/responses to any routed
provider — upstreams without a Responses endpoint are served over their
chat-completions wire transparently. Upstream, OpenAI and xAI speak their
Responses APIs by default; OpenRouter's beta POST /responses (signed
open-reasoning replay, routed cost), Groq's beta POST /responses (open
unsigned reasoning), and Azure OpenAI's v1 surface (resource-scoped POST /openai/v1/responses) are per-provider opt-ins
(chat_api = "responses").The use of the websocket api is recommended, this greatly improves the chances of prompt caching actually working.
zai-coding (GLM Coding Plan, /api/coding/paas/v4) and kimi-coding (Kimi for Coding,
api.kimi.com/coding/v1); MiniMax's Token Plan shares the standard endpoint, so
minimax-token-plan is an alias of minimax (only the key differs).echo for local testing.# Run with the embedded dev config (echo provider, key `local-dev:s3cret`)
cargo run -p llmleaf
# …or point at your own config
cargo run -p llmleaf -- llmleaf.tomlCopy llmleaf.example.toml, fill in provider credentials (use env:VAR indirection — secrets
never live in the file), and pass it as the argument. Container image: docker buildx bake image
(listens on :8080). Send a request:
curl localhost:8080/v1/chat/completions \
-H "Authorization: Bearer $(printf 'local-dev:s3cret' | base64)" \
-d '{"model":"demo","messages":[{"role":"user","content":"hi"}]}'Base64 the
id:passwordcredential with no trailing newline — useprintf(orbase64 -w0), notecho. A stray newline is encoded into the value, so the decoded password becomespw\nand fails the hash check →401 unknown api key, even when the configuredpw_hashis correct.
See llmleaf.example.toml for the full configuration surface (providers, routes, keys, control plane).
Consumer endpoints (OpenAI-compatible unless noted):
| Endpoint | Purpose |
|---|---|
POST /v1/chat/completions |
Chat (SSE streaming) |
POST /v1/messages |
Anthropic Messages dialect |
POST /v1/responses |
OpenAI Responses dialect (stateless; store always false, GET /v1/responses/{id} is a 404-by-design stub) |
POST /v1/embeddings |
Embeddings |
POST /v1/rerank |
Rerank (Cohere/Jina/OpenRouter dialect) |
POST /v1/audio/speech, GET /v1/audio/voices
|
Text-to-speech |
POST /v1/audio/transcriptions |
Speech-to-text |
GET /v1/realtime |
OpenAI Realtime (WebSocket) |
POST /v1/batches, GET /v1/batches/{id}[/results]
|
Batch jobs (ids HMAC-signed + owner-bound with [server].batch_id_secret) |
GET /v1/models, GET /v1/openapi.json, GET /healthz
|
Discovery & health |
Read-only admin (optional token): GET /admin/routes, /admin/health, /admin/keys.
Official client SDKs for 6 languages live in clients/.
Two strictly separated planes. The core (data plane) is the proxy; the control plane is
reached only outbound — the core pulls identity/verdicts/topology and pushes usage, never the
reverse. A pulled topology ([control.topology]) lets the controller also serve provider and route
configuration, diffed against the previous pull on every refresh so resources are added, updated,
and removed incrementally on top of the immutable config file. See SOUL.md for the full
design constitution. To build a compatible controller, see the
external control-plane implementation guide.
flowchart LR
Cons["Consumers<br/>OpenAI · OpenRouter · Anthropic"] --> Surf["Compat surfaces"]
subgraph Core["llmleaf core — data plane"]
direction LR
Surf --> Auth["authenticate"] --> In["map in"] --> Route["route + fallback"] --> Stream["stream"] --> Out["map out"] --> Ev["emit events"]
end
Route --> Prov["Providers<br/>compiled-in traits · WASM plugins"]
Prov --> Up["LLM providers"]
Ctrl[["Control plane (outbound)"]]
Auth -. "pull identity / verdicts" .-> Ctrl
Route -. "pull topology (providers + routes)" .-> Ctrl
Ev -. "push usage" .-> CtrlThis project is being developed with AI assistance.
Copyright (C) 2026 Fionn Langhans fionnlanghans@codefionn.eu.
llmleaf and its clients are dual-licensed under either the MIT License or the Apache License 2.0, at your option.