CogniGate
One endpoint in front of every model your applications use.
A self-hosted, multi-tenant LLM gateway: one key and the OpenAI API shape in front of every model your applications use. The edge gateway is Go 1.26 on Fiber v2, with provider plugins, analytics, and a conformance suite that runs in CI.
Built with
Highlights
- ▸Speaks the OpenAI API shape, so existing clients point at one gateway endpoint and keep working.
- ▸Multi-tenant by design: tenants get their own keys in front of every upstream provider.
- ▸A conformance suite runs against the gateway in CI, not just at release time.
From the README

CogniGate
One endpoint in front of every model your applications use
Self-hosted · Multi-tenant · OpenAI-compatible · Open Source
📖 Documentation · 🤖 AI Agents · 🐛 Report Bug · ✨ Request Feature · 💬 Discussions
What Is CogniGate?
CogniGate is a self-hosted, multi-tenant LLM gateway that sits between your applications and the models they call. Your applications hold one CogniGate key and speak the OpenAI API; your provider credentials stay inside the deployment. It is an open-source alternative to OpenRouter and LiteLLM, for teams that would rather not route their traffic — or their keys — through someone else.
Key Capabilities
| Feature | Description |
|---|---|
| OpenAI-Compatible API | Point an existing SDK at CogniGate and change nothing else |
| Capability Aliases | fast, balanced, best and transcribe resolve against the live catalogue, so a client outlives the model names it was written against |
| Fallback Chains | An ordered cascade per tenant, rotating keys within a provider pool before moving to the next candidate |
| Circuit Breakers | Exponential backoff on 429/5xx; an open breaker is skipped rather than retried, and the refusal names every attempt |
| Per-Tenant Quotas | Token and spend caps with soft thresholds, plus an observe mode for sizing them before they refuse anything |
| Provider Keys Stay Put | Held in memory, returned by no route, written to no disk, printed in no log line |
| Durable Usage Metering | Every request recorded out-of-band in PostgreSQL and readable back per tenant, model, provider or key |
Architecture
External Clients
OpenAI SDK / curl / LangChain / LlamaIndex
|
| POST /v1/chat/completions
| Authorization: Bearer cg-...
v
+-------------------------------------------------------------+
| Gateway - Go 1.26 + Fiber v2 - :8080 |
| |
| Auth & tenant Alias & chain Circuit breakers |
| resolution resolution + key rotation |
| |
| Quotas, rate limits, catalogue cache - all in memory |
+-------------------------------------------------------------+
| |
| proxied request | async, buffered
| | POST /api/v1/usage
v v
OpenAI-compatible providers Analytics - :8081
OpenAI, Together, Groq, Java 25 + Spring Boot 4.1
Fireworks, vLLM, Ollama, |
Azure OpenAI, OpenRouter v
PostgreSQL 16 - :5432
The two processes divide along a single line: the gateway owns the request path and holds its configuration in memory, and analytics owns everything durable. A request is never blocked on metering — if analytics is down the gateway keeps serving traffic and buffers what it cannot deliver.
Quick Start
Prerequisites
- Docker & Docker Compose (v2+)
- No Java or Go installation required for running — everything is containerized
One-Command Setup
# Linux / macOS
curl -sSL https://cognigate.vkrishna04.me/install.sh | bash
# Windows (PowerShell)
irm https://cognigate.vkrishna04.me/install.ps1 | iex
The installer clones the repository, generates credentials and starts the stack. To do the same by hand — or if you would rather read the script before running it, which you should:
git clone https://github.com/Life-Experimentalist/CogniGate.git
cd CogniGate
./setup.sh --dev --detach # PowerShell: .\setup.ps1 -Mode dev -Detach
setup.sh expects to be run from inside a checkout; it is the second half of
what the installer does, not a substitute for the clone.
This single command:
- ✅ Copies
.env.example→.env - ✅ Generates a
GATEWAY_BOOTSTRAP_KEYand anANALYTICS_TOKEN - ✅ Builds and starts the three services (Gateway, Analytics, PostgreSQL), waiting until each reports healthy
Or Pull the Published Images
Every push to main publishes both services to the GitHub Container Registry
and to Docker Hub, built for linux/amd64 and linux/arm64. Both are public,
so no docker login is needed to pull them:
docker pull ghcr.io/life-experimentalist/cognigate-gateway:main
docker pull ghcr.io/life-experimentalist/cognigate-analytics:main
The same digests are on Docker Hub, for environments that already authenticate there:
docker pull vkrishna04/cognigate-gateway:main
docker pull vkrishna04/cognigate-analytics:main
docker-compose.yml names those images alongside its build contexts, so a
checkout that does not want to compile anything can fetch them instead:
git clone https://github.com/Life-Experimentalist/CogniGate.git
cd CogniGate
cp .env.example .env
openssl rand -hex 24 # -> GATEWAY_BOOTSTRAP_KEY in .env
openssl rand -hex 32 # -> ANALYTICS_TOKEN in .env
docker compose pull && docker compose up -d --wait
The tags are main for the tip of the default branch and sha-<commit> to
pin one commit of it. latest currently resolves to the same image as main;
once the first release is tagged it will follow releases instead, so main is
the tag that keeps meaning the branch either way. Releases will additionally
carry X.Y.Z and X.Y — none has been cut yet.
Checking where an image came from
Both images are published with signed build provenance and an SPDX SBOM, attached to the image digest and stored in the registry beside it. That attestation lives on the GitHub Container Registry copy, which is where the verification below reads it from. Nothing needs to be cloned to check one:
gh attestation verify oci://ghcr.io/life-experimentalist/cognigate-gateway:main --repo Life-Experimentalist/CogniGate
That confirms the image was built by this repository's workflow at a specific commit, rather than by whoever else could push a tag with the same name. Release binaries carry the same guarantee; the command is in each release's notes.
Verify It's Running
curl -s http://localhost:8080/healthz
# → {"status":"ok"}
Your First API Key
The admin plane is reached with an Authorization: Bearer credential, exactly
like the data plane. Before any tenant exists there is only one such credential:
the bootstrap key that setup generated into .env.
BOOTSTRAP=$(grep '^GATEWAY_BOOTSTRAP_KEY=' .env | cut -d= -f2-)
# 1. Create a tenant.
curl -s -X POST http://localhost:8080/admin/v1/tenants \
-H "Authorization: Bearer $BOOTSTRAP" \
-H "Content-Type: application/json" \
-d '{"name":"my-org"}'
# → {"id":"ten_...","name":"my-org","status":"active",...}
# 2. Register an upstream provider for that tenant. `keys` is a pool — CogniGate
# rotates within it before it ever falls back to another model.
curl -s -X POST http://localhost:8080/admin/v1/tenants/ten_.../providers \
-H "Authorization: Bearer $BOOTSTRAP" \
-H "Content-Type: application/json" \
-d '{"name":"openai","kind":"openai","base_url":"https://api.openai.com/v1","keys":["sk-..."]}'
# 3. Mint a data-plane key. The secret is returned once and never again.
curl -s -X POST http://localhost:8080/admin/v1/tenants/ten_.../keys \
-H "Authorization: Bearer $BOOTSTRAP" \
-H "Content-Type: application/json" \
-d '{"name":"first","plane":"data"}'
# → {"key":{"id":"key_...","prefix":"cg-mP6XKz-R",...},"secret":"cg-...","warning":"..."}
Only a SHA-256 hash and the displayable prefix are stored, so nothing in CogniGate can recover the secret later. If it is lost, revoke the key and mint a new one.
The control plane is in memory. Tenants, keys, providers, aliases, routes and quotas live in the gateway process. Postgres holds usage records and nothing else, so restarting or recreating the gateway container — a
docker compose pull, a config change, a crash — takes the three steps above with it, and the first sign is a401on a key that worked a minute ago. Keep those calls in a script. Making the control plane durable is known work, not a surprise.
These examples assume a POSIX shell. In PowerShell, use Invoke-RestMethod —
PowerShell rewrites the arguments it passes to a native executable, and the
inner double quotes of a JSON body do not survive that rewrite, so curl.exe -d '{"name":"my-org"}' sends {name:my-org} and the gateway rejects it:
$b = (Select-String -Path .env -Pattern "^GATEWAY_BOOTSTRAP_KEY=").Line.Split("=",2)[1]
Invoke-RestMethod -Method Post -Uri http://localhost:8080/admin/v1/tenants `
-Headers @{ Authorization = "Bearer $b" } -ContentType "application/json" `
-Body '{"name":"my-org"}'
Your First Request
curl -s http://localhost:8080/v1/models -H "Authorization: Bearer cg-..."
curl -s -X POST http://localhost:8080/v1/chat/completions \
-H "Authorization: Bearer cg-..." \
-H "Content-Type: application/json" \
-d '{"model":"gpt-4o","messages":[{"role":"user","content":"Hello, CogniGate!"}]}'
Every tenant is created with the portable aliases fast, balanced, best and
transcribe. Sending "model": "balanced" resolves to whatever currently fits
that constraint in the tenant's catalog, so a client can be written once and keep
working as providers ship new models.
An ordered fallback chain is a routing rule:
curl -s -X PUT http://localhost:8080/admin/v1/tenants/ten_.../routing-rules \
-H "Authorization: Bearer $BOOTSTRAP" \
-H "Content-Type: application/json" \
-d '{"match":"gpt-4o","chain":["gpt-4o","llama-3.3-70b","gpt-4o-mini"]}'
Configuration
All configuration is documented in:
.env.example— Environment variables referencecognigate.config.yml— Unified configuration manifest
Environment Variables
| Variable | Default | Description |
|---|---|---|
GATEWAY_BOOTSTRAP_KEY | (auto-generated) | Admin-plane bootstrap credential; min 16 characters, no default |
ANALYTICS_TOKEN | (auto-generated) | Shared secret between the two processes. Analytics refuses to start without it rather than expose an unauthenticated metering API |
SPRING_DATASOURCE_URL | jdbc:postgresql://postgres-db:5432/cognigate | PostgreSQL JDBC URL |
SPRING_DATASOURCE_USERNAME | cognigate_user | DB username |
SPRING_DATASOURCE_PASSWORD | cognigate_pass | DB password |
POSTGRES_DB | cognigate | Database name |
The gateway itself is configured from cognigate.config.yml,
which docker-compose.yml bind-mounts read-only into the container: edit it and
recreate the gateway, and the new values are live. Recreating the gateway also
empties the in-memory control plane, so re-provision afterwards.
The settings below can additionally be set from the environment. The
environment always wins over the file, and the file over the defaults. Each is
also read without the CG_ prefix, for platforms that inject a bare PORT.
| Variable | Sets |
|---|---|
CG_PORT | gateway.port |
CG_ADMIN_BOOTSTRAP_KEY | admin.bootstrap_key — what docker-compose.yml passes |
CG_ANALYTICS_URL | analytics.base_url |
CG_ANALYTICS_TOKEN | analytics.token |
CG_METRICS_TOKEN | metrics.token |
CG_LOG_LEVEL | log.level |
CG_QUOTA_ENFORCEMENT | quotas.enforcement (on or observe) |
CG_CACHE_ENABLED | cache.enabled |
Documentation
Full documentation is available at https://cognigate.vkrishna04.me.
| Section | Description |
|---|---|
| Getting Started | Installation, first run, quick test |
| Architecture | System design, data flows, component interactions |
| API Reference | All endpoints, request/response schemas |
| Routing | Aliases, fallback chains, breakers, and how a model name is resolved |
| Integrating an App | Putting CogniGate in front of software that already calls an LLM API |
| AI Agents | The agent skill, and what it will and will not do on your machine |
| Security | Credential handling, the two planes, tenant isolation |
| Usage & Cost | What is metered, how to read it back, where the cost figure comes from |
| Deployment | Production hardening, TLS, scaling, and what it still owes you |
Let an Agent Do It
CogniGate ships an agent skill — one markdown file that teaches a coding agent to install the gateway, provision a tenant, register providers, wire an existing application into it, and diagnose it when it breaks. Install it once and it is there in every future session.
In Claude Code, this repository is a plugin marketplace:
/plugin marketplace add Life-Experimentalist/CogniGate
/plugin install cognigate@cognigate
Anywhere else, it is one file over HTTPS:
mkdir -p ~/.claude/skills/cognigate && curl -fsSL https://cognigate.vkrishna04.me/skill/SKILL.md -o ~/.claude/skills/cognigate/SKILL.md
Then say "set up CogniGate and point my app at it". For other agents, the file is plain markdown — drop it wherever your tool loads instructions from.
Read it first, it is short:
skill/SKILL.md.
It refuses to print secrets, refuses to wipe the database to fix a container,
and refuses to report success from a script's own output. Details and the
verification commands are in AI Agents.
There is also a shorter one-URL bootstrap at
prompt.md, and
a whole-repository briefing in
COGNIGATE_AI_CONTEXT.md
for questions about the code itself.
Integrating an Existing Application
If it already speaks the OpenAI API, the integration is two settings — a base URL and a key. No SDK, no adapter, no change to any request or response:
client = OpenAI(
base_url=os.environ["COGNIGATE_URL"] + "/v1",
api_key=os.environ["COGNIGATE_API_KEY"], # the cg- secret
)
Then delete the provider credential from the application, and rotate it at the provider if it was ever committed. The full procedure — finding the call sites, one key per service per environment, moving model names behind aliases, and how to prove the traffic is actually going through the gateway — is in Integrating an Application.
Adding a Provider
CogniGate ships three provider kinds, and registering any of them is a single admin call that needs no code.
openai covers every service that reimplements the OpenAI wire format:
Together, Groq, Fireworks, Azure OpenAI, OpenRouter, vLLM, Ollama, LM Studio.
Only the base URL differs.
curl -s -X POST http://localhost:8080/admin/v1/tenants/ten_.../providers \
-H "Authorization: Bearer $BOOTSTRAP" \
-H "Content-Type: application/json" \
-d '{"name":"local","kind":"openai","base_url":"http://localhost:11434/v1","keys":["unused"]}'
gemini and anthropic are that same adapter pointed at each vendor's own
OpenAI-compatible endpoint. They carry its base URL as a default, so base_url
is optional for them, and they report the vendor's name rather than "openai" in
logs, metrics and X-CogniGate-Served-By.
curl -s -X POST http://localhost:8080/admin/v1/tenants/ten_.../providers \
-H "Authorization: Bearer $BOOTSTRAP" \
-H "Content-Type: application/json" \
-d '{"name":"gemini","kind":"gemini","keys":["...","..."]}'
keys is a pool, and key_strategy decides which key a request starts from.
round_robin, the default, advances one key per request, so several keys from
one vendor share a steady load instead of piling onto the first until it
throttles. failover pins every request to the first key and keeps the rest
for when it starts refusing, which is what a paid key backed by a free one
wants. Under either strategy a 429 walks the whole pool before the fallback
chain moves on.
A provider that speaks a protocol none of the three cover needs a translating
proxy in front of it. There is no runtime plugin mechanism, and adding a native
adapter is a code change in gateway/internal/provider/.
Tech Stack
| Component | Technology |
|---|---|
| Edge Gateway | Go 1.26, Fiber v2 |
| Analytics Engine | Java 25 LTS, Spring Boot 4.1, Hibernate, virtual threads (Project Loom) |
| Database | PostgreSQL 16 |
| Container Runtime | Docker, Docker Compose |
| Docs Site | Next.js 16, Tailwind CSS 4, Three.js / React Three Fiber |
Brand
The mark, the palette and the exact image-model prompts used to generate them
live in docs/public/brand/logo-prompt.md
— four prompts (icon, favicon, wordmark lockup, social banner), the seven hex
values nothing may drift off, and the checks a generated logo has to survive
before it ships. docs/public/logo.png and docs/public/banner.png are served
at /logo.png and /banner.png, so replacing either is a file swap.
Contributing
We welcome contributions! Please read our Contributing Guide and follow our Code of Conduct.
For security vulnerabilities, please see our Security Policy — report privately, never via public issues.
License
Copyright 2026 VKrishna04 and Life Experimentalist
Licensed under the Apache License, Version 2.0. See LICENSE for the full text.