The LLM gateway
criogenai-gateway is a server that speaks the OpenAI Chat Completions
API and the Anthropic Messages API. Any OpenAI or Anthropic client (the
official SDKs, curl, LangChain, Claude tooling, CriogenAI itself) can use
it, and behind it each route can be any model CriogenAI supports: a
client written for OpenAI can use Claude, one written for Claude can use
Gemini or a model on your own GPUs.
Run it#
# gateway.toml: secrets are never in this file, only the names of the
# environment variables that hold them.
[[routes]]
name = "support"
models = [
{ provider = "openai", model = "gpt-5-mini", api_key_env = "OPENAI_API_KEY" },
{ provider = "ollama", model = "llama3.2" }, # used when OpenAI fails
]
[[keys]]
name = "team-a"
key_env = "GATEWAY_KEY_TEAM_A"
routes = ["support"]
requests_per_minute = 60 # optional
tokens_per_minute = 100000 # optional
budget = { tokens = 5000000, period = "month" } # optional: "day", "month" or "total"criogenai-gateway --config gateway.toml --listen 0.0.0.0:4000 --log-usageCall it#
from openai import OpenAI
client = OpenAI(base_url="http://localhost:4000/v1", api_key="<team-a key>")
reply = client.chat.completions.create(
model="support",
messages=[{"role": "user", "content": "Hello"}],
)Streaming (stream=True, with stream_options.include_usage), tool calls
and response_format work as with OpenAI. Anthropic's SDK works the same
way, on /v1/messages:
from anthropic import Anthropic
client = Anthropic(base_url="http://localhost:4000", api_key="<team-a key>")
reply = client.messages.create(
model="support", max_tokens=512,
messages=[{"role": "user", "content": "Hello"}],
)Streaming, tools and tool results, images and system work as with
Anthropic; fields another provider could not honour (thinking,
top_k, …) are ignored.
Or embed it#
use std::sync::Arc;
use criogenai_gateway::Gateway;
use criogenai_providers::{Endpoint, ProviderModel};
let openai = ProviderModel::new(
Endpoint::openai(std::env::var("OPENAI_API_KEY")?),
"gpt-5-mini",
)?;
let local = ProviderModel::new(Endpoint::ollama(), "llama3.2")?;
let team_key = std::env::var("GATEWAY_KEY_TEAM_A")?;
let gateway = Gateway::builder()
.route_with_fallbacks("support", [Arc::new(openai) as _, Arc::new(local) as _])
.key("team-a", team_key, ["support"])
.build()?;
// An axum Router: serve it, or nest it in your own server.
let listener = tokio::net::TcpListener::bind("127.0.0.1:4000").await?;
axum::serve(listener, gateway.router()).await?;What it does#
| Feature | Behaviour |
|---|---|
| Routes | The model a client asks for; each lists models tried in order |
| Fallback | On rate limits, overload, outages or exhausted quota; a stream only before its first byte |
| Keys | Keys per route (Authorization: Bearer or x-api-key), stored as SHA-256 digests; without keys the server only listens on localhost unless --allow-open |
| Rate limits | Requests and tokens per minute per key; over the limit: 429 key_rate_limited with Retry-After |
| Budgets | Tokens per UTC day, month or in total per key; spent: 429 budget_exceeded, with Retry-After until the period ends |
| Errors | OpenAI's or Anthropic's error format (by endpoint), with CriogenAI's codes and matching HTTP statuses |
| Usage | One record per request: key, route, model, tokens, latency, outcome; no content; never slows a request |
| Disconnects | When a client goes away, the call to the provider stops, and the tokens it may have used stay charged to its key |
| Concurrency | At most 1024 requests in flight over all keys (max_concurrent_requests), and max_concurrent per key; over: 429 concurrency_limited |
max_tokens | A client's value is lowered to a cap (max_tokens, default 200000) before it reaches the provider |
| Slow clients | A client that stops reading a stream is cut off, not buffered |
| Upstream errors | A client sees the text of errors it can act on (bad request, too long, rate limit, timeout) and only the kind of the rest; the upstream model is revealed only with expose_model_header = true |
A request's tokens are reserved when it is admitted (its input size and
its max_tokens) and replaced by the real usage when the answer is
complete, so many requests at once cannot overshoot a budget by more than
one request, and a client that hangs up still pays for what it started.
Add allow_insecure_http = true to a model only when it is a server on
your own network reached over plain http:// with a key; otherwise the
gateway refuses to start rather than send the key unencrypted.
Limits and budgets are counted in memory: they start over when the gateway restarts, and two gateway processes count separately. A request in flight may go past a budget; the next one is refused.
Planned: shared counters (Postgres, Redis), budgets in money, and a pass-through mode.