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-usage

Call 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#

FeatureBehaviour
RoutesThe model a client asks for; each lists models tried in order
FallbackOn rate limits, overload, outages or exhausted quota; a stream only before its first byte
KeysKeys 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 limitsRequests and tokens per minute per key; over the limit: 429 key_rate_limited with Retry-After
BudgetsTokens per UTC day, month or in total per key; spent: 429 budget_exceeded, with Retry-After until the period ends
ErrorsOpenAI's or Anthropic's error format (by endpoint), with CriogenAI's codes and matching HTTP statuses
UsageOne record per request: key, route, model, tokens, latency, outcome; no content; never slows a request
DisconnectsWhen a client goes away, the call to the provider stops, and the tokens it may have used stay charged to its key
ConcurrencyAt most 1024 requests in flight over all keys (max_concurrent_requests), and max_concurrent per key; over: 429 concurrency_limited
max_tokensA client's value is lowered to a cap (max_tokens, default 200000) before it reaches the provider
Slow clientsA client that stops reading a stream is cut off, not buffered
Upstream errorsA 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.